From 31ed48885f6f61fe3436754a019669962da57de1 Mon Sep 17 00:00:00 2001 From: scab24 Date: Sat, 16 May 2026 21:20:24 +0200 Subject: [PATCH 01/10] chore(cli): drop solidity subcommands and api routes Removes the analyze and context CLI subcommands, the /api/contract REST handlers, the Solidity Anchor fixtures and the Solidity guide chapters. Serve and Explore now reject any path detected as Solidity with a clear error pointing at the standalone ilold-evm repo. --- crates/ilold-cli/src/analyze.rs | 251 -------- crates/ilold-cli/src/context.rs | 293 ---------- crates/ilold-cli/src/main.rs | 113 +--- crates/ilold-web/src/api/contract.rs | 544 ------------------ crates/ilold-web/src/api/mod.rs | 1 - crates/ilold-web/src/lib.rs | 8 - docs/guide/src/SUMMARY.md | 16 - docs/guide/src/roadmap/solidity.md | 29 - docs/guide/src/solidity/cli-analyze.md | 74 --- docs/guide/src/solidity/cli-context.md | 67 --- docs/guide/src/solidity/limitations.md | 40 -- docs/guide/src/solidity/overview.md | 39 -- docs/guide/src/solidity/repl/analysis.md | 183 ------ docs/guide/src/solidity/repl/contract.md | 122 ---- docs/guide/src/solidity/repl/findings.md | 86 --- docs/guide/src/solidity/repl/scenarios.md | 81 --- docs/guide/src/solidity/repl/session.md | 139 ----- docs/guide/src/solidity/repl/workspace.md | 53 -- .../solidity/workflows/audit-walkthrough.md | 234 -------- .../src/solidity/workflows/taint-analysis.md | 103 ---- tests/fixtures/erc20.sol | 55 -- tests/fixtures/governor.sol | 140 ----- tests/fixtures/multi/.gitignore | 4 - tests/fixtures/multi/foundry.toml | 5 - tests/fixtures/multi/src/Helper.sol | 22 - tests/fixtures/multi/src/Main.sol | 24 - tests/fixtures/simple_storage.sol | 18 - tests/fixtures/staking.sol | 117 ---- tests/fixtures/uniswap_v2_pair.sol | 138 ----- 29 files changed, 18 insertions(+), 2981 deletions(-) delete mode 100644 crates/ilold-cli/src/analyze.rs delete mode 100644 crates/ilold-cli/src/context.rs delete mode 100644 crates/ilold-web/src/api/contract.rs delete mode 100644 docs/guide/src/roadmap/solidity.md delete mode 100644 docs/guide/src/solidity/cli-analyze.md delete mode 100644 docs/guide/src/solidity/cli-context.md delete mode 100644 docs/guide/src/solidity/limitations.md delete mode 100644 docs/guide/src/solidity/overview.md delete mode 100644 docs/guide/src/solidity/repl/analysis.md delete mode 100644 docs/guide/src/solidity/repl/contract.md delete mode 100644 docs/guide/src/solidity/repl/findings.md delete mode 100644 docs/guide/src/solidity/repl/scenarios.md delete mode 100644 docs/guide/src/solidity/repl/session.md delete mode 100644 docs/guide/src/solidity/repl/workspace.md delete mode 100644 docs/guide/src/solidity/workflows/audit-walkthrough.md delete mode 100644 docs/guide/src/solidity/workflows/taint-analysis.md delete mode 100644 tests/fixtures/erc20.sol delete mode 100644 tests/fixtures/governor.sol delete mode 100644 tests/fixtures/multi/.gitignore delete mode 100644 tests/fixtures/multi/foundry.toml delete mode 100644 tests/fixtures/multi/src/Helper.sol delete mode 100644 tests/fixtures/multi/src/Main.sol delete mode 100644 tests/fixtures/simple_storage.sol delete mode 100644 tests/fixtures/staking.sol delete mode 100644 tests/fixtures/uniswap_v2_pair.sol diff --git a/crates/ilold-cli/src/analyze.rs b/crates/ilold-cli/src/analyze.rs deleted file mode 100644 index 361b910..0000000 --- a/crates/ilold-cli/src/analyze.rs +++ /dev/null @@ -1,251 +0,0 @@ -use std::collections::HashMap; -use std::path::PathBuf; - -use anyhow::{Context, Result}; -use colored::Colorize; - -use ilold_core::callgraph::builder::build_call_graph; -use ilold_core::callgraph::types::CallKind; -use ilold_core::cfg::builder::CfgBuilder; -use ilold_core::classify::entry_points::classify_function; -use ilold_core::model::contract::{ContractDef, ContractKind}; -use ilold_core::model::function::Visibility; -use ilold_core::model::project::Project; -use ilold_core::parse::solar_frontend::SolarParser; -use ilold_core::parse::ProjectParser; -use ilold_core::pathtree::config::PruningConfig; -use ilold_core::pathtree::types::PathTree; -use ilold_core::pathtree::walker::build_path_tree; -use ilold_core::sequence::analysis::{analyze_project, analyze_sequences, SequenceAnalysis}; -use ilold_core::sequence::builder::build_sequence_tree; - -use crate::colors::*; - -pub fn run( - path: &PathBuf, - contract_filter: Option<&str>, - max_seq_depth: usize, - verbose: bool, -) -> Result<()> { - let paths = crate::collect_sol_files(path)?; - if paths.is_empty() { - anyhow::bail!("No .sol files found at {}", path.display()); - } - - let parser = SolarParser; - let mut project = parser - .parse(&paths) - .context(format!("Failed to parse {}", path.display()))?; - project.rebuild_index(); - - println!("Parsed {} file(s), {} contract(s)\n", - project.source_files.len(), project.contracts.len()); - - let config = PruningConfig::default(); - let mut all_analyses: HashMap = HashMap::new(); - for contract in &project.contracts { - let combined_state_vars = project.inherited_state_vars(contract); - let mut pt_map: HashMap<(String, String), PathTree> = HashMap::new(); - for func in &contract.functions { - if let Ok(cfg) = CfgBuilder::build_with_project(func, contract, Some(&project)) { - let pt = build_path_tree(&cfg, &contract.name, &func.name, &combined_state_vars, &config); - pt_map.insert((contract.name.clone(), func.name.clone()), pt); - } - } - let analysis = analyze_sequences(&pt_map, &contract.name); - all_analyses.insert(contract.name.clone(), analysis); - } - analyze_project(&project, &mut all_analyses); - - for contract in &project.contracts { - if let Some(filter) = contract_filter { - if contract.name != filter { continue; } - } - print_contract(&project, contract, max_seq_depth, verbose, &all_analyses); - } - - Ok(()) -} - -fn print_contract( - project: &Project, - contract: &ContractDef, - max_seq_depth: usize, - verbose: bool, - all_analyses: &HashMap, -) { - let kind_str = match contract.kind { - ContractKind::Contract => "contract", - ContractKind::Interface => "interface", - ContractKind::Library => "library", - ContractKind::Abstract => "abstract", - }; - - println!("{} {} ({} functions, {} state vars)", - kind_str.dimmed(), contract.name.bold(), contract.functions.len(), contract.state_vars.len()); - - if !contract.inherits.is_empty() { - println!(" {} {}", "inherits:".dimmed(), contract.inherits.join(", ")); - } - - let config = PruningConfig::default(); - let mut path_trees: Vec = Vec::new(); - let combined_state_vars = project.inherited_state_vars(contract); - - for func in &contract.functions { - let display_name = if func.name.is_empty() { - format!("{:?}", func.kind).to_lowercase() - } else { - func.name.clone() - }; - - let access = classify_function(func, contract); - let vis = match func.visibility { - Visibility::Public => "public", - Visibility::External => "external", - Visibility::Internal => "internal", - Visibility::Private => "private", - }; - - match CfgBuilder::build(func, contract) { - Ok(cfg) => { - let pt = build_path_tree( - &cfg, &contract.name, &func.name, &combined_state_vars, &config, - ); - - println!(" {} {} {} — {} blocks, {} edges, {} paths ({} happy, {} revert)", - access_colored(&access), c_muted(vis), c_bright(&display_name), - cfg.node_count(), cfg.edge_count(), - pt.stats.total_paths, - c_ok(&pt.stats.happy_paths.to_string()), - c_danger(&pt.stats.revert_paths.to_string())); - - if verbose { - for node in cfg.node_indices() { - let block = &cfg[node]; - println!(" {} {:?} {}", - c_muted(&format!("[{}]", block.id)), - block.kind, - c_muted(&format!("({} stmts)", block.statements.len()))); - } - for edge in cfg.edge_indices() { - let (src, dst) = cfg.edge_endpoints(edge).unwrap(); - println!(" {} {} {} {}", - cfg[src].id, c_muted("→"), cfg[dst].id, c_muted(&format!("{:?}", cfg[edge]))); - } - println!(); - } - - path_trees.push(pt); - } - Err(e) => { - println!(" {} {} {} — {}", - access_colored(&access), c_muted(vis), display_name, c_danger(&format!("CFG error: {e}"))); - } - } - } - - if verbose { - let cg = build_call_graph(project, contract); - let edges: Vec<_> = cg.edge_indices().collect(); - if !edges.is_empty() { - println!(" {}", c_muted("Call graph:")); - for edge_idx in edges { - let (src, dst) = cg.edge_endpoints(edge_idx).unwrap(); - let edge = &cg[edge_idx]; - let kind_color = match edge.kind { - CallKind::Internal => c_muted("internal"), - CallKind::External => c_danger("external"), - CallKind::Inherited => c_muted("inherited"), - }; - println!(" {} {} {} {}", - c_bright(&cg[src].function), c_muted("→"), - c_accent(&format!("{}.{}", cg[dst].contract, cg[dst].function)), - kind_color); - } - println!(); - } - } - - if contract.kind != ContractKind::Interface { - let st = build_sequence_tree(contract, &path_trees, max_seq_depth); - if !st.functions.is_empty() { - let state_changing = st.functions.iter().filter(|f| !f.read_only).count(); - let read_only = st.functions.iter().filter(|f| f.read_only).count(); - - println!(" {} {} total ({} functions: {} state-changing, {} read-only)", - c_muted(&format!("Sequences (depth {}):", max_seq_depth)), - st.sequences.len(), - st.functions.len(), state_changing, read_only); - - if verbose { - for d in 1..=max_seq_depth { - let count = st.sequences.iter().filter(|s| s.depth == d).count(); - println!(" {} {} sequences", c_muted(&format!("depth {}:", d)), count); - } - println!(); - } - } - - let default_analysis; - let analysis = match all_analyses.get(&contract.name) { - Some(a) => a, - None => { - default_analysis = analyze_sequences(&HashMap::new(), &contract.name); - &default_analysis - } - }; - - if verbose { - println!(" {}", c_muted("Function behaviors:")); - let func_count = analysis.functions.len(); - for (i, func) in analysis.functions.iter().enumerate() { - let is_last_func = i == func_count - 1; - let branch = if is_last_func { "└── " } else { "├── " }; - let pipe = if is_last_func { " " } else { "│ " }; - - let view_tag = if func.read_only { c_muted(" (view)").to_string() } else { String::new() }; - println!(" {}{}{}", c_muted(branch), c_bright(&func.name), view_tag); - - if !func.preconditions.is_empty() { - println!(" {}├── {} {}", c_muted(pipe), c_muted("requires:"), c_warn(&func.preconditions.join(", "))); - } - if !func.state_writes.is_empty() { - println!(" {}├── {} {}", c_muted(pipe), c_muted("writes:"), c_accent(&func.state_writes.join(", "))); - } - if !func.external_calls.is_empty() { - println!(" {}├── {} {}", c_muted(pipe), c_muted("calls:"), c_danger(&func.external_calls.join(", "))); - } - if !func.events.is_empty() { - println!(" {}├── {} {}", c_muted(pipe), c_muted("emits:"), func.events.join(", ")); - } - - let outgoing: Vec<_> = analysis.transitions.iter() - .filter(|t| t.from == func.name && (!t.shared_state.is_empty() || !t.conditions_affected.is_empty())) - .collect(); - - if !outgoing.is_empty() { - println!(" {}└── {}", c_muted(pipe), c_muted("transitions:")); - let out_count = outgoing.len(); - for (j, t) in outgoing.iter().enumerate() { - let is_last_t = j == out_count - 1; - let t_branch = if is_last_t { "└── " } else { "├── " }; - let t_pipe = if is_last_t { " " } else { "│ " }; - - println!(" {} {}→ {}", c_muted(pipe), c_muted(t_branch), c_bright(&t.to)); - - if !t.shared_state.is_empty() { - println!(" {} {}{} {}", c_muted(pipe), c_muted(t_pipe), c_muted("shared:"), c_accent(&t.shared_state.join(", "))); - } - for cond in &t.conditions_affected { - println!(" {} {}{}", c_muted(pipe), c_muted(t_pipe), c_warn(cond)); - } - } - } - } - println!(); - } - } - - println!(); -} diff --git a/crates/ilold-cli/src/context.rs b/crates/ilold-cli/src/context.rs deleted file mode 100644 index d7b4a03..0000000 --- a/crates/ilold-cli/src/context.rs +++ /dev/null @@ -1,293 +0,0 @@ -use std::collections::HashMap; -use std::path::PathBuf; - -use anyhow::{Context, Result}; - -use ilold_core::cfg::builder::CfgBuilder; -use ilold_core::cfg::types::CfgGraph; -use ilold_core::classify::entry_points::{classify_all, AccessLevel}; -use ilold_core::model::contract::ContractDef; -use ilold_core::model::project::Project; -use ilold_core::narrative::function::build_function_narrative; -use ilold_core::narrative::sequence::build_sequence_narrative; -use ilold_core::narrative::types::*; -use ilold_core::parse::solar_frontend::SolarParser; -use ilold_core::parse::ProjectParser; -use ilold_core::pathtree::config::PruningConfig; -use ilold_core::pathtree::types::{PathTree, TerminalKind}; -use ilold_core::pathtree::walker::build_path_tree; -use ilold_core::sequence::analysis::{analyze_project, analyze_sequences}; - -use crate::colors::*; - -pub fn run( - path: &PathBuf, - contract_filter: Option<&str>, - function_filter: Option<&str>, - sequence_filter: Option<&str>, - list: bool, -) -> Result<()> { - let paths = crate::collect_sol_files(path)?; - if paths.is_empty() { anyhow::bail!("No .sol files found at {}", path.display()); } - - let parser = SolarParser; - let mut project = parser.parse(&paths).context("Failed to parse")?; - project.rebuild_index(); - - let contract = find_contract(&project, contract_filter)?; - let classifications = classify_all(contract); - - if list { - print_list(contract, &classifications); - return Ok(()); - } - - let config = PruningConfig::default(); - let mut cfgs: HashMap<(String, String), CfgGraph> = HashMap::new(); - let mut pt_map: HashMap<(String, String), PathTree> = HashMap::new(); - let combined_state_vars = project.inherited_state_vars(contract); - for func in &contract.functions { - if let Ok(cfg) = CfgBuilder::build_with_project(func, contract, Some(&project)) { - let pt = build_path_tree(&cfg, &contract.name, &func.name, &combined_state_vars, &config); - let key = (contract.name.clone(), func.name.clone()); - cfgs.insert(key.clone(), cfg); - pt_map.insert(key, pt); - } - } - let mut all_sequence_analyses: HashMap = HashMap::new(); - for c in &project.contracts { - let combined = project.inherited_state_vars(c); - let mut c_pt_map: HashMap<(String, String), PathTree> = HashMap::new(); - for func in &c.functions { - if let Ok(cfg) = CfgBuilder::build_with_project(func, c, Some(&project)) { - let pt = build_path_tree(&cfg, &c.name, &func.name, &combined, &config); - c_pt_map.insert((c.name.clone(), func.name.clone()), pt); - } - } - let a = analyze_sequences(&c_pt_map, &c.name); - all_sequence_analyses.insert(c.name.clone(), a); - } - analyze_project(&project, &mut all_sequence_analyses); - let analysis = all_sequence_analyses - .get(&contract.name) - .cloned() - .unwrap_or_else(|| analyze_sequences(&pt_map, &contract.name)); - - if let Some(seq_str) = sequence_filter { - let names: Vec<&str> = seq_str.split(',').map(|s| s.trim()).collect(); - let narrative = build_sequence_narrative( - &contract.name, &names, &analysis.functions, &analysis.transitions, &classifications, - ); - print_sequence(&narrative); - return Ok(()); - } - - if let Some(func_name) = function_filter { - let func = contract.functions.iter().find(|f| f.name == func_name) - .ok_or_else(|| anyhow::anyhow!("Function '{}' not found. Use --list to see available functions.", func_name))?; - let key = (contract.name.clone(), func_name.to_string()); - let cfg = cfgs.get(&key).ok_or_else(|| anyhow::anyhow!("No CFG for {}", func_name))?; - let pt = pt_map.get(&key).ok_or_else(|| anyhow::anyhow!("No paths for {}", func_name))?; - let narrative = build_function_narrative(contract, func, pt, cfg, &analysis.functions, &project, &all_sequence_analyses); - print_function(&narrative); - return Ok(()); - } - - let narratives: Vec<_> = contract.functions.iter().filter_map(|func| { - let key = (contract.name.clone(), func.name.clone()); - let cfg = cfgs.get(&key)?; - let pt = pt_map.get(&key)?; - Some(build_function_narrative(contract, func, pt, cfg, &analysis.functions, &project, &all_sequence_analyses)) - }).collect(); - print_overview(contract, &narratives); - Ok(()) -} - -fn find_contract<'a>(project: &'a Project, filter: Option<&str>) -> Result<&'a ContractDef> { - project.find_contract(filter).map_err(|e| anyhow::anyhow!(e)) -} - -fn print_list(contract: &ContractDef, classifications: &[(String, AccessLevel)]) { - println!("\n {} — {} functions\n", - c_bright(&contract.name), contract.functions.len()); - - for (name, access) in classifications { - if name.is_empty() { continue; } - let vis = contract.functions.iter().find(|f| f.name == *name) - .map(|f| format!("{:?}", f.visibility).to_lowercase()).unwrap_or_default(); - println!(" {} {:<20} {}", access_colored(access), c_bright(name), c_muted(&vis)); - } - - println!("\n {}", c_muted("Usage:")); - println!(" ilold context --function "); - println!(" ilold context --sequence \"fn1,fn2\""); - - let names: Vec<&str> = classifications.iter() - .filter(|(n, _)| !n.is_empty()).map(|(n, _)| n.as_str()).take(2).collect(); - if names.len() >= 2 { - println!("\n {}", c_muted("Example:")); - println!(" ilold context --function {}", names[0]); - println!(" ilold context --sequence \"{},{}\"", names[0], names[1]); - } - println!(); -} - -fn print_function(n: &FunctionNarrative) { - println!("\n {} {}\n", c_bright(&n.name), c_muted(&format!("({})", n.access))); - - if !n.modifiers.is_empty() { - println!(" {} {}", c_muted("Modifiers:"), n.modifiers.join(", ")); - } - println!(" {} {} ({} return, {} revert)", - c_muted("Paths:"), n.total_paths, c_ok(&n.happy_paths.to_string()), c_danger(&n.revert_paths.to_string())); - if !n.state_writes.is_empty() { - println!(" {} {}", c_muted("Writes:"), c_warn(&n.state_writes.join(", "))); - } - if !n.external_calls.is_empty() { - println!(" {} {}", c_muted("Calls:"), c_danger(&n.external_calls.join(", "))); - } - - for path in &n.paths { - println!(); - let terminal = match path.terminal { - TerminalKind::Return => c_ok("Return"), - TerminalKind::Revert => c_danger("Revert"), - TerminalKind::DepthCutoff => c_muted("DepthCutoff"), - TerminalKind::LoopCutoff => c_muted("LoopCutoff"), - }; - println!(" {} #{} → {}", c_muted("Path"), path.id, terminal); - - for (i, step) in path.steps.iter().enumerate() { - let is_last = i == path.steps.len() - 1; - let connector = if is_last { " └── " } else { " ├── " }; - - let branch_str = match step.branch { - Some(BranchDirection::True) => format!(" {}", c_ok("✓")), - Some(BranchDirection::False) => format!(" {}", c_danger("✗")), - None => String::new(), - }; - - let desc = match step.step_type { - StepType::Entry => c_bright(&step.description).to_string(), - StepType::Condition => c_warn(&step.description).to_string(), - StepType::ExternalCall => c_danger(&step.description).to_string(), - StepType::InternalCall => c_muted(&step.description).to_string(), - StepType::StateWrite => c_accent(&step.description).to_string(), - StepType::StateRead => c_muted(&step.description).to_string(), - StepType::EthTransfer => c_danger(&step.description).to_string(), - StepType::Event => c_muted(&step.description).to_string(), - StepType::Return => c_ok(&step.description).to_string(), - StepType::Revert => c_danger(&step.description).to_string(), - StepType::Assembly => c_muted(&step.description).to_string(), - }; - - println!("{}{} {}{}", - c_muted(connector), step.step_type.icon(), desc, branch_str); - } - } - - if !n.observations.is_empty() { - println!("\n {}", c_muted("Observations:")); - for obs in &n.observations { - println!(" {} {}: {}", - c_warn("⚠"), c_muted(&obs.kind.to_string()), obs.description); - } - } - println!(); -} - -fn print_sequence(n: &SequenceNarrative) { - let names: Vec<&str> = n.steps.iter().map(|s| s.function.as_str()).collect(); - println!("\n {} {}\n", - c_muted("Sequence:"), c_bright(&names.join(" → "))); - - for (i, step) in n.steps.iter().enumerate() { - let is_last_step = i == n.steps.len() - 1; - let step_conn = if is_last_step { "└─" } else { "├─" }; - let pipe = if is_last_step { " " } else { "│ " }; - - println!(" {} {} {}", - c_muted(step_conn), - c_bright(&format!("Step {}: {}", i + 1, step.function)), - c_muted(&format!("({})", step.access))); - - let mut lines: Vec<(String, String)> = Vec::new(); - - for req in &step.requires { - lines.push(("require".into(), c_warn(req).to_string())); - } - for eff in &step.effects { - lines.push(("writes".into(), c_accent(eff).to_string())); - } - for call in &step.external_calls { - lines.push(("calls".into(), c_danger(call).to_string())); - } - for ev in &step.events { - lines.push(("emits".into(), c_muted(ev).to_string())); - } - for dep in &step.dependencies { - lines.push(("depends".into(), c_warn(&dep.relationship).to_string())); - } - - if lines.is_empty() { - println!(" {} {} {}", c_muted(pipe), c_muted("·"), c_muted("read-only")); - } else { - for (j, (label, value)) in lines.iter().enumerate() { - let is_last = j == lines.len() - 1; - let conn = if is_last { "└─" } else { "├─" }; - println!(" {} {} {} {}", - c_muted(pipe), c_muted(conn), c_muted(&format!("{:<7}", label)), value); - } - } - - if !is_last_step { - println!(" {}", c_muted("│")); - } - } - - if !n.observations.is_empty() { - println!("\n {}", c_muted("Observations:")); - for obs in &n.observations { - println!(" {} {}: {}", - c_warn("⚠"), c_muted(&obs.kind.to_string()), obs.description); - } - } - println!(); -} - -fn print_overview(contract: &ContractDef, narratives: &[FunctionNarrative]) { - let total_paths: usize = narratives.iter().map(|n| n.total_paths).sum(); - println!("\n {} — {} functions, {} paths\n", - c_bright(&contract.name), narratives.len(), total_paths); - - for n in narratives { - if n.name.is_empty() { continue; } - let access = access_colored(&n.access); - let paths = format!("{} paths", n.total_paths); - let mut extras = Vec::new(); - if !n.state_writes.is_empty() { - extras.push(format!("writes: {}", n.state_writes.join(", "))); - } - if !n.external_calls.is_empty() { - extras.push(format!("calls: {}", n.external_calls.join(", "))); - } - let extra_str = if extras.is_empty() { String::new() } - else { format!(" {}", c_muted(&extras.join(" | "))) }; - - println!(" {} {:<20} {}{}", - access, c_bright(&n.name), c_muted(&paths), extra_str); - } - - let obs: Vec<&Observation> = narratives.iter().flat_map(|n| n.observations.iter()).collect(); - if !obs.is_empty() { - println!("\n {}", c_muted("Observations:")); - let mut seen = std::collections::HashSet::new(); - for o in &obs { - if seen.insert(&o.description) { - println!(" {} {}: {}", - c_warn("⚠"), c_muted(&o.kind.to_string()), o.description); - } - } - } - println!(); -} diff --git a/crates/ilold-cli/src/main.rs b/crates/ilold-cli/src/main.rs index 987e361..ac2aa16 100644 --- a/crates/ilold-cli/src/main.rs +++ b/crates/ilold-cli/src/main.rs @@ -4,16 +4,14 @@ use anyhow::Result; use clap::Parser; use ilold_solana_core::ingest::{detect, ProjectKind}; -mod analyze; mod colors; -mod context; mod explore; mod fmt; mod help; mod interactive; #[derive(Parser)] -#[command(name = "ilold", version, about = "Smart contract execution path analyzer")] +#[command(name = "ilold", version, about = "Solana program execution path analyzer")] struct Cli { #[command(subcommand)] command: Commands, @@ -21,43 +19,17 @@ struct Cli { #[derive(clap::Subcommand)] enum Commands { - /// Analyze Solidity contracts - Analyze { - path: PathBuf, - #[arg(long)] - contract: Option, - #[arg(long, default_value = "3")] - max_seq_depth: usize, - #[arg(long)] - verbose: bool, - }, - /// Generate context for a function or sequence - Context { - path: PathBuf, - #[arg(long)] - contract: Option, - #[arg(long)] - function: Option, - #[arg(long)] - sequence: Option, - #[arg(long)] - list: bool, - }, /// Start interactive web viewer Serve { path: PathBuf, #[arg(long, default_value = "8080")] port: u16, - #[arg(long, default_value = "3")] - max_seq_depth: usize, }, /// Interactive exploration REPL with web canvas Explore { path: PathBuf, #[arg(long, default_value = "0")] port: u16, - #[arg(long, default_value = "3")] - max_seq_depth: usize, /// Attach to a running server instead of starting one locally #[arg(long)] attach: Option, @@ -76,83 +48,34 @@ enum Commands { }, } +fn ensure_solana(path: &PathBuf) -> Result { + let detected = detect(path)?; + match detected.kind { + ProjectKind::Solana => Ok(detected), + ProjectKind::Solidity => anyhow::bail!( + "Only Solana (Anchor) projects are supported in ilold. Solidity support lives in the standalone ilold-evm repo." + ), + } +} + #[tokio::main] async fn main() -> Result<()> { let cli = Cli::parse(); match cli.command { - Commands::Analyze { path, contract, max_seq_depth, verbose } => { - analyze::run(&path, contract.as_deref(), max_seq_depth, verbose) - } - Commands::Context { path, contract, function, sequence, list } => { - context::run(&path, contract.as_deref(), function.as_deref(), sequence.as_deref(), list) - } - Commands::Serve { path, port, max_seq_depth } => { - let detected = detect(&path)?; - match detected.kind { - ProjectKind::Solidity => serve_solidity(&path, port, max_seq_depth).await, - ProjectKind::Solana => ilold_web::serve_solana(detected, port).await, - } + Commands::Serve { path, port } => { + let detected = ensure_solana(&path)?; + ilold_web::serve_solana(detected, port).await } - Commands::Explore { path, port, max_seq_depth, attach } => { + Commands::Explore { path, port, attach } => { if attach.is_some() { - return explore::run(Vec::new(), port, max_seq_depth, attach).await; - } - let detected = detect(&path)?; - match detected.kind { - ProjectKind::Solidity => explore_solidity(&path, port, max_seq_depth, attach).await, - ProjectKind::Solana => explore::run_solana(detected, port).await, + return explore::run(Vec::new(), port, 0, attach).await; } + let detected = ensure_solana(&path)?; + explore::run_solana(detected, port).await } Commands::Mcp { server_url, contract, narration } => { ilold_mcp::run(ilold_mcp::Config { server_url, contract, narration }).await } } } - -async fn serve_solidity(path: &PathBuf, port: u16, max_seq_depth: usize) -> Result<()> { - let paths = collect_sol_files(path)?; - if paths.is_empty() { - anyhow::bail!("No .sol files found at {}", path.display()); - } - ilold_web::serve(paths, port, max_seq_depth).await -} - -async fn explore_solidity(path: &PathBuf, port: u16, max_seq_depth: usize, attach: Option) -> Result<()> { - let paths = collect_sol_files(path)?; - if paths.is_empty() { - anyhow::bail!("No .sol files found at {}", path.display()); - } - explore::run(paths, port, max_seq_depth, attach).await -} - -pub fn collect_sol_files(path: &PathBuf) -> Result> { - if path.is_file() { - return Ok(vec![path.clone()]); - } - if path.is_dir() { - let mut files = Vec::new(); - walk_sol_files(path, &mut files)?; - files.sort(); - return Ok(files); - } - anyhow::bail!("Path does not exist: {}", path.display()); -} - -fn walk_sol_files(dir: &PathBuf, out: &mut Vec) -> Result<()> { - const SKIP: &[&str] = &["out", "cache", "node_modules", "lib", "target", ".git", ".svelte-kit"]; - for entry in std::fs::read_dir(dir)? { - let entry = entry?; - let p = entry.path(); - if p.is_dir() { - let name = p.file_name().and_then(|n| n.to_str()).unwrap_or(""); - if name.starts_with('.') || SKIP.contains(&name) { - continue; - } - walk_sol_files(&p, out)?; - } else if p.extension().is_some_and(|ext| ext == "sol") { - out.push(p); - } - } - Ok(()) -} diff --git a/crates/ilold-web/src/api/contract.rs b/crates/ilold-web/src/api/contract.rs deleted file mode 100644 index 0aa2f2d..0000000 --- a/crates/ilold-web/src/api/contract.rs +++ /dev/null @@ -1,544 +0,0 @@ -use std::sync::Arc; - -use axum::extract::{Path, Query, State}; -use axum::http::StatusCode; -use axum::Json; -use serde::{Deserialize, Serialize}; - -use ilold_core::model::common::SourceSpan; -use ilold_core::pathtree::types::PathTree; -use ilold_core::sequence::types::SequenceTree; - -use crate::state::{require_solidity, AppState}; - -#[derive(Serialize)] -pub struct ContractDetail { - pub name: String, - pub kind: String, - pub inherits: Vec, - pub functions: Vec, - pub state_vars: Vec, - pub inherited_functions: Vec, - pub inherited_state_vars: Vec, -} - -#[derive(Serialize)] -pub struct FunctionSummary { - pub name: String, - pub kind: String, - pub visibility: String, - pub mutability: String, - pub params: Vec, - pub path_count: usize, - pub happy_paths: usize, - pub revert_paths: usize, - pub modifiers: Vec, -} - -#[derive(Serialize)] -pub struct ParamSummary { - pub name: String, - pub type_name: String, -} - -#[derive(Serialize)] -pub struct StateVarSummary { - pub name: String, - pub type_name: String, - pub visibility: String, - pub is_constant: bool, - pub is_immutable: bool, -} - -pub async fn get_contract( - State(state): State>, - Path(name): Path, -) -> Result, StatusCode> { - let s = require_solidity(&state)?; - let contract = s - .project - .contracts - .iter() - .find(|c| c.name == name) - .ok_or(StatusCode::NOT_FOUND)?; - - let functions = contract - .functions - .iter() - .map(|f| { - let key = (contract.name.clone(), f.name.clone()); - let pt = s.path_trees.get(&key); - FunctionSummary { - name: f.name.clone(), - kind: format!("{:?}", f.kind), - visibility: format!("{:?}", f.visibility), - mutability: format!("{:?}", f.mutability), - params: f.params.iter().map(|p| ParamSummary { - name: p.name.clone(), - type_name: p.type_name.clone(), - }).collect(), - path_count: pt.map(|p| p.stats.total_paths).unwrap_or(0), - happy_paths: pt.map(|p| p.stats.happy_paths).unwrap_or(0), - revert_paths: pt.map(|p| p.stats.revert_paths).unwrap_or(0), - modifiers: f.modifiers.iter().map(|m| m.name.clone()).collect(), - } - }) - .collect(); - - let state_vars = contract - .state_vars - .iter() - .map(|sv| StateVarSummary { - name: sv.name.clone(), - type_name: sv.type_name.clone(), - visibility: format!("{:?}", sv.visibility), - is_constant: sv.is_constant, - is_immutable: sv.is_immutable, - }) - .collect(); - - let inherited_functions: Vec = s.project - .accessible_functions(contract) - .into_iter() - .filter(|af| af.is_inherited) - .map(|af| { - let f = af.function; - let key = (af.origin.clone(), f.name.clone()); - let pt = s.path_trees.get(&key); - FunctionSummary { - name: f.name.clone(), - kind: format!("{:?}", f.kind), - visibility: format!("{:?}", f.visibility), - mutability: format!("{:?}", f.mutability), - params: f.params.iter().map(|p| ParamSummary { - name: p.name.clone(), - type_name: p.type_name.clone(), - }).collect(), - path_count: pt.map(|p| p.stats.total_paths).unwrap_or(0), - happy_paths: pt.map(|p| p.stats.happy_paths).unwrap_or(0), - revert_paths: pt.map(|p| p.stats.revert_paths).unwrap_or(0), - modifiers: f.modifiers.iter().map(|m| m.name.clone()).collect(), - } - }) - .collect(); - - let own_var_names: std::collections::HashSet = contract.state_vars.iter() - .map(|sv| sv.name.clone()) - .collect(); - let inherited_state_vars: Vec = s.project - .inherited_state_vars(contract) - .into_iter() - .filter(|sv| !own_var_names.contains(&sv.name)) - .map(|sv| StateVarSummary { - name: sv.name.clone(), - type_name: sv.type_name.clone(), - visibility: format!("{:?}", sv.visibility), - is_constant: sv.is_constant, - is_immutable: sv.is_immutable, - }) - .collect(); - - Ok(Json(ContractDetail { - name: contract.name.clone(), - kind: format!("{:?}", contract.kind), - inherits: contract.inherits.clone(), - functions, - state_vars, - inherited_functions, - inherited_state_vars, - })) -} - -#[derive(Serialize)] -pub struct CytoscapeGraph { - pub nodes: Vec, - pub edges: Vec, -} - -#[derive(Serialize)] -pub struct CytoscapeNode { - pub data: CytoscapeNodeData, -} - -#[derive(Serialize)] -pub struct CytoscapeNodeData { - pub id: String, - pub label: String, - pub node_type: String, - pub contract: String, - pub is_external: bool, - #[serde(skip_serializing_if = "Vec::is_empty")] - pub statements: Vec, -} - -#[derive(Serialize)] -pub struct CytoscapeEdge { - pub data: CytoscapeEdgeData, -} - -#[derive(Serialize)] -pub struct CytoscapeEdgeData { - pub id: String, - pub source: String, - pub target: String, - pub kind: String, - pub call_count: usize, -} - -pub async fn get_callgraph( - State(state): State>, - Path(name): Path, -) -> Result, StatusCode> { - let s = require_solidity(&state)?; - let cg = s - .call_graphs - .get(&name) - .ok_or(StatusCode::NOT_FOUND)?; - - let nodes: Vec = cg - .node_indices() - .map(|idx| { - let node = &cg[idx]; - CytoscapeNode { - data: CytoscapeNodeData { - id: format!("{}::{}", node.contract, node.function), - label: node.function.clone(), - node_type: if node.is_external { "external" } else { "internal" }.into(), - contract: node.contract.clone(), - is_external: node.is_external, - statements: Vec::new(), - }, - } - }) - .collect(); - - let edges: Vec = cg - .edge_indices() - .enumerate() - .map(|(i, edge_idx)| { - let (src, dst) = cg.edge_endpoints(edge_idx).unwrap(); - let edge = &cg[edge_idx]; - CytoscapeEdge { - data: CytoscapeEdgeData { - id: format!("e{i}"), - source: format!("{}::{}", cg[src].contract, cg[src].function), - target: format!("{}::{}", cg[dst].contract, cg[dst].function), - kind: format!("{:?}", edge.kind), - call_count: edge.call_count, - }, - } - }) - .collect(); - - Ok(Json(CytoscapeGraph { nodes, edges })) -} - -pub async fn get_cfg( - State(state): State>, - Path((contract_name, func_name)): Path<(String, String)>, -) -> Result, StatusCode> { - let s = require_solidity(&state)?; - let contract = s.project.contracts.iter() - .find(|c| c.name == contract_name) - .ok_or(StatusCode::NOT_FOUND)?; - - let (owning, _func) = s.project.resolve_function(contract, &func_name) - .ok_or(StatusCode::NOT_FOUND)?; - - let key = (owning.name.clone(), func_name); - let cfg = s.cfgs.get(&key).ok_or(StatusCode::NOT_FOUND)?; - - let nodes: Vec = cfg - .node_indices() - .map(|idx| { - let block = &cfg[idx]; - let stmts: Vec = block.statements.iter().map(|s| summarize_stmt(s)).collect(); - let label = make_block_label(block, &stmts); - CytoscapeNode { - data: CytoscapeNodeData { - id: format!("b{}", block.id), - label, - node_type: format!("{:?}", block.kind), - contract: key.0.clone(), - is_external: false, - statements: stmts, - }, - } - }) - .collect(); - - let edges: Vec = cfg - .edge_indices() - .enumerate() - .map(|(i, edge_idx)| { - let (src, dst) = cfg.edge_endpoints(edge_idx).unwrap(); - let edge = &cfg[edge_idx]; - CytoscapeEdge { - data: CytoscapeEdgeData { - id: format!("e{i}"), - source: format!("b{}", cfg[src].id), - target: format!("b{}", cfg[dst].id), - kind: format!("{:?}", edge), - call_count: 0, - }, - } - }) - .collect(); - - Ok(Json(CytoscapeGraph { nodes, edges })) -} - -#[derive(Serialize)] -pub struct FunctionSourceResponse { - pub file_path: String, - pub source: String, - pub span: SourceSpan, -} - -pub async fn get_function_source( - State(state): State>, - Path((contract_name, func_name)): Path<(String, String)>, -) -> Result, StatusCode> { - let s = require_solidity(&state)?; - let contract = s.project.contracts.iter() - .find(|c| c.name == contract_name) - .ok_or(StatusCode::NOT_FOUND)?; - - let (_owning, func) = s.project.resolve_function(contract, &func_name) - .ok_or(StatusCode::NOT_FOUND)?; - - let file = s.project.source_files.get(func.span.file_index) - .ok_or(StatusCode::NOT_FOUND)?; - - let source = slice_lines( - &file.content, - func.span.start_line as usize, - func.span.end_line as usize, - ); - - Ok(Json(FunctionSourceResponse { - file_path: file.path.clone(), - source, - span: func.span, - })) -} - -fn slice_lines(src: &str, start_1based: usize, end_1based: usize) -> String { - if start_1based == 0 || end_1based < start_1based { - return String::new(); - } - src.lines() - .skip(start_1based - 1) - .take(end_1based - start_1based + 1) - .collect::>() - .join("\n") -} - -pub async fn get_paths( - State(state): State>, - Path((contract_name, func_name)): Path<(String, String)>, -) -> Result, StatusCode> { - let s = require_solidity(&state)?; - let contract = s - .project - .contracts - .iter() - .find(|c| c.name == contract_name) - .ok_or(StatusCode::NOT_FOUND)?; - - let (owning, _func) = s.project.resolve_function(contract, &func_name) - .ok_or(StatusCode::NOT_FOUND)?; - - let key = (owning.name.clone(), func_name); - s.path_trees - .get(&key) - .cloned() - .map(Json) - .ok_or(StatusCode::NOT_FOUND) -} - -#[derive(Deserialize)] -pub struct SequenceQuery { - pub depth: Option, -} - -pub async fn get_sequences( - State(state): State>, - Path(name): Path, - Query(_query): Query, -) -> Result, StatusCode> { - let s = require_solidity(&state)?; - s.sequence_trees - .get(&name) - .cloned() - .map(Json) - .ok_or(StatusCode::NOT_FOUND) -} - -use ilold_core::sequence::analysis::{analyze_sequences, SequenceAnalysis}; - -pub async fn get_sequence_analysis( - State(state): State>, - Path(name): Path, -) -> Result, StatusCode> { - let s = require_solidity(&state)?; - let analysis = analyze_sequences(&s.path_trees, &name); - Ok(Json(analysis)) -} - -#[derive(Serialize)] -pub struct SearchSuggestions { - pub functions: Vec, - pub state_vars: Vec, - pub events: Vec, - pub external_calls: Vec, - pub categories: Vec, -} - -#[derive(Serialize)] -pub struct SuggestionCategory { - pub label: String, - pub items: Vec, -} - -pub async fn get_search_suggestions( - State(state): State>, - Path(name): Path, -) -> Result, StatusCode> { - let s = require_solidity(&state)?; - let contract = s - .project - .contracts - .iter() - .find(|c| c.name == name) - .ok_or(StatusCode::NOT_FOUND)?; - - let accessible = s.project.accessible_functions(contract); - let functions: Vec = accessible - .iter() - .filter(|af| !af.function.name.is_empty()) - .map(|af| af.function.name.clone()) - .collect(); - - let state_vars: Vec = s.project - .inherited_state_vars(contract) - .into_iter() - .map(|sv| sv.name) - .collect(); - - let events: Vec = contract - .events - .iter() - .map(|e| e.name.clone()) - .collect(); - - let mut origins: std::collections::HashSet = std::collections::HashSet::new(); - origins.insert(name.clone()); - for af in &accessible { - if af.is_inherited { - origins.insert(af.origin.clone()); - } - } - - let mut ext_calls = std::collections::HashSet::new(); - for ((c, _), pt) in &s.path_trees { - if !origins.contains(c) { continue; } - for path in &pt.paths { - for call in &path.annotations.external_calls { - ext_calls.insert(format!("{}.{}", call.target, call.function)); - } - } - } - let external_calls: Vec = ext_calls.into_iter().collect(); - - let categories = vec![ - SuggestionCategory { - label: "Functions".into(), - items: functions.clone(), - }, - SuggestionCategory { - label: "State Variables".into(), - items: state_vars.clone(), - }, - SuggestionCategory { - label: "Events".into(), - items: events.clone(), - }, - SuggestionCategory { - label: "External Calls".into(), - items: external_calls.clone(), - }, - SuggestionCategory { - label: "Path Types".into(), - items: vec!["revert".into(), "return".into(), "assembly".into()], - }, - ]; - - Ok(Json(SearchSuggestions { - functions, - state_vars, - events, - external_calls, - categories, - })) -} - -use ilold_core::cfg::types::{BasicBlock, BlockKind, CfgStatement}; - -fn summarize_stmt(stmt: &CfgStatement) -> String { - match stmt { - CfgStatement::RequireCheck { condition, .. } => format!("require({})", condition), - CfgStatement::AssertCheck { condition, .. } => format!("assert({})", condition), - CfgStatement::ExternalCall { target, function, .. } => format!("{}.{}()", target, function), - CfgStatement::InternalCall { function, .. } => format!("{}()", function), - CfgStatement::EmitEvent { event, .. } => format!("emit {}", event), - CfgStatement::StateWrite { variable, .. } => format!("{} = ...", variable), - CfgStatement::StateRead { variable, .. } => format!("read {}", variable), - CfgStatement::EthTransfer { to, .. } => format!("transfer → {}", to), - CfgStatement::Assignment { target, .. } => { - if target.is_empty() { - "...".into() - } else { - format!("{} = ...", target) - } - } - CfgStatement::AssemblyBlock { .. } => "assembly { ... }".into(), - } -} - -fn make_block_label(block: &BasicBlock, stmts: &[String]) -> String { - match block.kind { - BlockKind::Entry => { - if stmts.is_empty() { - "▶ Entry".into() - } else { - let s = truncate_label(&stmts[0], 24); - format!("▶ {s}") - } - } - BlockKind::Return => "✓ Return".into(), - BlockKind::Revert => "✗ Revert".into(), - BlockKind::Assembly => "⚙ Assembly".into(), - BlockKind::LoopCondition => "⟳ Loop".into(), - BlockKind::Normal => { - if stmts.is_empty() { - "·".into() - } else { - let first = truncate_label(&stmts[0], 24); - if stmts.len() == 1 { - first - } else { - format!("{first} (+{})", stmts.len() - 1) - } - } - } - } -} - -fn truncate_label(s: &str, max: usize) -> String { - if s.len() <= max { - s.to_string() - } else { - format!("{}…", &s[..max]) - } -} diff --git a/crates/ilold-web/src/api/mod.rs b/crates/ilold-web/src/api/mod.rs index 32e2345..dd3fb20 100644 --- a/crates/ilold-web/src/api/mod.rs +++ b/crates/ilold-web/src/api/mod.rs @@ -1,4 +1,3 @@ pub mod project; -pub mod contract; pub mod annotations; pub mod session; diff --git a/crates/ilold-web/src/lib.rs b/crates/ilold-web/src/lib.rs index ed23e9b..509e751 100644 --- a/crates/ilold-web/src/lib.rs +++ b/crates/ilold-web/src/lib.rs @@ -21,14 +21,6 @@ fn build_router(state: Arc) -> Router { .route("/api/program/{name}/overlay", get(api::project::get_program_overlay)) .route("/api/program/{name}/{ix}/source", get(api::project::get_instruction_source)) .route("/api/users/{scenario}/labels", get(api::project::get_user_labels)) - .route("/api/contract/{name}", get(api::contract::get_contract)) - .route("/api/contract/{name}/callgraph", get(api::contract::get_callgraph)) - .route("/api/contract/{name}/{func}/cfg", get(api::contract::get_cfg)) - .route("/api/contract/{name}/{func}/paths", get(api::contract::get_paths)) - .route("/api/contract/{name}/{func}/source", get(api::contract::get_function_source)) - .route("/api/contract/{name}/sequences", get(api::contract::get_sequences)) - .route("/api/contract/{name}/analysis", get(api::contract::get_sequence_analysis)) - .route("/api/contract/{name}/suggestions", get(api::contract::get_search_suggestions)) .route("/api/annotations", get(api::annotations::list_annotations)) .route("/api/annotations", post(api::annotations::create_annotation)) .route("/api/annotations/{id}", put(api::annotations::update_annotation)) diff --git a/docs/guide/src/SUMMARY.md b/docs/guide/src/SUMMARY.md index 0fc65bb..6668fb6 100644 --- a/docs/guide/src/SUMMARY.md +++ b/docs/guide/src/SUMMARY.md @@ -8,21 +8,6 @@ - [What ilold does](./concepts/overview.md) - [Architecture](./concepts/architecture.md) -# Solidity Backend - -- [Overview](./solidity/overview.md) -- [CLI: analyze](./solidity/cli-analyze.md) -- [CLI: context](./solidity/cli-context.md) -- [REPL: Session](./solidity/repl/session.md) -- [REPL: Analysis](./solidity/repl/analysis.md) -- [REPL: Contract](./solidity/repl/contract.md) -- [REPL: Findings](./solidity/repl/findings.md) -- [REPL: Scenarios](./solidity/repl/scenarios.md) -- [REPL: Workspace](./solidity/repl/workspace.md) -- [Workflow: Audit walkthrough](./solidity/workflows/audit-walkthrough.md) -- [Workflow: Taint analysis](./solidity/workflows/taint-analysis.md) -- [Limitations](./solidity/limitations.md) - # Solana Backend - [Overview](./solana/overview.md) @@ -47,6 +32,5 @@ # Roadmap -- [Solidity: future work](./roadmap/solidity.md) - [Solana: Phase 2](./roadmap/solana.md) - [Cross-cutting](./roadmap/cross-cutting.md) diff --git a/docs/guide/src/roadmap/solidity.md b/docs/guide/src/roadmap/solidity.md deleted file mode 100644 index ba80299..0000000 --- a/docs/guide/src/roadmap/solidity.md +++ /dev/null @@ -1,29 +0,0 @@ -# Solidity Roadmap - -Solidity covers the MVP scope. The items below are tracked enhancements; the section after them is open to ideas. - -## Slicer precision - -The slicer is intraprocedural and assignment-only. Mutations via `x++`, `delete x`, `arr.push(v)` show up as USEs but not DEFs; tuple destructuring may not surface; forward slices include lexical ancestors and can over-taint. - -## Cross-function dataflow - -The slicer stops at call boundaries. Following a value through a helper requires `tr ` (manual inlining) or a separate run on the helper. - -## Modifier placeholder split - -Modifier bodies are split at the first top-level `_;`. Nested placeholders fall back to "before" code. - -## Sequence depth bound - -`--max-seq-depth` defaults to 3. Deeper bounds grow combinatorially; no change planned. - -## Open to ideas - -The roadmap is not closed. Examples of integrations we have considered but not started: - -- **Foundry**: today ilold reads Foundry projects (the `multi/` and `recursive/` fixtures are Foundry layouts) but does not invoke `forge build` or `forge test`. Possible directions include using `forge build` artefacts as an alternative ingest path, replaying PoCs from findings via `forge test --debug`, or cross-linking traces. -- **Cross-tool reports**: emitting findings in a format consumable by other audit pipelines. -- **New analysis passes**: anything that fits the CFG + path-tree model. - -If you have a use case the current backend does not cover, open an issue or reach out. diff --git a/docs/guide/src/solidity/cli-analyze.md b/docs/guide/src/solidity/cli-analyze.md deleted file mode 100644 index 7d57742..0000000 --- a/docs/guide/src/solidity/cli-analyze.md +++ /dev/null @@ -1,74 +0,0 @@ -# CLI: `analyze` - -`ilold analyze` parses a Solidity project, runs the full static-analysis pipeline (parser → CFG → path tree → sequence analysis with cross-contract transitive effects), and prints a structured summary to stdout. It is the one-shot view of what `explore` keeps in memory. - -## Synopsis - -``` -ilold analyze [--contract ] [--max-seq-depth ] [--verbose] -``` - -| Flag | Default | Description | -| --- | --- | --- | -| `--contract ` | (all contracts) | Restrict output to a single contract | -| `--max-seq-depth ` | `3` | Depth bound for the sequence tree (call combinations up to N steps) | -| `--verbose` | off | Per-block CFG layout, call-graph edges, full function-behavior breakdown | - -`` may be a single `.sol` file or a directory; the walker skips `out`, `cache`, `node_modules`, `lib`, `target`, and dot-prefixed directories. - -## Example: default output - -``` -$ ilold analyze tests/fixtures/staking.sol -Parsed 1 file(s), 2 contract(s) - -interface IERC20 (3 functions, 0 state vars) - [P] external transfer — 1 blocks, 0 edges, 0 paths (0 happy, 0 revert) - [P] external transferFrom — 1 blocks, 0 edges, 0 paths (0 happy, 0 revert) - [P] external balanceOf — 1 blocks, 0 edges, 0 paths (0 happy, 0 revert) - -contract Staking (9 functions, 11 state vars) - [S] internal constructor — 2 blocks, 1 edges, 1 paths (1 happy, 0 revert) - [P] external deposit — 8 blocks, 8 edges, 5 paths (2 happy, 3 revert) - [P] external withdraw — 8 blocks, 8 edges, 6 paths (2 happy, 4 revert) - [P] external claimRewards — 6 blocks, 7 edges, 4 paths (4 happy, 0 revert) - [R] external setRewardRate — 4 blocks, 3 edges, 2 paths (1 happy, 1 revert) - [R] external pause — 4 blocks, 3 edges, 2 paths (1 happy, 1 revert) - [R] external unpause — 4 blocks, 3 edges, 2 paths (1 happy, 1 revert) - [P] public rewardPerToken — 5 blocks, 4 edges, 2 paths (2 happy, 0 revert) - [P] public earned — 2 blocks, 1 edges, 1 paths (1 happy, 0 revert) - Sequences (depth 3): 584 total (8 functions: 6 state-changing, 2 read-only) -``` - -Each function line prints an access badge (`[P]` public/external, `[R]` restricted/admin-gated, `[S]` system/internal), visibility, block/edge counts from the CFG, and the path-tree breakdown (`total`, `happy`, `revert`). - -## Example: `--max-seq-depth` - -The sequence tree enumerates ordered combinations of entry-point calls up to depth N. Raising the bound surfaces longer interaction patterns that the static analyzer reasons about: - -``` -$ ilold analyze tests/fixtures/staking.sol --max-seq-depth 5 -... -Sequences (depth 5): 37448 total (8 functions: 6 state-changing, 2 read-only) -``` - -The sequence tree is consumed by the cross-step transitive-effect pass and feeds `seq` in the REPL. - -## Example: `--verbose` - -`--verbose` adds: - -- One line per CFG block (`[id] BlockKind (N stmts)`). -- One line per CFG edge (`src → dst EdgeKind`). -- The intra-contract call graph (`fn → contract.fn` with `internal | external | inherited`). -- A per-function behavior tree with `requires`, `writes`, `calls`, `emits`, and transitions to other functions, including the shared state variables that link them. - -The output is meant to be read top-down by a human; `context` (next page) is the machine-readable counterpart. - -## Notes - -- `analyze` does not require a configured project; it works on raw `.sol` files. -- Interfaces are listed in the contract header but have no sequence tree. -- Errors building the CFG for a single function are reported inline and do not abort the run. - -See [Solidity: Limitations](./limitations.md) for the analysis boundaries (intraprocedural slicing, modifier placeholder split, etc.). diff --git a/docs/guide/src/solidity/cli-context.md b/docs/guide/src/solidity/cli-context.md deleted file mode 100644 index ef73f14..0000000 --- a/docs/guide/src/solidity/cli-context.md +++ /dev/null @@ -1,67 +0,0 @@ -# CLI: `context` - -`ilold context` produces a structured narrative for a single function or for a comma-separated sequence of functions. It runs the same pipeline as `analyze` (parser → CFG → path tree → sequence analysis with cross-contract transitive effects) but emits a focused narrative instead of the project-wide pretty-print. - -## Synopsis - -``` -ilold context [--contract ] [--function ] - [--sequence ] [--list] -``` - -| Flag | Description | -| --- | --- | -| `--contract ` | Pick the active contract. Required when the project has more than one. | -| `--function ` | Build a function narrative: paths, state reads/writes, internal/external calls, transitive effects, observations. | -| `--sequence ` | Build a sequence narrative across the listed functions. | -| `--list` | List functions of the resolved contract with access level and tags, then exit. | - -The function/sequence narratives are the same data structures the REPL renders for `i ` and `seq` respectively (see `crates/ilold-core/src/narrative/function.rs` and `narrative/sequence.rs`). - -## Example: list mode - -``` -$ ilold context tests/fixtures/staking.sol --list - Staking — 9 functions - - [P] deposit external - [P] withdraw external - [P] claimRewards external - [R] setRewardRate external - [R] pause external - [R] unpause external - [P] rewardPerToken public - [P] earned public - - Usage: - ilold context --function - ilold context --sequence "fn1,fn2" - - Example: - ilold context --function deposit - ilold context --sequence "deposit,withdraw" -``` - -The badges line up with `analyze`: `[P]` public/external entry point, `[R]` restricted/admin-gated, `[S]` system/internal. - -## Example: single function - -``` -$ ilold context tests/fixtures/staking.sol --function withdraw -``` - -Output is the same `FunctionNarrative` printed by the REPL's `i withdraw`, including transitive effects through the call chain. - -## Example: a sequence - -``` -$ ilold context tests/fixtures/staking.sol --sequence deposit,withdraw,claimRewards -``` - -The output narrates the per-step writes and the cross-step dependencies (variables shared between consecutive steps). - -## Notes - -- `context` is read-only; it does not start the API server. -- `--list` short-circuits before computing path trees, so it is cheap on large projects. -- Use [`explore`](./repl/session.md) when you need to iterate; `context` is meant for scripts and one-off questions. diff --git a/docs/guide/src/solidity/limitations.md b/docs/guide/src/solidity/limitations.md deleted file mode 100644 index fb83df8..0000000 --- a/docs/guide/src/solidity/limitations.md +++ /dev/null @@ -1,40 +0,0 @@ -# Known Limitations - -This page documents the current analysis boundaries of ilold. Understanding these limitations is necessary for interpreting slice, timeline, and trace results correctly. - -## Intraprocedural slicing only - -The dataflow slicer operates within a single function body (plus its inlined modifiers). It does not follow values across function call boundaries. If `deposit` calls an internal helper `_updateBalance(amount)`, the slice for `amount` in `deposit` will show the call site but not the writes inside `_updateBalance`. Use `tr ` to inspect internal call bodies separately. - -## Assignment-only DEF extraction - -Only `Assignment` expressions (`x = ...`, `x += ...`, `x -= ...`) produce DEF entries in the slicer's use-def analysis. Solidity mutations that are not modeled as assignments -- specifically `x++`, `x--`, `++x`, `--x`, `delete x`, `arr.push(v)`, and `arr.pop()` -- are captured as USEs of the target variable but not as DEFs. A backward slice on a variable mutated exclusively through `.push()` will miss the mutating statement as a definition point. - -## Modifier placeholder split - -Modifier bodies are split at the first top-level `_;` (placeholder) statement to separate "before" code from "after" code. If the placeholder appears inside a nested block (e.g., inside an `if` branch), the entire modifier body is treated as "before" code. This is over-inclusive: statements that should execute after the function body will appear before it in the flattened view. In practice, most modifiers place `_;` at the top level, so this rarely triggers. - -## Forward slice over-tainting via ancestor merge - -When a statement is included in a forward slice, its lexical ancestors (enclosing `if`, `for`, `while` blocks) are also included so that the rendered slice shows control-flow context. The ancestor's condition variables are merged into the tainted set. This means an `if (unrelatedCondition)` enclosing a tainted write will add `unrelatedCondition` to the taint set, potentially pulling in unrelated statements in subsequent iterations. The result is a conservative (larger) slice rather than a precise one. - -## Tuple destructuring - -Tuple destructuring assignments such as `(a, b) = foo()` may not be recognized as DEFs depending on how the Solidity frontend lowers them. If the frontend does not emit a top-level `Assignment` node, the individual targets (`a`, `b`) are treated as USEs only. This can cause a backward slice to miss the destructuring as a definition point for `a` or `b`. - -## Timeline tracks state mutations only - -The `timeline` command tracks writes to state variables across session steps. Local variable assignments within a function body are recorded separately (`local_entries`) but are not visible in the default timeline output. If you need to trace a local variable, use `slice` within the specific function instead. - -## Session requires at least one call - -The `timeline`, `state`, and `sequence` endpoints require an active session with at least one `Call` step. The `timeline` and `state` commands return empty results if no steps have been added. The `sequence` command requires at least two steps. Use `tr ` for read-only inspection of a function's flow without adding it to the session. - -## Internal and private functions cannot be session entry points - -Session steps model real external transactions. Functions with `internal` or `private` visibility cannot be called from outside the contract, so they cannot be added as session steps via `c `. Use `tr ` to inspect their execution flow, or call a public/external function that invokes them to see their effects through the modifier and internal-call inlining in the trace. - -## Related pages - -- [Taint Analysis](./workflows/taint-analysis.md) -- forward slice caveats in practice -- [HTTP API Reference](../reference/api-endpoints.md) diff --git a/docs/guide/src/solidity/overview.md b/docs/guide/src/solidity/overview.md deleted file mode 100644 index cfd4df9..0000000 --- a/docs/guide/src/solidity/overview.md +++ /dev/null @@ -1,39 +0,0 @@ -# Solidity Backend Overview - -The Solidity backend is built on top of `solar-compiler` (parser) and a set of analysis passes in `ilold-core`. It supports both a one-shot CLI (`analyze`, `context`) and the interactive REPL (`explore`, `serve`). - -## What it parses - -`crates/ilold-cli/src/main.rs::collect_sol_files` walks the input path: - -- A single `.sol` file is loaded as-is. -- A directory is walked recursively; the directories `out`, `cache`, `node_modules`, `lib`, `target`, `.git`, `.svelte-kit` and any dot-prefixed directory are skipped. - -Once the project is parsed, ilold builds a per-function CFG via `CfgBuilder::build_with_project` and a path tree via `build_path_tree` (see `crates/ilold-core/src/cfg/builder.rs` and `pathtree/walker.rs`). Inheritance is resolved transitively, so inherited functions and state variables show up in `funcs-all` / `vars-all` and in `info` output. - -## What you can do with it - -| Surface | Purpose | -| --- | --- | -| [`analyze`](./cli-analyze.md) | One-shot pretty-print of every contract: functions, CFG and path-tree stats, sequences up to `--max-seq-depth`, optional verbose function behavior breakdown | -| [`context`](./cli-context.md) | Generate machine-readable narratives for a function or a comma-separated sequence | -| [`serve`](./repl/workspace.md) | Start the HTTP/WS server only, no REPL: feed the web canvas | -| [`explore`](./repl/session.md) | Interactive REPL, with the HTTP/WS API running alongside | - -## REPL command groups - -The REPL has six command groups, all documented in their own pages: - -- [Session](./repl/session.md): `c/call`, `b/back`, `cl/clear`, `s/state`, `seq/sequence`, `st/step`, `ss/session`. -- [Analysis](./repl/analysis.md): `w/who`, `i/info`, `tr/trace`, `tl/timeline`, `sl/slice`. -- [Contract](./repl/contract.md): `f/functions`, `fa/funcs-all`, `v/vars`, `va/vars-all`, `ct/contracts`, `use`. -- [Findings](./repl/findings.md): `fi/finding`, `n/note`, `status`, `fl/findings`, `ex/export`. -- [Scenarios](./repl/scenarios.md): `sc/scenario` (`new`, `list`, `switch`, `fork`, `delete`). -- [Workspace](./repl/workspace.md): `save`, `load`, `browser`, `q/quit/exit`, `?/help`. - -## Workflows - -Two end-to-end walkthroughs are included: - -- [Audit walkthrough](./workflows/audit-walkthrough.md): full session against a Staking contract. -- [Taint analysis](./workflows/taint-analysis.md): forward slicing of user-controlled parameters. diff --git a/docs/guide/src/solidity/repl/analysis.md b/docs/guide/src/solidity/repl/analysis.md deleted file mode 100644 index 2b03e40..0000000 --- a/docs/guide/src/solidity/repl/analysis.md +++ /dev/null @@ -1,183 +0,0 @@ -# Analysis Commands - -Analysis commands query the contract model without modifying the session. Each command produces cross-reference hints at the bottom of its output, suggesting related commands to run next. - -## who - -`w ` or `who ` - -Shows which functions read and write a state variable, with their access level. Searches across the active contract and its ancestors. - -``` -ilold[Staking]> who totalStaked - - who: totalStaked - Writers: - [P] deposit - [P] withdraw - Readers: - [P] rewardPerToken - → sl deposit totalStaked, sl withdraw totalStaked - → tl totalStaked -``` - -The cross-reference hints suggest running [slice](#slice) for each writer and [timeline](#timeline) for the variable. Access badges: `[P]` public/external, `[R]` restricted (admin-gated), `[I]` internal, `[S]` special. - -**Returns:** `VariableInfo { variable, writers, readers }` where each writer/reader is a `(String, AccessLevel)` pair. - -## info - -`i ` or `info ` - -Displays a full function narrative: execution paths, state reads/writes, internal and external calls, transitive effects through the call chain, and observations. Works on any function including internal ones. - -``` -ilold[Staking]> i withdraw - - withdraw [public] — whenNotPaused, nonReentrant - ├── Paths: 3 total, 1 happy, 2 revert - ├── State reads: balances, totalStaked - ├── State writes: balances, totalStaked - ├── External calls: msg.sender.call{value: amount} - ├── Transitive effects: - │ via _updateRewards: - │ writes: rewardDebt - │ reads: rewardPerToken - └── Observations: - · External call after state writes (checks-effects-interactions followed) - → c withdraw, tr withdraw -``` - -The `info` command does not require the function to be in the session. It analyzes the function in isolation. - -## trace - -`tr [--depth N] [--reverts] [+N...] [-i]` -`tr step ` - -Renders the execution flow tree for a function. Modifier bodies are inlined into the tree with `[from: modifier]` annotations. Internal calls are expanded up to the depth limit (default 2). - -``` -ilold[Staking]> tr withdraw - - ╭──────────────────────────────────────╮ - │ Staking::withdraw(uint256) │ - │ modifiers: whenNotPaused, nonReentrant│ - │ max inlining depth: 2 │ - ╰──────────────────────────────────────╯ - - 001 │ ▶ withdraw(uint256) - 002 │ ├─ ◇ require(!paused, "Paused") [from: whenNotPaused] - 003 │ ├─ ◇ require(!locked) [from: nonReentrant] - 004 │ ├─ ✏ locked = true [from: nonReentrant] - 005 │ ├─ ◇ require(amount <= balances[msg.sender]) - 006 │ ├─ ○ _updateRewards(msg.sender) [+8 ops, depth limited] - 007 │ ├─ ✏ balances[msg.sender] -= amount - 008 │ ├─ ✏ totalStaked -= amount - 009 │ ├─ → msg.sender.call{value: amount} - 010 │ ├─ ◆ emit Withdrawn(msg.sender, amount) - 011 │ └─ ✏ locked = false [from: nonReentrant] - - tip: expand with `tr +N` — candidates: 6 - → sl withdraw balances, sl withdraw totalStaked -``` - -### Icon legend - -| Icon | Meaning | -|------|---------| -| `▶` | Function entry | -| `◇` | require/assert | -| `✏` | State write | -| `▸` | State read | -| `○` | Internal call | -| `→` | External call | -| `◆` | Event emission | -| `?` | Branch (if/else) | -| `↻` | Loop header | -| `✓` | Return | -| `✗` | Revert | - -### Options - -- `--depth N` -- Set max inlining depth for internal calls. Default is 2. -- `--reverts` -- Include revert paths in the tree. -- `+N` -- Force-expand a depth-limited internal call at step N. Multiple `+N` flags allowed. -- `-i` -- Open the trace in an interactive TUI with keyboard navigation. Increases default depth to 4. -- `step N` -- Re-render the persisted flow tree from session step N (depth/expand flags are ignored). - -``` -ilold[Staking]> tr withdraw +6 - - (same tree with _updateRewards fully expanded at step 6) -``` - -``` -ilold[Staking]> tr step 0 - - (renders the persisted trace from session step 0) -``` - -## timeline - -`tl ` or `timeline ` - -Shows the cross-step mutation history of a variable across the current session. Each mutation includes the operator, value expression, and path conditions (reached-when). - -``` -ilold[→ deposit → withdraw]> tl totalStaked - - totalStaked — mutation timeline - ════════════════════════════════════════════════════════════ - [state] - session step 0 deposit - ✏ totalStaked += msg.value [trace step 5] - session step 1 withdraw - ✏ totalStaked -= amount [trace step 8] - reached when: - · amount <= balances[msg.sender] - → sl deposit totalStaked, sl withdraw totalStaked -``` - -If the variable has no mutations in the current session, timeline tells you to add steps with `c ` first. - -## slice - -`sl [--backward|--forward|--both]` - -Performs dataflow analysis on a variable within a function. Walks the function body and modifier bodies to find definitions (backward) and uses (forward) of the variable. Modifier entries are prefixed with `[mod name]`. - -The direction flags can appear in any position: `sl --backward deposit totalStaked` and `sl deposit totalStaked --backward` are equivalent. The default direction is `--both`. - -``` -ilold[Staking]> sl withdraw balances --backward - - withdraw · balances — dataflow slice - ════════════════════════════════════════════════════════════ - [backward] - L31 require(amount <= balances[msg.sender]) - L34 balances[msg.sender] -= amount - → tr withdraw | tl balances -``` - -``` -ilold[Staking]> sl withdraw totalStaked --both - - withdraw · totalStaked — dataflow slice - ════════════════════════════════════════════════════════════ - [backward] - L35 totalStaked -= amount - [forward] - L38 emit Withdrawn(msg.sender, amount) - → tr withdraw | tl totalStaked -``` - -Short flags are also accepted: `-b` for `--backward`, `-f` for `--forward`. - -When a variable is defined inside a modifier body, the entry shows its origin: - -``` - [backward] - L12 [mod whenNotPaused] require(!paused, "Paused") - L35 totalStaked -= amount -``` diff --git a/docs/guide/src/solidity/repl/contract.md b/docs/guide/src/solidity/repl/contract.md deleted file mode 100644 index fb1b3d8..0000000 --- a/docs/guide/src/solidity/repl/contract.md +++ /dev/null @@ -1,122 +0,0 @@ -# Contract Commands - -Contract commands inspect the structure of the loaded contracts without modifying the session. - -## functions - -`f` or `functions` - -Lists the callable functions in the active contract with their access level and tags. - -``` -ilold[Staking]> f - - [P] deposit writes state, external calls - [P] withdraw writes state, external calls - [P] claimRewards writes state, external calls - [R] setRewardRate writes state - [R] pause writes state - [R] unpause writes state - [P] rewardPerToken view - [P] earned view -``` - -Badges: `[P]` public/external, `[R]` restricted (admin-gated), `[I]` internal, `[S]` special. Tags indicate `writes state`, `external calls`, or `view` (read-only, no external calls). - -**Returns:** `FunctionList { functions: [FunctionEntry { name, access, writes_state, has_external_calls, is_read_only }] }`. - -## funcs-all - -`fa` or `funcs-all` - -Lists all accessible functions including those inherited from parent contracts. - -``` -ilold[Staking]> fa - - [P] deposit writes state, external calls - [P] withdraw writes state, external calls - [P] claimRewards writes state, external calls - [R] setRewardRate writes state - [R] pause writes state - [R] unpause writes state - [P] rewardPerToken view - [P] earned view - - inherited: - [P] owner from Ownable - [P] transferOwnership from Ownable -``` - -Inherited functions are listed separately with their origin contract. - -**Returns:** `FunctionListAll { functions: [AccessibleFunctionEntry { name, access, writes_state, has_external_calls, is_read_only, origin, is_inherited }] }`. - -## vars - -`v` or `vars` - -Lists the state variables of the active contract with their type and mutability tag. - -``` -ilold[Staking]> v - - mutable owner address - mutable paused bool - mutable rewardRate uint256 - mutable lastUpdateTime uint256 - mutable rewardPerTokenStored uint256 - mutable balances mapping(address => uint256) - mutable userRewardPerTokenPaid mapping(address => uint256) - mutable rewards mapping(address => uint256) - mutable totalStaked uint256 -``` - -Tags are `mutable`, `const`, or `immutable`. - -## vars-all - -`va` or `vars-all` - -Lists all accessible state variables including inherited ones. - -``` -ilold[Staking]> va - - mutable owner address - mutable paused bool - mutable rewardRate uint256 - ... -``` - -**Returns:** `StateVarListAll { state_vars: [AccessibleStateVarEntry { name, type_name, is_constant, is_immutable, origin, is_inherited }] }`. Inherited entries print under an `inherited:` section with `from `. - -## contracts - -`ct` or `contracts` - -Lists all contracts in the loaded project with their type badge, function count, state variable count, and `inherits` clause when present. - -``` -ilold[Staking]> ct - - [I] IERC20 3 functions, 0 state vars - [C] Staking 9 functions, 11 state vars ← current -``` - -Type badges: `[C]` contract, `[I]` interface, `[L]` library, `[A]` abstract. - -## use - -`use ` - -Switches the active contract. Clears the current session steps. - -``` -ilold[Staking]> use Ownable - - ✓ Now using: Ownable - Cleared 2 step(s) from previous contract -``` - -After switching, all session and analysis commands operate on the new contract. diff --git a/docs/guide/src/solidity/repl/findings.md b/docs/guide/src/solidity/repl/findings.md deleted file mode 100644 index 290c7a4..0000000 --- a/docs/guide/src/solidity/repl/findings.md +++ /dev/null @@ -1,86 +0,0 @@ -# Findings Commands - -Findings commands let you record security observations during an audit session. Findings are tied to the current session state and can be exported as a markdown report. - -## finding - -`fi [severity] [title]` or `finding [severity] [title]` - -Records a security finding. Can be used in two modes: - -**Inline mode** -- pass severity and title directly: - -``` -ilold[→ deposit → withdraw]> fi high Reentrancy in withdraw before balance update - - ✓ Finding F-001 added -``` - -**Interactive mode** -- run `fi` with no arguments to be prompted: - -``` -ilold[→ deposit → withdraw]> fi - Severity (critical/high/medium/low/info): - > high - Title: - > Reentrancy in withdraw before balance update - Description (optional): - > The external call on L38 occurs before totalStaked is decremented. - ✓ Finding F-001 added -``` - -Valid severities: `critical`, `high`, `medium`, `low`, `info` (or `informational`). - -The finding captures the current session sequence automatically. - -## note - -`n ` or `note ` - -Attaches a free-text note to the current session step. Notes are included in the exported report. - -``` -ilold[→ deposit → withdraw]> n Check if msg.value can be zero here - - ✓ Note added -``` - -Scenarios are managed by the dedicated `sc | scenario` command family (`scenario new `, `scenario fork [at ]`, `scenario switch `, `scenario list`, `scenario delete `). See [Scenarios](./scenarios.md) for the full reference. - -## status - -`status ` - -Sets the review status for a function. Useful for tracking audit progress. - -``` -ilold[Staking]> status deposit reviewed - - ✓ Status updated -``` - -Valid statuses: `reviewed`, `suspicious`, `vulnerable`, `clean`, `inprogress`, `notreviewed`. - -## findings - -`fl` or `findings` - -Lists the count of recorded findings. Use [export](#export) to see full details. - -``` -ilold[Staking]> fl - - 2 finding(s) recorded. Use export to export. -``` - -## export - -`ex` or `export` - -Exports all findings, notes, and status changes as a markdown report. The file is written to the current directory. - -``` -ilold[Staking]> ex - - ✓ Exported to ilold-report-Staking.md -``` diff --git a/docs/guide/src/solidity/repl/scenarios.md b/docs/guide/src/solidity/repl/scenarios.md deleted file mode 100644 index 0536f90..0000000 --- a/docs/guide/src/solidity/repl/scenarios.md +++ /dev/null @@ -1,81 +0,0 @@ -# Scenario Commands - -A scenario is a named branch of the session timeline. Every session starts on the default scenario `main`; the auditor can create more scenarios, switch between them, fork an existing one at a specific step, or delete a scenario that is no longer needed. The prompt shows the active scenario as `ilold[Contract/scenario]` when it is not `main`. - -## scenario new - -`scenario new ` (alias: `sc new`) - -Creates an empty scenario with no steps. The new scenario is not activated automatically. Use `scenario switch` to make it the active one. - -``` -ilold[Staking]> sc new reentrancy - ✓ Created scenario 'reentrancy' -``` - -**Returns:** `ScenarioCreated { name }`. - -## scenario list - -`scenario list` (aliases: `scenario ls`, `sc list`) - -Lists every scenario in the active session, marking the active one. - -``` -ilold[Staking]> sc list - scenarios — 2 total, active: main - name steps - → main 2 - reentrancy 0 -``` - -**Returns:** `ScenarioList { items: [ScenarioInfo { name, step_count, active }] }`. The CLI renders it inside a framed header box. - -## scenario switch - -`scenario switch ` (alias: `sc switch `) - -Activates an existing scenario. All subsequent session and analysis commands operate against its step list. The prompt updates to reflect the new active scenario. - -``` -ilold[Staking]> sc switch reentrancy - ✓ Switched: 'main' → 'reentrancy' -ilold[Staking/reentrancy]> -``` - -**Returns:** `ScenarioSwitched { from, to }`. Switching to the active scenario is idempotent and prints `· Already on scenario ''`. - -## scenario fork - -`scenario fork [at ]` (alias: `sc fork`) - -Creates a new scenario branching from the active one. With `at `, the new scenario inherits steps `0..N` from the source scenario; without it, the new scenario inherits the full step list of the source. After forking, the new scenario is activated. - -``` -ilold[Staking → deposit → withdraw → claimRewards]> sc fork attack-v2 at 1 - ✓ Forked 'main' → 'attack-v2' at step 1 -ilold[Staking/attack-v2 → deposit]> -``` - -**Returns:** `ScenarioForked { from, to, at_step }`. - -Forks are useful when the auditor wants to keep an existing line of reasoning intact while testing a divergent path. - -## scenario delete - -`scenario delete ` (aliases: `scenario rm `, `sc delete`, `sc rm`) - -Removes a scenario. The active scenario cannot be deleted; switch first. - -``` -ilold[Staking]> sc delete reentrancy - ✓ Deleted scenario 'reentrancy' -``` - -**Returns:** `ScenarioDeleted { name }`. - -## Notes - -- Solidity scenarios share the same parsed model, so analysis commands stay cheap across forks. -- For the Solana counterpart (each scenario carries its own VM, signers and PDAs), see [Solana: Scenarios](../../solana/repl/scenarios.md). -- The full scenario tree is included in `save` / `load` and in the `export` report. diff --git a/docs/guide/src/solidity/repl/session.md b/docs/guide/src/solidity/repl/session.md deleted file mode 100644 index 9f0bc7b..0000000 --- a/docs/guide/src/solidity/repl/session.md +++ /dev/null @@ -1,139 +0,0 @@ -# Session Commands - -Session commands manage the call sequence -- the ordered list of function calls that represents an execution scenario you want to analyze. - -## call - -`c ` or `call ` - -Adds a function call to the session sequence. Only external and public functions are accepted; internal and private functions are rejected since they cannot be entry points for a real transaction. - -``` -ilold[Staking]> c deposit - - + Step 0: deposit [P] external - State writes: - · balances - · totalStaked - Sequence: deposit -``` - -``` -ilold[→ deposit]> c withdraw - - + Step 1: withdraw [P] external - State writes: - · balances - · totalStaked - Sequence: deposit → withdraw -``` - -**Returns:** `StepAdded { step_index, function, access, state_changed }`. `access` is one of `Public`, `Restricted { role }`, `Internal`, `Special { kind }`. - -Attempting to call an internal function: - -``` -ilold[Staking]> c _updateRewards - - '_updateRewards' is internal and cannot be called from outside the contract — - not a valid session entry point. Use `tr _updateRewards` to view its flow, - or `c ` to trace a real entry point. -``` - -## back - -`b` or `back` - -Removes the last step from the session sequence. - -``` -ilold[→ deposit → withdraw]> b - - - Step removed. 1 remaining. - Sequence: deposit -``` - -## clear - -`cl` or `clear` - -Resets the session, removing all steps. Prompts for confirmation if steps exist. - -``` -ilold[→ deposit → withdraw]> cl - Clear 2 steps? (y/n) - y - Session cleared. -``` - -## state - -`s` or `state` - -Shows the accumulated state mutations across all steps in the session. Each variable lists every mutation with the operator symbol (`+=`, `-=`, `=`) and the originating function. - -``` -ilold[→ deposit → withdraw]> s - - ═══════════════════[ STATE ]═══════════════════ - balances - += msg.value (step 0, deposit) - -= amount (step 1, withdraw) - totalStaked - += msg.value (step 0, deposit) - -= amount (step 1, withdraw) -``` - -Each change line is ` (step , )`, with an optional `via ` suffix when the mutation comes from a modifier body. - -**Returns:** `StateView { summary: [VariableSummary { variable, changes }] }`. If the session is empty, `state` tells you to add steps first. - -## sequence - -`seq` or `sequence` - -Displays a narrative of the current call sequence, including dependencies between steps and observations about the interaction pattern. Requires at least 2 steps. - -``` -ilold[→ deposit → withdraw]> seq - - Step 0: deposit - writes: balances, totalStaked - - Step 1: withdraw - writes: balances, totalStaked - depends on: deposit (shared state: balances, totalStaked) - - Observations: - · deposit and withdraw modify the same variables (balances, totalStaked) -``` - -## step - -`st ` or `step ` - -Re-inspects a specific session step, showing its full function narrative (same output as [info](./analysis.md#info)). You can also write `st0`, `st1` without a space. - -``` -ilold[→ deposit → withdraw]> st 0 - - deposit [public] — whenNotPaused - ├── Paths: 2 total, 1 happy, 1 revert - ├── State reads: balances - ├── State writes: balances, totalStaked - └── Events: Deposited -``` - -## session - -`ss` or `session` - -Shows the full session overview: active contract, current step sequence, and findings count. - -``` -ilold[→ deposit → withdraw]> ss - - Contract: Staking - Steps: deposit → withdraw - Findings: 0 -``` diff --git a/docs/guide/src/solidity/repl/workspace.md b/docs/guide/src/solidity/repl/workspace.md deleted file mode 100644 index a24a2f2..0000000 --- a/docs/guide/src/solidity/repl/workspace.md +++ /dev/null @@ -1,53 +0,0 @@ -# Workspace Commands - -Workspace commands handle session persistence and external tools. - -## save - -`save ` - -Saves the current session (steps, findings, notes, statuses) to a JSON file under `~/.ilold/sessions/`. - -``` -ilold[→ deposit → withdraw]> save staking-audit - - ✓ Saved to /Users/you/.ilold/sessions/staking-audit.json -``` - -You can resume this session later with [load](#load), even across different ilold runs, as long as the same contract files are loaded. - -## load - -`load ` - -Loads a previously saved session from `~/.ilold/sessions/`. - -``` -ilold[Staking]> load staking-audit - - ✓ Session loaded (2 steps) -``` - -The prompt updates to reflect the loaded steps. The session replaces whatever is currently in memory. - -## browser - -`browser` - -Prints the base URL of the HTTP API the REPL is talking to. `explore` runs the API in-process by default; pass `--attach ` to point the REPL at a separate `serve` instance instead. - -``` -ilold[Staking]> browser - - API running at http://127.0.0.1:52431/api/ -``` - -The web canvas (when running `serve` and opening the URL in a browser) subscribes to the same HTTP/WS endpoints. See [HTTP API Reference](../../reference/api-endpoints.md) for the full surface. - -## quit - -`q`, `quit`, or `exit` - -Exits the REPL. `Ctrl+D` and `Ctrl+C` also work. - -Unsaved session data is lost on exit. Use [save](#save) before quitting if the session needs to survive. diff --git a/docs/guide/src/solidity/workflows/audit-walkthrough.md b/docs/guide/src/solidity/workflows/audit-walkthrough.md deleted file mode 100644 index 27033ee..0000000 --- a/docs/guide/src/solidity/workflows/audit-walkthrough.md +++ /dev/null @@ -1,234 +0,0 @@ -# Full Audit Walkthrough - -This walkthrough demonstrates a realistic audit of a Staking contract using the ilold interactive REPL. The contract allows users to deposit tokens, withdraw them, and claim rewards, with owner-only administrative functions and a pause mechanism. - -## Starting the session - -Launch ilold against the Staking contract source files: - -``` -ilold explore contracts/Staking.sol -``` - -The REPL starts and auto-selects the main contract: - -``` -ilold explore -- Staking -8 functions | Type ? for help -Web UI: http://localhost:3001 - -Staking > -``` - -List available functions and state variables to orient yourself: - -``` -Staking > f - deposit External writes state - withdraw External writes state - claimRewards External writes state - setRewardRate External writes state onlyOwner - pause External writes state onlyOwner - unpause External writes state onlyOwner - rewardPerToken Public read-only - earned Public read-only - -Staking > v - stakingToken address - rewardToken address - owner address - paused bool - rewardRate uint256 - lastUpdateTime uint256 - rewardPerTokenStored uint256 - balances mapping(address => uint256) - userRewardPerTokenPaid mapping(address => uint256) - rewards mapping(address => uint256) - totalStaked uint256 -``` - -The function list shows access level, whether a function writes state, and which modifiers restrict access. Read-only functions (view/pure) are marked separately. This is your starting map. - -## Calling deposit -- observing state writes - -``` -Staking > c deposit - Step 0: deposit [External] - State changed: balances, totalStaked -``` - -The session records `deposit` as step 0. The engine analyzed the CFG and reports which state variables are mutated. Two writes: `balances` and `totalStaked`. - -## Investigating totalStaked with who, slice, trace - -The `who` command reveals which functions read and write a variable across the entire contract: - -``` -Staking > who totalStaked - Writers: - deposit External - withdraw External - Readers: - rewardPerToken Public -``` - -Both `deposit` and `withdraw` modify `totalStaked`, and `rewardPerToken` reads it. This tells you the impact surface: any bug in how `totalStaked` is updated will propagate to reward calculations. - -Now use `slice` to see the dataflow within `deposit`: - -``` -Staking > sl deposit totalStaked - Backward slice (sources -> totalStaked): - [0] stakingToken.transferFrom(msg.sender, address(this), amount) - [2] totalStaked += amount - - Forward slice (totalStaked -> sinks): - [2] totalStaked += amount -``` - -The backward slice shows that the `transferFrom` call precedes the state write. The forward slice is short because `totalStaked` is only written, not read within this function. The real consumers are in `rewardPerToken` -- the `who` output already told you that. - -Use `trace` to see the full execution flow: - -``` -Staking > tr deposit - 1. [mod whenNotPaused] require(!paused, "paused") - 2. [mod updateReward] rewardPerTokenStored = rewardPerToken() - 3. [mod updateReward] lastUpdateTime = block.timestamp - 4. [mod updateReward] userRewardPerTokenPaid[account] = rewardPerTokenStored - 5. [mod updateReward] rewards[account] = earned(account) - 6. require(amount > 0, "Cannot stake 0") - 7. stakingToken.transferFrom(msg.sender, address(this), amount) - 8. balances[msg.sender] += amount - 9. totalStaked += amount -``` - -Lines prefixed with `[mod ...]` come from modifiers inlined before the function body. The `updateReward` modifier writes four state variables before `deposit`'s own body runs. This is critical context: if you only looked at the function body, you would miss these writes. - -## Calling withdraw -- observing the sequence - -``` -Staking > c withdraw - Step 1: withdraw [External] - State changed: balances, totalStaked - -Staking[deposit > withdraw] > -``` - -The prompt now shows both steps. Use `state` to see the accumulated picture: - -``` -Staking[deposit > withdraw] > s - balances written by: deposit (+= amount), withdraw (-= amount) - totalStaked written by: deposit (+= amount), withdraw (-= amount) - rewardPerTokenStored written by: deposit (via updateReward), withdraw (via updateReward) - lastUpdateTime written by: deposit (via updateReward), withdraw (via updateReward) - userRewardPerTokenPaid written by: deposit (via updateReward), withdraw (via updateReward) - rewards written by: deposit (via updateReward), withdraw (via updateReward) -``` - -The state view aggregates every write from every step. Mutations introduced by modifiers are tagged with `via `. You can see that `updateReward` runs in both `deposit` and `withdraw`, updating the reward accounting state each time. - -## Checking claimRewards -- modifier writes in the slice - -``` -Staking > c claimRewards - Step 2: claimRewards [External] - State changed: rewards -``` - -Slice `claimRewards` for the `rewards` variable: - -``` -Staking[deposit > withdraw > claimRewards] > sl claimRewards rewards --both - Backward slice (sources -> rewards): - [mod updateReward] rewards[account] = earned(account) - [5] uint256 reward = rewards[msg.sender] - - Forward slice (rewards -> sinks): - [mod updateReward] rewards[account] = earned(account) - [5] uint256 reward = rewards[msg.sender] - [7] rewards[msg.sender] = 0 - [8] rewardToken.transfer(msg.sender, reward) -``` - -Entries tagged `[mod updateReward]` are statements from the modifier body that touch `rewards`. The slicer walks both the function body and every applied modifier, so nothing is hidden. The forward slice shows that `rewards[msg.sender]` is read into a local, zeroed, and then transferred -- the standard claim pattern. - -## Using timeline to track a variable across all steps - -``` -Staking[deposit > withdraw > claimRewards] > tl balances - Variable: balances - - Step 0 deposit balances[msg.sender] += amount - Step 1 withdraw balances[msg.sender] -= amount -``` - -``` -Staking[deposit > withdraw > claimRewards] > tl rewardPerTokenStored - Variable: rewardPerTokenStored - - Step 0 deposit rewardPerTokenStored = rewardPerToken() via updateReward - Step 1 withdraw rewardPerTokenStored = rewardPerToken() via updateReward - Step 2 claimRewards rewardPerTokenStored = rewardPerToken() via updateReward -``` - -The timeline shows every mutation of a variable across the entire session in chronological order. Each entry includes the step index, the function that caused it, the assignment expression, and whether it came from a modifier. Path conditions (from branching logic) are included when the function has conditional writes. - -This gives you a cross-function view that no single-function analysis can provide. - -## Recording findings and exporting - -Record an observation while looking at the claim flow: - -``` -Staking[deposit > withdraw > claimRewards] > n claimRewards zeroes rewards before transfer -- CEI pattern followed -``` - -If you spot an issue, record a finding with severity: - -``` -Staking[deposit > withdraw > claimRewards] > fi medium No check that reward > 0 before transfer call - Finding F-001 added (Medium) -``` - -Mark functions as reviewed: - -``` -Staking[deposit > withdraw > claimRewards] > status deposit reviewed -Staking[deposit > withdraw > claimRewards] > status claimRewards suspicious -``` - -Export the session to a markdown report: - -``` -Staking[deposit > withdraw > claimRewards] > export - Exported to ilold-report-Staking.md -``` - -Save the session for later: - -``` -Staking[deposit > withdraw > claimRewards] > save staking-audit-day1 - Saved to ~/.ilold/sessions/staking-audit-day1.json -``` - -## How cross-reference hints guide exploration - -The output of each command naturally points to the next: - -1. `functions` shows which functions write state -- you call the important ones first. -2. `c deposit` reports `balances, totalStaked` changed -- you run `who totalStaked` to see the impact. -3. `who` shows `rewardPerToken` reads `totalStaked` -- you run `sl rewardPerToken totalStaked` or `tr rewardPerToken` to trace the dependency. -4. `slice` shows modifier-origin statements -- you trace the modifier with `tr deposit` to see full execution order. -5. `state` aggregates writes across steps -- variables with writes from multiple functions deserve `timeline` inspection. -6. `timeline` reveals the chronological mutation history -- unexpected patterns become findings. - -Each command's output contains the variable names, function names, and modifier names needed for the next query. The workflow is: call, observe, investigate, record, repeat. - -## Related pages - -- [Session commands](../repl/session.md) -- [Taint Analysis](./taint-analysis.md) -- [HTTP API Reference](../../reference/api-endpoints.md) -- [Known Limitations](../limitations.md) diff --git a/docs/guide/src/solidity/workflows/taint-analysis.md b/docs/guide/src/solidity/workflows/taint-analysis.md deleted file mode 100644 index 67cc4ac..0000000 --- a/docs/guide/src/solidity/workflows/taint-analysis.md +++ /dev/null @@ -1,103 +0,0 @@ -# Taint Analysis -- Tracing User Input - -Forward slicing traces how a variable's value propagates through a function body. When the starting variable is a user-controlled parameter, the slice acts as a taint analysis: it reveals every state variable, local variable, and external call that the attacker-controlled input can reach. - -## Identifying entry points - -List functions and note which ones are externally callable: - -``` -Staking > f - deposit External writes state - withdraw External writes state - claimRewards External writes state - setRewardRate External writes state onlyOwner - pause External writes state onlyOwner - unpause External writes state onlyOwner - rewardPerToken Public read-only - earned Public read-only -``` - -Functions marked `External` accept parameters from untrusted callers. The `onlyOwner` annotation means the function has an access-control modifier, but the parameter values themselves are still caller-supplied. For taint analysis, focus on functions where user-controlled parameters flow into state writes or external calls. - -In this contract, `deposit(uint256 amount)` and `withdraw(uint256 amount)` both take an `amount` parameter from the caller and write state. - -## Forward-slicing amount in deposit - -Use the forward slice to trace where `amount` goes: - -``` -Staking > sl deposit amount --forward - Forward slice (amount -> sinks): - [0] require(amount > 0, "Cannot stake 0") - [1] stakingToken.transferFrom(msg.sender, address(this), amount) - [2] balances[msg.sender] += amount - [3] totalStaked += amount -``` - -The slice shows four statements that depend on `amount`: - -1. A `require` check validates that `amount` is positive. -2. An external call to `stakingToken.transferFrom` uses `amount` directly. -3. `balances[msg.sender]` is incremented by `amount`. -4. `totalStaked` is incremented by `amount`. - -The user-controlled value reaches two state variables (`balances`, `totalStaked`) and one external call (`transferFrom`). The `require` at line 0 is the only validation gate. - -## Forward-slicing amount in withdraw - -``` -Staking > sl withdraw amount --forward - Forward slice (amount -> sinks): - [0] require(amount > 0, "Cannot withdraw 0") - [1] require(balances[msg.sender] >= amount, "Insufficient balance") - [2] balances[msg.sender] -= amount - [3] totalStaked -= amount - [4] stakingToken.transfer(msg.sender, amount) -``` - -The `withdraw` slice is similar but has an additional check: `balances[msg.sender] >= amount`. This prevents withdrawing more than deposited. The subtraction operations mirror the additions in `deposit`. - -Compare the two slices side by side: - -| Statement type | deposit | withdraw | -|---|---|---| -| Validation | `amount > 0` | `amount > 0`, `balances >= amount` | -| External call | `transferFrom(sender, this, amount)` | `transfer(sender, amount)` | -| State writes | `balances += amount`, `totalStaked += amount` | `balances -= amount`, `totalStaked -= amount` | - -The asymmetry in validation is expected here: `deposit` relies on the ERC-20 `transferFrom` to enforce that the caller actually has the tokens, while `withdraw` must check the internal balance explicitly. - -## What the slice reveals about state variable control - -The forward slice of a user parameter tells you which state variables are directly controlled by external input. From the two slices above: - -- `balances` is written in both functions using `amount` directly. Any arithmetic error in the `+=` or `-=` operations would let an attacker manipulate their balance. -- `totalStaked` is written in both functions using `amount` directly. Since `rewardPerToken` reads `totalStaked` (visible via `who totalStaked`), a corrupted `totalStaked` would affect reward calculations for all users. -- Neither `rewardPerTokenStored` nor `rewards` appear in the forward slice of `amount`. These variables are written by the `updateReward` modifier, which derives values from `rewardPerToken()` and `earned()` -- not from the caller's `amount` parameter directly. - -## Mapping to vulnerability patterns - -Forward slice results map to common vulnerability classes: - -**Unchecked arithmetic.** If `balances[msg.sender] += amount` or `totalStaked += amount` can overflow, the attacker controls the input that triggers it. For Solidity 0.8+, the compiler inserts overflow checks automatically. For older versions, look for SafeMath usage in the slice. - -**Missing validation.** If the slice shows a state write or external call with no preceding `require` that bounds the parameter, the input flows unchecked. In `deposit`, the only check is `amount > 0` -- there is no upper bound. Whether this is a problem depends on the token's `transferFrom` behavior. - -**External call with user input.** Both slices show external calls (`transferFrom`, `transfer`) that use `amount`. If the token contract is untrusted or implements callbacks (e.g., ERC-777), the attacker controls the value passed to a potentially re-entrant call. Check whether the state writes happen before or after the external call (CEI pattern). - -**Cross-function impact.** Use `who` to find all readers of a tainted state variable, then forward-slice the reader to see downstream effects. For example, `rewardPerToken` reads `totalStaked`, so a manipulated `totalStaked` propagates into every user's reward calculation. - -## Practical workflow - -1. Run `f` to list entry points. -2. For each external function with parameters, run `sl --forward`. -3. Note which state variables appear in each forward slice. -4. Run `who ` on each affected state variable to find cross-function readers. -5. Run `sl --forward` to trace second-order propagation. -6. Record findings with `fi` when a tainted path reaches a sensitive sink without adequate validation. - -## Related pages - -- [Full Audit Walkthrough](./audit-walkthrough.md) -- [Known Limitations](../limitations.md) -- forward slice caveats diff --git a/tests/fixtures/erc20.sol b/tests/fixtures/erc20.sol deleted file mode 100644 index 503d7cf..0000000 --- a/tests/fixtures/erc20.sol +++ /dev/null @@ -1,55 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -contract ERC20 { - string public name; - string public symbol; - uint8 public decimals; - uint256 public totalSupply; - - mapping(address => uint256) public balanceOf; - mapping(address => mapping(address => uint256)) public allowance; - - event Transfer(address indexed from, address indexed to, uint256 value); - event Approval(address indexed owner, address indexed spender, uint256 value); - - constructor(string memory _name, string memory _symbol, uint8 _decimals) { - name = _name; - symbol = _symbol; - decimals = _decimals; - } - - function transfer(address to, uint256 amount) public returns (bool) { - require(to != address(0), "Transfer to zero address"); - require(balanceOf[msg.sender] >= amount, "Insufficient balance"); - - balanceOf[msg.sender] -= amount; - balanceOf[to] += amount; - - emit Transfer(msg.sender, to, amount); - return true; - } - - function approve(address spender, uint256 amount) public returns (bool) { - require(spender != address(0), "Approve to zero address"); - - allowance[msg.sender][spender] = amount; - - emit Approval(msg.sender, spender, amount); - return true; - } - - function transferFrom(address from, address to, uint256 amount) public returns (bool) { - require(from != address(0), "Transfer from zero address"); - require(to != address(0), "Transfer to zero address"); - require(balanceOf[from] >= amount, "Insufficient balance"); - require(allowance[from][msg.sender] >= amount, "Insufficient allowance"); - - balanceOf[from] -= amount; - balanceOf[to] += amount; - allowance[from][msg.sender] -= amount; - - emit Transfer(from, to, amount); - return true; - } -} diff --git a/tests/fixtures/governor.sol b/tests/fixtures/governor.sol deleted file mode 100644 index 98872b9..0000000 --- a/tests/fixtures/governor.sol +++ /dev/null @@ -1,140 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -interface IVotes { - function getVotes(address account) external view returns (uint256); -} - -contract TimelockController { - uint256 public delay; - mapping(bytes32 => bool) public executed; - address public admin; - - modifier onlyAdmin() { - require(msg.sender == admin, "Not admin"); - _; - } - - function schedule(bytes32 id, uint256 eta) external onlyAdmin { - require(eta >= block.timestamp + delay, "ETA too soon"); - } - - function execute(bytes32 id) external { - require(executed[id] == false, "Already executed"); - executed[id] = true; - } -} - -contract Governor is TimelockController { - IVotes public token; - uint256 public proposalThreshold; - uint256 public votingPeriod; - - struct Proposal { - address proposer; - uint256 startBlock; - uint256 endBlock; - uint256 forVotes; - uint256 againstVotes; - bool executed; - bool canceled; - } - - enum ProposalState { Pending, Active, Defeated, Succeeded, Executed, Canceled } - - mapping(uint256 => Proposal) public proposals; - mapping(uint256 => mapping(address => bool)) public hasVoted; - uint256 public proposalCount; - - event ProposalCreated(uint256 id, address proposer); - event VoteCast(address voter, uint256 proposalId, bool support, uint256 weight); - event ProposalExecuted(uint256 id); - - error ProposalNotActive(uint256 proposalId); - error AlreadyVoted(address voter, uint256 proposalId); - error InsufficientVotingPower(uint256 required, uint256 actual); - - modifier onlyActiveProposal(uint256 proposalId) { - require(state(proposalId) == ProposalState.Active, "Not active"); - _; - } - - function propose() external returns (uint256) { - require( - token.getVotes(msg.sender) >= proposalThreshold, - "Below threshold" - ); - - proposalCount++; - uint256 proposalId = proposalCount; - - proposals[proposalId] = Proposal({ - proposer: msg.sender, - startBlock: block.number + 1, - endBlock: block.number + 1 + votingPeriod, - forVotes: 0, - againstVotes: 0, - executed: false, - canceled: false - }); - - emit ProposalCreated(proposalId, msg.sender); - return proposalId; - } - - function castVote(uint256 proposalId, bool support) - external - onlyActiveProposal(proposalId) - { - require(!hasVoted[proposalId][msg.sender], "Already voted"); - - uint256 weight = token.getVotes(msg.sender); - require(weight > 0, "No voting power"); - - hasVoted[proposalId][msg.sender] = true; - - if (support) { - proposals[proposalId].forVotes += weight; - } else { - proposals[proposalId].againstVotes += weight; - } - - emit VoteCast(msg.sender, proposalId, support, weight); - } - - function executeProposal(uint256 proposalId) external { - require(state(proposalId) == ProposalState.Succeeded, "Not succeeded"); - - proposals[proposalId].executed = true; - emit ProposalExecuted(proposalId); - } - - function cancelProposal(uint256 proposalId) external { - Proposal storage proposal = proposals[proposalId]; - require(msg.sender == proposal.proposer, "Not proposer"); - require(!proposal.executed, "Already executed"); - - proposal.canceled = true; - } - - function state(uint256 proposalId) public view returns (ProposalState) { - Proposal storage proposal = proposals[proposalId]; - - if (proposal.canceled) { - return ProposalState.Canceled; - } - if (proposal.executed) { - return ProposalState.Executed; - } - if (block.number <= proposal.startBlock) { - return ProposalState.Pending; - } - if (block.number <= proposal.endBlock) { - return ProposalState.Active; - } - if (proposal.forVotes > proposal.againstVotes) { - return ProposalState.Succeeded; - } - return ProposalState.Defeated; - } -} diff --git a/tests/fixtures/multi/.gitignore b/tests/fixtures/multi/.gitignore deleted file mode 100644 index 97544b8..0000000 --- a/tests/fixtures/multi/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -out/ -cache/ -broadcast/ -.env diff --git a/tests/fixtures/multi/foundry.toml b/tests/fixtures/multi/foundry.toml deleted file mode 100644 index 41f3dfb..0000000 --- a/tests/fixtures/multi/foundry.toml +++ /dev/null @@ -1,5 +0,0 @@ -[profile.default] -src = "src" -out = "out" -libs = ["lib"] -solc_version = "0.8.20" diff --git a/tests/fixtures/multi/src/Helper.sol b/tests/fixtures/multi/src/Helper.sol deleted file mode 100644 index 205426c..0000000 --- a/tests/fixtures/multi/src/Helper.sol +++ /dev/null @@ -1,22 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -contract Helper { - uint256 public counter; - address public owner; - - constructor() { - owner = msg.sender; - } - - function add(uint256 amount) external returns (uint256) { - require(amount > 0, "zero"); - counter += amount; - return counter; - } - - function reset() external { - require(msg.sender == owner, "not owner"); - counter = 0; - } -} diff --git a/tests/fixtures/multi/src/Main.sol b/tests/fixtures/multi/src/Main.sol deleted file mode 100644 index 1c7368c..0000000 --- a/tests/fixtures/multi/src/Main.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -import {Helper} from "./Helper.sol"; - -contract Main { - Helper public helper; - uint256 public total; - - constructor(address _helper) { - helper = Helper(_helper); - } - - function bumpAndStore(uint256 amount) external returns (uint256) { - uint256 newCounter = helper.add(amount); - total = newCounter * 2; - return total; - } - - function clear() external { - helper.reset(); - total = 0; - } -} diff --git a/tests/fixtures/simple_storage.sol b/tests/fixtures/simple_storage.sol deleted file mode 100644 index 004afbd..0000000 --- a/tests/fixtures/simple_storage.sol +++ /dev/null @@ -1,18 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -contract SimpleStorage { - uint256 private value; - - event ValueChanged(uint256 newValue); - - function get() public view returns (uint256) { - return value; - } - - function set(uint256 newValue) public { - require(newValue > 0, "Value must be positive"); - value = newValue; - emit ValueChanged(newValue); - } -} diff --git a/tests/fixtures/staking.sol b/tests/fixtures/staking.sol deleted file mode 100644 index 9fc8d3e..0000000 --- a/tests/fixtures/staking.sol +++ /dev/null @@ -1,117 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -interface IERC20 { - function transfer(address to, uint256 amount) external returns (bool); - function transferFrom(address from, address to, uint256 amount) external returns (bool); - function balanceOf(address account) external view returns (uint256); -} - -contract Staking { - IERC20 public stakingToken; - IERC20 public rewardToken; - address public owner; - bool public paused; - - uint256 public rewardRate; - uint256 public lastUpdateTime; - uint256 public rewardPerTokenStored; - - mapping(address => uint256) public balances; - mapping(address => uint256) public userRewardPerTokenPaid; - mapping(address => uint256) public rewards; - uint256 public totalStaked; - - event Staked(address indexed user, uint256 amount); - event Withdrawn(address indexed user, uint256 amount); - event RewardPaid(address indexed user, uint256 reward); - - error InsufficientBalance(uint256 requested, uint256 available); - error ZeroAmount(); - - struct StakeInfo { - uint256 amount; - uint256 timestamp; - } - - enum Status { Active, Paused, Closed } - - modifier onlyOwner() { - require(msg.sender == owner, "Not owner"); - _; - } - - modifier whenNotPaused() { - require(!paused, "Contract is paused"); - _; - } - - modifier updateReward(address account) { - rewardPerTokenStored = rewardPerToken(); - lastUpdateTime = block.timestamp; - if (account != address(0)) { - rewards[account] = earned(account); - userRewardPerTokenPaid[account] = rewardPerTokenStored; - } - _; - } - - constructor(address _stakingToken, address _rewardToken) { - stakingToken = IERC20(_stakingToken); - rewardToken = IERC20(_rewardToken); - owner = msg.sender; - } - - function deposit(uint256 amount) external whenNotPaused updateReward(msg.sender) { - require(amount > 0, "Cannot stake 0"); - - stakingToken.transferFrom(msg.sender, address(this), amount); - balances[msg.sender] += amount; - totalStaked += amount; - - emit Staked(msg.sender, amount); - } - - function withdraw(uint256 amount) external updateReward(msg.sender) { - require(amount > 0, "Cannot withdraw 0"); - require(balances[msg.sender] >= amount, "Insufficient stake"); - - balances[msg.sender] -= amount; - totalStaked -= amount; - stakingToken.transfer(msg.sender, amount); - - emit Withdrawn(msg.sender, amount); - } - - function claimRewards() external updateReward(msg.sender) { - uint256 reward = rewards[msg.sender]; - if (reward > 0) { - rewards[msg.sender] = 0; - rewardToken.transfer(msg.sender, reward); - emit RewardPaid(msg.sender, reward); - } - } - - function setRewardRate(uint256 _rate) external onlyOwner { - rewardRate = _rate; - } - - function pause() external onlyOwner { - paused = true; - } - - function unpause() external onlyOwner { - paused = false; - } - - function rewardPerToken() public view returns (uint256) { - if (totalStaked == 0) { - return rewardPerTokenStored; - } - return rewardPerTokenStored + (rewardRate * (block.timestamp - lastUpdateTime) * 1e18 / totalStaked); - } - - function earned(address account) public view returns (uint256) { - return (balances[account] * (rewardPerToken() - userRewardPerTokenPaid[account]) / 1e18) + rewards[account]; - } -} diff --git a/tests/fixtures/uniswap_v2_pair.sol b/tests/fixtures/uniswap_v2_pair.sol deleted file mode 100644 index da956ba..0000000 --- a/tests/fixtures/uniswap_v2_pair.sol +++ /dev/null @@ -1,138 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -interface IERC20 { - function balanceOf(address) external view returns (uint256); - function transfer(address to, uint256 amount) external returns (bool); -} - -interface IFactory { - function feeTo() external view returns (address); -} - -contract UniswapV2Pair { - address public factory; - address public token0; - address public token1; - - uint112 private reserve0; - uint112 private reserve1; - uint32 private blockTimestampLast; - - uint256 public totalSupply; - mapping(address => uint256) public balanceOf; - - uint256 private unlocked = 1; - - event Mint(address indexed sender, uint256 amount0, uint256 amount1); - event Burn(address indexed sender, uint256 amount0, uint256 amount1, address indexed to); - event Swap(address indexed sender, uint256 amount0In, uint256 amount1In, uint256 amount0Out, uint256 amount1Out, address indexed to); - event Sync(uint112 reserve0, uint112 reserve1); - - modifier lock() { - require(unlocked == 1, "LOCKED"); - unlocked = 0; - _; - unlocked = 1; - } - - function getReserves() public view returns (uint112, uint112, uint32) { - return (reserve0, reserve1, blockTimestampLast); - } - - function mint(address to) external lock returns (uint256 liquidity) { - uint256 balance0 = IERC20(token0).balanceOf(address(this)); - uint256 balance1 = IERC20(token1).balanceOf(address(this)); - uint256 amount0 = balance0 - reserve0; - uint256 amount1 = balance1 - reserve1; - - if (totalSupply == 0) { - liquidity = _sqrt(amount0 * amount1); - require(liquidity > 0, "INSUFFICIENT_LIQUIDITY_MINTED"); - } else { - uint256 l0 = amount0 * totalSupply / reserve0; - uint256 l1 = amount1 * totalSupply / reserve1; - liquidity = l0 < l1 ? l0 : l1; - } - - require(liquidity > 0, "INSUFFICIENT_LIQUIDITY_MINTED"); - balanceOf[to] += liquidity; - totalSupply += liquidity; - - _update(balance0, balance1); - emit Mint(msg.sender, amount0, amount1); - } - - function swap(uint256 amount0Out, uint256 amount1Out, address to) external lock { - require(amount0Out > 0 || amount1Out > 0, "INSUFFICIENT_OUTPUT_AMOUNT"); - require(amount0Out < reserve0 && amount1Out < reserve1, "INSUFFICIENT_LIQUIDITY"); - require(to != token0 && to != token1, "INVALID_TO"); - - if (amount0Out > 0) { - IERC20(token0).transfer(to, amount0Out); - } - if (amount1Out > 0) { - IERC20(token1).transfer(to, amount1Out); - } - - uint256 balance0 = IERC20(token0).balanceOf(address(this)); - uint256 balance1 = IERC20(token1).balanceOf(address(this)); - - uint256 amount0In = balance0 > reserve0 - amount0Out ? balance0 - (reserve0 - amount0Out) : 0; - uint256 amount1In = balance1 > reserve1 - amount1Out ? balance1 - (reserve1 - amount1Out) : 0; - require(amount0In > 0 || amount1In > 0, "INSUFFICIENT_INPUT_AMOUNT"); - - // k invariant check - unchecked { - uint256 balance0Adjusted = balance0 * 1000 - amount0In * 3; - uint256 balance1Adjusted = balance1 * 1000 - amount1In * 3; - require(balance0Adjusted * balance1Adjusted >= uint256(reserve0) * uint256(reserve1) * 1000000, "K"); - } - - _update(balance0, balance1); - emit Swap(msg.sender, amount0In, amount1In, amount0Out, amount1Out, to); - } - - function flashLoan(uint256 amount0, uint256 amount1, address to, bytes calldata data) external lock { - require(amount0 > 0 || amount1 > 0, "INSUFFICIENT_AMOUNT"); - - if (amount0 > 0) { - IERC20(token0).transfer(to, amount0); - } - if (amount1 > 0) { - IERC20(token1).transfer(to, amount1); - } - - // Callback — attacker could re-enter here - (bool success,) = to.call(data); - require(success, "CALLBACK_FAILED"); - - // Verify repayment - uint256 balance0 = IERC20(token0).balanceOf(address(this)); - uint256 balance1 = IERC20(token1).balanceOf(address(this)); - require(balance0 >= reserve0 + amount0 * 3 / 1000, "INSUFFICIENT_REPAYMENT_0"); - require(balance1 >= reserve1 + amount1 * 3 / 1000, "INSUFFICIENT_REPAYMENT_1"); - - _update(balance0, balance1); - } - - function _update(uint256 balance0, uint256 balance1) private { - reserve0 = uint112(balance0); - reserve1 = uint112(balance1); - blockTimestampLast = uint32(block.timestamp); - emit Sync(reserve0, reserve1); - } - - function _sqrt(uint256 y) private pure returns (uint256 z) { - if (y > 3) { - z = y; - uint256 x = y / 2 + 1; - while (x < z) { - z = x; - x = (y / x + x) / 2; - } - } else if (y != 0) { - z = 1; - } - } -} From 2b30666356b764a6f9feda328303e1fac02ac717 Mon Sep 17 00:00:00 2001 From: scab24 Date: Sat, 16 May 2026 21:28:44 +0200 Subject: [PATCH 02/10] chore(cli): rewrite explore.rs as solana-only Drops the 3133-line dual REPL (handle_input, print_result, slicing/trace helpers, Solidity prompt help) and keeps only the Solana code path: handle_solana_input, dispatch_solana, kv parser, scenario sync and prompt. The standalone fmt.rs / interactive.rs modules now have no callers and will be removed next. --- crates/ilold-cli/src/explore.rs | 2344 +++++-------------------------- 1 file changed, 333 insertions(+), 2011 deletions(-) diff --git a/crates/ilold-cli/src/explore.rs b/crates/ilold-cli/src/explore.rs index b1e5fb7..6b60db2 100644 --- a/crates/ilold-cli/src/explore.rs +++ b/crates/ilold-cli/src/explore.rs @@ -1,123 +1,42 @@ use std::borrow::Cow; -use std::collections::HashMap; use std::path::PathBuf; use anyhow::Result; -use colored::Colorize; use reedline::{ Completer, DefaultHinter, FileBackedHistory, Prompt, PromptEditMode, PromptHistorySearch, PromptHistorySearchStatus, Reedline, Signal, Span, Suggestion, }; -use ilold_core::classify::entry_points::AccessLevel; -use ilold_core::exploration::commands::CommandResult; use ilold_solana_core::exploration::SolanaCommandResult; use ilold_solana_core::view::ProgramView; use crate::colors::*; use crate::fmt; -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum BackendKind { - Solidity, - Solana, - Attached, -} - -fn detect_backend_kind(state: &Option>) -> BackendKind { - match state { - None => BackendKind::Attached, - Some(s) => { - if s.solana().is_some() { - BackendKind::Solana - } else { - BackendKind::Solidity - } - } - } -} - -pub async fn run(paths: Vec, port: u16, max_seq_depth: usize, attach: Option) -> Result<()> { - if let Some(url) = attach { - let client = reqwest::Client::new(); - let map_resp = client - .get(format!("{url}/api/project/map")) - .send() - .await - .map_err(|e| anyhow::anyhow!("Cannot reach server at {url}: {e}"))?; - if !map_resp.status().is_success() { - anyhow::bail!("Server at {url} returned {}", map_resp.status()); - } - let project_map: serde_json::Value = map_resp.json().await?; - let kind = project_map["kind"].as_str().unwrap_or("solidity"); - - if kind == "solana" { - return run_solana_attach(url, client, project_map).await; - } - - let resp = client.get(format!("{url}/api/project")) - .send().await - .map_err(|e| anyhow::anyhow!("Cannot reach server at {url}: {e}"))?; - if !resp.status().is_success() { - anyhow::bail!("Server at {url} returned {}", resp.status()); - } - let project_info: serde_json::Value = resp.json().await?; - - let contracts_arr = project_info["contracts"].as_array(); - let contract_name = contracts_arr - .and_then(|arr| arr.iter().rev().find(|c| c["kind"].as_str() == Some("Contract"))) - .or_else(|| contracts_arr.and_then(|arr| arr.last())) - .and_then(|c| c["name"].as_str()) - .unwrap_or("unknown") - .to_string(); - - let mut functions_by_contract = HashMap::>::new(); - let contract_names_raw: Vec = contracts_arr - .map(|arr| arr.iter().filter_map(|c| c["name"].as_str().map(String::from)).collect()) - .unwrap_or_default(); - for cname in &contract_names_raw { - if let Ok(resp) = client.get(format!("{url}/api/contract/{cname}")).send().await { - if let Ok(detail) = resp.json::().await { - let funcs: Vec = detail["functions"].as_array() - .map(|fs| fs.iter().filter_map(|f| f["name"].as_str().map(String::from)).collect()) - .unwrap_or_default(); - functions_by_contract.insert(cname.clone(), funcs); - } - } - } - - let function_names = functions_by_contract.get(&contract_name).cloned().unwrap_or_default(); - let contract_names: Vec = functions_by_contract.keys().cloned().collect(); - let func_count = function_names.len(); - - let banner = fmt::header_box(&[ - &format!("ilold explore — {} (attached)", contract_name), - &format!("{} functions | Type ? for help", func_count), - &format!("Server: {}", url), - ]); - println!("{}\n", banner); - - let base_url = url; - let handle = tokio::runtime::Handle::current(); - let repl_thread = std::thread::spawn(move || { - repl_loop( - handle, - contract_name, - function_names, - contract_names, - None, - base_url, - Some(functions_by_contract), - BackendKind::Attached, - ); - }); - repl_thread.join().map_err(|_| anyhow::anyhow!("REPL thread panicked"))?; - return Ok(()); - } - - println!("Analyzing {} file(s)...", paths.len()); - let (state, actual_port) = ilold_web::start_server(paths, port, max_seq_depth).await?; - run_with_state(state, actual_port).await +pub async fn run( + _paths: Vec, + _port: u16, + _max_seq_depth: usize, + attach: Option, +) -> Result<()> { + let url = attach.ok_or_else(|| { + anyhow::anyhow!("explore::run requires --attach ; use explore::run_solana for local projects") + })?; + let client = reqwest::Client::new(); + let map_resp = client + .get(format!("{url}/api/project/map")) + .send() + .await + .map_err(|e| anyhow::anyhow!("Cannot reach server at {url}: {e}"))?; + if !map_resp.status().is_success() { + anyhow::bail!("Server at {url} returned {}", map_resp.status()); + } + let project_map: serde_json::Value = map_resp.json().await?; + let kind = project_map["kind"].as_str().unwrap_or("solana"); + if kind != "solana" { + anyhow::bail!("Only Solana servers are supported (got kind={kind})"); + } + run_solana_attach(url, client, project_map).await } async fn run_solana_attach( @@ -157,16 +76,7 @@ async fn run_solana_attach( let handle = tokio::runtime::Handle::current(); let repl_thread = std::thread::spawn(move || { - repl_loop( - handle, - program_name, - function_names, - program_names, - None, - url, - None, - BackendKind::Solana, - ); + repl_loop(handle, program_name, function_names, program_names, None, url); }); repl_thread .join() @@ -187,67 +97,24 @@ async fn run_with_state( state: std::sync::Arc, actual_port: u16, ) -> Result<()> { - let backend = detect_backend_kind(&Some(state.clone())); - - let (contract_name, function_names, contract_names, header_label) = match backend { - BackendKind::Solana => { - let s = state.solana().expect("solana backend"); - let program = s.project.programs.first(); - let name = program - .map(|p| p.name.clone()) - .unwrap_or_else(|| "unknown".into()); - let ix_names: Vec = program - .map(|p| p.instructions.iter().map(|i| i.name.clone()).collect()) - .unwrap_or_default(); - let program_names: Vec = s - .project - .programs - .iter() - .map(|p| p.name.clone()) - .collect(); - let label = format!("ilold explore — {} (solana)", name); - (name, ix_names, program_names, label) - } - _ => { - let s = state.unwrap_solidity(); - let name = s - .project - .find_contract(None) - .map(|c| c.name.clone()) - .unwrap_or_else(|_| "unknown".into()); - let function_names: Vec = s - .project - .contracts - .iter() - .find(|c| c.name == name) - .map(|c| { - s.project - .accessible_functions(c) - .iter() - .map(|af| af.function.name.clone()) - .collect() - }) - .unwrap_or_default(); - let contract_names: Vec = s - .project - .contracts - .iter() - .map(|c| c.name.clone()) - .filter(|n| !n.is_empty()) - .collect(); - let label = format!("ilold explore — {}", name); - (name, function_names, contract_names, label) - } - }; + let s = state.solana().expect("solana backend required"); + let program = s.project.programs.first(); + let program_name = program + .map(|p| p.name.clone()) + .unwrap_or_else(|| "unknown".into()); + let function_names: Vec = program + .map(|p| p.instructions.iter().map(|i| i.name.clone()).collect()) + .unwrap_or_default(); + let program_names: Vec = s + .project + .programs + .iter() + .map(|p| p.name.clone()) + .collect(); - let func_count = function_names.len(); - let func_label = match backend { - BackendKind::Solana => "instructions", - _ => "functions", - }; let banner = fmt::header_box(&[ - &header_label, - &format!("{} {} | Type ? for help", func_count, func_label), + &format!("ilold explore — {} (solana)", program_name), + &format!("{} instructions | Type ? for help", function_names.len()), &format!("Web UI: http://localhost:{}", actual_port), ]); println!("{}\n", banner); @@ -258,17 +125,17 @@ async fn run_with_state( let repl_thread = std::thread::spawn(move || { repl_loop( handle, - contract_name, + program_name, function_names, - contract_names, + program_names, Some(state_for_thread), base_url, - None, - backend, ); }); - repl_thread.join().map_err(|_| anyhow::anyhow!("REPL thread panicked"))?; + repl_thread + .join() + .map_err(|_| anyhow::anyhow!("REPL thread panicked"))?; Ok(()) } @@ -277,11 +144,9 @@ fn repl_loop( handle: tokio::runtime::Handle, mut contract: String, mut functions: Vec, - contract_names: Vec, + program_names: Vec, state: Option>, base_url: String, - functions_by_contract: Option>>, - backend: BackendKind, ) { let history_path = dirs::home_dir() .map(|h| h.join(".ilold").join("history")) @@ -297,7 +162,7 @@ fn repl_loop( let completer = std::sync::Arc::new(std::sync::Mutex::new(IloldCompleter { functions: functions.clone(), - contracts: contract_names.clone(), + contracts: program_names.clone(), scenarios: vec!["main".to_string()], })); @@ -318,44 +183,41 @@ fn repl_loop( scenario: scenario_name.clone(), }; - if state.is_none() { - if let Some(server_steps) = sync_steps(&handle, &client, &base_url, &contract, backend) { + let attached = state.is_none(); + if attached { + if let Some(server_steps) = sync_steps(&handle, &client, &base_url, &contract) { steps = server_steps; prompt.steps = steps.clone(); } - if backend == BackendKind::Solana { - if let Some(active) = sync_active_scenario(&handle, &client, &base_url, &contract) { - scenario_name = active; - prompt.scenario = scenario_name.clone(); - } - if let Some(scns) = sync_scenarios(&handle, &client, &base_url, &contract) { - if let Ok(mut comp) = completer.lock() { - comp.scenarios = scns; - } + if let Some(active) = sync_active_scenario(&handle, &client, &base_url, &contract) { + scenario_name = active; + prompt.scenario = scenario_name.clone(); + } + if let Some(scns) = sync_scenarios(&handle, &client, &base_url, &contract) { + if let Ok(mut comp) = completer.lock() { + comp.scenarios = scns; } } } loop { - if state.is_none() { - if let Some(server_steps) = sync_steps(&handle, &client, &base_url, &contract, backend) { + if attached { + if let Some(server_steps) = sync_steps(&handle, &client, &base_url, &contract) { if server_steps != steps { steps = server_steps; prompt.steps = steps.clone(); } } - if backend == BackendKind::Solana { - if let Some(active) = sync_active_scenario(&handle, &client, &base_url, &contract) { - if active != scenario_name { - scenario_name = active; - prompt.scenario = scenario_name.clone(); - } + if let Some(active) = sync_active_scenario(&handle, &client, &base_url, &contract) { + if active != scenario_name { + scenario_name = active; + prompt.scenario = scenario_name.clone(); } - if let Some(scns) = sync_scenarios(&handle, &client, &base_url, &contract) { - if let Ok(mut comp) = completer.lock() { - if comp.scenarios != scns { - comp.scenarios = scns; - } + } + if let Some(scns) = sync_scenarios(&handle, &client, &base_url, &contract) { + if let Ok(mut comp) = completer.lock() { + if comp.scenarios != scns { + comp.scenarios = scns; } } } @@ -364,11 +226,19 @@ fn repl_loop( match editor.read_line(&prompt) { Ok(Signal::Success(line)) => { let line = line.trim(); - if line.is_empty() { continue; } - - match handle_input( - line, &handle, &client, &base_url, &contract, - &mut steps, &mut scenario_name, &completer, &state, backend, + if line.is_empty() { + continue; + } + + match handle_solana_input( + line, + &handle, + &client, + &base_url, + &contract, + &mut steps, + &mut scenario_name, + &completer, ) { InputResult::Continue => {} InputResult::Quit => break, @@ -381,47 +251,17 @@ fn repl_loop( steps.clear(); scenario_name = "main".into(); if let Some(state) = state.as_ref() { - match backend { - BackendKind::Solana => { - if let Some(s) = state.solana() { - if let Some(p) = - s.project.programs.iter().find(|p| p.name == new_name) - { - functions = p - .instructions - .iter() - .map(|i| i.name.clone()) - .collect(); - if let Ok(mut comp) = completer.lock() { - comp.functions = functions.clone(); - } - } - } - } - _ => { - let s = state.unwrap_solidity(); - if let Some(c) = - s.project.contracts.iter().find(|c| c.name == new_name) - { - functions = s - .project - .accessible_functions(c) - .iter() - .map(|af| af.function.name.clone()) - .collect(); - if let Ok(mut comp) = completer.lock() { - comp.functions = functions.clone(); - } + if let Some(s) = state.solana() { + if let Some(p) = + s.project.programs.iter().find(|p| p.name == new_name) + { + functions = + p.instructions.iter().map(|i| i.name.clone()).collect(); + if let Ok(mut comp) = completer.lock() { + comp.functions = functions.clone(); } } } - } else if let Some(fbc) = functions_by_contract.as_ref() { - if let Some(funcs) = fbc.get(&new_name) { - functions = funcs.clone(); - if let Ok(mut comp) = completer.lock() { - comp.functions = functions.clone(); - } - } } prompt.contract = contract.clone(); prompt.steps = Vec::new(); @@ -452,12 +292,28 @@ struct CompleterWrapper(std::sync::Arc>); impl Completer for CompleterWrapper { fn complete(&mut self, line: &str, pos: usize) -> Vec { - self.0.lock().map(|mut c| c.complete(line, pos)).unwrap_or_default() + self.0 + .lock() + .map(|mut c| c.complete(line, pos)) + .unwrap_or_default() + } +} + +fn inline_help_target(cmd: &str, arg: &str) -> Option { + if cmd == "?" || cmd == "help" || cmd == "h" { + return None; + } + if cmd.ends_with('?') && cmd.len() > 1 { + return Some(cmd[..cmd.len() - 1].to_string()); } + if arg.trim() == "?" { + return Some(cmd.to_string()); + } + None } #[allow(clippy::too_many_arguments)] -fn handle_input( +fn handle_solana_input( line: &str, handle: &tokio::runtime::Handle, client: &reqwest::Client, @@ -466,800 +322,145 @@ fn handle_input( steps: &mut Vec, scenario_name: &mut String, completer: &std::sync::Arc>, - state: &Option>, - backend: BackendKind, ) -> InputResult { - if backend == BackendKind::Solana { - return handle_solana_input( - line, - handle, - client, - base_url, - contract, - steps, - scenario_name, - completer, - ); - } let normalized = split_numeric_suffix(line); let parts: Vec<&str> = normalized.splitn(2, ' ').collect(); let cmd = parts[0].to_lowercase(); let arg = parts.get(1).map(|s| s.trim()).unwrap_or(""); - if cmd.ends_with('?') && cmd.len() > 1 { - let base = &cmd[..cmd.len() - 1]; - print_inline_help(base); + if let Some(base) = inline_help_target(&cmd, arg) { + match crate::help::render_solana_help_block(&base) { + Some(text) => print!("{text}"), + None => println!( + " {} no help registered for {}", + c_danger("✗"), + c_accent(&base) + ), + } return InputResult::Continue; } match cmd.as_str() { - "?" | "h" | "help" => { print_help(); InputResult::Continue } - "q" | "quit" | "exit" => InputResult::Quit, - "browser" => { - println!(" {}", c_muted("Web UI not yet available in explore mode.")); - println!(" API running at {base_url}/api/"); + "?" | "help" | "h" => { + print_solana_help(); InputResult::Continue } - "sc" | "scen" | "scenario" => { - let sub_parts: Vec<&str> = arg.splitn(2, ' ').collect(); - let sub = sub_parts.first().copied().unwrap_or("").trim(); - let name_arg = sub_parts.get(1).map(|s| s.trim()).unwrap_or(""); - - use ilold_core::exploration::commands::ScenarioAction; - - let parse_fork = |raw: &str| -> Result { - let parts: Vec<&str> = raw.split_whitespace().collect(); - match parts.as_slice() { - [name] => Ok(ScenarioAction::Fork { - name: name.to_string(), - at_step: None, - }), - [name, "at", n_str] => n_str - .parse::() - .map(|n| ScenarioAction::Fork { - name: name.to_string(), - at_step: Some(n), - }) - .map_err(|_| format!( - "Invalid at-step: '{n_str}' is not a non-negative integer" - )), - _ => Err("Usage: scenario fork [at ]".to_string()), - } - }; - - let action: Option = match sub { - "new" if !name_arg.is_empty() => Some(ScenarioAction::New { name: name_arg.to_string() }), - "list" | "ls" => Some(ScenarioAction::List), - "switch" if !name_arg.is_empty() => Some(ScenarioAction::Switch { name: name_arg.to_string() }), - "fork" if !name_arg.is_empty() => match parse_fork(name_arg) { - Ok(a) => Some(a), - Err(msg) => { - eprintln!(" {}", c_danger(&msg)); - return InputResult::Continue; - } - }, - "delete" | "rm" if !name_arg.is_empty() => Some(ScenarioAction::Delete { name: name_arg.to_string() }), - _ => None, - }; - - let Some(action) = action else { - println!(" Usage: scenario new|list|switch|fork|delete "); + "quit" | "q" | "exit" => InputResult::Quit, + "funcs" | "functions" | "f" => dispatch_solana( + handle, + client, + base_url, + contract, + serde_json::json!("Funcs"), + steps, + ), + "funcs-all" | "fa" => dispatch_solana( + handle, + client, + base_url, + contract, + serde_json::json!("Funcs"), + steps, + ), + "info" | "i" => { + if arg.is_empty() { + println!(" Usage: info "); return InputResult::Continue; - }; - - let body = serde_json::json!({ - "contract": contract, - "command": { "Scenario": { "sub": action } } - }); - match send_command(handle, client, base_url, &body) { - Ok(result) => { - let mut did_update_scenario = false; - match &result { - CommandResult::ScenarioCreated { name } => { - if let Ok(mut comp) = completer.lock() { - if !comp.scenarios.iter().any(|s| s == name) { - comp.scenarios.push(name.clone()); - } - } - } - CommandResult::ScenarioForked { to, .. } => { - if let Ok(mut comp) = completer.lock() { - if !comp.scenarios.iter().any(|s| s == to) { - comp.scenarios.push(to.clone()); - } - } - } - CommandResult::ScenarioSwitched { from, to } if from != to => { - *scenario_name = to.clone(); - did_update_scenario = true; - } - CommandResult::ScenarioDeleted { name } => { - if let Ok(mut comp) = completer.lock() { - comp.scenarios.retain(|s| s != name); - } - } - CommandResult::ScenarioList { items } => { - if let Ok(mut comp) = completer.lock() { - comp.scenarios = items.iter().map(|i| i.name.clone()).collect(); - } - } - _ => {} - } - print_result(&result, steps); - if did_update_scenario { - return InputResult::UpdatePrompt; - } - } - Err(e) => eprintln!(" {}", c_danger(&e)), } - InputResult::Continue + let body = serde_json::json!({"Info": {"ix": arg}}); + dispatch_solana(handle, client, base_url, contract, body, steps) } - - "ct" | "contracts" | "programs" | "progs" => { - if let Some(state) = state { - match backend { - BackendKind::Solana => print_programs(state, contract), - _ => print_contracts(state, contract), + "vars" | "v" | "vars-all" | "va" => dispatch_solana( + handle, + client, + base_url, + contract, + serde_json::json!("Vars"), + steps, + ), + "coupling" | "cp" => dispatch_solana( + handle, + client, + base_url, + contract, + serde_json::json!("Coupling"), + steps, + ), + "coverage" | "cov" => dispatch_solana( + handle, + client, + base_url, + contract, + serde_json::json!("Coverage"), + steps, + ), + "state" => dispatch_solana( + handle, + client, + base_url, + contract, + serde_json::json!("State"), + steps, + ), + "session" | "s" => { + let r = dispatch_solana( + handle, + client, + base_url, + contract, + serde_json::json!("Session"), + steps, + ); + *scenario_name = sync_active_scenario(handle, client, base_url, contract) + .unwrap_or_else(|| scenario_name.clone()); + r + } + "back" => dispatch_solana( + handle, + client, + base_url, + contract, + serde_json::json!("Back"), + steps, + ), + "clear" => dispatch_solana( + handle, + client, + base_url, + contract, + serde_json::json!("Clear"), + steps, + ), + "users" => { + if arg.starts_with("new") { + let rest = arg.trim_start_matches("new").trim(); + if rest.is_empty() { + println!(" Usage: users new []"); + return InputResult::Continue; } + let parts: Vec<&str> = rest.split_whitespace().collect(); + let name = parts[0].to_string(); + let lamports: u64 = parts + .get(1) + .and_then(|s| s.parse().ok()) + .unwrap_or(10_000_000_000); + let body = serde_json::json!({"UsersNew": {"name": name, "lamports": lamports}}); + dispatch_solana(handle, client, base_url, contract, body, steps) } else { - match handle.block_on(async { - let resp = client.get(format!("{base_url}/api/project")).send().await?; - resp.json::().await - }) { - Ok(info) => { - if let Some(arr) = info["contracts"].as_array() { - println!(); - for c in arr { - let name = c["name"].as_str().unwrap_or("?"); - let marker = if name == contract { c_ok(" ← current").to_string() } else { String::new() }; - println!(" {} {}{}", c_accent("[C]"), name, marker); - } - println!(); - } - } - Err(e) => eprintln!(" {}", c_danger(&format!("Failed to fetch contracts: {e}"))), - } + dispatch_solana( + handle, + client, + base_url, + contract, + serde_json::json!("Users"), + steps, + ) } - InputResult::Continue } - "use" => { - if arg.is_empty() { - println!(" Usage: use "); - return InputResult::Continue; - } - if let Some(state) = state { - match backend { - BackendKind::Solana => { - let s = state.solana().expect("solana backend"); - match s.project.find_program(arg) { - Some(p) => { - let name = p.name.clone(); - if name == contract { - println!(" Already using {}", c_accent(&name)); - return InputResult::Continue; - } - println!(" {} Now using: {}", c_ok("✓"), c_accent(&name)); - if !steps.is_empty() { - println!( - " {}", - c_muted(&format!( - "Cleared {} step(s) from previous program", - steps.len() - )) - ); - } - InputResult::SwitchContract(name) - } - None => { - eprintln!(" {}", c_danger(&format!("program '{arg}' not found"))); - InputResult::Continue - } - } - } - _ => { - let s = state.unwrap_solidity(); - match s.project.find_contract(Some(arg)) { - Ok(c) => { - let name = c.name.clone(); - if name == contract { - println!(" Already using {}", c_accent(&name)); - return InputResult::Continue; - } - println!(" {} Now using: {}", c_ok("✓"), c_accent(&name)); - if !steps.is_empty() { - println!( - " {}", - c_muted(&format!( - "Cleared {} step(s) from previous contract", - steps.len() - )) - ); - } - InputResult::SwitchContract(name) - } - Err(e) => { - eprintln!(" {}", c_danger(&e)); - InputResult::Continue - } - } - } - } - } else { - let name = arg.to_string(); - if name == contract { - println!(" Already using {}", c_accent(&name)); - return InputResult::Continue; - } - println!(" {} Now using: {}", c_ok("✓"), c_accent(&name)); - if !steps.is_empty() { - println!(" {}", c_muted(&format!("Cleared {} step(s) from previous contract", steps.len()))); - } - InputResult::SwitchContract(name) - } - } - - "c" | "call" => { - if arg.is_empty() { - println!(" Usage: call "); - return InputResult::Continue; - } - let body = serde_json::json!({ - "contract": contract, - "command": { "Call": { "func": arg } } - }); - match send_command(handle, client, base_url, &body) { - Ok(result) => { - if let CommandResult::StepAdded { function, .. } = &result { - steps.push(function.clone()); - } - print_result(&result, steps); - if matches!(&result, CommandResult::StepAdded { .. }) { - return InputResult::UpdatePrompt; - } - } - Err(e) => eprintln!(" {}", c_danger(&e)), - } - InputResult::Continue - } - "b" | "back" => { - let body = serde_json::json!({ - "contract": contract, - "command": "Back" - }); - match send_command(handle, client, base_url, &body) { - Ok(result) => { - if matches!(&result, CommandResult::StepRemoved { .. }) { - steps.pop(); - } - print_result(&result, steps); - if matches!(&result, CommandResult::StepRemoved { .. }) { - return InputResult::UpdatePrompt; - } - } - Err(e) => eprintln!(" {}", c_danger(&e)), - } - InputResult::Continue - } - "cl" | "clear" => { - if !steps.is_empty() { - println!(" Clear {} steps? (y/n)", steps.len()); - let mut input = String::new(); - match std::io::stdin().read_line(&mut input) { - Ok(_) if input.trim().eq_ignore_ascii_case("y") => { /* proceed */ } - _ => { - println!(" Cancelled."); - return InputResult::Continue; - } - } - } - let body = serde_json::json!({ - "contract": contract, - "command": "Clear" - }); - match send_command(handle, client, base_url, &body) { - Ok(result) => { - print_result(&result, steps); - steps.clear(); - return InputResult::UpdatePrompt; - } - Err(e) => eprintln!(" {}", c_danger(&e)), - } - InputResult::Continue - } - "s" | "state" => { - send_and_print(handle, client, base_url, contract, "State", steps); - InputResult::Continue - } - "f" | "functions" => { - send_and_print(handle, client, base_url, contract, "Functions", steps); - InputResult::Continue - } - "fa" | "funcs-all" => { - send_and_print(handle, client, base_url, contract, "FunctionsAll", steps); - InputResult::Continue - } - "va" | "vars-all" => { - send_and_print(handle, client, base_url, contract, "StateVarsAll", steps); - InputResult::Continue - } - "ss" | "session" => { - send_and_print(handle, client, base_url, contract, "Session", steps); - InputResult::Continue - } - "n" | "note" => { - if arg.is_empty() { - println!(" Usage: note "); - return InputResult::Continue; - } - let body = serde_json::json!({ - "contract": contract, - "command": { "Note": { "text": arg } } - }); - match send_command(handle, client, base_url, &body) { - Ok(result) => print_result(&result, steps), - Err(e) => eprintln!(" {}", c_danger(&e)), - } - InputResult::Continue - } - "fi" | "finding" => { - if arg.is_empty() { - handle_finding_interactive(handle, client, base_url, contract, steps); - } else { - let finding_parts: Vec<&str> = arg.splitn(2, ' ').collect(); - if finding_parts.len() < 2 { - println!(" Usage: fi "); - println!(" Or just: fi (interactive mode)"); - } else { - let severity_input = finding_parts[0]; - let rest = finding_parts[1]; - match normalize_severity(severity_input) { - Some(severity) => { - let body = serde_json::json!({ - "contract": contract, - "command": { - "Finding": { - "severity": severity, - "title": rest, - "description": "" - } - } - }); - match send_command(handle, client, base_url, &body) { - Ok(result) => print_result(&result, steps), - Err(e) => eprintln!(" {}", c_danger(&e)), - } - } - None => { - println!(" {}", c_danger("Invalid severity. Valid: critical, high, medium, low, info")); - } - } - } - } - InputResult::Continue - } - "status" => { - let status_parts: Vec<&str> = arg.splitn(2, ' ').collect(); - if status_parts.len() < 2 { - println!(" Usage: status <function> <reviewed|suspicious|vulnerable|clean|inprogress|notreviewed>"); - return InputResult::Continue; - } - let normalized = match normalize_status(status_parts[1]) { - Some(s) => s, - None => { - println!(" {}", c_danger("Invalid status. Valid: reviewed, suspicious, vulnerable, clean, inprogress, notreviewed")); - return InputResult::Continue; - } - }; - let body = serde_json::json!({ - "contract": contract, - "command": { "Status": { "func": status_parts[0], "status": normalized } } - }); - match send_command(handle, client, base_url, &body) { - Ok(result) => print_result(&result, steps), - Err(e) => eprintln!(" {}", c_danger(&e)), - } - InputResult::Continue - } - "w" | "who" => { - if arg.is_empty() { - println!(" Usage: who <variable>"); - return InputResult::Continue; - } - let body = serde_json::json!({ - "contract": contract, - "command": { "Who": { "variable": arg } } - }); - match send_command(handle, client, base_url, &body) { - Ok(result) => print_result(&result, steps), - Err(e) => eprintln!(" {}", c_danger(&e)), - } - InputResult::Continue - } - - "v" | "vars" => { - match send_get(handle, client, &format!("{base_url}/api/contract/{contract}")) { - Ok(val) => print_vars(&val), - Err(e) => eprintln!(" {}", c_danger(&e)), - } - InputResult::Continue - } - - "i" | "info" => { - if arg.is_empty() { - println!(" Usage: info <function>"); - return InputResult::Continue; - } - match send_get(handle, client, &format!("{base_url}/api/session/function/{contract}/{arg}")) { - Ok(val) => print_narrative(&val), - Err(e) => eprintln!(" {}", c_danger(&e)), - } - InputResult::Continue - } - "tr" | "trace" => { - if arg.is_empty() { - println!(" Usage: trace <function> [--depth N] [--reverts] [+N...] [-i]"); - println!(" trace step <N>"); - return InputResult::Continue; - } - let parsed = parse_trace_args(arg); - let target = match parsed.target { - Some(t) => t, - None => { - println!(" Usage: trace <function> [--depth N] [--reverts] [+N...] [-i]"); - println!(" trace step <N>"); - return InputResult::Continue; - } - }; - let url = match target { - TraceTarget::Function(func_name) => { - let mut url = format!("{base_url}/api/session/trace/{contract}/{func_name}"); - let mut sep = '?'; - let effective_depth = parsed.depth - .or(if parsed.interactive { Some(4) } else { None }); - if let Some(d) = effective_depth { - url.push_str(&format!("{sep}depth={d}")); - sep = '&'; - } - if parsed.reverts { - url.push_str(&format!("{sep}reverts=true")); - sep = '&'; - } - if !parsed.expand.is_empty() { - let csv = parsed.expand.iter() - .map(|n| n.to_string()) - .collect::<Vec<_>>() - .join(","); - url.push_str(&format!("{sep}expand={csv}")); - } - url - } - TraceTarget::SessionStep(idx) => { - format!("{base_url}/api/session/step/{idx}/trace") - } - }; - match send_get(handle, client, &url) { - Ok(val) => match serde_json::from_value::<ilold_core::narrative::trace::FlowTree>(val) { - Ok(tree) => { - if parsed.interactive { - if let Err(e) = crate::interactive::run_trace_viewer(tree) { - eprintln!(" {} interactive viewer: {}", c_danger("✗"), e); - } - } else { - print!("{}", fmt::render_flow_tree(&tree)); - } - } - Err(e) => eprintln!(" {} Parse FlowTree: {}", c_danger("✗"), e), - }, - Err(e) => eprintln!(" {}", c_danger(&e)), - } - InputResult::Continue - } - "seq" | "sequence" => { - match send_get(handle, client, &format!("{base_url}/api/session/sequence")) { - Ok(val) => print_sequence_narrative(&val), - Err(e) => eprintln!(" {}", c_danger(&e)), - } - InputResult::Continue - } - "tl" | "timeline" => { - if arg.is_empty() { - println!(" Usage: timeline <variable>"); - return InputResult::Continue; - } - match send_get(handle, client, &format!("{base_url}/api/session/timeline/{arg}")) { - Ok(val) => match serde_json::from_value::<ilold_core::exploration::timeline::VariableTimeline>(val) { - Ok(tl) => print!("{}", fmt::render_variable_timeline(&tl)), - Err(e) => eprintln!(" {} Parse VariableTimeline: {}", c_danger("✗"), e), - }, - Err(e) => eprintln!(" {}", c_danger(&e)), - } - InputResult::Continue - } - "sl" | "slice" => { - let parts: Vec<&str> = arg.split_whitespace().collect(); - let mut positionals: Vec<&str> = Vec::new(); - let mut direction: Option<&str> = None; - for part in &parts { - match *part { - "--backward" | "-b" => direction = Some("backward"), - "--forward" | "-f" => direction = Some("forward"), - "--both" => direction = Some("both"), - _ => positionals.push(part), - } - } - if positionals.len() < 2 { - println!(" Usage: slice <function> <variable> [--backward|--forward|--both]"); - return InputResult::Continue; - } - let func_name = positionals[0]; - let var_name = positionals[1]; - let mut url = format!("{base_url}/api/session/slice/{func_name}/{var_name}"); - if let Some(d) = direction { - url.push_str(&format!("?direction={d}")); - } - match send_get(handle, client, &url) { - Ok(val) => match serde_json::from_value::<ilold_core::slicing::SliceResult>(val) { - Ok(res) => print!("{}", fmt::render_slice_result(&res)), - Err(e) => eprintln!(" {} Parse SliceResult: {}", c_danger("✗"), e), - }, - Err(e) => eprintln!(" {}", c_danger(&e)), - } - InputResult::Continue - } - "st" | "step" => { - if arg.is_empty() { - println!(" Usage: step <index>"); - return InputResult::Continue; - } - match send_get(handle, client, &format!("{base_url}/api/session/step/{arg}/narrative")) { - Ok(val) => print_narrative(&val), - Err(e) => eprintln!(" {}", c_danger(&e)), - } - InputResult::Continue - } - - "fl" | "findings" => { - print_findings_list(handle, client, base_url, contract); - InputResult::Continue - } - "ex" | "export" => { - let body = serde_json::json!({ "contract": contract, "command": "Export" }); - match send_command(handle, client, base_url, &body) { - Ok(CommandResult::Exported { markdown }) => { - let filename = format!("ilold-report-{}.md", contract); - match std::fs::write(&filename, &markdown) { - Ok(_) => println!(" {} Exported to {}", c_ok("✓"), c_accent(&filename)), - Err(e) => eprintln!(" {} Failed to write: {}", c_danger("✗"), e), - } - } - Ok(other) => print_result(&other, steps), - Err(e) => eprintln!(" {}", c_danger(&e)), - } - InputResult::Continue - } - - "save" => { - if arg.is_empty() { - println!(" Usage: save <name>"); - return InputResult::Continue; - } - let body = serde_json::json!({ "contract": contract, "command": "SaveSession" }); - match send_command(handle, client, base_url, &body) { - Ok(CommandResult::SessionSaved { json }) => { - let dir = dirs::home_dir() - .map(|h| h.join(".ilold").join("sessions")) - .unwrap_or_else(|| std::path::PathBuf::from(".ilold/sessions")); - std::fs::create_dir_all(&dir).ok(); - let path = dir.join(format!("{}.json", arg)); - match std::fs::write(&path, &json) { - Ok(_) => println!(" {} Saved to {}", c_ok("✓"), c_accent(&path.display().to_string())), - Err(e) => eprintln!(" {} Write failed: {}", c_danger("✗"), e), - } - } - Ok(other) => print_result(&other, steps), - Err(e) => eprintln!(" {}", c_danger(&e)), - } - InputResult::Continue - } - "load" => { - if arg.is_empty() { - println!(" Usage: load <name>"); - return InputResult::Continue; - } - let dir = dirs::home_dir() - .map(|h| h.join(".ilold").join("sessions")) - .unwrap_or_else(|| std::path::PathBuf::from(".ilold/sessions")); - let path = dir.join(format!("{}.json", arg)); - let json = match std::fs::read_to_string(&path) { - Ok(j) => j, - Err(e) => { - eprintln!(" {} File not found: {} ({})", c_danger("✗"), path.display(), e); - return InputResult::Continue; - } - }; - let body = serde_json::json!({ "contract": contract, "command": { "LoadSession": { "json": json } } }); - match send_command(handle, client, base_url, &body) { - Ok(CommandResult::SessionLoaded { steps: loaded_steps, .. }) => { - *steps = loaded_steps; - println!(" {} Session loaded ({} steps)", c_ok("✓"), steps.len()); - return InputResult::UpdatePrompt; - } - Ok(other) => print_result(&other, steps), - Err(e) => eprintln!(" {}", c_danger(&e)), - } - InputResult::Continue - } - - _ => { - println!(" Unknown command: {}. Type {} for help.", c_danger(cmd.as_str()), c_accent("?")); - InputResult::Continue - } - } -} - -fn inline_help_target(cmd: &str, arg: &str) -> Option<String> { - if cmd == "?" || cmd == "help" || cmd == "h" { - return None; - } - if cmd.ends_with('?') && cmd.len() > 1 { - return Some(cmd[..cmd.len() - 1].to_string()); - } - if arg.trim() == "?" { - return Some(cmd.to_string()); - } - None -} - -#[allow(clippy::too_many_arguments)] -fn handle_solana_input( - line: &str, - handle: &tokio::runtime::Handle, - client: &reqwest::Client, - base_url: &str, - contract: &str, - steps: &mut Vec<String>, - scenario_name: &mut String, - completer: &std::sync::Arc<std::sync::Mutex<IloldCompleter>>, -) -> InputResult { - let parts: Vec<&str> = line.splitn(2, ' ').collect(); - let cmd = parts[0].to_lowercase(); - let arg = parts.get(1).map(|s| s.trim()).unwrap_or(""); - - if let Some(base) = inline_help_target(&cmd, arg) { - match crate::help::render_solana_help_block(&base) { - Some(text) => print!("{text}"), - None => println!( - " {} no help registered for {}", - c_danger("✗"), - c_accent(&base) - ), - } - return InputResult::Continue; - } - - match cmd.as_str() { - "?" | "help" | "h" => { - print_solana_help(); - InputResult::Continue - } - "quit" | "q" | "exit" => InputResult::Quit, - "funcs" | "functions" | "f" => { - dispatch_solana( - handle, - client, - base_url, - contract, - serde_json::json!("Funcs"), - steps, - ) - } - "funcs-all" | "fa" => dispatch_solana( - handle, - client, - base_url, - contract, - serde_json::json!("Funcs"), - steps, - ), - "info" | "i" => { - if arg.is_empty() { - println!(" Usage: info <instruction>"); - return InputResult::Continue; - } - let body = serde_json::json!({"Info": {"ix": arg}}); - dispatch_solana(handle, client, base_url, contract, body, steps) - } - "vars" | "v" | "vars-all" | "va" => dispatch_solana( - handle, - client, - base_url, - contract, - serde_json::json!("Vars"), - steps, - ), - "coupling" | "cp" => dispatch_solana( - handle, - client, - base_url, - contract, - serde_json::json!("Coupling"), - steps, - ), - "coverage" | "cov" => dispatch_solana( - handle, - client, - base_url, - contract, - serde_json::json!("Coverage"), - steps, - ), - "state" => dispatch_solana( - handle, - client, - base_url, - contract, - serde_json::json!("State"), - steps, - ), - "session" | "s" => { - let r = dispatch_solana( - handle, - client, - base_url, - contract, - serde_json::json!("Session"), - steps, - ); - *scenario_name = sync_active_scenario(handle, client, base_url, contract) - .unwrap_or_else(|| scenario_name.clone()); - r - } - "back" => dispatch_solana( - handle, - client, - base_url, - contract, - serde_json::json!("Back"), - steps, - ), - "clear" => dispatch_solana( - handle, - client, - base_url, - contract, - serde_json::json!("Clear"), - steps, - ), - "users" => { - if arg.starts_with("new") { - let rest = arg.trim_start_matches("new").trim(); - if rest.is_empty() { - println!(" Usage: users new <name> [<lamports>]"); - return InputResult::Continue; - } - let parts: Vec<&str> = rest.split_whitespace().collect(); - let name = parts[0].to_string(); - let lamports: u64 = parts - .get(1) - .and_then(|s| s.parse().ok()) - .unwrap_or(10_000_000_000); - let body = serde_json::json!({"UsersNew": {"name": name, "lamports": lamports}}); - dispatch_solana(handle, client, base_url, contract, body, steps) - } else { - dispatch_solana( - handle, - client, - base_url, - contract, - serde_json::json!("Users"), - steps, - ) - } - } - "airdrop" | "air" => { - let parts: Vec<&str> = arg.split_whitespace().collect(); - if parts.len() != 2 { - println!(" Usage: airdrop <name> <lamports>"); + "airdrop" | "air" => { + let parts: Vec<&str> = arg.split_whitespace().collect(); + if parts.len() != 2 { + println!(" Usage: airdrop <name> <lamports>"); return InputResult::Continue; } let lamports: u64 = match parts[1].parse() { @@ -1303,9 +504,7 @@ fn handle_solana_input( "call" | "c" => { let parts: Vec<&str> = arg.splitn(2, ' ').collect(); if parts.is_empty() || parts[0].is_empty() { - println!( - " Usage: call <ix> [arg=val ...] [account=user_or_pubkey ...]" - ); + println!(" Usage: call <ix> [arg=val ...] [account=user_or_pubkey ...]"); println!(" or: call <ix> {{json}} for full control"); return InputResult::Continue; } @@ -1361,6 +560,13 @@ fn handle_solana_input( } InputResult::Continue } + "use" => { + if arg.is_empty() { + println!(" Usage: use <program>"); + return InputResult::Continue; + } + InputResult::SwitchContract(arg.to_string()) + } "sc" | "scenario" => { let parts: Vec<&str> = arg.split_whitespace().collect(); let sub = parts.first().copied().unwrap_or(""); @@ -1385,9 +591,7 @@ fn handle_solana_input( let action = match action { Some(a) => a, None => { - println!( - " Usage: scenario new|list|switch|fork|delete <name> [step]" - ); + println!(" Usage: scenario new|list|switch|fork|delete <name> [step]"); return InputResult::Continue; } }; @@ -1449,21 +653,16 @@ fn handle_solana_input( }); dispatch_solana(handle, client, base_url, contract, body, steps) } - "seq" | "sequence" => { - dispatch_solana( - handle, - client, - base_url, - contract, - serde_json::json!("Session"), - steps, - ) - } + "seq" | "sequence" => dispatch_solana( + handle, + client, + base_url, + contract, + serde_json::json!("Session"), + steps, + ), "browser" => { - println!( - " {} Web UI not yet available in explore mode.", - c_muted("·") - ); + println!(" {} Web UI not yet available in explore mode.", c_muted("·")); println!(" {} API running at {}/api/", c_muted("·"), base_url); InputResult::Continue } @@ -1489,9 +688,7 @@ fn handle_solana_input( if tok == "--with-keypairs" { with_keypairs = true; } else if tok.starts_with("--") { - println!( - " Unknown flag: {tok}. Use --with-keypairs (or no flags)." - ); + println!(" Unknown flag: {tok}. Use --with-keypairs (or no flags)."); return InputResult::Continue; } else if name.is_none() { name = Some(tok); @@ -1597,11 +794,16 @@ fn handle_solana_input( let mut version: Option<String> = None; let mut date: Option<String> = None; for tok in arg.split_whitespace() { - if let Some(v) = tok.strip_prefix("--auditor=") { auditor = Some(strip_quotes(v).to_string()); } - else if let Some(v) = tok.strip_prefix("--version=") { version = Some(strip_quotes(v).to_string()); } - else if let Some(v) = tok.strip_prefix("--date=") { date = Some(strip_quotes(v).to_string()); } - else { - println!(" Unknown flag: {tok}. Use --auditor= / --version= / --date= (or no flags)"); + if let Some(v) = tok.strip_prefix("--auditor=") { + auditor = Some(strip_quotes(v).to_string()); + } else if let Some(v) = tok.strip_prefix("--version=") { + version = Some(strip_quotes(v).to_string()); + } else if let Some(v) = tok.strip_prefix("--date=") { + date = Some(strip_quotes(v).to_string()); + } else { + println!( + " Unknown flag: {tok}. Use --auditor= / --version= / --date= (or no flags)" + ); return InputResult::Continue; } } @@ -1648,8 +850,7 @@ fn handle_solana_input( return InputResult::Continue; } }; - let body = - serde_json::json!({"Status": {"ix": parts[0], "status": st}}); + let body = serde_json::json!({"Status": {"ix": parts[0], "status": st}}); dispatch_solana(handle, client, base_url, contract, body, steps) } _ => { @@ -1976,82 +1177,14 @@ fn sync_steps( client: &reqwest::Client, base_url: &str, contract: &str, - backend: BackendKind, -) -> Option<Vec<String>> { - let body = serde_json::json!({ - "contract": contract, - "command": "Session" - }); - if backend == BackendKind::Solana { - match send_solana_command(handle, client, base_url, &body) { - Ok(SolanaCommandResult::SessionView { steps, .. }) => Some(steps), - _ => None, - } - } else { - match send_command(handle, client, base_url, &body) { - Ok(CommandResult::SessionView { steps, .. }) => Some(steps), - _ => None, - } - } -} - -fn send_command( - handle: &tokio::runtime::Handle, - client: &reqwest::Client, - base_url: &str, - body: &serde_json::Value, -) -> Result<CommandResult, String> { - handle.block_on(async { - client.post(format!("{base_url}/api/cmd")) - .json(body) - .send() - .await - .map_err(|e| format!("Request failed: {e}"))? - .json::<CommandResult>() - .await - .map_err(|e| format!("Parse failed: {e}")) - }) -} - -fn send_get( - handle: &tokio::runtime::Handle, - client: &reqwest::Client, - url: &str, -) -> Result<serde_json::Value, String> { - handle.block_on(async { - let resp = client.get(url) - .send() - .await - .map_err(|e| format!("Request failed: {e}"))?; - - if !resp.status().is_success() { - let status = resp.status(); - let body = resp.text().await.unwrap_or_default(); - return Err(if body.is_empty() { - format!("Server error: {status}") - } else { - body - }); - } - - resp.json::<serde_json::Value>() - .await - .map_err(|e| format!("Parse failed: {e}")) - }) -} - -fn send_and_print( - handle: &tokio::runtime::Handle, - client: &reqwest::Client, - base_url: &str, - contract: &str, - command: &str, - steps: &[String], -) { - let body = serde_json::json!({ "contract": contract, "command": command }); - match send_command(handle, client, base_url, &body) { - Ok(result) => print_result(&result, steps), - Err(e) => eprintln!(" {}", c_danger(&e)), +) -> Option<Vec<String>> { + let body = serde_json::json!({ + "contract": contract, + "command": "Session" + }); + match send_solana_command(handle, client, base_url, &body) { + Ok(SolanaCommandResult::SessionView { steps, .. }) => Some(steps), + _ => None, } } @@ -2073,64 +1206,6 @@ fn split_numeric_suffix(line: &str) -> String { line.to_string() } -enum TraceTarget { - Function(String), - SessionStep(usize), -} - -struct TraceArgs { - target: Option<TraceTarget>, - depth: Option<usize>, - reverts: bool, - expand: Vec<usize>, - interactive: bool, -} - -fn parse_trace_args(arg: &str) -> TraceArgs { - let tokens: Vec<&str> = arg.split_whitespace().collect(); - let mut target: Option<TraceTarget> = None; - let mut depth: Option<usize> = None; - let mut reverts = false; - let mut expand: Vec<usize> = Vec::new(); - let mut interactive = false; - let mut i = 0; - while i < tokens.len() { - let t = tokens[i]; - if t == "--depth" { - if let Some(v) = tokens.get(i + 1).and_then(|s| s.parse::<usize>().ok()) { - depth = Some(v); - i += 2; - continue; - } - i += 1; - } else if t == "--reverts" { - reverts = true; - i += 1; - } else if t == "-i" || t == "--interactive" { - interactive = true; - i += 1; - } else if let Some(rest) = t.strip_prefix('+') { - if let Ok(id) = rest.parse::<usize>() { - expand.push(id); - } - i += 1; - } else if t == "step" - && target.is_none() - && tokens.get(i + 1).and_then(|s| s.parse::<usize>().ok()).is_some() - { - let idx = tokens[i + 1].parse::<usize>().unwrap(); - target = Some(TraceTarget::SessionStep(idx)); - i += 2; - } else if target.is_none() { - target = Some(TraceTarget::Function(t.to_string())); - i += 1; - } else { - i += 1; - } - } - TraceArgs { target, depth, reverts, expand, interactive } -} - fn strip_quotes(s: &str) -> &str { let s = s.trim(); if (s.starts_with('"') && s.ends_with('"') && s.len() >= 2) @@ -2153,74 +1228,6 @@ fn normalize_severity(input: &str) -> Option<&'static str> { } } -fn normalize_status(input: &str) -> Option<&'static str> { - match input.to_lowercase().as_str() { - "reviewed" => Some("Reviewed"), - "suspicious" => Some("Suspicious"), - "vulnerable" => Some("Vulnerable"), - "clean" => Some("Clean"), - "inprogress" => Some("InProgress"), - "notreviewed" => Some("NotReviewed"), - _ => None, - } -} - -fn read_prompt(label: &str) -> Option<String> { - println!(" {} {}", label, c_muted("(empty to cancel)")); - print!(" > "); - std::io::Write::flush(&mut std::io::stdout()).ok(); - let mut input = String::new(); - match std::io::stdin().read_line(&mut input) { - Ok(0) | Err(_) => None, - Ok(_) => { - let trimmed = input.trim(); - if trimmed.is_empty() { None } else { Some(trimmed.to_string()) } - } - } -} - -fn handle_finding_interactive( - handle: &tokio::runtime::Handle, - client: &reqwest::Client, - base_url: &str, - contract: &str, - steps: &[String], -) { - let severity_input = match read_prompt("Severity (critical/high/medium/low/info):") { - Some(s) => s, - None => { println!(" {}", c_muted("Cancelled.")); return; } - }; - let severity = match normalize_severity(&severity_input) { - Some(s) => s.to_string(), - None => { - println!(" {}", c_danger("Invalid severity. Valid: critical, high, medium, low, info")); - return; - } - }; - - let title = match read_prompt("Title:") { - Some(t) => t, - None => { println!(" {}", c_muted("Cancelled.")); return; } - }; - - let description = read_prompt("Description (optional):").unwrap_or_default(); - - let body = serde_json::json!({ - "contract": contract, - "command": { - "Finding": { - "severity": severity, - "title": title, - "description": description - } - } - }); - match send_command(handle, client, base_url, &body) { - Ok(result) => print_result(&result, steps), - Err(e) => eprintln!(" {}", c_danger(&e)), - } -} - fn print_remote_programs(map: &serde_json::Value, current: &str) { let arr = match map.get("programs").and_then(|v| v.as_array()) { Some(a) => a, @@ -2260,708 +1267,60 @@ fn print_remote_programs(map: &serde_json::Value, current: &str) { println!(); } -fn print_programs(state: &std::sync::Arc<ilold_web::state::AppState>, current: &str) { - let s = match state.solana() { - Some(s) => s, - None => return, - }; - println!(); - if s.project.programs.is_empty() { - println!(" {}", c_muted("No programs detected")); - println!(); - return; - } - for p in &s.project.programs { - let marker = if p.name == current { - c_ok(" ← current").to_string() - } else { - String::new() - }; - println!( - " {} {} {} {}{}", - c_accent("[P]"), - p.name, - c_muted(&format!("({} ix)", p.instructions.len())), - c_muted(&p.program_id.to_string()), - marker - ); - } - println!(); -} - -fn print_contracts(state: &std::sync::Arc<ilold_web::state::AppState>, current: &str) { - use ilold_core::model::contract::ContractKind; - let s = state.unwrap_solidity(); - println!(); - let max_name = s.project.contracts.iter() - .filter(|c| !c.name.is_empty()) - .map(|c| c.name.chars().count()) - .max().unwrap_or(0); - - for c in &s.project.contracts { - if c.name.is_empty() { continue; } - let badge = match c.kind { - ContractKind::Contract => c_accent("[C]"), - ContractKind::Interface => c_muted("[I]"), - ContractKind::Library => c_muted("[L]"), - ContractKind::Abstract => c_warn("[A]"), - }; - let marker = if c.name == current { - c_ok(" ← current").to_string() - } else { - String::new() - }; - let padded = fmt::pad_right(&c.name, max_name); - let details = format!( - "{} functions, {} state vars", - c.functions.iter().filter(|f| !f.name.is_empty()).count(), - c.state_vars.len(), - ); - let inherits = if c.inherits.is_empty() { - String::new() - } else { - format!(", inherits {}", c.inherits.join(", ")) - }; - println!(" {} {} {}{}{}", badge, c_accent(&padded), c_muted(&details), c_muted(&inherits), marker); - } - println!(); -} - -fn print_findings_list( - handle: &tokio::runtime::Handle, - client: &reqwest::Client, - base_url: &str, - contract: &str, -) { - let body = serde_json::json!({ "contract": contract, "command": "Session" }); - match send_command(handle, client, base_url, &body) { - Ok(CommandResult::SessionView { findings_count, .. }) => { - if findings_count == 0 { - println!(" No findings recorded yet."); - } else { - println!(" {} finding(s) recorded. Use {} to export.", findings_count, c_accent("export")); - } - } - _ => println!(" Could not retrieve findings."), - } -} - -fn print_result(result: &CommandResult, steps: &[String]) { - match result { - CommandResult::StepAdded { step_index, function, access, state_changed } => { - let badge = access_colored(access); - println!(); - println!(" {} Step {}: {} {} {}", c_ok("+"), step_index, c_bright(function), badge, format_access_detail(access)); - if !state_changed.is_empty() { - println!(" {}:", c_muted("State writes")); - for var in state_changed { - println!(" {} {}", c_muted("·"), c_warn(var)); - } - } - let seq_str = steps.iter().map(|s| s.as_str()).collect::<Vec<_>>().join(" → "); - println!(" {}: {}", c_muted("Sequence"), c_accent(&seq_str)); - println!(); - } - CommandResult::StepRemoved { remaining } => { - println!(); - println!(" {} Step removed. {} remaining.", c_warn("-"), remaining); - if !steps.is_empty() { - let seq_str = steps.iter().map(|s| s.as_str()).collect::<Vec<_>>().join(" → "); - println!(" {}: {}", c_muted("Sequence"), c_accent(&seq_str)); - } - println!(); - } - CommandResult::Cleared => { - println!(" {}", c_ok("Session cleared.")); - } - CommandResult::StateView { summary } => { - if summary.is_empty() { - println!(" No state changes yet. Use {} to add steps.", c_accent("call <func>")); - return; - } - println!(); - println!("{}", fmt::separator("STATE")); - for var in summary { - println!(" {} {}", c_bright(&var.variable), ""); - for change in &var.changes { - println!(" {}", c_muted(change)); - } - } - println!(); - } - CommandResult::FunctionList { functions } => { - println!(); - let max_name = functions.iter() - .filter(|f| !f.name.is_empty()) - .map(|f| f.name.chars().count()) - .max().unwrap_or(0); - for entry in functions { - if entry.name.is_empty() { continue; } - let badge = access_colored(&entry.access); - let padded_name = fmt::pad_right(&entry.name, max_name); - let mut tags: Vec<&str> = Vec::new(); - if entry.writes_state { tags.push("writes state"); } - if entry.has_external_calls { tags.push("external calls"); } - if entry.is_read_only { tags.push("view"); } - let tag_str = if tags.is_empty() { - String::new() - } else { - format!(" {}", c_muted(&tags.join(", "))) - }; - println!(" {} {}{}", badge, c_accent(&padded_name), tag_str); - } - println!(); - } - CommandResult::FunctionListAll { functions } => { - println!(); - let max_name = functions.iter() - .filter(|f| !f.name.is_empty()) - .map(|f| f.name.chars().count()) - .max().unwrap_or(0); - - let own: Vec<_> = functions.iter().filter(|f| !f.is_inherited).collect(); - let inherited: Vec<_> = functions.iter().filter(|f| f.is_inherited).collect(); - - for entry in &own { - if entry.name.is_empty() { continue; } - let badge = access_colored(&entry.access); - let padded_name = fmt::pad_right(&entry.name, max_name); - let mut tags: Vec<&str> = Vec::new(); - if entry.writes_state { tags.push("writes state"); } - if entry.has_external_calls { tags.push("external calls"); } - if entry.is_read_only { tags.push("view"); } - let tag_str = if tags.is_empty() { - String::new() - } else { - format!(" {}", c_muted(&tags.join(", "))) - }; - println!(" {} {}{}", badge, c_accent(&padded_name), tag_str); - } - - if !inherited.is_empty() { - println!(); - println!(" {}", c_muted("inherited:")); - for entry in &inherited { - let badge = access_colored(&entry.access); - let padded_name = fmt::pad_right(&entry.name, max_name); - let origin = format!("from {}", entry.origin); - println!(" {} {} {}", badge, c_muted(&padded_name), c_muted(&origin)); - } - } - println!(); - } - CommandResult::StateVarListAll { state_vars } => { - println!(); - let max_name = state_vars.iter() - .map(|v| v.name.chars().count()) - .max().unwrap_or(0); - let max_tag = 9; - - let own: Vec<_> = state_vars.iter().filter(|v| !v.is_inherited).collect(); - let inherited: Vec<_> = state_vars.iter().filter(|v| v.is_inherited).collect(); - - let render_tag = |is_const: bool, is_immut: bool| -> String { - let text = if is_const { "const" } - else if is_immut { "immutable" } - else { "mutable" }; - let padded = fmt::pad_right(text, max_tag); - if is_const || is_immut { - c_muted(&padded).to_string() - } else { - c_warn(&padded).to_string() - } - }; - - for entry in &own { - let tag = render_tag(entry.is_constant, entry.is_immutable); - let padded_name = fmt::pad_right(&entry.name, max_name); - println!(" {} {} {}", tag, c_accent(&padded_name), c_muted(&entry.type_name)); - } - - if !inherited.is_empty() { - println!(); - println!(" {}", c_muted("inherited:")); - for entry in &inherited { - let tag = render_tag(entry.is_constant, entry.is_immutable); - let padded_name = fmt::pad_right(&entry.name, max_name); - let origin = format!("from {}", entry.origin); - println!(" {} {} {} {}", - tag, - c_muted(&padded_name), - c_muted(&entry.type_name), - c_muted(&origin), - ); - } - } - println!(); - } - CommandResult::FindingAdded { id } => { - println!(" {} Finding {} added", c_ok("✓"), c_accent(id)); - } - CommandResult::NoteAdded => { - println!(" {} Note added", c_ok("✓")); - } - CommandResult::StatusUpdated => { - println!(" {} Status updated", c_ok("✓")); - } - CommandResult::SessionView { contract, steps: session_steps, findings_count } => { - println!(); - println!(" Contract: {}", c_bright(contract)); - println!(" Steps: {}", if session_steps.is_empty() { - c_muted("(empty)").to_string() - } else { - session_steps.iter().map(|s| s.as_str()).collect::<Vec<_>>().join(" → ") - }); - println!(" Findings: {}", findings_count); - println!(); - } - CommandResult::VariableInfo { variable, writers, readers } => { - println!(); - println!(" {} {}", c_bright("who:"), c_accent(variable)); - if !writers.is_empty() { - println!(" {}:", c_warn("Writers")); - for (name, access) in writers { - println!(" {} {}", access_colored(access), c_accent(name)); - } - } - if !readers.is_empty() { - println!(" {}:", c_muted("Readers")); - for (name, access) in readers { - println!(" {} {}", access_colored(access), c_muted(name)); - } - } - if !writers.is_empty() { - let slice_hints: Vec<String> = writers.iter() - .take(4) - .map(|(name, _)| format!("sl {} {}", name, variable)) - .collect(); - let suffix = if writers.len() > 4 { - format!(" (+{})", writers.len() - 4) - } else { - String::new() - }; - println!(" {}{}", c_muted(&format!("→ {}", slice_hints.join(", "))), c_muted(&suffix)); - } - println!(" {}", c_muted(&format!("→ tl {}", variable))); - println!(); - } - CommandResult::Exported { markdown } => { - println!(" {} chars exported", markdown.len()); - } - CommandResult::SessionSaved { json } => { - println!(" Session serialized ({} bytes)", json.len()); - } - CommandResult::SessionLoaded { contract, steps: loaded_steps } => { - println!(" Session loaded: {} ({} steps)", contract, loaded_steps.len()); - } - CommandResult::Error { message } => { - println!(" {}", c_danger(message)); - } - CommandResult::ScenarioList { items } => { - print!("{}", fmt::render_scenario_list(items)); - } - CommandResult::ScenarioCreated { name } => { - println!("{}", fmt::render_scenario_created(name)); - } - CommandResult::ScenarioSwitched { from, to } => { - println!("{}", fmt::render_scenario_switched(from, to)); - } - CommandResult::ScenarioForked { from, to, at_step } => { - println!("{}", fmt::render_scenario_forked(from, to, *at_step)); - } - CommandResult::ScenarioDeleted { name } => { - println!("{}", fmt::render_scenario_deleted(name)); - } - } -} - -fn format_access_detail(access: &AccessLevel) -> String { - match access { - AccessLevel::Public => "external".truecolor(110, 120, 140).to_string(), - AccessLevel::Restricted { role } => format!("{}", c_warn(&format!("restricted({role})"))), - AccessLevel::Internal => "internal".truecolor(110, 120, 140).to_string(), - AccessLevel::Special { kind } => format!("{}", c_muted(&format!("special({kind})"))), - } -} - -fn print_vars(val: &serde_json::Value) { - let vars = match val.get("state_vars").and_then(|v| v.as_array()) { - Some(v) => v, - None => { println!(" No state variables found."); return; } - }; - let max_name = vars.iter() - .filter_map(|v| v.get("name").and_then(|n| n.as_str())) - .map(|n| n.chars().count()) - .max().unwrap_or(0); - let max_tag = 9; // "immutable" is the longest - println!(); - for v in vars { - let name = v.get("name").and_then(|n| n.as_str()).unwrap_or("?"); - let type_name = v.get("type_name").and_then(|n| n.as_str()).unwrap_or("?"); - let is_const = v.get("is_constant").and_then(|n| n.as_bool()).unwrap_or(false); - let is_immut = v.get("is_immutable").and_then(|n| n.as_bool()).unwrap_or(false); - - let tag_text = if is_const { "const" } - else if is_immut { "immutable" } - else { "mutable" }; - let padded_tag = fmt::pad_right(tag_text, max_tag); - let tag = if is_const || is_immut { - c_muted(&padded_tag).to_string() - } else { - c_warn(&padded_tag).to_string() - }; - - let padded_name = fmt::pad_right(name, max_name); - println!(" {} {} {}", tag, c_accent(&padded_name), c_muted(type_name)); - } - println!(); -} - -fn print_narrative(val: &serde_json::Value) { - println!(); - if let Some(name) = val.get("name").and_then(|v| v.as_str()) { - let access = val.get("access").and_then(|v| v.as_str()).unwrap_or(""); - let mods = val.get("modifiers").and_then(|v| v.as_array()) - .map(|arr| arr.iter().filter_map(|m| m.as_str()).collect::<Vec<_>>().join(", ")) - .unwrap_or_default(); - let mod_str = if mods.is_empty() { String::new() } else { format!(" — {}", c_muted(&mods)) }; - println!(" {} [{}]{}", c_bright(name), c_accent(access), mod_str); - } - - #[derive(Default)] - struct TransitiveGroup { - writes: Vec<String>, - reads: Vec<String>, - external: Vec<String>, - events: Vec<String>, - } - - enum Section<'a> { - Paths { total: u64, happy: u64, revert: u64 }, - StringList { label: &'a str, label_color: SectionColor, items: Vec<String> }, - Transitive(Vec<(String, TransitiveGroup)>), - Observations(Vec<String>), - } - enum SectionColor { Muted, Danger, Warn, Accent } - - let color = |c: &SectionColor, s: &str| -> String { - match c { - SectionColor::Muted => c_muted(s).to_string(), - SectionColor::Danger => c_danger(s).to_string(), - SectionColor::Warn => c_warn(s).to_string(), - SectionColor::Accent => c_accent(s).to_string(), - } - }; - - let mut sections: Vec<Section> = Vec::new(); - - if let Some(total) = val.get("total_paths").and_then(|v| v.as_u64()) { - let happy = val.get("happy_paths").and_then(|v| v.as_u64()).unwrap_or(0); - let revert = val.get("revert_paths").and_then(|v| v.as_u64()).unwrap_or(0); - sections.push(Section::Paths { total, happy, revert }); - } - - let collect_strs = |key: &str| -> Vec<String> { - val.get(key) - .and_then(|v| v.as_array()) - .map(|arr| arr.iter().filter_map(|s| s.as_str().map(|s| s.to_string())).collect()) - .unwrap_or_default() - }; - - let reads = collect_strs("state_reads"); - if !reads.is_empty() { - sections.push(Section::StringList { label: "State reads", label_color: SectionColor::Muted, items: reads }); - } - let writes = collect_strs("state_writes"); - if !writes.is_empty() { - sections.push(Section::StringList { label: "State writes", label_color: SectionColor::Danger, items: writes }); - } - let internal = collect_strs("internal_calls"); - if !internal.is_empty() { - sections.push(Section::StringList { label: "Internal calls", label_color: SectionColor::Accent, items: internal }); - } - let externals = collect_strs("external_calls"); - if !externals.is_empty() { - sections.push(Section::StringList { label: "External calls", label_color: SectionColor::Warn, items: externals }); - } - let events = collect_strs("events"); - if !events.is_empty() { - sections.push(Section::StringList { label: "Events", label_color: SectionColor::Accent, items: events }); - } - - let collect_transitive = |key: &str| -> Vec<(Vec<String>, String)> { - val.get(key) - .and_then(|v| v.as_array()) - .map(|arr| { - arr.iter() - .filter_map(|e| { - let via = e.get("via")?.as_array()? - .iter().filter_map(|s| s.as_str().map(|s| s.to_string())) - .collect::<Vec<_>>(); - let item = e.get("item")?.as_str()?.to_string(); - Some((via, item)) - }) - .collect() - }) - .unwrap_or_default() - }; - - let t_writes = collect_transitive("transitive_state_writes"); - let t_reads = collect_transitive("transitive_state_reads"); - let t_external = collect_transitive("transitive_external_calls"); - let t_events = collect_transitive("transitive_events"); - - if !t_writes.is_empty() || !t_reads.is_empty() || !t_external.is_empty() || !t_events.is_empty() { - use std::collections::BTreeMap; - let mut groups: BTreeMap<String, TransitiveGroup> = BTreeMap::new(); - let join_chain = |via: &[String]| via.join(" → "); - for (via, item) in t_writes { groups.entry(join_chain(&via)).or_default().writes.push(item); } - for (via, item) in t_reads { groups.entry(join_chain(&via)).or_default().reads.push(item); } - for (via, item) in t_external { groups.entry(join_chain(&via)).or_default().external.push(item); } - for (via, item) in t_events { groups.entry(join_chain(&via)).or_default().events.push(item); } - let ordered: Vec<(String, TransitiveGroup)> = groups.into_iter().collect(); - sections.push(Section::Transitive(ordered)); - } - - let obs_items: Vec<String> = val - .get("observations") - .and_then(|v| v.as_array()) - .map(|arr| { - arr.iter() - .filter_map(|o| o.get("description").and_then(|v| v.as_str()).map(|s| s.to_string())) - .collect() - }) - .unwrap_or_default(); - if !obs_items.is_empty() { - sections.push(Section::Observations(obs_items)); - } - - let total = sections.len(); - for (i, section) in sections.iter().enumerate() { - let last = i == total - 1; - let branch = if last { "└──" } else { "├──" }; - let cont = if last { " " } else { "│ " }; - match section { - Section::Paths { total, happy, revert } => { - println!( - " {} {} path(s): {} happy, {} revert", - c_muted(branch), total, c_ok(&happy.to_string()), c_danger(&revert.to_string()) - ); - } - Section::StringList { label, label_color, items } => { - println!(" {} {}:", c_muted(branch), color(label_color, label)); - for (j, item) in items.iter().enumerate() { - let leaf = if j == items.len() - 1 { "└──" } else { "├──" }; - println!(" {} {} {}", c_muted(cont), c_muted(leaf), c_muted(item)); - } - } - Section::Transitive(groups) => { - println!(" {} {}:", c_muted(branch), c_warn("Transitive effects")); - let gtotal = groups.len(); - for (gi, (chain, g)) in groups.iter().enumerate() { - let glast = gi == gtotal - 1; - let gbranch = if glast { "└──" } else { "├──" }; - let gcont = if glast { " " } else { "│ " }; - println!(" {} {} {} {}", c_muted(cont), c_muted(gbranch), c_muted("via"), c_muted(chain)); - - let mut parts: Vec<(&str, &Vec<String>)> = Vec::new(); - if !g.writes.is_empty() { parts.push(("writes", &g.writes)); } - if !g.reads.is_empty() { parts.push(("reads", &g.reads)); } - if !g.external.is_empty() { parts.push(("external", &g.external)); } - if !g.events.is_empty() { parts.push(("emits", &g.events)); } - let ptotal = parts.len(); - for (pi, (plabel, pitems)) in parts.iter().enumerate() { - let plast = pi == ptotal - 1; - let pbranch = if plast { "└──" } else { "├──" }; - println!( - " {} {} {} {}: {}", - c_muted(cont), c_muted(gcont), c_muted(pbranch), - c_muted(plabel), c_muted(&pitems.join(", ")) - ); - } - } - } - Section::Observations(items) => { - println!(" {} {}:", c_muted(branch), c_danger("Observations")); - for (j, item) in items.iter().enumerate() { - let leaf = if j == items.len() - 1 { "└──" } else { "├──" }; - println!(" {} {} {}", c_muted(cont), c_muted(leaf), c_danger(item)); - } - } - } - } - if let Some(name) = val.get("name").and_then(|v| v.as_str()) { - println!(" {}", c_muted(&format!("→ c {} | tr {}", name, name))); - } - println!(); -} - -fn print_sequence_narrative(val: &serde_json::Value) { - println!(); - if let Some(steps) = val.get("steps").and_then(|v| v.as_array()) { - let names: Vec<&str> = steps.iter() - .filter_map(|s| s.get("function").and_then(|f| f.as_str())) - .collect(); - if !names.is_empty() { - println!(" {}", c_bright(&names.join(" → "))); - } - } - if let Some(deps) = val.get("dependencies").and_then(|v| v.as_array()) { - if !deps.is_empty() { - println!(" {}:", c_warn("Dependencies")); - for dep in deps { - if let Some(desc) = dep.get("description").and_then(|v| v.as_str()) { - println!(" • {}", c_muted(desc)); - } - } - } - } - if let Some(obs) = val.get("observations").and_then(|v| v.as_array()) { - if !obs.is_empty() { - println!(" {}:", c_danger("Observations")); - for o in obs { - if let Some(desc) = o.get("description").and_then(|v| v.as_str()) { - println!(" ! {}", c_danger(desc)); - } else if let Some(desc) = o.as_str() { - println!(" ! {}", c_danger(desc)); - } - } - } - } - if let Some(steps) = val.get("steps").and_then(|v| v.as_array()) { - let any_summary = steps.iter().any(|s| !s.get("flow_summary").map(|v| v.is_null()).unwrap_or(true)); - if any_summary { - println!(" {}:", c_warn("Flow summaries")); - for (i, step) in steps.iter().enumerate() { - let summary = match step.get("flow_summary") { - Some(s) if !s.is_null() => s, - _ => continue, - }; - let func = step.get("function").and_then(|v| v.as_str()).unwrap_or("?"); - let total = summary.get("total_steps").and_then(|v| v.as_u64()).unwrap_or(0); - let muts = summary.get("mutation_count").and_then(|v| v.as_u64()).unwrap_or(0); - let ext = summary.get("external_call_count").and_then(|v| v.as_u64()).unwrap_or(0); - let int_n = summary.get("internal_call_count").and_then(|v| v.as_u64()).unwrap_or(0); - let dl = summary.get("depth_limited_count").and_then(|v| v.as_u64()).unwrap_or(0); - println!( - " {} {} {} ops | {} mutations | {} ext calls | {} internal{}", - c_accent(&format!("step {}", i)), - c_bright(func), - total, muts, ext, int_n, - if dl > 0 { format!(" ({} depth-limited)", dl) } else { String::new() }, - ); - } - } - } - println!(); -} - -fn print_help() { - print_help_for(BackendKind::Solidity); -} - fn print_solana_help() { - print_help_for(BackendKind::Solana); -} - -fn print_help_for(backend: BackendKind) { - let groups: &[(&str, &[(&str, &str, &str)])] = match backend { - BackendKind::Solana => &[ - ("Session", &[ - ("c", "call <ix> arg=val acc=user", "Concise: keys auto-distributed; signers auto from IDL"), - ("", "call <ix> {json}", "Full control with explicit args/accounts/signers"), - ("", " --signer=a,b", "Add signers (override IDL defaults)"), - ("", " --no-signer=a", "Remove a signer (simulate negative cases)"), - ("b", "back", "Remove last step from active scenario"), - ("cl", "clear", "Reset active scenario steps"), - ("", "state", "Decoded view of accounts mutated this session"), - ("s", "session", "Active scenario summary (steps + findings)"), - ]), - ("Programs", &[ - ("ct", "programs (alias contracts)", "List programs in workspace"), - ("", "use <program>", "Switch active program"), - ("f", "funcs (alias functions)", "List instructions of active program"), - ("fa", "funcs-all", "Instructions with arg/account/signer/pda counts"), - ("i", "info <ix>", "Detail an instruction: args (typed), accounts (flags), discriminator"), - ("v", "vars", "List declared account types with discriminators"), - ("va", "vars-all", "Account types with their decoded field layout"), - ]), - ("Solana runtime", &[ - ("", "users", "List keypairs in active scenario"), - ("", "users new <name> [lamports]", "Create keypair + airdrop (default 10 SOL)"), - ("", "airdrop <user> <lamports>", "Top up an existing keypair"), - ("tw", "time-warp <secs>", "Advance Clock unix_timestamp + slot"), - ("", "pda <ix>", "List PDAs declared by an instruction (symbolic)"), - ("", "inspect <pubkey>", "Read VM account, decode by Anchor discriminator"), - ]), - ("Analysis", &[ - ("st", "step <index>", "Re-inspect a step: CU, logs, diffs"), - ("", "who <query>", "Resolve query: AccountType | Instruction | Field"), - ("tl", "timeline <pubkey>", "Cross-step mutation history of an account, decoded"), - ("cp", "coupling", "List ix pairs that share writable accounts"), - ("cov","coverage", "Aggregated runtime metrics for the active scenario"), - ]), - ("Findings", &[ - ("fi", "finding <sev> <title>", "Record a security finding"), - ("fl", "findings", "List recorded findings"), - ("n", "note <text>", "Add annotation to active sequence"), - ("", "status <ix> <s>", "Set review status: open|reviewed|finding"), - ("ex", "export", "Markdown report: sequence + findings + program info"), - ]), - ("Workspace", &[ - ("sc", "scenario <sub>", "new|list|switch|fork|delete <name> [step]"), - ("", "save <name>", "Save active scenario JSON to disk"), - ("", "load <name>", "Load scenario JSON from disk"), - ("?", "help", "Print this help (append ? to any cmd for full reference)"), - ("q", "quit/exit", "Exit"), - ]), - ], - _ => &[ + let groups: &[(&str, &[(&str, &str, &str)])] = &[ ("Session", &[ - ("c", "call <func>", "Add function to sequence"), - ("b", "back", "Remove last step"), - ("cl", "clear", "Reset sequence"), - ("s", "state", "Show accumulated state"), - ("seq", "sequence", "Sequence narrative with dependencies"), - ("st", "step <index>", "Re-inspect a specific step"), - ("ss", "session", "Full session state"), + ("c", "call <ix> arg=val acc=user", "Concise: keys auto-distributed; signers auto from IDL"), + ("", "call <ix> {json}", "Full control with explicit args/accounts/signers"), + ("", " --signer=a,b", "Add signers (override IDL defaults)"), + ("", " --no-signer=a", "Remove a signer (simulate negative cases)"), + ("b", "back", "Remove last step from active scenario"), + ("cl", "clear", "Reset active scenario steps"), + ("", "state", "Decoded view of accounts mutated this session"), + ("s", "session", "Active scenario summary (steps + findings)"), ]), - ("Analysis", &[ - ("w", "who <var>", "Who reads/writes a variable"), - ("i", "info <func>", "Function detail"), - ("tr", "trace <func>", "Execution flow tree (inlined)"), - ("", "trace <func> -i", "Interactive trace viewer"), - ("", "trace step <N>", "Re-render persisted step trace"), - ("tl", "timeline <var>", "Cross-step variable mutation history"), - ("sl", "slice <fn> <var>", "Dataflow slice for a variable"), + ("Programs", &[ + ("ct", "programs (alias contracts)", "List programs in workspace"), + ("", "use <program>", "Switch active program"), + ("f", "funcs (alias functions)", "List instructions of active program"), + ("fa", "funcs-all", "Instructions with arg/account/signer/pda counts"), + ("i", "info <ix>", "Detail an instruction: args (typed), accounts (flags), discriminator"), + ("v", "vars", "List declared account types with discriminators"), + ("va", "vars-all", "Account types with their decoded field layout"), ]), - ("Contract", &[ - ("f", "functions", "List callable functions"), - ("fa", "funcs-all", "List all accessible (incl. inherited)"), - ("v", "vars", "List state variables"), - ("va", "vars-all", "List all accessible (incl. inherited)"), - ("ct", "contracts", "List project contracts"), - ("", "use <contract>", "Switch active contract"), + ("Solana runtime", &[ + ("", "users", "List keypairs in active scenario"), + ("", "users new <name> [lamports]", "Create keypair + airdrop (default 10 SOL)"), + ("", "airdrop <user> <lamports>", "Top up an existing keypair"), + ("tw", "time-warp <secs>", "Advance Clock unix_timestamp + slot"), + ("", "pda <ix>", "List PDAs declared by an instruction (symbolic)"), + ("", "inspect <pubkey>", "Read VM account, decode by Anchor discriminator"), + ]), + ("Analysis", &[ + ("st", "step <index>", "Re-inspect a step: CU, logs, diffs"), + ("", "who <query>", "Resolve query: AccountType | Instruction | Field"), + ("tl", "timeline <pubkey>", "Cross-step mutation history of an account, decoded"), + ("cp", "coupling", "List ix pairs that share writable accounts"), + ("cov","coverage", "Aggregated runtime metrics for the active scenario"), ]), ("Findings", &[ - ("fi", "finding [sev] [t]","Record a finding"), - ("n", "note <text>", "Add note to current step"), - ("sc", "scenario <name>", "Name the current sequence"), - ("", "status <f> <s>", "Change review status"), - ("fl", "findings", "List recorded findings"), - ("ex", "export", "Export findings as markdown"), + ("fi", "finding <sev> <title>", "Record a security finding"), + ("fl", "findings", "List recorded findings"), + ("n", "note <text>", "Add annotation to active sequence"), + ("", "status <ix> <s>", "Set review status: open|reviewed|finding"), + ("ex", "export", "Markdown report: sequence + findings + program info"), ]), ("Workspace", &[ - ("", "save <name>", "Save session to disk"), - ("", "load <name>", "Load session from disk"), - ("", "browser", "Open web UI"), - ("q", "quit/exit", "Exit"), + ("sc", "scenario <sub>", "new|list|switch|fork|delete <name> [step]"), + ("", "save <name>", "Save active scenario JSON to disk"), + ("", "load <name>", "Load scenario JSON from disk"), + ("?", "help", "Print this help (append ? to any cmd for full reference)"), + ("q", "quit/exit", "Exit"), ]), - ], - }; + ]; println!(); - println!(" {} {}", c_bright("ilold explore"), c_muted("— append ? to any command for inline help (e.g. sl?)")); + println!(" {} {}", c_bright("ilold explore"), c_muted("— append ? to any command for inline help (e.g. call?)")); println!(); for (group_name, cmds) in groups { println!(" {}", c_warn(group_name)); @@ -2977,52 +1336,6 @@ fn print_help_for(backend: BackendKind) { } } -fn print_inline_help(cmd: &str) { - let entries: &[(&[&str], &str, &str)] = &[ - (&["c", "call"], "call <func>", "Add function call to session. Example: c deposit"), - (&["b", "back"], "back", "Remove last step from the session sequence."), - (&["cl", "clear"], "clear", "Reset the entire session (all steps removed)."), - (&["s", "state"], "state", "Show accumulated state mutations across all steps."), - (&["f", "functions"], "functions", "List callable functions in the active contract."), - (&["v", "vars"], "vars", "List state variables of the active contract."), - (&["w", "who"], "who <variable>", "Show which functions read/write a variable. Example: who totalStaked"), - (&["i", "info"], "info <func>", "Function detail: paths, reads, writes, calls. Example: i deposit"), - (&["tr", "trace"], "trace <func> [--depth N] [-i]", "Execution flow tree. -i for interactive TUI. Example: tr swap --depth 3"), - (&["seq", "sequence"],"sequence", "Show the narrative of the current call sequence."), - (&["tl", "timeline"], "timeline <variable>", "Cross-step mutation history. Example: tl totalStaked"), - (&["sl", "slice"], "slice <func> <var> [--backward]", "Dataflow slice. Example: sl deposit totalStaked --backward"), - (&["st", "step"], "step <index>", "Re-inspect a specific session step. Example: st 0"), - (&["ss", "session"], "session", "Full session state with all steps."), - (&["fi", "finding"], "finding [severity] [text]", "Record a security finding for the current step."), - (&["n", "note"], "note <text>", "Attach a note to the current step."), - (&["sc", "scenario"],"scenario <name>", "Name the current sequence. Example: sc reentrancy-attack"), - (&["fl", "findings"], "findings", "List all recorded findings."), - (&["ex", "export"], "export", "Export findings as a markdown report."), - (&["fa", "funcs-all"],"funcs-all", "List all accessible functions including inherited."), - (&["va", "vars-all"], "vars-all", "List all accessible state variables including inherited."), - (&["ct", "contracts"],"contracts", "List all contracts in the project."), - (&["use"], "use <contract>", "Switch the active contract. Example: use Staking"), - (&["status"], "status <func> <status>", "Change review status. Example: status deposit reviewed"), - (&["save"], "save <name>", "Save session to disk. Example: save my-audit"), - (&["load"], "load <name>", "Load session from disk. Example: load my-audit"), - (&["browser"], "browser", "Open the web UI in a browser."), - (&["users"], "users [new <name> [<lamports>]]", "List or create keypairs in active scenario."), - (&["airdrop", "air"], "airdrop <name> <lamports>", "Top up a user's SOL balance."), - (&["tw", "time-warp"],"time-warp <delta_seconds>", "Warp the Clock sysvar (positive forward, negative back)."), - (&["pda"], "pda <instruction>", "List declared PDAs of an instruction (Anchor IDL)."), - (&["inspect", "acc"], "inspect <pubkey>", "Decode an account by Anchor discriminator."), - (&["programs", "progs"], "programs", "List programs in the workspace (Solana)."), - ]; - - for (aliases, usage, desc) in entries { - if aliases.iter().any(|a| *a == cmd) { - println!(" {} {}", c_accent(usage), c_muted(desc)); - return; - } - } - println!(" {} unknown command: {}", c_danger("✗"), cmd); -} - struct IloldPrompt { contract: String, steps: Vec<String>, @@ -3045,7 +1358,9 @@ impl Prompt for IloldPrompt { if self.steps.is_empty() { Cow::Owned(format!("ilold[{}]", label)) } else if self.steps.len() <= 3 { - let path = self.steps.iter() + let path = self + .steps + .iter() .map(|s| s.as_str()) .collect::<Vec<_>>() .join(" → "); @@ -3054,15 +1369,27 @@ impl Prompt for IloldPrompt { let skipped = self.steps.len() - 2; Cow::Owned(format!( "ilold[{} → {} → ...{} more → {}]", - label, self.steps[0], skipped, self.steps.last().unwrap() + label, + self.steps[0], + skipped, + self.steps.last().unwrap() )) } } - fn render_prompt_right(&self) -> Cow<'_, str> { Cow::Borrowed("") } - fn render_prompt_indicator(&self, _: PromptEditMode) -> Cow<'_, str> { Cow::Borrowed("> ") } - fn render_prompt_multiline_indicator(&self) -> Cow<'_, str> { Cow::Borrowed(".. ") } - fn render_prompt_history_search_indicator(&self, search: PromptHistorySearch) -> Cow<'_, str> { + fn render_prompt_right(&self) -> Cow<'_, str> { + Cow::Borrowed("") + } + fn render_prompt_indicator(&self, _: PromptEditMode) -> Cow<'_, str> { + Cow::Borrowed("> ") + } + fn render_prompt_multiline_indicator(&self) -> Cow<'_, str> { + Cow::Borrowed(".. ") + } + fn render_prompt_history_search_indicator( + &self, + search: PromptHistorySearch, + ) -> Cow<'_, str> { match search.status { PromptHistorySearchStatus::Passing => Cow::Borrowed("(search) "), PromptHistorySearchStatus::Failing => Cow::Borrowed("(search failed) "), @@ -3084,22 +1411,16 @@ impl Completer for IloldCompleter { || line_lower.starts_with("call ") || line_lower.starts_with("i ") || line_lower.starts_with("info ") - || line_lower.starts_with("tr ") - || line_lower.starts_with("trace ") || line_lower.starts_with("w ") || line_lower.starts_with("who ") - || line_lower.starts_with("status ") - || (line_lower.starts_with("sl ") && line[..pos].matches(' ').count() == 1) - || (line_lower.starts_with("slice ") && line[..pos].matches(' ').count() == 1); + || line_lower.starts_with("status "); let needs_contract = line_lower.starts_with("use "); let needs_scenario = line_lower.starts_with("scenario switch ") || line_lower.starts_with("scenario delete ") || line_lower.starts_with("sc switch ") - || line_lower.starts_with("sc delete ") - || line_lower.starts_with("scen switch ") - || line_lower.starts_with("scen delete "); + || line_lower.starts_with("sc delete "); if !needs_func && !needs_contract && !needs_scenario { return Vec::new(); @@ -3116,7 +1437,8 @@ impl Completer for IloldCompleter { &self.functions }; - source.iter() + source + .iter() .filter(|f| f.starts_with(partial)) .map(|f| Suggestion { value: f.clone(), From 2a2afb6aa5c268f4f9b1a0d9177d135e81d4fb22 Mon Sep 17 00:00:00 2001 From: scab24 <git.seco@protonmail.com> Date: Sat, 16 May 2026 21:30:16 +0200 Subject: [PATCH 03/10] chore(cli): drop fmt.rs and interactive.rs solidity helpers Moves header_box and pad_right (the only two helpers still needed) into colors.rs, deletes fmt.rs (FlowTree/SliceResult renderers) and interactive.rs (TUI trace viewer), and removes the now-unused terminal_size/ratatui/crossterm dependencies and the ilold-core path dep from ilold-cli. --- crates/ilold-cli/Cargo.toml | 6 +- crates/ilold-cli/src/colors.rs | 42 ++ crates/ilold-cli/src/explore.rs | 11 +- crates/ilold-cli/src/fmt.rs | 607 ---------------------------- crates/ilold-cli/src/interactive.rs | 508 ----------------------- crates/ilold-cli/src/main.rs | 2 - 6 files changed, 48 insertions(+), 1128 deletions(-) delete mode 100644 crates/ilold-cli/src/fmt.rs delete mode 100644 crates/ilold-cli/src/interactive.rs diff --git a/crates/ilold-cli/Cargo.toml b/crates/ilold-cli/Cargo.toml index 002b150..80c3c6d 100644 --- a/crates/ilold-cli/Cargo.toml +++ b/crates/ilold-cli/Cargo.toml @@ -5,14 +5,13 @@ edition.workspace = true authors.workspace = true license.workspace = true repository.workspace = true -description = "CLI for Ilold: smart contract execution path analyzer" +description = "CLI for Ilold: Solana program execution path analyzer" [[bin]] name = "ilold" path = "src/main.rs" [dependencies] -ilold-core = { path = "../ilold-core" } ilold-solana-core = { path = "../ilold-solana-core" } ilold-web = { path = "../ilold-web" } ilold-mcp = { path = "../ilold-mcp" } @@ -27,6 +26,3 @@ nu-ansi-term = "0.50" reqwest = { version = "0.12", features = ["json"] } serde_json = { workspace = true } dirs = "6" -terminal_size = "0.4" -ratatui = { version = "0.30", default-features = false, features = ["crossterm_0_29"] } -crossterm = "0.29" diff --git a/crates/ilold-cli/src/colors.rs b/crates/ilold-cli/src/colors.rs index 10a86fc..6f3d750 100644 --- a/crates/ilold-cli/src/colors.rs +++ b/crates/ilold-cli/src/colors.rs @@ -1 +1,43 @@ pub use ilold_render::colors::*; + +use colored::Colorize; + +pub fn header_box(lines: &[&str]) -> String { + let max_content = lines.iter().map(|l| l.chars().count()).max().unwrap_or(0); + let inner_width = max_content + 2; + + let mut out = String::new(); + out.push_str(&format!( + " {}{}{}\n", + "╭".truecolor(60, 70, 90), + "─".repeat(inner_width).truecolor(60, 70, 90), + "╮".truecolor(60, 70, 90), + )); + for line in lines { + let visible_len = line.chars().count(); + let pad = max_content.saturating_sub(visible_len); + out.push_str(&format!( + " {} {}{} {}\n", + "│".truecolor(60, 70, 90), + line, + " ".repeat(pad), + "│".truecolor(60, 70, 90), + )); + } + out.push_str(&format!( + " {}{}{}", + "╰".truecolor(60, 70, 90), + "─".repeat(inner_width).truecolor(60, 70, 90), + "╯".truecolor(60, 70, 90), + )); + out +} + +pub fn pad_right(s: &str, width: usize) -> String { + let len = s.chars().count(); + if len >= width { + s.to_string() + } else { + format!("{}{}", s, " ".repeat(width - len)) + } +} diff --git a/crates/ilold-cli/src/explore.rs b/crates/ilold-cli/src/explore.rs index 6b60db2..7f7989a 100644 --- a/crates/ilold-cli/src/explore.rs +++ b/crates/ilold-cli/src/explore.rs @@ -11,7 +11,6 @@ use ilold_solana_core::exploration::SolanaCommandResult; use ilold_solana_core::view::ProgramView; use crate::colors::*; -use crate::fmt; pub async fn run( _paths: Vec<PathBuf>, @@ -67,7 +66,7 @@ async fn run_solana_attach( }) .unwrap_or_default(); - let banner = fmt::header_box(&[ + let banner = header_box(&[ &format!("ilold explore — {} (solana, attached)", program_name), &format!("{} instructions | Type ? for help", function_names.len()), &format!("Server: {}", url), @@ -112,7 +111,7 @@ async fn run_with_state( .map(|p| p.name.clone()) .collect(); - let banner = fmt::header_box(&[ + let banner = header_box(&[ &format!("ilold explore — {} (solana)", program_name), &format!("{} instructions | Type ? for help", function_names.len()), &format!("Web UI: http://localhost:{}", actual_port), @@ -1326,11 +1325,11 @@ fn print_solana_help() { println!(" {}", c_warn(group_name)); for (shortcut, name, desc) in *cmds { let sc = if shortcut.is_empty() { - format!(" {} ", fmt::pad_right("", 3)) + format!(" {} ", pad_right("", 3)) } else { - format!(" {} {}", c_accent(&fmt::pad_right(shortcut, 3)), c_muted("|")) + format!(" {} {}", c_accent(&pad_right(shortcut, 3)), c_muted("|")) }; - println!(" {} {} {}", sc, c_accent(&fmt::pad_right(name, 22)), c_muted(desc)); + println!(" {} {} {}", sc, c_accent(&pad_right(name, 22)), c_muted(desc)); } println!(); } diff --git a/crates/ilold-cli/src/fmt.rs b/crates/ilold-cli/src/fmt.rs deleted file mode 100644 index fcb4e51..0000000 --- a/crates/ilold-cli/src/fmt.rs +++ /dev/null @@ -1,607 +0,0 @@ -use colored::Colorize; - -use ilold_core::exploration::commands::ScenarioInfo; -use ilold_core::exploration::session::MutationScope; -use ilold_core::exploration::timeline::{TimelineEntry, VariableTimeline}; -use ilold_core::narrative::trace::{FlowKind, FlowNode, FlowTree}; -use ilold_core::slicing::{SliceDirection, SliceEntry, SliceResult, StatementOrigin}; - -use crate::colors::{c_accent, c_bright, c_danger, c_muted, c_ok, c_warn}; - -pub fn term_width() -> usize { - terminal_size::terminal_size() - .map(|(terminal_size::Width(w), _)| w as usize) - .unwrap_or(80) -} - -pub fn separator(label: &str) -> String { - let width = term_width().min(100); - let inner = format!("[ {} ]", label); - let remaining = width.saturating_sub(inner.len() + 2); - let left = remaining / 2; - let right = remaining - left; - format!( - " {}{}{}", - "═".repeat(left).truecolor(60, 70, 90), - inner.truecolor(190, 200, 215), - "═".repeat(right).truecolor(60, 70, 90), - ) -} - -pub fn header_box(lines: &[&str]) -> String { - let max_content = lines.iter().map(|l| l.chars().count()).max().unwrap_or(0); - let inner_width = max_content + 2; - - let mut out = String::new(); - out.push_str(&format!( - " {}{}{}\n", - "╭".truecolor(60, 70, 90), - "─".repeat(inner_width).truecolor(60, 70, 90), - "╮".truecolor(60, 70, 90), - )); - for line in lines { - let visible_len = line.chars().count(); - let pad = max_content.saturating_sub(visible_len); - out.push_str(&format!( - " {} {}{} {}\n", - "│".truecolor(60, 70, 90), - line, - " ".repeat(pad), - "│".truecolor(60, 70, 90), - )); - } - out.push_str(&format!( - " {}{}{}", - "╰".truecolor(60, 70, 90), - "─".repeat(inner_width).truecolor(60, 70, 90), - "╯".truecolor(60, 70, 90), - )); - out -} - -pub fn pad_right(s: &str, width: usize) -> String { - let len = s.chars().count(); - if len >= width { - s.to_string() - } else { - format!("{}{}", s, " ".repeat(width - len)) - } -} - -pub fn render_flow_tree(tree: &FlowTree) -> String { - let mut out = String::new(); - - let title = format!("{}::{}", tree.contract, tree.signature); - let mods = if tree.modifiers.is_empty() { - "modifiers: (none)".to_string() - } else { - format!("modifiers: {}", tree.modifiers.join(", ")) - }; - let depth = format!("max inlining depth: {}", tree.max_depth); - let lines: Vec<&str> = vec![&title, &mods, &depth]; - out.push_str(&header_box(&lines)); - out.push('\n'); - out.push('\n'); - - let mut step: usize = 1; - render_flow_root(&tree.root, &mut step, &mut out); - - append_expand_hint(tree, &mut out); - - let mut raw_vars: Vec<&str> = Vec::new(); - collect_written_vars(&tree.root, &mut raw_vars); - let mut base_vars: Vec<String> = raw_vars.iter() - .map(|v| v.split('[').next().unwrap_or(v).to_string()) - .collect(); - base_vars.sort_unstable(); - base_vars.dedup(); - if !base_vars.is_empty() { - let hints = base_vars.iter() - .take(5) - .map(|v| format!("sl {} {}", tree.function, v)) - .collect::<Vec<_>>() - .join(", "); - let suffix = if base_vars.len() > 5 { - format!(" (+{} more)", base_vars.len() - 5) - } else { - String::new() - }; - out.push_str(&format!(" {}{}\n", c_muted(&format!("→ {}", hints)), c_muted(&suffix))); - } - - out -} - -fn collect_written_vars<'a>(node: &'a FlowNode, out: &mut Vec<&'a str>) { - match &node.kind { - FlowKind::Write { target, .. } | FlowKind::StateWrite { variable: target } => { - out.push(target.as_str()); - } - _ => {} - } - for child in &node.children { - collect_written_vars(child, out); - } -} - -fn append_expand_hint(tree: &FlowTree, out: &mut String) { - let mut candidates: Vec<usize> = Vec::new(); - collect_depth_limited(&tree.root, &mut candidates); - if candidates.is_empty() { - return; - } - - let total = candidates.len(); - candidates.truncate(10); - let csv = candidates.iter() - .map(|n| n.to_string()) - .collect::<Vec<_>>() - .join(", "); - let suffix = if total > 10 { - format!(" (… {} more)", total - 10) - } else { - String::new() - }; - - out.push('\n'); - out.push_str(&format!( - " {} {}{}\n", - c_muted("tip: expand with `tr <func> +N` — candidates:"), - c_accent(&csv), - c_muted(&suffix), - )); -} - -fn collect_depth_limited(node: &FlowNode, out: &mut Vec<usize>) { - if let FlowKind::InternalCall { depth_limited: true, .. } = &node.kind { - out.push(node.step_id); - } - for child in &node.children { - collect_depth_limited(child, out); - } -} - -fn render_flow_root(root: &FlowNode, step: &mut usize, out: &mut String) { - let (icon, text) = format_flow_label(&root.kind); - let gutter = format_gutter(*step); - *step += 1; - out.push_str(&format!( - "{} {} {}\n", - gutter, - c_accent(icon), - c_bright(&text), - )); - - let n = root.children.len(); - for (i, child) in root.children.iter().enumerate() { - render_flow_node(child, "", i == n - 1, step, out); - } -} - -fn render_flow_node( - node: &FlowNode, - parent_prefix: &str, - is_last: bool, - step: &mut usize, - out: &mut String, -) { - let connector = if is_last { "└─ " } else { "├─ " }; - let (icon, text) = format_flow_label(&node.kind); - let gutter = format_gutter(*step); - *step += 1; - - let suffix = match &node.from_modifier { - Some(name) => format!(" {}", c_warn(&format!("[from: {}]", name))), - None => String::new(), - }; - - let colored_icon = color_for_kind(&node.kind, icon); - let colored_text = color_for_kind_text(&node.kind, &text); - - out.push_str(&format!( - "{} {}{}{} {}{}\n", - gutter, - c_muted(parent_prefix), - c_muted(connector), - colored_icon, - colored_text, - suffix, - )); - - let extension = if is_last { " " } else { "│ " }; - let new_prefix = format!("{}{}", parent_prefix, extension); - let n = node.children.len(); - for (i, child) in node.children.iter().enumerate() { - render_flow_node(child, &new_prefix, i == n - 1, step, out); - } -} - -fn format_gutter(step: usize) -> String { - format!(" {} {}", c_accent(&format!("{:03}", step)), c_muted("│")) -} - -pub(crate) fn format_flow_label(kind: &FlowKind) -> (&'static str, String) { - match kind { - FlowKind::Entry { signature } => ("▶", signature.clone()), - FlowKind::Require { condition, message } => { - let text = match message { - Some(m) => format!("require({}, {})", condition, m), - None => format!("require({})", condition), - }; - ("◇", text) - } - FlowKind::Assert { condition } => ("◇", format!("assert({})", condition)), - FlowKind::Write { target, value, op } => { - ("✏", format!("{} {} {}", target, op.as_str(), value)) - } - FlowKind::StateWrite { variable } => ("✏", format!("write {}", variable)), - FlowKind::StateRead { variable } => ("▸", format!("read {}", variable)), - FlowKind::InternalCall { - function, - origin, - depth_limited, - ops_count, - } => { - let mut text = function.clone(); - if !origin.is_empty() { - text.push_str(&format!(" [from: {}]", origin)); - } - if *depth_limited { - text.push_str(&format!(" [+{} ops, depth limited]", ops_count)); - } - ("○", text) - } - FlowKind::ExternalCall { target, function } => { - ("→", format!("{}.{}", target, function)) - } - FlowKind::EmitEvent { name } => ("◆", format!("emit {}", name)), - FlowKind::BranchTrue { condition } => ("?", format!("if ({}) == true", condition)), - FlowKind::BranchFalse { condition } => ("?", format!("if ({}) == false", condition)), - FlowKind::LoopHeader { kind } => ("↻", format!("loop ({})", kind)), - FlowKind::Return => ("✓", "return".to_string()), - FlowKind::Revert { reason } => { - let text = match reason { - Some(r) => format!("revert({})", r), - None => "revert".to_string(), - }; - ("✗", text) - } - FlowKind::EthTransfer { to } => ("$", format!("transfer ETH → {}", to)), - FlowKind::AssemblyBlock => ("⚙", "assembly { … }".to_string()), - FlowKind::DepthLimit => ("…", "depth limit".to_string()), - } -} - -fn color_for_kind(kind: &FlowKind, icon: &str) -> colored::ColoredString { - match kind { - FlowKind::Entry { .. } => c_accent(icon), - FlowKind::Require { .. } | FlowKind::Assert { .. } => c_muted(icon), - FlowKind::Write { .. } | FlowKind::StateWrite { .. } => c_danger(icon), - FlowKind::StateRead { .. } => c_muted(icon), - FlowKind::InternalCall { .. } => c_warn(icon), - FlowKind::ExternalCall { .. } | FlowKind::EthTransfer { .. } => c_danger(icon), - FlowKind::EmitEvent { .. } => c_accent(icon), - FlowKind::BranchTrue { .. } | FlowKind::BranchFalse { .. } => c_warn(icon), - FlowKind::LoopHeader { .. } => c_warn(icon), - FlowKind::Return => c_ok(icon), - FlowKind::Revert { .. } => c_danger(icon), - FlowKind::AssemblyBlock => c_warn(icon), - FlowKind::DepthLimit => c_muted(icon), - } -} - -fn color_for_kind_text(kind: &FlowKind, text: &str) -> colored::ColoredString { - match kind { - FlowKind::Entry { .. } => c_bright(text), - FlowKind::Write { .. } | FlowKind::StateWrite { .. } => c_danger(text), - FlowKind::ExternalCall { .. } | FlowKind::EthTransfer { .. } => c_danger(text), - FlowKind::InternalCall { .. } => c_warn(text), - FlowKind::BranchTrue { .. } | FlowKind::BranchFalse { .. } => c_warn(text), - FlowKind::Revert { .. } => c_danger(text), - FlowKind::Return => c_ok(text), - _ => c_muted(text), - } -} - -pub fn render_variable_timeline(tl: &VariableTimeline) -> String { - let mut out = String::new(); - out.push('\n'); - - let header = format!("{} {}", c_bright(&tl.variable), c_muted("— mutation timeline")); - out.push_str(&format!(" {}\n", header)); - out.push_str(&format!( - " {}\n", - "═".repeat(60).truecolor(60, 70, 90) - )); - - if tl.state_entries.is_empty() && tl.local_entries.is_empty() { - out.push_str(&format!(" {}\n", c_muted(&format!("no mutations of '{}' in current session — add steps with 'c <func>' first", tl.variable)))); - return out; - } - - if !tl.state_entries.is_empty() { - out.push_str(&format!(" {}\n", c_warn("[state]"))); - render_entries(&tl.state_entries, &mut out); - } - - if !tl.local_entries.is_empty() { - out.push_str(&format!(" {}\n", c_warn("[local]"))); - render_entries(&tl.local_entries, &mut out); - } - - let all_entries = tl.state_entries.iter().chain(tl.local_entries.iter()); - let mut seen = std::collections::HashSet::new(); - let hints: Vec<String> = all_entries - .filter(|e| seen.insert(e.function.clone())) - .map(|e| format!("sl {} {}", e.function, tl.variable)) - .collect(); - if !hints.is_empty() { - out.push_str(&format!(" {}\n", c_muted(&format!("→ {}", hints.join(", "))))); - } - - out.push('\n'); - out -} - -fn render_entries(entries: &[TimelineEntry], out: &mut String) { - let mut prev_step: Option<usize> = None; - for entry in entries { - if Some(entry.session_step_index) != prev_step { - out.push_str(&format!( - " {} {}\n", - c_accent(&format!("session step {}", entry.session_step_index)), - c_bright(&entry.function), - )); - prev_step = Some(entry.session_step_index); - } - - let op = entry.operator.as_str(); - let flow_ref = entry.flow_step_id - .map(|id| format!(" [trace step {}]", id)) - .unwrap_or_default(); - let via = entry.via.as_deref() - .map(|c| format!(" via {}", c)) - .unwrap_or_default(); - let scope_tag = match entry.scope { - MutationScope::State => "", - MutationScope::Local => " (local)", - }; - - out.push_str(&format!( - " {} {} {} {}{}{}{}\n", - c_danger("✏"), - c_muted(&entry.target), - c_muted(op), - c_muted(&entry.value_expr), - c_muted(&flow_ref), - c_muted(&via), - c_muted(scope_tag), - )); - - if !entry.reached_when.is_empty() { - out.push_str(&format!(" {}\n", c_muted("reached when:"))); - for cond in &entry.reached_when { - out.push_str(&format!(" {} {}\n", c_muted("•"), c_muted(cond))); - } - } - } -} - -pub fn render_slice_result(res: &SliceResult) -> String { - let mut out = String::new(); - out.push('\n'); - - let header = format!( - "{} {} {} {}", - c_bright(&res.function), - c_muted("·"), - c_bright(&res.variable), - c_muted("— dataflow slice"), - ); - out.push_str(&format!(" {}\n", header)); - out.push_str(&format!(" {}\n", "═".repeat(60).truecolor(60, 70, 90))); - - let show_backward = matches!( - res.direction, - SliceDirection::Backward | SliceDirection::Both - ); - let show_forward = matches!( - res.direction, - SliceDirection::Forward | SliceDirection::Both - ); - - if show_backward { - render_slice_side("backward", &res.backward, &res.variable, &mut out); - } - if show_forward { - render_slice_side("forward", &res.forward, &res.variable, &mut out); - } - - out.push_str(&format!( - " {}\n", - c_muted(&format!("→ tr {} | tl {}", res.function, res.variable)), - )); - - out.push('\n'); - out -} - -fn render_slice_side(label: &str, entries: &[SliceEntry], var: &str, out: &mut String) { - out.push_str(&format!(" {}\n", c_warn(&format!("[{}]", label)))); - if entries.is_empty() { - let reason = if label == "backward" { - format!("no definitions of '{}' found — may be a parameter or set only in constructor/modifier", var) - } else { - format!("no statements depend on '{}' after its definition in this function", var) - }; - out.push_str(&format!(" {}\n", c_muted(&reason))); - return; - } - for entry in entries { - let line_tag = entry.span - .map(|s| format!("L{:<4}", s.start_line)) - .unwrap_or_else(|| "L? ".into()); - let origin_tag = match &entry.origin { - StatementOrigin::FunctionBody => String::new(), - StatementOrigin::Modifier(name) => format!("{} ", c_accent(&format!("[mod {}]", name))), - }; - out.push_str(&format!( - " {} {}{}\n", - c_muted(&line_tag), - origin_tag, - entry.text, - )); - } -} - -pub fn render_scenario_list(items: &[ScenarioInfo]) -> String { - if items.is_empty() { - return format!(" {}\n", c_muted("(no scenarios)")); - } - - let name_width = items - .iter() - .map(|s| s.name.chars().count()) - .max() - .unwrap_or(0) - .max(4); - - let title = format!( - "scenarios — {} total, active: {}", - items.len(), - items - .iter() - .find(|s| s.active) - .map(|s| s.name.as_str()) - .unwrap_or("?"), - ); - let header = format!( - " {} {} {}", - pad_right("", 1), - pad_right("name", name_width), - "steps", - ); - let mut lines: Vec<String> = vec![title, header]; - for s in items { - let marker = if s.active { "→" } else { " " }; - lines.push(format!( - " {} {} {}", - marker, - pad_right(&s.name, name_width), - s.step_count, - )); - } - let refs: Vec<&str> = lines.iter().map(|s| s.as_str()).collect(); - let mut out = header_box(&refs); - out.push('\n'); - out -} - -pub fn render_scenario_forked(from: &str, to: &str, at_step: usize) -> String { - format!( - " {} Forked '{}' → '{}' at step {}", - c_ok("✓"), - c_accent(from), - c_accent(to), - at_step, - ) -} - -pub fn render_scenario_created(name: &str) -> String { - format!(" {} Created scenario '{}'", c_ok("✓"), c_accent(name)) -} - -pub fn render_scenario_switched(from: &str, to: &str) -> String { - if from == to { - format!(" {} Already on scenario '{}'", c_muted("·"), c_accent(to)) - } else { - format!( - " {} Switched: '{}' → '{}'", - c_ok("✓"), - c_accent(from), - c_accent(to), - ) - } -} - -pub fn render_scenario_deleted(name: &str) -> String { - format!(" {} Deleted scenario '{}'", c_ok("✓"), c_accent(name)) -} - -#[cfg(test)] -mod tests { - use super::*; - - fn strip_ansi(s: &str) -> String { - let mut out = String::with_capacity(s.len()); - let mut in_esc = false; - for c in s.chars() { - if in_esc { - if c == 'm' { - in_esc = false; - } - continue; - } - if c == '\u{1b}' { - in_esc = true; - continue; - } - out.push(c); - } - out - } - - fn make_require_tree(message: Option<&str>) -> FlowTree { - FlowTree { - contract: "C".into(), - function: "f".into(), - signature: "f()".into(), - modifiers: vec![], - max_depth: 2, - root: FlowNode { - step_id: 0, - depth: 0, - kind: FlowKind::Entry { signature: "f()".into() }, - from_modifier: None, - children: vec![FlowNode { - step_id: 1, - depth: 1, - kind: FlowKind::Require { - condition: "x == 1".into(), - message: message.map(|s| s.into()), - }, - from_modifier: None, - children: vec![], - }], - }, - } - } - - #[test] - fn render_require_message_not_double_escaped() { - let tree = make_require_tree(Some("\"LOCKED\"")); - let rendered = strip_ansi(&render_flow_tree(&tree)); - - assert!( - rendered.contains("require(x == 1, \"LOCKED\")"), - "expected unescaped message in output.\nrendered:\n{}", - rendered, - ); - assert!( - !rendered.contains("\\\""), - "rendered output must not contain backslash-escaped quotes.\nrendered:\n{}", - rendered, - ); - } - - #[test] - fn render_require_without_message() { - let tree = make_require_tree(None); - let rendered = strip_ansi(&render_flow_tree(&tree)); - assert!( - rendered.contains("require(x == 1)"), - "rendered:\n{}", - rendered, - ); - } -} diff --git a/crates/ilold-cli/src/interactive.rs b/crates/ilold-cli/src/interactive.rs deleted file mode 100644 index 826f35b..0000000 --- a/crates/ilold-cli/src/interactive.rs +++ /dev/null @@ -1,508 +0,0 @@ -use std::collections::HashSet; -use std::io; -use std::time::Duration; - -use crossterm::event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind}; -use crossterm::execute; -use crossterm::terminal::{ - disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen, -}; -use ratatui::backend::CrosstermBackend; -use ratatui::layout::{Constraint, Direction, Layout, Rect}; -use ratatui::style::{Color, Modifier, Style}; -use ratatui::text::{Line, Span}; -use ratatui::widgets::{Block, Borders, Clear, List, ListItem, ListState, Paragraph, Wrap}; -use ratatui::{Frame, Terminal}; - -use ilold_core::narrative::trace::{FlowKind, FlowNode, FlowTree}; - -struct TerminalGuard; - -impl TerminalGuard { - fn new() -> io::Result<Self> { - enable_raw_mode()?; - let mut stdout = io::stdout(); - execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; - Ok(TerminalGuard) - } -} - -impl Drop for TerminalGuard { - fn drop(&mut self) { - let _ = disable_raw_mode(); - let _ = execute!(io::stdout(), LeaveAlternateScreen, DisableMouseCapture); - } -} - -struct ViewerState { - tree: FlowTree, - collapsed: HashSet<usize>, - selected_step_id: usize, - show_help: bool, -} - -impl ViewerState { - fn new(tree: FlowTree) -> Self { - let selected_step_id = tree.root.step_id; - ViewerState { - tree, - collapsed: HashSet::new(), - selected_step_id, - show_help: false, - } - } - - fn flatten(&self) -> Vec<FlatRow<'_>> { - let mut out = Vec::new(); - flatten_node(&self.tree.root, "", true, true, &self.collapsed, &mut out); - out - } - - fn snapshot(&self) -> Vec<RowSnapshot> { - self.flatten() - .iter() - .map(|r| RowSnapshot { - step_id: r.node.step_id, - has_children: r.has_children, - is_collapsed: r.is_collapsed, - }) - .collect() - } - - fn cursor_in(&self, flat: &[FlatRow<'_>]) -> usize { - flat.iter() - .position(|r| r.node.step_id == self.selected_step_id) - .unwrap_or(0) - } - - fn cursor_in_snapshot(&self, snap: &[RowSnapshot]) -> usize { - snap.iter() - .position(|r| r.step_id == self.selected_step_id) - .unwrap_or(0) - } -} - -#[derive(Debug, Clone, Copy)] -struct RowSnapshot { - step_id: usize, - has_children: bool, - is_collapsed: bool, -} - -struct FlatRow<'a> { - node: &'a FlowNode, - prefix: String, - has_children: bool, - is_collapsed: bool, -} - -fn flatten_node<'a>( - node: &'a FlowNode, - parent_prefix: &str, - is_last: bool, - is_root: bool, - collapsed: &HashSet<usize>, - out: &mut Vec<FlatRow<'a>>, -) { - let connector = if is_root { - "" - } else if is_last { - "└─ " - } else { - "├─ " - }; - let prefix = format!("{}{}", parent_prefix, connector); - - let has_children = !node.children.is_empty(); - let is_collapsed = collapsed.contains(&node.step_id); - - out.push(FlatRow { - node, - prefix, - has_children, - is_collapsed, - }); - - if has_children && !is_collapsed { - let extension = if is_root { - "" - } else if is_last { - " " - } else { - "│ " - }; - let new_prefix = format!("{}{}", parent_prefix, extension); - let n = node.children.len(); - for (i, child) in node.children.iter().enumerate() { - flatten_node(child, &new_prefix, i == n - 1, false, collapsed, out); - } - } -} - -pub fn run_trace_viewer(tree: FlowTree) -> io::Result<()> { - let _guard = TerminalGuard::new()?; - - let backend = CrosstermBackend::new(io::stdout()); - let mut terminal = Terminal::new(backend)?; - - let mut state = ViewerState::new(tree); - run_loop(&mut terminal, &mut state)?; - - terminal.show_cursor()?; - Ok(()) -} - -fn draw_ui(frame: &mut Frame, state: &ViewerState, flat: &[FlatRow<'_>]) { - let area = frame.area(); - - let chunks = Layout::default() - .direction(Direction::Vertical) - .constraints([ - Constraint::Length(3), - Constraint::Min(1), - Constraint::Length(1), - ]) - .split(area); - - draw_header(frame, chunks[0], state); - draw_list(frame, chunks[1], state, flat); - draw_footer(frame, chunks[2]); - - if state.show_help { - draw_help_overlay(frame, area); - } -} - -fn draw_help_overlay(frame: &mut Frame, area: Rect) { - let width = area.width.min(64); - let height = area.height.min(26); - let x = area.x + (area.width.saturating_sub(width)) / 2; - let y = area.y + (area.height.saturating_sub(height)) / 2; - let popup = Rect { x, y, width, height }; - - frame.render_widget(Clear, popup); - - let cyan = Style::default().fg(Color::Cyan); - let gray = Style::default().fg(Color::Gray); - let yellow = Style::default().fg(Color::Yellow); - let bold_white = Style::default().fg(Color::White).add_modifier(Modifier::BOLD); - - let lines: Vec<Line> = vec![ - Line::from(Span::styled("Navigation", bold_white)), - Line::from(vec![ - Span::styled(" ↑/↓ k/j ", cyan), - Span::styled("move cursor", gray), - ]), - Line::from(vec![ - Span::styled(" g Home ", cyan), - Span::styled("jump to top", gray), - ]), - Line::from(vec![ - Span::styled(" G End ", cyan), - Span::styled("jump to bottom", gray), - ]), - Line::from(""), - Line::from(Span::styled("Tree control", bold_white)), - Line::from(vec![ - Span::styled(" → Enter l ", cyan), - Span::styled("expand current", gray), - ]), - Line::from(vec![ - Span::styled(" ← h ", cyan), - Span::styled("collapse (leaf → parent)", gray), - ]), - Line::from(""), - Line::from(Span::styled("Icons", bold_white)), - Line::from(vec![ - Span::styled(" ▶ ", yellow), - Span::styled("collapsed ", gray), - Span::styled("▼ ", yellow), - Span::styled("expanded", gray), - ]), - Line::from(vec![ - Span::styled(" ◇ ", gray), - Span::styled("require ", gray), - Span::styled("✏ ", Style::default().fg(Color::Red)), - Span::styled("write", gray), - ]), - Line::from(vec![ - Span::styled(" → ", Style::default().fg(Color::Red)), - Span::styled("ext call ", gray), - Span::styled("○ ", yellow), - Span::styled("internal call", gray), - ]), - Line::from(vec![ - Span::styled(" ◆ ", Style::default().fg(Color::Cyan)), - Span::styled("emit event ", gray), - Span::styled("? ", yellow), - Span::styled("branch", gray), - ]), - Line::from(vec![ - Span::styled(" ✓ ", Style::default().fg(Color::Green)), - Span::styled("return ", gray), - Span::styled("✗ ", Style::default().fg(Color::Red)), - Span::styled("revert", gray), - ]), - Line::from(""), - Line::from(Span::styled("Tags", bold_white)), - Line::from(vec![ - Span::styled(" [from: name] ", yellow), - Span::styled("originated in modifier", gray), - ]), - Line::from(vec![ - Span::styled(" [+K ops, depth ", yellow), - Span::styled("call body hidden;", gray), - ]), - Line::from(vec![ - Span::styled(" limited] ", yellow), - Span::styled("use --depth N to see it", gray), - ]), - Line::from(""), - Line::from(vec![ - Span::styled(" ? ", cyan), - Span::styled("toggle this help", gray), - Span::styled(" q ", cyan), - Span::styled("quit", gray), - ]), - ]; - - let block = Block::default() - .borders(Borders::ALL) - .border_style(cyan) - .title(Span::styled(" Help ", bold_white)); - let help = Paragraph::new(lines) - .block(block) - .wrap(Wrap { trim: false }); - frame.render_widget(help, popup); -} - -fn draw_header(frame: &mut Frame, area: Rect, state: &ViewerState) { - let tree = &state.tree; - let mods = if tree.modifiers.is_empty() { - "(none)".to_string() - } else { - tree.modifiers.join(", ") - }; - let title = format!( - " {}::{} │ modifiers: {} │ depth: {} ", - tree.contract, tree.signature, mods, tree.max_depth, - ); - let header = Paragraph::new(title) - .style(Style::default().fg(Color::White)) - .block( - Block::default() - .borders(Borders::ALL) - .title(" ilold trace — interactive "), - ); - frame.render_widget(header, area); -} - -fn draw_list( - frame: &mut Frame, - area: Rect, - state: &ViewerState, - flat: &[FlatRow<'_>], -) { - let items: Vec<ListItem> = flat.iter().map(format_row).collect(); - - let list = List::new(items) - .highlight_style( - Style::default() - .bg(Color::Indexed(238)) - .add_modifier(Modifier::BOLD), - ) - .highlight_symbol("▌ "); - - let mut list_state = ListState::default(); - list_state.select(Some(state.cursor_in(flat))); - frame.render_stateful_widget(list, area, &mut list_state); -} - -fn draw_footer(frame: &mut Frame, area: Rect) { - let hint = Line::from(vec![ - Span::styled(" ↑↓/jk ", Style::default().fg(Color::Cyan)), - Span::raw("navigate "), - Span::styled("→/Enter ", Style::default().fg(Color::Cyan)), - Span::raw("expand "), - Span::styled("← ", Style::default().fg(Color::Cyan)), - Span::raw("collapse "), - Span::styled("g/G ", Style::default().fg(Color::Cyan)), - Span::raw("top/bot "), - Span::styled("? ", Style::default().fg(Color::Cyan)), - Span::raw("help "), - Span::styled("q ", Style::default().fg(Color::Cyan)), - Span::raw("quit"), - ]); - let footer = Paragraph::new(hint).style(Style::default().fg(Color::DarkGray)); - frame.render_widget(footer, area); -} - -fn format_row<'a>(row: &FlatRow<'a>) -> ListItem<'a> { - let collapse_icon = if row.has_children { - if row.is_collapsed { "▶ " } else { "▼ " } - } else { - " " - }; - - let (kind_icon, kind_text) = crate::fmt::format_flow_label(&row.node.kind); - let modifier_tag = row.node.from_modifier.as_deref() - .map(|m| format!(" [from: {}]", m)) - .unwrap_or_default(); - - let line = Line::from(vec![ - Span::styled(row.prefix.clone(), Style::default().fg(Color::DarkGray)), - Span::styled(collapse_icon, Style::default().fg(Color::Yellow)), - Span::styled( - kind_icon, - Style::default().fg(kind_color(&row.node.kind)), - ), - Span::raw(" "), - Span::styled(kind_text, Style::default().fg(kind_text_color(&row.node.kind))), - Span::styled(modifier_tag, Style::default().fg(Color::Yellow)), - ]); - ListItem::new(line) -} - -fn kind_color(kind: &FlowKind) -> Color { - match kind { - FlowKind::Entry { .. } | FlowKind::EmitEvent { .. } => Color::Cyan, - FlowKind::Write { .. } - | FlowKind::StateWrite { .. } - | FlowKind::ExternalCall { .. } - | FlowKind::EthTransfer { .. } - | FlowKind::Revert { .. } => Color::Red, - FlowKind::InternalCall { .. } - | FlowKind::BranchTrue { .. } - | FlowKind::BranchFalse { .. } - | FlowKind::LoopHeader { .. } - | FlowKind::AssemblyBlock => Color::Yellow, - FlowKind::Return => Color::Green, - _ => Color::Gray, - } -} - -fn kind_text_color(kind: &FlowKind) -> Color { - match kind { - FlowKind::Entry { .. } => Color::White, - FlowKind::Write { .. } - | FlowKind::StateWrite { .. } - | FlowKind::ExternalCall { .. } - | FlowKind::EthTransfer { .. } - | FlowKind::Revert { .. } => Color::Red, - FlowKind::InternalCall { .. } - | FlowKind::BranchTrue { .. } - | FlowKind::BranchFalse { .. } => Color::Yellow, - FlowKind::Return => Color::Green, - _ => Color::Gray, - } -} - -fn run_loop( - terminal: &mut Terminal<CrosstermBackend<io::Stdout>>, - state: &mut ViewerState, -) -> io::Result<()> { - loop { - let snap = state.snapshot(); - if !snap.iter().any(|r| r.step_id == state.selected_step_id) { - if let Some(first) = snap.first() { - state.selected_step_id = first.step_id; - } - } - - { - let flat = state.flatten(); - terminal.draw(|f| draw_ui(f, state, &flat))?; - } - - if !event::poll(Duration::from_millis(200))? { - continue; - } - let ev = event::read()?; - let Event::Key(key) = ev else { continue }; - if key.kind != KeyEventKind::Press && key.kind != KeyEventKind::Repeat { - continue; - } - - if !handle_key(state, &snap, key.code) { - return Ok(()); - } - } -} - -fn handle_key(state: &mut ViewerState, snap: &[RowSnapshot], code: KeyCode) -> bool { - if state.show_help { - match code { - KeyCode::Char('q') => return false, - KeyCode::Char('?') | KeyCode::Esc | KeyCode::F(1) => { - state.show_help = false; - } - _ => {} - } - return true; - } - - match code { - KeyCode::Char('q') | KeyCode::Esc => return false, - KeyCode::Char('?') | KeyCode::F(1) => { - state.show_help = true; - } - - KeyCode::Up | KeyCode::Char('k') => { - let idx = state.cursor_in_snapshot(snap); - if idx > 0 { - state.selected_step_id = snap[idx - 1].step_id; - } - } - KeyCode::Down | KeyCode::Char('j') => { - let idx = state.cursor_in_snapshot(snap); - if idx + 1 < snap.len() { - state.selected_step_id = snap[idx + 1].step_id; - } - } - - KeyCode::Right | KeyCode::Enter | KeyCode::Char('l') => { - let idx = state.cursor_in_snapshot(snap); - if let Some(row) = snap.get(idx) { - if row.has_children && row.is_collapsed { - state.collapsed.remove(&row.step_id); - } - } - } - KeyCode::Left | KeyCode::Char('h') => { - let idx = state.cursor_in_snapshot(snap); - if let Some(row) = snap.get(idx) { - if row.has_children && !row.is_collapsed { - state.collapsed.insert(row.step_id); - } else if let Some(parent_id) = find_parent_id(&state.tree.root, row.step_id) { - state.selected_step_id = parent_id; - } - } - } - - KeyCode::Char('g') | KeyCode::Home => { - if let Some(first) = snap.first() { - state.selected_step_id = first.step_id; - } - } - KeyCode::Char('G') | KeyCode::End => { - if let Some(last) = snap.last() { - state.selected_step_id = last.step_id; - } - } - - _ => {} - } - true -} - -fn find_parent_id(node: &FlowNode, child_id: usize) -> Option<usize> { - for child in &node.children { - if child.step_id == child_id { - return Some(node.step_id); - } - if let Some(found) = find_parent_id(child, child_id) { - return Some(found); - } - } - None -} diff --git a/crates/ilold-cli/src/main.rs b/crates/ilold-cli/src/main.rs index ac2aa16..54e88ab 100644 --- a/crates/ilold-cli/src/main.rs +++ b/crates/ilold-cli/src/main.rs @@ -6,9 +6,7 @@ use ilold_solana_core::ingest::{detect, ProjectKind}; mod colors; mod explore; -mod fmt; mod help; -mod interactive; #[derive(Parser)] #[command(name = "ilold", version, about = "Solana program execution path analyzer")] From ee8435f2d2cc6747d41e9e68551bd1fbc2e577ba Mon Sep 17 00:00:00 2001 From: scab24 <git.seco@protonmail.com> Date: Sat, 16 May 2026 21:31:17 +0200 Subject: [PATCH 04/10] chore(web): drop solidity routing and serve entry points Removes the three /api/session/{function,trace,slice} routes (Solidity-only), the dual serve()/start_server() functions that built AppState from .sol paths, and the now-unused std::path::PathBuf import. The Solana entrypoints (serve_solana, start_solana_server) stay as the only public entry points; renaming to plain serve/start_server is deferred to a follow-up to keep this commit focused on the purge. --- crates/ilold-web/src/lib.rs | 55 ++++--------------------------------- 1 file changed, 6 insertions(+), 49 deletions(-) diff --git a/crates/ilold-web/src/lib.rs b/crates/ilold-web/src/lib.rs index 509e751..24fb96a 100644 --- a/crates/ilold-web/src/lib.rs +++ b/crates/ilold-web/src/lib.rs @@ -2,7 +2,6 @@ pub mod api; pub mod state; pub mod ws; -use std::path::PathBuf; use std::sync::Arc; use axum::routing::{delete, get, post, put}; @@ -31,9 +30,6 @@ fn build_router(state: Arc<AppState>) -> Router { .route("/api/session/timeline/{variable}", get(api::session::get_variable_timeline_handler)) .route("/api/session/state", get(api::session::get_state_detail)) .route("/api/session/sequence", get(api::session::get_sequence_detail)) - .route("/api/session/function/{contract}/{func}", get(api::session::get_function_detail)) - .route("/api/session/trace/{contract}/{func}", get(api::session::get_flow_trace)) - .route("/api/session/slice/{func}/{variable}", get(api::session::get_function_slice)) .route("/api/scenarios", get(api::session::get_scenarios)) .route("/api/scenarios/all", get(api::session::get_all_scenarios)) .route("/ws", get(ws::handler::ws_handler)) @@ -42,44 +38,6 @@ fn build_router(state: Arc<AppState>) -> Router { .with_state(state) } -pub async fn serve(paths: Vec<PathBuf>, port: u16, max_seq_depth: usize) -> anyhow::Result<()> { - println!("Analyzing {} file(s)...", paths.len()); - let project_root = paths.first().map(|p| p.parent().unwrap_or(p).to_path_buf()).unwrap_or_default(); - let listener = tokio::net::TcpListener::bind(format!("0.0.0.0:{port}")).await?; - let actual_port = listener.local_addr()?.port(); - let state = Arc::new(AppState::from_paths(&paths, max_seq_depth, actual_port, project_root)?); - if let Some(s) = state.solidity() { - println!( - "Ready: {} contracts, {} functions analyzed\n", - s.project.contracts.len(), - s.cfgs.len(), - ); - } - - let app = build_router(state); - println!("Server running at http://localhost:{actual_port}"); - axum::serve(listener, app).await?; - Ok(()) -} - -pub async fn start_server( - paths: Vec<PathBuf>, - port: u16, - max_seq_depth: usize, -) -> anyhow::Result<(Arc<AppState>, u16)> { - let project_root = paths.first().map(|p| p.parent().unwrap_or(p).to_path_buf()).unwrap_or_default(); - let listener = tokio::net::TcpListener::bind(format!("0.0.0.0:{port}")).await?; - let actual_port = listener.local_addr()?.port(); - let state = Arc::new(AppState::from_paths(&paths, max_seq_depth, actual_port, project_root)?); - let app = build_router(state.clone()); - - tokio::spawn(async move { - axum::serve(listener, app).await.ok(); - }); - - Ok((state, actual_port)) -} - pub async fn serve_solana(detected: DetectedProject, port: u16) -> anyhow::Result<()> { println!("Analyzing {} IDL(s)...", detected.idl_paths.len()); let project = build_solana_project(&detected)?; @@ -92,13 +50,12 @@ pub async fn serve_solana(detected: DetectedProject, port: u16) -> anyhow::Resul actual_port, detected.root.clone(), )?); - if let Some(s) = state.solana() { - println!( - "Ready: {} program(s) analyzed, {} .so loaded\n", - s.project.programs.len(), - s.program_artifacts.len() - ); - } + let s = state.solana().expect("solana backend required"); + println!( + "Ready: {} program(s) analyzed, {} .so loaded\n", + s.project.programs.len(), + s.program_artifacts.len() + ); let app = build_router(state); println!("Server running at http://localhost:{actual_port}"); axum::serve(listener, app).await?; From f7798ac287bc6af680951b591aec4be81bd23047 Mon Sep 17 00:00:00 2001 From: scab24 <git.seco@protonmail.com> Date: Sat, 16 May 2026 21:43:24 +0200 Subject: [PATCH 05/10] chore(web): collapse backend to solana-only Drops SolidityState and the dual Backend enum, replaces unwrap_solidity/require_solidity helpers with direct solana() access, rewrites api/session.rs to dispatch every command through the Solana pipeline (1074 to 538 LOC), purges Solidity routes (/api/session/{state,timeline,sequence,trace/contract/func,slice/func/var,function/contract/func} and /api/contract/*), trims the WS handler to broadcast-only (search.rs deleted), and removes six Solidity integration tests. SourceSpan moves inline next to the Solana source endpoint. --- Cargo.lock | 219 ---- crates/ilold-web/src/api/project.rs | 177 +--- crates/ilold-web/src/api/session.rs | 482 +-------- crates/ilold-web/src/lib.rs | 4 - crates/ilold-web/src/state.rs | 136 +-- crates/ilold-web/src/ws/handler.rs | 52 +- crates/ilold-web/src/ws/mod.rs | 1 - crates/ilold-web/src/ws/search.rs | 136 --- .../ilold-web/tests/contract_source_test.rs | 103 -- .../ilold-web/tests/explore_commands_test.rs | 941 ------------------ .../ilold-web/tests/scenario_commands_test.rs | 596 ----------- .../tests/scenario_endpoints_test.rs | 454 --------- crates/ilold-web/tests/scenario_ws_test.rs | 209 ---- crates/ilold-web/tests/ws_broadcast_test.rs | 148 --- 14 files changed, 72 insertions(+), 3586 deletions(-) delete mode 100644 crates/ilold-web/src/ws/search.rs delete mode 100644 crates/ilold-web/tests/contract_source_test.rs delete mode 100644 crates/ilold-web/tests/explore_commands_test.rs delete mode 100644 crates/ilold-web/tests/scenario_commands_test.rs delete mode 100644 crates/ilold-web/tests/scenario_endpoints_test.rs delete mode 100644 crates/ilold-web/tests/scenario_ws_test.rs delete mode 100644 crates/ilold-web/tests/ws_broadcast_test.rs diff --git a/Cargo.lock b/Cargo.lock index 771758a..ecd4ae2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1193,15 +1193,6 @@ dependencies = [ "serde", ] -[[package]] -name = "castaway" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" -dependencies = [ - "rustversion", -] - [[package]] name = "cc" version = "1.2.58" @@ -1333,20 +1324,6 @@ dependencies = [ "unreachable", ] -[[package]] -name = "compact_str" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb1325a1cece981e8a296ab8f0f9b63ae357bd0784a9faaf548cc7b480707a" -dependencies = [ - "castaway", - "cfg-if", - "itoa", - "rustversion", - "ryu", - "static_assertions", -] - [[package]] name = "concurrent-queue" version = "2.5.0" @@ -1690,15 +1667,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "deranged" -version = "0.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" -dependencies = [ - "powerfmt", -] - [[package]] name = "derivation-path" version = "0.2.0" @@ -2896,20 +2864,16 @@ dependencies = [ "anyhow", "clap", "colored", - "crossterm", "dirs", - "ilold-core", "ilold-help", "ilold-mcp", "ilold-render", "ilold-solana-core", "ilold-web", "nu-ansi-term", - "ratatui", "reedline", "reqwest", "serde_json", - "terminal_size", "tokio", ] @@ -3059,15 +3023,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "indoc" -version = "2.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" -dependencies = [ - "rustversion", -] - [[package]] name = "inout" version = "0.1.4" @@ -3077,19 +3032,6 @@ dependencies = [ "generic-array", ] -[[package]] -name = "instability" -version = "0.3.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5eb2d60ef19920a3a9193c3e371f726ec1dafc045dac788d0fb3704272458971" -dependencies = [ - "darling 0.23.0", - "indoc", - "proc-macro2", - "quote", - "syn 2.0.117", -] - [[package]] name = "inturn" version = "0.1.2" @@ -3238,17 +3180,6 @@ dependencies = [ "signature", ] -[[package]] -name = "kasuari" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bde5057d6143cc94e861d90f591b9303d6716c6b9602309150bd068853c10899" -dependencies = [ - "hashbrown 0.16.1", - "portable-atomic", - "thiserror 2.0.18", -] - [[package]] name = "keccak" version = "0.1.6" @@ -3417,15 +3348,6 @@ dependencies = [ "thiserror 1.0.69", ] -[[package]] -name = "line-clipping" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f50e8f47623268b5407192d26876c4d7f89d686ca130fdc53bced4814cd29f8" -dependencies = [ - "bitflags 2.11.0", -] - [[package]] name = "linux-raw-sys" version = "0.12.1" @@ -3526,15 +3448,6 @@ dependencies = [ "value-bag", ] -[[package]] -name = "lru" -version = "0.16.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593" -dependencies = [ - "hashbrown 0.16.1", -] - [[package]] name = "matchers" version = "0.2.0" @@ -3737,12 +3650,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "num-conv" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" - [[package]] name = "num-derive" version = "0.4.2" @@ -3807,15 +3714,6 @@ dependencies = [ "libm", ] -[[package]] -name = "num_threads" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" -dependencies = [ - "libc", -] - [[package]] name = "once_cell" version = "1.21.4" @@ -4107,12 +4005,6 @@ dependencies = [ "universal-hash", ] -[[package]] -name = "portable-atomic" -version = "1.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" - [[package]] name = "portable-pty" version = "0.8.1" @@ -4143,12 +4035,6 @@ dependencies = [ "zerovec", ] -[[package]] -name = "powerfmt" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" - [[package]] name = "ppv-lite86" version = "0.2.21" @@ -4395,69 +4281,6 @@ dependencies = [ "rustversion", ] -[[package]] -name = "ratatui" -version = "0.30.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1ce67fb8ba4446454d1c8dbaeda0557ff5e94d39d5e5ed7f10a65eb4c8266bc" -dependencies = [ - "instability", - "ratatui-core", - "ratatui-crossterm", - "ratatui-widgets", -] - -[[package]] -name = "ratatui-core" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ef8dea09a92caaf73bff7adb70b76162e5937524058a7e5bff37869cbbec293" -dependencies = [ - "bitflags 2.11.0", - "compact_str", - "hashbrown 0.16.1", - "indoc", - "itertools 0.14.0", - "kasuari", - "lru", - "strum 0.27.2", - "thiserror 2.0.18", - "unicode-segmentation", - "unicode-truncate", - "unicode-width", -] - -[[package]] -name = "ratatui-crossterm" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "577c9b9f652b4c121fb25c6a391dd06406d3b092ba68827e6d2f09550edc54b3" -dependencies = [ - "cfg-if", - "crossterm", - "instability", - "ratatui-core", -] - -[[package]] -name = "ratatui-widgets" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7dbfa023cd4e604c2553483820c5fe8aa9d71a42eea5aa77c6e7f35756612db" -dependencies = [ - "bitflags 2.11.0", - "hashbrown 0.16.1", - "indoc", - "instability", - "itertools 0.14.0", - "line-clipping", - "ratatui-core", - "strum 0.27.2", - "time", - "unicode-segmentation", - "unicode-width", -] - [[package]] name = "rayon" version = "1.11.0" @@ -6991,16 +6814,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "terminal_size" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874" -dependencies = [ - "rustix", - "windows-sys 0.61.2", -] - [[package]] name = "termios" version = "0.2.2" @@ -7059,27 +6872,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "time" -version = "0.3.47" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" -dependencies = [ - "deranged", - "libc", - "num-conv", - "num_threads", - "powerfmt", - "serde_core", - "time-core", -] - -[[package]] -name = "time-core" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" - [[package]] name = "tiny-keccak" version = "2.0.2" @@ -7442,17 +7234,6 @@ version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" -[[package]] -name = "unicode-truncate" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16b380a1238663e5f8a691f9039c73e1cdae598a30e9855f541d29b08b53e9a5" -dependencies = [ - "itertools 0.14.0", - "unicode-segmentation", - "unicode-width", -] - [[package]] name = "unicode-width" version = "0.2.2" diff --git a/crates/ilold-web/src/api/project.rs b/crates/ilold-web/src/api/project.rs index 3673da0..2879f59 100644 --- a/crates/ilold-web/src/api/project.rs +++ b/crates/ilold-web/src/api/project.rs @@ -4,7 +4,6 @@ use std::sync::Arc; use axum::extract::{Path, Query, State}; use axum::http::StatusCode; use axum::Json; -use ilold_core::model::common::SourceSpan; use ilold_solana_core::model::ProgramDef; use ilold_solana_core::overlay::RuntimeOverlay; use ilold_solana_core::view::ProgramView; @@ -12,7 +11,16 @@ use serde::{Deserialize, Serialize}; use solana_keypair::Signer; use syn::spanned::Spanned; -use crate::state::{require_solidity_msg, AppState, Backend}; +use crate::state::AppState; + +#[derive(Debug, Serialize)] +pub struct SourceSpan { + pub file_index: u32, + pub start_line: u32, + pub start_col: u32, + pub end_line: u32, + pub end_col: u32, +} fn find_solana_program( state: &Arc<AppState>, @@ -30,48 +38,45 @@ fn find_solana_program( #[derive(Serialize)] pub struct ProjectSummary { - pub files: usize, - pub contracts: Vec<ContractSummary>, + pub kind: &'static str, + pub programs: Vec<ProgramSummary>, } #[derive(Serialize)] -pub struct ContractSummary { +pub struct ProgramSummary { pub name: String, - pub kind: String, - pub functions: usize, - pub state_vars: usize, - pub inherits: Vec<String>, + pub program_id: String, + pub instructions: usize, + pub account_types: usize, } pub async fn get_project( State(state): State<Arc<AppState>>, ) -> Result<Json<ProjectSummary>, (StatusCode, String)> { - let s = require_solidity_msg(&state)?; - let contracts = s + let solana = state + .solana() + .ok_or((StatusCode::BAD_REQUEST, "endpoint is Solana-only".into()))?; + let programs = solana .project - .contracts + .programs .iter() - .map(|c| ContractSummary { - name: c.name.clone(), - kind: format!("{:?}", c.kind), - functions: c.functions.len(), - state_vars: c.state_vars.len(), - inherits: c.inherits.clone(), + .map(|p| ProgramSummary { + name: p.name.clone(), + program_id: p.program_id.to_string(), + instructions: p.instructions.len(), + account_types: p.account_types.len(), }) .collect(); - Ok(Json(ProjectSummary { - files: s.project.source_files.len(), - contracts, + kind: "solana", + programs, })) } #[derive(Serialize)] pub struct ProjectMap { pub kind: &'static str, - pub contracts: Vec<MapContract>, pub programs: Vec<MapProgram>, - pub relationships: Vec<MapRelationship>, } #[derive(Serialize)] @@ -95,42 +100,6 @@ pub struct MapAccountType { pub name: String, } -#[derive(Serialize)] -pub struct MapContract { - pub name: String, - pub kind: String, - pub inherits: Vec<String>, - pub functions: Vec<MapFunction>, - pub state_vars: Vec<MapStateVar>, -} - -#[derive(Serialize)] -pub struct MapFunction { - pub name: String, - pub visibility: String, - pub mutability: String, - pub path_count: usize, - pub happy_paths: usize, - pub revert_paths: usize, - pub has_external_calls: bool, -} - -#[derive(Serialize)] -pub struct MapStateVar { - pub name: String, - pub type_name: String, - pub is_constant: bool, -} - -#[derive(Serialize)] -pub struct MapRelationship { - pub from_contract: String, - pub from_function: String, - pub to_contract: String, - pub to_function: String, - pub kind: String, -} - pub async fn get_program_view( State(state): State<Arc<AppState>>, Path(name): Path<String>, @@ -189,10 +158,16 @@ pub async fn get_program_overlay( pub async fn get_project_map( State(state): State<Arc<AppState>>, ) -> Json<ProjectMap> { - match &state.backend { - Backend::Solidity(_) => Json(build_solidity_map(&state)), - Backend::Solana(s) => Json(build_solana_map(s)), - } + let solana = match state.solana() { + Some(s) => s, + None => { + return Json(ProjectMap { + kind: "solana", + programs: Vec::new(), + }); + } + }; + Json(build_solana_map(solana)) } fn build_solana_map(s: &crate::state::SolanaState) -> ProjectMap { @@ -223,83 +198,7 @@ fn build_solana_map(s: &crate::state::SolanaState) -> ProjectMap { ProjectMap { kind: "solana", - contracts: Vec::new(), programs, - relationships: Vec::new(), - } -} - -fn build_solidity_map(state: &Arc<AppState>) -> ProjectMap { - let s = state.unwrap_solidity(); - let mut contracts = Vec::new(); - let mut relationships = Vec::new(); - - for contract in &s.project.contracts { - let functions: Vec<MapFunction> = contract - .functions - .iter() - .filter(|f| !f.name.is_empty()) - .map(|f| { - let key = (contract.name.clone(), f.name.clone()); - let pt = s.path_trees.get(&key); - let has_ext = pt - .map(|p| p.paths.iter().any(|path| !path.annotations.external_calls.is_empty())) - .unwrap_or(false); - MapFunction { - name: f.name.clone(), - visibility: format!("{:?}", f.visibility), - mutability: format!("{:?}", f.mutability), - path_count: pt.map(|p| p.stats.total_paths).unwrap_or(0), - happy_paths: pt.map(|p| p.stats.happy_paths).unwrap_or(0), - revert_paths: pt.map(|p| p.stats.revert_paths).unwrap_or(0), - has_external_calls: has_ext, - } - }) - .collect(); - - let state_vars: Vec<MapStateVar> = contract - .state_vars - .iter() - .map(|sv| MapStateVar { - name: sv.name.clone(), - type_name: sv.type_name.clone(), - is_constant: sv.is_constant, - }) - .collect(); - - contracts.push(MapContract { - name: contract.name.clone(), - kind: format!("{:?}", contract.kind), - inherits: contract.inherits.clone(), - functions, - state_vars, - }); - - if let Some(cg) = s.call_graphs.get(&contract.name) { - for edge_idx in cg.edge_indices() { - let (src, dst) = cg.edge_endpoints(edge_idx).unwrap(); - let src_node = &cg[src]; - let dst_node = &cg[dst]; - let edge = &cg[edge_idx]; - - if dst_node.is_external || src_node.contract != dst_node.contract { - relationships.push(MapRelationship { - from_contract: src_node.contract.clone(), - from_function: src_node.function.clone(), - to_contract: dst_node.contract.clone(), - to_function: dst_node.function.clone(), - kind: format!("{:?}", edge.kind), - }); - } - } - } - } - - ProjectMap { - kind: "solidity", - contracts, - programs: Vec::new(), - relationships, } } diff --git a/crates/ilold-web/src/api/session.rs b/crates/ilold-web/src/api/session.rs index 289bdd0..293b8e7 100644 --- a/crates/ilold-web/src/api/session.rs +++ b/crates/ilold-web/src/api/session.rs @@ -1,35 +1,27 @@ use std::sync::Arc; -use axum::extract::{Path, Query, State}; +use axum::extract::{Path, State}; use axum::http::StatusCode; use axum::Json; use serde::{Deserialize, Serialize}; use serde_json::Value; -use ilold_core::classify::entry_points::AccessLevel; -use ilold_core::exploration::commands::{ - AnalysisData, CanvasPatch, CommandResult, ScenarioAction, ScenarioInfo, SessionCommand, - canvas_patch_from, execute_command, get_flow_tree, get_function_info, get_sequence_narrative, - get_session_state, get_step_narrative, validate_scenario_name, +use ilold_session_core::exploration::scenario::{ + ScenarioAction as SharedScenarioAction, ScenarioInfo, }; -use ilold_core::exploration::session::{ExplorationSession, ForkOrigin, VariableSummary}; -use ilold_core::journal::types::JournalEntry; -use ilold_core::exploration::timeline::{build_variable_timeline, VariableTimeline}; -use ilold_core::narrative::trace::FlowTree; -use ilold_core::narrative::types::{FunctionNarrative, SequenceNarrative}; -use ilold_core::slicing::{build_slice_result, SliceDirection, SliceResult}; -use ilold_session_core::exploration::scenario::ScenarioAction as SharedScenarioAction; +use ilold_session_core::exploration::session::{ExplorationSession, ForkOrigin}; +use ilold_session_core::journal::types::JournalEntry; use ilold_solana_core::exploration::{ canvas_patches_from_solana, execute_airdrop, execute_back, execute_call, execute_clear, - execute_coupling, execute_coverage, execute_export, execute_findings_list, execute_info, - execute_step, execute_timeline, execute_vars, execute_who, execute_finding, - execute_funcs, execute_inspect, execute_note, execute_pda, execute_session, - execute_state, execute_status, execute_time_warp, execute_users, execute_users_new, - SolanaCommand, SolanaCommandResult, + execute_coupling, execute_coverage, execute_export, execute_findings_list, execute_finding, + execute_funcs, execute_info, execute_inspect, execute_note, execute_pda, execute_session, + execute_state, execute_status, execute_step, execute_time_warp, execute_timeline, + execute_users, execute_users_new, execute_vars, execute_who, SolanaCommand, + SolanaCommandResult, }; use solana_keypair::Keypair; -use crate::state::{require_solidity_msg, AppState, Backend, ScenarioStore}; +use crate::state::{AppState, ScenarioStore}; #[derive(Deserialize)] pub struct CommandRequest { @@ -37,52 +29,6 @@ pub struct CommandRequest { pub command: Value, } -fn build_analysis_data<'a>( - state: &'a AppState, - contract_name: &str, -) -> Result<AnalysisData<'a>, (StatusCode, String)> { - let s = require_solidity_msg(state)?; - let contract = s.project.contracts.iter() - .find(|c| c.name == contract_name) - .ok_or((StatusCode::NOT_FOUND, format!("Contract '{}' not found", contract_name)))?; - - let seq_analysis = s.sequence_analyses.get(contract_name) - .ok_or((StatusCode::NOT_FOUND, "No analysis for contract".into()))?; - - let classifs = s.classifications.get(contract_name) - .ok_or((StatusCode::NOT_FOUND, "No classifications for contract".into()))?; - - Ok(AnalysisData { - project: &s.project, - contract, - cfgs: &s.cfgs, - path_trees: &s.path_trees, - behaviors: &seq_analysis.functions, - transitions: &seq_analysis.transitions, - classifications: classifs, - all_sequence_analyses: &s.sequence_analyses, - all_classifications: &s.classifications, - }) -} - -fn resolve_contract(state: &AppState, explicit: Option<&str>) -> Result<String, (StatusCode, String)> { - if let Some(name) = explicit { - return Ok(name.to_string()); - } - - let scenarios_guard = state.scenarios.read().unwrap(); - let session = scenarios_guard.active_session(); - if !session.contract.is_empty() { - return Ok(session.contract.clone()); - } - drop(scenarios_guard); - - let s = require_solidity_msg(state)?; - s.project.find_contract(None) - .map(|c| c.name.clone()) - .map_err(|e| (StatusCode::BAD_REQUEST, e)) -} - fn timestamp_now() -> String { std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) @@ -90,212 +36,11 @@ fn timestamp_now() -> String { .unwrap_or_default() } -fn reserve_name(store: &ScenarioStore, name: &str) -> Result<(), CommandResult> { - if let Err(e) = validate_scenario_name(name) { - return Err(CommandResult::Error { message: e }); - } - if store.contains(name) { - return Err(CommandResult::Error { - message: format!("Scenario '{name}' already exists"), - }); - } - Ok(()) -} - -fn execute_scenario( - store: &mut ScenarioStore, - action: ScenarioAction, - timestamp: &str, - contract: &str, -) -> CommandResult { - match action { - ScenarioAction::New { name } => { - if let Err(err) = reserve_name(store, &name) { - return err; - } - let session = ExplorationSession::new(contract, "ilold"); - store.insert(name.clone(), session); - CommandResult::ScenarioCreated { name } - } - ScenarioAction::List => { - let active = store.active().to_string(); - let items: Vec<ScenarioInfo> = store - .names() - .iter() - .map(|n| ScenarioInfo { - name: n.clone(), - active: n == &active, - step_count: store.get(n).map(|s| s.steps.len()).unwrap_or(0), - }) - .collect(); - CommandResult::ScenarioList { items } - } - ScenarioAction::Switch { name } => { - let from = store.active().to_string(); - if name == from { - return CommandResult::ScenarioSwitched { from, to: name }; - } - match store.set_active(name.clone()) { - Ok(()) => CommandResult::ScenarioSwitched { from, to: name }, - Err(e) => CommandResult::Error { message: e }, - } - } - ScenarioAction::Fork { name, at_step } => fork_scenario(store, name, at_step, timestamp), - ScenarioAction::Delete { name } => { - if name == store.active() { - return CommandResult::Error { - message: "Cannot delete active scenario — switch first.".into(), - }; - } - if store.len() == 1 { - return CommandResult::Error { - message: "Cannot delete the only remaining scenario.".into(), - }; - } - if !store.contains(&name) { - return CommandResult::Error { - message: format!("Scenario '{name}' does not exist"), - }; - } - store.remove(&name); - CommandResult::ScenarioDeleted { name } - } - } -} - -fn fork_scenario( - store: &mut ScenarioStore, - new_name: String, - at_step: Option<usize>, - timestamp: &str, -) -> CommandResult { - if let Err(err) = reserve_name(store, &new_name) { - return err; - } - let from = store.active().to_string(); - let mut cloned = store.active_session().clone(); - cloned.reset_scenario_local_observations(); - - let len = cloned.steps.len(); - let effective = match at_step { - None => len, - Some(n) if n > len => { - let noun = if len == 1 { "step" } else { "steps" }; - return CommandResult::Error { - message: format!( - "Cannot fork at step {n}: only {len} {noun} in active scenario" - ), - }; - } - Some(n) => { - cloned.steps.truncate(n); - n - } - }; - - cloned.forked_from = Some(ForkOrigin { - scenario: from.clone(), - at_step: effective, - }); - cloned.journal.record(JournalEntry::BranchCreated { - from_function: from.clone(), - branch_function: new_name.clone(), - timestamp: timestamp.to_string(), - }); - store.insert(new_name.clone(), cloned); - CommandResult::ScenarioForked { - from, - to: new_name, - at_step: effective, - } -} - pub async fn handle_command( State(state): State<Arc<AppState>>, Json(req): Json<CommandRequest>, ) -> Result<Json<Value>, (StatusCode, String)> { - if matches!(state.backend, Backend::Solana(_)) { - return handle_solana_command(state, req).await; - } - let contract_name = resolve_contract(&state, req.contract.as_deref())?; - let data = build_analysis_data(&state, &contract_name)?; - let timestamp = timestamp_now(); - let solidity_command: SessionCommand = serde_json::from_value(req.command).map_err(|e| { - ( - StatusCode::BAD_REQUEST, - format!("invalid Solidity command: {e}"), - ) - })?; - - let mut scenarios_guard = state.scenarios.write().unwrap(); - let is_persistence = matches!( - solidity_command, - SessionCommand::SaveSession | SessionCommand::LoadSession { .. } - ); - let needs_reset = !is_persistence - && scenarios_guard.active_session().contract != contract_name; - if needs_reset { - let had_steps = !scenarios_guard.active_session().steps.is_empty(); - let active_before_reset = scenarios_guard.active().to_string(); - *scenarios_guard = ScenarioStore::new_for_contract(&contract_name); - if had_steps { - state.session_tx.send(CanvasPatch::ClearAll { - scenario: active_before_reset, - }).ok(); - } - } - let result: CommandResult = match solidity_command { - SessionCommand::Scenario { sub } => { - let active_before = scenarios_guard.active().to_string(); - let result = execute_scenario(&mut scenarios_guard, sub, ×tamp, &contract_name); - if let Some(patch) = canvas_patch_from(&result, &active_before) { - state.session_tx.send(patch).ok(); - } - result - } - SessionCommand::SaveSession => { - let active_before = scenarios_guard.active().to_string(); - let result = match scenarios_guard.save_to_json(crate::state::SaveOpts::none()) { - Ok(json) => CommandResult::SessionSaved { json }, - Err(message) => CommandResult::Error { message }, - }; - if let Some(patch) = canvas_patch_from(&result, &active_before) { - state.session_tx.send(patch).ok(); - } - result - } - SessionCommand::LoadSession { json } => { - let result = match ScenarioStore::load_from_json(&json) { - Ok((loaded, _kp_bundle)) => { - let contract = loaded.contract.clone(); - let step_names: Vec<String> = loaded - .active_session() - .steps - .iter() - .map(|s| s.function.clone()) - .collect(); - *scenarios_guard = loaded; - CommandResult::SessionLoaded { contract, steps: step_names } - } - Err(message) => CommandResult::Error { message }, - }; - let active_after = scenarios_guard.active().to_string(); - if let Some(patch) = canvas_patch_from(&result, &active_after) { - state.session_tx.send(patch).ok(); - } - result - } - other => { - let active_name = scenarios_guard.active().to_string(); - let session = scenarios_guard.active_session_mut(); - let result = execute_command(other, session, &data, ×tamp); - if let Some(patch) = canvas_patch_from(&result, &active_name) { - state.session_tx.send(patch).ok(); - } - result - } - }; - Ok(Json(serde_json::to_value(result).unwrap_or(Value::Null))) + handle_solana_command(state, req).await } async fn handle_solana_command( @@ -681,7 +426,7 @@ fn solana_scenario_action( let items = scenarios .names() .iter() - .map(|n| ilold_session_core::exploration::scenario::ScenarioInfo { + .map(|n| ScenarioInfo { name: n.clone(), active: n == &active, step_count: scenarios.get(n).map(|s| s.steps.len()).unwrap_or(0), @@ -813,21 +558,6 @@ fn solana_scenario_action( } } -pub async fn get_step_detail( - State(state): State<Arc<AppState>>, - Path(step_index): Path<usize>, -) -> Result<Json<FunctionNarrative>, (StatusCode, String)> { - let scenarios_guard = state.scenarios.read().unwrap(); - let session = scenarios_guard.active_session(); - - let data = build_analysis_data(&state, &session.contract)?; - - let narrative = get_step_narrative(session, step_index, &data) - .map_err(|e| (StatusCode::NOT_FOUND, e))?; - - Ok(Json(narrative)) -} - pub async fn get_session_step_trace( State(state): State<Arc<AppState>>, Path(step_index): Path<usize>, @@ -838,165 +568,28 @@ pub async fn get_session_step_trace( let step = session.steps.get(step_index) .ok_or((StatusCode::NOT_FOUND, format!("step {} not found", step_index)))?; - let tree = step.flow_tree.clone() + let trace = step.runtime_trace.clone() .ok_or(( StatusCode::NOT_FOUND, - format!( - "step {} has no persisted trace (loaded from a pre-Phase-2a session); \ - use 'tr <func>' to rebuild from source", - step_index - ), + format!("step {} has no persisted runtime trace", step_index), ))?; - Ok(Json(tree)) -} - -pub async fn get_variable_timeline_handler( - State(state): State<Arc<AppState>>, - Path(variable): Path<String>, -) -> Result<Json<VariableTimeline>, (StatusCode, String)> { - let scenarios_guard = state.scenarios.read().unwrap(); - let session = scenarios_guard.active_session(); - - let timeline = build_variable_timeline(session, &variable); - Ok(Json(timeline)) -} - -pub async fn get_state_detail( - State(state): State<Arc<AppState>>, -) -> Result<Json<Vec<VariableSummary>>, (StatusCode, String)> { - let scenarios_guard = state.scenarios.read().unwrap(); - let session = scenarios_guard.active_session(); - - Ok(Json(get_session_state(session))) + Ok(Json(trace)) } -pub async fn get_sequence_detail( - State(state): State<Arc<AppState>>, -) -> Result<Json<SequenceNarrative>, (StatusCode, String)> { - let scenarios_guard = state.scenarios.read().unwrap(); - let session = scenarios_guard.active_session(); - - let data = build_analysis_data(&state, &session.contract)?; - - let narrative = get_sequence_narrative(session, &data) - .map_err(|e| (StatusCode::BAD_REQUEST, e))?; - - Ok(Json(narrative)) -} - -pub async fn get_function_detail( - State(state): State<Arc<AppState>>, - Path((contract_name, func_name)): Path<(String, String)>, -) -> Result<Json<FunctionNarrative>, (StatusCode, String)> { - let data = build_analysis_data(&state, &contract_name)?; - - let narrative = get_function_info(&func_name, &data) - .map_err(|e| (StatusCode::NOT_FOUND, e))?; - - Ok(Json(narrative)) -} - -#[derive(Deserialize)] -pub struct TraceQuery { - #[serde(default)] - pub depth: Option<usize>, - #[serde(default)] - pub reverts: Option<bool>, - #[serde(default)] - pub expand: Option<String>, -} - -pub async fn get_flow_trace( - State(state): State<Arc<AppState>>, - Path((contract_name, func_name)): Path<(String, String)>, - Query(params): Query<TraceQuery>, -) -> Result<Json<FlowTree>, (StatusCode, String)> { - let data = build_analysis_data(&state, &contract_name)?; - - let max_depth = params.depth.unwrap_or(2); - let include_reverts = params.reverts.unwrap_or(false); - let expand_set = parse_expand_set(params.expand.as_deref()) - .map_err(|e| (StatusCode::BAD_REQUEST, e))?; - - let tree = get_flow_tree(&func_name, &data, max_depth, include_reverts, expand_set) - .map_err(|e| (StatusCode::NOT_FOUND, e))?; - - Ok(Json(tree)) -} - -#[derive(Deserialize)] -pub struct SliceQuery { - #[serde(default)] - pub direction: Option<String>, -} - -pub async fn get_function_slice( - State(state): State<Arc<AppState>>, - Path((func_name, variable)): Path<(String, String)>, - Query(params): Query<SliceQuery>, -) -> Result<Json<SliceResult>, (StatusCode, String)> { - let contract_name = { - let guard = state.scenarios.read().unwrap(); - guard.active_session().contract.clone() - }; - - let s = require_solidity_msg(&state)?; - let contract = s.project.contracts.iter() - .find(|c| c.name == contract_name) - .ok_or(( - StatusCode::NOT_FOUND, - format!("Contract '{}' not found", contract_name), - ))?; - - let function = contract.functions.iter() - .find(|f| f.name == func_name) - .ok_or(( - StatusCode::NOT_FOUND, - format!("Function '{}' not found in {}", func_name, contract_name), - ))?; - - let direction = parse_slice_direction(params.direction.as_deref()) - .map_err(|e| (StatusCode::BAD_REQUEST, e))?; - - Ok(Json(build_slice_result( - &s.project, - contract, - function, - &variable, - direction, - ))) -} - -fn parse_slice_direction(raw: Option<&str>) -> Result<SliceDirection, String> { - let Some(value) = raw.map(str::trim).filter(|s| !s.is_empty()) else { - return Ok(SliceDirection::Both); - }; - match value.to_ascii_lowercase().as_str() { - "backward" | "back" | "b" => Ok(SliceDirection::Backward), - "forward" | "fwd" | "f" => Ok(SliceDirection::Forward), - "both" | "all" => Ok(SliceDirection::Both), - other => Err(format!( - "invalid direction {:?}, expected backward|forward|both", - other - )), - } +#[derive(Serialize)] +pub struct ScenarioSnapshot { + pub name: String, + pub steps: Vec<SessionStepView>, + pub forked_from: Option<ForkOrigin>, } #[derive(Serialize)] pub struct SessionStepView { pub function: String, - pub access: AccessLevel, pub step_index: usize, } -#[derive(Serialize)] -pub struct ScenarioSnapshot { - pub name: String, - pub steps: Vec<SessionStepView>, - pub forked_from: Option<ForkOrigin>, -} - #[derive(Serialize)] pub struct AllScenariosResponse { pub active: String, @@ -1023,27 +616,18 @@ pub async fn get_scenarios( pub async fn get_all_scenarios( State(state): State<Arc<AppState>>, ) -> Result<Json<AllScenariosResponse>, (StatusCode, String)> { - let solidity = state.solidity(); let guard = state.scenarios.read().unwrap(); let active = guard.active().to_string(); let mut scenarios: Vec<ScenarioSnapshot> = Vec::with_capacity(guard.len()); for name in guard.names() { let Some(session) = guard.get(name) else { continue }; - let classifs = solidity.and_then(|s| s.classifications.get(&session.contract)); let steps = session .steps .iter() .enumerate() - .map(|(idx, step)| { - let access = classifs - .and_then(|c| c.iter().find(|(n, _)| n == &step.function)) - .map(|(_, a)| a.clone()) - .unwrap_or(AccessLevel::Public); - SessionStepView { - function: step.function.clone(), - access, - step_index: idx, - } + .map(|(idx, step)| SessionStepView { + function: step.function.clone(), + step_index: idx, }) .collect(); scenarios.push(ScenarioSnapshot { @@ -1054,21 +638,3 @@ pub async fn get_all_scenarios( } Ok(Json(AllScenariosResponse { active, scenarios })) } - -fn parse_expand_set(raw: Option<&str>) -> Result<std::collections::HashSet<usize>, String> { - let mut set = std::collections::HashSet::new(); - let raw = match raw { - Some(s) if !s.trim().is_empty() => s, - _ => return Ok(set), - }; - for part in raw.split(',') { - let trimmed = part.trim(); - if trimmed.is_empty() { - continue; - } - let id: usize = trimmed.parse() - .map_err(|_| format!("invalid step_id in expand: {:?}", trimmed))?; - set.insert(id); - } - Ok(set) -} diff --git a/crates/ilold-web/src/lib.rs b/crates/ilold-web/src/lib.rs index 24fb96a..f3479fd 100644 --- a/crates/ilold-web/src/lib.rs +++ b/crates/ilold-web/src/lib.rs @@ -25,11 +25,7 @@ fn build_router(state: Arc<AppState>) -> Router { .route("/api/annotations/{id}", put(api::annotations::update_annotation)) .route("/api/annotations/{id}", delete(api::annotations::delete_annotation)) .route("/api/cmd", post(api::session::handle_command)) - .route("/api/session/step/{index}/narrative", get(api::session::get_step_detail)) .route("/api/session/step/{index}/trace", get(api::session::get_session_step_trace)) - .route("/api/session/timeline/{variable}", get(api::session::get_variable_timeline_handler)) - .route("/api/session/state", get(api::session::get_state_detail)) - .route("/api/session/sequence", get(api::session::get_sequence_detail)) .route("/api/scenarios", get(api::session::get_scenarios)) .route("/api/scenarios/all", get(api::session::get_all_scenarios)) .route("/ws", get(ws::handler::ws_handler)) diff --git a/crates/ilold-web/src/state.rs b/crates/ilold-web/src/state.rs index 4739699..2f879f5 100644 --- a/crates/ilold-web/src/state.rs +++ b/crates/ilold-web/src/state.rs @@ -2,25 +2,10 @@ use std::collections::HashMap; use std::path::PathBuf; use std::sync::RwLock; -use axum::http::StatusCode; use tokio::sync::broadcast; -use ilold_core::callgraph::builder::build_call_graph; -use ilold_core::exploration::commands::CanvasPatch; -use ilold_core::callgraph::types::CallGraph; -use ilold_core::cfg::builder::CfgBuilder; -use ilold_core::cfg::types::CfgGraph; -use ilold_core::classify::entry_points::{classify_all, AccessLevel}; -use ilold_core::exploration::session::ExplorationSession; -use ilold_core::model::project::Project; -use ilold_core::parse::solar_frontend::SolarParser; -use ilold_core::parse::ProjectParser; -use ilold_core::pathtree::config::PruningConfig; -use ilold_core::pathtree::types::PathTree; -use ilold_core::pathtree::walker::build_path_tree; -use ilold_core::sequence::analysis::{analyze_project, analyze_sequences, SequenceAnalysis}; -use ilold_core::sequence::builder::build_sequence_tree; -use ilold_core::sequence::types::SequenceTree; +use ilold_session_core::exploration::canvas::CanvasPatch; +use ilold_session_core::exploration::session::ExplorationSession; use ilold_solana_core::execute::VmHost; use ilold_solana_core::model::SolanaProject; use solana_address::Address; @@ -252,16 +237,6 @@ fn decode_keypair_bundle( Ok(out) } -pub struct SolidityState { - pub project: Project, - pub cfgs: HashMap<(String, String), CfgGraph>, - pub path_trees: HashMap<(String, String), PathTree>, - pub call_graphs: HashMap<String, CallGraph>, - pub sequence_trees: HashMap<String, SequenceTree>, - pub sequence_analyses: HashMap<String, SequenceAnalysis>, - pub classifications: HashMap<String, Vec<(String, AccessLevel)>>, -} - pub struct SolanaState { pub project: SolanaProject, pub program_artifacts: Vec<(Address, Vec<u8>)>, @@ -271,7 +246,6 @@ pub struct SolanaState { } pub enum Backend { - Solidity(SolidityState), Solana(SolanaState), } @@ -285,37 +259,12 @@ pub struct AppState { } impl AppState { - pub fn solidity(&self) -> Option<&SolidityState> { - match &self.backend { - Backend::Solidity(s) => Some(s), - Backend::Solana(_) => None, - } - } - pub fn solana(&self) -> Option<&SolanaState> { - match &self.backend { - Backend::Solana(s) => Some(s), - Backend::Solidity(_) => None, - } - } -} - -impl AppState { - pub fn unwrap_solidity(&self) -> &SolidityState { - self.solidity().expect("Solidity backend required") + let Backend::Solana(s) = &self.backend; + Some(s) } } -pub fn require_solidity(state: &AppState) -> Result<&SolidityState, StatusCode> { - state.solidity().ok_or(StatusCode::BAD_REQUEST) -} - -pub fn require_solidity_msg(state: &AppState) -> Result<&SolidityState, (StatusCode, String)> { - state - .solidity() - .ok_or_else(|| (StatusCode::BAD_REQUEST, "endpoint is Solidity-only".to_string())) -} - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Annotation { pub id: String, @@ -387,81 +336,4 @@ impl AppState { project_root, }) } - - pub fn from_paths(paths: &[PathBuf], max_seq_depth: usize, port: u16, project_root: PathBuf) -> anyhow::Result<Self> { - let parser = SolarParser; - let mut project = parser.parse(paths)?; - project.rebuild_index(); - - let config = PruningConfig::default(); - let mut cfgs = HashMap::new(); - let mut path_trees = HashMap::new(); - let mut call_graphs = HashMap::new(); - let mut sequence_trees = HashMap::new(); - let mut sequence_analyses = HashMap::new(); - let mut classifications = HashMap::new(); - - for contract in &project.contracts { - let cg = build_call_graph(&project, contract); - call_graphs.insert(contract.name.clone(), cg); - - let mut contract_path_trees = Vec::new(); - let combined_state_vars = project.inherited_state_vars(contract); - - for func in &contract.functions { - let key = (contract.name.clone(), func.name.clone()); - - if let Ok(cfg) = CfgBuilder::build_with_project(func, contract, Some(&project)) { - let pt = build_path_tree( - &cfg, - &contract.name, - &func.name, - &combined_state_vars, - &config, - ); - contract_path_trees.push(pt.clone()); - path_trees.insert(key.clone(), pt); - cfgs.insert(key, cfg); - } - } - - let st = build_sequence_tree(contract, &contract_path_trees, max_seq_depth); - sequence_trees.insert(contract.name.clone(), st); - - let pt_map: HashMap<(String, String), PathTree> = contract_path_trees - .iter() - .map(|pt| ((pt.contract.clone(), pt.function.clone()), pt.clone())) - .collect(); - let analysis = analyze_sequences(&pt_map, &contract.name); - sequence_analyses.insert(contract.name.clone(), analysis); - - classifications.insert(contract.name.clone(), classify_all(contract)); - } - - analyze_project(&project, &mut sequence_analyses); - - let (session_tx, _) = broadcast::channel(64); - - let default_contract = project.contracts.iter() - .find(|c| !c.name.is_empty()) - .map(|c| c.name.clone()) - .unwrap_or_else(|| "unknown".to_string()); - - Ok(Self { - backend: Backend::Solidity(SolidityState { - project, - cfgs, - path_trees, - call_graphs, - sequence_trees, - sequence_analyses, - classifications, - }), - annotations: RwLock::new(Vec::new()), - scenarios: RwLock::new(ScenarioStore::new_for_contract(default_contract)), - session_tx, - port, - project_root, - }) - } } diff --git a/crates/ilold-web/src/ws/handler.rs b/crates/ilold-web/src/ws/handler.rs index 8615340..ca60378 100644 --- a/crates/ilold-web/src/ws/handler.rs +++ b/crates/ilold-web/src/ws/handler.rs @@ -3,31 +3,18 @@ use std::sync::Arc; use axum::extract::ws::{Message, WebSocket}; use axum::extract::{State, WebSocketUpgrade}; use axum::response::IntoResponse; -use serde::{Deserialize, Serialize}; +use serde::Serialize; use tokio::sync::broadcast; -use ilold_core::classify::entry_points::AccessLevel; -use ilold_core::exploration::commands::{CanvasPatch, ScenarioEvent}; +use ilold_session_core::exploration::access::AccessLevel; +use ilold_session_core::exploration::canvas::CanvasPatch; +use ilold_session_core::exploration::scenario::ScenarioEvent; use crate::state::AppState; -use super::search::{self, SearchQuery}; - -#[derive(Deserialize)] -#[serde(tag = "type")] -enum ClientMessage { - #[serde(rename = "search")] - Search(SearchQuery), -} #[derive(Serialize)] #[serde(tag = "type")] enum ServerMessage { - #[serde(rename = "search_result")] - SearchResult(search::SearchResult), - #[serde(rename = "search_complete")] - SearchComplete { total: usize }, - #[serde(rename = "error")] - Error { message: String }, #[serde(rename = "session_add_node")] SessionAddNode { scenario: String, @@ -127,37 +114,10 @@ async fn handle_socket(mut socket: WebSocket, state: Arc<AppState>) { loop { tokio::select! { client_msg = socket.recv() => { - let msg = match client_msg { - Some(Ok(Message::Text(text))) => text, + match client_msg { + Some(Ok(Message::Text(_))) => continue, Some(Ok(Message::Close(_))) | None => break, _ => continue, - }; - - let parsed: ClientMessage = match serde_json::from_str(&msg) { - Ok(m) => m, - Err(e) => { - let err = ServerMessage::Error { message: format!("Invalid message: {e}") }; - if !send_json(&mut socket, &err).await { break; } - continue; - } - }; - - match parsed { - ClientMessage::Search(query) => { - let results = match state.solidity() { - Some(s) => search::search_paths(s, &query), - None => Vec::new(), - }; - let total = results.len(); - - for result in results { - let msg = ServerMessage::SearchResult(result); - if !send_json(&mut socket, &msg).await { break; } - } - - let complete = ServerMessage::SearchComplete { total }; - if !send_json(&mut socket, &complete).await { break; } - } } } diff --git a/crates/ilold-web/src/ws/mod.rs b/crates/ilold-web/src/ws/mod.rs index cfda953..c86cb83 100644 --- a/crates/ilold-web/src/ws/mod.rs +++ b/crates/ilold-web/src/ws/mod.rs @@ -1,3 +1,2 @@ pub mod handler; pub mod pty; -pub mod search; diff --git a/crates/ilold-web/src/ws/search.rs b/crates/ilold-web/src/ws/search.rs deleted file mode 100644 index a3dca7c..0000000 --- a/crates/ilold-web/src/ws/search.rs +++ /dev/null @@ -1,136 +0,0 @@ -use serde::{Deserialize, Serialize}; - -use crate::state::SolidityState; - -#[derive(Debug, Deserialize)] -pub struct SearchQuery { - pub query: String, - pub contract: Option<String>, - pub function: Option<String>, -} - -#[derive(Debug, Serialize)] -pub struct SearchResult { - pub contract: String, - pub function: String, - pub path_id: usize, - pub terminal: String, - pub matches: Vec<SearchMatch>, - pub depth: usize, -} - -#[derive(Debug, Serialize)] -pub struct SearchMatch { - pub field: String, - pub value: String, -} - -#[derive(Debug, Serialize)] -pub struct SearchComplete { - pub total: usize, -} - -pub fn search_paths(state: &SolidityState, query: &SearchQuery) -> Vec<SearchResult> { - let q = query.query.to_lowercase(); - let mut results = Vec::new(); - - for ((contract, function), path_tree) in &state.path_trees { - if let Some(filter_contract) = query.contract.as_ref() { - if contract != filter_contract { continue; } - } - if let Some(filter_function) = query.function.as_ref() { - if function != filter_function { continue; } - } - - for path in &path_tree.paths { - let mut matches = Vec::new(); - - for check in &path.annotations.require_checks { - if check.to_lowercase().contains(&q) { - matches.push(SearchMatch { - field: "require".into(), - value: check.clone(), - }); - } - } - - for call in &path.annotations.external_calls { - let call_str = format!("{}.{}", call.target, call.function); - if call_str.to_lowercase().contains(&q) - || "external".contains(&q) - || "ext call".contains(&q) - { - matches.push(SearchMatch { - field: "external_call".into(), - value: call_str, - }); - } - } - - for call in &path.annotations.internal_calls { - if call.to_lowercase().contains(&q) { - matches.push(SearchMatch { - field: "internal_call".into(), - value: call.clone(), - }); - } - } - - for write in &path.annotations.state_writes { - if write.to_lowercase().contains(&q) - || "state write".contains(&q) - || "write".contains(&q) - { - matches.push(SearchMatch { - field: "state_write".into(), - value: write.clone(), - }); - } - } - - for event in &path.annotations.events_emitted { - if event.to_lowercase().contains(&q) { - matches.push(SearchMatch { - field: "event".into(), - value: event.clone(), - }); - } - } - - if path.annotations.has_assembly && "assembly".contains(&q) { - matches.push(SearchMatch { - field: "assembly".into(), - value: "contains assembly block".into(), - }); - } - - let terminal_str = format!("{:?}", path.terminal); - if terminal_str.to_lowercase().contains(&q) { - matches.push(SearchMatch { - field: "terminal".into(), - value: terminal_str.clone(), - }); - } - - if function.to_lowercase().contains(&q) { - matches.push(SearchMatch { - field: "function".into(), - value: function.clone(), - }); - } - - if !matches.is_empty() { - results.push(SearchResult { - contract: contract.clone(), - function: function.clone(), - path_id: path.id, - terminal: format!("{:?}", path.terminal), - matches, - depth: path.depth, - }); - } - } - } - - results -} diff --git a/crates/ilold-web/tests/contract_source_test.rs b/crates/ilold-web/tests/contract_source_test.rs deleted file mode 100644 index 6275404..0000000 --- a/crates/ilold-web/tests/contract_source_test.rs +++ /dev/null @@ -1,103 +0,0 @@ -//! Integration tests for `GET /api/contract/:contract/:func/source`. -//! -//! Scaffolding mirrors `scenario_commands_test.rs` — starts a server on -//! port 0 (parallel-safe) against a fixture, then issues GET requests. - -use std::path::PathBuf; - -fn fixture(name: &str) -> PathBuf { - PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .parent() - .unwrap() - .parent() - .unwrap() - .join("tests/fixtures") - .join(name) -} - -async fn start_with(fixture_name: &str) -> (reqwest::Client, u16) { - let paths = vec![fixture(fixture_name)]; - let (_state, port) = ilold_web::start_server(paths, 0, 2).await.unwrap(); - (reqwest::Client::new(), port) -} - -#[tokio::test] -async fn get_function_source_returns_source_for_deposit() { - let (client, port) = start_with("staking.sol").await; - - let res = client - .get(format!( - "http://127.0.0.1:{port}/api/contract/Staking/deposit/source" - )) - .send() - .await - .unwrap(); - assert!(res.status().is_success(), "status: {}", res.status()); - - let body: serde_json::Value = res.json().await.unwrap(); - let source = body.get("source").and_then(|v| v.as_str()).unwrap(); - assert!( - source.contains("function deposit"), - "expected signature line, got:\n{source}" - ); - // Full-body guarantee: deposit writes to balances + transfers + emits. - // If the span only captured the header we would miss these. - assert!( - source.contains("balances[msg.sender]"), - "expected deposit body, got:\n{source}" - ); - assert!( - source.contains("emit Staked"), - "expected deposit emit statement, got:\n{source}" - ); - // Span must cover multiple lines. - let start_line = body.pointer("/span/start_line").and_then(|v| v.as_u64()).unwrap(); - let end_line = body.pointer("/span/end_line").and_then(|v| v.as_u64()).unwrap(); - assert!(end_line > start_line, "full-body span must span >1 lines, got: {start_line}..{end_line}"); - // File path must be absolute (CLI passes absolute paths) and end in .sol. - let file_path = body.get("file_path").and_then(|v| v.as_str()).unwrap(); - assert!(file_path.ends_with("staking.sol"), "unexpected file_path: {file_path}"); -} - -#[tokio::test] -async fn get_function_source_returns_404_for_missing_function() { - let (client, port) = start_with("staking.sol").await; - - let res = client - .get(format!( - "http://127.0.0.1:{port}/api/contract/Staking/doesNotExist/source" - )) - .send() - .await - .unwrap(); - assert_eq!( - res.status(), - reqwest::StatusCode::NOT_FOUND, - "unknown function must 404, got: {}", - res.status(), - ); -} - -#[tokio::test] -async fn get_function_source_resolves_inherited_function() { - // `Governor is TimelockController`; `schedule` is declared in the parent. - // `resolve_function` must walk the inheritance chain so that asking for - // Governor/schedule returns the parent's FunctionDef (with its span). - let (client, port) = start_with("governor.sol").await; - - let res = client - .get(format!( - "http://127.0.0.1:{port}/api/contract/Governor/schedule/source" - )) - .send() - .await - .unwrap(); - assert!(res.status().is_success(), "status: {}", res.status()); - - let body: serde_json::Value = res.json().await.unwrap(); - let source = body.get("source").and_then(|v| v.as_str()).unwrap(); - assert!( - source.contains("function schedule"), - "inherited function source missing, got:\n{source}" - ); -} diff --git a/crates/ilold-web/tests/explore_commands_test.rs b/crates/ilold-web/tests/explore_commands_test.rs deleted file mode 100644 index 9bf2172..0000000 --- a/crates/ilold-web/tests/explore_commands_test.rs +++ /dev/null @@ -1,941 +0,0 @@ -use std::path::PathBuf; - -fn fixture(name: &str) -> PathBuf { - PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .parent().unwrap() - .parent().unwrap() - .join("tests/fixtures") - .join(name) -} - -#[derive(Debug, Clone)] -struct FlowEvent { - variant: String, - payload: serde_json::Value, -} - -fn flatten_flow_tree(node: &serde_json::Value, out: &mut Vec<FlowEvent>) { - if let Some(kind) = node.get("kind") { - match kind { - serde_json::Value::String(s) => { - out.push(FlowEvent { variant: s.clone(), payload: serde_json::Value::Null }); - } - serde_json::Value::Object(map) => { - if let Some((variant, payload)) = map.iter().next() { - out.push(FlowEvent { variant: variant.clone(), payload: payload.clone() }); - } - } - _ => {} - } - } - if let Some(children) = node.get("children").and_then(|c| c.as_array()) { - for child in children { - flatten_flow_tree(child, out); - } - } -} - -#[tokio::test] -async fn vars_endpoint_returns_state_variables() { - let paths = vec![fixture("staking.sol")]; - let (_, port) = ilold_web::start_server(paths, 0, 2).await.unwrap(); - - let client = reqwest::Client::new(); - let res = client - .get(format!("http://127.0.0.1:{port}/api/contract/Staking")) - .send().await.unwrap(); - - assert!(res.status().is_success()); - let body: serde_json::Value = res.json().await.unwrap(); - - let vars = body["state_vars"].as_array().unwrap(); - assert!(!vars.is_empty(), "Should have state variables"); - - let names: Vec<&str> = vars.iter() - .filter_map(|v| v["name"].as_str()) - .collect(); - assert!(names.contains(&"balances"), "Should contain balances: {:?}", names); - assert!(names.contains(&"totalStaked"), "Should contain totalStaked: {:?}", names); - assert!(names.contains(&"rewardRate"), "Should contain rewardRate: {:?}", names); - - let balances = vars.iter().find(|v| v["name"] == "balances").unwrap(); - assert_eq!(balances["is_constant"], false); - assert_eq!(balances["is_immutable"], false); - assert!(balances["type_name"].as_str().is_some()); -} - -#[tokio::test] -async fn info_endpoint_returns_narrative_with_tree_data() { - let paths = vec![fixture("staking.sol")]; - let (_, port) = ilold_web::start_server(paths, 0, 2).await.unwrap(); - - let client = reqwest::Client::new(); - - // Need an active session first - client.post(format!("http://127.0.0.1:{port}/api/cmd")) - .json(&serde_json::json!({"contract": "Staking", "command": {"Call": {"func": "deposit"}}})) - .send().await.unwrap(); - - let res = client - .get(format!("http://127.0.0.1:{port}/api/session/function/Staking/deposit")) - .send().await.unwrap(); - - assert!(res.status().is_success()); - let body: serde_json::Value = res.json().await.unwrap(); - - assert_eq!(body["name"], "deposit"); - assert!(body["total_paths"].as_u64().unwrap() > 0); - assert!(body["state_writes"].as_array().unwrap().len() > 0); - assert!(body["modifiers"].as_array().unwrap().len() > 0); - assert!(body["external_calls"].as_array().unwrap().len() > 0); -} - -#[tokio::test] -async fn sequence_endpoint_with_two_steps() { - let paths = vec![fixture("staking.sol")]; - let (_, port) = ilold_web::start_server(paths, 0, 2).await.unwrap(); - - let client = reqwest::Client::new(); - - // Build 2-step sequence - client.post(format!("http://127.0.0.1:{port}/api/cmd")) - .json(&serde_json::json!({"contract": "Staking", "command": {"Call": {"func": "deposit"}}})) - .send().await.unwrap(); - client.post(format!("http://127.0.0.1:{port}/api/cmd")) - .json(&serde_json::json!({"contract": "Staking", "command": {"Call": {"func": "withdraw"}}})) - .send().await.unwrap(); - - let res = client - .get(format!("http://127.0.0.1:{port}/api/session/sequence")) - .send().await.unwrap(); - - assert!(res.status().is_success()); - let body: serde_json::Value = res.json().await.unwrap(); - - assert!(body["steps"].as_array().unwrap().len() >= 2); - assert!(body["observations"].as_array().is_some()); -} - -#[tokio::test] -async fn sequence_with_one_step_returns_error() { - let paths = vec![fixture("staking.sol")]; - let (_, port) = ilold_web::start_server(paths, 0, 2).await.unwrap(); - - let client = reqwest::Client::new(); - - client.post(format!("http://127.0.0.1:{port}/api/cmd")) - .json(&serde_json::json!({"contract": "Staking", "command": {"Call": {"func": "deposit"}}})) - .send().await.unwrap(); - - let res = client - .get(format!("http://127.0.0.1:{port}/api/session/sequence")) - .send().await.unwrap(); - - assert_eq!(res.status(), 400); - let body = res.text().await.unwrap(); - assert!(body.contains("at least 2 steps")); -} - -#[tokio::test] -async fn full_explore_workflow() { - let paths = vec![fixture("staking.sol")]; - let (_, port) = ilold_web::start_server(paths, 0, 2).await.unwrap(); - - let client = reqwest::Client::new(); - let cmd = |body: serde_json::Value| { - let c = client.clone(); - let p = port; - async move { - c.post(format!("http://127.0.0.1:{p}/api/cmd")) - .json(&body) - .send().await.unwrap() - .json::<serde_json::Value>().await.unwrap() - } - }; - - // 1. Functions - let res = cmd(serde_json::json!({"contract": "Staking", "command": "Functions"})).await; - let funcs = res["FunctionList"]["functions"].as_array().unwrap(); - assert!(funcs.len() >= 8); - - // 2. Call deposit - let res = cmd(serde_json::json!({"contract": "Staking", "command": {"Call": {"func": "deposit"}}})).await; - assert_eq!(res["StepAdded"]["function"], "deposit"); - assert_eq!(res["StepAdded"]["step_index"], 0); - - // 3. State after 1 step - let res = cmd(serde_json::json!({"contract": "Staking", "command": "State"})).await; - let summary = res["StateView"]["summary"].as_array().unwrap(); - assert!(!summary.is_empty()); - - // 4. Who balances - let res = cmd(serde_json::json!({"contract": "Staking", "command": {"Who": {"variable": "balances"}}})).await; - let writers = res["VariableInfo"]["writers"].as_array().unwrap(); - assert!(writers.len() >= 2, "balances should have at least 2 writers"); - - // 5. Call withdraw - let res = cmd(serde_json::json!({"contract": "Staking", "command": {"Call": {"func": "withdraw"}}})).await; - assert_eq!(res["StepAdded"]["function"], "withdraw"); - assert_eq!(res["StepAdded"]["step_index"], 1); - - // 6. Back - let res = cmd(serde_json::json!({"contract": "Staking", "command": "Back"})).await; - assert_eq!(res["StepRemoved"]["remaining"], 1); - - // 7. Session - let res = cmd(serde_json::json!({"contract": "Staking", "command": "Session"})).await; - assert_eq!(res["SessionView"]["contract"], "Staking"); - let steps = res["SessionView"]["steps"].as_array().unwrap(); - assert_eq!(steps.len(), 1); - - // 8. Clear - let res = cmd(serde_json::json!({"contract": "Staking", "command": "Clear"})).await; - assert_eq!(res, serde_json::json!("Cleared")); - - // 9. Session after clear - let res = cmd(serde_json::json!({"contract": "Staking", "command": "Session"})).await; - assert_eq!(res["SessionView"]["steps"].as_array().unwrap().len(), 0); -} - -#[tokio::test] -async fn trace_swap_shows_external_call_before_update_writes() { - let paths = vec![fixture("uniswap_v2_pair.sol")]; - let (_, port) = ilold_web::start_server(paths, 0, 2).await.unwrap(); - - let client = reqwest::Client::new(); - let res = client - .get(format!("http://127.0.0.1:{port}/api/session/trace/UniswapV2Pair/swap?depth=4")) - .send().await.unwrap(); - - assert!(res.status().is_success(), "trace endpoint failed: {}", res.status()); - let tree: serde_json::Value = res.json().await.unwrap(); - - assert_eq!(tree["contract"], "UniswapV2Pair"); - assert_eq!(tree["function"], "swap"); - assert_eq!(tree["max_depth"], 4); - let modifiers = tree["modifiers"].as_array().unwrap(); - assert!(modifiers.iter().any(|m| m == "lock")); - - let mut events: Vec<FlowEvent> = Vec::new(); - flatten_flow_tree(&tree["root"], &mut events); - - let first_external = events.iter().position(|e| { - e.variant == "ExternalCall" - && e.payload.get("function").and_then(|v| v.as_str()) == Some("transfer") - }); - assert!(first_external.is_some(), "Expected IERC20.transfer ExternalCall"); - - assert!( - events.iter().any(|e| { - e.variant == "InternalCall" - && e.payload.get("function").and_then(|v| v.as_str()) == Some("_update") - }), - "Expected _update as InternalCall", - ); - - assert!( - events.iter().any(|e| { - e.variant == "Write" - && e.payload.get("target").and_then(|v| v.as_str()) == Some("reserve0") - }), - "Expected inlined write to reserve0", - ); - - // CEI order: a reserve0 write must exist after the first external transfer. - let ext_idx = first_external.unwrap(); - assert!( - events[ext_idx..].iter().any(|e| { - e.variant == "Write" - && e.payload.get("target").and_then(|v| v.as_str()) == Some("reserve0") - }), - "Expected reserve0 write AFTER first transfer (CEI). first_external={}", - ext_idx, - ); -} - -#[tokio::test] -async fn trace_getreserves_has_no_empty_target_writes() { - // Regression for classify_expression catchall emitting fake Assignments - // for tuple returns. getReserves must have zero Write events. - let paths = vec![fixture("uniswap_v2_pair.sol")]; - let (_, port) = ilold_web::start_server(paths, 0, 2).await.unwrap(); - - let client = reqwest::Client::new(); - let res = client - .get(format!("http://127.0.0.1:{port}/api/session/trace/UniswapV2Pair/getReserves")) - .send().await.unwrap(); - - assert!(res.status().is_success()); - let tree: serde_json::Value = res.json().await.unwrap(); - - let mut events: Vec<FlowEvent> = Vec::new(); - flatten_flow_tree(&tree["root"], &mut events); - - let write_count = events.iter().filter(|e| e.variant == "Write").count(); - assert_eq!(write_count, 0, "getReserves should emit no Write events"); -} - - -#[tokio::test] -async fn trace_update_has_no_internal_calls_and_shows_state_writes() { - let paths = vec![fixture("uniswap_v2_pair.sol")]; - let (_, port) = ilold_web::start_server(paths, 0, 2).await.unwrap(); - - let client = reqwest::Client::new(); - let res = client - .get(format!("http://127.0.0.1:{port}/api/session/trace/UniswapV2Pair/_update")) - .send().await.unwrap(); - - assert!(res.status().is_success()); - let tree: serde_json::Value = res.json().await.unwrap(); - - let mut events: Vec<FlowEvent> = Vec::new(); - flatten_flow_tree(&tree["root"], &mut events); - - let has_write_to = |name: &str| { - events.iter().any(|e| { - e.variant == "Write" - && e.payload.get("target").and_then(|v| v.as_str()) == Some(name) - }) - }; - - // _update has 3 writes + 1 emit, no internal calls - assert!(has_write_to("reserve0"), "expected write to reserve0"); - assert!(has_write_to("reserve1"), "expected write to reserve1"); - assert!(events.iter().any(|e| e.variant == "EmitEvent")); - assert!(!events.iter().any(|e| e.variant == "InternalCall")); -} - -/// Collect (step_id, variant) pairs from a FlowTree in pre-order. -fn collect_step_ids(node: &serde_json::Value, out: &mut Vec<(u64, String)>) { - let step_id = node.get("step_id").and_then(|v| v.as_u64()).unwrap_or(0); - let variant = match node.get("kind") { - Some(serde_json::Value::String(s)) => s.clone(), - Some(serde_json::Value::Object(map)) => { - map.keys().next().cloned().unwrap_or_default() - } - _ => String::new(), - }; - out.push((step_id, variant)); - if let Some(children) = node.get("children").and_then(|c| c.as_array()) { - for child in children { - collect_step_ids(child, out); - } - } -} - -/// Canonical step_ids must be stable across configs that only differ -/// in max_depth and expand_set. For every step_id present in BOTH trees, -/// the FlowKind variant must match. -#[tokio::test] -async fn step_ids_are_stable_across_max_depth_configs() { - let paths = vec![fixture("uniswap_v2_pair.sol")]; - let (_, port) = ilold_web::start_server(paths, 0, 2).await.unwrap(); - - let client = reqwest::Client::new(); - let get_tree = |depth: usize| { - let client = client.clone(); - async move { - let res = client - .get(format!( - "http://127.0.0.1:{port}/api/session/trace/UniswapV2Pair/swap?depth={depth}" - )) - .send().await.unwrap(); - assert!(res.status().is_success()); - res.json::<serde_json::Value>().await.unwrap() - } - }; - - let tree_shallow = get_tree(2).await; - let tree_deep = get_tree(4).await; - - let mut shallow: Vec<(u64, String)> = Vec::new(); - collect_step_ids(&tree_shallow["root"], &mut shallow); - let mut deep: Vec<(u64, String)> = Vec::new(); - collect_step_ids(&tree_deep["root"], &mut deep); - - // Both walks must visit a non-trivial tree. - assert!(shallow.len() > 5, "shallow tree too small: {}", shallow.len()); - assert!(deep.len() > shallow.len(), - "deep tree should have at least as many nodes as shallow (deep={}, shallow={})", - deep.len(), shallow.len()); - - // Build maps keyed by step_id. - let shallow_map: std::collections::HashMap<u64, &str> = - shallow.iter().map(|(id, v)| (*id, v.as_str())).collect(); - let deep_map: std::collections::HashMap<u64, &str> = - deep.iter().map(|(id, v)| (*id, v.as_str())).collect(); - - // Every step_id that exists in both trees must map to the same variant. - let mut common_ids = 0usize; - for (id, shallow_variant) in &shallow_map { - if let Some(deep_variant) = deep_map.get(id) { - assert_eq!( - shallow_variant, deep_variant, - "step_id {} has different variants: shallow={:?}, deep={:?}", - id, shallow_variant, deep_variant, - ); - common_ids += 1; - } - } - assert!(common_ids > 5, "expected meaningful overlap, got {}", common_ids); -} - -/// After `c <func>`, the persisted session must contain a non-null -/// flow_tree on the new step AND every harvested mutation must carry a -/// flow_step_id resolving to a Write/StateWrite node in that tree. -#[tokio::test] -async fn session_step_persists_flow_tree_with_populated_flow_step_ids() { - let paths = vec![fixture("staking.sol")]; - let (_, port) = ilold_web::start_server(paths, 0, 2).await.unwrap(); - - let client = reqwest::Client::new(); - - // 1. Add deposit to the session. - let res = client - .post(format!("http://127.0.0.1:{port}/api/cmd")) - .json(&serde_json::json!({"contract": "Staking", "command": {"Call": {"func": "deposit"}}})) - .send().await.unwrap(); - assert!(res.status().is_success()); - - // 2. Save the session via the SaveSession command to get its JSON form. - let res = client - .post(format!("http://127.0.0.1:{port}/api/cmd")) - .json(&serde_json::json!({"contract": "Staking", "command": "SaveSession"})) - .send().await.unwrap(); - assert!(res.status().is_success()); - let body: serde_json::Value = res.json().await.unwrap(); - let json_str = body["SessionSaved"]["json"].as_str() - .expect("SaveSession should return a JSON string"); - let session: serde_json::Value = serde_json::from_str(json_str).unwrap(); - - // 3. Inspect the persisted step. Save format is v2: scenarios are - // keyed under `scenarios.<name>` and the active scenario name - // sits at the top level — drill in to reach the bare session. - assert_eq!(session["version"], 2); - let main = &session["scenarios"]["main"]; - assert!(!main.is_null(), "scenarios.main must be present in v2 save"); - let steps = main["steps"].as_array().unwrap(); - assert_eq!(steps.len(), 1); - let step = &steps[0]; - - // 3a. flow_tree must be non-null. - assert!( - !step["flow_tree"].is_null(), - "step.flow_tree must be persisted (not null) after c <func>" - ); - let flow_tree = &step["flow_tree"]; - assert_eq!(flow_tree["function"], "deposit"); - let root = &flow_tree["root"]; - assert!(root["children"].as_array().unwrap().len() > 0, - "persisted flow_tree must have non-empty root.children"); - - // 3b. trace_config must be persisted with default depth. - assert_eq!(step["trace_config"]["depth"], 2); - - // 3c. Every mutation must have flow_step_id = Some(_). - let mutations = step["mutations"].as_array().unwrap(); - assert!(!mutations.is_empty(), "deposit should produce at least one mutation"); - for m in mutations { - assert!( - !m["flow_step_id"].is_null(), - "mutation {:?} must have flow_step_id populated", - m - ); - } - - // 3d. Each flow_step_id must resolve to a Write or StateWrite node in the - // persisted flow_tree. - let mut tree_step_ids: std::collections::HashMap<u64, String> = std::collections::HashMap::new(); - collect_step_id_kinds(root, &mut tree_step_ids); - for m in mutations { - let id = m["flow_step_id"].as_u64().unwrap(); - let variant = tree_step_ids.get(&id) - .unwrap_or_else(|| panic!("flow_step_id {} not found in persisted tree", id)); - assert!( - variant == "Write" || variant == "StateWrite", - "flow_step_id {} resolves to {}, expected Write or StateWrite", - id, variant, - ); - } -} - -/// Session steps must model real external transactions — an internal -/// function like `_update` should be rejected with a clear error because -/// it cannot be called from outside the contract. Letting it in would -/// build an execution sequence that is impossible on-chain. -#[tokio::test] -async fn call_rejects_internal_function_as_session_entry() { - let paths = vec![fixture("uniswap_v2_pair.sol")]; - let (_, port) = ilold_web::start_server(paths, 0, 2).await.unwrap(); - - let client = reqwest::Client::new(); - let res = client - .post(format!("http://127.0.0.1:{port}/api/cmd")) - .json(&serde_json::json!({ - "contract": "UniswapV2Pair", - "command": {"Call": {"func": "_update"}} - })) - .send().await.unwrap(); - assert!(res.status().is_success(), "request should succeed — the error lives in the payload"); - - let body: serde_json::Value = res.json().await.unwrap(); - let msg = body["Error"]["message"].as_str() - .expect("expected CommandResult::Error for internal function"); - assert!(msg.contains("_update"), "error should name the function: {}", msg); - assert!(msg.contains("internal") || msg.contains("private"), - "error should explain why: {}", msg); - assert!(msg.contains("tr") || msg.contains("view"), - "error should suggest an alternative: {}", msg); - - // Public entry points must still work — sanity check the happy path. - let res = client - .post(format!("http://127.0.0.1:{port}/api/cmd")) - .json(&serde_json::json!({ - "contract": "UniswapV2Pair", - "command": {"Call": {"func": "swap"}} - })) - .send().await.unwrap(); - assert!(res.status().is_success()); - let body: serde_json::Value = res.json().await.unwrap(); - assert_eq!(body["StepAdded"]["function"], "swap"); -} - -/// `?expand=N` forces a specific InternalCall to be inlined regardless -/// of `depth`. Verifies the new query parameter wires through to the -/// walker's expand_set. -#[tokio::test] -async fn tr_swap_expand_inlines_update() { - let paths = vec![fixture("uniswap_v2_pair.sol")]; - let (_, port) = ilold_web::start_server(paths, 0, 2).await.unwrap(); - - let client = reqwest::Client::new(); - - // 1. Build the trace at depth 2 — _update should appear depth_limited. - let res = client - .get(format!("http://127.0.0.1:{port}/api/session/trace/UniswapV2Pair/swap?depth=2")) - .send().await.unwrap(); - assert!(res.status().is_success()); - let tree: serde_json::Value = res.json().await.unwrap(); - - // 2. Find the first InternalCall to _update with depth_limited=true. - let mut update_step_id: Option<u64> = None; - fn find_depth_limited_update(node: &serde_json::Value, out: &mut Option<u64>) { - if out.is_some() { return; } - if let Some(obj) = node.get("kind").and_then(|v| v.as_object()) { - if let Some(ic) = obj.get("InternalCall") { - let is_update = ic.get("function").and_then(|v| v.as_str()) == Some("_update"); - let is_limited = ic.get("depth_limited").and_then(|v| v.as_bool()) == Some(true); - if is_update && is_limited { - if let Some(id) = node.get("step_id").and_then(|v| v.as_u64()) { - *out = Some(id); - return; - } - } - } - } - if let Some(children) = node.get("children").and_then(|c| c.as_array()) { - for child in children { - find_depth_limited_update(child, out); - if out.is_some() { return; } - } - } - } - find_depth_limited_update(&tree["root"], &mut update_step_id); - let target_id = update_step_id - .expect("expected at least one depth_limited _update at depth=2"); - - // 3. Re-fetch with ?expand=<id>. Now _update at that step_id should be - // inlined (depth_limited=false) AND have non-empty children. - let res = client - .get(format!( - "http://127.0.0.1:{port}/api/session/trace/UniswapV2Pair/swap?depth=2&expand={target_id}" - )) - .send().await.unwrap(); - assert!(res.status().is_success()); - let tree2: serde_json::Value = res.json().await.unwrap(); - - // 4. Walk the new tree, find the node with target_id, and assert it's - // no longer depth_limited and has children. - fn find_node_by_id<'a>( - node: &'a serde_json::Value, - target: u64, - ) -> Option<&'a serde_json::Value> { - if node.get("step_id").and_then(|v| v.as_u64()) == Some(target) { - return Some(node); - } - if let Some(children) = node.get("children").and_then(|c| c.as_array()) { - for child in children { - if let Some(found) = find_node_by_id(child, target) { - return Some(found); - } - } - } - None - } - let expanded_node = find_node_by_id(&tree2["root"], target_id) - .expect("step_id should still exist after expand (canonical step_ids are stable)"); - - let kind = expanded_node["kind"].as_object().unwrap(); - let ic = kind.get("InternalCall") - .expect("expanded node should still be an InternalCall"); - assert_eq!( - ic.get("depth_limited").and_then(|v| v.as_bool()), - Some(false), - "step {} should be expanded (not depth_limited) after ?expand={}", - target_id, target_id - ); - - let children = expanded_node["children"].as_array().unwrap(); - assert!(!children.is_empty(), - "expanded _update must have inlined children"); - - // 5. Bonus: those children should include writes to reserve0 / reserve1. - let has_reserve_write = children.iter().any(|c| { - c.get("kind") - .and_then(|k| k.as_object()) - .and_then(|m| m.get("Write")) - .and_then(|w| w.get("target")) - .and_then(|t| t.as_str()) - .map(|s| s.starts_with("reserve")) - .unwrap_or(false) - }); - assert!(has_reserve_write, - "expanded _update children should include reserve* writes"); -} - -/// `GET /api/session/timeline/{var}` returns every mutation of `var` -/// across the session, ordered by session step then flow_step_id, with -/// path conditions populated for each entry. -#[tokio::test] -async fn timeline_balances_across_multiple_steps() { - let paths = vec![fixture("staking.sol")]; - let (_, port) = ilold_web::start_server(paths, 0, 2).await.unwrap(); - - let client = reqwest::Client::new(); - let post_call = |func: &'static str| { - let client = client.clone(); - async move { - client - .post(format!("http://127.0.0.1:{port}/api/cmd")) - .json(&serde_json::json!({"contract": "Staking", "command": {"Call": {"func": func}}})) - .send().await.unwrap() - } - }; - - assert!(post_call("deposit").await.status().is_success()); - assert!(post_call("withdraw").await.status().is_success()); - - let res = client - .get(format!("http://127.0.0.1:{port}/api/session/timeline/balances")) - .send().await.unwrap(); - assert!(res.status().is_success(), "endpoint failed: {}", res.status()); - - let tl: serde_json::Value = res.json().await.unwrap(); - assert_eq!(tl["variable"], "balances"); - - let state = tl["state_entries"].as_array().unwrap(); - assert!(state.len() >= 2, - "expected at least 2 entries (deposit + withdraw), got {}", state.len()); - - // Locals always empty in Phase 2a-2 (walker doesn't emit locals yet). - let locals = tl["local_entries"].as_array().unwrap(); - assert!(locals.is_empty()); - - // Entries are ordered by session_step_index then flow_step_id. - let mut prev_key: Option<(u64, u64)> = None; - for entry in state { - let session_idx = entry["session_step_index"].as_u64().unwrap(); - let flow_id = entry["flow_step_id"].as_u64().unwrap_or(u64::MAX); - let key = (session_idx, flow_id); - if let Some(p) = prev_key { - assert!(key >= p, "timeline entries not sorted: {:?} after {:?}", key, p); - } - prev_key = Some(key); - - // Each entry must point to its source function and carry a flow_step_id. - assert!(!entry["function"].as_str().unwrap().is_empty()); - assert!(entry["flow_step_id"].as_u64().is_some(), - "non-legacy mutation should have flow_step_id"); - // Target should start with 'balances' (our base-name match). - let target = entry["target"].as_str().unwrap(); - assert!(target.starts_with("balances"), - "target should be a balances variant, got {:?}", target); - } -} - -/// A pre-Phase-2a session JSON (no flow_tree, no flow_step_id, no scope, -/// no trace_config) must load cleanly and the existing commands must -/// still work in a degraded mode: -/// - `s` (state) renders mutations using the legacy `step N` format -/// - `seq` works without flow_summary entries -/// - `tr step <N>` returns the documented 404 for legacy sessions -#[tokio::test] -async fn legacy_session_loads_and_degrades_gracefully() { - let paths = vec![fixture("staking.sol")]; - let (_, port) = ilold_web::start_server(paths, 0, 2).await.unwrap(); - - let client = reqwest::Client::new(); - - // 1. Build a real session with two steps so we get a valid AuditJournal - // serialized form, then save it. - for func in ["deposit", "withdraw"] { - let res = client - .post(format!("http://127.0.0.1:{port}/api/cmd")) - .json(&serde_json::json!({"contract": "Staking", "command": {"Call": {"func": func}}})) - .send().await.unwrap(); - assert!(res.status().is_success()); - } - let res = client - .post(format!("http://127.0.0.1:{port}/api/cmd")) - .json(&serde_json::json!({"contract": "Staking", "command": "SaveSession"})) - .send().await.unwrap(); - let body: serde_json::Value = res.json().await.unwrap(); - let json_str = body["SessionSaved"]["json"].as_str().unwrap().to_string(); - - // 2. Extract the bare `main` ExplorationSession out of the v2 wrapper - // and strip Phase-2a fields from its steps. Sending it back as a - // bare session (no `version`/`scenarios` wrapper) exercises the v1 - // fallback path in `ScenarioStore::load_from_json`. - let v2: serde_json::Value = serde_json::from_str(&json_str).unwrap(); - let mut session = v2["scenarios"]["main"].clone(); - assert!(!session.is_null(), "v2 save must contain scenarios.main"); - { - let steps = session["steps"].as_array_mut().unwrap(); - for step in steps { - let obj = step.as_object_mut().unwrap(); - obj.remove("flow_tree"); - obj.remove("trace_config"); - if let Some(muts) = obj.get_mut("mutations").and_then(|v| v.as_array_mut()) { - for m in muts { - if let Some(m_obj) = m.as_object_mut() { - m_obj.remove("flow_step_id"); - m_obj.remove("scope"); - } - } - } - } - } - let stripped = serde_json::to_string(&session).unwrap(); - - // 3. Load the stripped session — must succeed thanks to #[serde(default)]. - let res = client - .post(format!("http://127.0.0.1:{port}/api/cmd")) - .json(&serde_json::json!({ - "contract": "Staking", - "command": {"LoadSession": {"json": stripped}} - })) - .send().await.unwrap(); - assert!(res.status().is_success(), "load failed: {}", res.status()); - - // 4. `s` (state) — change lines should NOT have a `:N` flow ref because - // the legacy mutations have flow_step_id = None. - let res = client - .post(format!("http://127.0.0.1:{port}/api/cmd")) - .json(&serde_json::json!({"contract": "Staking", "command": "State"})) - .send().await.unwrap(); - assert!(res.status().is_success()); - let body: serde_json::Value = res.json().await.unwrap(); - let summary = body["StateView"]["summary"].as_array().unwrap(); - assert!(!summary.is_empty()); - for var in summary { - for change in var["changes"].as_array().unwrap() { - let s = change.as_str().unwrap(); - assert!(s.contains("(step "), "missing step ref: {:?}", s); - // Legacy format: 'step N,' (comma after digit) — NOT 'step N:M' - assert!( - !s.contains(":") || !s.split("step ").nth(1).map(|r| r.starts_with(|c: char| c.is_ascii_digit())).unwrap_or(false) - || s.split("(step ").nth(1).and_then(|r| r.split(',').next()) - .map(|n| !n.contains(':')).unwrap_or(true), - "legacy mutation should not have flow_step ref: {:?}", s - ); - } - } - - // 5. `seq` — must work but flow_summary must be null on every step. - let res = client - .get(format!("http://127.0.0.1:{port}/api/session/sequence")) - .send().await.unwrap(); - assert!(res.status().is_success()); - let narrative: serde_json::Value = res.json().await.unwrap(); - let steps = narrative["steps"].as_array().unwrap(); - assert_eq!(steps.len(), 2); - for step in steps { - assert!(step["flow_summary"].is_null(), - "legacy step should have null flow_summary: {:?}", step); - } - - // 6. `tr step 0` — must return 404 with the documented legacy message. - let res = client - .get(format!("http://127.0.0.1:{port}/api/session/step/0/trace")) - .send().await.unwrap(); - assert_eq!(res.status(), reqwest::StatusCode::NOT_FOUND); - let err = res.text().await.unwrap(); - assert!( - err.contains("no persisted trace") || err.contains("pre-Phase-2a"), - "expected legacy-session error message, got: {:?}", err - ); -} - -/// After 2 `c <func>` calls, `seq` must return a SequenceNarrative whose -/// every step has a populated `flow_summary` with sane counts. Verifies -/// Task 1.9 enrichment is wired through the API. -#[tokio::test] -async fn sequence_narrative_includes_flow_summary_per_step() { - let paths = vec![fixture("staking.sol")]; - let (_, port) = ilold_web::start_server(paths, 0, 2).await.unwrap(); - - let client = reqwest::Client::new(); - let post_call = |func: &'static str| { - let client = client.clone(); - async move { - client - .post(format!("http://127.0.0.1:{port}/api/cmd")) - .json(&serde_json::json!({"contract": "Staking", "command": {"Call": {"func": func}}})) - .send().await.unwrap() - } - }; - - assert!(post_call("deposit").await.status().is_success()); - assert!(post_call("withdraw").await.status().is_success()); - - let res = client - .get(format!("http://127.0.0.1:{port}/api/session/sequence")) - .send().await.unwrap(); - assert!(res.status().is_success()); - let narrative: serde_json::Value = res.json().await.unwrap(); - - let steps = narrative["steps"].as_array().unwrap(); - assert_eq!(steps.len(), 2); - - for step in steps { - let summary = &step["flow_summary"]; - assert!(!summary.is_null(), - "step {:?} has null flow_summary; should have been populated by get_sequence_narrative", - step["function"]); - assert!(summary["total_steps"].as_u64().unwrap() > 0); - assert!(summary["mutation_count"].as_u64().unwrap() > 0, - "expected at least one mutation in {:?}", step["function"]); - // mutation_refs is a list mirroring mutation_count - let refs = summary["mutation_refs"].as_array().unwrap(); - assert_eq!(refs.len() as u64, summary["mutation_count"].as_u64().unwrap()); - // Each ref carries variable + flow_step_id + session_step_index - for r in refs { - assert!(!r["variable"].as_str().unwrap().is_empty()); - assert!(r["flow_step_id"].as_u64().is_some()); - } - } -} - -/// `GET /api/session/step/{N}/trace` returns the persisted FlowTree of -/// the session step. Verifies the new endpoint is wired correctly and -/// that the persisted tree round-trips through HTTP. -#[tokio::test] -async fn tr_step_endpoint_returns_persisted_flow_tree() { - let paths = vec![fixture("staking.sol")]; - let (_, port) = ilold_web::start_server(paths, 0, 2).await.unwrap(); - - let client = reqwest::Client::new(); - - // Add a step to the session. - let res = client - .post(format!("http://127.0.0.1:{port}/api/cmd")) - .json(&serde_json::json!({"contract": "Staking", "command": {"Call": {"func": "deposit"}}})) - .send().await.unwrap(); - assert!(res.status().is_success()); - - // GET the persisted trace. - let res = client - .get(format!("http://127.0.0.1:{port}/api/session/step/0/trace")) - .send().await.unwrap(); - assert!(res.status().is_success(), "endpoint failed: {}", res.status()); - - let tree: serde_json::Value = res.json().await.unwrap(); - assert_eq!(tree["function"], "deposit"); - assert_eq!(tree["max_depth"], 2); - let root = &tree["root"]; - assert!(root["children"].as_array().unwrap().len() > 0); -} - -/// Requesting an out-of-range step index returns 404. -#[tokio::test] -async fn tr_step_endpoint_404_on_unknown_step() { - let paths = vec![fixture("staking.sol")]; - let (_, port) = ilold_web::start_server(paths, 0, 2).await.unwrap(); - - let client = reqwest::Client::new(); - - // No steps added — index 0 doesn't exist. - let res = client - .get(format!("http://127.0.0.1:{port}/api/session/step/0/trace")) - .send().await.unwrap(); - assert_eq!(res.status(), reqwest::StatusCode::NOT_FOUND); -} - -/// After `c <func>`, the `s` (state) command's variable summaries must -/// include `step N:flow_id` references in each change line, proving the -/// new walker's flow_step_id is propagating through to the render layer. -#[tokio::test] -async fn state_view_renders_flow_step_refs() { - let paths = vec![fixture("staking.sol")]; - let (_, port) = ilold_web::start_server(paths, 0, 2).await.unwrap(); - - let client = reqwest::Client::new(); - - // Add a step. - let res = client - .post(format!("http://127.0.0.1:{port}/api/cmd")) - .json(&serde_json::json!({"contract": "Staking", "command": {"Call": {"func": "deposit"}}})) - .send().await.unwrap(); - assert!(res.status().is_success()); - - // Fetch the state view. - let res = client - .post(format!("http://127.0.0.1:{port}/api/cmd")) - .json(&serde_json::json!({"contract": "Staking", "command": "State"})) - .send().await.unwrap(); - assert!(res.status().is_success()); - let body: serde_json::Value = res.json().await.unwrap(); - let summary = body["StateView"]["summary"].as_array().unwrap(); - assert!(!summary.is_empty(), "deposit should produce state changes"); - - // Every change line must carry a `step 0:` prefix followed by a digit - // (the new format — `0:` is the session step, the digit is the - // flow_step_id from the canonical walker). - for var in summary { - let changes = var["changes"].as_array().unwrap(); - for change in changes { - let s = change.as_str().unwrap(); - let has_ref = s.split("step 0:").nth(1) - .map(|rest| rest.chars().next().is_some_and(|c| c.is_ascii_digit())) - .unwrap_or(false); - assert!( - has_ref, - "change line missing 'step 0:N' flow ref: {:?}", - s - ); - } - } -} - -/// Collect (step_id, FlowKind variant) pairs from a serialized FlowTree. -fn collect_step_id_kinds( - node: &serde_json::Value, - out: &mut std::collections::HashMap<u64, String>, -) { - if let Some(id) = node.get("step_id").and_then(|v| v.as_u64()) { - let variant = match node.get("kind") { - Some(serde_json::Value::String(s)) => s.clone(), - Some(serde_json::Value::Object(map)) => { - map.keys().next().cloned().unwrap_or_default() - } - _ => String::new(), - }; - out.insert(id, variant); - } - if let Some(children) = node.get("children").and_then(|c| c.as_array()) { - for child in children { - collect_step_id_kinds(child, out); - } - } -} diff --git a/crates/ilold-web/tests/scenario_commands_test.rs b/crates/ilold-web/tests/scenario_commands_test.rs deleted file mode 100644 index b32edea..0000000 --- a/crates/ilold-web/tests/scenario_commands_test.rs +++ /dev/null @@ -1,596 +0,0 @@ -//! Integration tests for scenario lifecycle commands (Phase S2 / Task 2.12). -//! -//! Each test starts a fresh server on port 0 (parallel-safe) and POSTs -//! commands through `/api/cmd`, asserting on the JSON response body. The -//! helper `cmd()` keeps each test body focused on the scenario semantics -//! rather than HTTP plumbing. - -use std::path::PathBuf; - -fn fixture(name: &str) -> PathBuf { - PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .parent() - .unwrap() - .parent() - .unwrap() - .join("tests/fixtures") - .join(name) -} - -async fn start() -> (reqwest::Client, u16) { - let paths = vec![fixture("staking.sol")]; - let (_state, port) = ilold_web::start_server(paths, 0, 2).await.unwrap(); - (reqwest::Client::new(), port) -} - -async fn cmd( - client: &reqwest::Client, - port: u16, - contract: &str, - command: serde_json::Value, -) -> serde_json::Value { - let res = client - .post(format!("http://127.0.0.1:{port}/api/cmd")) - .json(&serde_json::json!({ "contract": contract, "command": command })) - .send() - .await - .expect("POST /api/cmd failed"); - assert!( - res.status().is_success(), - "POST /api/cmd returned {}", - res.status() - ); - res.json().await.expect("response was not JSON") -} - -fn scenario_new(name: &str) -> serde_json::Value { - serde_json::json!({ "Scenario": { "sub": { "New": { "name": name } } } }) -} - -fn scenario_list() -> serde_json::Value { - serde_json::json!({ "Scenario": { "sub": "List" } }) -} - -fn scenario_switch(name: &str) -> serde_json::Value { - serde_json::json!({ "Scenario": { "sub": { "Switch": { "name": name } } } }) -} - -fn scenario_fork(name: &str) -> serde_json::Value { - serde_json::json!({ "Scenario": { "sub": { "Fork": { "name": name } } } }) -} - -fn scenario_fork_at(name: &str, at_step: usize) -> serde_json::Value { - serde_json::json!({ "Scenario": { "sub": { "Fork": { "name": name, "at_step": at_step } } } }) -} - -fn scenario_delete(name: &str) -> serde_json::Value { - serde_json::json!({ "Scenario": { "sub": { "Delete": { "name": name } } } }) -} - -fn call(func: &str) -> serde_json::Value { - serde_json::json!({ "Call": { "func": func } }) -} - -/// Extract the ScenarioList items from a CommandResult JSON body. -fn list_items(r: &serde_json::Value) -> &Vec<serde_json::Value> { - r.get("ScenarioList") - .and_then(|v| v.get("items")) - .and_then(|v| v.as_array()) - .expect("ScenarioList.items array") -} - -fn find_scenario<'a>(items: &'a [serde_json::Value], name: &str) -> &'a serde_json::Value { - items - .iter() - .find(|it| it.get("name").and_then(|n| n.as_str()) == Some(name)) - .unwrap_or_else(|| panic!("scenario '{name}' not in list: {items:?}")) -} - -#[tokio::test] -async fn scenario_new_creates_empty_scenario() { - let (client, port) = start().await; - - let r = cmd(&client, port, "Staking", scenario_new("alt1")).await; - assert_eq!( - r.get("ScenarioCreated") - .and_then(|v| v.get("name")) - .and_then(|n| n.as_str()), - Some("alt1"), - "expected ScenarioCreated{{name: alt1}}, got: {r}" - ); - - let r = cmd(&client, port, "Staking", scenario_list()).await; - let items = list_items(&r); - assert_eq!(items.len(), 2, "expected 2 scenarios, got: {items:?}"); - let main = find_scenario(items, "main"); - assert_eq!(main.get("active").and_then(|v| v.as_bool()), Some(true)); - let alt1 = find_scenario(items, "alt1"); - assert_eq!(alt1.get("active").and_then(|v| v.as_bool()), Some(false)); - assert_eq!( - alt1.get("step_count").and_then(|v| v.as_u64()), - Some(0), - "alt1 should start empty" - ); -} - -#[tokio::test] -async fn scenario_list_shows_all_with_active_marker() { - let (client, port) = start().await; - - let r = cmd(&client, port, "Staking", scenario_list()).await; - let items = list_items(&r); - assert_eq!(items.len(), 1); - let main = find_scenario(items, "main"); - assert_eq!(main.get("active").and_then(|v| v.as_bool()), Some(true)); - assert_eq!(main.get("step_count").and_then(|v| v.as_u64()), Some(0)); - - cmd(&client, port, "Staking", call("deposit")).await; - - let r = cmd(&client, port, "Staking", scenario_list()).await; - let items = list_items(&r); - let main = find_scenario(items, "main"); - assert_eq!( - main.get("step_count").and_then(|v| v.as_u64()), - Some(1), - "main should have 1 step after deposit" - ); -} - -#[tokio::test] -async fn scenario_switch_changes_active() { - let (client, port) = start().await; - - cmd(&client, port, "Staking", scenario_new("alt1")).await; - - let r = cmd(&client, port, "Staking", scenario_switch("alt1")).await; - let switched = r.get("ScenarioSwitched").expect("ScenarioSwitched variant"); - assert_eq!( - switched.get("from").and_then(|v| v.as_str()), - Some("main") - ); - assert_eq!(switched.get("to").and_then(|v| v.as_str()), Some("alt1")); - - cmd(&client, port, "Staking", call("deposit")).await; - - let r = cmd(&client, port, "Staking", scenario_list()).await; - let items = list_items(&r); - let alt1 = find_scenario(items, "alt1"); - assert_eq!(alt1.get("active").and_then(|v| v.as_bool()), Some(true)); - assert_eq!(alt1.get("step_count").and_then(|v| v.as_u64()), Some(1)); - let main = find_scenario(items, "main"); - assert_eq!(main.get("active").and_then(|v| v.as_bool()), Some(false)); - assert_eq!(main.get("step_count").and_then(|v| v.as_u64()), Some(0)); -} - -#[tokio::test] -async fn scenario_fork_clones_prefix_at_current_step() { - let (client, port) = start().await; - - cmd(&client, port, "Staking", call("deposit")).await; - - let r = cmd(&client, port, "Staking", scenario_fork("alt1")).await; - let forked = r.get("ScenarioForked").expect("ScenarioForked variant"); - assert_eq!(forked.get("from").and_then(|v| v.as_str()), Some("main")); - assert_eq!(forked.get("to").and_then(|v| v.as_str()), Some("alt1")); - assert_eq!(forked.get("at_step").and_then(|v| v.as_u64()), Some(1)); - - let r = cmd(&client, port, "Staking", scenario_list()).await; - let items = list_items(&r); - assert_eq!( - find_scenario(items, "main") - .get("step_count") - .and_then(|v| v.as_u64()), - Some(1) - ); - assert_eq!( - find_scenario(items, "alt1") - .get("step_count") - .and_then(|v| v.as_u64()), - Some(1), - "fork should copy the prefix" - ); -} - -#[tokio::test] -async fn scenario_fork_emits_branch_created_journal_entry() { - let (client, port) = start().await; - - cmd(&client, port, "Staking", call("deposit")).await; - cmd(&client, port, "Staking", scenario_fork("alt1")).await; - cmd(&client, port, "Staking", scenario_switch("alt1")).await; - - // SaveSession returns the serialized session; the journal.entries array - // is the source of truth for BranchCreated. - let r = cmd( - &client, - port, - "Staking", - serde_json::json!("SaveSession"), - ) - .await; - let json_str = r - .get("SessionSaved") - .and_then(|v| v.get("json")) - .and_then(|v| v.as_str()) - .expect("SessionSaved.json"); - - let saved: serde_json::Value = - serde_json::from_str(json_str).expect("SaveSession payload was not JSON"); - // Save format is v2 — journal lives under scenarios.<active>.journal. - let active = saved - .get("active") - .and_then(|v| v.as_str()) - .expect("active scenario name"); - let entries = saved - .pointer(&format!("/scenarios/{active}/journal/entries")) - .and_then(|e| e.as_array()) - .expect("scenarios.<active>.journal.entries array"); - - let branch = entries - .iter() - .find_map(|e| e.get("BranchCreated")) - .unwrap_or_else(|| panic!("no BranchCreated entry in {entries:?}")); - assert_eq!( - branch.get("from_function").and_then(|v| v.as_str()), - Some("main") - ); - assert_eq!( - branch.get("branch_function").and_then(|v| v.as_str()), - Some("alt1") - ); -} - -#[tokio::test] -async fn scenario_delete_refuses_when_active() { - let (client, port) = start().await; - - // `main` is active by default — deletion must fail. - let r = cmd(&client, port, "Staking", scenario_delete("main")).await; - let msg = r - .get("Error") - .and_then(|v| v.get("message")) - .and_then(|v| v.as_str()) - .expect("Error.message"); - assert!( - msg.contains("Cannot delete active scenario"), - "unexpected error message: {msg}" - ); -} - -/// The "only remaining" branch of `execute_scenario` is defensive — it is -/// unreachable via HTTP because, when only one scenario exists, it is by -/// definition the active one and the "active" check fires first. We still -/// verify that calling delete on the sole `main` scenario surfaces the -/// active-guard error (not a panic or a "does not exist" error). The internal -/// `ScenarioStore::remove` path for the only-remaining case is exercised by -/// unit tests in the core crate. -#[tokio::test] -async fn scenario_delete_only_remaining_is_guarded_by_active_check() { - let (client, port) = start().await; - - let r = cmd(&client, port, "Staking", scenario_delete("main")).await; - let msg = r - .get("Error") - .and_then(|v| v.get("message")) - .and_then(|v| v.as_str()) - .expect("Error.message"); - assert!( - msg.contains("Cannot delete active scenario"), - "expected active-guard error, got: {msg}" - ); -} - -#[tokio::test] -async fn scenario_delete_removes_other_scenario() { - let (client, port) = start().await; - - cmd(&client, port, "Staking", scenario_new("alt1")).await; - cmd(&client, port, "Staking", scenario_new("alt2")).await; - cmd(&client, port, "Staking", scenario_switch("alt1")).await; - - let r = cmd(&client, port, "Staking", scenario_delete("alt2")).await; - assert_eq!( - r.get("ScenarioDeleted") - .and_then(|v| v.get("name")) - .and_then(|n| n.as_str()), - Some("alt2"), - "expected ScenarioDeleted{{name: alt2}}, got: {r}" - ); - - let r = cmd(&client, port, "Staking", scenario_list()).await; - let items = list_items(&r); - assert_eq!(items.len(), 2); - assert_eq!( - find_scenario(items, "alt1") - .get("active") - .and_then(|v| v.as_bool()), - Some(true) - ); - assert_eq!( - find_scenario(items, "main") - .get("active") - .and_then(|v| v.as_bool()), - Some(false) - ); -} - -#[tokio::test] -async fn scenario_name_validation_rejects_invalid() { - let (client, port) = start().await; - - for bad in [ - "InvalidName!", - "1starts-with-digit", - "", - "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - ] { - let r = cmd(&client, port, "Staking", scenario_new(bad)).await; - let msg = r - .get("Error") - .and_then(|v| v.get("message")) - .and_then(|v| v.as_str()) - .unwrap_or_else(|| panic!("expected Error for name {bad:?}, got: {r}")); - assert!( - msg.contains("Invalid scenario name"), - "unexpected error for {bad:?}: {msg}" - ); - } -} - -// ── Phase S7: fork-at-step-N ──────────────────────────────────────────────── - -#[tokio::test] -async fn scenario_fork_at_step_truncates_to_n() { - let (client, port) = start().await; - - // Build main with 3 steps, then fork at step 2. - cmd(&client, port, "Staking", call("deposit")).await; - cmd(&client, port, "Staking", call("deposit")).await; - cmd(&client, port, "Staking", call("withdraw")).await; - - let r = cmd(&client, port, "Staking", scenario_fork_at("alt1", 2)).await; - let forked = r.get("ScenarioForked").expect("ScenarioForked variant"); - assert_eq!(forked.get("at_step").and_then(|v| v.as_u64()), Some(2)); - - let r = cmd(&client, port, "Staking", scenario_list()).await; - let items = list_items(&r); - assert_eq!( - find_scenario(items, "main") - .get("step_count") - .and_then(|v| v.as_u64()), - Some(3), - "main must be unchanged" - ); - assert_eq!( - find_scenario(items, "alt1") - .get("step_count") - .and_then(|v| v.as_u64()), - Some(2), - "alt1 must be truncated to first 2 steps" - ); -} - -#[tokio::test] -async fn scenario_fork_at_step_zero_creates_empty() { - let (client, port) = start().await; - - cmd(&client, port, "Staking", call("deposit")).await; - cmd(&client, port, "Staking", call("withdraw")).await; - - let r = cmd(&client, port, "Staking", scenario_fork_at("alt1", 0)).await; - assert_eq!( - r.get("ScenarioForked") - .and_then(|v| v.get("at_step")) - .and_then(|v| v.as_u64()), - Some(0) - ); - - let r = cmd(&client, port, "Staking", scenario_list()).await; - let items = list_items(&r); - assert_eq!( - find_scenario(items, "alt1") - .get("step_count") - .and_then(|v| v.as_u64()), - Some(0), - "fork at 0 must yield an empty scenario" - ); -} - -#[tokio::test] -async fn scenario_fork_at_step_greater_than_length_errors() { - let (client, port) = start().await; - - cmd(&client, port, "Staking", call("deposit")).await; - - let r = cmd(&client, port, "Staking", scenario_fork_at("alt1", 5)).await; - let msg = r - .get("Error") - .and_then(|v| v.get("message")) - .and_then(|v| v.as_str()) - .unwrap_or_else(|| panic!("expected Error, got: {r}")); - assert!( - msg.contains("Cannot fork at step 5") && msg.contains("only 1 step"), - "unexpected error: {msg}" - ); - - // Verify no scenario was created on failure. - let r = cmd(&client, port, "Staking", scenario_list()).await; - let items = list_items(&r); - assert_eq!(items.len(), 1, "failed fork must not create a scenario"); -} - -#[tokio::test] -async fn scenario_name_reserved_main_cannot_be_recreated() { - let (client, port) = start().await; - - // `main` is auto-seeded on startup, so `scenario new main` collides on - // the duplicate check. - let r = cmd(&client, port, "Staking", scenario_new("main")).await; - let msg = r - .get("Error") - .and_then(|v| v.get("message")) - .and_then(|v| v.as_str()) - .unwrap_or_else(|| panic!("expected Error, got: {r}")); - assert!( - msg.contains("already exists"), - "unexpected error message: {msg}" - ); -} - -// ── Phase S11: save/load v2 ───────────────────────────────────────────────── - -fn save_session() -> serde_json::Value { - serde_json::json!("SaveSession") -} - -fn load_session(json: &str) -> serde_json::Value { - serde_json::json!({ "LoadSession": { "json": json } }) -} - -#[tokio::test] -async fn save_produces_v2_json_with_all_scenarios_and_active() { - let (client, port) = start().await; - - cmd(&client, port, "Staking", call("deposit")).await; - cmd(&client, port, "Staking", call("deposit")).await; - cmd(&client, port, "Staking", scenario_fork_at("alt1", 1)).await; - cmd(&client, port, "Staking", scenario_switch("alt1")).await; - - let r = cmd(&client, port, "Staking", save_session()).await; - let json_str = r - .get("SessionSaved") - .and_then(|v| v.get("json")) - .and_then(|v| v.as_str()) - .expect("SessionSaved.json"); - let parsed: serde_json::Value = - serde_json::from_str(json_str).expect("v2 save JSON must parse"); - - assert_eq!(parsed["version"], 2, "v2 save must declare version 2"); - assert_eq!(parsed["contract"], "Staking"); - assert_eq!( - parsed["active"], "alt1", - "active scenario at save time must be persisted" - ); - let scenarios = parsed["scenarios"] - .as_object() - .expect("scenarios must be an object"); - assert!(scenarios.contains_key("main") && scenarios.contains_key("alt1")); - assert_eq!( - parsed["order"] - .as_array() - .expect("order array") - .iter() - .map(|v| v.as_str().unwrap()) - .collect::<Vec<_>>(), - vec!["main", "alt1"], - ); - let alt1 = &scenarios["alt1"]; - assert_eq!(alt1["forked_from"]["scenario"], "main"); - assert_eq!(alt1["forked_from"]["at_step"], 1); -} - -#[tokio::test] -async fn load_v2_json_restores_all_scenarios_active_and_order() { - let (client, port) = start().await; - - // Build state, save, then mutate the live store so we can verify load - // restores the captured snapshot (not the post-save state). - cmd(&client, port, "Staking", call("deposit")).await; - cmd(&client, port, "Staking", scenario_fork_at("alt1", 1)).await; - let saved = cmd(&client, port, "Staking", save_session()).await; - let json = saved["SessionSaved"]["json"].as_str().unwrap().to_string(); - - // Mutate post-save: add a step and a new scenario that should disappear. - cmd(&client, port, "Staking", call("withdraw")).await; - cmd(&client, port, "Staking", scenario_new("ghost")).await; - - // Load → must replace the entire store with the saved snapshot. - cmd(&client, port, "Staking", load_session(&json)).await; - - let r = cmd(&client, port, "Staking", scenario_list()).await; - let items = list_items(&r); - let names: Vec<&str> = items - .iter() - .map(|it| it.get("name").and_then(|n| n.as_str()).unwrap()) - .collect(); - assert_eq!(names, vec!["main", "alt1"], "ghost scenario must be gone"); - assert_eq!( - find_scenario(items, "main") - .get("step_count") - .and_then(|v| v.as_u64()), - Some(1), - "main step count must reflect the snapshot, not post-save mutations" - ); - assert_eq!( - find_scenario(items, "alt1") - .get("step_count") - .and_then(|v| v.as_u64()), - Some(1) - ); - assert_eq!( - find_scenario(items, "main") - .get("active") - .and_then(|v| v.as_bool()), - Some(true), - "saved active was 'main'" - ); -} - -#[tokio::test] -async fn load_v1_json_wraps_as_single_main_scenario() { - let (client, port) = start().await; - - // A v1 file is a bare ExplorationSession JSON. Build one programmatically - // by saving a single-scenario state, then unwrapping the v2 envelope. - cmd(&client, port, "Staking", call("deposit")).await; - let saved = cmd(&client, port, "Staking", save_session()).await; - let v2_json = saved["SessionSaved"]["json"].as_str().unwrap(); - let v2: serde_json::Value = serde_json::from_str(v2_json).unwrap(); - let bare = v2["scenarios"]["main"].clone(); - let v1_json = serde_json::to_string(&bare).unwrap(); - - // Replace the store with something different first to prove load resets it. - cmd(&client, port, "Staking", scenario_new("alt1")).await; - cmd(&client, port, "Staking", load_session(&v1_json)).await; - - let r = cmd(&client, port, "Staking", scenario_list()).await; - let items = list_items(&r); - assert_eq!( - items.len(), - 1, - "v1 load must collapse the store to a single scenario, got: {items:?}" - ); - let only = &items[0]; - assert_eq!(only.get("name").and_then(|v| v.as_str()), Some("main")); - assert_eq!(only.get("step_count").and_then(|v| v.as_u64()), Some(1)); - assert_eq!(only.get("active").and_then(|v| v.as_bool()), Some(true)); -} - -#[tokio::test] -async fn load_replaces_existing_store_destructively() { - let (client, port) = start().await; - - // Save an empty fresh store. - let saved = cmd(&client, port, "Staking", save_session()).await; - let json = saved["SessionSaved"]["json"].as_str().unwrap().to_string(); - - // Build a rich state after saving. - cmd(&client, port, "Staking", call("deposit")).await; - cmd(&client, port, "Staking", call("withdraw")).await; - cmd(&client, port, "Staking", scenario_new("alt1")).await; - cmd(&client, port, "Staking", scenario_new("alt2")).await; - - // Load the empty snapshot — must wipe everything except `main` (empty). - cmd(&client, port, "Staking", load_session(&json)).await; - - let r = cmd(&client, port, "Staking", scenario_list()).await; - let items = list_items(&r); - assert_eq!(items.len(), 1, "load must drop alt1/alt2"); - assert_eq!( - find_scenario(items, "main") - .get("step_count") - .and_then(|v| v.as_u64()), - Some(0), - "main must be empty per snapshot" - ); -} diff --git a/crates/ilold-web/tests/scenario_endpoints_test.rs b/crates/ilold-web/tests/scenario_endpoints_test.rs deleted file mode 100644 index 26c1899..0000000 --- a/crates/ilold-web/tests/scenario_endpoints_test.rs +++ /dev/null @@ -1,454 +0,0 @@ -//! Integration tests verifying existing `/api/session/*` endpoints target the -//! ACTIVE scenario after the ScenarioStore refactor. -//! -//! These are regression tests: the behavior with the single (auto-seeded) -//! "main" scenario must be unchanged. The spec S12 case "switching active -//! scenario flips endpoint output" lands in Phase S2 when the switch command -//! exists. - -use std::path::PathBuf; - -fn fixture(name: &str) -> PathBuf { - PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .parent() - .unwrap() - .parent() - .unwrap() - .join("tests/fixtures") - .join(name) -} - -/// Fresh server with no auto-seeded steps. Mirrors `start()` in -/// `scenario_commands_test.rs` — duplicated because integration test files -/// cannot share helper modules without extra plumbing. -async fn start() -> (reqwest::Client, u16) { - let paths = vec![fixture("staking.sol")]; - let (_state, port) = ilold_web::start_server(paths, 0, 2).await.unwrap(); - (reqwest::Client::new(), port) -} - -/// POST /api/cmd helper. Same shape as `cmd()` in `scenario_commands_test.rs`. -async fn cmd( - client: &reqwest::Client, - port: u16, - contract: &str, - command: serde_json::Value, -) -> serde_json::Value { - let res = client - .post(format!("http://127.0.0.1:{port}/api/cmd")) - .json(&serde_json::json!({ "contract": contract, "command": command })) - .send() - .await - .expect("POST /api/cmd failed"); - assert!( - res.status().is_success(), - "POST /api/cmd returned {}", - res.status() - ); - res.json().await.expect("response was not JSON") -} - -async fn start_with_staking() -> (reqwest::Client, u16) { - let paths = vec![fixture("staking.sol")]; - let (_state, port) = ilold_web::start_server(paths, 0, 2).await.unwrap(); - let client = reqwest::Client::new(); - - // Add a step to the auto-seeded active "main" scenario. - let res = client - .post(format!("http://127.0.0.1:{port}/api/cmd")) - .json(&serde_json::json!({ - "contract": "Staking", - "command": { "Call": { "func": "deposit" } } - })) - .send() - .await - .expect("POST /api/cmd failed"); - assert!( - res.status().is_success(), - "seed POST /api/cmd returned {}", - res.status() - ); - - (client, port) -} - -#[tokio::test] -async fn existing_state_endpoint_targets_active_scenario() { - let (client, port) = start_with_staking().await; - - let res = client - .get(format!("http://127.0.0.1:{port}/api/session/state")) - .send() - .await - .unwrap(); - - assert!( - res.status().is_success(), - "GET /api/session/state returned {}", - res.status() - ); - let body: serde_json::Value = res.json().await.unwrap(); - assert!( - body.is_array(), - "response should be a JSON array of VariableSummary, got: {body}" - ); - // `deposit` mutates `balances` and `totalStaked` on the active scenario. - let arr = body.as_array().unwrap(); - assert!( - !arr.is_empty(), - "active scenario state should have at least one variable summary after `c deposit`" - ); -} - -#[tokio::test] -async fn existing_sequence_endpoint_targets_active_scenario() { - let (client, port) = start_with_staking().await; - - // `sequence` requires at least two steps in the active scenario. - let res = client - .post(format!("http://127.0.0.1:{port}/api/cmd")) - .json(&serde_json::json!({ - "contract": "Staking", - "command": { "Call": { "func": "withdraw" } } - })) - .send() - .await - .unwrap(); - assert!( - res.status().is_success(), - "seed second step POST returned {}", - res.status() - ); - - let res = client - .get(format!("http://127.0.0.1:{port}/api/session/sequence")) - .send() - .await - .unwrap(); - - assert!( - res.status().is_success(), - "GET /api/session/sequence returned {}", - res.status() - ); - let body: serde_json::Value = res.json().await.unwrap(); - assert!( - body.is_object(), - "response should be a SequenceNarrative object, got: {body}" - ); -} - -#[tokio::test] -async fn existing_timeline_endpoint_targets_active_scenario() { - let (client, port) = start_with_staking().await; - - // `deposit` writes `balances[msg.sender]`. - let res = client - .get(format!( - "http://127.0.0.1:{port}/api/session/timeline/balances" - )) - .send() - .await - .unwrap(); - - assert!( - res.status().is_success(), - "GET /api/session/timeline/balances returned {}", - res.status() - ); - let body: serde_json::Value = res.json().await.unwrap(); - assert!( - body.is_object(), - "response should be a VariableTimeline object, got: {body}" - ); -} - -#[tokio::test] -async fn existing_narrative_endpoint_targets_active_scenario() { - let (client, port) = start_with_staking().await; - - let res = client - .get(format!( - "http://127.0.0.1:{port}/api/session/step/0/narrative" - )) - .send() - .await - .unwrap(); - - assert!( - res.status().is_success(), - "GET /api/session/step/0/narrative returned {}", - res.status() - ); - let body: serde_json::Value = res.json().await.unwrap(); - assert!( - body.is_object(), - "response should be a FunctionNarrative object, got: {body}" - ); -} - -#[tokio::test] -async fn existing_trace_endpoint_targets_active_scenario() { - let (client, port) = start_with_staking().await; - - let res = client - .get(format!("http://127.0.0.1:{port}/api/session/step/0/trace")) - .send() - .await - .unwrap(); - - assert!( - res.status().is_success(), - "GET /api/session/step/0/trace returned {}", - res.status() - ); - let body: serde_json::Value = res.json().await.unwrap(); - assert!( - body.is_object(), - "response should be a FlowTree object, got: {body}" - ); -} - -// --------------------------------------------------------------------------- -// Phase S3: GET /api/scenarios and GET /api/scenarios/all -// --------------------------------------------------------------------------- - -fn scenario_new(name: &str) -> serde_json::Value { - serde_json::json!({ "Scenario": { "sub": { "New": { "name": name } } } }) -} - -fn scenario_switch(name: &str) -> serde_json::Value { - serde_json::json!({ "Scenario": { "sub": { "Switch": { "name": name } } } }) -} - -fn call(func: &str) -> serde_json::Value { - serde_json::json!({ "Call": { "func": func } }) -} - -async fn get_scenarios(client: &reqwest::Client, port: u16) -> serde_json::Value { - let res = client - .get(format!("http://127.0.0.1:{port}/api/scenarios")) - .send() - .await - .expect("GET /api/scenarios failed"); - assert!( - res.status().is_success(), - "GET /api/scenarios returned {}", - res.status() - ); - res.json().await.expect("response was not JSON") -} - -async fn get_scenarios_all(client: &reqwest::Client, port: u16) -> serde_json::Value { - let res = client - .get(format!("http://127.0.0.1:{port}/api/scenarios/all")) - .send() - .await - .expect("GET /api/scenarios/all failed"); - assert!( - res.status().is_success(), - "GET /api/scenarios/all returned {}", - res.status() - ); - res.json().await.expect("response was not JSON") -} - -#[tokio::test] -async fn get_scenarios_returns_main_on_fresh_server() { - let (client, port) = start().await; - - let body = get_scenarios(&client, port).await; - let arr = body - .as_array() - .unwrap_or_else(|| panic!("expected array, got: {body}")); - assert_eq!(arr.len(), 1, "expected single auto-seeded main scenario"); - assert_eq!(arr[0].get("name").and_then(|v| v.as_str()), Some("main")); - assert_eq!(arr[0].get("active").and_then(|v| v.as_bool()), Some(true)); - assert_eq!(arr[0].get("step_count").and_then(|v| v.as_u64()), Some(0)); -} - -#[tokio::test] -async fn get_scenarios_reflects_new_and_step_count() { - let (client, port) = start().await; - - cmd(&client, port, "Staking", scenario_new("alt1")).await; - cmd(&client, port, "Staking", call("deposit")).await; - - let body = get_scenarios(&client, port).await; - let arr = body - .as_array() - .unwrap_or_else(|| panic!("expected array, got: {body}")); - assert_eq!(arr.len(), 2, "expected main + alt1, got: {arr:?}"); - - let main = arr - .iter() - .find(|it| it.get("name").and_then(|n| n.as_str()) == Some("main")) - .expect("main entry missing"); - assert_eq!(main.get("active").and_then(|v| v.as_bool()), Some(true)); - assert_eq!(main.get("step_count").and_then(|v| v.as_u64()), Some(1)); - - let alt1 = arr - .iter() - .find(|it| it.get("name").and_then(|n| n.as_str()) == Some("alt1")) - .expect("alt1 entry missing"); - assert_eq!(alt1.get("active").and_then(|v| v.as_bool()), Some(false)); - assert_eq!(alt1.get("step_count").and_then(|v| v.as_u64()), Some(0)); -} - -#[tokio::test] -async fn get_scenarios_all_returns_ordered_snapshot() { - let (client, port) = start().await; - - cmd(&client, port, "Staking", scenario_new("alt1")).await; - cmd(&client, port, "Staking", scenario_new("alt2")).await; - cmd(&client, port, "Staking", call("deposit")).await; - - let body = get_scenarios_all(&client, port).await; - assert_eq!(body.get("active").and_then(|v| v.as_str()), Some("main")); - - let scenarios = body - .get("scenarios") - .and_then(|v| v.as_array()) - .unwrap_or_else(|| panic!("scenarios should be an array, got: {body}")); - assert_eq!(scenarios.len(), 3, "expected 3 scenarios, got: {scenarios:?}"); - - // Insertion order: main, alt1, alt2. Each entry is a ScenarioSnapshot - // object with `name`, `steps`, `forked_from` fields. - let names: Vec<&str> = scenarios - .iter() - .map(|entry| entry.get("name").and_then(|v| v.as_str()).expect("name")) - .collect(); - assert_eq!(names, vec!["main", "alt1", "alt2"], "insertion order broken"); - - let main_steps = scenarios[0] - .get("steps") - .and_then(|v| v.as_array()) - .expect("main steps array"); - assert_eq!(main_steps.len(), 1, "main should have 1 step"); - let step0 = &main_steps[0]; - assert_eq!( - step0.get("function").and_then(|v| v.as_str()), - Some("deposit") - ); - assert_eq!(step0.get("step_index").and_then(|v| v.as_u64()), Some(0)); - assert!( - step0.get("access").is_some(), - "SessionStepView must serialize an `access` field, got: {step0}" - ); - - // main was never forked; alt1/alt2 were created with `scenario new`, not - // `scenario fork`, so none of the three carry a forked_from marker. - for idx in 0..3 { - assert!( - scenarios[idx].get("forked_from").map(|v| v.is_null()).unwrap_or(true), - "scenarios[{idx}] ({}) should have forked_from = null, got: {}", - names[idx], - scenarios[idx] - ); - } - - for idx in [1usize, 2] { - let steps = scenarios[idx] - .get("steps") - .and_then(|v| v.as_array()) - .unwrap_or_else(|| panic!("scenarios[{idx}] missing steps array")); - assert!( - steps.is_empty(), - "scenarios[{idx}] ({}) should be empty, got: {steps:?}", - names[idx] - ); - } -} - -#[tokio::test] -async fn get_scenarios_all_tracks_active_after_switch() { - let (client, port) = start().await; - - cmd(&client, port, "Staking", scenario_new("alt1")).await; - cmd(&client, port, "Staking", scenario_switch("alt1")).await; - - let body = get_scenarios_all(&client, port).await; - assert_eq!( - body.get("active").and_then(|v| v.as_str()), - Some("alt1"), - "active should reflect post-switch scenario, got: {body}" - ); -} - -#[tokio::test] -async fn get_scenarios_all_exposes_fork_origin() { - let (client, port) = start().await; - - // Build main with 3 steps, fork alt1 at step 2. - cmd(&client, port, "Staking", call("deposit")).await; - cmd(&client, port, "Staking", call("deposit")).await; - cmd(&client, port, "Staking", call("withdraw")).await; - cmd( - &client, - port, - "Staking", - serde_json::json!({ "Scenario": { "sub": { "Fork": { "name": "alt1", "at_step": 2 } } } }), - ) - .await; - - let body = get_scenarios_all(&client, port).await; - let scenarios = body - .get("scenarios") - .and_then(|v| v.as_array()) - .expect("scenarios array"); - - let main = scenarios - .iter() - .find(|s| s.get("name").and_then(|v| v.as_str()) == Some("main")) - .expect("main snapshot"); - assert!( - main.get("forked_from").map(|v| v.is_null()).unwrap_or(true), - "main must have forked_from = null, got: {main}" - ); - - let alt1 = scenarios - .iter() - .find(|s| s.get("name").and_then(|v| v.as_str()) == Some("alt1")) - .expect("alt1 snapshot"); - let fork = alt1 - .get("forked_from") - .unwrap_or_else(|| panic!("alt1.forked_from missing: {alt1}")); - assert_eq!( - fork.get("scenario").and_then(|v| v.as_str()), - Some("main"), - "fork origin must be main, got: {fork}" - ); - assert_eq!( - fork.get("at_step").and_then(|v| v.as_u64()), - Some(2), - "fork at_step must be 2, got: {fork}" - ); -} - -#[tokio::test] -async fn get_scenarios_access_level_resolves_correctly() { - let (client, port) = start().await; - - cmd(&client, port, "Staking", call("deposit")).await; - - let body = get_scenarios_all(&client, port).await; - let scenarios = body - .get("scenarios") - .and_then(|v| v.as_array()) - .expect("scenarios array"); - let main_steps = scenarios[0] - .get("steps") - .and_then(|v| v.as_array()) - .expect("main steps array"); - let access = main_steps[0] - .get("access") - .expect("access field"); - // `AccessLevel::Public` is a unit variant, so serde serializes it as the - // bare string "Public" (non-unit variants like `Restricted { role }` - // would serialize as an object). - assert_eq!( - access.as_str(), - Some("Public"), - "deposit should be Public, got: {access}" - ); -} diff --git a/crates/ilold-web/tests/scenario_ws_test.rs b/crates/ilold-web/tests/scenario_ws_test.rs deleted file mode 100644 index c6ba4f1..0000000 --- a/crates/ilold-web/tests/scenario_ws_test.rs +++ /dev/null @@ -1,209 +0,0 @@ -use std::path::PathBuf; -use std::time::Duration; - -use futures_util::StreamExt; -use tokio::time::timeout; -use tokio_tungstenite::connect_async; -use tokio_tungstenite::tungstenite::Message; - -fn fixture(name: &str) -> PathBuf { - PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .parent() - .unwrap() - .parent() - .unwrap() - .join("tests/fixtures") - .join(name) -} - -/// Boot a server on an ephemeral port with the staking fixture and open a -/// WS connection. Returns the HTTP client, port, and the connected WS -/// stream. -async fn start_with_ws() -> ( - reqwest::Client, - u16, - tokio_tungstenite::WebSocketStream<tokio_tungstenite::MaybeTlsStream<tokio::net::TcpStream>>, -) { - let paths = vec![fixture("staking.sol")]; - let (_state, port) = ilold_web::start_server(paths, 0, 2).await.unwrap(); - let (ws, _) = connect_async(format!("ws://127.0.0.1:{port}/ws")) - .await - .expect("WS connection failed"); - (reqwest::Client::new(), port, ws) -} - -async fn post_cmd( - client: &reqwest::Client, - port: u16, - contract: &str, - command: serde_json::Value, -) { - let res = client - .post(format!("http://127.0.0.1:{port}/api/cmd")) - .json(&serde_json::json!({ "contract": contract, "command": command })) - .send() - .await - .expect("POST /api/cmd failed"); - assert!( - res.status().is_success(), - "POST /api/cmd returned {}", - res.status() - ); -} - -async fn next_ws_json<S>(ws: &mut S) -> serde_json::Value -where - S: StreamExt<Item = Result<Message, tokio_tungstenite::tungstenite::Error>> + Unpin, -{ - let msg = timeout(Duration::from_secs(5), ws.next()) - .await - .expect("timed out waiting for WS message") - .expect("WS stream ended") - .expect("WS error"); - let text = msg.into_text().expect("not a text message"); - serde_json::from_str(&text).expect("WS payload was not JSON") -} - -fn scenario_new(name: &str) -> serde_json::Value { - serde_json::json!({ "Scenario": { "sub": { "New": { "name": name } } } }) -} - -fn scenario_switch(name: &str) -> serde_json::Value { - serde_json::json!({ "Scenario": { "sub": { "Switch": { "name": name } } } }) -} - -fn scenario_delete(name: &str) -> serde_json::Value { - serde_json::json!({ "Scenario": { "sub": { "Delete": { "name": name } } } }) -} - -fn call(func: &str) -> serde_json::Value { - serde_json::json!({ "Call": { "func": func } }) -} - -#[tokio::test] -async fn ws_event_session_add_node_carries_scenario_field() { - let (client, port, mut ws) = start_with_ws().await; - - post_cmd(&client, port, "Staking", call("deposit")).await; - - let payload = next_ws_json(&mut ws).await; - assert_eq!(payload["type"], "session_add_node"); - assert_eq!(payload["scenario"], "main"); - assert_eq!(payload["function"], "deposit"); - assert!(payload["step_index"].is_number()); -} - -#[tokio::test] -async fn ws_event_scenario_created_broadcasts_on_new() { - let (client, port, mut ws) = start_with_ws().await; - - post_cmd(&client, port, "Staking", scenario_new("alt1")).await; - - let payload = next_ws_json(&mut ws).await; - assert_eq!( - payload["type"], "scenario_created", - "expected scenario_created, got: {payload}" - ); - assert_eq!(payload["name"], "alt1"); -} - -#[tokio::test] -async fn ws_event_scenario_switched_broadcasts_on_switch() { - let (client, port, mut ws) = start_with_ws().await; - - post_cmd(&client, port, "Staking", scenario_new("alt1")).await; - // consume the `scenario_created` event so the next read is the switch - let _created = next_ws_json(&mut ws).await; - - post_cmd(&client, port, "Staking", scenario_switch("alt1")).await; - - let payload = next_ws_json(&mut ws).await; - assert_eq!( - payload["type"], "scenario_switched", - "expected scenario_switched, got: {payload}" - ); - assert_eq!(payload["from"], "main"); - assert_eq!(payload["to"], "alt1"); -} - -#[tokio::test] -async fn ws_event_scenario_deleted_broadcasts_on_delete() { - let (client, port, mut ws) = start_with_ws().await; - - post_cmd(&client, port, "Staking", scenario_new("alt1")).await; - let _created = next_ws_json(&mut ws).await; - - // Delete a non-active scenario (active is still "main") - post_cmd(&client, port, "Staking", scenario_delete("alt1")).await; - - let payload = next_ws_json(&mut ws).await; - assert_eq!( - payload["type"], "scenario_deleted", - "expected scenario_deleted, got: {payload}" - ); - assert_eq!(payload["name"], "alt1"); -} - -#[tokio::test] -async fn canvas_patch_scenario_field_propagates_to_ws_handler() { - // Explicit check that the scenario field on a data event matches the - // active scenario at the moment the command was dispatched. We switch - // to a new scenario first, then `c deposit`; the `session_add_node` - // must carry `scenario: "alt1"`, not `"main"`. - let (client, port, mut ws) = start_with_ws().await; - - post_cmd(&client, port, "Staking", scenario_new("alt1")).await; - let _created = next_ws_json(&mut ws).await; - - post_cmd(&client, port, "Staking", scenario_switch("alt1")).await; - let _switched = next_ws_json(&mut ws).await; - - post_cmd(&client, port, "Staking", call("deposit")).await; - let payload = next_ws_json(&mut ws).await; - - assert_eq!(payload["type"], "session_add_node"); - assert_eq!( - payload["scenario"], "alt1", - "scenario field must track active scenario at call time, got: {payload}" - ); - assert_eq!(payload["function"], "deposit"); -} - -#[tokio::test] -async fn ws_event_scenario_store_reloaded_broadcasts_after_load() { - let (client, port, mut ws) = start_with_ws().await; - - // Build a tiny state and save it so we have a valid v2 JSON to load back. - post_cmd(&client, port, "Staking", call("deposit")).await; - let _added = next_ws_json(&mut ws).await; // session_add_node - - // SaveSession does not broadcast — POST and read the JSON inline. - let res = client - .post(format!("http://127.0.0.1:{port}/api/cmd")) - .json(&serde_json::json!({ - "contract": "Staking", - "command": "SaveSession", - })) - .send() - .await - .unwrap(); - let body: serde_json::Value = res.json().await.unwrap(); - let json = body["SessionSaved"]["json"].as_str().unwrap().to_string(); - - // LoadSession must broadcast scenario_store_reloaded with the active name. - post_cmd( - &client, - port, - "Staking", - serde_json::json!({ "LoadSession": { "json": json } }), - ) - .await; - let payload = next_ws_json(&mut ws).await; - - assert_eq!(payload["type"], "scenario_store_reloaded"); - assert_eq!( - payload["active"], "main", - "reload event must carry the post-load active scenario, got: {payload}" - ); -} - diff --git a/crates/ilold-web/tests/ws_broadcast_test.rs b/crates/ilold-web/tests/ws_broadcast_test.rs deleted file mode 100644 index 5c735e0..0000000 --- a/crates/ilold-web/tests/ws_broadcast_test.rs +++ /dev/null @@ -1,148 +0,0 @@ -use std::path::PathBuf; -use std::time::Duration; - -use futures_util::StreamExt; -use tokio_tungstenite::connect_async; - -fn fixture(name: &str) -> PathBuf { - PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .parent().unwrap() - .parent().unwrap() - .join("tests/fixtures") - .join(name) -} - -#[tokio::test] -async fn broadcast_add_node_on_call_command() { - let paths = vec![fixture("staking.sol")]; - let (_, port) = ilold_web::start_server(paths, 0, 2).await.unwrap(); - - let (mut ws, _) = connect_async(format!("ws://127.0.0.1:{port}/ws")) - .await - .expect("WS connection failed"); - - let client = reqwest::Client::new(); - let res = client - .post(format!("http://127.0.0.1:{port}/api/cmd")) - .json(&serde_json::json!({ - "contract": "Staking", - "command": { "Call": { "func": "deposit" } } - })) - .send() - .await - .expect("POST failed"); - - assert!(res.status().is_success(), "POST returned {}", res.status()); - - let msg = tokio::time::timeout(Duration::from_secs(5), ws.next()) - .await - .expect("Timed out waiting for WS message") - .expect("WS stream ended") - .expect("WS error"); - - let text = msg.into_text().expect("Not a text message"); - let payload: serde_json::Value = serde_json::from_str(&text).unwrap(); - - assert_eq!(payload["type"], "session_add_node"); - assert_eq!(payload["function"], "deposit"); - assert_eq!(payload["scenario"], "main"); - assert!(payload["step_index"].is_number()); - assert!(payload["access"].is_string() || payload["access"].is_object()); -} - -#[tokio::test] -async fn broadcast_clear_after_call_and_clear() { - let paths = vec![fixture("staking.sol")]; - let (_, port) = ilold_web::start_server(paths, 0, 2).await.unwrap(); - - let (mut ws, _) = connect_async(format!("ws://127.0.0.1:{port}/ws")) - .await - .expect("WS connection failed"); - - let client = reqwest::Client::new(); - - // First: call deposit - client - .post(format!("http://127.0.0.1:{port}/api/cmd")) - .json(&serde_json::json!({ - "contract": "Staking", - "command": { "Call": { "func": "deposit" } } - })) - .send() - .await - .unwrap(); - - // Consume the add_node message - let msg = tokio::time::timeout(Duration::from_secs(5), ws.next()) - .await.unwrap().unwrap().unwrap(); - let payload: serde_json::Value = serde_json::from_str(&msg.into_text().unwrap()).unwrap(); - assert_eq!(payload["type"], "session_add_node"); - - // Then: clear - client - .post(format!("http://127.0.0.1:{port}/api/cmd")) - .json(&serde_json::json!({ - "contract": "Staking", - "command": "Clear" - })) - .send() - .await - .unwrap(); - - let msg = tokio::time::timeout(Duration::from_secs(5), ws.next()) - .await.unwrap().unwrap().unwrap(); - let payload: serde_json::Value = serde_json::from_str(&msg.into_text().unwrap()).unwrap(); - assert_eq!(payload["type"], "session_clear"); - assert_eq!(payload["scenario"], "main"); -} - -#[tokio::test] -async fn no_broadcast_on_non_mutating_command() { - let paths = vec![fixture("staking.sol")]; - let (_, port) = ilold_web::start_server(paths, 0, 2).await.unwrap(); - - let (mut ws, _) = connect_async(format!("ws://127.0.0.1:{port}/ws")) - .await - .expect("WS connection failed"); - - let client = reqwest::Client::new(); - - // Functions command does NOT produce a CanvasPatch - let res = client - .post(format!("http://127.0.0.1:{port}/api/cmd")) - .json(&serde_json::json!({ - "contract": "Staking", - "command": "Functions" - })) - .send() - .await - .unwrap(); - - assert!(res.status().is_success()); - - // WS should NOT receive anything — timeout is expected - let result = tokio::time::timeout(Duration::from_millis(500), ws.next()).await; - assert!(result.is_err(), "Should have timed out — no broadcast expected"); -} - -#[tokio::test] -async fn who_command_returns_variable_info() { - let paths = vec![fixture("staking.sol")]; - let (_, port) = ilold_web::start_server(paths, 0, 2).await.unwrap(); - - let client = reqwest::Client::new(); - let res = client - .post(format!("http://127.0.0.1:{port}/api/cmd")) - .json(&serde_json::json!({ - "contract": "Staking", - "command": { "Who": { "variable": "balances" } } - })) - .send() - .await - .unwrap(); - - assert!(res.status().is_success()); - let body: serde_json::Value = res.json().await.unwrap(); - assert_eq!(body["VariableInfo"]["variable"], "balances"); - assert!(body["VariableInfo"]["writers"].is_array()); -} From 82f598919c28dc4afa96e494c7be3e53f5786674 Mon Sep 17 00:00:00 2001 From: scab24 <git.seco@protonmail.com> Date: Sat, 16 May 2026 21:46:15 +0200 Subject: [PATCH 06/10] chore(frontend): default kind to solana in all components Switches the kind prop default from solidity to solana in the 8 Svelte files that branch on it, rewrites the root +page.svelte to render only Solana programs (drops the Solidity contract grid), and pins the contract page detector to solana. The dead Solidity branches inside individual components stay until a follow-up cleanup; cross-check pipeline stays green. --- .../contract/FunctionSidebar.svelte | 2 +- .../contract/FunctionSourcePanel.svelte | 2 +- .../src/lib/components/contract/Legend.svelte | 2 +- .../src/lib/components/contract/TopBar.svelte | 2 +- .../components/session/SessionSidebar.svelte | 2 +- .../frontend/src/routes/+page.svelte | 155 ++---------------- .../src/routes/contract/[name]/+page.svelte | 4 +- 7 files changed, 23 insertions(+), 146 deletions(-) diff --git a/crates/ilold-web/frontend/src/lib/components/contract/FunctionSidebar.svelte b/crates/ilold-web/frontend/src/lib/components/contract/FunctionSidebar.svelte index 1a4e532..bd79693 100644 --- a/crates/ilold-web/frontend/src/lib/components/contract/FunctionSidebar.svelte +++ b/crates/ilold-web/frontend/src/lib/components/contract/FunctionSidebar.svelte @@ -17,7 +17,7 @@ program = null, canvasFuncs, mode, - kind = 'solidity', + kind = 'solana', onadd, onremove, }: { diff --git a/crates/ilold-web/frontend/src/lib/components/contract/FunctionSourcePanel.svelte b/crates/ilold-web/frontend/src/lib/components/contract/FunctionSourcePanel.svelte index dbcf7eb..e96c8cd 100644 --- a/crates/ilold-web/frontend/src/lib/components/contract/FunctionSourcePanel.svelte +++ b/crates/ilold-web/frontend/src/lib/components/contract/FunctionSourcePanel.svelte @@ -10,7 +10,7 @@ // Uses the CodeMirror `oneDark` theme and the JS grammar as a Solidity/Rust // fallback so it stays close enough for highlighting without pulling extra // language bundles. - let { contract, func, kind = 'solidity', onclose }: { + let { contract, func, kind = 'solana', onclose }: { contract: string; func: string; kind?: 'solidity' | 'solana'; diff --git a/crates/ilold-web/frontend/src/lib/components/contract/Legend.svelte b/crates/ilold-web/frontend/src/lib/components/contract/Legend.svelte index 2a161c0..dbc8cc7 100644 --- a/crates/ilold-web/frontend/src/lib/components/contract/Legend.svelte +++ b/crates/ilold-web/frontend/src/lib/components/contract/Legend.svelte @@ -1,7 +1,7 @@ <script lang="ts"> let { mode, - kind = 'solidity', + kind = 'solana', }: { mode: 'cfg' | 'sequences' | 'session'; kind?: 'solidity' | 'solana'; diff --git a/crates/ilold-web/frontend/src/lib/components/contract/TopBar.svelte b/crates/ilold-web/frontend/src/lib/components/contract/TopBar.svelte index 28472c6..3b7e008 100644 --- a/crates/ilold-web/frontend/src/lib/components/contract/TopBar.svelte +++ b/crates/ilold-web/frontend/src/lib/components/contract/TopBar.svelte @@ -9,7 +9,7 @@ contractName, mode, seqDirection, - kind = 'solidity', + kind = 'solana', hideSystem = false, onmodechange, onsearch, diff --git a/crates/ilold-web/frontend/src/lib/components/session/SessionSidebar.svelte b/crates/ilold-web/frontend/src/lib/components/session/SessionSidebar.svelte index 55e26b3..0c646a7 100644 --- a/crates/ilold-web/frontend/src/lib/components/session/SessionSidebar.svelte +++ b/crates/ilold-web/frontend/src/lib/components/session/SessionSidebar.svelte @@ -24,7 +24,7 @@ lookupBlock = () => null, onpathselect = () => {}, onexpandcfg = () => {}, - kind = 'solidity', + kind = 'solana', program = null, solanaUsers = [], onsolanarun = () => {}, diff --git a/crates/ilold-web/frontend/src/routes/+page.svelte b/crates/ilold-web/frontend/src/routes/+page.svelte index 82a7828..ac4c1e0 100644 --- a/crates/ilold-web/frontend/src/routes/+page.svelte +++ b/crates/ilold-web/frontend/src/routes/+page.svelte @@ -18,12 +18,6 @@ } }); - // Publish project-level commands: navigate into any contract. The - // contract page replaces this list with its richer set on mount. - // ProjectMap may report the same name twice (e.g. an interface that - // appears as its own entry and as an inherited reference elsewhere), - // so we dedupe by name before mapping to Commands — Svelte's keyed - // each block would otherwise error on the duplicate id. $effect(() => { if (!projectMap) { setPaletteCommands([]); @@ -31,76 +25,33 @@ } const seen = new Set<string>(); const cmds: Command[] = []; - if (projectMap.kind === 'solana') { - for (const p of projectMap.programs ?? []) { - if (seen.has(p.name)) continue; - seen.add(p.name); - cmds.push({ - id: `program:${p.name}`, - label: p.name, - category: 'Contract' as const, - icon: '◊', - detail: `${p.instructions.length} ix · ${p.account_types.length} account types`, - keywords: ['program', 'open', 'navigate', 'solana'], - run: () => goto(`/contract/${encodeURIComponent(p.name)}`), - }); - } - } else { - for (const c of projectMap.contracts ?? []) { - if (seen.has(c.name)) continue; - seen.add(c.name); - cmds.push({ - id: `contract:${c.name}`, - label: c.name, - category: 'Contract' as const, - icon: '◈', - detail: c.kind, - keywords: ['contract', 'open', 'navigate'], - run: () => goto(`/contract/${encodeURIComponent(c.name)}`), - }); - } + for (const p of projectMap.programs ?? []) { + if (seen.has(p.name)) continue; + seen.add(p.name); + cmds.push({ + id: `program:${p.name}`, + label: p.name, + category: 'Contract' as const, + icon: '◊', + detail: `${p.instructions.length} ix · ${p.account_types.length} account types`, + keywords: ['program', 'open', 'navigate', 'solana'], + run: () => goto(`/contract/${encodeURIComponent(p.name)}`), + }); } setPaletteCommands(cmds); }); onDestroy(() => clearPaletteCommands()); - let contracts: any[] = $state([]); - let interfaces: any[] = $state([]); - let programs: any[] = $state([]); - let kind = $state<'solidity' | 'solana'>('solidity'); - - $effect(() => { - if (projectMap) { - kind = projectMap.kind === 'solana' ? 'solana' : 'solidity'; - if (kind === 'solana') { - programs = projectMap.programs ?? []; - contracts = []; - interfaces = []; - } else { - contracts = (projectMap.contracts ?? []).filter(c => c.kind !== 'Interface'); - interfaces = (projectMap.contracts ?? []).filter(c => c.kind === 'Interface'); - programs = []; - } - } - }); - - function mutColorClass(m: string): string { - if (m === 'View' || m === 'Pure') return 'bg-accent'; - return 'bg-accent-hover'; - } + let programs = $derived(projectMap?.programs ?? []); </script> <div class="fixed inset-0 flex flex-col bg-dark"> <div class="flex items-center gap-2.5 px-4 py-2 bg-hover border-b border-border-subtle z-10 shrink-0"> <span class="text-lg font-bold text-text">ilold</span> - <span class="text-xs text-text-dim">execution path analyzer</span> + <span class="text-xs text-text-dim">Solana execution path analyzer</span> {#if projectMap} - {#if kind === 'solana'} - <span class="text-xs text-text-muted">{programs.length} programs · solana</span> - {:else} - <span class="text-xs text-text-muted">{contracts.length + interfaces.length} contracts · {projectMap.relationships.length} cross-contract calls</span> - {/if} + <span class="text-xs text-text-muted">{programs.length} programs</span> {/if} <div class="ml-auto flex gap-1"> <button class="bg-hover border border-border-subtle text-accent-hover px-3 py-1 rounded-sm cursor-pointer text-xs hover:border-accent" onclick={togglePalette}>⌘K Search</button> @@ -111,7 +62,7 @@ <div class="p-6 text-danger">{error}</div> {:else if !projectMap} <div class="p-6 text-text-muted">Analyzing...</div> - {:else if kind === 'solana'} + {:else} <div class="flex-1 overflow-y-auto p-6"> <div class="grid grid-cols-[repeat(auto-fill,minmax(340px,1fr))] gap-4"> {#each programs as program} @@ -148,80 +99,6 @@ {/each} </div> </div> - {:else} - <div class="flex-1 overflow-y-auto p-6"> - <div class="grid grid-cols-[repeat(auto-fill,minmax(340px,1fr))] gap-4"> - {#each contracts as contract} - <div class="bg-hover border border-border-subtle rounded-[10px] overflow-hidden"> - <div class="px-3.5 pt-3 pb-2 border-b border-border-subtle"> - <span class="text-[10px] text-text-muted uppercase tracking-wide">{contract.kind.toLowerCase()}</span> - <h2 class="text-lg mt-0.5 mb-0"><a class="text-text no-underline hover:text-accent-hover" href="/contract/{contract.name}">{contract.name}</a></h2> - {#if contract.inherits.length > 0} - <div class="text-[11px] text-text-dim italic mt-0.5">inherits {contract.inherits.join(', ')}</div> - {/if} - </div> - - <div class="card-section"> - <div class="text-[9px] text-text-muted uppercase tracking-wide mb-1 font-semibold">Functions</div> - {#each contract.functions as func} - <a href="/contract/{contract.name}/{func.name}" class="flex items-center gap-1.5 px-1 py-1 rounded-sm text-xs text-inherit no-underline hover:bg-border"> - <span class="size-1.5 rounded-full shrink-0 {mutColorClass(func.mutability)}"></span> - <span class="text-text font-semibold font-mono flex-1">{func.name}</span> - <span class="text-[10px] text-text-dim">{func.visibility.toLowerCase()}</span> - {#if func.has_external_calls} - <span class="text-[9px] px-1 py-px rounded-md bg-warning/10 text-warning">ext</span> - {/if} - <span class="text-[10px] text-text-muted flex gap-0.5"> - {func.path_count}p - {#if func.happy_paths > 0}<span class="text-success">{func.happy_paths}✓</span>{/if} - {#if func.revert_paths > 0}<span class="text-danger">{func.revert_paths}✗</span>{/if} - </span> - </a> - {/each} - </div> - - {#if contract.state_vars.length > 0} - <div class="card-section"> - <div class="text-[9px] text-text-muted uppercase tracking-wide mb-1 font-semibold">Variables</div> - {#each contract.state_vars as sv} - <div class="flex justify-between px-1 py-0.5 text-[11px] font-mono"> - <span class="text-text">{sv.name}</span> - <span class="text-text-dim text-[10px] max-w-[150px] overflow-hidden text-ellipsis whitespace-nowrap">{sv.type_name}</span> - </div> - {/each} - </div> - {/if} - - {#if projectMap.relationships.filter(r => r.from_contract === contract.name).length > 0} - <div class="card-section"> - <div class="text-[9px] text-text-muted uppercase tracking-wide mb-1 font-semibold">Calls to</div> - {#each projectMap.relationships.filter(r => r.from_contract === contract.name) as rel} - <div class="flex items-center gap-1 px-1 py-0.5 text-[11px] font-mono"> - <span class="text-text">{rel.from_function}</span> - <span class="text-accent">→</span> - <span class="text-accent-hover">{rel.to_contract}.{rel.to_function}</span> - </div> - {/each} - </div> - {/if} - </div> - {/each} - </div> - - {#if interfaces.length > 0} - <div class="mt-6"> - <h3 class="text-sm text-text-muted mb-2">Interfaces</h3> - <div class="flex gap-2 flex-wrap"> - {#each interfaces as iface} - <div class="bg-hover border border-dashed border-border-subtle rounded-md px-3 py-2 flex gap-2 items-center"> - <span class="text-text-muted font-semibold text-[13px]">{iface.name}</span> - <span class="text-text-dim text-[11px]">{iface.functions.length} functions</span> - </div> - {/each} - </div> - </div> - {/if} - </div> {/if} </div> diff --git a/crates/ilold-web/frontend/src/routes/contract/[name]/+page.svelte b/crates/ilold-web/frontend/src/routes/contract/[name]/+page.svelte index 75d8f89..c9540d1 100644 --- a/crates/ilold-web/frontend/src/routes/contract/[name]/+page.svelte +++ b/crates/ilold-web/frontend/src/routes/contract/[name]/+page.svelte @@ -48,7 +48,7 @@ let contract: ContractDetail | null = $state(null); let solanaProgram: ProgramView | null = $state(null); - let kind: 'solidity' | 'solana' = $state('solidity'); + let kind: 'solidity' | 'solana' = $state('solana'); let solanaCanvasIxs: Set<string> = $state(new Set()); let solanaExpandedIxs: Set<string> = $state(new Set()); let solanaUsers: { name: string; pubkey: string; lamports: number }[] = $state([]); @@ -986,7 +986,7 @@ try { const pm = await getProjectMap(); if (!stillFresh()) return; - kind = pm.kind === 'solana' ? 'solana' : 'solidity'; + kind = 'solana'; if (kind === 'solana') { try { const prog = await getProgramView(contractName); From 49fd14570293a533e6ce882d3c58b8f95043f4e4 Mon Sep 17 00:00:00 2001 From: scab24 <git.seco@protonmail.com> Date: Sat, 16 May 2026 21:53:40 +0200 Subject: [PATCH 07/10] chore: remove ilold-core crate and solidity docs Drops the ilold-core crate from the workspace and its 7 path dependents (ilold-web, ilold-render, plus solar 0.1.8 from workspace.dependencies). AccessLevel imports in ilold-render move from ilold-core to its real home in ilold-session-core. README, SUMMARY, introduction and getting-started are rewritten Solana-only, and images/diagram_solidity.png is removed. cargo test --workspace --release stays green. --- Cargo.lock | 917 +----------------------------- Cargo.toml | 4 +- README.md | 63 +- crates/ilold-render/Cargo.toml | 1 - crates/ilold-render/src/colors.rs | 2 +- crates/ilold-web/Cargo.toml | 1 - docs/guide/src/getting-started.md | 102 +--- docs/guide/src/introduction.md | 22 +- images/diagram_solidity.png | Bin 417549 -> 0 bytes 9 files changed, 69 insertions(+), 1043 deletions(-) delete mode 100644 images/diagram_solidity.png diff --git a/Cargo.lock b/Cargo.lock index ecd4ae2..e7b29be 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -135,65 +135,6 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" -[[package]] -name = "alloy-json-abi" -version = "1.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9dbe713da0c737d9e5e387b0ba790eb98b14dd207fe53eef50e19a5a8ec3dac" -dependencies = [ - "alloy-primitives", - "alloy-sol-type-parser", - "serde", - "serde_json", -] - -[[package]] -name = "alloy-primitives" -version = "1.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de3b431b4e72cd8bd0ec7a50b4be18e73dab74de0dba180eef171055e5d5926e" -dependencies = [ - "alloy-rlp", - "bytes", - "cfg-if", - "const-hex", - "derive_more", - "foldhash 0.2.0", - "hashbrown 0.16.1", - "indexmap", - "itoa", - "k256", - "keccak-asm", - "paste", - "proptest", - "rand 0.9.2", - "rapidhash", - "ruint", - "rustc-hash", - "serde", - "sha3", -] - -[[package]] -name = "alloy-rlp" -version = "0.3.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e93e50f64a77ad9c5470bf2ad0ca02f228da70c792a8f06634801e202579f35e" -dependencies = [ - "arrayvec", - "bytes", -] - -[[package]] -name = "alloy-sol-type-parser" -version = "1.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6df77fea9d6a2a75c0ef8d2acbdfd92286cc599983d3175ccdc170d3433d249" -dependencies = [ - "serde", - "winnow 0.7.15", -] - [[package]] name = "anchor-lang-idl" version = "0.1.2" @@ -226,17 +167,6 @@ dependencies = [ "libc", ] -[[package]] -name = "annotate-snippets" -version = "0.12.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74fc7650eedcb2fee505aad48491529e408f0e854c2d9f63eb86c1361b9b3f93" -dependencies = [ - "anstyle", - "memchr", - "unicode-width", -] - [[package]] name = "ansi_term" version = "0.12.1" @@ -246,21 +176,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "anstream" -version = "0.6.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" -dependencies = [ - "anstyle", - "anstyle-parse 0.2.7", - "anstyle-query", - "anstyle-wincon", - "colorchoice", - "is_terminal_polyfill", - "utf8parse", -] - [[package]] name = "anstream" version = "1.0.0" @@ -268,7 +183,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" dependencies = [ "anstyle", - "anstyle-parse 1.0.0", + "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", @@ -282,15 +197,6 @@ version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" -[[package]] -name = "anstyle-parse" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" -dependencies = [ - "utf8parse", -] - [[package]] name = "anstyle-parse" version = "1.0.0" @@ -386,24 +292,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "ark-ff" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b3235cc41ee7a12aaaf2c575a2ad7b46713a8a50bda2fc3b003a04845c05dd6" -dependencies = [ - "ark-ff-asm 0.3.0", - "ark-ff-macros 0.3.0", - "ark-serialize 0.3.0", - "ark-std 0.3.0", - "derivative", - "num-bigint 0.4.6", - "num-traits", - "paste", - "rustc_version 0.3.3", - "zeroize", -] - [[package]] name = "ark-ff" version = "0.4.2" @@ -420,7 +308,7 @@ dependencies = [ "num-bigint 0.4.6", "num-traits", "paste", - "rustc_version 0.4.1", + "rustc_version", "zeroize", ] @@ -444,16 +332,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "ark-ff-asm" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db02d390bf6643fb404d3d22d31aee1c4bc4459600aef9113833d17e786c6e44" -dependencies = [ - "quote", - "syn 1.0.109", -] - [[package]] name = "ark-ff-asm" version = "0.4.2" @@ -474,18 +352,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "ark-ff-macros" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db2fd794a08ccb318058009eefdf15bcaaaaf6f8161eb3345f907222bac38b20" -dependencies = [ - "num-bigint 0.4.6", - "num-traits", - "quote", - "syn 1.0.109", -] - [[package]] name = "ark-ff-macros" version = "0.4.2" @@ -540,16 +406,6 @@ dependencies = [ "hashbrown 0.15.5", ] -[[package]] -name = "ark-serialize" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d6c2b318ee6e10f8c2853e73a83adc0ccb88995aa978d8a3408d492ab2ee671" -dependencies = [ - "ark-std 0.3.0", - "digest 0.9.0", -] - [[package]] name = "ark-serialize" version = "0.4.2" @@ -597,16 +453,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "ark-std" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1df2c09229cbc5a028b1d70e00fdb2acee28b1055dfb5ca73eea49c5a25c4e7c" -dependencies = [ - "num-traits", - "rand 0.8.5", -] - [[package]] name = "ark-std" version = "0.4.0" @@ -851,17 +697,6 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" -[[package]] -name = "auto_impl" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffdcb70bdbc4d478427380519163274ac86e52916e10f0a8889adf0f96d3fee7" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - [[package]] name = "autocfg" version = "1.5.0" @@ -1018,18 +853,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "bitvec" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" -dependencies = [ - "funty", - "radium", - "tap", - "wyz", -] - [[package]] name = "blake3" version = "1.8.5" @@ -1115,12 +938,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "boxcar" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36f64beae40a84da1b4b26ff2761a5b895c12adc41dc25aaee1c4f2bbfe97a6e" - [[package]] name = "bs58" version = "0.5.1" @@ -1146,12 +963,6 @@ dependencies = [ "serde", ] -[[package]] -name = "byte-slice-cast" -version = "1.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7575182f7272186991736b70173b0ea045398f984bf5ebbb3804736ce1330c9d" - [[package]] name = "bytecount" version = "0.6.9" @@ -1189,9 +1000,6 @@ name = "bytes" version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" -dependencies = [ - "serde", -] [[package]] name = "cc" @@ -1266,7 +1074,7 @@ version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" dependencies = [ - "anstream 1.0.0", + "anstream", "anstyle", "clap_lex", "strsim", @@ -1333,44 +1141,12 @@ dependencies = [ "crossbeam-utils", ] -[[package]] -name = "const-hex" -version = "1.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "531185e432bb31db1ecda541e9e7ab21468d4d844ad7505e0546a49b4945d49b" -dependencies = [ - "cfg-if", - "cpufeatures 0.2.17", - "proptest", - "serde_core", -] - [[package]] name = "const-oid" version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" -[[package]] -name = "const_format" -version = "0.2.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7faa7469a93a566e9ccc1c73fe783b4a65c274c5ace346038dca9c39fe0030ad" -dependencies = [ - "const_format_proc_macros", -] - -[[package]] -name = "const_format_proc_macros" -version = "0.2.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744" -dependencies = [ - "proc-macro2", - "quote", - "unicode-xid", -] - [[package]] name = "constant_time_eq" version = "0.4.2" @@ -1430,25 +1206,6 @@ dependencies = [ "libc", ] -[[package]] -name = "crossbeam-deque" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" -dependencies = [ - "crossbeam-epoch", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-epoch" -version = "0.9.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" -dependencies = [ - "crossbeam-utils", -] - [[package]] name = "crossbeam-utils" version = "0.8.21" @@ -1551,7 +1308,7 @@ dependencies = [ "digest 0.10.7", "fiat-crypto", "rand_core 0.6.4", - "rustc_version 0.4.1", + "rustc_version", "serde", "subtle", "zeroize", @@ -1637,20 +1394,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "dashmap" -version = "6.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" -dependencies = [ - "cfg-if", - "crossbeam-utils", - "hashbrown 0.14.5", - "lock_api", - "once_cell", - "parking_lot_core", -] - [[package]] name = "data-encoding" version = "2.10.0" @@ -1702,9 +1445,8 @@ dependencies = [ "convert_case", "proc-macro2", "quote", - "rustc_version 0.4.1", + "rustc_version", "syn 2.0.117", - "unicode-xid", ] [[package]] @@ -1807,12 +1549,6 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" -[[package]] -name = "dunce" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" - [[package]] name = "dyn-clone" version = "1.0.20" @@ -2028,28 +1764,6 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" -[[package]] -name = "fastrlp" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139834ddba373bbdd213dffe02c8d110508dcf1726c2be27e8d1f7d7e1856418" -dependencies = [ - "arrayvec", - "auto_impl", - "bytes", -] - -[[package]] -name = "fastrlp" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce8dba4714ef14b8274c371879b175aa55b16b30f269663f19d576f380018dc4" -dependencies = [ - "arrayvec", - "auto_impl", - "bytes", -] - [[package]] name = "fd-lock" version = "4.0.4" @@ -2124,30 +1838,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "059c31d7d36c43fe39d89e55711858b4da8be7eb6dabac23c7289b1a19489406" -[[package]] -name = "fixed-hash" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "835c052cb0c08c1acf6ffd71c022172e18723949c8282f2b9f27efbc51e64534" -dependencies = [ - "byteorder", - "rand 0.8.5", - "rustc-hex", - "static_assertions", -] - [[package]] name = "fixedbitset" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" -[[package]] -name = "fixedbitset" -version = "0.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" - [[package]] name = "fluent-uri" version = "0.4.1" @@ -2211,12 +1907,6 @@ dependencies = [ "num 0.4.3", ] -[[package]] -name = "funty" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" - [[package]] name = "futures" version = "0.3.32" @@ -2440,12 +2130,6 @@ dependencies = [ "ahash", ] -[[package]] -name = "hashbrown" -version = "0.14.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" - [[package]] name = "hashbrown" version = "0.15.5" @@ -2465,8 +2149,6 @@ dependencies = [ "allocator-api2", "equivalent", "foldhash 0.2.0", - "serde", - "serde_core", ] [[package]] @@ -2490,12 +2172,6 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" -[[package]] -name = "hex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" - [[package]] name = "hmac" version = "0.12.1" @@ -2877,18 +2553,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "ilold-core" -version = "0.1.0" -dependencies = [ - "ilold-session-core", - "petgraph 0.8.3", - "serde", - "serde_json", - "solar-compiler", - "thiserror 2.0.18", -] - [[package]] name = "ilold-help" version = "0.1.0" @@ -2920,7 +2584,6 @@ name = "ilold-render" version = "0.1.0" dependencies = [ "colored", - "ilold-core", "ilold-session-core", "ilold-solana-core", "serde_json", @@ -2969,7 +2632,6 @@ dependencies = [ "anyhow", "axum", "futures-util", - "ilold-core", "ilold-session-core", "ilold-solana-core", "portable-pty", @@ -2985,32 +2647,6 @@ dependencies = [ "tower-http", ] -[[package]] -name = "impl-codec" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba6a270039626615617f3f36d15fc827041df3b78c439da2cadfa47455a77f2f" -dependencies = [ - "parity-scale-codec", -] - -[[package]] -name = "impl-trait-for-tuples" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0eb5a3343abf848c0984fe4604b2b105da9539376e24fc0a3b0007411ae4fd9" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "index_vec" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44faf5bb8861a9c72e20d3fb0fdbd59233e43056e2b80475ab0aacdc2e781355" - [[package]] name = "indexmap" version = "2.13.0" @@ -3032,19 +2668,6 @@ dependencies = [ "generic-array", ] -[[package]] -name = "inturn" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2efbe120e37f17bb33fcdc82bc1c65087242608be37ace3cf7ebf49f3164e37" -dependencies = [ - "boxcar", - "bumpalo", - "dashmap", - "hashbrown 0.14.5", - "thread_local", -] - [[package]] name = "ioctl-rs" version = "0.1.6" @@ -3189,16 +2812,6 @@ dependencies = [ "cpufeatures 0.2.17", ] -[[package]] -name = "keccak-asm" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa468878266ad91431012b3e5ef1bf9b170eab22883503a318d46857afa4579a" -dependencies = [ - "digest 0.10.7", - "sha3-asm", -] - [[package]] name = "kv-log-macro" version = "1.0.7" @@ -3219,7 +2832,7 @@ dependencies = [ "ena", "itertools 0.11.0", "lalrpop-util", - "petgraph 0.6.5", + "petgraph", "pico-args", "regex", "regex-syntax", @@ -3263,12 +2876,6 @@ version = "0.2.183" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" -[[package]] -name = "libm" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" - [[package]] name = "libredox" version = "0.1.15" @@ -3561,12 +3168,6 @@ dependencies = [ "pin-utils", ] -[[package]] -name = "normalize-path" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5438dd2b2ff4c6df6e1ce22d825ed2fa93ee2922235cc45186991717f0a892d" - [[package]] name = "nu-ansi-term" version = "0.50.3" @@ -3711,7 +3312,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", - "libm", ] [[package]] @@ -3726,18 +3326,6 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" -[[package]] -name = "once_map" -version = "0.4.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29eefd5038c9eee9e788d90966d6b5578dd3f88363a91edaec117a7ae0adc2d5" -dependencies = [ - "ahash", - "hashbrown 0.16.1", - "parking_lot", - "stable_deref_trait", -] - [[package]] name = "opaque-debug" version = "0.3.1" @@ -3800,34 +3388,6 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" -[[package]] -name = "parity-scale-codec" -version = "3.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "799781ae679d79a948e13d4824a40970bfa500058d245760dd857301059810fa" -dependencies = [ - "arrayvec", - "bitvec", - "byte-slice-cast", - "const_format", - "impl-trait-for-tuples", - "parity-scale-codec-derive", - "rustversion", - "serde", -] - -[[package]] -name = "parity-scale-codec-derive" -version = "3.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34b4653168b563151153c9e4c08ebed57fb8262bebfa79711552fa983c623e7a" -dependencies = [ - "proc-macro-crate", - "proc-macro2", - "quote", - "syn 2.0.117", -] - [[package]] name = "parking" version = "2.2.1" @@ -3893,38 +3453,16 @@ dependencies = [ "num 0.2.1", ] -[[package]] -name = "pest" -version = "2.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662" -dependencies = [ - "memchr", - "ucd-trie", -] - [[package]] name = "petgraph" version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" dependencies = [ - "fixedbitset 0.4.2", + "fixedbitset", "indexmap", ] -[[package]] -name = "petgraph" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455" -dependencies = [ - "fixedbitset 0.5.7", - "hashbrown 0.15.5", - "indexmap", - "serde", -] - [[package]] name = "phf_shared" version = "0.11.3" @@ -4060,17 +3598,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "primitive-types" -version = "0.12.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b34d9fd68ae0b74a41b21c03c2f62847aa0ffea044eee893b4c140b37e244e2" -dependencies = [ - "fixed-hash", - "impl-codec", - "uint", -] - [[package]] name = "proc-macro-crate" version = "3.5.0" @@ -4084,28 +3611,9 @@ dependencies = [ name = "proc-macro2" version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "proptest" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ - "bit-set 0.8.0", - "bit-vec 0.8.0", - "bitflags 2.11.0", - "num-traits", - "rand 0.9.2", - "rand_chacha 0.9.0", - "rand_xorshift", - "regex-syntax", - "rusty-fork", - "tempfile", - "unarray", + "unicode-ident", ] [[package]] @@ -4128,12 +3636,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "quick-error" -version = "1.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" - [[package]] name = "quote" version = "1.0.45" @@ -4155,12 +3657,6 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" -[[package]] -name = "radium" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" - [[package]] name = "rand" version = "0.7.3" @@ -4193,7 +3689,6 @@ checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ "rand_chacha 0.9.0", "rand_core 0.9.5", - "serde", ] [[package]] @@ -4251,7 +3746,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" dependencies = [ "getrandom 0.3.4", - "serde", ] [[package]] @@ -4263,44 +3757,6 @@ dependencies = [ "rand_core 0.5.1", ] -[[package]] -name = "rand_xorshift" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" -dependencies = [ - "rand_core 0.9.5", -] - -[[package]] -name = "rapidhash" -version = "4.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e48930979c155e2f33aa36ab3119b5ee81332beb6482199a8ecd6029b80b59" -dependencies = [ - "rustversion", -] - -[[package]] -name = "rayon" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" -dependencies = [ - "either", - "rayon-core", -] - -[[package]] -name = "rayon-core" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" -dependencies = [ - "crossbeam-deque", - "crossbeam-utils", -] - [[package]] name = "redox_syscall" version = "0.5.18" @@ -4345,8 +3801,8 @@ dependencies = [ "nu-ansi-term", "serde", "strip-ansi-escapes", - "strum 0.26.3", - "strum_macros 0.26.4", + "strum", + "strum_macros", "thiserror 2.0.18", "unicase", "unicode-segmentation", @@ -4483,16 +3939,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "rlp" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb919243f34364b6bd2fc10ef797edbfa75f33c252e7998527479c6d6b47e1ec" -dependencies = [ - "bytes", - "rustc-hex", -] - [[package]] name = "rmcp" version = "1.6.0" @@ -4528,74 +3974,19 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "ruint" -version = "1.17.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c141e807189ad38a07276942c6623032d3753c8859c146104ac2e4d68865945a" -dependencies = [ - "alloy-rlp", - "ark-ff 0.3.0", - "ark-ff 0.4.2", - "ark-ff 0.5.0", - "bytes", - "fastrlp 0.3.1", - "fastrlp 0.4.0", - "num-bigint 0.4.6", - "num-integer", - "num-traits", - "parity-scale-codec", - "primitive-types", - "proptest", - "rand 0.8.5", - "rand 0.9.2", - "rlp", - "ruint-macro", - "serde_core", - "valuable", - "zeroize", -] - -[[package]] -name = "ruint-macro" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48fd7bd8a6377e15ad9d42a8ec25371b94ddc67abe7c8b9127bec79bebaaae18" - [[package]] name = "rustc-demangle" version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" -[[package]] -name = "rustc-hash" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" - -[[package]] -name = "rustc-hex" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e75f6a532d0fd9f7f13144f392b6ad56a32696bfcd9c78f797f16bbb6f072d6" - -[[package]] -name = "rustc_version" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0dfe2087c51c460008730de8b57e6a320782fbfb312e1f4d520e6c6fae155ee" -dependencies = [ - "semver 0.11.0", -] - [[package]] name = "rustc_version" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" dependencies = [ - "semver 1.0.27", + "semver", ] [[package]] @@ -4650,18 +4041,6 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" -[[package]] -name = "rusty-fork" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2" -dependencies = [ - "fnv", - "quick-error", - "tempfile", - "wait-timeout", -] - [[package]] name = "ryu" version = "1.0.23" @@ -4712,12 +4091,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "scoped-tls" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" - [[package]] name = "scopeguard" version = "1.2.0" @@ -4761,30 +4134,12 @@ dependencies = [ "libc", ] -[[package]] -name = "semver" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6" -dependencies = [ - "semver-parser", -] - [[package]] name = "semver" version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" -[[package]] -name = "semver-parser" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9900206b54a3527fdc7b8a938bffd94a568bac4f4aa8113b209df75a09c0dec2" -dependencies = [ - "pest", -] - [[package]] name = "serde" version = "1.0.228" @@ -5006,16 +4361,6 @@ dependencies = [ "keccak", ] -[[package]] -name = "sha3-asm" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59cbb88c189d6352cc8ae96a39d19c7ecad8f7330b29461187f2587fdc2988d5" -dependencies = [ - "cc", - "cfg-if", -] - [[package]] name = "sharded-slab" version = "0.1.7" @@ -6480,152 +5825,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "solar-ast" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b6aaf98d032ba3be85dca5f969895ade113a9137bb5956f80c5faf14689de59" -dependencies = [ - "alloy-primitives", - "bumpalo", - "either", - "num-rational 0.4.2", - "semver 1.0.27", - "solar-data-structures", - "solar-interface", - "solar-macros", - "strum 0.27.2", -] - -[[package]] -name = "solar-compiler" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95e792060bcbb007a6b9b060292945fb34ff854c7d93a9628f81b6c809eb4360" -dependencies = [ - "alloy-primitives", - "solar-ast", - "solar-config", - "solar-data-structures", - "solar-interface", - "solar-macros", - "solar-parse", - "solar-sema", -] - -[[package]] -name = "solar-config" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff16d692734c757edd339f5db142ba91b42772f8cbe1db1ce3c747f1e777185f" -dependencies = [ - "colorchoice", - "strum 0.27.2", -] - -[[package]] -name = "solar-data-structures" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dea34e58332c7d6a8cde1f1740186d31682b7be46e098b8cc16fcb7ffd98bf5" -dependencies = [ - "bumpalo", - "index_vec", - "indexmap", - "parking_lot", - "rayon", - "rustc-hash", - "smallvec", -] - -[[package]] -name = "solar-interface" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d6163af2e773f4d455212fa9ba2c0664506029dd26232eb406f5046092ac311" -dependencies = [ - "annotate-snippets", - "anstream 0.6.21", - "anstyle", - "derive_more", - "dunce", - "inturn", - "itertools 0.14.0", - "itoa", - "normalize-path", - "once_map", - "rayon", - "scoped-tls", - "serde", - "serde_json", - "solar-config", - "solar-data-structures", - "solar-macros", - "thiserror 2.0.18", - "tracing", - "unicode-width", -] - -[[package]] -name = "solar-macros" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44a98045888d75d17f52e7b76f6098844b76078b5742a450c3ebcdbdb02da124" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "solar-parse" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19b77a9cbb07948e4586cdcf64f0a483424197308816ebd57a4cf06130b68562" -dependencies = [ - "alloy-primitives", - "bitflags 2.11.0", - "bumpalo", - "itertools 0.14.0", - "memchr", - "num-bigint 0.4.6", - "num-rational 0.4.2", - "num-traits", - "ruint", - "smallvec", - "solar-ast", - "solar-data-structures", - "solar-interface", - "tracing", -] - -[[package]] -name = "solar-sema" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd033af43a38da316a04b25bbd20b121ce5d728b61e6988fd8fd6e2f1e68d0a1" -dependencies = [ - "alloy-json-abi", - "alloy-primitives", - "bitflags 2.11.0", - "bumpalo", - "derive_more", - "either", - "once_map", - "paste", - "rayon", - "serde", - "serde_json", - "solar-ast", - "solar-data-structures", - "solar-interface", - "solar-macros", - "solar-parse", - "strum 0.27.2", - "thread_local", - "tracing", -] - [[package]] name = "spki" version = "0.7.3" @@ -6642,12 +5841,6 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" -[[package]] -name = "static_assertions" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" - [[package]] name = "string_cache" version = "0.8.9" @@ -6681,15 +5874,6 @@ version = "0.26.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" -[[package]] -name = "strum" -version = "0.27.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" -dependencies = [ - "strum_macros 0.27.2", -] - [[package]] name = "strum_macros" version = "0.26.4" @@ -6703,18 +5887,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "strum_macros" -version = "0.27.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" -dependencies = [ - "heck 0.5.0", - "proc-macro2", - "quote", - "syn 2.0.117", -] - [[package]] name = "subtle" version = "2.6.1" @@ -6784,12 +5956,6 @@ dependencies = [ "libc", ] -[[package]] -name = "tap" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" - [[package]] name = "tempfile" version = "3.27.0" @@ -7009,7 +6175,7 @@ dependencies = [ "indexmap", "toml_datetime", "toml_parser", - "winnow 1.0.0", + "winnow", ] [[package]] @@ -7018,7 +6184,7 @@ version = "1.1.0+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2334f11ee363607eb04df9b8fc8a13ca1715a72ba8662a26ac285c98aabb4011" dependencies = [ - "winnow 1.0.0", + "winnow", ] [[package]] @@ -7186,30 +6352,6 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" -[[package]] -name = "ucd-trie" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" - -[[package]] -name = "uint" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76f64bba2c53b04fcab63c01a7d7427eadc821e3bc48c34dc9ba29c501164b52" -dependencies = [ - "byteorder", - "crunchy", - "hex", - "static_assertions", -] - -[[package]] -name = "unarray" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" - [[package]] name = "unicase" version = "2.9.0" @@ -7366,15 +6508,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "wait-timeout" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" -dependencies = [ - "libc", -] - [[package]] name = "walkdir" version = "2.5.0" @@ -7510,7 +6643,7 @@ dependencies = [ "bitflags 2.11.0", "hashbrown 0.15.5", "indexmap", - "semver 1.0.27", + "semver", ] [[package]] @@ -7731,15 +6864,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" -[[package]] -name = "winnow" -version = "0.7.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" -dependencies = [ - "memchr", -] - [[package]] name = "winnow" version = "1.0.0" @@ -7838,7 +6962,7 @@ dependencies = [ "id-arena", "indexmap", "log", - "semver 1.0.27", + "semver", "serde", "serde_derive", "serde_json", @@ -7852,15 +6976,6 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" -[[package]] -name = "wyz" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" -dependencies = [ - "tap", -] - [[package]] name = "yoke" version = "0.8.2" diff --git a/Cargo.toml b/Cargo.toml index e39b8f2..89258a3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,6 @@ [workspace] resolver = "2" members = [ - "crates/ilold-core", "crates/ilold-cli", "crates/ilold-web", "crates/ilold-session-core", @@ -18,10 +17,9 @@ authors = ["scab24 <git.seco@protonmail.com>"] license = "AGPL-3.0-only" repository = "https://github.com/scab24/ilold" homepage = "https://github.com/scab24/ilold" -description = "Execution path analyzer and interactive security workbench for smart contracts" +description = "Solana program execution path analyzer and interactive security workbench" [workspace.dependencies] -solar = { version = "=0.1.8", package = "solar-compiler", default-features = false } petgraph = "0.8" serde = { version = "1", features = ["derive"] } serde_json = "1" diff --git a/README.md b/README.md index 6624ef3..6057464 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,14 @@ # ilold -Execution path analyzer and interactive security workbench for smart contracts. It maps every possible path through a protocol (every branch, function combination, and state mutation) and lets users navigate them visually, branch by branch, with an LLM reasoning over each path. +Solana program execution path analyzer and interactive security workbench. It maps every reachable instruction path through an Anchor program, executes them on a real LiteSVM, and lets the auditor (or an LLM agent) navigate branches, fork scenarios, and ship a Markdown deliverable. ![ilold](images/Ilold.jpg) ## What it does -ilold loads a smart contract project, builds an in-memory model, and drops the auditor into an interactive REPL backed by a live web canvas. Each command answers a question about the protocol: list entry points, add a call to a session, inspect state changes, trace execution flow, slice data dependencies, record findings, export a report. The canvas reflects the same state in a visual graph that the user navigates by clicking, expanding and forking. +ilold loads an Anchor workspace, builds an in-memory model from the IDL, boots a LiteSVM with the compiled `.so`, and drops the auditor into an interactive REPL backed by a live web canvas. Each command answers a question about the program: list instructions, add a call to a scenario, inspect decoded account state, fork the timeline, record a finding, export the report. The canvas reflects the same state in a visual graph that the user navigates by clicking, expanding and forking. -Two backends, one shell: - -| Backend | Input | Execution model | -| --- | --- | --- | -| Solana | Anchor project (`Anchor.toml` + IDL + `.so`) | Concrete execution on LiteSVM with scenario forking | -| Solidity | `.sol` sources | Static analysis: parser, CFG, path tree, slicer | - -The Solana backend also ships a Model Context Protocol (MCP) server so an LLM agent can drive the audit end to end while the canvas reflects every step in real time. +ilold also ships a Model Context Protocol (MCP) server so any LLM client (Claude Code, Cursor, Continue) can drive the audit end to end while the canvas reflects every step in real time. ## Quick start @@ -27,7 +20,7 @@ cd ilold cargo build --release ``` -The binary is at `target/release/ilold` with four subcommands: `analyze`, `context`, `serve`, and `explore`. +The binary is at `target/release/ilold` with three subcommands: `serve`, `explore`, and `mcp`. ## Solana backend @@ -52,7 +45,7 @@ ilold[staking]> finding High "..." ilold[staking]> export ``` -### LLM-driven audit (Solana via MCP) +### LLM-driven audit (MCP) Register the MCP server once in your client: @@ -67,37 +60,6 @@ Then ask the LLM in natural language: The LLM picks the right tools, the backend executes them in LiteSVM, and the canvas reflects each step in real time. The MCP server is agnostic to the active program: a single registration works against multi-program workspaces, the LLM switches with `ilold_use <program>`. -## Solidity backend - -Contracts are parsed into a typed model. Per-function CFGs and path trees drive the interactive analysis commands: `info`, `trace`, `slice`, `timeline`, and sequence narratives. - -![Solidity canvas](images/diagram_solidity.png) - -``` -./target/release/ilold explore tests/fixtures/staking.sol -``` - -``` -ilold[Staking]> f -ilold[Staking]> c deposit -ilold[Staking → deposit]> s -ilold[Staking → deposit]> who totalStaked -ilold[Staking → deposit]> sl deposit totalStaked -ilold[Staking → deposit]> tr deposit -``` - -For one-shot static analysis without the REPL: - -``` -./target/release/ilold analyze tests/fixtures/staking.sol --max-seq-depth 5 -``` - -## Trace and slice (Solidity) - -`trace` builds the full execution tree of a function with modifier bodies inlined, requires highlighted, state writes flagged, and external calls marked. `slice` produces backward and forward dataflow slices for any variable in any function. Both are static and only available on the Solidity side today; Solana coverage is reconstructed at runtime via `coverage` and the runtime overlay until the AST + CFG layer lands (see [Roadmap](docs/guide/src/roadmap/solana.md)). - -![Solidity trace command output](images/tr_menu.png) - ## Documentation The published book lives at **[scab24.github.io/ilold](https://scab24.github.io/ilold/)**. Sources are in [`docs/guide/`](docs/guide/src/SUMMARY.md); build it locally with: @@ -111,7 +73,6 @@ Key pages: - [Introduction](docs/guide/src/introduction.md) - [Getting Started](docs/guide/src/getting-started.md) - [Solana Backend](docs/guide/src/solana/overview.md) -- [Solidity Backend](docs/guide/src/solidity/overview.md) - [MCP server](docs/guide/src/reference/mcp.md) - [Roadmap](docs/guide/src/roadmap/solana.md) @@ -119,19 +80,20 @@ Key pages: MVP scope shipped: -- Typed graph from Anchor IDL (Solana) and `solar-compiler` AST (Solidity) +- Typed graph from Anchor IDL with discriminator + admin-gating + coupling metadata - Executable scenarios with fork from any step, save/load, runtime overlay - LLM-aware REPL with structured `?` help per command - MCP server with 30 typed tools agnostic to the active program -- Source viewer parity Solana - Solidity with "open in IDE" deep links +- Source viewer with "open in IDE" deep links - Markdown audit deliverable with severity matrix and methodology Documented future work (see [Roadmap](docs/guide/src/roadmap/solana.md)): -- AST + CFG layer on the Solana side via Elozer, our in-house static analyzer +- AST + CFG layer for Anchor handlers via Elozer, our in-house static analyzer - Detector engine measured against the public sealevel-attacks corpus - Pre-built attack-pattern query catalog -- MCP server for the Solidity backend + +For the legacy Solidity backend, see the standalone [`ilold-evm`](https://github.com/scab24/ilold-evm) repo. ## Project layout @@ -139,16 +101,15 @@ Single Rust monorepo: ``` crates/ - ilold-cli Interactive shell + analyze/context CLI + ilold-cli Interactive shell + serve/explore/mcp CLI ilold-solana-core Solana engine (Anchor IDL + LiteSVM + runtime overlay) - ilold-core Solidity engine (parser, CFG, path tree, slicer) ilold-session-core Shared session, scenarios, findings, export ilold-web REST + WebSocket server (Svelte frontend inside) ilold-mcp MCP server for LLM agent integration ilold-render Pretty printers shared by CLI and MCP ilold-help Tool registry shared by REPL help and MCP tests/ - fixtures/ Anchor + Solidity Foundry fixtures (binaries committed) + fixtures/solana/ Anchor fixtures (binaries committed) scenarios/ Bash + Python end-to-end suite docs/guide/ Public mdbook documentation ``` diff --git a/crates/ilold-render/Cargo.toml b/crates/ilold-render/Cargo.toml index 32b16af..6067cc7 100644 --- a/crates/ilold-render/Cargo.toml +++ b/crates/ilold-render/Cargo.toml @@ -8,7 +8,6 @@ repository.workspace = true description = "Shared text renderers for Ilold (REPL + MCP). Output is byte-identical to the legacy CLI prints." [dependencies] -ilold-core = { path = "../ilold-core" } ilold-solana-core = { path = "../ilold-solana-core" } ilold-session-core = { path = "../ilold-session-core" } serde_json = { workspace = true } diff --git a/crates/ilold-render/src/colors.rs b/crates/ilold-render/src/colors.rs index 8a156de..22e1c4f 100644 --- a/crates/ilold-render/src/colors.rs +++ b/crates/ilold-render/src/colors.rs @@ -1,5 +1,5 @@ use colored::{ColoredString, Colorize}; -use ilold_core::classify::entry_points::AccessLevel; +use ilold_session_core::exploration::access::AccessLevel; pub fn c_accent(s: &str) -> ColoredString { s.truecolor(140, 170, 210) } pub fn c_warn(s: &str) -> ColoredString { s.truecolor(180, 150, 80) } diff --git a/crates/ilold-web/Cargo.toml b/crates/ilold-web/Cargo.toml index 7d1ffe6..e59d648 100644 --- a/crates/ilold-web/Cargo.toml +++ b/crates/ilold-web/Cargo.toml @@ -8,7 +8,6 @@ repository.workspace = true description = "Web server and interactive viewer for Ilold" [dependencies] -ilold-core = { path = "../ilold-core" } ilold-session-core = { path = "../ilold-session-core" } ilold-solana-core = { path = "../ilold-solana-core" } solana-address = { version = "2.0", features = ["serde", "decode"] } diff --git a/docs/guide/src/getting-started.md b/docs/guide/src/getting-started.md index ec3df85..28b37de 100644 --- a/docs/guide/src/getting-started.md +++ b/docs/guide/src/getting-started.md @@ -10,36 +10,7 @@ cd ilold cargo build --release ``` -The binary is at `target/release/ilold`. The four subcommands are `analyze`, `context`, `serve`, and `explore` (see `crates/ilold-cli/src/main.rs`). - -## Backend detection - -`serve` and `explore` auto-detect the backend from the path: - -- A directory or file containing `.sol` sources is treated as a Solidity project. -- A directory containing `Anchor.toml` (with `idls/<program>.json`) is treated as a Solana project. - -`analyze` and `context` are Solidity-only. - -## Running against a Solidity project - -``` -cargo run -- explore tests/fixtures/staking.sol -``` - -ilold parses the files, builds the model and per-function CFGs, and drops the auditor into the REPL: - -``` - ╭──────────────────────────────────────────╮ - │ ilold explore — Staking │ - │ 8 functions | Type ? for help │ - │ Web UI: http://localhost:52431 │ - ╰──────────────────────────────────────────╯ - -ilold[Staking]> -``` - -The port defaults to `0` (auto-assigned) for `explore` and `8080` for `serve`. Override with `--port`. +The binary is at `target/release/ilold`. The three subcommands are `serve`, `explore`, and `mcp` (see `crates/ilold-cli/src/main.rs`). ## Running against a Solana project @@ -47,72 +18,61 @@ The port defaults to `0` (auto-assigned) for `explore` and `8080` for `serve`. O cargo run -- explore tests/fixtures/solana/staking ``` -ilold loads the IDL under `idls/<program>.json`, boots a LiteSVM with the program binary from `target/deploy/<program>.so` (or `bin/<program>.so`), and opens the REPL: +ilold detects the project as Solana via `Anchor.toml`, loads the IDL under `idls/<program>.json`, boots a LiteSVM with the program binary from `target/deploy/<program>.so` (or `bin/<program>.so`), and opens the REPL: ``` ilold[staking]> ``` +The port defaults to `0` (auto-assigned) for `explore` and `8080` for `serve`. Override with `--port`. + Without a compiled `.so` the REPL still starts and IDL navigation (`f`, `i`, `pda`, `vars`) works, but commands that drive the VM (`call`, `state`, `inspect`) fail until the program is built. ## First session -A typical first exploration of the Solidity staking contract: +A typical first exploration of the staking fixture: ``` -ilold[Staking]> f +ilold[staking]> f - [P] deposit writes state, external calls - [P] withdraw writes state, external calls - [P] claimRewards writes state, external calls - [R] setRewardRate writes state - [R] pause writes state - [R] unpause writes state - [P] rewardPerToken view - [P] earned view + initialize_pool 0a 4acc 1sig 1pda + stake 1a 4acc 1sig 1pda + unstake 1a 4acc 1sig 1pda + claim_rewards 0a 4acc 1sig 1pda + set_reward_rate 1a 2acc 1sig 0pda ``` ``` -ilold[Staking]> c deposit +ilold[staking]> users new alice + ✓ alice created with 10 SOL +``` - + Step 0: deposit [P] external - State writes: - · balances[msg.sender] - · lastUpdateTime - · rewardPerTokenStored - · rewards[account] - · totalStaked - · userRewardPerTokenPaid[account] - Sequence: deposit ``` +ilold[staking]> c stake amount=1000 pool=pool user_stake=alice_stake user=alice + + Step 0: stake + accounts: pool, alice_stake (pda), alice (signer) + compute_units: 4823 + diffs: alice_stake.amount = 1000 ``` -ilold[Staking → deposit]> s - ════════════════════════════════════════════[ STATE ]═════════════════════════════════════════════ - balances[msg.sender] - += amount (step 0:15, deposit) - lastUpdateTime - = block.timestamp (step 0:8, deposit) - rewardPerTokenStored - = rewardPerToken() (step 0:7, deposit) - rewards[account] - = earned(account) (step 0:11, deposit) - totalStaked - += amount (step 0:16, deposit) - userRewardPerTokenPaid[account] - = rewardPerTokenStored (step 0:12, deposit) ``` +ilold[staking]> state -The full audit flow (`who`, `tr`, `sl`, `tl`, scenarios, findings, export) is covered in [Solidity: Audit walkthrough](./solidity/workflows/audit-walkthrough.md). For the Solana equivalent (`users new`, `call <ix>`, `state`, `step`, `timeline <pubkey>`) see [Solana: Audit walkthrough](./solana/workflows/audit-walkthrough.md). + alice_stake (UserStake) + amount = 1000 + last_update_ts = 1715789432 + pool (Pool) + total_staked = 1000 +``` + +The full audit flow (`users new`, `call <ix>`, `state`, `step`, `timeline <pubkey>`, scenarios, findings, export) is covered in [Audit walkthrough](./solana/workflows/audit-walkthrough.md). ## Inline help -Type `?` at the prompt for the full command reference. Append `?` to any command for its usage: +Type `?` at the prompt for the full command reference. Append `?` to any command for its structured usage block (syntax, flags, examples, return shape, related commands): ``` -ilold[Staking]> sl? - slice <func> <var> [--backward] Dataflow slice. Example: sl deposit totalStaked --backward +ilold[staking]> call? + ... structured help block ... ``` - -On Solana, appending `?` renders the structured help block for the command, including syntax, flags, examples, return shape, and related commands. diff --git a/docs/guide/src/introduction.md b/docs/guide/src/introduction.md index 8c1d5a5..5151124 100644 --- a/docs/guide/src/introduction.md +++ b/docs/guide/src/introduction.md @@ -1,32 +1,26 @@ # Introduction -ilold is an execution path analyzer and interactive security workbench for smart contracts. It maps every possible path through a protocol (every branch, function combination, and state mutation) and lets users navigate them visually, branch by branch, with an LLM reasoning over each path. +ilold is a Solana program execution path analyzer and interactive security workbench. It maps every reachable instruction path through an Anchor program, executes them on a real LiteSVM, and lets the auditor (or an LLM agent) navigate branches, fork scenarios, and ship a Markdown deliverable. -The tool loads a project, builds an in-memory model, and drops the auditor into a REPL backed by a live canvas. Each command answers a question about the protocol: list entry points, add a call to a session, inspect state changes, trace execution flow, slice data dependencies, record findings, export a report. The canvas reflects the same state in a visual graph that the user navigates by clicking, expanding, and forking. +The tool loads an Anchor workspace, builds an in-memory model from the IDL, boots a LiteSVM with the compiled `.so`, and drops the auditor into a REPL backed by a live canvas. Each command answers a question about the program: list instructions, add a call to a scenario, inspect decoded account state, fork the timeline, record a finding, export the report. The canvas reflects the same state in a visual graph that the user navigates by clicking, expanding, and forking. -ilold supports two backends: - -- **Solidity**: static analysis on top of `solar-compiler`. Contracts are parsed into a typed model; per-function CFGs and path trees drive `info`, `trace`, `slice`, `timeline`, and sequence narratives. -- **Solana**: concrete execution on top of LiteSVM. Programs run inside an in-process VM, accounts are decoded from the program IDL, and timelines are reconstructed from per-step account diffs. - -The REPL surface is the same shell for both backends, with backend-specific commands documented in their respective sections. +Programs run inside an in-process VM, accounts are decoded from the program IDL, and timelines are reconstructed from per-step account diffs. Every `call` runs the compiled program for real — actual compute units, actual logs, actual account state. ## Core concept -An audit is a conversation with the code. ilold models that conversation as a **session**: the auditor adds entry-point calls one at a time, and the tool tracks how state accumulates across the sequence. Every analysis command operates either on a single entry point or on the accumulated session state. +An audit is a conversation with the code. ilold models that conversation as a **session**: the auditor adds instruction calls one at a time, and the tool tracks how account state accumulates across the sequence. Every analysis command operates either on a single instruction or on the accumulated session state. -Sessions can be branched into named **scenarios**. A scenario is an independent timeline with its own state; on Solana each scenario also owns its own VM and user keypairs. +Sessions can be branched into named **scenarios**. A scenario is an independent timeline with its own VM, user keypairs, and account state. ## Key differentiator -ilold does not detect vulnerabilities automatically. It gives the auditor primitives to drive the analysis: build a sequence, inspect state changes, trace execution flow, slice dependencies, and record findings. The auditor leads, the tool answers questions grounded in the actual structure of the code (Solidity) or the actual runtime behaviour (Solana). +ilold does not detect vulnerabilities automatically. It gives the auditor primitives to drive the analysis: build a sequence, inspect state changes, fork scenarios, record findings. The auditor leads, the tool answers questions grounded in actual runtime behaviour against a real VM. ## Where to start - [Getting Started](./getting-started.md): install and first session. - [Concepts](./concepts/overview.md): what the tool does and the data pipeline. -- [Solidity Backend](./solidity/overview.md): Solidity REPL, CLI, workflows. -- [Solana Backend](./solana/overview.md): Solana REPL, runtime commands, workflows. +- [Solana Backend](./solana/overview.md): REPL, runtime commands, workflows. - [Reference](./reference/api-endpoints.md): HTTP API and WebSocket events. - [MCP server](./reference/mcp.md): drive ilold from an LLM agent. -- [Roadmap](./roadmap/solidity.md): known gaps and future work. +- [Roadmap](./roadmap/solana.md): known gaps and future work. diff --git a/images/diagram_solidity.png b/images/diagram_solidity.png deleted file mode 100644 index 26c003adff40bd64901201092c169d888de5760a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 417549 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þRO#i)Ps^I z{$=oKpxS2y@(3+n5+?c=R%oZiM~*!9^3)L&Ec3cP;7>zn9RM9gdVRs8OJk#MILL}l z7N8Qxy2#!Lc5Yqcxxb3d`O3w7g?YGaf5djhJ$-Ugwb}vEwRl^iaLnx{EtfNke{4*0 zECJlB-)4fLX8vhh>ee%zx{>QmcS9qjzFNn8E$3bxS@S{J<l8h~l8;^>y}9r}+{~24 zA3IsHG{KFREv|clF+O90pv{!JZt#i1Vr`uzqKko<UrJ=_n-#y;YR<u@T90CGQ=2zW zqU005$(5AObjD)QN4t+MzeF1rLS4U_ujMzleLCnW2Eb8UWz~ep6Cx5zZ^1Q-QC7e) zJ~UpxS=A%kMH7pbaD`Kr-c)w%f)^t*G=F!p5Ue0!y0|rH%j)mm>X#i#9Z^VKzo3+w z<kNeO%s5XbHa1?e%LS8In3#yjsAI38tf@IJE+qoQHYTpyA@=Lg%A4elEnWn2vRa9h zm-T{z24yxIoH~@Ou32=Gj!upW3-0ac%HiM_i|51*)oeYh%=|fd`(UMJwK<<Kuf=sH zrXiT|!~Cs92hz)l^j$J0^&fBebOtU5attQ$>irDv>r)+_TFEMO@v-S22w$OVc}1*{ zSEp>J2=g&ak-1f8v1zrh^!RaP1Cgbzu{>p#UQqtQ_SY_Bo9+vcB=Uph`XKojg|sL8 z-|(3QRTf2czTb3d`_R~A)R23j=Bp|$VxEhoLCggH?o+bMGtZhiVUI4yjrjBDY$5<d zE)``bI=mcPD3p{Od5xE?p#x^qase4<MePH<_DV0!gX(8Y9j_(3Zsg#g@c3hj%ZGkc zQlI7>sMK3Iy%R?7UorK`{)pK6_h6jx&lp(O=%M!mprIeR82quVr2?R0VfQ)%v}slI zb&G2E^peB|mEn(~wm3?*WwN#!x>f5vXZmL{^Hx2BRr)OAwMcrCM)FQ2ih7qQr1v4X z?jSusYm$WvLe+h0Rn-`58g`$Eot`ZBhc2`DE2iF-&gw`sRhoI3D?6<~0rMVaw0B?o zUdy9|)4$i5#v1pk-P`%0YYH6LzLiKFaHPj%C-s3mQB<TPC3oXM+-wwW(_KyN@%lo4 zC?6-+wy>yIyLI-olaqo($HHwiq+4Q1$tBE_$x3{Zr!uwW@IYI|Fp~Lf(tHB0^Y8+@ zzv&Yfi>WkijpCSg?>z*$@C+b8%#HJxFV6{=?4EVmL9nSt2VbBoj@q_%-C^($ej1jC z(DoiBhqw-+ZccA2N7D{HcLOs05bz;p#8Na^B1Br;Ea$<Kpx=)7Z+79=cVbNF3E&=+ zEbaq9ZNeA7Q5D*<C{jBA@!E4409mpEgn8v7)wMl`Rinnpn8=72a5nt|r*hAgxh$T1 zs|(cJ>X+GcjVo=es$38J+9?XN_`L=^wVC_0i&`Vnd`j-Hsp~CY9v%cP<GUJIGpZPi zt74Lv0c9F#djq?}Tf0#b9T($TYEb8nn*sN#lY7w1Y3y(ApyQdWa8GEt!x~MF`A!%U ze2EAM$lnc39COS1>7UH!{)O>#MGRrySu!C<e=`nb_;<R!mM8UIrbObzL_e&iI=c;- zwuxCczHK;dtWKdi>?c3)?myIZ<22uXk|5$yFYFI<a7Ri#?#YC11;_0~DQv`RNvQ=@ zH#{QJ(N4<J;nm3Mtc5XhHf)bYRg{=nj=sk`L^(C5!I8(9JARFO;x?)N&{;j=(9r)u z%D&39Wymosttk#xNJC$|5%g=E<u%xH=Hw+AIk~vJ;Ft&c`lKrpl`bC~RfZiha^r`) zGn-%@YNEjN6uTs33<3NY6g)VeE8ytq5oKlSUL&J-Bkpd8-wQwHo5D@%WFRRXF-@XM z=VXy<Zr%g6<z{IK<t0(&R4B2Z3OF}0EHe9Y6#1USjC;60zlc1S$NnmM4*Ed>+eltR zqJ0um{9oPqCJ$zD4(UMtdf$p?Z}<vYQ~Dex`eVcz_A!n6Q$&|;B{M#oS6&u4!{qN= z#Yy3q@5>kpTl-eL$PYL|H*JM7ZKdbm(71;BG8)(1!9ty_YfFb|0a!!yFIaQ=O1x-O zs?Xf_*Hq&d6w$1}zl}w@%Q+xa7d;KF{`aSFGbkxO_gr>+7TDIKjf{M`TDrPVvG91V zkB=1mGrbEJ+}fpUPyXs;=TgPKzfl^h1Q~)c?N6*HVa5^qKXv*x-APDM#$f9Kpl|!s zl;p_8D?IkE@F+#J6t4<jXUJA6D`m)ke1`_qIDmrQ%!&#vw!wkskIZ%#(EUv#0sSQ( zBEVZBbzcK1>C(#c_$LV7bQfGRaWhyv+3F9Q*v+EAZu0+j_mrO=EkHzic4X#Q$4&hv z?|UwzGY9t%Li9ajx<oU(+a&JRY;H?{!Gs}V-d5xnCHA{phaZ6=3lkLgsI;urPpX#Q zN28^UF;}T$BkZ0AHnCPN_VU|nyu8;0szAn{{?0EQj>YWC>CJuTXVH|*L?E5C5*La6 z=kpI=Ql|)-Shp0}Lv!3En!M8hma(v(Jt2Toeh*2Z1`|%#Xt@T8FU|bLZmkPru+ws} zv2}960Koe>GBkAEs&JDbTV@%6VpQGE^UFuTR}6j;dM=}Jui=}w(egGQ_%VgTKlX%v zLr2dz4*dj>*$CO(fI?;Uh#a@<xaa7JLB;ru!QUBGmK6AkU`$XX6ypx2RQwS^j)CPK z2GUR-UFDe!e-rG#zN@<@Ml7D6&#Dltl}|X&&Gqi_Z239mYk(B+TgWAxuU<KXF=%VN zE`m`==w2v#Wr(15VC2J;Do!0^li**P_n+zT`Wn!UoOkxh$2X@Bo|CK-p9O~+z6Iqb ziphN6FWF7EC!kp}CZ*Kqe2rp(U}3Lq$J~N~GK7_xneAnl`M%s~_NF|mvF&YlQ#)|i zjq>tx9&26Ql+r3<CdEr3*_-__tQyImZ(hG1-rRhbVzZ<p%bD06oIgke_~ePa%6xNB z&=wgVZ`KgXrdu<~Qt;|3{{v9@W2IA9tgFxzL=e~BF2A6orZ&Y=fJZ5yT2fkSH5N?D zE)Xo}y8NgWT?MhR6PwS>&aQ-wPi{1!S7qC%_Hb0ad?4*SI4QI92MP(-zx(Qn!35Y{ zyi;?mpx{P#v4K_bzK|ogv60bp@8UjW7dX1aiA24pr$^F<$84;mgu@ghi60oXMKkjh zp<Hwnw(H!QUy<=FC2*SR1jK-nngnpVSzYnV;NX<cA;G~>)ebWwRoO~u&X+YXL4i<L zv#`^AbT3F(a}N|7#1VVB@Q$pHkMs4qxJ<~y$3r;57oF%YHv8)#s0DDe#u!b~_4V<T z`=fBRS%@?|X=bULcL0Z&QJ!$m(+_;uSn8Zv+IDzY%-%Kr#wBE9*u~nq$Z5LTV?B9C z3tqQ(F2gS-MJpd`N=}iJl+IQV<f)MRlV6UtO3dYanLaJoJN?-W;wC^!J$z^;`bETb zIb9*sq5m7$_+>Px02GCY&wdOzZ}p?Y5A*CN@BtimCq<lJR>wxn$*p;Z_)PiX6`9n8 zIl9`!Vl{-(-@?Pv!a`@f^R*1-HjV!1!s?w4a1NqmR0{fE^q`uJJyA6a;k5O^2y86V zQ>-saF=+hywaBTyvyvlvp!8(>d!6^Y0#L`oa)E-Uo$=nh*A&P7`=faS;8)ydiKp^6 z%Rphm$8WwEn37Q_&;U`#zGB)vy<CJ-!mS)QCs%|5D3Y;LqJFLKv$p|iRX){$)9<=P zf+{wDu1s`#Q*C*QRL^B4JsC4>3FQyY$IbW}$6jd{c9s9fjUsHVYWue#9zT~2vhB+2 zq{FDn-|J#$A(!qZ)k7#uL1{tq90X#wv;@Y8UuXY40T{JSlH;MXjIJMa(SMu@Nh@jK zoL!UEAnqC;zqd99Mi*T0p)X1|$5QG&9P1zp)q)@c?V-yraBPL4qgb!?XjKoy<ye_( z3fOW;TZ8juJlLq8o5zSMeCb2gyviq7*<g18#S`Zo6MyD)vJu;G7d7{yf9R*%s6p5J z>{`5zVr()4l^F_3N|iA(;9e|N1z$DOI1+F#qaYZi=6;JDEku`-jsmKmAPZgLV;_?H z9>Q7#(_kYWii(VUvLTDEw{TaQ8+^A=wauLgXXP&XD%-X#EaAiBr~&4n&c5GM^^gG| z(Lqq=p_%D*W6-YLCYvOLyGRR?3@Ri<bOtQ*t(^JYyk?hP<_``UQ`fMg#vyMA6yW0> z8n=JTi(0F24kA661SH~B_omlnlm7LAEL?cInY?i)#qUr~`2^#-c5X{l<2$W_?k>Uy zOGQ=d<8`dm;~Lvw3T%W@flyt0Ud?&l6&9Cl!E<gb^;||OMe%PfNB*Kd*x!i`z4kF< z8Lr-mGDIf-<DL%j!0YR`0L6=%_2Av-etDmd>v%~1fV&yzm1(&gSt}%e+jqhj?g7f- zCxgUOeyKFKZutb>IP#ll##o8ZxbxzFEG<A2apwWAF7Px;qg6(F7}p83-$Z)KydF?l z9$N~%ElF|uw`2mts7TXo7onB*i_3n##_hjcBM8OI#Lf8jq$2oUyb(9EsiQO_J<(<_ z4PSp~y$8aa|HPL^5+Te#tq0=fu43~qKR?t>Ok5%4QueQg(cdy{Bq9?ukQIxnAJwMU z_o`=jM8^5+KkMpf+Gl`O4ssGa(LC|<@PXWE3%%5u#Y9cuq3`@}WOXPfnx4~%gj%|~ z<`+B)#d9>6xuP`+3CU|IL0Jhz_E}I!v`nu_>u?Xz@Y)m)8JAI&Jcy!LI%5*(n+m^E zJ}<g2dg{~_A{og$syD>7O^r-mu-}#e>5A~}ni}=6#Bf6PV2xd6a&mH`f0rL*)a&us zxXL|v&^sBJq~6w_uVE!$s5tGuN+gaZm@jyij{4g~kI0qVZ`p?|sL)dV64m&%bQ1Ar zJ3f>hAbingBe`>ObZJHoxk-s|LUJqf(6Eyj@L0MpfkT(L|8j}vOjK5D!B+6pZneD| za)b2AhKzzht!suj%x$WA7TacG_N({1yY^=<!~#ly6=Z&_omkV@?+@&*lRA&rWc$G9 zd31H-6Z-Y7bxQ}R>4Zzu#+*0zocBLB>^5G|8129EeF@3nzcgH}i$d&Q8yg?~1#SJP z*ntD5^1(I#qj8e5fbODWXtg^zfX8ZljTS(Z=FjE2ASS+5QLYDwqT+0EGgl3hu}`@i zJAp}=uXhV-51Iv9+2iRQNN>iRCz&eQFDLyd_|U(iK9;ZsM(<B$v61uQ#fy^d{aHqT z6QhkN>H>4wQ~jB}Wv}F`_fpR;Zh>>J=Zy_$e@Xd1=@GU92n%2r@{O<Jr;ZWFakKv0 zRKI#QG?*G_j7uJQJouAuv&prR?*&!aqsjQw)dsDy%r`|Ck_H+Wo20{HK#;dcxC(Mm z{OJtLxw%AX;WXU+YK#{p=s~hdMph=GZH>f!%HHZjMThB+iHW!R2Ulg}y29m5^vCNn zGU~S&P54k*h6^CgI?gzZX>T-RO8s>%N_ua!UpF_ZhE+hm;GS)OatCuKC6{VZBJCu1 z^~QWZ_JMb!O<9kr``y4#2?uRoJMVdUEj0BoWoNdfi0&tF=~+dSrfof=XJq&~pC<H} zU{Fu(fx@FsN<K+8b#+iuV-#c$1i##GbU2vl#pr0GSl4bO_>aN-3pxf29~~AKZCOoC zPqO7GP+(RO?zF*k867hle5yG3$Q}YR5!uVdRSJrfqSv%}>gk>Z;>7{pKKw^ii!qov z*jE?TRc><js&?!GPLadA&55y*a~$M?Y1^g4l4u;PuSa9)KlK?6+{PZRMi}zjf1@}k zLGujqQIAGos-8dMpTL%5$1?#(TkQNK2An{$W7s#?pHxwM)AXeh_Ek1IPPy1I&+JT7 z6YLwJE)}XG@RCMVsY0BGMK20Ag?_SjPt<{On|Co5NUzz!*E0uUrU$~0MyEL|$M<|i zo3{s3d0T`BJ)PoUQG_E5BF={y7&K`~%T;wS*bNv>kH&y(++(Bv{}9*vn_zw{HHiHG zWo4OW0r~v79b4ta8pw8Y82!Ab!q3@iolSDLyeEg({7kpD{OfE*0jxd1xhi~|!;z8c zmDGy>WgCxKA})_^4kj7Qm+s7rYy#b3U=vZa08Cb*T)%U|zKZ93)#8?YQi=`m^<C2W zcBz~(chFf{VhL{ZwGAhw9oCogis$90dM{?3&MRW&_ZlmJTzeM8lM<#*;&l+>gc|BN zs78MGDVfP=>6wF_^||Tf6gSmBv)|{*d9d^ICsvf?cD#>XelpcA%JRTd0}f`M+8xwk zlr%dBVkX3#M)=yvpy@BZ2|WpCV}Pf~x$qvL=4`=b7!f2;3>2#pZ5b+MOUPEej<c2W zAC2wp^W@{*Q<8;Mu7YzXsro@WX_I|_YSVehwN9PYx<O-mX0ol-P}*oA2svvZNK#az zf_>q4`p+%c>SHcrDmwl55cQ1P<jY3m^uvXY+XQaGn0iSXI961ZTkn?AtlgDBjBK`P zS?{%$m!fsX5%z;5(kSrRphK;y*KL~4$vKB7r)T|aP(xFo!i)#^ty75)e90G)*!3z+ zs3~60H0qJ)-fSIDP^L}2QVtTF6s-0VacE`S^L^EE@RtspxRe5*Fn3-k+y7vU;x8@} zi3@s&i$v945&GQ1jm`d8L>x$Lwg{muFK!fZkaOe$-mdQQyKVN*?0p?|j{-zuyY9p5 z#thk0O)mOY?}q_*=mH7=zF%H&x&yME$okb}RlO>nF~|isQBW!}T<4JFS?ZBJu1~C? zaWF{=k{+$n3;H&<lRfkAJhr99&A9D)pO4|Nz1d)Jo!S&`ooxIp?rF?5)1lrQ!k&l> zD<wXgzeABj;=sx2t$APhxeUFxP-VSxV2GSSfDIG=!QjtBJ+J#_Y7(ELsS|!PE-!Dg zBQ6z}&(C}B>=)=oLwaGlUY)6|g=F)<$Sw+@K0H$W$AKX}{Es|R(76w-0H~AFfylQj zdOeea`rd3F!OlMiI}$4&o?}!hT_a^>|59CQI;k^E#_v8WSp7IJWW&GJaui*o0$*Zx z|H$kR`fFZGs)_;Ew0R(q2y{@A9pmipFd7V^-$UZ-p6j@+6r2)zyB(_!1-U!V{zb3T z3D7pxRQCxxtz(mR@jW-q_Xm@svN)821~1J{8)~bx;AUi~;ox)Ass=@+aaqOAhA-jP zV;z`t1KTYOso2SXv90mx8wHWL;qq%+*~5k@0`6-*I_c3#>jNdmD*K%l*2y6nAuhTy z^ZdVdX<ZS}5voCJC>B9(9v<18HDJ+P0XK9PsRxowqMkEq%z_Rh)YqA8-sKY3Fs6%} zd3q*~l~<TQN4=&@Yx|D5v#Z7z6h_W^KCZ}KkG4<nI|xMQ21TW@Coe>U)S)cO_bS9< z&yxL1z)O3Z(s)k>S}sQR^Gu-H6o=-U@;?I2X*w@?{iNd9vWto5csN`;!Fc>e!)2F% z>T4I_TD@TBFaI34Vi$5V_B{;;8dxsdP7!B!Mw2;^#?ubTO+Dk)FKG-6xk?utJI;bG zUn-wDxN3Xa@El{?MsREZ08+!}GBB9BpG=4DP{l@<aSgwpl8CC>Rw(6uzZvTf9fB|> zUqoXDUu}RvYc?3ke*uVJvR)^|pybiZR!|awNp!~d{sYYaL87x(NANSz>0j{kjY}MW z-{jw88bq0$G>Yy$e*C)N=XbBjgnlIya`YxBf_kh(V*@4BbYHO8*$*MzzsAkokMi;q zuO0I`FeEZ%d0xS7v<rIC7XMTRqoq`kcSm{xBmzmDWv)v!&s?~AL(aNhg-yRGW)tq( zNDsnSFE5d_v8@Sd08FSGHUp!RMY?au&cHfry(2EkC$f>UvKqa~&2s<z=~W-tiY=3# z{{p(RVoD^xz*6WmsJ2uo=9vjOsYkTu|DpVXY6&R*HeNJ@GC+EFA8>BBSfmo>bpvGb z#W&fHr&?tO?lU2kd!4<vl88F(5z$Q_#uqX&J<4zzkgU3>|5_;!k75dr&PSkgQaIZX zgx&nY{M3BE`%b<2>{iyfR1y8x1zYB`xEb!gpm0PdGEyaT#o|peD|>lM-$MbgH%KI} zzI$^$vP~2hSEL#oc(YCLeyucY>RYA$&J1Kb5L8T@D-r$GPg0=XN)1nw4?iCFQBq1Q zC?u_95#0f)xMzRsp8qv8fK51^5n__L$+wo(UE95tH8<jExlk^iQ(0GujtF%p(bW#1 z#P;6Zsj#@(MhPpzLf>k`1x9|iMO!iGB1Uz|qt=edVfInBLCJQ`&rFk_x+OSV3=EP; z#GNMWh_hw)e=?&>ihSCO8+LV2ml5yQw1yo(r7jV>1jxw<zRfcJ^7LbpRDJ+Y8c0=y zBa%el;TCnoxuCp2UhE6Vi&_1c6D$^CQj3`p^vJoq6G1rg)c?$(B74dInuxVqj4xyN zq(QLtKFNRjZ(aQt_NyXA`yh(pBS0Lb*>;Hp7!<T=i=S+Ltg^>Byqjj0Z&^XAoNu4? zHv22E0!cqdVHIox<{$0c?}|pbUS$U>ceqI8lUTy<ayZ{ruTa%cOU#7kM+g;w>4C1s z+j3Coe;7v1e;t%_J~z91A@Fc$#0mgy&ZS<AH*;u9e%pqBweToj%|7!BonA4MQC675 zLxRPp*huLBWeK9Ut#UZyKT)h%HKumauWg4=SfHF>dUO%4K@X|0F|U8Vc{JCx0H;Yl zcCcUuhO-XkoNn;z`@g>LxHa$5bmqfqqKuTFIKp{g*R9wqUS^6}XeH8TW2rY6R;%6w zMv0Cr(C}B$KVd1FQy!LUdU0|Qz_BX+>s9N<hqim!I`Kz)_>T~i>qyHO@uX>OeKbxQ z1UZe;m%9s@=B4xi{DNJ(%>yMGu56vU`kM-v6+1G&^P&%E%P>{d|5kGGuXoZw)xb;3 zN?TSK&Z^7yO?+yZgRULdqJv)7-AB{pD|tPPCvExjcytw8B(F&zI=}y%G79sr-6MSI z3sw+mXe25Wt6`CJ`*%X8(I^1$`S?<)R>$#}Un=v75ZP4EvXlIzqZOc4{y9tRT#Po# zFY1QRoc!(gBRT<<Qnu#2cw82Tvo3|E7(VAKCjAj_8-X#enT%kXe3MNT*V80Li);z} z>##ejeUQTm;XHGPLcsufXaaDZL~b(_eg=(4%&Qsr9E*d-`G`3|=z}V7=VB()U+R1` z#uqPxP1Zi9U%l{`X44n}(Q{2wKIK_w$i}{T@kLlK?aWcxAKKx#zY*(W_3w<oEddtx zv1Ti#*w6D?jd11W<b?NTD}4nFM9Sw>P;0EJu0B$QkB{H^Loty{CNDq#m?;N~QvYGf z&zw2)dS!iEuiE~z=GgA8XVRTQJNBd}78<>?-ESB~L|_#fb-D_4(xK6xK1otkW@R<2 zJtPzHtjf&H9I66S+WxZ3EHooBHPw1zelRNEeEA#5y2cn!sH>?}pJT;5dm$PF6MG2R zMar({c$t@%S7UW`^@+}&_nbvfMPB*7P>t>Pr}^*D^sI-H<=EOxmtb&GLIU%fyCEfr z{<I*l6p*|J8MEzGQd>Kw_E1Jafn;wu-L4$|-9lDTF%}$xI8tS7Y+Uql1lch%14YHg z#tH-{g@@AsQB>M>CG$_*DB73m0n2Eu#v&t$OzpU#Aje=e_T$H6H1TRtFgP?N39qw4 z9C1uC^noGSNLYmf{UFpOEYoDPWj@%;1pzmQht_+HZc1qkt^-a#5RXh8UY8$Exu^uM z!%W919GGEQAE)ylZZw#MxSYC_(J$l@Mrd?4Xs&2xX`(rVc+c~W59w63$63<c^ZAgq zYL7g(CGR#Jc>OAxrX{uRBXoI-q`Ia`%G>J?owwK%`W%fzf;M3NamfRZMOpmGiGI(t z^tf|o0Z`kW5Cb(F-HIm($$WO=7q0R>BA(OXXVWZ^<T7{!PWVtH7AL)_WkmewkV7ZE z@$eCG^5Nx7N0KS=eKem=KhvIINPX`Yqi!F_cm=Gq6S>trNuhw#U&TgDvVnPC!5OR$ zCC28imKBZvb(L^KP;j)_DWNL6^;{x7nhOV4Pq%6F55JSaq`UhHtgdBTGS}|c0`$0_ zBC!pi;%z2s<kP=`gbB3(nm#U>8w_WRHU|qpp8%d6tp8kUjbhSu;-$ZNQ(?L*&1S;F zr=>p=o@7(I0@5}^?+uGSzU-Qb`uI_tViTxk=MS(l1T2ZL8Ur~R!Qgy9^+GM<2`T}{ zFXlQ{NU$FC$&)z0IY?!pF4{yGgS}+I2gKD@ZZJl<!>IS$kKw{y>*JeAB4@oeE0DoO zMHxt#S|b#djigFc(WrcV_-PYq>9zS*E^Y+wv$6Sz@^pLuCkGeap7D(_UE=(WwSfBd zG2MfoYNMkXsSr~mqahS3W@c%;$Ave5Couj8@%CN<NS^GZ4iBb6RA{-}H;JTw7j*ub z!WQB$85gT}jkVaL)t~laNhyptY>CMq5Ghi}n|bVr>+CIyv1R~qsXy*-nD28fny?^| zP60&vxp#@PnXR{qCgz)H6(swM>EduZ6Y=t7q(@-&gc`(ck$_tVLUy#2lG=MDn*x~l z`C@YIiJAhgbSpK>LA#BSdEr8RX*S}S@^c~HNGQr#S_d%k{$6{j8KM+GNhaolu(wO0 zV09#{z>6|uq2y`l{*9jc6u{D#YOtQS)Tyh~Q5+}HI8YWVF02?noIqA+gbYllmCdCe ziA&pPR{c3?PH+-fb9(L3bBN0z5^sH6qE<|I>=jUW42iMvL+1VYqZITPOy33GQ+RDE zraL_*O@+9eE+mFPU65E>OwM3raLW`I!E_@wOBo87SxOOG_{641t(7)h?YXqX;Gfu1 z3Rkj2Im{~002t<NJ?;tO8u!?L^7C~{$KeM-zPK5Z?v2!6P5Zw*prk&q^oS?)`t}yV zx)T#)YFBJ7CjWv_hkw6M^4uMyFjIy9$P53)oFDRx=s7EHh7NpFVsZK<JC_gRUT;+# zZu;L|RgC>40GG4DPW&Gb51}9$Nu*ddCj4%{@Ezcrg}<}d9mTW1fB0Lsy%Y}}S^#ip ze$d!rI}<lU)~`twO7b$6?_rDGGgr-T9Y;b?4G?RZdF(au^=9Dd0S2Sq-UX_7?C|~{ zbgDJ+A%k(cezNZ}q`3tE($frqmyL{2Q3h|Czxa;bv9Hd$TR&FMAJ`eHa_85BK!P2h z^a6d_PJ)v%eS9~4xtj@{36}+>7V-Q{idZLqK63o&o5#{lXTE5`>Jhj(FQ;hvUO!m} zQTIr=*7#XaX!vJV>$ob{y3T7yG{fBY!XN8Lpy0z>G*43r|9OCM>eRu(r<Tfz<DG&R z$Im*dYSheb6-U!)UYofp6^i`~xA_XwE4xZV$mr52qP`Md7q+?7zTj4BGLV>%>Q+~T zT=0M|SXbIXQbsV69#LRcG+udSb&KcW75Pq?dpNYXQ(QZvpCj^AWz`}>dkzZDWGfDN zr0of2XTDL<_ZsbeBOP|m;Ll<}f%p$kO;bySVed_x&Mxqe|1N?!Mm|6&X1IPsL)P|d zCW>Daq$OLhqw&|4?R-wjvTDAcOjYYCmyR?t(TSmc&=5hPIY@sn-Jx#pcoq9-u*=T) z{rLDXouF4j%nWLB>7O_kFd)t}Vq#{3d@md^{wsPjiPMT=IW2S#41VwlPfG>3WqgI@ zT2yBtE_U0+><EquKUlhC0&Bxk7v3sx{V25B-CQYu%glsTP~gHbjWY67Oy>Um4}1Dv zn_*b~6#<-IZ=Z}%#G=mA6UantDTy+r)m3TrC}s1w?Wz8S@&K`Xu`e;<JcKQN&<F?r zZvND!(`j+I#{vWMjq7Gxm_KliwWR|$oVKhZh2z4NlO58M_4S3uMrP~&SHsf{&zXst z>7`yED`vjql(YG5I+3=_ou9*>JW^et&~=^c#jBT*Hz{D@$rPG}>(}{j96iFU<21N& z8+7ff5sp=dX|&-V)RGCw!;Mj^NIOO~=K{^~0)+Bk(rN&E8{r@x4t@|O5N5W+#Vy%u za<acETxjT&3Of>$SQHNEkIa?aKc<GjOJ5UQ<Eo#b_7!tX$5Z+^egV9G$>G4!V;6#h zV4kH&H-Ey8UK^5sb90nkt61~V)I>x0@^jna_0IfA7tYG*l!w1_1%0&24^*$%9%#V5 z=*h$vP8lDqX!@JA`R6+Y+HY9?fm#}pj!(q8Z-reD?&>4Q`avux7thp=<rgSpU%by@ z-h8#S>uVIjK>Zl`g)qY-CG_LDE*1#oaqZzV`tQj6+Y0%i*wZmG+}!xH-Y`n#bS#pM z30)IlOHY<=8x_@|nERf`lt!WypQ?h4j&RmQA%_`wOE^;0+K+<+zjQ3wmoMA*x6h;p z!g(WUxvoF{l@mBQAx<jIkxJ3HVsQ9l+YH3Yw(Gr9y+-$jUPwr%##t%$`4=4im8Q)S zA7oKQAATsw(SNlfPLYXgcej31J~4s+&v)2jCHpFe$t<fH5DA|V414tg|N6{(vNy?> z<0~)wJ%4`BZFwb4PRcEyc4s6lbFBS$=-~J0jO@#Em`exqkT7})vv+Naf20->iZpKb zUS_5#TDk1KmiT9={<$iY{(y^vzw!LqYeUSJfo?clcw*)ZtV<q0nP0pJj<QOE#k9Z5 zFPzNb!#h@ZIiRgpa;CwCCoPnpI&FGm`dXT|qKJnb_nnNwAFpf)|9Q*tmDtWDs4uj( z_G&MG)U`en6rMHJMM_fTPP!=saX#hx{66)8>T3n{!7!LMD{ql$*)yu{Ge_^W)lSdM z`NEZ}f6C&g7af`SxQJZYA4OV)=*g|_B*1s&zdDpYeh$o^@zfD=!HU%t{d=yGRi5uq zYCshW?Wom%+af2l)nKEn_%p;U$0^uG@U-$pu?P+n6d4ozJ=liwVKCA384?Mp#Y-wR zcG4+Sw9Zcn@ffO3ga-F?&@C^kG?b9oc&;%pv)nLCy_<8aCnM#+HW~oixUtg+Hr2(k z8jQnA`NM_;;@ohZ=7&&VWZ-u{rv7i|PWdSV?SU${StLB49rAr`Kt(AYoy&cWPofz) zT&~cWA}~Ns5Mg-aZv52Qz+6-Je#$3w@=<0fr^yKux%i|8_D$|D2#JNB`^y|i?>Ntg zk&#H&CpdQ=f3_e|BmR<7xmDQnP)@SfyOkc9yXv~_T1g+kw(`c%clS~6N=iAV=BLiv ze_LSJ8a1#NI#L7ZOQ5tbPotIA2PiG8NXFGaxY;VYa3OlBW;Rdp3pPG#z#Y`|UN8I` z<Z-+XWX{#67)8saS{=(gb339AoJ10f@hs>CQ#;)juT9%oWNi<}S&97PH#463LO8;x zL~-^HBJ@~2|72^Rurpv%cAT;x@*0s(Z)~SCe2m%V_9U<y#WslAAx#?^PDLj14_h^G zt{>Yd9pdZEEO=E1`N;hUyo{n0D)K*_{9jGMY!0x~W;uII5@tE}(Z7d&Bnudj(P_7d zoMY4Vmk~LB@kq5Vw*U0hRJ>l|r3{`@Q`@!YvsJ$KRLnLr7LLE+)@;>j1}UkmHO6vJ z!p$(?bgBl=3;z%4JX;xGybQKGy?Dn?24KWQVToC=F%~^1rwZ6S?gz_o{B6U@<`9aC z@t5a9T%AnGxh<k6r>94pzkVIxG3kGPk&a$e6kdVe>Ppd6fwd_RKk6E>TEPt1OQ^5h zIS7yJ@o2BJoUE*lr3*Cu&g;Rb3T+*oMJ)&>HeUF<ceh51+#%c>3xi5vgD)#cKtO;s zH>=n7dPIEul~L<Ecl<07sQG?5kdC*4Y<t^VqKO2>6oFyqwbj)z3w8JPSz55^ML7ai z>(*QD3~^d886DV+*spc8obP!XyuLWxi{fRHZjf2%%$!j#Hb>Zi&7}lG64i-_h$P`n z=H}+8%?xe)md~GCKlou<K{Pcrt+_3IOlFbT2qb8gt|c!Ei)<hPwe@KK4rOK5^XE9- z9v2YEa?3&R_K!_XQpopZ<F8)f4UTNM3UPSvx{g}AyVqDEEF_qjnJdOYEo99|5lSPZ zf28Auhl@*DwTnwj0=P*2<3jTQHUq2MD%{}j0ReQii;cI%PNl5e7-V6qzrkn2?O77K zYOH&@8Y0*!gnq)z(h|CKjMCI=3D)4_v$LZi8suzge_Fi5@WC4Y+t#&9lR_MfJn6|T zCa>)C&z3#=Jp;aF9hoJs&4@e3IJ#wK5js`2cS5N}tug1u=Et6%IlqY61*+g)B>5$* zO?Q;~k&Wo{Vjl7jm?32i*4Xztllgn`?uUbVp(MYTy;%yS7zyZGNI+TVlVT=ea0{Nr zt(Vx}&3wl3uJomc&(4~8c#q?aQ3$)2;hXgC*N|0ZH<FQa<;)Crh)#m@zFA~sWGZxd zF^9SCy><bF&acb+nArWp9ljj+>QDm8a`0Un*EbE!%`$KWsc}KFZBq?6kMG*vgG)p) zDwH7fY8+b&^>;vt==E*LogI&rA5w!O{ilY8OHD_eyV69FT9ED1E!3z@Wdmtvir|bE zg#0-mw8Tf@U=z2hfg)^h{7p??g~xm!Gx%R1`Y`ZOVYk)Hd<4vGCgpXry8?vY4sZ_6 ztKpYqL{mn-eH?@E#^>D^v`50O*JG1T!|EDULq^`e8HX&unYUdc_o*gfb@$^GQv$SC zVL+wlP<j<~;KvP5)Z(x}myC)v_??-1p(xm6v&JjYg0*WrNw#Fk-E~Q?Qc1-MFi<Ko z1G-9b!Y+fR@a-E~KASH7GfR{!Fx`ePg=zZLTVvz6IE6cT#a@;GnSE%!73P5(bDG@r z%<*LQ6)Qc*<q@ciZ{LIGV{MYAa=Rln1LNgLQ4*Or_bF*k>6-B(?=>Y%WwdY~aYNEg z=$#<Cdr<M6+{OrN6G_Bab*Yzxg^!>(B#}~zQx-j*wgVs7@O{y@nPi(t_B)T`6$}() zCK^K#&fMV&oTX+EYm?fLkPwo+8E43dMmZQFyhi{5*Hw&JYZNnk5m-X3u{#$$?)j*r z-#(Pv5CsO5^ZY^=%O2&TGZ-RGq!ak2AwAtM!QBQtWc#ZI=dK0B+WYovhSB~<_QI7T zro2o`;}X4k%k2&o-$J?bDO52wm+&83tSf>cNql#Q#1;vo$-0aDROj&q_wPS44!`i& zWHczKBua%0%JtC;I#$grO%Eqz4fi(GjeEkn95xxNbruSN#5ahzTQ3N?*bp_lv)X|f z7FM7g)rvHKAjIv+4*=>TpRg+4p0^HtzmX^g*4V}Cw{J%^j<F|-hBD;fWh^;9Q~$$7 z_{ClXcqy}yxdg=<tTlbg>=-qiybFLE;FU9+!|^gQ+V20RpUsj^ccjvmS(VuK{o&ul zny?(d{5%S_1adMaDAWZ#{zN>#?sqf--c^<JY2v4J9!|4cYhU7reC-)q%%mCLB9EUa z2DyCF;~};q(;bd~L)#L<SEn#C8qMDSZDF(6EwtaeW$d}hAdypyU3~230Kf+b1?3Yu zcU1EA>w#RQAM)VVQLi|2OAI^aO{`zcAOGQ`UlfQ00b(ARcwJQea>ugys#jw4FNkt} zXeq)zn`8Q))*<8pqP+fXzqo!!n!~@MG}w3PB*+k(zyc8Dqq?o@PJT%K^PYeHc;f0O z4iLRa{!@3EKxlf^5ZQ54%+}B-c`ZBq!pTSewyysKY3Qpm=|e&LCQ7FB5dRi*wwXuf zYo&kQbr-bu)^Tg?=l+N1(m>k}kwr&1z(@RTzsJ~01-NsowOYGB731%%L+F42p&u^; z);hV##hglBa}^-=cMZ~QDQ)p|%K0DO-Ji%*nX$HC3{q|V+EL^r%5&5*u>~Ta8t0C| zP~pN=d#-<a^M76;l+gi6xh072)r$6_lbm)^h=?gz;hxnh_6{XR&9$G2NJ-guHkTq7 z3l_MY%RC;iYnJWaf!sv&xD9WuoaJ897;T%Y9H(8uO))|)Sw;G+nM_&NZC?oQ2Ae{Z zuXAmAA5hH=L(*Q!B(Mx}a?(4&Nn>`ZH?Cddki8o!6Ojq?$}Gk8?x^urDAKE@Y4PE^ zPBMk12yvn^JESwx&Cw9DEVb{C<j?=n9T2yp0rSN^dhGIqJI@jhV{lIXyGfHnIhj^C zLO+M#GLlCAo)sIy#EY)?)rGcSN#2!eEmGoXjR~HySd?^hEEwB-ll_T!I)d(OL(DVy zQm$RYh&t?GB2Bie8dYKK*~>ob_;YrJ!);oq+D=$S#2tB)Ou)WCK1EpHzO>Y64XnY> zFVY;4mTHMlAB>3{%5@Oi8!Xa`0DC9M^54n8;3+r^O3n5FGz`Gl^w}5}jifExNYe-< zJXU7(4LX<aQ9Z7g5#f^pn7n@~*8I1fIT6#Q*Bx3SR2bS4m8aP&ip^$g^W3{TV(j)U zBJBmX#M17pmkh)+V<6OB{*t|;{HsfsFZI<ZSy5P=$LTki&2F#JE#W9`Z9z8Nln4xq zHD^pRb5_s7rSi(Jm`^m`I|?i!)8d$RHW$k-@14buYUP|B@f}8PBGJ5;d2B`QrNd_X zO<$ssFmsrH!28?H0sVHof`N~da<J~OIvkwOu;*wIG{;MMGLAj&vYE)?l=OFJih}oM zx?0TIOFHU&*rzhM{zSTzt{r(4A1jHtN*C$N#d%P<KllL_;*MrHMJ7Z{OB0f&J?epO z7p*?Wj){(q{aLi`QRw$Zx5g>XjMymA`m`c<zKNRHtTxY3FY_(iFSZG8J4D3s?lL0d zCHJ=ps2EFU;O0T$@cr)srj@>K@vW*Obk&uUM(|r0d0QBx5}I~hd1?1_JZn8ShFk3S z+h4S?IO6QAM41kK)5L#$w8}WW5I6b~$(gZ(ewTxLfzS%)Sbn{e5udHpqkfypVZ00@ zT&IAfyC*%$uc*S$nH^2&jNIq&4`dniZo8?y@|@mziG{j;r)I3ZBlhj)!94*N1e-Qb zEiFH9$*w@ne5)zq)=E9AO2d|HsCNV0MQv(<ZQT9Kt{}3!scE6@<?1<pltdmLImbur z40URbdY1-2un~!Uwd4M)MOeM2J8Fq(Ee>WHSR`r_`0gOR1UAneTkI+RgP0gGKkuXt zucZA-{;6i3scS?R?lz_5EzxB8t^m6uHa5DgX7FGve17N`<>;r*fDUs1O$L~KJ$*ja zt&?gWfPqnaUOwlO@;K6CKlN*%*A{n`qptLsVB?!mkr7iUcTQfhVyK(AHAeWb%=4|I z(mXS=ZX=R%e=ih56`rZ*HMv)*ANzDys6nD?qG*TV+O@W#UYZ`j4CUUWQ7v_bS(b#| zO|q2FE|VjrXXNQLIbY7&?$iH_m;39?M~KFkYLynFt|F_RLg|tl^)s~9w$quOTf@?d zw;S9ZKCBX2pJZnl^xElH<ab+Fp0dBr4gw0}DdL9kk{%AX&GMPOyywrKN1vrWa0?mT zbvF9N;rgkw9Fur^fG*LVYn^;|a*WfB&*0!#Y5HjWCH@;p0#20NqTvf6d7jP=Gw-8i zTZ+uBJ-=cF>@NE`QL}hQ1We)r<&Y_dVQl|MH`>}h3koaZrs1k~(bs&p?yc<|)hd!` zS`s$TWiduClfW}tUqnSnPDxBh*P>V^@ikjlz$P#uEGA;&!Q@CkSGDI#!A+H3pC;+- zcz33e?=#%CWks}$mMr>SDa1zeSpKLv(R+cRS6RWve`sEah)|32l`(qo{CLm<&Er(V z_weLc0#>$F;(^hAZ&&LG{DwtGQ0x2O-tUB|F1z?^mJ38hea{7|MX2TF!{IMr-@ZVU z!%lN-R$DtdBRdx0IRz{nELy-sm+6AOXjQA6Z^%gn1$fT~Gk)|I>qK@{dfgK<JD*0K zcqmb`WM7*=<?O8a&1d~_t%URUZvQE0Pb@AP)$H}&ToFsR-?a=Uow}GA+IgG3l|qlg z)v5XOI8GdXslslmDDV1lf<-`;^<7|qYhjA#o8nM%8sxnq7nMOF_jK4&g{8!D;KRT4 z09XOQX%(j24kbOdZ6^%&F@12P80iv+?uxdrb@EcX5Jwh^M2<=*UIyil9&<;gQ3o-z z4ZMu{hZiqKVEJ!9T|TVcNbkV?X@cVfxSXUYG~;gaC*yun<NkRWXcQW;Qt^_BOT@g< zX~Gl_OTfz*-Tg2k8wdqPLUX5$dM|XGY_-0iOdjTiaBHx-@k=z&%UHnsUhX7n`D=RM zPY473j%HOm2<PP$1DTK|t()fReHFrJEG|eUPAN0(gxg0-DBw8xFhRhheiB~GOkKMs zYpm9N`?mc-n2$+ZrL`y6>Vbo`vOJ5FmHHsSU)uj`g~9?oCtcwx6dR9>w$xhxM)j~} ziQ67U%t8t&)tbpdYTsA3*yu}@Bm4X<U$3G?o<8x19S(I#ff}CF)&FBS;sE&sRZe0! zB!%+XY09MSZ8Tjs&z&!~>UAepb^BbO%7@?~<7fL^535;GAim{R*B!4l^ZYh3Juj17 zg$<16vs6Gx(He?j^Ima1=HzNX>FQKy7&yLBl}m8;>Dx`Y!6o6zPZ3DtgH&A%1lpzr z#}=0@bfx08hmtG%lKh${RX4-}mE#)!>hAv;&?AQtc&I)YeXWaV^#Q!XRWfmkYw#5m z8OP&Sner)W!z*9Dn63D`Nd_shv$E#uG`Q`-GjpI6;clDH!eqs>*_^elQTW}9>G!*D zSG+QTKY8g3U0%k~-1-Dpt2jVt2u@9QOT>K5sGm(8le-Kn|A@LD6xxuQV9nu!D(jB{ zZa^A92VMtjELBk+<HT1<LgPj7nntWzn3?AXOEJ1%SVt`V;%SEIU}6w>&3iF|CSoYw zKRF_12WU5;ctk!{9CY?boc#INQJ4_ho)(v|0$4hu&8gO&u2~T*NSf&zT?w(-f6GC` z#U^rpnyp5=#&N;=E6McCS5TlG3#tL-<aLst+?FVsiO(3M5bV%*Pg`FcSK_jDSPSH} z!0W?%i;sUU#cotwW^obUqF=yTr>l%if}6rzA<E=z|A+2^cFWcGxa7x9z8pQh%Tq3f z9_QK)XWU3FpabX2K%B$_Cl&ZJ+Blj>_Q1Ma2YzAcD%5`ObRvmr=`)L1=yW0%vAMTi zDA*8z*Nyo5_wS6o=B5t2nh&g>Dq1FelyB~f2|MU9A=#S;g_yeu#N~)K_g}Knk_Upg zc~`0Fgngxf>Yd@D;WD><S+Ljf8Z{!d{kQb@x2K2RMg@)dL+x>}&aKZ5YhPy5rWMHC z4dm6!;dJGC$7T7;lGsXvw=D7Xp8EsU(fCc$GIJcv%}kb(YH&XBJS%{7{7a;4W&zB7 zG+u_nO*t<dEcROlZFpqD26<Qce->Wdbyiv?Q9j)N^b`guFf#L}ITZdd$1OxOG-ZY< zHtxUEWdHok6?fQ$z29B<^r=}EjooV#=#1aK^%H#eYpX{;bsFd*nYeTaIRl^&?KkGt zG$4YHKjEO)9V5W`096C1cLq)E><3Bc>Z_h-agc$fa9WAX^z;vh%&8XS88&_-D0CXN z&JE@j0;O1&tdvsFtm%RcC=ik=X0p=QK@9X&+UVn+KVOQtOhQuKU$DM3u6<0K7L||Y zYp^@eQCh(ptSgd|lW*AB*;VLziB@xO%yvrvzO%pq>}sgpAzNuZp6M{tSutK)D>OKQ z05qwUHe7mpt*20C2UH|mgB8QzE>QZ6W=|{f8{)<i(W{}9(nKo87Z;yYCdNEm3wpO- zpgIF~h^-p1mY078=+Zj4ZRvPU&UG>&*LR3w7_y36N>cJU%pqqD(5T}U1N&^`{Pvkv zyquhXrxl*gM}ky&1ws$v+)?gap7GwWWyG12gF^>&Gr!qAbUeAr#YPLE)-cXUX8ORm zC9&U|!~wa~(*5K-pm-<>^*@h6cI>!DPj4=D++EPPy}9IlwWH!gokwZ+wpu|~M3u`X zoq5HXrSYVYf#Sz|6c5w%=iqxnE}L)6d(Nanc$rNLV^+rM)Ds!UKLDm@s-T<$@IbP? znG{EkWqZX%B95D;Y<r)|4!Pzq6QA%;C1w@fw<Ov=^Y`0(k2Z|Mw&?O2U7pLu-iQDN zabH14p3tQ9-gD?8Wz)G1PAP~0{jAycHQV3adX0hki%RWGvBZ4zi%JFC{bM_t17b}z z`L<!8mC~par>zZGPmU5$H|53LWK>MP4kjzA#-7vH8f-cFnz|3o!lhGin!Lgf?^|0i zp-J*q?kTs_SXeZ96ej{zl$9+()Q}Cmz4L_pR!*li_Cs^qW*TDoqQRIX40SXiw_mfx za0VZ{pCG8L0r1})dke)S-d^8wkp>9l)YmxHLEg>tUIYqY+u91ip?;pWE-LW`2NwA7 zn)$HnR4G^2SL>S*Zdp~11A5eDNymX#L$d+9gZ<&V4|*D4eSDz~e2kro&<Xstq5&** zw_ciNSs3(%8h?!)c2-Ii(yWGskfIk?BU1V7K88%Qj8-iaK_o*q5}h4>!0OD)x#e;c z=dxAySgcz;T=occeVFW!OD?jBj(&i!0|QkOTkA5&9yz!3++$NqN$CwQAp6WdVyi(T zH<tjDTZW`t8mDl~6CVKtU-*zX3m^f(=If+AU|syKtj}4O=$WOPc1RJhKS2`CorBAO zW`)L?KRdrXc1YjePdiijsSYBtGeA$hEdowWexqoR5+FF@;9?_|!|1*I0tcV4Ck$x` zTLSQ-LAYIIZ^HWkfzdyDswZ(_b4hM%WOwC0MiD^~fw#A-&*i>#Q6vG3x>nS}-PCn( zWgll_?|TO!d9uDTMuxjYk)MM#cGR3yhR>wA$sMb~4!jKVzPHpxfEmT!H>1g^SeWL` zgt#EBsXtVY7nw+RdjlR6u&xAh>+bdt-Hx1`##?~RB@o(3N=dx`5#cQ8G*7?X7*Mfb z;#-OxZGKm^a<qRjoqX&qq9(yjo%5c>hvHF$#i^A2UO>y<?QW03&FZRVWX(-VFztU> zd=pWegqY`ki!|tvoi~9b-d6E30jjzCO$?n7p$F~r^}-(*gg07O=8Gn6=t%=*Z7SYp z>+ui-@9_`>>@uQe>+|#2CmNhfbJhJKXu^CclxGtpE5PIp<^21FCF>XjV%XsTk|J(P z;4YBqushCle63D$V@ovTd@7KT{1mV(s>Wb7Qv_{aii0%ku|E=%!#PH(XGsW0h*DlF z{Kc#P2PBWw_;MO~=Ikw-;B2r{#?$us2+AEl^3-W7jSop+A>jl_L-MhseisA&9rhlg zBpNf|g!=Hl8QtaJ!VJRZ2U$Pp<UiFoBW38U-}>Cdx4*HQ|9b97{-N7B_!S`>|L9l2 z3}T`RHyEGA|9=PloY_FQi=9N)H=Kr-f!IBr;c?tMF>X-tppI$qyXyQ9_?WTyy+5Bu zxK{}n>BKzQ`J>{lKjbzmcE~&GGX5_a&2)U6e^vif_|Z)h{;m63QJ}x`M!V}Nf-Rc& zX@5Xqnga-O!ucD54q@@&wB6qh8Sx;!2rol1C}l_7bZM^p%~_Y{p%)zfkVg7tjML(l zeg^;AE&ecq5PODIi;*#Gq94|^zisE$6kA6ozPCxR7R_5#TY3Lr^)aR_#em3^+Np*> zy<#199rjxNpKgs^=e--;eMpzr-rW`@0RMQl<0dX<)*sVyyovrV;FzxHcfwyeEw$YJ zqg}_KI`4d63<$(#RR@JQ0`W5X6*PZq*GQ?DE}6I`#D<=xOj!ToI{r!FC#pVb<m?*W z2EqLUb(Y`R<YVQ)z!LC3R2oxspX7qvxC}4-Q{5xxaS7--evv6qiJ8x^e*_AuiTuu> zGxF<j5Kzy5IOj-t@});lr@4gIZNt0ag*V@*=q|JplD`x%9;p$_X45qq%E7gt*yiT< z+6}6JQHD|<{v8zK0NuSrD)^7r{5^&zUw(*SzlHkp(r}G(nOqaMOyQa^?HEOOq4F%4 zm~g5gyiv=w4XtwPG|fu(i1;DCt?92Rkx5YuoYOaBtEyaw8(iXyOm%s6afJ5SbRnZf z<6qhe>>!o!&I7-xl$)^Xol#QQUi$jICF3M`$#?wL>WC?yd){&i9-g{28{sm&Hf!dM z+6Ui}C>i$=hO~`NIV5VRiZ6lR=0$W&Y+I34QagWejAkXA864tNvJgceFIKgr(c4$n zf-HmPBTtbDdza|QHC%%u;jF?Qn^Cu!Ri@lc0yCF(J`Q)Zvl^|okG^$?vyNF<f9OzB zO25}8Rc$|4i%^+r_9ev^-T&#YNJK_rTr@!;;>1(yDc0*OwHAl$clmbnE0<w)cH)$^ z!zJ#6T@;ng8Z|XyLVwLeV$goCyni!@SHjG81`x944a7rF|BR3S78m#)mzoJWG7yNM zT!=1`(WY5>DHSE2sv`*#F?=@f>{TLDq$8y9rlUz|udPV`VEa7S<o>~gNTJ&l*r?Zd zvPy>0?43MJf2ZUUDM^iURoxDLv|Di8*)o?$$Qk?Hpl28hDI_maYT8Q9Qoj$jIoKc| zN*mGyJYPFo!&)1)!lN8)R245iwJ;3Jy^~W@opb8VCM+_8uBEMSc#UBY%iUV<AQ0|% z!(;ij%wpfLO-gC{j?Ip33jbND&b;s$#dIWTnsr3*jAD>Z*yx#O?;UrB%s>gmNH*HC z&bYnR_>X4&R}fa{tOtHM1q?4^|1Bn`|F(^3t{0lLNV6WMAv2Gg@{{&v1VL)a+M7)V z#A*P!-~yI^kq6=o^Gz!cjEoWHj(53!>DFf-xC>Fujg_-#PhW}>E|u<rJ<u1+u{o84 z?cdBm{{<BMnRj)K#2q}84up~`8NAWBK*6n;^=}rua@?S#&7^H0#Ad4>iRHhv=Lym2 zs$wztZqQ@%?G_oo^K1OOiVgNM&QHc?H5j7<D#!`RUm0I#i$>e#yz{P{{_6VLn;f8$ z4`uN8J;week&w$EvQ>DMfQ!S48Hde(RE{R`foB_<lOBRIwz;fAWEe<yF!{`7a=YA- z!YHRPaFnMos{K`2o8GK|sr~7TmDa8*va-P`{jV_?-<sT8n^$plt#U*}+#oS=b+CGq z^}T~znS)0JW`elEa^n^m14OpgyfmGr3vbSQTrazRqxmz_Jx$Hr=u4z>4~k(t@oLWm zC*&#b-@pBXva>!<Qd08&BkVn(n%cUqVNpa>swhZDX$mSzFH!^nl`2&_O7BRO8k(Sh zfFNC(^cv}%ps4gBRZ0lG1qi(*A>WSex%YYB_x}fD+=00ea?aU%?X~8bbFQrWU+-%d z_h~OE#iYeA!q$hQiNb>uXK%IDTa6EAOlB$_7Ik%r@`cLD$tuuLZRBq^OxOZr#5Z(k zB)_Q5aSE^T#z)Un(wW^}yLc$=vR0dJ45bFrXme?xijI_`*k)x+yqzqgq@_Y=h#vr; zi2{(<w0m&+u2F|R)x>h+d|GJ<rK;rcw5Av;iaKn|uG=dQ3q6>>jA;*}`=~_Yb^%eP zs9wRs^Uc@&`OY--mb$7@MwQdU+>3XYXC_{DW-C;$Z{ZP*t*_<Pdlc6NbZmY2qE;$1 zN1p8-fohGNS9B~D(H~ua6c!XzzRU4S00)dAA57FL2sy9CCfckf<=0zF+TFf!W1=DL z88_4fIGlOzzHLYr;qbEV9{JpSrNwMvO`%J``%hPat9)_9R%i6OAwI;up#bd>){Sd_ zjlcfyxa%k7eD=jD>2jrAjtdn|{don!H@(1)Krt^|cX0G@h&i5DddE0Vzl5N}#1+4| ze?iPW7I?85*e9L%23iDdrXN@FErO>f?+EyPl)lT+W;YZA+hMY2#CetL=Ol?Yj2Jz6 zk}y%VVZRJ_@H5Ki<BVt<sh4c5?D1-}jDq){@wE-O<_v;m-Hs4Ja8K5~<a!AY1mS5z zp+YW2t#(U~jZzB$dFjo}TwuF<b%H3EN%W%%M|Xlrn23L9&#tg1#x(J^Yn*y^idP`F zKmzXuc82f{NX8BM8oWchlU6P&!}qfp#9gL_pAGFwcznA{D`DPHo}99n?AOoZZY%Jq zQv1~2$R#?B{7<o(w9<tu+teGkY?@aK((c-vq8MpBwxGhS84N!~A$-;j^<0r%um<zZ zL6Uc(nv#-kW|i2&c&J_N|K8}})V`G}w77E0VQ3F`j&Be<>(+K)mpn`#(rI2iGHy~r zzF4=_IO*aA@H<7nWdp&$JoQAPb12M#Pjc*_&Z0i7$4P0Mo2I8796zf$O9_{$o}kM+ znspJ!N{Ir9CvReZx5a8THD4BaW8(Md25g>U-qFS?E$wa(^+e8p9*v1b9+6$PEjcvX z^G4S0H%17s?371N^4)6_;@%(G^V+AMGdavCT}7E3HjN5x92FHb<Gj&(t@Y&No3lsL z*coCC>|KyE6zkfz^_~addBi{X4gX0x{`)&i4gyX9E;qiWEZ5m9=(QPNHznSMD+jB} zXPR`;+%&olkMZpAa%g0GdHS?ds{#3s@L&`STXu2ItK7nRV|r$uTr&Vf6H_DP#?-tS zoz!XyS?+o{K|oI#Ur;qskHKAtF2CLfDhAKJxjUoiLqQ8|vGAjQG(^9)h~OQa@Q6b* z1y$6_+s_#9!>N?`H44;Yj<DHNkuS<kn8bEc#3$`JPUS1pRR~#Q*kbPSb&wbw2cNyV zYyNc=G(Rj=^OQwRYl#{~M+Mh9tZ^a)8FjklQP%01O!dQs1AE;Z<MTt@dzHtFB#9+Z zzW!SZ8D|YwO-^F@pJ<jpXoBsTsjIxT-%F-Tx}tGj95V<lYVmTw4Gouq_E~mT)&s$Y zO&XY}4^7+z`Xgwxw3O7jtXHfayM&<@qKw>CR&D~EHp)3SqCM4M{63Zju^shZ(x;#B zq3@hF)I>*I#?lETQD(e+s+u?GIG;DBpxluAdpcr=n<m=<_en6OI2Al!FZD9Jk7aEx ze2PBtC6mWi)Y^->&r1x7XFIaDIog~foA`AM3(o|N4ei15%NINQ-kQw!9YbZIW)`}K z2Ry4DgJ#_qJa;=zQ!R-!@IDzjn=`xyL;_5?OksxSlPJMlNF%r{-<-9eAtGO}^Sp&} zM||3$-cj5vKHH`!@Qb-Jc-;uw^j&8U4t_23(@&uMPv%Qunf{aM(K2pSiKBZG1pn{F zgU_QyYovDl`iw;y@LB9jd={%NPuaKnPPA)p|4dU=i>1v}77>MYRrl6b867SE?Z>&U z&_?6nS3LR8nHl5IshTQdcJv5L*u})@IoJL=fd`LsD|wK&rzerc<+SF2F(r<HOAH*3 zxTQVedyj22f8vz_j#tZ&Ywp1cBU0VF5It7$Xs_2qY1S0WRZ6Hw9Tg(H))V(fVHWZ4 zg|`}SBGE<~F5zdd?RH;St{*~=oqaaQj+k{BPLptZvgdP{XkQgW#eV<*c`iP=;1Kx) z#T?0Me}AKd8yPgcsZMUt_h+amM9n{I(4Sb@vDyi`Zf?1CLFBmQQ^!#OtWW9-zU$o# zhr$jt?Db(<KJig;0w0~`e!PyhKgl$p*lY%{$L=i8HR~jV<C<=*$wD`U=T~!~wE;+! zxXSAk(KDu(Too~V?I8Z;Cq9T|z{rs$H6vDHsQfT5&w4gm1{m2S(ScHnUaII`|9;8Q zYf?3FCr6k_D%92)Jg{5dO=~i>-qW_tN4pTO?>xf7mlgDAarhS7n_mUQg_F{O+s_>S zRoO^U-`#$%`J>cafbPHYjbH!de*khWS6-3RV(z>_M{%q&mVrxRJIa7Wa5Q3cn;=x+ zOb{8J;3IQjf?^`~1r5Qd$K>A`HXYTsD6`GcQ9$^ql5Pzet5`S%m%5+4n?jP2RJ<AJ zZdls#)~0WIvjx%a4~2b9lyZG>5Qy-;F5l!H?B;GwC5G{mIVHW18np{}q|SXll2=Dg zT@kJ+)30q`u=#cMw8GlQ>7?47GgNI)e4GN93xmU<P4eSLZlHKvc}=9_TW1!iuK?|+ zTG3}tv)6Ipwbd$iFS*XSQSIr|%}7z_g*Jzg)KKO2mfkC++sXfwCJMrZS+?2Mx=pV( z)3bzj>bjZlu<}GX0hy&;o8{Q?7yckguemt|;$b6h0A(>)sVJ2<Zt;5WpSu7Mv4L8p zf9j$RI$74PiO1#d-uc~}z*kqzj06r+mQ_|=;g^g=tiFBydu)Hd(oC;4Ii3aa;IkB; zAFoA#M7~jc7v#EhHMB1p!|8<DP5s{6h5S%fsh?}qu*XVT{&;}yNN#^zZtlqy63)Xb zQ;cV_r(wBGo2MxI3mqUL&NF$Y`i-W+c9C~JxvGrUxn4dD`f|%@zoYejNF>f4VGowG zEa(NeBI1ELxvw5X^W`ND7XD#QnI-0vRme@@d*L91<V%!qyjQ3z|2vIJJn@Zx6p$dw zb5=Z2L%A^8rl#sIU%o7txUGy@_<iBzI3kfTtc0!KwmH5q0Gn331h6)EwXee)I|no< zE~@X<XR~K>AZ_zW+lHD<EzHJ>pvrqsav_DOnAq+D&-KBxG553}Odg!wqJ^|7DN@F$ zF|NoY&{3^I9{Yo%zG~#IsrS#5Q!r|~vDz^DbAY#c*uE!fOdRj7=fF3R%1dkF3iz~z z0a}GCMon91y>1>DscVkgot7A(>g4i^bZ&eu?kLVbkN$+miS}s-GA7-x;Qn3ik^*i` z^wr<uiOQ_BlA~`Pf!xOO|Jpsjp0Ga|C%|^d+&lDSL+GQg3<72+(oYiig(Tl+vk3kb zRNR-r#DGA++)R1elafhHLcDALoY!9zt(nF=hO1;yhHLv&o+WM^APgeg1{Kba>~5%I zfK4@|X43p5qllm^hcZklH0+$x2FggWFBd|i4OCsE#Jfg%mG6>M*po+Kk1J<}3&B0Y zx}PT@E*>*<mdZ=;>oYna45HS!lsBzu3?L6>uvN9}7V%>frs6==1&_{Q{O83i^_?UP za$6cuS^S?UkAD<i%n)@hS^8(~)pB>}LpesMJjf3eU*Hu1a#~OVrQUUV&;&0pH>@;1 zDb1Ss!G|}f=@~CD%WbF76)<f8HScV2xC3a%FW!QiE}xR5W<17(G3wn9p@7z0nt!4f zm2s;7?MZ3IPS9M8$=J1`H$B{4UlZIeoXv3NWT_}YcDh3rT}@6&=XR&^$jC+PwlAy{ zC;F+%Cjk9c1r32iaNm@t4ERsO5Bh)MARkW_`qnGqDN@IQ|I%vx*}eWpKb1bTpPtP2 z2K>=vUGYTPB(5>)fcR(dtDFYCcHF%d)4c#O_Qh|mqh-_Gp0xpIiS6uYr8#@wTZE8F zLXLvGE4NKxO+9sR(-VxmH(1*GM)SE?cFu!i!o6q3O#lkm31acQu2pzuz`5=1`NnG8 z$_T<s3wwWNX4Y)6mHEL~_O|c%imv4YsXWdbU4-wC`?bnlTj-;GSPRQK>Y2SYE{M9m zb%2ds=Zs`3(8rSvW~nZ)a|nJqp6C@sl}FEf=rCEp6Ny#h!DB}DKc)X?U+(`ZDQ>b& zkrH<c7AL<_rEmK!-_lRWy*qa#El$>P+I>=Y3<(bCQNKomG}~xyx3>M}*~r&#V$r@k z>drS`pC$HbAqrl3&gsHCdY9R|s|0s|t!C%uYiZbfU@VnqUO-x;4c%_by9@wLo@9~? z18?4-^|<e|xotuD<zJGoxIm=DAAc|9keTnbmFum9Vy+n1S0M@S*pF8z^YZm~Nleb3 zW#Da+2D8hqn{X<}VjAuq8W$4@c}9wXmhQUFZk7&pnx87p_f0Z?_q!DPu^O&4dqbZ2 zd9&F06i+%n=G!l7EaTLQ_rp}GAAK8AP|}6Xs*QM$21Y7;5J7=89B+qi8fm2wtLpe# z&dD8V^KIfQFNm;s7L>?BcXjYPXcX;sTw8hu>)c9g*w*MUxO6dz0SCQnfMp}-UwLM} zdD4JjJOvA6TDcdfXgG5t7zID2rHvldbMC&A?bJ@<eHO6|*IS(MV$zm4R!F-tyKPgf zjU)$Gwc^cVbeIJ~XVTGbQk|NGxT}XK7Lb-MMGqE6GL<zr1X!O}bhMM{5SdRL!EB$V z(HH%U&Z|*9)mG>$Auv}5vi9SO|9y@HK0;Zl`QMJ|mc=b+$=r^MT@K0q3(88hUUCHG z!Aa%dzTjJcf8nza-YWA`gP=cv!ptypv~JGNA3vS99~Xl1`KJkS#iy*s*IgCkRu2t5 z$9BCI3Z$h@>w|Yj%Wd@LoBRnfVv|oA4houLejV)KSe88Kj(3{#LPc%TP0=zk8t7<) z1k7j63uB?o6pK>yNGUieJ%TtlSzgF~n?)lFl;2K^K9Xf(mft!%X?h;SuRp}|ud1UC zaUj#3cU)=eFk5`-pu_0Yl$FKRnMpKZoo;E8W5&(&_Dhd}-6cBXS}*j%Hw_v!omMAw z2y$Mxj!4=wcXS)|?l>^yp}lgIt>J!M`PhrSDE9rLvXO^6R*6};24((V=rjuCl9l{6 zyWbZeyRI+M59;WuSqB&^tZ|T%NchaWDo?hIS}i(MSS?%ER*kYarR6Gn6su6yLX@nv zeyjKncmPF^rPmh4Z_OVM*%sh$u$0W{_aqrT$E39oHkQHf&;HMG0DYDOksiD$pzG^{ z-iy7io|AB8^RClWGePZ(%@0}G*`rf^azfI7CGk@97xMsZ%a-P<V$PQp^#pl7xjR=> z>-~sKBk(!Ga|ScMm!e);W&<w6ua+EFfF&zXL%!;WwNPP<_W>n*Ea^b~7&xM=h&MX| zstrK|_o%HcTZU`1OJu~{fGj;1aa$5Qv^Ua}$|DM~j>=P3Rz(zZS-(#a_fXvPT_;sD zsG509^<Z>fORKYh<y+E8>B_R>z~I;Bc`ipfYWg>56o)(e`_*Xgm`fKEWaw6bDjR^o zIvfht?gmBg-E2oK1Fvvv2W>ptA&j}{+M%fDgjyV*FFNb#>Q&HPrVf11MovZkXvv=( zDd9+W8(vdb6>*Hv=KJWoJx>-ByZ64F%UHs8P#CaRia;Tj>7=LhsYXUF{4L17N4wH` zB0LRN95=JszxHr}Dd#Eslb#RHoj-@E(llBEiFTKPOSOKzr!2sTpiKi?u__0q#6v~p z87iQ(O%BiDwmm(xCJl{^2GIIr!qD@DO<mvnB&aCHRIl&-FR1;m^CM3?T^nk+)qIM) z4G3^Bd+<iD>IU?Bz0+YCnONIj4}O=scpM(Ea8@S(^Gpv<;X;WzWwz2)&{8vT2st|| z#<e;LCE30e1P6hVs%4f@&LFi2MVg#R_r{W$+5Aj0>5zqUp)V~p^({JKUE_dz0xh=c zDxOXJ&B8)rorKq_Q;ZLu$C$VMc&#oQXe#*x?WA6=$_$Q1XM9<Y!lu!_YV{`2lb_4? z030?k*tMnK9=ZP`@t=y50>EHbl7NCQ7HKr{V=hU+W^O=5((K@X&h^P3V%(ZRGd)F; zU7eN0CmK@otrW3FXw#$)^(J7R#Wil<jwC`_iTBLG8VeY!yiRXy!qyo;XSIa<l4D88 zigQWiH+|+Xl*fa*-3M`4FNt_6d9vZDa0e4_M)4BF3d&BTf##1FfS$!DrBhsC@95+h z6mU@cRu;Q;N)9onFHwks`RVOFJ#j$9l`E*dAQMdTRzF4p117$fka6<kz2o^PkH+k4 z!w{ad!j9<|lU$%<_BuL=+~39?0BS~dR@RrQ!u&osh`UG&bUx^_uk9@lv|=RkqDW?o zux>Uh@cL>-4uxKa(ud-x;lXkXmDjIceO#>0%WEq(U)wN#{P=N|L1PI#2`^?VEiJ8r z2Y9M|5}Lo`iX2v&=@yFF!PJe~kCYg&-?{U>Dmg6;@y?*2yaZ^N6o99XZT&Ci##p15 z`}jIAm4{p7#cgcYuebl^ZgdF|?1~rRKi3M7-I!a|i-?HWyJ~;P%#;O9{d!Nt^zcQi zCV~Q3U?m*S=ol)(ju!1RjM-~x#o4R{zkJ!=mmy=|pe-g02rc95cvTz{D#-6N_h8)K z+&rgn%^Fe({YBYum-oUHvG1j>&$*{Lqb9@W09k{zL}kb`EfW^9yQnE&>pp@O!|J{B za!)&-@!AMTr@o|DYP2?b-Mp>Tpqp0ch%%J41~y4^i597EY5B|tYn-jE3oy$nkh1+Y zfpudoCzN5b;y}q_t=E9(uoitU+$P3qY?pDZR&0M=$qPI9_obVM69(l;NJP-Oggc5u z$YEhH@q2!iqDoyV;0bwQALA36zKN4bR{I&8zE-?zT5U(L;yZWltbfhncSq!bHr52C z<sL}}xW}=8Ez|xzQ{I5IOTXboDWlv{+gsKWBJ;`Tt&Dg&4;pEnKGWc_ZOalWT2Fgh z*rJDl{zM_*UXb9Ga#gul*KL^^tAU(R<U{bIC*X7KF|vV<c>YJ3Qo`+f&z?QA1)?ao zH;iJ#L|g^s$^HhwQR#xulgyv=jg2=%>v)2QOFwxQzn?PbxldU(`w{S3_OT#~BzeHP z+gsxX-a6ITM$g>>z3xy6209?LTSI|)3i>wMr{04J1-A8g_ore5!0Sj69TGKac=5f` zZMQ`R3r|NO0Bxln$lsiMQ^ZE9y!KXgfc&kQzHzbdI90^?8~*23#ZV0czm;*j2@|`2 z=^J?Jju)yC{2J!qx%PM?h*+|3b{XnLTr0fX86z~9rwYrH_t*Dvxv&Qs)&6ChIQ?&p zUVMgAzt;sI@fh>ZDgc@e<w&meI^5@W(x#bLtvgD@CE8_8=+T9`-M4K^3A$fhmg<3D zKh^ChleCPXIuj~~jTfI+5t+3MVV*+U6KJ)pgd6HWVkc-s(IwFPk?uIVjozXu4vk9S z0@}cW$I@vAMQ}{53!T1ma~}1|KrYl0;v8sM{q0+T{%E~BGrB>!w=`KDYS_5e=P$C? znVU47@l^f!)6|Iuw}&N0!dGb*$png`dJS&^Lm?D=_b}G*y!~W7P+%_33C(uK)wz$P zi5wNlK)C_82yxba8IVncV97>1^FY;E!j4E18*8lRGi`w0yAA!>o(=M#^TkaWMgKBs z6(SUMUp9|<H@_%=aj+4J2onVCQafVM-yw1@0wT>JhbPuV!a{-V6=FvrenjW2>n|M* zamI6yuJjjO17r;HH~|U`urT=YeKR%q%m97F{BYnmS41&F)T-WI;>F+zZqnfAfHX3b zC*uZQ0W=bl=p&Y%HMd=v>)nb4fHd@^c+`QvrW3qEUy(#92i`DUk5Air98gSjvMQj~ zYi{*)8pR_OfL?N<-cbFB$*mr&sO8>AfEm*4(LI<CqX!~Ch}JSn8oUx-jo0#t@LNuH zyX|(K^^D90`ylWF7G8J{4&D*b;Lz?SP@X|&lZGdIfynV??vVHSJR~*+a@gy%J4oP8 zCs4f9%iS0<l*VX(+}+$*FP59|U(#9fZV{_KLRx;0*Y`PHN-xb>WIwcedJnWw7DbNs z6V11;al1NbFjK`_jlFpeGhoetoQY5HiYQ}|qv~jMQ_dklauhP@>SWI6C^2^|p5b}$ ztbdBG8rh*bR$N)eB6vUf*xCs0t(6x-!$@TZ%{Lx@6E4sjSjRTXw2+A0U#(_-I6NN= z*^F3Rzo*ae=Eo2SZ?Lm)@8j-9FIHt;3fWqB@{@&>^;Lt7^}hc4Y5qub{Nnl>VgKEW zYnHw66hBpGoBUI>L@Cz+d2v@?)%HJ3fJCWs3t~G9LzjQiKPfEVNdft3;y;8gnJXXu zxc<~|-e#F6`wq#LJ?*p{!oxPJQSgqMrq2DuT0=IIxM(L7g0=bnR>PZ<@$`&444p#T z%|1dTbznd1YI*W;>Fembw$VHEV)9)d?>ikKm7ETjQv0|vFa9q4`0IZ*=_^dGO3PAR zIV}Xt0nT5eS-4JKmuft2hMVf4GJNrJ>v*AV8ho8S-oJ{XpA=%8_EFb=auFDg7{LNN zD6Fk5{!2E$LHQ3Gm9_G?_}_U%pYG|S@YI_!IX7e87cS1v#nWfk_+1%KI?_+9xzE=x z%UO#%Mw{*|eRr#P546hd!KC!_N4xYKEgtIC4}6{*M4SCsMYH#ql89H?6q~*mC;r1L zt&qkiGR~IDIe)5^{U6P07Nq~-kofPh0gTS|XfO&b50HeL{}2}9GjigeN|U&&Ec}Fb zLw^;*Q)N>6i>8U*2q-&FE|}ry0IFsm46(3gBJdGrq@rR^5oI4Ea&q1VY|p5!^zIfr zn>P=auA0PSzRb~Xw>6RCxXjp18|~!m3w}L+7B>GCAg<K9K*>n2%GQ-hJCyJTWdPmo zOsVHjH+tOx4J1+<%&Yz<KsZ7OrVjxU4~tBm%?$sqtWj#|WQ#eRvoTmDCR1$k9lf@4 zh#@6ArDZk{QmC7@h0i-3*RMGpPeo{XzGI;N^{^SWe*(5*{{d`U8&jA5INVe~e&vEj zFApVD4LX7E4-Of?^w(?SUzwmEWx{qF?urmJw|&|UI^!=@e<{HQ1<h(`&}f1mpxwsh zMSoj|>Rs9(3NlY_5n3KxQ$>C1X8kaGvg4&khfE32;B30S<02_fgR}?C8SV(aEQxGO zPJyq$na>~m(6FPD^CzWAY5zla`4=6c!omLw6PC?vlxJ?{Q;O4O-})Y0$8#j(bg-fk zM|%rL`v?jnbp2M_S|7E`?}-V+9QY*=QCQWbTxo{~t?hr-w12x9mJm?xs^(h!|I}q` zTIsFL&v{q&nA=~&>Djh1<ZD%FE+qaEnHC*>56`o^Zf{3>tzBjLv*<$!3(f=)d0A3^ zFC~!ud^brX>_1bn(#6kRq@gB)-vj_WN9MmS@IP=&g@Aw7Co0pq2~8)=w)_0iOB1`b z4WJIAyV2)0$4@J4_L^a$PR^;nSD=PL?2iS}Hs1TA(#);Ep%Jui+$DK~1aTgjf9zSL z(z6Lc4cfF1xwL?ma=i%(88wSAxBrRn{Y0h(0QbruGbe+Z8gQy#s+{Ql1&FGK{0tte z(|2(Ee?2Uorf#Jp&sG`ik+0vKeyM#8<xoNozLKXPJX=lEJ_D4KLSJ9X;+2cL$$qLb z-~LuXKd+Zx3r~k?pI(DLI$9q5rX`CXQuq_8AOfEunIM)+FB?ddi?=EqRe#;zC-`OC zGHvQ+y;O$3P`o0D?r&4me~%OY%sq$y_ifKgsD*V)CM6z9SN&t|Xz2sM&Z;Ko&y)g@ z;4gL0J}Un8hxlLmLBQ&e<7Cyvc7kVV&WXbo@&*kXP1c7%4p(+d!y|=enMTx25Iob< zl)Q@(^$ZWhpn2Eg;p0LIFC%u51}4WNB#@pjyCG*r0=Mm__9-RF4?L!@9;*6JEGMZy z8IThOX<^x>|24jrg`&*QoOs9zaHX$|+`ISZZb;zoMsHk_UOfJ8AiW)bYjqFK2?H#- zOZig|Xs9&J1719LmkptAq?D<~yP_96iGHU%7h@$KdDH^I*vG`*qLG8M;Fo+*xE-(4 z_?56C-*)m}xFAImkSB1ED!;9S>0H!3{qB!7jrk=@Zi0*!`G`1B)r&Z<+%NZ3^*~<& zYXqCB=lL@N^zEI-@CAt`rqW5^eP@V|(b{GJo;LE4j2jQ>EgFx1plJKvdwbp@I@wvq zyh{(z+c$wV)2;TNIwnV-Ag9TCm$jz|Ti^+moF(B9T)nW*EZ`fuvp#MF1_ZEy-rKvk zCY%as$EW6YHd&q4ckT?(bXN>xTBj?hC~0q!ODvEQ!vQ_arl5P#)S}jtp=a~9`#xs& zO?9%QL(Sya{;n1{$aYYqC#4%<Zil~EZhsKs|IbsIzfnq$nAo6Z$DTHgchv1T3~yo& zz~|tEJ7_^*J{C7CPo{7*RFRHc9xbdUVHS{k6dGoVimwm6g@`-%;_lPQUgf0ebncGM z<c#6ELO>0)XRi9Scl)C8VQBI4`z~ct12+(+)>VEr+L^sqQEXRvr6tCXQuQ01zMH2H zD!!|i7-a*z1e*&%A!$dk#THm3&jVn*5i__f$L>r6w>v3yru{pL{K1Nl(b)7Dnv~&c zD_L%byHPz(=nas7eKtL1U{e;r`%7;Je1Y-3-tqryw}9h`kV*DelyJP4ev>fhmW~@V zoHp&5Koj<e`_Q|!iq4Gb$?hwX%ltn~2|M1#r{b6e*0-6D4<URq@-N%>^U-GLVRD;^ z+>+R8xNnGJib(7F78<t)8LG5Np2!iTymR&f<t6S5)RW;pc~MSO8hUCj>KzJU$)za; z1pu2WoBF*yij~qmr7S<T(oov18x$x#W$J#>HuxCdj8LhzjS8>m+-hc(jLBQNJbL&Y z9qo1~9djE5M5LkDr*OkQ&<=E{pS`tgb5aU)Q?<x^>*dY6Vd_LAOdRs$KAidJCd&)S z#$2yog_qGvgT9O08=s#SU2q;mYb_&96fZ%>?uF7*|Jo*fxA9clDBySiyaX=O{5ENr zD!?z4H?LkrZBlV60EB_zkvPkIrD;6o`>e`Cs023aBa2ZgZXX!`fJGojUC(>ts@jLy zdI4t^OJg+q=}|S^1e%(R*CP}ZV_(uWetBp~QQ?)_%I3Moi(siBoWLLaOf^VUP=}h% zW#hP|3<e=H*81KwSoc_Z_*&sX6kw@5G?(jUJ;mm-a^v~t*$d{k-rFo|+mLJxioSn{ z9JE3P3uS*<R8XReQq$F4S=AC5@f$3GdnUkCw%E|YWFbt#%S)%<K7RY5Wzssz$|by7 zQQ56(ZgHN7x~5VtkW0*U{n=im0DAs`n^yNOavj(@y}Q>UH)^a3#8HA4{v@<_<jB@= zx`6djSo!84=>F^1O1D$3)E<9#As99#E{=Uq`ZLJhBmNzUMl|s$2oXV7$?sG2zc_z= zf~P0W5x?PW5pY}Y*WB9PW<xC~y^e^eVm79xrv4G0O1Cpwspo!OP=~eLVd7)L!*=mg zs=I-MGcyI@(aMc=v;Fh?5BIZD)l4O#eD=GwxjU&7PJB<Uog`NWJPI;8UX82WA^D3v z@3#s^t{1fmIBuw-36_>)TC=N2GT?RlQ5zeM<1~>TTSdFpQ!9t%x!FMTli%YoS+8RB zaAj2C+}8Fyn1*d`c<CZ74KvT?Ld){|ReCiI4YtjNz$HL%)q^E{{r2sl^s^5W{Bce5 z7&>C}PYlkAxu%=^Hd=jS<axq83LT*~ZUv1Rj$r+e_JlS5-gfFgv9+X~1PI*=>R)Dh z_-@RSXMkNiHc(8Zd%4o~D{J(R+CuMxb(@7rN%^=*94lZo*4vJ51ke32?kYfXNo17Y zc;X5Bl8m-OJUsPq-?ypQ3DPKJxI4$=<DS2{>))8xxyh{7EeMs@_vWmfEjfc(;o-g+ zgY|OjSf-NHSkFbPB#Nj6zhUBOCCh%~1eE}GmTG47@jUNidEC8Cn=a>m%F%3<benOZ z6|(Z2_dWXX1@p#bYiXZjX`8!z4r0imoW{-Cn$uQov{r!zVnf!<Zz{a4<$B5throR0 zCRC=Q_fDd$Yn{_(!|Kr-mTrGrykuo<xQc<2b}i=04!M$+gWX!`B+j$>RHW);wZ4$P zQ1NEncOG>@FMYxwI^I9$`9C`BKl2F0N&C}5lY)>8#gsdGWxRZIu{3G%r^DhOb<A0D z2=i+XW2X0cpY~s@HEHKXPa_o|$7$49W?Vc@`~(C&tYluZe<~Kqsg!uDny0QD*rw9Z zYD3!)hQ8nJL0=iFU&qqUahY$z^y^^$gefz8>yxL*^VK|qVH^=D)75>6gO9W#;-5<f z$e;0~N?M${@A?unKyV+AAeF8Lb&kqMJ4lC+?CO__)&b&1>(S!%?xV6;s0C`W?Aefs zAX;Vq(e}*TCTp2hOTF9BtR%LU)LseembUt%N;B2_D87G)Foj9-DA3l|A*JZW!DHIn zmaiBjUPwudmbF(WMH$L}<4DSw!Aeq)=U+{ep5{n!TWK(h9<=bgI0t{XMsK(`x`HeF zP;~#CGcQJ4aPtB|h7!pm*Wc++1~?ijs<^t{wYz9stjD=#@D#AXE3-ZVV>l$}VvV5} z0i@n;I(OqELE=M><}DzyCp$Pf1UMPlxb%z<_mZsQd&iAC41JEqNiua3N9Jbt5mm-V zve2)c%mrVZ0|d&;dAkwhbdIYj{GAKg+ZL+<<_8VvPpwMBz1Q?mU!8M77l_S^2P(Gu z%O`K)rsZ)O@3tG@Dk!@gxTM@N5@t<NWU=|sZdfqzHW2M_+G9+X%5>}(S$S@v=GI6T zLiB8kSh8`yuNyq#COy0OTD1G39xgIPxSJ?yef044dodq<IhBN~R5@MzpP=LOtL+6a zi1+zy#T2suInHrSz)&b#-<3q$+Yj1+p7llMmp`|qJwAo9IhDVB{MN5;tm4VPY#sj- z!q()Pi~ahhGvzwr`^EgDV&~^naC<ykeXJ@E5XYyw&*W!sp>SKcj+yLEGusZf<b3Cr zF`*&{nUm6-%84A?G&hU4y|WCOt~oUBY1c=BRPAEkZJoQ~PoKJ#c9DEP2*DkTC6B60 zVtn?}j?3Gb=<KRBH4?CQC$aC#1WpHuj#u2`{Gl<@coZ+0m_ZF4?S|}=<Jd}{PiAlm zB<GteRL<%o7JI>dTsKOb`d*LnQVouzV+FQaajp(nCEr~YoUMdLgkXz-VeE*Z=e^XU zL#X1nO#3oyCiy%XwKwibzn#`i=E=QLd1qJHOyHGUdq-NLNyME`^k#c48<E3JQ?@0~ z4c{eSnFsS2I6wlc5a;<Ts984r%&jFzqXxqDdo<9%zN2Wg9pa@K+;`OJwZFk*)F=_B zSKaIPsYP+hjQsGpd4!{;Y=Y36to&X=2-zy%{=Ql4h<wnY^pC(u+(hbD!D`0Urwa}N zwA<7Q?`jt(_rK;=2ixq!j+uNx2ch;Sxy*g!@p()&a%*Zj)9lF3{XtXv9PX1C^aplV zlC)%yrL)L}c5CyDO*b7h7kCt*ZC}jtsOf9u@hCMf+@Puv8J(1e;>PL9zt!WiXr0Zz zB%F;lZ<?ywHt8N)PB|iF;F6J#sQi>JdZVEnX1|s?r~rtYY!=E4e5_=q538Bl1y3!F z#<X_5BJ5G!K3>$8zmagqz4S}c<MU*+1KYU*uLM5AbYbpWJg9Ns-8I0_06K!*qbFJf zn531XInNo-8SJ2&Z|cQl3+qp*JRpCSoIm(?{`xoajg^9#IvGd)GrhQD$)u@fr^9Ll zR&r|xdK}2c=2BLM+cv^UCU2IhAdbs=N%RiB_AnC6C_DbN4-h+E+RV+;PV?npRc*S9 zL-iio7kIDHQ`9Yx<KW(X$rj`5l8ztYi^bYT(G}eBS=wN%RVJse^$6f}Y7N7%LR4R9 zIeRfHX<WzxV6K7e=3_q5Yw%GuM={NAUvi_HX|xtgMJx35!wr4UYJ=)SpP08hOO)TA zE~jBzA%ckyTIPe>1$Va2L*7~OS#-W1r+MTy?0Sei4~c?c7ID@Rh^<#-G`NPddDtNc zKfwo%C%Z#5O6mFS&jx>7ud+$gExdf!DS+g%gz`*RUsA15N&6;h|KU+~xf?DO8{D?k zUJmFHhP);3<7e}Y6ztt{NDU1tW^&6c#^n-u9r8&dih-2>3kki^Sa{1fDhgfXb@Sgz z-_2Y2!tyWuZTrWGd}<WknZ6FfoiHPACT#8f`*LkPfSOAy%_Dd~%<Q|O?S|Ne!dTeZ z1t4#&78=-j9hV#ruk6wThoi}kx?NNTpn81yrKyN2AesQ8IHh`*Nx~Pt#v{v+KLU(S zRKrnjM|c(;Nx{7g(FW{ZH<D{ao7(mi55Lz+sC-(f&f^if^S=*2X-d$;Kbpi@$sN|k zfizY&iT6LT=%3&9<q(2&O7kM;<!!J=csM?8G28W-K3lw1c3eDS+P;Oh^;<$CC*uY< zBhOqGbd_4F<>NF1p~+56^b5K`9`eix(s1Z=IE~hzm3&@yqhWx)=5Wu3r#-fF@+nO` zyjofgjJ?}0zRoh}{^ID`_Ptyp&y|=ZzFg%tFI1CRrwGo9`f9gS)__dpUHgz4mjE5~ z9<%YVxE1foOYO-ySWe$P1u9Dq89;9-Bd8RV8hnSwqK+N(no4m))D%g5waGVX_Mh{0 znU|RAi*h!eA&YmUs;Qe_lT^_6{&1rPb@n=6OCzF9QK%Ksx?XLEHL^=%RgCLXi`jJk z9-2!B-ooLebl5q%@mSqX@Q}Gl>3@R`&k1mVWp^z%k(eG|zn=dA<C3v#`2%lMQbXJM z?Bl0RzKtU#ggD2|r+e$-I4rslSJ=NM*%dAL4!yrJh&$5#xFhj!9o|be@5?9DS~ZOk z!t?W-;7FUSimbLOKC^|pe5RNe$uGAz+|gZi1xH-t+oOoHIfJ1t2_B405`Zd0A^_3i zEiv5VHqw*S_4Elkd^R2!37CnH+pYTLLdfTvK9Y`rx8^9Wf%TojPWeb)q*+1rPw5jA z3z+_;bo@k@q}0#i<$J)R&IuT$DQU9r{>GW4sEBK5DUQO7pn{2PvvVzPP#y4n=)1r( zO>fV}<N!i2DvGvzF-fY0_;3IP;gpp<k{W?nulV($)b~60ikZjcE?l6RU$($~Z<?%{ zKPBR_A-2gFI=P=<UymhA6m^b(SLgWkskcUUoS~o0zx$$?lb5%H@SWkiu*Kfd-SWkr zc*@My2l2PyU3}g?uM7mh)lGBQEJ+p}^DNZ5y|l%>C?{1`g2PnQI8<WX5#E|w#|LNW zZxqN(2z}Mild_CoZ|8L%jhtV3sqVCtY@nu*;J}+CH<ocDBqH%ySzymwsJl0dHcx7t zKS!7)F2iIFse*`qhQ3#~-v+AMP`>+&zcxgL10GNsP~a?Amx;%NC}>a-e>XNXVBr$g zm3xzH8*Hd@$4LJibfSnL4#E6`d-BQ%x3+eZB`|F|t$-3D$nfFr9NhUV)RG5ULu8%n zq{PKsk4IB!d7H9oYMf+Ls2oK<kw`*iU}$9#p_0xL6$yT$hHdiuyYO6r2Dbp+A;5tu zo(^{;0iQ=|{34!(Baiss3vw!AOQ|JHZdy#Qq5e&vu1-#uEBGDt{c|WB`B-+or`=_J zDDIU=lHO|isy!leR0`!WbtPHcxozXEc})xN;XdTh?hV|!kRW47EK}wiPaTq}UQ`m5 zW15t~<l28_1sEf%0$afmYzyRwNASV0Cn@6<RKsS$-JPWvC(P2*<m{cgcEX*NuwG=z zn^e3nSg6G<PpN26@{e}!;x2YBGw*el26~xwy{qMQ=`uZZneB{@Pc}f_q4>u>lbVXd zpZ_6W+YdBwzt4VL0cf7Y@2^iAHF^5nsFqCo_|b8>zc_@nhynPe`4o?Q1_G-`z;4WA z9z9=tF2C!?;n;Z&swN`KBZm<)P8OH_y>6#v3~llHuts?rIKV%p(_PdRIILL2ql8}i z-T^MlNo+vuc2V3{B|*ObmcebAUYLVhVXZh~OMnqZKhD}kvrqu;zRb7k$=Tf`NAH`i zjZ@H*`Vf7HKg^*4^(Ml{=OT@@7HVpe86bBCA`Sn7?Fs2_0+;^Gv-!WUe1CIF@IYLa z#rHd}@$s8He(Ha}^-IS#KK+YHle9r+C(3L)EOeZQ9QeG!x`?q13OR!057u7Gm%H{H zj-KM8?<zP?3J8;pe3Cdc1aNve6N<!Z%f978BXK*6zB1ozYID=Dm27NoRfeJsm~Gsw zk=?K=DV-)Xrqwq}iGD=W;cC`=9U$d=UP=&NP8#ea^q=I|d~Gm4>pt8z_4qokH^yZY zi!0xs{O)S7-?&Il!^lGHM|l2+7tb??3t{))USKbv(W|oUjJv?uGuIue#ZexOiw+-2 z7EtiuiLnr?dRJ;zoA)^70{h#m`O>eivWGT?Hu^0Q5?^2}hoHA6BAooa$o;~4hcBp% zVx9Ly+-E0H*!6wG3kJ{lpvJE*MWl*UAw*w^lHQ|Oi84M<KrD6QB!!=p4Ix8_YU}9r zvEo?@x&-o&zF|w}CV`Xt5=TL%re?r=Pck~Dl{|!fTg2^nTByITkAfP?Nj>RZ7@i`m z8Nu{}8?k!C|17CFCW56RYw3gc&Lpzw+NFY$#{I`$n2v4OJM-C`QLW1Y_R%+Vz<`Jl z*l{k-n%k<nb(&&qE#8j^|0y02pNx>PoWk_ps4$_KwXzFi;lFZAJjL$>A<K#1pYsHv z=LMiLY|?q8ycS#BD*=y5lGXeDe|?htk^}l`&r`Q=>5V*@yxH9i()?Z=djy4=iXpGP zAP;uhT~LCLThFOWZC&|AS527pfm17|)8`wVB^h-qq53di_&QS3W*B~Rz`W<q!ik%1 zqkG)3mjso+#w*tW@C*~<acA}IUR*dczv(M)lsx3=c6_XU94Qa>dku;tO1^2I)k_(- z9L}<n3qe<0auysMd0jr?b?is5)spLL-bw00?6XuK+4<_R$?9QfnVQFOtx-FIZbVhZ zDO7xabT86$geGkiTXGzeXk-~m?DBpiPS^>fkyv>kDB?a7WkBuQzxp*=$nlk}7xMH& zK4pD{5ed_Yd)#wfrKhwQk^q4X)tFb3eUsPs=&oK{(DJtI>EhNs$n~LEGc}hU?vmZ% zDO`0^j&#xrc4UB=D!K7<`+3uy27xk%fpRrXZw|qiYUlpk>N{5`Rw-75#a+FD>x$VJ z<@-q#1)aA9KVz#;n98TAD`ie=ST${TgGtCXJ8Syjp+&Uo0t{EmxprY|905@Xy_H(I zc&fGaNIvzp<a&o-XBmt&z9)?=Y{Usfcgvteo1)v>Yw-}2^94pe-?AIJO}d!QEU(&L zy#{>_#T2WK3!^pDkxrEbpD~TmZ0F-VUS<zzbIrj=j!mC6E^8O*IA|GMzIA4Qy{@#P zaUV07=)A_eRcDLwg5pNwC);V(sv!1FMNga~u~!gHP>Fbxkfu9kD~R(3%skigmkG!+ zV6Sv)ORbsbzbuHhP)3x#FuWacS>EnV=FwPPo@6{l5S=OEubuIC@s+ZDT*<eveNHTG z@-VjS_nI?coyuXUaQxxuv9rlMPGkgg)AR*6hRk~1{=GzDvn26qTr$+O6O9_fy6#ID z$IChtx_iMU+4UqR<`nuRg;abo1GsMY)m4UxdU=V7oe1&OJQfWqu^FXYhI-c<L|ykR z2s8F2b{J{Jd_PoQV+}3l``kw|8@Cc9b#JL)92GU{O!MQKf#7j5DNm;nW)%`&<ZGC* z$_?>0y!C#Br=Xsx-<c?slU=+h{oOka)+6sHPG?BUo_8y~aXOx(97TVn_PT5G=^U+? zN7Z2jN4s#MVT_ECkM#_B*h2kwz2W{yvo!Qwd|NC1?)kM1Rrl;4!ps7}@?Vxn0{Sld zXI=gsJQ#?D$ud%Ij@6rbJKC#6pBo{jAm(($nMmG|wD0&h)3sIhEZKM}{fe6xm>uVV zj$LfwOXM3c?~PA%rWvg{s?#+}>ZTWZup9fxtEe*r;el}Pe&3iT-sHX9kd<xr;JEpu z7v}38`FvNqNmD}vXhDA{Yv?Mu5EZ?GrTmeal6Lvdf&{1TH`@L0r@y__XsJx1drXiQ zSIm9e^1v2Mq=X#l)gxjd3peUyIh2yk!Ypkq-;qKNT(v{bchGyZAEdJ}{0xF82*=o` z40xO1ruWWL1QAV7;UnYU=MRG9$XP|$+L;eH7$&l?-taQ}@84pfpgMUvGpGGaHhEjM z7~<`gv%HC#xy{Qwg*n7AR&sQy)BNEVNlBbhnd5SI*-o71-ozPH6MM)WsuJC^wN)IO zy$Bm^RIjo-WVzNJ@Kxc>E2k#xg`wB4<S33leG{7RBS6uV1cs2ro`G%qAmR(To{78f z6;T#=o-Xna=z|^B;{Z)beO_<Y@MsSS71j)~6X<qlhJ3z7?!fw{xuW0uwVus;<q$IZ zD-o(%vpL-f>^AFlBOK$0qi%<ofyj9=4rtE6297Rd$efkyjjXD6@nBJdR+yT$8aDdS zQ_1xO9V?NNkbMRhm=k4bpBGzKIG@rznc>Yv%qTHyF0%9eyqcy{iHyes!rxKaPmFNP zjo5XpFNrVXl{N((1InG)P7)?0>*xz(y>v+RM#Wr9Qm1Z(?E2-qgS9noh1?XYn1fT- zxgWpH<70Zyb~D;KC`H|Tg3W|Ou~Iohxog2)FyU@|SH?)YQp!$zv93+Z-I%+c%31lP z56Qf@vl&Klu{<d`?t~fgd#F-L44sH)gC&y^1hZi{T90;4Mj%qD6NO#lx{|5~G1%!# zZzdnGw34sIL*4Dkxe%|<ccCf<>Rr`@vAg!?iD)|P+{KMqRnmm7ib;+#sFlP>IQc?+ zrZ+!<sL}AY`8tmaeJYG~&7shD%d6E&Z-m*mN7xkOaaQ=QboGXR<OO0znx5yF)(~xq zl|h=iH~J8bfWA8>-yUdc#=eVh-G3Q7pkX2Axno&t?VIsow`z%Jz~CI1J6NG`!>X~= zbNa{IoXE>}K0m1j>}Z|D!d)_YK}9+`x*Ll*IXMyh0vuN7&Yzb>s`tEkvxzqIwloPz znE%Lc**NA<{dwwBU@^9Fe3y)lzwHJ=lr37%$Oxt7ZZ_5Izc6BKRb|LHV*irB{A?NE zYw9`J731P<HcKRVcvQ;D%JiZ5p@m0@MpMf*PI19u?naf~2K~y&VNa+55)*&rdzB`- zpa@=~4K0qaDz5&9q5EDO1qK*;lG`*Mc`Rx5c|Ayr)pc-n4dVGcGf3fMzGM+NzMGT8 z!yN)903e4BBlPa2b>qh!?&4=K7F)0J28*^9<mKHq>hOeoM{3Q3`GMtF*ZRrL+17E- z%3gb-HRzsZYelj@aPF@dPpT(p64DD*@qEctYESCC|Agv|K9lo)oBlJOxLU8G*K)Ta zYrRZwl}dC@h!5Xqji<3%Pfdw&ph8AZW?UJ+;hKjlOjcA6YsFx7S=r-5T`G6;0&ZF4 zjXXiH)*jw!@Ah==MvQf2uLL6_j~OS1j&pKzJN*LgIUV+YC4(Uck=ZEd%vTX{F{8Uy zzSU|LET)Jh2cL$?OLR4^L+l^cGyU(0AicgnX(;E@HKpZuNiZ!`S65Hb(0VrxLtibO zYxJ7Ze1(v3-ajZMy^&b!#q#E0jn6%jdAvC~vHnc|D}=9mH=Hp<m9Mg5yTr0Oe<6fh z)aZOyyy@w4n<ldDjcI-n@vFYIdtUx@+by{{mm!=`NcmDfl9Y6JA;d_)JUkdzI!`Q3 zzIv!e=3CzBYm{ozWKy)!yic{_l#~GpOVm75(mIWPfTdTL{Wx+Jd#&hvyyTEKVz+re zwTxX~*tuW3-$D;jzF&$W6#EtR`~`{mFITVk9U6JHK0YfwQmn@bJgMRUT~Z!H;gD%( z5UN}6R*WBjK*lIKI2|8sP_QAl0LF4EJbzwVJff5_14b9FDTvN9gvI+YnmzBlS^XsA zt&^L2O|{GN1$Cpw`PG~#yXns_-R@qu5PE5VADHl25xn@(2AKTsDV)c@$3QqOUy-G% zNd0w8qNr=ZQs;GGwDdU*%)`_rNZHeth1;LLM4!(hi@bUL`tw5F?>uDRA{cE%kid~G z-A8kH4DsQ^)#`%S44IIQo#g?$Mj9}lv)J%b;H1ldQe3$&Q{AN<rX)#7(;BMQd0iwF zKlC#0mVq@=+<75f4_;|R0ew>X*=ZQRdS6yo!`+hCQ0+0tG$+BnVP}_JZ`fAWLxB%7 zV~_&%_POKIE5853Gm>$ehh|oRqBR9@I(vt`b}YT&>C*KdeX*X8za4cs@$FRB)=$bG z(dMOLyWq1z{R#E0k#@%IUGP&@UmT1UV?<#Ci$q|lJRfGBP)y=)_a);6OT}Vg{p#WA zmwV?Ml2#s_z4&s)rv%mDqMYOGKzfFp=^f&D&wT;)(4eY*c0?A^rKzfF47t6y5P8*= ztXP`UCvt8<>;|^~#jEFJL!NN&kZkSyP`w7EQfE)hTt!hjLAqq<;QY11*$tB}YSxp* zFs<v|Z>A*&hmnOSS*T(0dOuAI7ZIblXAtI~RgxNe?>!NAdb$wW#05OI$j}YQ5-<x6 z9vEyeK^AvI=NZhXua*o2pAFXe*R`e9dSQML#Z6Sn<b^JEu@z~aDWIOEDA<mla`6r6 zo7&D;77kYNn&nR2+@|J<KdWvL%1XR;e)juwVp4CSrQ=Q*DB$*y%$arw)R&fAo(axy z=Wimj4cNnnKE+9>%CuV>zM+pO3LwNwVZuW2!ktr(A@>S-bSftwz&O71ZxXL4=kx?{ zJ##KM^x4dlRs~C`C+leDG3@S$C)|ZtGb3hr(XH|5q!F}m7@1=!1wXwj+Z4eqgzWC^ zt(MkUb6}JAdodxSn^Pye0xhU86gu6rqMLs!(0-&)S`ZuX5b3J^q!@NtOXw_Ey7C&X ztE$z?)ySbHE}P<!Zkn;9u&0cA;Ac%9p`L;lco=t?`Yp1Dkqhw=M_qlrMAb-5|14i3 zn?`%z(R?E=WPA$vVTC6v3<^ls0Rk;a`oWh5NZ0K%=ioZf&>yj-d!yaWV2;UH7aTs_ z1-|L|*EbU}3+oXq8eZ$Kq+Lsj1FD&^OY%>pcI1t5dk#2`>}<xg%w;r#YyIH9h<($* zch6mvv`^ZsdE>e6=l7KcJ;kqEML&Bl9TTm-Cbq5EqJtizP-tb_o-EHVRi;Dg8tKSF zp-s$d9b#gaHWn5a+az(@lH<$|L&!WCQT1+X=R8rMrJ(v~Q)Hph%ey8xu0S%P&z-Ts zot&L&e70$C(g5Lu6m#^+ZSmUw5s}^8e>64$2anm5q6V6_OxbnG=OHLa5ogP1AFBDE zAUG+WER`o?*=uiStD6hyoL?X(zST8R5_@Sq@!V<c9?G9(%Z?nrezdWR<cEYmq8QWt z^giSt+1tOro#OE4FV9M&Dnl>og{UspWHf0VndP)0uyDldV^t2dgEZVW;*HcgQV`}C z`7Y9#V(B&EAfpSsu5#;#Nu;c8s_5+TiZ-l_`<bj_^TR)-g8t8{Nn=@9q7w2xI@RjJ z>D+y>psF-P@>OAor3XVxEAlq=_$I&i;1gQGXBD6gCJibj_j5veFIoeunQwbf7m~eG zHokkC=`wi_yf0Y-vhpU;tYS?TXn6Zq@BLRk`uB?951J`Gz)@Q_(2ou19XzpVA)g%o zcI+zdhzGZ-$0fm!P~d4~bAVy(H9EhA#C^I$=y}e6aM09a<~4I02-2*AV5stgV>ASZ zasu9iuQuNY{eJIeMsQ2>X6G)h4TFb+KaO12E7ElTQIAisPYDMR_232{Hx-)@W~>{s zS_l69VE;~^(>N#|?$AEm$iJ1QoCn=Ti7nEtl!(<|SfMEB{I<Ag-ig3r5eiSAX7CtH z^*}JKkhY$bddeUo>8G%f3#uV~^(aG5#9>b+CED>|<LTj79$$YM%lo2#K5UIV%a1FA z&#jRMg6!g*{DQ)bh%5XD-j<>f)3dJM0#7jvUWt0%ENsl+$h^U@bhFnVU+blf6HCLB z5$aTb3g4d}xv!PD$a95ud=?Kew0bJLU-<<<7@mwcP(PhaC;c)l<AaL)oDMS!(Qe$} z9t_0gD%+dToC0Zn2oPONxQ-Z5#`j4;jvJmUU|`t|4FzifuU|^>TuCpXIDPtX3kn`h zbW9A@u=OP=)Q?h`q@<+JGr|Oa@@FrA#`N_JFB$Mk2v>7HJk9uzV$}aW8z@pF>Qb)| zzlos9|NY7S+-aAD69Ii>nOCp;ydE}U|0=?Cu3?Sb-kwm2ap|l8Rnkk@@d45KVoN|% zRCIgrwea(qpu77M&Q8Y|_KACp*KDZ*{OW>~-wPZ1ZpAQnf)Q1=Y1TG2^OYlpF})IK z#Yc}GUHFjZa`IPc)R%mAiXxPiF~UW-f0tTbbgX?lbn#Lzcj_teSrI=OONUhKfZLzl z@pIL%OcVd7h@vWSa3TKnPU(bWRDK0|xxto<l6E=QjsZJaOCH#%nwpz;OkH`||6}HD z#}@DPn@!=D`Oh^q-m!9e6#61ux~w*T6oas&xlghdvaKt*<Lw}!_1F>s6ucjeqbm4^ z+UGwgsHv;70tpbZ8~$PKcWM)K1+3bFdiSvJiTCT3y4y%P<1aqXiMidjMErLlEDaFS zb&;=TrU=bYjOB)qr2IRY%se#Ss2MHWYPT>QcUDo@Y;W8w3N8EnI7%pqFZzorG>a*~ z>e)}MDO#L~cz<=42F{ubJCf??Wn5FFY~)h04wdg$6Q6lJWWo?|WXdmC`M|&@#^*X? z2$!g)si$WWAhc-_(U8T2hu^5R4~mKpyVa5HoSR)zQlf6QSEcwehUUTayO*HF5T<6} z*;Fl%wkQ27L|jNZL6*^@;`C#tA!M7W$LsEy7i1d@QzFgI>8!m34AN9by{AOa{KeGp z4+aF!NQ`$m!^D;s8scCpX&3x!7Yh?=5GTPUjZVs@i|d?NsTdz6qZ3o$03AUF^xRMo zb&zNH$qI;DXjU$Vm_a16>g$t>8b&tnkgiJ4i*s3DT3A?g2AFXJr@K39x-QDrHU(=5 zYeMO&x894~^m4bawohcdUMQ6N_}<czhm9#B$0EDnK+$3SoZau6&Bj2`dZnVAQQ=`P zN$1K#wR@xl|0&8@D9;X#49mf9GDv62$r1f)ot-$z$cYDRip26lrO*C;?2~fo)nQVn zJoZoD%L~1CW=AE~)Iexr-f`D6b1(r{&z;4_*?Dw^S#Ui)bFTa*s^hf@;*pO@W>#)? zTh{fM`5ZHw`MFY)t>q901s|FH$^lXd5Ixcr8GpZh52fWeAoi6DwDMCzf%)ZR%%ClA zK}#G}z$~^${fpPxX3jEJ#@WEl48j`88p*37f?M8Ght1m=Vrh|v|BtS>fQowkzJ~=- zMnFoD4h2M7N$HZ75Co)^ZX}0B5T(1MLFts18bp+#ySux)d7r_!`hEYu<+?8Pjx+Ol z;>6zjoFimp+X29yb{l`jKS%Iy^Z{%KMd*`cKhZJ4m6cByqE2!`!}usE6=w?6lx?Jj zoe~43)!AA^L`or(ZUn3b5u-bsy4K5!oZIK=Z3el=qDgdUH8Y7-)X>exL$EC+E}Qw> zlbg1MVkjw0-_q%RAvqBU!$b9{{E^rS?ivR>B`bq=7~X5qA?z@b|D4Y*8uCrM(pNjR z`}3g?Vdo!dX`#Q(&u>KJ7hdbb>>(lCInIQ9lPYS_vom6rits}m+2GuVwTa4?pEc-e zn7rkb%2m^uh_#h^It_>dN%##url#vR4&6dctKi~5?*7pd)f}eZGWRynvbpZ9cm41~ z{o&iH&K+;Jhs4$O4x+Wae$aH^KuPhnWnV)7Y4eCY%1DLPZQQn#<Rm*-Bu?SWdJ9m< zO@8v&Al<_!_WWMx8uz$_S(pg$1JL<79Wi$!76e_*H3v@%YPNhS*t*XBk_G;+=kXLw z#xALQ{N{cbgbiEdBf2naPk5TUy~)dH-Li`_9&th;qdc=ArBQ<z8L?4SHPs1&*mK9L zh{2Bv%C=f`Ez-kJN4g|};gsdM8q_@oZDE|+j*@=N(FEm;XWPxym9<kac85j^$3tg< z5K?l>=rNvJpBU`YPv*9)q@MDe#)|kq&jtlIBGj?1IL)e&Za4|H>j+QvyovnZxe>gF zyU9dIuGxX@B?kBJ{XZTD0PaIvM_~!!h9_Z-!tx6`)Rh!$^pv93MGEoj@68QmS|hjW zi>bMWC`B9AY~bhClzwKRF}JMszxkLQjgsI;_BF>hIkKcbl;$5(zt7sRAf;7%yQ%$Y zp>ZG5^ZI#a|Fq*+cS-w<qC3S}N>=YG?>xb5YNBN^n6@=G%8V|axL)OOCy0P1aK4&` zOSPM|I5qp@)BNWVAE*n5?>x>Vuw5N|Wj$h!PQ^zlBAfV_wsc@6*HT$mHyqEc?8B#k z05-x$CLbaQ3Ku<GGv-cz6nWvjy9Wv6J%9X*)mXfN@*48PDxXihAn|rm#B3<wKBenI zgh6BR$hXc=Nma?vipVcPd`sL`d<4M(;_(cQm{j{VSG+qDANpt9{67|LF$D#AOt93~ zRS2@I;Rj=YB(PK_wI*>0Hs>tQshNid5X93O`sX*nou#Px+9<K|iI@Uq82gcS+tWnS zzNL;Fs)jNou*+}TC{WkB#|9R_3Q11)`KYb;VpT|%H~Y+;Jv-~(eWIdqPsVfRVCQJ; z%SOv8$SD;KNZ{@95mKBz?lX~;l!@%!#i+2I_tMBQc8+lhrV(9#!6V*Z5J#u4uaert zmQz^Rb*}B0CT3!iuNp0{84JtLm+cPlUGDlcpx1GZ&LQ*i<d#0UkkLmC$>X14hpz-h zF9@GlvBtPvvG|f=fqFtiHh17AU(@;R2KOL7$ahb{u%oHI=F%1Y%}yNHH2=vWAD8=l z>c$7s-<Ee;F1fCn2R-3p$!d|e%h)#&ez!NJ?#_P}8(~BQNp4_}agIc3xma70n5G#0 zGCg|8uBwn_?4=gZPIiCq#^h=?$EErmou@+jXN>Q}Ah8V0?hftKgggw=QEg0{Qa4(O z=LX8OZo_Xrzh!INbXbOx(nQ-pYTNx(J&D^RJvBYcvzPlFf`UO4Rw{9vKNQ+>2-9DU zP*pJ35cX$ID0Ngj5x<s_W0^p^(k~l9ucQ*VsQ4qffv^{%L7x*yvAb;7!`x%svo#-O zuq6>r!ly-ff`xY%Vt$iu;djjCLyYjHBX#tlB*WKl65cP*ubLP~cZRNov_!W{D(R;k zAt}G*g4=x$eHOVCaJ2zx=FQ2Qg*U_q7d*qkp7HYcy8Vi$6KIlkIwR%yDMu2W)v&$n z6DheaKPuS<<#@PzGw<0lSe&ZBD{KWxg$)=G88M2Zg@YPTc|O;R>{nstQ3(QW>)ETk z@?;?_9G;zK#ouQ;LuXQ}4?jMS(28nZbz?ExeKJvqCcFaTxo<HuW$Rmt2I|mG>4jl? z3I(A>^BjZ51k?L3gpAD1-?FiD`0sB>DFtd&rDLYfOVjmv5!GdZSTtn-5L~#Poyj?^ zNWTq^1vbPE5?IzAvD^BYA(0Ai7MlWF9a+(!HFO9)s(H?TL4V}LvZ3s?)bG?FxgKTL zRkZ}kyVJ-wl<a@b2MvUGd#6po+N8kDb$>~A_Wn=9E=b{o?2(`BYHWPiFR`aDv{<Px zLOlQx)*f;h_ZHW`D$^jUSf81qTI3QmaJS?KgjRdR{+Y?jsq{Dax4UdSG#wCOxKt>P zL;rJa9QdPMDb>OVE!ay^VPayU+0iCSgQgrRLg0X7^A}HPM;iLi3o!xWU87s{TuDjV z-2Bs0G>%s+Ofw5+@+57*a#1KyBoj@z@9yr4z*Q0KVIqb0-yRZM9Qm==G}Zk3@D~gu zE9tHA^u-Izp{x^Rk<9$#X6tBYBOWWLs0~O5NF7LtS9wuK-7qJyiQr9KqQBqam-k#m z^H7rG(=7%V2%6~jWoRcT4xvB^e453KF1#Sq7M-PQJv-S%M_Z%7>~DziuqA7t#hb^U znz4@(Yn*$OiMn<z2e-m?>LqPfX81wvm({CBo69&HW1)(D-9}F$Pl7ZHwNX!SM+c2< znhy1f^{TCq`sSwSrTtU2FOb59n8leAxLzd)GWJF!{O$$Ncehu!JA%;4$i$>_mt3A= zjygK^W9oyF*koP!-Q6JenLvt?28b|N9Gth=4@$C+V|z6eg)&vy#$B|#rXj*|;89UR zul_#x8k8UokDaI;Jd!LK=?!T8>>`O{XpnE`>xUch^^0VWUR{xr_0lNek;4wpjbZ7^ zYd>Sw#Um7=Ws~}O_v6HzJVz?b6uyhfZsFf=`ZLc!f;?_%Krl78k~wXE28~X19;cqd zk}@ry+j1CcsP^`{m}z?zPx|}p5sr_qO*iVDoWBn(HJ79^Gts?k72}7oD}hD&DuvJc z*qTi!uaI&-6yYD%WWbj;(O+8iV6k{n<^rpI3ggq(Z6Ox8r#>%3N?}a9jB;;#0jpiL zD~%6wm{G^jr=_Jt{`$THXO*@)A+JjSj@y0pYfs+cypNi|cFCrAPAVK8_<)UzU1?YU z;7Ac3kdrZy#rB15Ba{-fq{8v)AA7+v4iPQ@*D`Ma-dIg*$+4b6X3QqtFz|tT#{UHi zKdyZHZ)gBp1=-D8J|nh2zW*LPBh@elVmEIs9Oe)f+a_ehb?#v4H2s$AD_okwR^u`J z9%aMR5dEC>knZVwLq%3^CCL^csI~itI0Y#2TemCsy6^Hw_}gHRGb4G6FW%wbe1@Dd z^NmZkb!!R=S#!9+_TtVR{FQM0GGN#~lt|*(m1`9lX@|4pkNcl=T{_WD{2O`;v;Is? zc`50Y-KW}J_>W447M|dHnJl2rWe^L!fuRJGJSWKRx*6zk&$H2($7VkpCpLSqq$c@e zM|%3CEgdAUATJW#FvuSsKAMl+n<b(tYFTTZo~zEIMMt0SL@FG(Sv>T`OF0|ZZT6Xu zovUZsAO>s=+nUhg^z783gqXkflgu=qn!U#2G)^>BzNDH(#~&{5`ib?Ym-iT#{b~j1 zEdhmY*7_o+uxZwE^c<gCu6|#?XBF$OPsh!TE?Mfs`(<3SH*A0Tv(9kH;z^OwRnPyY z4ZE>ev_*HF@5|gl0)vOyuTOc2%z#DS`YG=di~`cuED;)?8&US~JELrL7;}!<7->n3 z-_>g=YiTzFRIHX{iW?daAAxx&CJB3t-JtzQTx@De$~cpirQT;T$V%bIPCdCW4JOm5 z85+O(Cs`hI`}3|SDK$sST|X0U30uTbfH%=q=1tt~O=2z*$rRup917jpgxDVM27C9G zur)O{a-^Fw8pP>G`*$?8vFdal1U}gHs$i#FnBmn>gD&p`GdJ9r@v{1M1~SK;4y0NU zBRCw#v-8PCj_3LBGxy6{{mNCSs5}H!<fFVTFK`iAmV&%cCYs0?Mw=LhnpJ7nV~b_$ zv;_1l<E3t$cs^oc9ws_Ft+yFzX%bRJI4-f-N~c2~F9=oSZ)(*)54Mvvf7H@oYQ(aZ zmxFHqpcLoEHkM~6N=1n6)k}|o8wN1J%yuP<r_apXM|fmG8ZTW?Opc6FDfO~BfQ!Ys zt~*(0fA#c95INV&`K5%U1ysDoKR)Z(Eu+kwn{gMVobFGsiP#NT{wH_&oqHvpK)fnF zl_Rv4VGX+<V^91jo-<2|sTJY811)v@X!2DuhpJuk^GsP<zH|Dn-*N&&ZBqDVxI58) z#WhsG8*mFOy&CC>?|vUhLU}8ry{Mjpm0sF<2USYMwWj)_A0QN(`t8o{Za?l|KIYp# zod$_Ih!d(1aROyqHrKzI`k(LVfpQnb3)P;&BXNKc*!iI$>bc8sKDH?BDes-m{nW7) zcggC;{7LZrnMKJ(P~z#eUu`Pv|Ldw^ddLIZ(?z_Tb}8nSB%gt=B{GVR%**wUb1YWF zPlqn{WVuS9&|Lov-rr`GTOt`&R`!<mtm!pmulOa_zh3(f=EoN$2*dGAClWFean0*} zt|U=2gv%|P%#0??BkLXE`{as{(LGI>W8_Oq?kjf1TOLYU`ugDt@5VD-n#Qn5)3n`f z>LJhya$fm6PViRm(bo6Fsq@QUT%AV?0NC)|$45Hz`165($LWhZD9F)1aJ-V8dL$vE zg`v=5D2^cZa8*8j-aEuU@PLVed#*%taOV@ck9}NhC}9+H6WZSK9dRdOcjQlSxu_p4 zD$-kN=tXut#9IX+M1KsML~=?#huQhSI4`bur_Tef8~>muf)xBH*MsBC(V6%wIQjA9 z=`JH9s@oo`@h(m6P4#$e*Hubi_S(i2ZC6?Az~dC0js-EKQX!Dtr^xl`jFHbc^TI4S z&ZY9BG#6t6Q<Z+%3x27u8`#TNUX#>w+XFmVA2+)`;#S!A@ZMFD59YRWp=4SqGfiBH zZ01p|;HD%2I2rU~wbc!%Ynht4hK%KaA`m=<gmr<+(t*0}JvfF141%FosQ~OFu2&Gj zBjJ3l8oJ2HNZFhDW`Lw+0vK8GURmb%zO;v+uTO>BS*1pWwP<YIRhsTC^RJ+hO<S1u zURkY0KaXA`wy~L6$sPwg`;YQ)q%<rt-WMhOq$=Qlfz~RYk}V4NOlrr_5Tlra;kqm4 zMn<o5)CeEGd_zS;Q>A4IW+|nnrw=>XX;k5DZ*NmEGPaG^+S%DzDAAvt97MOaJ{{2p zq)Ss%D43aqAgZ0EukV8~_q*<{vdh1iz2ni>#acCG-07uSXByV@dU|?;&Vy@CJv}{X zph#@LJUzW3Z++PCy&K2kmu^RI^}B$s+hu*hRi#?MA7|H|vZt(~Qq`w<qMB;MFsGrU zG^bXh)|L6}lbQQiJ`^-ZDAQUij(auR7`i{a%x=&65DN9S2Min?MDu8twFu$iia*NF zudi!pgWp~otgmA(tjAUv>g!7a(DDq!V4du&)IQ3`vI8%o!WllePyFzyMdeAs+&2%o zxlz~}Y0K>px~4(Sr3u4hes~*Ha!jefUh@LezN%KuQaP%B#Au~ox7>3d(Cu*>t0k6& z`OwqpJr<);hM}YGj<l5gy#xGeDAZ8z5%^-PVE)Tv*55xs2b`e@%&g|=Pt44E<7DS^ zGgs(ml=KcQ3PjPMA{?tFI>{~K^I59yualN2n+&@}l$?o@0-bOzkLR|N0;I0qJ6j8+ zikTpu*>CUAtv4ryRNlpuPq<ax#>XE49AcC6N?y5?sOUSub|H;z&$Z-T5d%c69#AjF z<jDc1swG9#|IUzA?p`IWErMjlGgx+=P2V&Gt$hH!3)na0_!w40wW&nFwqY9#ml!Jx zX2iF&w2V1vG^I+0=O#ZrQCC);SF4$U2jk<B7}ro!Q|I^b7_<@)^4RB|X?$@xDl*;6 zZZzJWZ7LfF-G(ZZEdhPd`r%Q%H4HRw1OM-h=Ef`1j)jgSC}4fVz!0X+{>wH!%WdpB z6Z)JJW?V^#Mm<hSM%5;BQd~xlS6twE$;@QngbW;vSk~I*s69hk)r5}eukg8@elgu! zjvhJ~gC^C$;6bD|d0Kgk9;jrW&-kooeRB53K|i050eeJMgdeJ^M5>By4*r%ZX1!;D zZrm=*WPJn;CQ{8{dHBztzMHI*t>JF9j`6rv+z~jIQ&6!H-M2h02r{e;Rd8=j6Nv30 z4*^OA$%Rh32R#Q8>;3fmq~dmaN-FMb8DI7`EWvUL^!GUJ!TFm9+o1Mp1zOr?Q%ik0 zefBpV@(n~j+&I6uzc-Mh-@#LBR7@~lRqjh;5ZXSfd2!!%$9uFI8aza8R!s5x@13DF z!BW!Fy2=*L*S5xM_=F36H`))@K{uz;4cajmFjWd-7oYHP<d{_W?5G+u<y^Ob5+D9r zKIQcSypEt)@AMw{uQWDid%vY4uBcL(>}YBra;x$1)#*bw?hD?9^QE(c;EB5>Gcy!) zLPpREV!H+A$h~WUeuJa7%e?|F&qI~~6Z=ik{Vad4a;Z0q>2<S;=x2-m5lzgL;~yTd zMwNDp%A1SPB6R1^+QDBZV5TgGe2gEv{WxbQ(64GVznq6uxYPnHisw3P5j&d+wRxzG z`J%E%U!oSH2h8!BOX8>8V|{@*rM2sT`?V_FA7~>-TQr?sw_aY&ck07YBBVFvyb7+C z5!8MihS2_9<)Me@udlc7f!{C<?Fg6iE2_AQmL?<+Ep|r760Fh&%Bn`ss<@#lkBqd0 zQm!efIyL3q>p7>x2K&mN-vFwg-^-}qhYPsMG`EwjJBWoBRmF2x<!HU@(UeUXQ?krW z;O61}s-tQ-HTw=ks%zFJ2hP@0ordNd9tZ2@)4r-yHU}S4z&_3Y^CvsVT#a*_j}C6u z#GI|&`gqKHFK(~c0*tqA*xln+CSnFJj#HDVXL)+)wgoBT1*07A<hdVub9@B)DJdrH zkTdO#i0$s_&F}HQy()#puLR2jJy$$dK#Up^sNO<L@UP1kB)|9Ytc={}{9;=me_f;| zJ?GMxbdvq@dg0fBkl>+?yzuIcRQ>tJ(~}Fp92d0IVQD|KNLfEOqS@zLuAp9SS*)%; zpZ6|aDhsu<yD8?SO}L9(wNM6wmgP=-eZ_LNPcAXmCpC1|aIu(>klgpnV4=`Vf+NU{ zkf^Q>0w*cAVF%eGcTV{Fcpuj%s9?ea(@iS*W#hw&qkbOhF-R!8%YK)$_4(6DDZ|Y< zH^l`$8U)E2fVyBYx#P7U#E^vg(>ZqF3uV^({VxO`4FQmqx-$P!fSL2UF)e?^K$=?C z^~>dOV)1a9L5npR6$1Sany>WX@l|Biz|7ym$Ed<B+$8c<q8fdf$D?Aczn2%nGRcCO z-ttSpb`r$(8v+RXHI<@|T(^GU&rSbmF(^%bF(BdS_-;n;G|ZWbol+gs;ymHG1~1a> z-yxV21&G=@jp9Cpt>evKo8O_jhV~j+chFnV43e6fDpebQZeUkR-`zV%5GPD~?bmL3 zVB%zQ=ER8;11wlNVE5<x{fS<51O-J?h_JmHuG|+zM3##`P;7k<SI>`pgfwCAD|7b` zMGpqm{DWL*sc8-*W#;sw)k8n@Ww9>4K{}gDI$uh965Wi60p|N`cU+t@gPAu$ibfP4 zm{u2VV?@`@7`7yD*_hFa^SeEhk|SRf_SL%#!@#FgB%+*RrUVr$+N#%r%p_?53=vzk zOPwabq`PfE@(_~%brkg{ZMC@eGx68};LKKhmm<e+)aieoL=6!Q^Uh#or*t#mXj?}4 z(Evsm+W3Ny)v$CHgUp=XJhzG|&~9MVRM4~kx#QLchD2WZbyauhcQT9dYSl7V8`)#5 zINV2x;iNPnv0ZXuJ>BN-{m|jFy94ABGNRAm=oHkyKRW*w0KEFhdrAlr#1{LFUo0Ly zFSadDhk7>9NPfLR<N4S55fsM$E76F>j!u;Q6T$!41w@#+d;b?2PjgcYMNLu3gP??% zSxalmf$*VAm>$-H+XXWUoE-O&vhyoGYm-&##*6|D6OOaZY$MLm3~?N_>0vHl?pGgs zykAk7GZ_-46>xu&E!A$%OG*11uw3K-Ibmmi?Brts(IB8qIG%YM9bm~*J8`!qNx&ia zREwtKCEYpY;nxgCBo4q+)g5jMFpC@@$f_>_!W(E8RwD=Ah-{M5ftgppv-71e$^SbG z6r{*ULH?19*FYF?#pB7nknnIyfuklly`}CnoT|M(740TVw;f`jI`ARr_@kbn2x&wS zCKdj)W-8@m@FMD^kkJb^n%@C9NXMKW0m^<+B~loB*q`~a%)fN(@7FDAqF}$>&tDNW z{X{dl<)8;*jcu%pO)MPNi<8=mDi?6sx_f(XgI)e}d?-S19Fhu?(qt@_0UO}XFQ-r} zG%v8v9b$!nYNw<7e;!8PQ~YbLzY)#<=&{RN^azFIk&%pDTJQ6jpg~<GjGMk(9zt)g zH!b(&%$=WXV9`1Eq{|dp&4^XF99efdV*dX6Zv?gPVlQLI>cIvqZ@dsHt(iffV!M*K z@`Bn(k^;Xs@{J(*-A;KGVvY+_%X1%!l3)zX%6~JGU0&q3@V2PmXtJN&`kLd-RME?a z#%4wK5z>7h=IM09<Mh-85tu)X<B$mpD4N=ws#nm`V$#>wuSrE^yY<K5e@}b^_MLXb z%4vD95OD&=SFo4C8l_mRQe%^X4rK(VlNl%k0=#m_*A4<u8?MgO9ofzvEw1k%G8hey z{S|TS<<C05>rH+oZ(sG1A;Oy1W$h4OtI}7{O(+Ghe*`M>@j_Kgxbo(lIP!NhneG;( z{CI)S#<^mh{+|w_6M6&qI5Fva)O@TA{xr1m$+&mgZu~ZsAk))&3er`M<$g*?AQ2(q zpY{Fu$=`#%W@jW8zB2~^^B|s-<mIw->Ov^4uRV(PeE;_aO&+;C9frS8M}bJ@#o)?h z!y}UC4_bkm<RRu4te@<9=Sb33aJ_;i0McCLIxKhfpAYqbJr%r`^0uKpD*D38PP;*H z0<oD?R7m0&YN}3>%Yf6Rh6)R(DE>K@2y0xAxDkOVl~xJ|SB*CwBN$oNFqGSq%zk`- zJu3>uUx$GWiHTS9H+u2&SUNEyMFoOhlR;4kh}p>hicV+QBKF^~f(J|zY}9bYd9j|$ z!k19=KO4WRjLbkdp<Y0;_Dc}(Yi*9%knC<S@>i%}S{%71%?n<VB2;Kd-;;f<sh3cH z8%o5(=tu)*iEX$W=U@Mm484U&%?rr${*RkHhq%ex=n^}BPCeK>fUbi}TC7~Hxy)B- zF6E^B0&!ot@HM$oy;Tew$Y<VL)qgRba%@?U8C-aHaP2^}T7Z1Tbmb!%l!4GP1m~YU z`Op4#;vpLw6NUkygF;(b(Qa7pz<_<)<Da1Wufuu;`G!O?AA%m|G<~KiN`Duz8Gzy! zd#~I)#rEF=3vT?{4RkZd)FVpIPC?<-|3UVD?+%3>rqy`UfQy*l)#d1ke7n^&$ud znP)d_MG=4y(C@)f(S=9aH=v&Zx;#!<JFvv~!S4dXuQ|nPduW8H{R9mQo6ZYrL+-w# z)E{1TRM1kUbB4N_JcNSz%wq5L4EDl@SJ>w-OwXka4=T0XRE{S?&*$c_N*MEUAKWyq zBHJI`N3<J3^9~N15#Mh)>Y>rw-sNI3n>FHcTn&i_zlmv&8oL`PmoaF$RRMyi2_82Q z70}9{TBH-m##!D7Cx_`jQF6nnxwT;aa(16|v`XENIbj3$Vs`d<Q&U^``}a;+SupFj z;*0mK&(5D0`f3;H<h=@`t%90GFK45vmbe7!>FYNQ@Cby`<LRF-f7{nlbbnC@yA?sc zv49S`AGl6@-dWpS-2#069{XcGn(8x%un4vl<iCCV8;tY_!WG0Aex{K6uZ<qI0qy_f z5iTa|mx}=x|7kq8;yjZUQ{<ff_IVt0^I=nu{&{z#f@_-axa*qSBjbhLA#}Zl(RbTx zU2nxU%NXzLSE@FCk~?ixE}wcgA)%}LHcknem9MTmp^uWRJKK29R|Gx4RJC78VA0uq z%;ogD02C_txsIW88SF*bVeuQ#yDXL0hOEYGRGg%2jLby4AGA;i8NJjnWgOM@3WH;= znKD+2ETCc%(dBHob@~W_q6-!Gi4@)~solwg3lL~2>W!px&K2QQ@^98;w3=`li!q!8 zX>hyW*XmPX0v6#K@?L3I!IO6sCFoz9(EguG)L9OwSdM<=uY@I*!B?HI{(6x|5cVdI z7q_fzm-!h;?_3g74HQd(;kyD1U+;}P!@##U6b+iIsp(U{J@$0l?DuD}m}XOby_44; z`MQVLJxJ2|%(L@qKT=_N;MH1+-2pM|VWSVb)hh(X8e?WC$@PI(b~j-KdJ`(z2+U8f zchJ{*_A6(@*&eIPNoYd&!mT&3-H{7w)iZ0ovw8tGb8N>WXziE6)uL!qds+AJeJkV4 zf@m<asV)7+*@Bu(VDEq?X9kbEz)cdi?+{^fG(ewD*tu?=2$Xtuf|qGZ`7t$~1p`&< z5@FHVS$XQ>^bpftae08s@Q~|F0sX-<ZI}hZcnf%Ad%fpx<NDXyQFO{&;(Mk5-z$$( z6D+;Fsn|C?T#BE%WUDffJu+hJmH6AcdB85MUXx1!`DdrX5@<XEYvgZ2YdB_7bQ*Vt z1dwq)yAzjI<<G1ck{34~5~>St>5haBQqPu~jbay4(q<WVPcW1wSTvlM#?n!e<w2yQ zW1}q2Ipj(gGM*;U-^yr<D9(^2$dJOj=OkjI7_KU;CF)vtw`tV2!XRd!O;Jo#wNSO2 zr-<HAxz<hD-!^YPoAIS!-ODWpUggo@-qdex?WpSefvZ8Ut$J46quw_-X{Xo?n2N4+ z?Hd>vj-Yy@;fcJh|Cj=+^C4L=E7{P{Ojy$fc4T%Azfq~!*0HT`x$~Z9P;rqBgS)2G z>G7Vakd+6jzeR;i!_yBsKa%OtG(Vd~2E?(+V7i>8f%-Q3hsK?-sHB)aInZg$GoI%y zuBc<O?i_66{DoR8?iRDag)}QicOd$Fc-d_`jGGuh;Nq&8R}Bh9(E|LwY<zxh6~+C< zAjUM*^o3RyL%69*jc$F@2T=96WMFK6^XhfXNBLP$iM3MG?WqQpcq)76F%5A;!jswA z+u<}KC8zWAVok&kB^28D<TdSn1YAs-M#jd{Wi2g%bcxEYbV^q}`rW|?j>o$2tle?( zq!5haC#T+)wK`w$)eCcj>e@EULW;uf(GaRb1d~y5j2?Dm8qNE2Cq2O)e#=B5^E+|= z>2HrYE{jxwYpgk|wG2-_KpYn*e&8JWE{MARAu4y-5FvScX#n*M4?IwV@2%jJPgl|D z{+{*JD(#VNix{E8V+G}Ls{+;Z<6PDJ<Gjyctfeu)xQZVxR&#SDJ}#PoU<n)~Bo!M~ zJdNdOfsz=Di`o>xHgl36Fgg$CUJILj24}a-e83o*w^2KFOL2J!R02AJe()tXYzdF! z?u?p|-r4htI$07YPFwWkrWYd*<89h*#Jb2H3vjk~GmB>IST`hLBtQ3%xfow@56f=e zPOb(f=|yi~ca?3G3O0}B78)2Pm`=j?S>-ww>HVW?Z(SUj5_uST%3zk?)9Zu>LHEL@ zBBd3VvtA53?$Pr)bukq6>6VSv(UQ$OF}$v2E_+Az&aMU>+V>R=KnsD|L%`SsnYlgG z*>$KYsgCLzCV1T(Y!C+&JmPM<1Koq24~Wk<la!yFGzPmF6eukcY@!L1b7CgbL0t34 zOh}^OSR2tP^<omT7BJ?HBhW+xUUm?OUD}jr(@@1MM9;GU+ZWG*P2+LP#oC9rs@fJC zsUC@8#3qE#v?!THK`(Yc9k}S)jHWIrJLKrfpDg)EvA+eptT)+37U{@%+Em9;v+BXu z1G6el*s&+xwF#>UXx;<vZij7s9aIprT}DiKhgl?lf~d=wNpRp&RIL8B-!N%iO=$Mg z!gzcEK8i@C`Zng@w%3aq<N!qG;y)uMKHsljh29wqt)kAhmW$8%2Xp#YnvKKGUvW8a zd%3{Q&XUu)+XI~xM1#|!nI-KnG$`+pzUnb|-MC51(V}Vk;i3L}O*gFrCBcjq?Hqzm zoS~&7ZpuT&N1txRr_Jqs0p;4g1PdLZV#y!k!^{%MCWbaWarW6qX0?PCwFh185}rqQ z`fcM9@>}QPjV@JiYMox>W_I59n<lqAtse7voG|Ib-WOV2(WGA6kkyioX3YM8RX^k7 zcd(Cxi9PPUM?2t(e<!na*~3y^%|O+{nZa23kOjx@NC-P`z+;DR_P%zQ7!X1p5fWo_ zT1hw<d2E;d{O-qnB~9wmg1GU+;^$-74~L)3CoOR@ex4p<E^~hXrk8|g70);2I{ND` z(5TPu`cldkC9ka>Ne39&=JPG!S)Ox0fYGA{^KY@PG#t%K*Ova^-L9|YSkJV3#gyFs zcm)uL^$O@pcknS<Myrv;Ud9-%a2>W;aD6t+7*lzbpFR*07BxSU_i^Uy=2Ab@b^T*_ zI<e5=H+`59-uv{pt=xepB9k>?@nQF{+%}k32~KTWs$g3wOJYGW6w)er?9*LNj%&dk z%e2~^X)C4Xma&g=@++M3N>9tRjuSL$@2qpLv@~J@Cv6UBIeTI!y8m@IcF2`_r_D4; zq2?WYj10sRc8L*)(1wBx#KaA6>A<j={|;BOB{l>KQ8N5?XUBc9e+tKipPFw)%*nl) zMp9I_ZYh3nb4Qk&=A8{mCH1&81%lE~YTxWitEM5Yi$mk;d&W8zu+YS6KwM}Ts=-^J zBQBr~-z#4HRzWN*yW#pVKWv6Zu3Ueydp6}Dm~!`Ye-UY@M%Vd8HD?XNp&GyYv>fEP zzI!81Hi4S_^jur**`oLcjEOp$d3cAG*0|wrFFmBjc0ItQ`pDcyD?y~ERbf_6RVb0$ z+s%15nJZ__*0U$6(|l!%POta#)I04s>Lxqd<IRQoOsjCcCO`K6t?m_!EG{*SkqYac zZ)wVcITpc~9ghAgMBbf*v5l24vk#xu=Q>IUzP4Gv*)?g|N@&RwovT6rsX_J^(P6Qb zpzR2ERSt1>n6Ri~s}9#Hr+Y}kZ<5<&^PRy`>(#X!r##uD$4(-=M1xp)nyuC=XL?<X z<<jXx**@E=O*8Xm1BYBkeMd^yl7VGm)xWRmVG^!7eKwdPSl&%VNx>16gT-g@BLSSs zK7(uT{EE7X9jn)p${i}~)cc+QQ<aNDHV{)o6=T{i$HtTk%G5^+PGvvIY-&U2J_)zR zE}J+m^exnMA#JtWCF*G?FECUOHGrwl=n6J_evF2@Uwn%OHEeDc@Lot4$5$Uz)GkK# zu^4>Qvk|y9PIHfbAl6i$<Y_S3p%`o^O}5bO+{t|B`fU(^YR9^95^gO%X(E5|GQW<g z>S;~h_wyM`_m^(dq{y2$$nY_-4jnGbRug6vwR1XR7ZZaS2sb_({2j-DLzf(Pg95TP zk1n`}aAU)FCw&0aN#7>zvSF>YrgxX<zmteKly8CoMazGCRWMlwxf10xUbI}o(YVlT z{7W5V;M5|<Tz?l`7>h_~vCK!3HLz!nFPQ(8=o{d#k~qKHji>rY7ld}r&@|Us+7o(= zM(^t-i)2Do%`Z;gfXJG<UM#4vrO>^2rN?Ja)TWymaux{cLzrjQBf@7NeN&pj$g)WH zXb)K-b7*C{mY<)pOty>y?^6){G9s|)r^?Qh;SS3M6SCu0=N8=ObX_K(?q;K6tokig z1*(S18S#&9r4t}6fD!T5svNaD**29bmZShK<9?>5L8ab$XyaMmUG=>tDGiNoV00&w zRFRpu&cKM^u~8<D7r%oD_VG!l(L^DrXKVSoGH5G2N#FvlPs&|Xp4mplWZC1_39kXM zOc7HlZI;;A)3c1#imxU6C>Ovt)P#X0tJyB<`lW^ZlW@8rGZ1eW){%ka(j)Q7l}AQ$ zl|&`KaotOYQ4=os1qgx9UN#*rI)qjp;7Z7VAx?3_S2bhRL5|ui)oQ(v5onVE%{r8* ziQ{($fkRd27rShc#WD%W0G??IRp*UQ{jYjfFqh>xK&H1sl5LR$^087Jtw-b9!)MWM zC$*=(ZQ~^%`<WjV?I-P(B#)qIek-1INE}zcf=#4~oqePF+xHmG_`4Hqve6a&OA0d_ zr*%G(#LIRA*J<+5G_c%e@G7{iV8_VYzy>F&4)g#mTy)f(T5J_kK2&fBt?F#0mJ`&y z<Cs)0mZAb*C1mDQfkZ3~wv$(AW*T5*0JQW#nyQj(2?wS`Ro314slD5&$42!!h=Mx5 zWX}^o`#ahcHU?a*R>)jNnZ#=sTIA*i>nsD_u4Ba1+NM3%&I*BQpL}8_1*NTm8`N(o zN)EJm)c$t4U@^bq%16uJ1R%kqO)SKMlL>(Z5B`F|_N&(AXKGoKh}f%h+-@ZN8yK`C zcZ=tl3tNIwmDQD0naJJ0bTMRT;yDbjL%-jB9^IlWWTZYdnBrPwxIn`pnandIU3(@f zUuImqfC>>VzxKm!KA93TMa@FiA~Dx_(zy)H?tQ#j6wyGLLv)EdO{NLFubUn+hIe}* zr-R9T9WRt0_^>h{z4Sst)k;ZWlAJhsJA<vJtwP^iSkp<=Qr6%TMVKd&se*m{%DJs@ ztMYjbld86+o#^=3CTfN2S^a)u*H+HV2k<1jZUA|6zF<#9{Tp-mjZ0iX=JOG>mxWmk zH?p=k6+U&rs$^jM_uPd6Jdmxar1r-t`536TqGw2=kt99+NbkM7x!iiVl5h2?J;L$m zn!;n904srbiywZX`0X)s9TR9(<foqUc91c$I|XO<B*HbwmH5Lo?g|R6xTC=r?<8ra z0gn?*?(W)-yVo@_;GyA)+=lOF9u;88#$Kj1KI~j;ddwof?{X9aT8oOa6cx>eV@-g; z_8lm5apewaPd{+ie|fteb=4X6(CU2uWVsj+gA&8{f<zYI<J%57;rSl-l=mf$e_-Ot zsI^nxLl;(G$UBQO^oqU}aHWSie=l85Qu2}RcdJmPuGg=*uA<(2B;CM+nNkT_TU6Wd zHbI;h7;H&|m9UApG$$QNldxPiXq~mq+%IHQ3-`6c<z`C-_-StLl^QTx6=+kS9yi&^ z&Th$Ts~fJ*?I)pn90(~033<#f%0FOWh=`3P%F0H&NE7u&Th#lAC{YIUfy`?d8A&ZH zXiRquiFqA9_xANwXl*15y#?B@y8e;T)$+;xat3~91BOA#cUnLtK?>$70kYlP#Kcc- z;a&vy%db~-t;X;5_4RSySH!{nG4&j%EvTrehgMb+zW+!GgNV-0%}H8Y7v-tIqt)+% zntxm5)PaFEr;~#XW@BUHvhic5je?lbMVnf;Gb&2T`aP%O;$rHSr`wUm{N<x)Y8m}& z_BAmvF`#@+cO@^c&o`Gx2=oMCbhTJq2vfhy$EUeVrVJ?AoP!Yl;*#2d{G(_xA80M8 zh8Gne27`P0b2{kG7<$i_5kz~3L4u8u`&h1ufWNIp#-3J<Qy)-mIDwHA23&xBtp{+l zIS^#1oa6*Vmj3Jn5K~oE9|Kwf#3O#Fr~=bsxEAM0D$6^@KH<G(fkGGfbr9@q-aGJ` z+&`}&T%A7weS@o5^Bp#TR0DQAo>1ZJ0#^Z5_ainC_5$;^gXp{izCVy&zyH1Kz%GCN z8H9}dV4g%@7yBZ}`dBNtyN7iS26r=~to{futL4G6bss%#&jC(jNM$jbXT7*3lYICj za`i&(N0cTyzehTrZrNCV;jN6uZ*i8aA8VZ?*(a8kiWUmawgz5xK;V5vzxyR!zs{Wo z+dgFuG^jseP`zjjZtb2O5_^48rW$sh%j|k@oyt=rQ!AR9zP~xs>XwV{RQFMFgXqE9 z2ThhU*=L%vdplW$g|eW40-kGz<_SDCP3KFEfRR{ou}cQGW+Bme7R*mV2sso(oo}^( z8Cgw7I}7GD7O;n^h*Ex-iz+6`<H_uon`I*w;b6v~dO@6U8JLBnX7wJNwOW$OqxrbJ zH!`5k`LI~8377s!-^xn*6~j8uYswT^K;<D3!voF7qoZ4(SIIBAc;fraZ>ZQnG6<*B z*qxr9UiZ1l$&aT={9|a%CF9%H2luJMY2?a~>GR4<N=nY&H$3>^bps4w0FoOnRSgZ2 zPYNT<K<cq0UA;B4w%50irY>~Ct>1iGeIrdedNjLVYed!H>B?Ts>nTql^mrk^@v6oh z%wbwktD%Fbf>~2}2dy!z`F%M$5$YBdKdkv(k4u1j!+%aGM|tSy=@ZsXuvy&W0~L_! zeST<`8?X}#^^HPZxJHi3g^|mo2kPn`pVMxp>7I!w7T<2j+QwA9w`Se};BWBta|Z^0 zR$q^CK~+);-R=Wb$3*G6sSPw?VKG?SRs-?pFU#u{Rc>T>cz8bw-rl7?n6Gdy(UD){ zwmAqv!p^9NaX?j&R>ADEm@2q2@3lLibw!BS>Hf*IGjx7^i&#z;v;-i<-5T2-2fc6- zx)Ts3_N4c=hU^DM#}Ht>TN=S#bt6G+A}Oz#ClsG`+W1(E8eemLfku;zmyZmjWw9;{ z>Cf5?9Uf?5X_vXoe()`_?Ng(CK_8Aa?v$lOnd?k`R%q8dTD+q>Q8Y{@S2ViUIU|NK zB)7kQt`W3)(cHg+=jAj;PA?^`x6`0vR-rn?RdgOoKFmf3tI|v0a+O)@4N?gh>hL=n zrtg@M4D}A6*AEj<KoSiiA7|EfFXYSBDxk1U=d0~vpmi(F?k0M{_`?3OWmvWJ*Nd)b zOAuzae7F%l_iacK6m)GImk5D5?xBxnXqx7vLD4`ur^^ygRgVDl&7z$wr|^NR_S(aN zSC)qU%eMRl6uVo*$TJJWjPO@Wc965~X#Up$4w`zd&+KU^S*6#{f>gXtkCPIM_pW7Z zlF7unDZUX|L=6pl!mIMoA#>DzKm_(hj>8gkMwC4Ys;Zo^d+a)R$L9F^Gti&7G8z}~ z1m%$Lu1$wvc$qp<T--F>y9{lz%$IVaVHRl5!~+OkB@BWN0HmyaHBzMK5yf$P%gkMU zb<a*#(l?b!UzYL%Q55{?nTXt5Yx`oYGwMpeSHSpj8)UB}RCXHISfB+e$)_{D1*#OM zzCL;Pv8*$@o=1xAIQf{5)e7RKG0vv7YO#AjTY`}1(XwX~YfgPilB%FJ09q4Jroc;p zVk$%f9x-0;L|&m<FfMYO+k0NQM3L2jac0)(G<Lf^M^ItR^73mHt1gfXYjcuP@sR}w z5V3<zoyWSiTMS(tER3>&_T~W5441L{9yM%VY2~NTjRS~Y{?n7O0%4hr%EEdA&RdfX zAZ)U6MOal;(^tUEn*J8Q0cqS%72M?y>&v4x#d)jw70%9>DQkPELPpMBK61B_X9DD{ z>e|YO*EM>S%s?HB`%4cztmUSTGK;XCjM|$k7GX#0GgEMP6VWinhOMHMx~ig&e#Et> zyOi3;R^jPQp87efR(Yz_1w|j9U%!nJRRW5j4BUKbt?JSF^Ta@^U|?io7M4wqSH|a* zYmsJId+Pj&%V?xav8N0_9ZW4|1-(|AN;GILvk}hvw&*)VVsy*BpO^LoTJY*=*+3hs z_9IYcycDggeDqYNFaxNEzti~j-C8E8G&rM-O4%>{vgo<Hl4SZc*;+IPX6p5Yj&9B5 zL-FFB7$CxA6dRM_Je?ZQWMo-B1$FZV7_+1=I@d`@=0=SRQvB<?Yrz<1{Ye5xfc!&R z*GFp2$F3?oOzkA&vC9gY(xmLR^HjmEeEKuMB}rJc2FP~=!zcVs)3Pb7i=p(mWJPW9 z@}QmkbW8JiyAp<OK)!d~@~NU=JH8+KQxpCFr?+an1r2{B*JOB?-2Nv~TVz6>0<qC5 z{udU*5jgFV>Z>I_$u^`hW1d>bR@dfxm4l#YUqwTJu}Hgq6E#dG7to7;2({q!4jtjN z9h8V+a}By3L@=ipil`pYc@pU{;J*6Zn8kEmXXW-XRj)1G_bRnKdJCr(;2=*|Kgp!a z_*2txb{J&kSL#G5yGxrOck}3T9ev{JPn*bS{h&+2o$(?=0kpNrI-Xqkk!oD5-~KU9 zbYu$e&ULt{JE=+w<;lT|y2-|_TP`{sGu{V8oV}!+*;_lU<c|j#L|_dAax%%^MuE%< z?X~b;#=#xzF{`@CR}FpJNO5}rA8N3(_8-EstSvV?<E-4~3F8Ox->A99s}VFe{2{8C zDuABGb0~t))YADX+y}(AUuw82j#%vGH3-@5!>BT-PO>qc3K&;bB%)hJwQs&CoYx}c zc1ewYC|jtlnlU(e%=7IN`#Hcx2N?(jp3pT*1@5f-a}JYLZP#2=&@w%p`h;@4f0$X& zN6z`=z$L(>$zg+HM*BYw_cEKq_TltcDnAnhQesB+Bv<ZnrDyz?#NsH$8-j?GzsW5m z$oFuNiEO1x5BW+7YVxqQnmFCuv$G!i;@H4#bfl^}1VkiwU308qC!d(;3vxoZWrID; z)$HJ|VX?7y9O%qym!&FPwzlR!?{p=;IQXh7RuaQJ;Zh+IL^gWsk#lBFTE0PcP*Ny~ zxCr{DLs*)-7EFUvr?awhC8bM~U%9M5vNXN2MSpJ&S)tXzdGkb06=kD#>!VdQqdy@g zDKpl^WI?Xa;f^_Z<B%d;SDE7)IIE<Tpd|qOaVXk92l0(y@ZWWB2&KZ-TAIftWX=O1 z^O`fXZnwV|YS#sEa=S`nsAoRSh?*b@6ZH<@{nBoL?YU{gep5!%JSyJ(pj6;}V$kx7 zcgZVchly=F4N2!YN_FWo*vodMr!3jr5~Dm21YrL72NS1kY2S8xP6xcC>zYVFc#i5i ztT5%7)<utHY6VwkBzL;fGXj9sp#f!o-kZOvUPPkqF)lF~nq(Ojrs!;SV=WLrm;;c# zHGXd>ubXEUMEZ40*JQ7;{aXUL$Oj5j*Z2nfqJ+QR#csOW>$}pv27nSDG^Ag2kWndZ z3Lt;)Q+*u3eI2iJus(cO9P^32-<sKiL7O_2qG4Nf{Ru40Rok^|NT(4H`L-UQ?*r3U zO8U#=@<+emktp`EPqAb0exaF*Top?RfTty>E}3o>d84RA5sX-gfk@b&mm|O)8e9M- z%CNMVJ-YC=Na#mJR8d!kj+1XlDwLN^+RB<8I?^PBf8)X&!`-09ZvMgbUQT;@1}JUT zr}4T+dl|GhM%Oi5itQRi7W*Y}oyNI>WI%t$om6dEcP__ip9s8_V0Fr90T(br__77c zS>A0;-j(NMA>&O8W(id7R*O*%%pUSg?S)k?VPU*1?oDXwOE5rdR}Qam5>1xS*b-}= zu&@+U@$o6<YvqNdH(aMF9(o_OLc${%Khw#$rG=cL5wCmrRs6=|nYt=aIK;YO3tRM> zbZ~cacLC{_&`V63_kRhrexfb5tO$w3PmoLS+buzi*v;q<nE$}6;y7RLmWXR&5?%dy zSrMELFDTJ)KBfM6d0hW8zTC2c*HU^cPdpF}6@Xr_e1=f+r$A>7K}s{-!~J{FuOL6V zkG+mfq<M|DA&rsEA#BqfP5;}YKYJ+wCJ74Pnv?3r`uiUIc?XX#IIq7as|juWP_)YN z_WWBe0Zax8?$^6)A=gj-O7zzdMG<6or~l`bzuxee*Q2h!LG5~ET68K5R@Sj|<qsfr z`5u#)9<PB6LeVO@{%_9_2!v#po&$g8-B$P3!mR`J%U}NP!r#uYTTW77WlW%eLhDHK z+O=yQl-=&;gZ~oqf0j?s6Q_~+K}2r}h7d;VOZ=$yKTnD{p!nEe8}K!#V*jK4ov;zx z(D@^o_OFNh>qkC>;w90J&*<i#B?X&zOoVujMIfCte6PPrHFa_O`ny|y{uPA~gaRTH zf-cGV=X?FL3_8+Zao+YnSCNvj6E#E6VEdn)A*BGe4wWJl<v&~ZRs4!jCf2oI?bZIA zAzc5jlUBw0or6QxuUj^5<>>Argr#rL0n%T2C5G;wPxr9D;l<;1KZe%>Gs&DB6!^VM zJD_4m?H#dq!<qgxb(6UPeI{+DgJvE7=dTMduamn`2O_`v8*GQ8p`js*r(O(y{#*QW z`BnJdUrM4pI}!4vVB=di5tL>pF+vh`Q^l9#pPOKb*aAxpL#O{VXF8&wZvI7RD#`z> z17E-!Zm)coZNj1b(H;d*@{MIm+IIQYtcapOX?xj3)q9Eh)6VmFAC>+rJ^+#z)lr^f zt7*E5hV?$qiR{!_jaTyrrYNG*K~CZGg`$y*5&QS@b`{k+RQ|j%_W-PQtVAqcatD5| zcDhseDEU^Ez;kROMr{(|Gu`mNv{FBxH5QDmnqZ*L*l$LVab0Vx&u506!n?DOn@~Jt zaXUHby}W?O1?`6^|7kxgC=5&hTM83R!hd|P6%E+vQ5?lTg=Ie<!egB><a0{{<@Gg~ zkZ4=z4GOft)-?PCQH)`KraLmfRj>%a9LT6A@w~K_Y=ismvatbC+xrF^jGw1N;yLot zyp2_JYlVVcnuaW?!0L*DiCQItW`**;``NpQ=sPFv#aDp`0!3Ap)g~o`{~fl=cSgNI z^}T4z+(%&K1!LY@FNy0O`8Y;B#J59@1|};%5I@TF|D5%&GxqTVLB+GFdyi0#sXTzf zL*il-$$Ga_D=TZg`VH@$O>65Nz3E<61ywCNnqX2t(a?x4amaUP5@%jX7U&ilp@3s0 zwbRRvXzR^5P5w&4m>BB~sGC_ZXb9X4;@|+Il5hAOm?%!SfGe$8GU-zOpVN{+k^Hf9 zI^3Bjv^cJ)<ldFQ2j^zg=A5PRCMWkiARjw??sY+yj_>6s4jKY3eJi{3{y%T<_3j&y zOc76WZK{X1hYw;*;!mag{c}wxLcWEvnD6t_GSf%aRPnE2hY5+iZwcT+pWXg}Zf%sL zecG4OT0_zGRY*jCZwzZF>W=1P`Vj#KdV@S!Sy?<h2M)Gg=nEsK5lt+~y2+HXn;SLf zG+EgU!K%(5X^oovN<Sjkj`5U9+bO*Ros7jmH_h3Vgrvquzak^QW$WP=0lIbt)j)?1 zI`_!ho4FRWm@gY&d%_d6vyE<sPe(7sGaBhAt?cI~%kBawG1v6oJ}s<f0idUesd<@| znr0}0&_V1hqj$?|<44@zeA(RUsPNb&l|;Z=usw<YZg@sV=qpNb^+&rg;c(>eA}k~o zK-k#faYp{HI9D(Z<sO8$-YZ$i*i$#$b9y+}R{~<|Cn){lGppb^Auc}lOddTIOz|d# z;?o-x*8^!~(f&2j(}LiZKNd`D)Dn3)-5b-;Wm#br#}Ixn;nKx@(xzRhkR50`QB`o3 zC2~f<2l_f|!PNYi;{CH;6p2Jug-Ar#!VY-kB*Lr&c&j()hYtGiALaDXcRM5GO@R;B z4@l{t`=@CJT<cs2QHEvBDGz=2ihKhBjnn3dJ1uP%ZIO9T9B9eu=s4*tCuyP<>vB59 z1NzU$5DfI&TNM((gPhZ>UH9Hym{on#A|PIS?6&KhG^AzW09uw04t81uiw7n=kXFtQ zjhesXH~@((P*y676;F5)T~HzZ{&rBvv^2inN;;>SjC0F!RlpV70Ft3v@%{DjyDb4m z(C>>@*1MBhNAoQ~zWwTq@sR&3=mMd6h~S$KtP=BtueGQohi|)HPYZlC@&FAP(}Uwl zf>xo?8;o7C;Ww=J%x9t5FIO-@kh}gm{mwr}IoTBD8>#SKp5&x<>|d6iEqm?snCX7( zu>@AZiy+V<@gEJ=$8}e93s@gFuZ7c0hk;pHhx6{Cq!_l7GhjSs4&=m}gZ15c)U_4D z#JgEpQuiw>NgMLrucG1+9Xu1qK6deTFg+uTQB{Tvn|+5Cn#r-bke!~(<xv1VgjRIe zM|A58T{kAYj^Dii<kSY>Ya6w+qWB##_Qpo;r{6!Gtp)2^nhu);v-Hii)_8Vmk2Oxp zo+R{__VHWB=9j_+f(lAUYA?wngL?E@Oi+@LKY;*_$S$PmW5@TF#I3gvQ|w#hIvOMP zur4IR6vF_)A2$aQxchPrN6W+9qYy#TM`eDpN-j<az1WXK)^Hm81KrePT5=fYb(_J% z+yW)Tk;OwQyxX@?<~O6;5aih(Uwio@1rah1O8l|fk4%2ci0QHSzJiwDi5)XJ6PV8e z&CDpUEs^;P7XnqN?h4D*hG+V6TH$D?dd>Pm!)Yhvt~|<2y5{`1pL!$MaUXbq4j6w4 z6AXn!MF5^=`TfIIky#(9{f#3=V$LVI8VR>|u4AxBN2)ZI#+oeoh!`o`2kml7e_BzP z9w^|^De?I+qThHjH>+CS)Lay1-?QS}cJ2ro^NdX}Xu65!E33q+h)!Ahu>?*s(++`N zW?H3oyB(wyZEZ3N&$LXQya{|2*woyP>EOK_sEi~}RsKZNf|s39Dc%7ucsXojm`wjR z0dJySQ)6>U#&rx59TKdKkYUpSFn*)}<$m}IJ3Gt`Q9hz3<|JPY!>TgqF2Ub*nwy;` z-k-~>^flO9F|?BzFv%YtXt*ZwUeYG~m9E#f<_8L24!UTb3KY%OmurV^ck58DpQIRZ z#Z3%R=u?Y^77f}QT}4$LS8902p`5}x^CD)rjE#MG9}r_Dlrt|{wy0?0R-j7Ep@auV z^TYasC!*03mh`i`oW^Ym%Xs=f@~olTrhKFoavurd_9Ua3f(04xWeP4-iHKG#<MFbp zwCH8ttG?VsizD}GUQ*tXOSB3fEqNIef4=_#^pdXsde8Obw-znIQR#N&%ON^2BQKWZ z@K_oFkRqtS*KxqtS=`Ft|L5WX$GM5?LovPUFC^lX?13zAI1xROYS;d*XfN$X;3NSu zDHZm%=lDV8c;W%sk=Asa4gb<YpbP(iJTZPsJMq%J@2}}(5h{%v3_t!3nyw&Q-A(rF zTwbPc&%sjnKBpx-Z2putbGta+*2pF7Xr=ViQR%nNQxa&rz-c9i8=81P)LeN%G`gW& zQ-(T6EOoqib2HC6yR=&pyR>^Pp1nY;X0@=bqXo@k_x3_Zd>GKfjK7AsvKsMXk@7b~ zo(`zeNYcn@uGv+UxV*!0HhJAp1)m93S1th3C~<DdHRWi<fwYeu3H%np3A;`Y&(hzq zxb7pbJT;<h6QDaBEnMTHl&Ut9<60tjIxWa0o;okE2Y$yte%9W1rK8*(Tlt();8az< zBKFnBdPkn(tHm=ukgCPJ{PMRBY)+dOss>!%gwJAiiv{>%YRb8)8~&E&3!s(%gJzWx z0rP&|7<P9_c?Heb6Tr$(%`80OJ|pEeOJ&fs%fRdBwxZ)5+pilDBU#ojSWqH3O16Ub zeugvD@tLmUwnzqp<}GVqzPL15o$hKE*kQSJpvfRaSvXT#p*-s+`Lpv_5eKc-Q%aF@ zv18XW_?#YEHc7<*K@?xD;R4)xwG*a1VtFgZ9eHP^LIPedb`0O5iR1L!A0vb#{Qs&W zFVI=2+U}T4>?x(pd+=3(ELU9^u$FsxK}N15(a}*Tk+th}aO#B%!wudmGnx5&?$4EL z)QcEZDxfW61Tz-8x^c^%H@sw#Z_qNQ@`Y7DcaVfMIUk`qAPJ*-o4l<fczsQG^kHL7 z^H`CdYK<xNMMp(3)V@q&I5JJ?B)=v$Mt$LYzJ0ZI%(C6rKck?`T-g(zX>@0V+cSU< z=TT*F(iBUOctwydVQtZdQe-64(U1R+t+$SA>e2sz6+tEl3?!sWq(M5QLlmSNq@|@h zMt6&pATe@ucd3+gHv<J}7?Oig&*9$ZKEHeYJ}>vjhTGXWJD>i%KcDwN(U)4cOva)( zYNvjqSwXR;O@(^xbW1RHa_aJ7!1A<kYHNd^yd3Tx|75`xK+9%571Hl|G>4Beos{F) z-$l9I-*tE1wCA-ABu?yr)#ywzm`;T-^IGW$LoRxr3wt}_6~t$}4VH^3G|(4cyZ-S< zm}=?5RDTlbcN3tzb{;`91!?22!@-6s&FDP155D)`k3fFe9G4Z>f&L+@3rK@V5BY?0 zA4jogw7%!EmX-wNg}Kxliw_#4$Tqc5`fNlFebI6^0s7T*V=NK3H=p^O(DAAniws(R zJGL_JUMX7`lwAzfc*~}w2$zrRIC!IhgZV|(r$A^l_x|fK)`o6OTjzC=dp4sep?~($ zYY%L9?aI!;;87?v%K=sYESFq0*n)&Sd<=cGr(_ruAzWKKqK3?ZygmF0P>v=CD5kzK zG5I}YYp;=E%FM&K8#BPE#UMMx#IaqjMc1>Ft#kXy@z<45@(gnUt3LgH{!!%>(uISV zBRrm8#x>DMCI{o9S$>R#1J<*TymV;aay;Y^iZIYyaX^&*Ci!SKT6e!t=QOy{Q1$yO zdV%p6FM)(%;zItO?-nq2jYm>=i1~urw83h&q<!)CO$*AAA|5Ego@zn0kU#?5y=7oy zM5oGbrdmtF?^G}uvBgHer3sYp!*Z40sluvv;b{>qeJ|{{af36<cLe~Ul3yRuY;VrV z)@^EurGNbx%c8l3+z0qOyQbEJynIv=gSJpZqR~L-CF)qifC2F_)8y=X&ohWuYP^E1 zSF`ZU1j47XQ)1nGnLX7I0w4a6%%iL6eD?mio#8FC_WPjDu_Z-cckg6SPh;^+zJuk3 zj@z4krPc56g1XyJk8X?IF3ilg2L-b_xA@)OhMME(>k>&;f#mIc%NRcB)FzIG*4h}{ zJXWNZqwjr+W{$s?lAH`XR+*4_d<{?r{^4>N3U~g}xfxlxro*;BqT3{st|=Lv-c#aW z7h4O#uZs#;B70t|kEGh0xh}QKuKj_uwN-21ovB9lMk_a;sFM&v%f9UD3qYh8dGKn_ zzx0foRT>K%?^4)rxGAwo(o_M0!dulgP6i6N_dk!ir}y!Ll{%453+H>%?5tSz5&Vi= zFw#B(CEd+<0+#_bJ4|wgNKL1(7aB=W;p~T{N2NHm?wKJBAl}3PS~NcPKza7UpOe)i zQY!3M8VY?41QB|&(6|~KT7TC7CXG*44Gi5m!(qfk!T<Zcy!7{_!FqFB#NARb9}q5g zmJ58X8V9XBHX!2XLmYBhq0P~n&{1{5ku}{V!A%Rxaey%Hox2C(bWrEZ=OJQAp6%n) zQqA<~k{ntPWR{J`8ugAqd$}*3B4<Bi_VeMSuGKt4J!bSX4;FzjVM+Zjgo9C9s-Gj) z6BAoYn#y<>-P~Ia$&oC`1=5!dW8d1tUaOyN7CI~yb!Q+>t+1hC2}h4K<A+=f8>bv0 z_7VJzX{Y+y&)@My%Ya#lrRvRPZChwTd~8J(p8LA$O~TH5$phXtZkj~x$2*fVbT$U) zm2_o<zp_C(kVmU(?@;#p;Xci8Cet7*_XliBb-a_Cd7U)H55mj`N;Yp|GV3g7wKRup zXf^bslF{LDW&Rz0IMm3z8k>hg4ct^`zV(^Nf>)8YvU|)a{I<3R22s0GnHNp*p9N(f zJ#O^x$ig~$CtD?)-KpT;TP^n7l*o1cXfrLA*1gSrJM-C$2#(|yFaUBc8b?ViRS+a# z2*$23<x1~KnI$vT*}b6mYKvWq_`%fTHxEyIyQ^K9gFUZwzZj6klJ3JIsMCjGpJM~O zlHo?98^-zlFgJbM7B|Sum2A=yLvmk#xQJ4m?kBQ$v&3(;JI**u%Gi)UI5%G-4I$^k z&IwV`N69)3atD5F4@J9kor)Or;)5dY1zJ1@l6sy8uTKBRngNz=EFHuKab-6U%#tL} z@`9PWD|BbZ8CMhwhX46A`Ia?@V3b(09BquoefZh7{0Xx{v4X>{H|41ie}R+>5i8A4 zlKUhd3mZ<Xr;MY-%qQt?G#t8Jgg%!gi*WxMVztpX*K|9=bFk^h&J$j%q#q#*JuRdK zfKZc;GVFhyNg(4rY-pfm9P#4wK-or!{v&qr>)#M}X8(!3?zBjC>#t+@xT%L>8D4MN zex<Y~PxrW$uBf7^8_<;B8m9}Ir#NqSoLx7%@+xMwC#cPzBEN|mF_Rox1f?dwZs7KY z4|^0Rc^E=wdb|sCRQBK=IW^DIygWN~_4GmTHv9VM(5{oiy`Xg2D${<|rC1+qQ_^W> zQ2C-1EW$>hTEU&tbdA9JCf5O`=s*kx?k-(8%?X1+*>VBxR9hb{=+vH8qt@eK_bYUp z*LziyW3OC`49DZ(!s2q(-JrX*wR33z&9JMd>GRwx?ZP2L3AlIRI21y9a(Xsw!TC&S zVu?M(CZ!|J%#L;n{OqM@B+gUrAcG0zg4`exlw*(cw7tyxeC8`x>b}Ve;>JJiI>xP; zXOQ-;C1wASC$$M?1$Q#ze{8U#nE`Yu>wvvR8EipgZ*BQ-DmdHMUJelK56osAvw-ZX z(B9t*B&Gvo0XtBx_OClYGe&@Jv4j*XIC)bfK$Qh27|vLItDdV{^faj_tMDAKIE!KD zJZURWXLsqIt-C#Xmr4*NJTv`0A!dLD4a6IDPeCZwS??>ef(zV)D5)pAevhob>fwNb zuE&SjnK{Zsu?Q((<KYyS4NS$<nWeQo>L)cP*7@4icn;M0HRgXicHnv7jD!R_w5KQZ zwBvv!{*zlf9$*FFlW_KsmZDZ9`L^3Jk=D;i;q$bVtCU&>ad*;6=>xyut*BxbiXfGu zP?gNv-_`WJOR;$YIwpHV94$R|mn8y5FX9~<sn~vG)T*o#?8XZ1@pt?*<-mT4*WBcs z`uNcsW?m#J<ITGd(X1COBg*}yeen%on<hcdPtS-55``xvi0cg75)^@Hs_F3!wSnjW zKt$Y#Udf}H-mG%_g-EwG!r53^3k!O*b#$~1))P0PLsi;uTXGtfitkQaL2THMftuB6 zv*IEm8acm5{H`{&O$!@ZDSnF=$=%h&TBP17*=2peLCj#$bf#c=G}EE<$=p_fgk;CG z_#`@(dc1gzn?LqgL4Vz~5nQ6n&l~5Kz4?+YqylnlN2RW7alJgJ-*?zrDPB~nG6>yX zOJ=<=3qKnlVJ9Er1q6jT|B`WcnJq+bk|7><qsi{86f7ybJ;xp!>NZ`uIPSY!171+^ z%5RK2z=VjvQ}-|KqOX-33<gHVW9Bve*KgiHojU?j|D>AJJUF1&ble11dNc=%9U#mP zOpBB<VNt`@3jPn~?bXx>G_|!$ZwBI30<j_^BQ@03^HAhJ+@BB-OvT$d`xabFgr1*! zPA5^UNHma>kn{;iN=CWREXKyh79=Eqrz_wH_y=#7s`$vrU*xT00gdF8l#2WrikWiG zVi+tkhvcTee*G%xWFN2iBIgXp(unvi5c!*aQx!NuL<Efq`*nF)*<%fl>FQ)I_G_H? zttKWW&o--0v^($~5dEe|1Xe-`B<v6HNq3OvcRH`&!)Rt-U*M+PJghOmm+Th}KWU6% ze1b;wyY)3<Kfg2ZgEnfm*=3X1R$Fk8F+mcwNU*=UEc!yOx!?d!uZ|k8^P$Rv>>8?N zZy@UX+FQ-sl^;n>x2Sg)v-zh>DHnk>mM|h;GSm~==GbJU(xI;lPc^u9n=<o;bz(C7 zIBjm|-6!p@#$%CwavK>Jw@MZq<No0M!*it2(KXHW>FH<5?&w}8N01HqHz~yK@5`}l zck=NVIsE3vPY1VHSxb$J8C##LvYH<-V6C70iEzYPyUgUiF@$YcJ}W&{mPXg>a=A~3 zyUyVMUYGSkFpmE1N0{r6G@hRqSQWEh>S1QoCp^Kv0_aCnEuu@Um-G*lH@TNueZJT$ zQU&@TpUdskO93x?CfGhUc{mW4Q6-x$!ymk<mGN9^6;NQ!x_kNQjZ3%ja=Q@BdxjY~ zcGMQ+<Dty2NIE%=fSb_PG0WlY*o>mTRq)%hSX2iKFLp`6MvO>4t$z2_yB@K%s1dQm zA?M37n2Xm+c|DA7D8y>NBT(ivgp8o!ohJ<G07A!^GPep_AU>d(>Rf3dt0}W;in08a zpTQ?LPdYiV$~rm)7!_}9XnK2-Od}PmbW+nC5TB@4*XV&D*B6ZM&PjvDOmHP(lkx=$ zh5QXBz%+F&&50QUXu7qs+^t0)G|lEFhAg-goFSH)?YNC22-&$>o|_ej3oneX1xX3) z*d$f-%pP~M{noLzqR>CiSewDbhVoQ4e^hI6&nwRmQ%&(IKjp2fSk(Q-u_g<OyN6~M zDp9KsYX22R&cws>380@b^Si)68v)iHZ3DD1bsh(X+5V4hqpn*v(rve{E{;n&0}025 zKl6kG>#H<c1FxK{fNdV~UugRT624~_s~54DB-fF@mK;ddHY^T-P1w_f(mr8S(h#qa zZ|&*nQRLEql$Zhd8~G;2ePUu_ltv*lcPtrSlngrS{>CQei1#1>d`$i-UqKG=)qIGm zIM?U{xSg2!_%u%emZsSvZpDw*Ujyz>m1=WN+XE$Pb&YLxN(kx%dUJhQ>b~wx3ZsD# z%{co4a5xf}MX`RPJ+Jfc*V71K+Y;JL#T5;o{gPmh?!UAPGyTn{=f&>ISt6t`i)4Vb ztIoOv4(waPEHn80I-AcnMfh&h5w=b%cQG51f;%goLWrQsUG|R;+--K;utSVm*jLba zf8g2RYESg^p{VB{<F5<JVE~6tiQvJ?f$Jhc1f@=$6+We~>+5>Th%;Am%Kc@?a+_CD zW@dS>mRzex_Q|l^lnGwQqGzg)N3@j@wp6{FGW$cm6t;UrPK?+%^<ZAXXH)M4Frajg zHR)8sJvBnI<oEhN)pmucvPbU%i=m<2Q6*h0it*<gy0~P5ui>rPf=*us*skreX1>(p zZixqc|K7FLefUH}PpdPcy`?OGE~vDhm@K!u=1f6SL-rRT8U9+?SYLey1eMsP!*I>M zLGlodXu`YbkVl%W(I+t~DmuMGm-G8ud`CbsJ_$20lz7FV`Dx)pi%?AWVIVOae3)L1 zxe5cwd=Sfm3QrXVl4TE4*^W$Yc)8Ht>PW@?J_1GcVXxiisXn@*@n8-CmUFGn>%~7I z><Bnfg~cs>E;XQ?FD)3-J8w|CFQ?ishXq#35WB(y?X46>UN^}XwGw2{t%|*nalQx3 zA2;CUS<;45I2vg$OoCbr-IKhKCnv6a;WOjF|GYD3a->!1igG-ldO8LC)^KnL|BZ)m zHF$>#2w7T+%*fFf=<ZJ@W@m(|bYZjs`}+Z{YrT&hysXpR%cy3mhINV%P-#Y{h=gGy zDmBkgY^>g;%BwpyU_EU=+cts0s2Nvju&`9~+4AKyzwLy%KFF;&M&Mt^9&8ZPLd>Xq zdg)Q1i(N-^f}uGD4)N137wDCOvCM_tq!CLARMYTIUg*Djufc2Jacls@hsvKtC)gmA zeG5`sEqKe&C5laljRmr@X<=K)n&%ws5wGtFdV}7s4eittm!b8RNcBeYX;^PHUuPu{ zs1J2ByD*jC(g-(gvJT8$G}otl&*qt}TK!5~)TBaIYdc6j3zA@^8QTj|!M`CSBEryk z$MDVvi70=LEAx!>$0II$ibx671)ThzAu3UCrojBV-BP|ZfL;sU>Y+XJ1`S>jv6-I@ z=&yg<!9sm0nKJN{T_~zbq3k;1HCMuZRPu}JGx}?s{4pK2tEjElz>#ESt7jPKSgi#T zo|GSLv|mMV07hYdAl{MA;?vE;i5h4GWSxHhq1qTbgs8L^I35Pv`Gf2;=6}5_W6K#A z@W?_RIW|>NISc^PLRb|otMM6~a0GB`6`Dr|OkdXh7*!-IhMh-gfumG$uE+wY?GkuU zP>_V1cKu|fk+_?$ey+hUiXS9|`CX99GluQbI?5jbKFF_Q_BcpHrZ=8;<}grzIaA6& zBG|Gfk%+LT8f?BLe9}|>vrp9@?^N9*qcEk%4~n`I>H3ON5!_NBSp~wLNM8rW(I5r} z;!3nvF2f6(=S`l|oEJCswKOkOXgT<mM0t{uoLf(Ikd@5M674O|Cu$0!r#*`HR!oR5 zI*%4Qnd5QrLi(Ox)(=*5RTsM5+W7n|+v}^^t*CmDA>5&9Ur~0oBVq7a@l$5vw>dC& zj_G|Zw(%dxjy8M_rv);YCc5{V?i|1|w3jSZJuZ>S3&T*|S2CRDKvThw(nIzr5=QxW zuzm@fB?r8sp3}kdi^akpfKx2YnRBEBykVNE`npxMkB1*W>ncg#x{zSP{bNKRG}a6G z)e&KJjNq1IUg_eV4e&y4kedPT7i*YF2NW0x*Yx7WW+Zd$(FEY$)br=9h~rb+t)VwX z_;_}1f3D^y>$caBLs%6C-i)$`2c8*XhCMUV%AXH!D&BtK){h(p`nDxYz6uwP#HK5M zVp}ye5yax*aTIda4<52sXY1@u(SnJkIIA))PpK=$6w_1QGOP}hWdHW!NF`<AmI+dH zpgmb&@K7PVb>FqeP@ju=`tqZ5&B#O;AM3OqS<sk6@oAhft?SW{EesgEOX`;zq}1mx z@f*y}vcBFPAa$V(8oT9LyZ)DkWpOr+-@soEdq2dZR@`2z%x_zwK)s*NU|9wum-A-e zebo&u%|4qXuEwJrwOK7%ZJCS+uhZuAZ*Ei{a+K_v0X!F52remC*sHh<nR#7Ht2MGB zrlocWa-XXGa{Tw_<9BQ`Et}SR?oy_VSdVbjdybt)F*Sf(0`jW4K{jU|18Rqjc}9#b zaqoX0IsK1x*j&g=RW59sqDb(WSeXpU{2#Io=0LBU34lVpEa46~Y#<RHf{9sMuM#e( zp^LvREW=xZ0Sy~VT1@aR&#c}H=WEF;(@x${m$JgR-XRe`-l~Zh^S`{5>rW90koxe8 zYMYYdJKYbB$)7eE>|RG|?XCl8N(|^kw2Yl)u$kn0NufDxKg!FC`O%?mWfg>yux=sv zk7&;%*40~66Aite@gMXhOFeDTXCSm-%8b|J)sHlWi6s@Hk03RVcNE%xJovAo<G%`! z?6UvPhq#AL9(uh$anolDY1zbEWmGX5RLDcDSMR^{|AW1B4sh41$;%_kx<lx$k}lH! zfQT)YO+gEfCAV5PJZH2XjAjXs&`nHJH~@Pwzj*T%RIe*bi!`XMlSos}a<(cAE6BsO z2BRi7;SvUFK4Z{!qsiHq{28Z4{>N}ad?ML!<U!%%IFew~8gT+@LJFDx6n~HJA~f<- z?#>Zpq`HM__I>F&6m9geyZ^S6Uwj5{EDDblx^wk&UmKPW3php}Kkw(!6zVEJFJ*yy z`^m|?a_Yn}*^Qdb;xkOJ|A{g)t7HeXgL5?8AXmW9_xoNpB4NtEM+1KxS8KHkhi$A@ zTRX9i&+?ZV10lRNx+jV>4BOKK8#;5%^R5@qqbWw9yMD&idNO<85b`i_irGvqq=%$t z6s5Vma@Mv^&2K}ezf3XucJBmO&QT*-?98Xk5wr<DJAdC4<Dm!2beyIT3k!Z7Z-#$5 za0-V|DCiT^g{tWKUnUoM;g-vj?zW&%=7fX2xbNHi>i5;(Xi3JeN&E05xhazNMP{m5 zAx;HKUGd{L)-6iiKIeIeEA#pzkGo;;({s;SV4kmaQy`>@3s(~PCUfu9v%qz`H^tIH z&FrXMz%VDuxhTB6WhWJ@Xrwt#&F3WdNgwWHgR;1m-BhX(_jw}K(B^I8VrO2(1ELR( zf8ev(>TZ{MR4XgbIkG)HGz{rLr{*Gh7wFZeVrv;={ZDQGPVoUz`k5K99;tTi4qa+H zChL1?(DAYia`d|dBq5yy)<B1v>?rEuhlMdo!Zc}YbnvEgRo3MV>&+t+r$+=EPUEag z^SxW?*;zTMiezWsLIHYBpZ&8aqJxPII3v^Z=G57fID%rv+t{0<lklN*$Fc8<x=}gm zkwTQFD&{yEx)|oPO#1TW_>wTTea-ikY-<G|nbOQvRf8-Hs9I-<emC@0Uayf{>kL!b ztnyc2osFfyA?WXm7^4CS8uXUxCJVPbQ%+i*#7RA@*v{fuGnf=5m-II9#Q`b<Waq92 z)8Q5Ad`6T6`1Bi_1}bQ(44!6#Pz15mlf=Yr8{&^qg6cBvK-E+2G4`qqA>+DsrKjQ} zMz5n+nMR)qM+*!gs8Uok*4)~AujZN@W;)SF2x7uZ9J0dh(8TAbse~jdBKjCfG?#Td z!>OLS{ra!2-Sk1H6h;tC+yd7IPgroQ(Vv;?1TSVvt<GZ>&6*s?J)%inu#IPIVqH*M zG~p9+WoUy+@+nYUW~R;IurwG+_kY6R@kUd1l}j#2humoRyBhsFH9G0TlDuG@l%$;F zLGO)^!E7;nOK$q}3h7PEB+*cUkm2;?!QvQE+BYEc2lt~piVb?jp{pm<KvXpZ)JxIx zg@>C@es8}0#Ak`R%IJVDeNiX_u?bY_?<aB`W_{CL=20}gQiy+VQWinE?kNwB&WwD+ zyhl``r~8!;9jb&4)k<0JJ1iB6!`3u7k^{0Od=k$0>S{$J7CzLS$cO7jZ8@Lb^ezDU zJk{`pggZ-Si%oK%;_@f^=VjjDO)s_H%{L_kwert*y;fHQ`FpeMt*nIgKT-0i+UfPp zFW@Z~u1b}rN(Xm-*^TUHw#f!Eu#}3h)a7_(pqi89sLS1PyD_Mtr<Pju`d~d1$FtCO zur;x$!(|lKRohl;uPg0sdw%02`h2E5<)v6ysLJg^IL(yzUKQ@&=95N6{C+r%hN6is zdPZ2Zr#n$4yfb=Q`Nry3QPW%1AeD>=sjhkFf@(@CQU#|>A9a~`j9K9KjVucTFx|$z zxHUPj+~Vnhao_9V8(>wGOTP$#cIEQ-nR5N@Jj%Y}!LjY-#FzG8gz}ibD}Kwt_)=3= zP;z9-GCY`@7`;g$HO5k6B5=lcCSPAx!Q>PrLs{`VIY&bVdi4sDsU<b+{*uGn-y4?; zCSI37@|Q1T$9Ke%B5#}p^56QhBAO{aN`0C!tw)b3(v<*BF^^i5K~Hx(dlsAJNMxQV zB%AHG>PpH9dHbGrDc@SV7WCrVUJVG}!+!qjg>t%64c!c$Y(UzKLzQl4cQiwv!6N_o zAwA~%Nrs5&krO9vmcblhT7j3*4UmQuj9jIUS^3t0T{j-F2Ify|IWTd_3sCQ4j~hCi zFFJuTTbI<<U6u8h`RMYsCbf)&=kEX=zopgkrl{yx<!4UK>@z&|=b8kD{x{SXr0oY5 zA*snv<nWw2#kthG2lAWnXX$kD$u|s!pzP$cRT6b`LyRsx(GIggDwY|E-++vw)|g%^ ztZEST(X3L1Vf|^Qpm!C1{X4&pBL+T9PPMV2Lj+hy&ekilFXtc^eDd$EW(EOyO+<{| zrSiZPziz+8%}=U;=VLJ}!w)YW3cDPoEi~NV+g8xOGBSyBHy^s&aZRaQU1)=I^M&yy z&dl~@LzN?e0J6qGbv!IhXF_(*>+rjmtycXqa{>0SVUtXG^qh7`+OkYm&FwX2{LkGV z@7>}kUe={nVv=h%7ovDROrbZRy_9@3y5e<e&2VN{(xQq7vO;qtt$btr=pQ0ECW8JB z9m;ku+3w~kz{fP0V@f$idqpT|#nk#Vv7m`7UTIGr9`-g@sXdH;)UV-KFWw(TFibfc z{VI-PyS~VFrVI)Rjhyn?<1?JZ*A)+p%8hnKjA^xa?%czclzluv-2m3p)#?ToxWCq5 z3-_)jWRX|LV}*i4X7H90OL}q&K3M0~39cjtY5s3t^*-3@68*8Hl|f2B8G;%eio+un zcP1-QNVT^5NoP-UuXapc#@Ojmq=up1zzpXgIXWF0uah|kqOE-9(Jl+qYW-H4j(Kxc zn9);Q*QY+AAgTF!l=fnBmCA-?4_QemLyyEk@_1I$Xk0{0;Zcp6nO~W?d)EiP#B!sV zsXq!oacyB1QrKlOvVA3~{h_g(-%_2**zby*l*CFF)zjmSH*-nJGhqt}T%20wJ;D{7 z-&T&hQ-wJhl~bpvc=va9-%2896tl<rhq^41p;xWlrvfYHdLzRkZP_x<7N<-`>J^es zP4PcEth99$m&!qCazGN`sKEbj^Zx5OWtzlh$9B>LW9C`8Pq7!%kH>77r`qM@h^6uw z$>87~zBV09BJIPlb9M9_y2m*6)ig=6Py<vfRLpFXh1Pnc`tg8pRWeph+{gBTY0bCC zuDOsYJ=@A0+UwRyJpVtU@wn491p~Lf=cyWBdohEgV+2M;xoN+%-p6MtI-z{iK4F;< z%UH>owV=%C4tYDh%97Pr1d=L8Qw3-|7WOstZ3W(qcFDuED>`Jt4(n-!IX*I$)9Zk+ zxL}+^37CJGk$rryq!pJwf$Q69819dkAszP7G*hDfV4u!E^=-UchE$6k$C%<V*)xtE zF&Xc5Ktz%-U&otLg<{ksS)4LWe0T%61?aCN)70giii^>Hex=xy*y4UrFoLmr0mdhl z1%gNYNHe7QUd~Y0W%|{nvLA`A_om0x+@)iWtwvgQ`0@Dpqhjf-_+hV`NT95Y`o+C` z0T1B&hvZG7hYh!6{;oZo4`*WJ0a#^M(}pL4R6hSnAjK_pQ*nAiJ>)>b$^|11srjnV zonuN{w;SM;)N49+^!?p{&NXkQcwOE$vNm`sE$yj-wxQ5RfI;}X)?+En%H!kXZKGT7 zxU>D2ucxQ$DMMr?J=A-5BAPvFL#Kt<v_|stjLP1Ba~9G}B4#pyH9Td}lD6G9Wd`i@ zDu0&vp0}Ze<xs)0=;vz5Kk^dYIgk2_S;}byqDyFSkpquD{uQOzW#3~9X}C6vpbC&s zc;eUyUYNKHKGTm2Xbl}pS^Sh@CzRNe4K*?0&~gqA4o+SlS)x!*Jyxu5WHx9SAG#1w zyXkp{I&YuNDAAjGfJOy$lcyd$qWedxV}`GT+R|%r6ba@RIaZnUc-NiXq7@F-z?PB) zk#!@|q$o_sjKiO5U`zG{$-a~4B;&utWf>eT95uSZN#5XFc^|VpeAW>YQ>Q8;^R%GQ z-3%$`J}@#;n47Cc%$P1$(oSzRbAyxVMvn8PCSrto{!NjGLw?k%h<X2vp#hXFlm&UX zDwQP*rF%$Z0%JyZ14)}>CKD0{sZ{pKw?<T2;9FU%igo80GSd?LC6Vpm7wFK?SOo67 zUI8*ntRRV2+Oc+Rqn(*NflM){q>=2GaC=l}+_|#xpFdoL<Rr?M-0o_+*44K}Io;;? zp!F50T(ebJZf*<<>i}md2jRm&6rjg?)Rg&w|J(QukieuFE7OuPK!<)S!*J8XB;ta( z6H%A22iKH(UO?%1ht40j*l;np?kejWq7;vHUhLoNYpmdSh(S8T$Y~G+uH$$W8=LLA z%UL`aP+jMLJ(%Q=fYlJ%RI&*e^ey(t!@AtsYJi2L1m14|OMtZw0P{UiT3l9Q`VVFQ z2e5M2*E&<WM=Z5?iT?`N)$yNSB**#@FhWr$qDuZhNC5yiQ1=Fn2jTN>-{X_sBlE)| zbane5h}C2oAiI=*T!Iee$HJ|y_BnUN!smhksV<k5Ct;8Kj#5`;NAw8d@{4=h1&Jof zO@M?xO~RDR3&r-$&h6EHh^yxyR*iNFX|76x6?4Vf{uLKGbWhl@HC8)UML-hfN(_V~ z(pf(^R_%Oanb<jgDtC~^6a7O`N6}@#dhgyb;c5@j#7A}54@Ng4E`rbbWd185?K=N< z5p;|(tAD(Uy~60{>%VsSV1+vM_?k9I#eWbJ=q#~H6OE`7|NrDEcAxL8FYonopY>WT z9$zej(jZ$Zs$I&i)@{rfKlGt`RfME2h=o{6x@<-g$S%mSrCM<*<l5zi3->E;BS$Fb z7E%sPS6<Qte#X1PlkIiSH8{8pE&<4o!Jz=_3Zz>e2Db-E*N2YjRpYYcjwt}q<FKNI zy|5$54BrYZT9SdVN^^{bc!usT{6>I;9PLk6QU}$LlA>2D4M6g7^lp2In&R1L_K4jn zTa?{kg7Z3t*v~-Vy0JGg&^8~8V1B_9q~;Br@ezH&7J>rcZXr8sQ4IgM*Ys#~Sa-Fw zbSf|WkSMSQM264?OUe%5AZG*Ek%FV1P7&iPw#$vu&KL2;uSgU1cq!Hz!7$L#WQ_N4 zR*JInEXFE4Zk31_F2b1VqTs!?X{u}?Gfa|9`XKY!!?4A3y_rBVYLHYWX8#UdM;#Nj zsR|=m*<ae?3Pw|sv|1DX0PNBoe25t-aqs?r#1xR%98rRR!Sgdujd8|*2!u5nP)+mJ z*3!pZFHi@Q+2~-JvqD!{mK{l`-_Sk8iH*pu`eU`jgCHZAT`Z;2Eo5V9a0O_MLFdxO z-EBUw6(VeFWV$7+jwWM|$XrPk)<7^|s!~nhU92Uh2WAl;IP9}wsC%FXA}Y-pcZzhx zKN!uCHvHeS2y}n*6&ym$*Wv&as_%@-|7Qs#GM)il?+lR-bJqX+5g^!EN8cd|i2~>& zLk8rQdX5=2^fAr3teB#uy6DVcqiM}ab7o**Sno>34B7T%END!WT3oK+Vl#R0+=e)G zVO}1VSsp@0EHw|POH(X~q*ZK}6cajhfd!Tj8tr+ohF$%aYoH7d07*qhV_z`nTV}7f zOByu-Qkf+lVI*@>-&VKtA74!Ug;UVTvE6P7Nm7yB<hP2NKi>rcfa`V4)WZ>5qM84* zAOA5VZTOBu&R#kobM3<cgSMs^Jieu#j35;!q60?~PL8$7-gk<Ta7-!iPAuoDp6Gpc z5d5<LSVJp8dYY&pEObmBKa?y;Wzw!KLh@A3!de&>s#2;sW@C|*oiV)=&*KjMFG2X% zSEgqKOq#dJ;_r;ZL=?Q01%Q%Np-)SUhawVYn5Di8Tlo>~`sYyW=7m_uy%D@G)PmwM zwt<bEg5r*TKd=6{TRe4&6&HVu4ZTSYr>R5G2NM2$Ywq2r{TWY_WY8NP7TJ3Kx&fC= z5pXovP(aHqB)9H?q_}byp50wXBZ8p;oL-I&Jp(QxDs;@b1i$gn9q8U$HXiCdE-4Ec zc!uMz^OVJwh3O+~*(zHBxUh*;G#2&|hippO%D;cgugZWD0N8BG`Cr@}PD?6S|99mJ zEccsY97^SIFLC}3=!ah$&y_6}1CYdpKSR>7wmphqGD5<#opB&sq~KUn+|6MFB~vqf z;0PMiO&Y@q8tbLJmt<p&rUz~ToJt{0T{;HT9Y|93A88_1d+gez30W>7+iG^<1R>9# z7bUW@1$DFBxl_$Ug8$aZf8YJDG{%5Xk;MWGVz2U~CI9c2wEBcj1G4)Rj`Q-LC#(R3 zF_Q<7FWoIr-z(f@bSMSm_c3E9^3k9%@ILpl2`%r`FK)xK-~~DWGzOBo1iH5?e^hh$ zz(7uFWRiL~uc2vZ>O^`_2iEHXs?OGuFyf${0>tZ=^<b>h9MnCte<VF_Pk=bo$qDM* zOBWx`y`G!elC<n@B;Nv1AFl<^F@~D$ywd!YRnZ73Hagn6LMS8&fSO#!7k@qwlah$X zL^AYZ-u)ZFL+%DG_nzLL4G*ipqXSK^^+0rI&jSZN6(1`6-=+WC`C9d%1IFH$U+faR z{@&tzRy`X7`iqdD(4be2+2hcZ0f_H_SR2p^>Qir*@X+wd-vA-Ok8clLH%|J!kLeDN z9$86l1>#f!D&Aw@8`>1Z2~-)iBL_V%Tty_Up6Wk{jZ+BG92=yzfc<q_avp#kTVgC3 zV5B;v@*Q^M#8L*wn3&a;Sk!~$iY+OYk+=iF*<8F+MlqOQlsgRK9Yyic0G03f>C07t zAxNEm>}{+AIz8}yAPIAMZ;JDOLilfaUEm8OMT%CSHXC=(QRvWB4zV=x1u0l7O;UMq zn5;}AmgLseu6=IWoX$eM89Ak>Z<QkTuK4YB_EfWN$xfFtRa-pA@Ar*SH|XJEA0q;S zrM|x_`^39GGF`J((^3;+v#+VBYuE1943ffj)_FWYEG3E#)ofFq-s8wJ=&emwnbh>X zyV0~(?a~h;|FUs3Ka&I0!8?4--(Cai0><b5KQAiLN_Q{yaDT@!m*?*fjn$#o?Rs{W zhoG22^`WxNBo?bIhTS>*cC@_pM$s)X{%6yT%u^P5ltv#48E*jmU?};|g`1;po}@1m zvh}Dx@f`}ABVW#1FvV|r9IWReTw6R3(XP*1kzT`-8Xp^Ao>39IOB&OBI?8Tdw3@{0 zWZwrLx)>QgR0p>l^05qBrXJEgG*^YS*L*1{9e};k1?>w4sWgRmwjG8{{B^FBsR6Vg zb{k&62$9F)Vo3hS1_PVw2!H_@MJoP1-oHIOdR#z+RK}Ef+q4uc7Tz<+$Tk6(h(Tg0 zOBFP9sG@t$lTX6qC%W2`<MiQHqqD6$Wk_xRbDmRj!8G}(=&f%)_N6>4>(6DnJxqQ% zEUjy;d3e2SwdWia-}Dgj#}>qZ4(fuvmu0~=l`M=te67)&%07)?7A?sqi8zCG)0#wp zAz$Z||NRfF6|UmHdkY+fd>qx7Dt)a@QHqd7I*^+(o@r`NM@s6$r9rbrkDH(2hm&c? z=3f)=2}!bL!q5B_8DwL{Z@F$>+>r6y{JEyFjKlwmx8h-U>I;^Hb*_<ctet+2|3{z` z5a59pSM;0a&H8Chj<E~ryHpxpI{9W?JhR;#Q|3AbbZ9Z$XDsKNwDs)BzJ`X51P*W` z=3mC5aGuKe@O0!ZVGB{QskOKkL_~lF28RnKCJ4x~#L^q-g>E@YequU1e6zsLJK-`b zZYzs9JAe=4?O(CRr-DgIWv?-DO$GkInNs1M2RA5%__lA~zD>1Ta>h>-DQ7uGeu5-H zlK<=<Tt|4H$mi8esG$y4u8KZa<~OAs9do2Frq&XCNBarOr%F}tF;6aM8!0H6xEd_U zgMJ50IvP-I8R1ydjj^d@%8R1`h5DR_y#fJSHU3v^zY3r!r5@U(^r*hx!17(>2YluR z9&X8LbK7{1tyhW;v@QPXn=**kug!ITIXW!nY57LS+1`TuT{~v%q&mriE0x>b*Z!wn z&P@^r-0FPmQ4$34cz>n+xaLfbEG90JL0?CZh=fQb7VbBH+I+m-P3337`HoF-9KfGj zP4*oerXTw%?}|sDPN;JyV#QOFwl<>`snyM8h2*?8?W!oc*lMjtE9`Jn*3#B7uoa5O zeZ2C@XEL)AlHTSqzTL=lJxaXUyYyyU|02G-6;$hQeT~U8D9LeQ&N0(p<A$8wN~jf| z>pip?+~n+an2a+5e1`ulu%RwiHKOw`9Rgmu;s4PFlT`q=xOULfot&CfK~zo&4;DI> zeT%{;6ZsG_pJx`1r#89_VPX?m>N3C5oV1uQSn*4cl9C?2(o7~|%kfQ%qkd`AD|&m7 zOxT?}bnVk&akv52Ono*pGc~5}e~Ud^%2*L8141o|UnjSmR)GY)(41#S>`st<RD8Qh z$)N&lg|dV8>TbIs3ib7_sHsP&10(Ub>+9w$-a9e#uE+-~v${j;nF^IWv*zX_e6thf z9`H?q%?_udb1u~B=uMYsi|XQde><}vZ<(t3E(_tn)yB#Z<qn_mQzDYz<I(ZAWff(v z#SLNd&fL3i9iq_LL847gLdj7yN$?)++C@(%Qisa-s#gCtrs^?NQ}_Q4EtKx!5uTB3 zzNQ#tIPp;fKiY$^n0zknw*^6M?@(KC|9?RTmy#!on_grC{e}|d*2UHpnikU?3S#Xn z3#au~vE*XT$uf*+=4Kadl<#%8>&EYDR1H51`<HT*VdtKG77{!6L2G9|+{o{j+9>Ri zy46^fYpUQpnCg2$#TX&mZb@r>chT9FqWb2$-RhS{c9%)G2P^ydD>^AuM6_skLas-8 z7aEykqPPB|d$}JZLH;Bp>Z|jifW5%@;NrDb^=2F=|Kt<@%UXGAK6kY1KbD=mM>WQ^ zRW#YFPd-gGfJCWLi1tDTMRw3Mk%G75>xhLqZSBcx4-PXE<NAf0$y>1=hYb&bnkY>b zOzFtEj&&aIBY!G&Yt!N5{>@bKQ^yW8d!w?rS2C|kPNXl9EG8u%-ewAgBRXz<Se8+J z24G>IWwe%kgoA#qXBoAb_HpNJkFv<>bu0)A7n(K#xn|dLxJghUhaVqzatB+Lm{%9+ zNPBMJE7O+MCr&J>?^U!<kWJ_BQUWIY-yPg2Xj2#``#=$kA>IFZyZ>Y$@2KwJ_fuQ) zKrqHDO!xr<9fe1wWX7nN=i2~69r{$NuiTfmy3*t+esN1zu6yB-eKrOT8F2?m^(<_x zhw5Sz>!L%4kH0&CN3p|>DU4;Cb*@}-On%`L5>eW{8ZoR2+2oR@0L&22GNgc6r)742 zp&>LQt0);l?$g67k4TgUZrM;NO<gsczJ6!5_?;^LRJs?W;z2j}@GtN!kP{{RtM>Wn zv#Li2|8vmwL8quBV3cd*k`+z{sr|D@zKFf?SVG<#PIHOf$<qX*){L&Sn*B(|w;p)F zrGU|)DZYC0>qGxr<#ovAO66FI$Fj<<mgH8vE|WJG5)E?Ohc~%(_(T)6a&!K2V6OgC z_w}VZC)uYty0)vy1HWRKtWN(ue`Yb-kBhJz2Z&2Ciy4uF7|KYzs0$NWdhSDYPE~Na zuS_xrX={~L?SHXdVtulIAfA8Jf-R+)zcyNV^T%>W!0|;gA<1|!a>+u`Ch_zqm|V6f z->Y1qtXRpw5&M)>B&xjl)mZZ<xvg5f8^usBA(qZ=BO#BE1XNjKE!hp_{_#i7BS+|7 zgz(eJ&ar__>LRZ}gtmPv89_+OdL}fTW%P|(<;$6fAAq$dvd!Ud(7R7-MgI^K_4nNm zi2rx31hz~*eGreTtvgi62WlmAp*3&eO?_sIp5i1FdU)Z>5X`asZ5L+7UAFRDM~u_G z$?WH&X?^yiqmv8Bl_TLe^DL%nb#9_Vg(>rbp5OMgQ;Ua00)LziAF=Tc=hf7x#e37| z<<6N}Mnq+E+uo0~J<8PQSDl%Evf-tnsaqiOy=j)(2D>PvTS>%-=v9HpidKG^Cz<_n z`KHNxjhGrN8+M~%m!Rn@?q<GhK7&<<tKN1SDn$U2r98XXjuO<h*QpKi&KJ_XYwy)k z3wIhm#K5S%$FI5XVJ@qidEx43l#*^KtNW;P2@)Bv9d%UpEXA0Wl|=-_+E>(2n8Gr5 z8CUO@G2H8-j&Fm|Fl_PIbX|{Wes)ca4GllrKYQ&l>OWbol)~+|G+7;cfU|qv<@wdq zYIseuL;MOZ4p_d!4T|}{Q#GI>0p{8|@u4odL*Y_aLXzgexI3JcgWb@foPKV>XGuZ9 z7hPJE(w<Tp&t;|c=ghC3jJC^jSEsq1J-*)b_m$iCVNzxiyByHB=`07(Eyy7S!oerq zpV^kK=&=QYDX+b+`YkW^ubv7wIpAF%Hg>AG0r|jkjKN8&oL}B*i>qRR`1N(R&Gr5| z=VS?y!^B<s(1D5Lv8VO1s42Ab?71`u^;5L5=5=jaqNmhlrLt(zK(=t!8|t=8{{5BK zjHqZV&36X=3qdrlC%>C{&2NU~L3*BC;Sb|G5}ijf^JlZKC>fr46fEAZsb-;9=2B#R zIGFLd*ZOTBkA%vsw7~cAhdu6&mXbuoRlga~+~EVIGcE5B8<vE~jy*?<w)y)P`-*+P zXwdVsxk)^oO+yc|V5qq%6&r{^TB-aOWp8>ZIUQZrQx@oB4FWjxYm4G0H{7VJJTkL* zA<L&pZX1LURHV+O>#%qjyA&=krT}oXb4(TN{lqt8BbLhCRGvk&>l%CGj4Fri&<z|9 z6~?camFANOj==nH6dR9xjdMTuEy@#j8sBU$AcgvMZ_^^%#HBMY>DLG36|4rs_*{XG z+p;s_cH(769y&BpE<7gFXbfmG9u@Lr*ZW0w0y9K9=ulJIsriojcQ)~c1DP$~ocU_^ zOiY?fMx3!pRz^4<^pzOs)jBuVjM$VnR-ElbXLoL8+*Oc$I+k7qhZU7xYvcUMj-k}w z`ng;=bd9f2EU@H^XJGnki=0!;x>w=ajBz;UCOCXN%V5#UzZX}@Wu4a=B|;o><^eQD z)*e=DBNh>>dPvs~v!i&vUyC}9sn+PL*78cE+VZ?^#bB&dK`O7p{@G9Z{IyEA@ssZk zVQ7tnau8}4{I<bD64qo)qOE{=p?D~MGxG~K3wb-Qy|VGhgj^Q`DqOjflPdmM4sY^+ z>ZV$LJZLP|Ry>y^%kTTp^>^|xpOR#Uj#sQJR|2}_eqxS&+$@}&>R#nis?{&jeZEyC zU4-}DD2aL(eLn4v(qVzOS&Op!9~#N?ZYTaAx03`5TaPI6`k|yyC&BBkOlGW+L^D4V zzy!AU&5A~f`dvsSOHyx@`tbY|yZV&#Xw52eH94>GT1Q#<TCpWzk-2ZQWzbpfx=t94 zy}}_micwFK|0*R01ZdxdEwjHL(gS4_NtqvGQIA~Z-xSPalZ^M9g<I_wWvi$;Tc!h$ zCE?_hg(Twt>;mvg>X@tRH6Oiv{8awQTxMIyK$P>qh_E!==S&kZ#en!XO3uSvGZbj= zT5s?Pl(>0~3(Kt^NaKIN(dZ7R^J800{mAgP$%a_};;q)MDJ3tHV8kYn(tRn9+(`i1 z<=9UecNE<n#5I~5QzNi9N2}cixq;3@`~tO4ede;yn%AjwEyoM9<=CB2%X#9%;s}k* zAeGEPmu+U#lOcMpo2n_k9}3qBh$bV4iBzKxxp|n#w8dD1@gNmuBLmF>^bR^5BGwXu znR35y=i1TYQ^yA9J;D&-O>G9JyC7{P=U1J_touT0wZVGh4gPKofLHx>-CF*}a28^| zUH^jaE^Ejk!nblyJ-nNs=ognyK1?;Ck(QD|(>x@R^YRkPkRa-@H4ds{ecoCCOeqM6 ziDm6wN$ch;#zsfy0n31Unj5s1pJBg^SQk(UsXS?<uOol*B=75hC-HJ1*7#Mh=Zn@4 zcQ{aiD2E#9b2+2!%9ve=&>ufucl0+D-?)dk)1TXwyyz$<9=V&b0O8?TB8%|v#x#;U z2Dnmg29VM@z?YlpjpRu*u$t+P{E}!SCnJ-09wrz$x~2w(Djr`bCEhg1v`(yGxtrWg zSy)^k@LXFZ*|rjWUIKv98?4#<{Abff+b%TH!1#!U{{y(a!YK(75lCvVzrEaUs0xz? zlNBN0kpmaPx`W6+<v`X<?1iESv9mK5x>zdtJpN+vvSB>DR10>#K@cLpUc13zOuOxC z&ZY7Em7AdV*1hXr+TxQL8#U}8Df&FK?G#LZ1umyYHl4Ct&1adL%r3VaEj#HM)<c>u z+dX~$_ALnlFD-Sg<AcUP9pU!HKFG8=Pb{oQW+oMis<<H_LL?&PK0Kxr0CT=-N-1$V zun!y`x_f2uHExFsKuC{Jx-A=IwLcKTV|UTvo~1pe2>d_-fI@}h^=vw*@1Ar}+TCHO ziTTJl(4n!f!h^~K0(8PDcs~%XSLGaC&8rhqQ5CbvZ&+k+dNn`c@DW6!%=ZS`pJWPs zev@5!t5Se*c)+Z1;3-i~9;C9yU>EtKJLr1*{NsZ<$gy{_+)V_AT6LC}F;Zz^n3xUT z1%&%^q2AilVRf3?I#<J&$>%PEVewlhk$EioARi??*Tu%9oD-J@0`HA(uF4+SxdqFw z;BXq()L9YXiAgD#l9K_dYeLWYYl178h;LHO;}-KZxvR_T_1g*&|8ctkDOlZcFSc^} z0vTntmlgH?T#agCKk&IIEq?%T_yQ<p0@>|DNKEI#8R^AykXvKJjvjTxoj=Mc2V1#I zZYc`DWq^TzJOCh_L2@YZwHeW}y9LZhl)xp#evD>|XOCoyR{}8ToDYblX4k`2*=Ydb zELUqWoNMZ?%XBv%kzhSp$owt7j`b5?AQrFzSZhsZft{K85x}6?L!`|V_&ZC~2L^*3 z;`!bn{xmtQB?Ty<^mTN$91dS?Qj2=m5xBo;y|}oDt>{|`qJI^W^ZWOEzN~d$ERCn! z??J#$W5s%(UqMY&ptny29cfN?rla4#Gq<$FEK~&+g06H-b^7~H+iPiQ0b>ds0LQgP zJ-`P&A5g#8?&Be^tgPL4w>9{_Z6m-Y&FgpNIgJ3OIqqgA?lI2F3;8R55yY=@U&my= zuunMr@>v(z<)MC%aOL>Ia)e?UF`n1k<*VNKt6c1}j$xH?;Hodc&FP?<&Y%?1E3o7k zY=gWbQJ)BFlwa@r3@kA&amNGJEwd}s_OR<Wyro<Lz}kHJ<jX9EHSZr$ig>Ej@3Xfz zuR0zqzk6@6Yv+JO=n=O3tI4jugb)mp2HPyPY8M}}<&Mp;fUf&G4WVgsf&;_-h1BAn z**h}t-xG?Ses|_~A^UAgd*Yw%_i^<J!S?S%Ghlz|bvUAWwtzT95B>uwzboB&d3;-P zb?bK_q*rUhpxEY&CHki;8`jiEeT}n!v5>aaemde~J#w|HarMBXW_?UJ%YSt)^x9wa z=&AB64^=I`gNt58s>?}Ybf|aH6Ja+f0`hQTDzp*UVDc5f;o`m4$Ahfh-bidJTdI*X zz{X0&TSFE)RCU4A<`9yQtitq?4Iil4|Jzd9{@J^(p_ibjNyMOGpelbR?{`>U6SFs+ ztkIRNmnBnKUaxK!o0%<eA*(>Fl%XJ3^bq$cc+AIRpP)m(F)L|8C{!s^L}`Ar7M6}| zFCqQBZ5H9<p_uEH3h2FIM~ge;fMb*e;Jcq0AMPOhb`UGDJO@R<9h%pPr&r>f4><a? zv;5aDUM7S+dU3G4S5#?p53mEj*p3m<tHeVDF86_$%@osV5Z4Ag!^0Qnrw?+u-KW&j zc&Jr?dN+>$0H`aiu1bB*N|1Y%T2Q!0RoTb@w6VSQIf3K>c2_HhuEA3~rZ~Np2Dj~i zTDcV!LgA0Ko>;VftC8;X(#atUaeUU4=bKi$Sz@VECvfSfqs01#SH)|SD7-=dsavan z{GfMvlR>rSGZWN9EV;!M?`jage80UsReK~&PIU6TQH?kQvC`gUdjY)tGr+>u+UY8E z;3nGQ=TVx1*!Et86c4uc?e1^9HL99(B<-{<1@Z9=-QbW4rA+iGn%b6IT6dAHg<ihs z2#egBb|k$G=~p-#wV`!Am|Yz9J~<t>p*;cngw+_zwVxgo>K|w#cC`V#BoPUjFvf5f zWV_r~`jj4i0lDw@@IKP1pz#}~Z%6<Y9zwyWHymgh3~ukY3|y{rcNlg0$-hp?$SB># z{!ycxEhZ0R%B_%4iYO>3P_?!$Ca`=s;aqT~Ll^XR+kpn73RuOhl7g{*4ov-djeR=H zhf82N09)m!Jqbnv>pslY+iG_RJyHWKOwh_m=F3l40W`o|Mo)WE5~2C^t2hV14N`ag z4kx=i)`)4W;xbmM`vdDhC3II)J8i&VRq3%&yk2zneK22fqREDM)9RdwtC3*FDrNSI z;x%ov4nwkw&H~Xdznq{i($3{c?hlxaNd{|=;52uG8&MK9mZMpBQ(+^6^}5efLS2A8 zlR3DRHDv6$_P{7e5M(>&ZQ<Y{j%zDjR;v5)(c{J!fkEuwy|zoMaNloPAra10{T?43 zd)qJ9V|n+5mkWqL!&hl$Hy6_NT7GPP%slv|UrKX==Wjx?*FG^NCVto`uG(9XdD4}A zlNc8FTH8r0^-%nLo6`zlixGLGtAHndLSvpODt%X$1v2ZWE8^i};Roe8L=H6xNx?wd z6qW6dRA4B(jL*I$IR$vpa<_g_L$q~C>*v33eb!!4bD9_Vb&)FPKPW!)d@TOvcy=He zg3+4LnQhRo0RFgjGJib9GtTLMCA$8~FpI?RxUW;~K<qlr0|yQnjAe0<PpufxYyJ?E z`69AH-?cXC_H@(_mU6VorY}^l&(^zg{1s_65F=#M@aw?eD~bKZ7i}+WtRq-{B!bnn z75BwrW5rPW#p0C1)>A-G5)JH!+YXi1xo?VBmXzU>4<~Xj{gg0BYON2>fOxpK{)0Jf z(#=Kd!O^Hrs?}bSt%l~Rp5fBa!CX}ohebAY=z)=b#*0}6=eT384`T7?ip(Hj4D07G z<e)+}N6X`nDq9pTHiKUv8#WTL@xvNL1WaRzsypzlF2l)_HzK>*eU2U7DmAl)e_v>4 zX`_+q*(_56L66Wtng`xv%b`}xFrE$ggD%oLJFy2~IbGOSNhe#TE`I{bk0S~5Y5qy$ zN2@wL>F7ltUNH)ZJo-tGzSI}GjJtQP<K5?xv_70Xc-zCVmdSY^i4aJrNE7Y@dU$Lh zWS2$9*&^;CeD|D{p3<@ihY1(C7jg50>vx7pgw&9II>79fo1(`C;|>ewqPybyN=}~Z z=~RI}XVk_DJC4An>GVWxTN4Zb$X_QO>x}wjuE6eM1lyKIls@WemSYXT?&4xQ0I<06 z&VSEu%>Gfh+_Z*cI+Wtx9s6t)2;iDx8l9c>0{PfNqPQN+x=sg#c9u42-QE-~x)_la z=+m#Hh;>*Q^?t@hti5uehtuSEHF191biQ~BOn*1;*d6#@0|S~YRp|eySZzt51CUg{ z({H<U=*ikufPlO13<<d+uUC#mB;vH61F3yraQKA(&4y~0@rIZatfcHGN&0uq+Y%Pm zZOoOHV51W*5mGBjScIILAN(~&Yj@i%u5=EwQ>Q6*wjNW-@zy$H=}Mlqk*Ag&E(LcI z)y+P&8-cQ8YU!EV^<wATTjiU5fQ!Q67Wx0s^&QY~eqX!MLI_4pbb^QyLi9EaB0=;L zJ%|#$jm|{xErdiLy#$HgC6SQmy^L<6ccTu*ef`V5-!J*@I%{QFi!kSX&)MbK&wjQf zdsW#-^tWBuzJ<8Q_t&+&Vg-*cmv7}gv};tG@K+hQL@|vQKdDr_?_Mw%@gn}<S!Q$f z8p@iIhAMCPyXD0SscF%rsiW@n2z5a46BGXEF0$rh9?qp2k+p>h!=IfOOs7X$j7Q5E z5(-xVOCpT>LORRZDHAQ@)9IR^Qj_JQwI9n0Hdw`@xe7T|j0CBB!z9>$7NXv~al*|A zQlCF)UJdH;0uVsQO5BvWedB26nwc#tfO-wc2YB{*WzNSq8<G(vnsW|IHT)X~LXZ~& z^;&=?K*sNLQ%$zmnvns?Wqv#3<oRrXMT)BI+B6SApFo*2RR}HLKs1%!XJAldNZJSQ zAArR-4dYNQ3ss*t>hv`rsI3c!URP>uxN*~FE&U9!0x$o5sWuR7p2^J_j!a(qRgub8 z0toxV-7Pc1&!N@a9GDPApgvd^VPj5_`YO*hDo!oGShe-_Nt4KAtp}81C*!1a=dz!4 zt#rLwJC{Jzb9?3q>#!oXh^($`@vFPyer!^wgD|em%&LdFV%7bb@H^T5Q6x;0d>ULf zvNoNm5)aCqz)@-{n){aapD#{;xkXjH8$U~_|5aK1`H8Iipg>+*PiF|NCY`^`hv`X+ zEgWb{b8Ou-<oQkH776xTp`19^8mYZ7%|_p0hbp=JLbmOvXJ19AzG8$%nmAwHOA~Tx zQ=TfM3<;aOcRG0t^s6+H_3Kgu$w5k5YJSo>d_;Lg#Y7!tt$diaQ}M=#c}&l>=pub% z)4EgkktKV6_uuJGa*Fc2-P&?&<>y86>4CVG)NTRP7~6H;8&n!@IWu3&7``z5Sq{H} zm3<4K8}kixNsj|ewfLNb@;-_|YA_(JK+C&etq)iV<FT{(fV0=fN*m`hd4QOLn*08( zXM($(hRG58C}O7J#u1KW4$WWdwqu$+AwFTZ^!jRE5X>%r_)3m^%`*CSQb})>uPN3X zx%BH3$d)SP_~<L7xLONYnW5^BPh9=N5L);vjfG%a4d9&2Rz#ZAA?_*j&B{<-pHhHm z1E^#Fg`GTCd!yj1%ybrVTnW=3YlY#oKi>s~MGfwDUiqYwI0Zz<IT|i=5tKhH%5NY@ zd+a72y-uD?H%baNk^{ucwZG;zt&qTl>5evL6>qWG=~E)qBO)Tycb4Lr+^DoSTmc5g zev;7Px5sL4U(6@_pm@i3=W4ZF37bRewPXV>osPOb`Icn04o_x!a%PG_H5(Hr9O{n? z9ty<WTWZGj6XA~LQ`af+I2%3e!^bnu&CeH2^Bbe2m9~$lvTC~eB8zBQCMqS2Tlsax zOdXn%*xK|qwEvxU4_I`xY7=cBJ=ZNoV^|=R>T~j4vpill8q6+%n<EX86aMqv+5Gqq z>28GUqNTV{@4^5v*_7qm$n23gGl^EOaOuLDNz0yoUQf%Xb}ea>Dds&&e}X`)0JXo@ zK<zJWe}cC)$@DmGvEk$ek56}uC)jkR9^k<BZAxEbeclg6Jcb2nAHBkjltA+d`~P-r zd!8no>T|@f0OEH%F$h?qEb2ME4gooL+W@Y?S{hYP$)|<f3ADlVso|&7bP~V-Y%xmr zY?%%S0zX;1+g`8~93*G6&OlmRb{M|f(&^v(7{v!Nd+mq)F99&um+>@}=f2Cgt!Axn z5J5yn;Qpq*`I~+|-XrAzBhO4f0rvUph1udF<(=I}H`H~%@qM~ee+$#p{zCW)`s>o~ zN8Qy4|J|Zkx}q_A+;8*yH&ge^PXli>-~6+JRvvT%TtJ!*l^{+(d+z2bPwDd#Y1e&_ z2iTO~nbJ=s^e%Mt30hxO<{W<YU&lDj+ocR~$4eap<D`7S;4fSphvH3Re9vXq5b<lF zMcFn-=X7ffx6OLqnGHSjyO<KNe;l4xDnK2y!p(2xCOlT6#WU->D<x?Dogmc;7=t6D z20o|Pxv4;x0^tKvEh>M+H`aGlhOc{V&$L7;jKB1FP#He>i!2IQM?>5UUZka}yvf}| zCr%UnnI1g+U&@~_W5E{-%hGWM)5jThsn;M{_b>4>`K;Ql(5KCGt5{B-LADu+6h*y- zHJIQRquv?PrtN8~3=I2PtLg^rfytTBFyV?sF_j6Fs^Ko>o!{o(!&1g|;qh%jVX?G| zOYAmdbOUaT78d{qf4Ek8cfq0RMXh_eX}2AjU1vYv+Cmmz-$4`OrYoT5H43ywtWGPu zou9_MK%J*c+g|7hI<GzD;_WVimg%YMSbDdfBb6_Kn)mLfo>Mm~f1pu|*l}XsPeegp zVd%5m`HPE2jmV7rfnm>E_ZVBf7F+b=cXs!ND9%J<R%`4H+R+S0xdHy?BEE~{L_H4~ zkJf#TT-Sc?3z~1qPvKEx0#u3w;`h>4_eak@e}_^caaI*`DsXhD-UXP(GZVZk6Z_lw z$;{&Zu6<5&pceu^5I-8Espphk`>JNDM-9=r*_B<Xdl^g^@016YMicz_{{2PLsE@8L zwG?^{rWFu?YC1c%ITc`KCO9H~sjmJAFC5|5GZSIdjlO?HFgPRoF}VN3<4ni)%&wNy zz0P!};idtPgBoYhRrB|8*|OrWhao0Q{v!Zs)nQSR?*WB{Ip=7x{72$7_0%=3;o42l zT9W;uCt-gvCTd>)i<H@fmAW5`4AXRomjUFS)=TwKV{xXXt@3Bycamjlno6TgbBW!6 zN_*-kyB0)~(Aa-llaP>P>Ffh-w-_wNd@{DNK_K;@Os_?WT_MZF>Nd*~`B+?QT5H>< z)jPKUSuXXJ#7kQ#7S0bVChaQaddcrT_nab=2RRkC<D-6qeLWqD{As4gtmXqd9{A6& zWy%X@F%6&VXd&IL+tHbNB~=jI5B<#MeM1yQr3CBY{icRm%DZitjK>zN;}D2_&FQgI zI{Odu)uCYD-tEo`fUn|I{o--Y$4)JfE!)w?<Am9E({kYuZ*T7saaD2_>;+;w-Mcii z7=!p=`VrdzS;KHOuzo>Eu8&~~rfPKD+%j9Y<+w)9*|wZ)^V($$3g9jXv{^O)^~gVH z*EaLGJV3}29JQd9l6js#@?!*F5*N<iShO}%bd?_tL#t;R1)xW=hZ^0Enhh$eor&00 zT3n;joTWW}gMiFu1mYWLg`>bMEc|w-dDoHlu~p2({ga(p<z2s)*f5h5iqykCHJE86 z<cCg6)ssa~NT(;AJf1bF2K1d<E>o`B9rwvUKGBUp54C?&RkQz1Rjrf7#tAPRDc(<d z_g>S(bmqJDXNl7xiu+pQAGFx+$=E%8mAC?QbaB@s#Z5l??95aJqOwOd7<+&^MN)() zg#>&0>##3w;f=G8%iF6CX>5sfT<)nz`7b?~+V)_VtXW|awNFMURy#9rbEjTK<H55S zUjpP3xy$gF{9+qDKFLZj?2IwGaG*J>DgW3x;*}|i>utG(=VnY6P?Q5kGM)T@6zhNx zsrLt3!e)PF7eSd?wl&*!zrY2<LMvI|ELInh7UA!jy?mkXXja?aBJ%3Vr#m@3T%RHK z7M0ksJtBV-o9f@JPVm|nVN$o~d!0GfDr|o(Q76*_0{Bw@pSQLF3gJ>tV1Y9R4F;#D zj1+w2Ti0v%Yf$9{pZ!wf<lZ#TTz`5aFy%gof&z{Ux9EyHYT^bUIQTL*xbQoc-cSK; zvl$IQhhxNS5d+Ce!z{U3@q2vZgP$CwSwvlrfew=ni&;C?i2o(@)ccRC-JRl6BggPM zkR>uMf42*5KVpU<PlF8?8Y2gCPea(a9&(!Rv-?_^k@65TgoCmwV`A0vqpHYDUlL^y zFQ0Ho32=o$9E7>JemV{N_^+pe&-G3HKkwh_t)AS6xOGUTml``A5vc7KvP#CLmKj>X z-b#4p=@U6>&WixT;F6IElhc^-E~+yenE#i3n-3_w8JvOErmAm6yWmYI0WWfTj1^b> zFTO>ej7-)&vEufYiiaUS6d{b$fXHz`Yn=Tt1btK)Ap7juGe!ybjZscCDw`=UC`ce) zQ2vdK9Sbur5`f~r=MJ8?><sr*wkV~0ZY@W?SAJ!EM@Q(QWSQyy_uBU!G<?;91|V_w zltrI5x9r5^Z{tv?7jhAphxixw5;*3nU)BXotn^<@6<QhknTCZadM6W+EZrfy{D^-> z*TppRrJR5DVdwKrCK1+7l^j5}>S2nte>*Sov<`!eMo}Xh%d?9BbIy1IBaXErfY8wV zF9ux-*AEn8B3YXq=jfkGIKael=Cbs*HRxtR!SHpbRcaJ4Fz(&m!@Wfn>h52!$gl}& zOt3R0IEJb$m2JL-+~SpJw431dxh#LDZN#8-+%1re3m4cf3~GV&!QD*dC`-#jOP^j~ z{grdtOaNKbU?-G<7fIJeT<|YoTN+!>)b}y_{h~oMsor2H%6hXWO7Pkq5r_ip7*bJJ z!9OHLfk`%RWB}GKr>CTnDbJNu=;XI4OLEu?P-#F;F>gUb1aq3010g#bo4TVTum8a> z1E4*}k~<0%To<0m%3B42K#Ds}I+#4B0oFa5!G-TxxwKMPfRfTZVg~7;CnQYY#$AGK zfDTP}QIzOA?a%|!3WplznznvY3%lHdDS$X#)%3oy^7inbWnzHO1O7K<g&~x=W@t_Q zU-a({HV^~QxGdSF{~|%pnvw0A>L8;`2HfC+pLxcA+1&wLT2RPM{BTX@2ShltxNsm{ z>*(`)&d$zOr1?sT330iI+A?TvG9DtdlK%R&Z=%3JSAV=aTbJ@4D=R=i(nubM-B9TI zuHmA=!T|P2j$@d7ZfR**ZljiNrH775f4={WlP$%WyR4M+0S_>$OG*E>!5m=eKQnSU zE-O%S$%eqk3lRTH4iqS?_;1SN#WxlS^lwFfSZZCw(|A|e3!lRO+;F-Ep_P^&4PSw^ ze|1i>O59JEA<~a31nwRZQ^_LjOf$d2Nb#xT&twJk5?kQ>o9rb(Ze-R4i>G{L;kj9^ z2*|0D$(bd79(DWeuJm+U5omzu3>xgd;hE!|79DrH;M9gLG+Zc;cJ%Y6&RDIsy5{qO z<>Qt07^d3JuC9@9KkA3yEV8cme!kS!bWSAK)a2x^1vAX0%Wls3#sXtFE{i`OSu+4d zs145_5b<kr8TBhI9ffDrgphLinp*Wp3bSJBa0Ap!hJUgVuS+ow*CgdseQNKsk@f;F zOBcR7^xB)AQkc5v3k(Ct9yZ?zigFrby6eoiDEZAh7<F?33n{Ly!_r=ICc?-}VEO8= z2K^h@z;I{gQ5;oGzxU+9K5T++@*1y&jCnrVuIO%5%u&-S&0kG**fY34Xg&ItRO5Ee zTlzBkg9e$IpxLl|TI+Aa!=pNf!%nxWcrGqONATv3EK<_|05?uvd<<K8WwrL4=TBB8 zFrShct1NV|$-~sp?;wSj7dbl;94ybgaC>LHpVESOorjOiUx5M3?+ZRMFD@xK&jsi} z_;MDA%HzdukQSUjk~Qi)@3|8+*z|;J>F-~D8yghT6caL8tgfU=;2BBu`$>Qc6&0l( ze$}(;ax&JiAyOs`v8BDSvf35OyR_IjN}5%l(&vhlF+aIx=nz2x%Ww5P`LK*YE7^fU z>Y!nhus?6g4Xk^5fQVT{-RbCF9nhjfof~rjbTbpDinyN6F&+?M?U_eL^HYwR?%g1b z!C$=<h{F1JV^Nw6pcN_)+St7$z=fk5{+Bl6h0Ghc@cXF=3(Om1Iy2Qm{&&TgvpR{N z<xh-fSe2y<9SJ*&H^*!V34C3k(bV+SjIRmgy(Ok!w7wPD!Rrz`)nK-NugT??fX^`# zEN?)1dP(C){;#ApL+}Mn>)9|-jn9};{9zsK5YWA_q-la}^s9bsDD?6VPTAU1+>d~a zYXJu=rH%lA6pVQz@S+8vdG((KdE5hWL>F4VK(QvUjPRWqzCd%NjPOgjGpQl|+oZT~ zhV@%G{&;^F-@wl66P8ciF)pi>_Dg&%s7!M`YVq0OWKZ}rqmGF?2T`TPpT7HMRY;-L z>*sJDtj;{c@E7aqXlZK8gnHlED69Hfv#<baI;(RUPX6$C-6<ZE1*m`h0E^N%$!tc} z&b@A3Uz`UkIq|{X>9(Knj*X7;**kn1(tQiBC4!XasuRc={>u64_!rgd4t52wPj=4v zRQTe-+fus#V6zimCw`MRnL6GwiM|Q$T{WYB39JQ4V58;0M!C2y+Z^RU8_Q4VHU2av zH?XO(4GuXpS{NxQ?RkUcAe^G_!9d&J=TNO;ztgT;E@oEI5rv;3?h;1F6lAd$X+dnk z21h>yN=s70C+cX(k>4J$bn345ld47+h*31F8`Z@2n|{m@naI*u>TjFPTkl+-eLVs9 zLwrJE6K_!|PpCdC*gij4Q&AP070i=RPz+0WDEJ;VT~c>U&LE`)*>x0Otu0>?kXWYn zT78eG<|{bfPy^zb6y7la6Eu#DP_RHI6*oMJ7l*E^0wT}J(ECYSW2zIo;+0SudU}<- zVf^7t&xiTNYc}YOp30K^;+Ud1J`J5(<6B@$sr%w@0VQIYqPXn*=bJ!zGtYWD^g)df z5m31z*C_Vmr5{iqhqwtmb|+8y`$3h501XF-1_lA6&gK+i5^`Dm{~0LA__DnO7-W^! zpb&HyQRAOiJO>H5uu;jCXFLS4m2w~ir*uq!SI-SD&iav|5q7~`Q_1Fl$^vkkm-@Ra zOckcPX)Vc#A<wsBS8gf5Wcd5i8dR$}G2%Jvw;)cA_O}YjscUzMAYeqAL!E<hi5dt| zt_R*gR5L9b|IBR&QSBC3Yt&LK0VrGO+qBOje;JSYc)#ZV!G6D*eTc$+#p`nCw;aU& zu>8EDa2IARkSXCCSh~%FTmI3C--c@0=s`|U(7olXio(K_?rtScfef3?x_fA^4zp56 z8GGApJv(D3q9Ap%A_cfPJ7o&QaMR{nz#r5c_gC0q%r(VBz*MShHtPQie*=Y|0khwP zy(d47{P~CnyMZoMTgDuq)kNzF`^cD{I~>;Po1to*Is&P?A@2C}-sM?=92<6j$P2p5 zH98y}<DkRZc-4_WWIq!iYDi;R$z0<J+d$;;=_J2Xf9(RV!u4U<q|EG>v>}9eI?aF4 zl`VU{_Wl=-3Nc(t0xWdC=AeKJ_neasZbOytyDvhdqovDUti9%FZcTA@RR9_E?yOHe zd0@y^zN|)|GcwK)T4UyR9~YiFsZ{o9=%qKqAKf_DzRrUDx(yy&)&tBRVnjFBD9HOH z6D@J5!Zu|wo%{GA&UV050+(Q5A+Jstu06h}Q^x9ZsAXu^rKy}Bp#5$kZI?6g=c8GW z!2007n1ea+QS_iO&1QC=%dZd=YfZwRxH{?7=>GM1rg>hlS|XP5z04h=vNc!=pr${q z4Rq%1GJd}|K<isu3j0~i{a}<@)z`yb02iJqK2q_}PRHO1p2vBgH?eK5Y0lDgMnoRu z(<M?gba@hIA#20<E+vY&CTz=|>=5F}LPeG0Ohzqrz$5>#D-?HXorpTbamMd<kUCH! z4X3F4>%4c6|HtW@4Ypg_cqJ=dmzBtd27D6c!8;3hCb43s{Em-+b#ax|aqaY1V^_Vk z0H@FA-8tdeCIjmJ3Hm!?++bO#WESLk3t`RuNzM4&ERp|JbOhXF{qX~qTBdTJS2SbV zbW1;d4D4&49o>RhtF5Vwbmk{CgbX|^7C2Y$(-ma@t}wj@H^l#xqbpx4&lljV*_bjx zldc4!?cb7<a{!cJic1*<U9lD9pJ;W~5f_FIR<^w;`{->jav0!c$VCL7&rl`x^S!q$ zjtlqjC2o!b+A*=HsHstYAuI&tB_$d`gXPmiHka1h${+v$bmijySy*~NURuG!?yEpE zRn|R)uq~jSsO9-qUS3=MV7y<HK1KFOGx(jJC)wZI6=wrt4s~J~L$3o%L%{5;0!4Oz z&g){lMfx&jZo3Qazr3}-@Pbd^CRPQ01LJ~E&aIl?M>0UPF)a=F>z`(Sb;w-{G+B(x z^~Iumf~g@2pl+~w1W1tQUYdR4X9fr;ZTw-{P9txu>Z^A{)~3sE3FH-U(WhnM@0<^s zmC+g#s$_?~-AMP<Z*v=17_}|cwo(KrCXOaUMnrX2RlkwSeeD%L16WV#gSB!iV-nh^ zvnc;KKg&OS@NA@OvY<KB=a5FeaaC=k!~$PM*5>OEdoIrI)tax`-92)?9$pTLinhQ* zZu8_Jf)GtYYzkcOnD*rk&O|E!UCKtCsE^-)K1|wPUS$0?k2%ofr4CAiWtDO%l{Dps zY`x>T>A)H^2=uI}ihZD|ahg>|e=`4j|3Cl@xKJn(v82^8Eik`S1G4-LR2>}}S`s@> zjuT7?-lUoZ_TD942;6_O|576XiT-lu=H8&5q7jpezkLNt78j0*;{#$S>-+y}kL;cf zh?e#Vi`n8UA`7B-yg;@960bIXK@c1NIq}7<a_)$C1CbqXlPx2oN(~H-ro<5b`3d)9 z*W^(go(1K;2W!Ct)iz%3qD|haY6w_ML<-E^K<5rQz5L1CV7QNa3X^BVG5y1Qk?|~z z%^et_aw%u!&)1=&X}r`=&q<VuPV--7!3YX-FN~;zcZy283E2srKUFg0x54^x-(53> zN8z@CNjJ6h<}D?V4_$LidpsW8o(ve>FlE_70O5NeYZJMc5r+pCS`q*xvkW~cKQRkd z8#l&)KmDtLjR;OHYXj8*?%C&1bziDlD<XJ@)vW44<9`Gqq!j6%{b{^zVD~9}xt7BB zIk0{m?QB3WwhYLfwZ^img@2O1MgIj0d1Do;0;hpwLHy08jBf+U1?+h4u~HPP@x&!! z=;nx^JJJ~GPsD-i%FGhTmLLaV1wTQYZ~6DPf-A=LJ<Ih^PB3mMJFS})+x;p1Hcx1` zX}n`^L&yn2cm#5u++A$7Yk6%m5X)DZ%hlr!)fss=HhaWY_N%)$;#43zCL*faXEXK* zYF4#$B9K|oIM2KLf6lwR9xG@}u%<;(J$co)smlNf4jS}Bf(+Dc3S;y<2XmB*sQ`(= z>E)srzKukV;GSsmIsIjA%?v(ZiV1mPb4oBr|7g#0)Awck!omR|t@k3~5FOR`yq1^v zKRYY+lub<9$eix;c3Ad4J~^&e<4b^DG=FeiI@NE~HngUP=v;W~*s5lxx~=lQ=UhvF z{*k)5ztxr=0&8L0Y(wx7KjE1gOMR^zz+vI|f@^xBjjMLm_M?PNUKC^#N2}8lPqugt zM;>n6dfycTdIN0bzq6%e^f*B4ppA^0x2CJ<s}Ie>i)GqA$=Mt%<7OQsFp{7td)j>- z*{lI`lO4h`O2t^fP8UyZrs+H0*Xw)xxJd2U(TQoA+pX+@UOLCb;kL_H9e2u>UANgy z2S$7u1NPSSq}5BU29icN;(Jz@+G7_64K8Hm!s$|0!l#A0Zuby}EkEjTtuyl;{&Z1l zxHrsHJJ5jXyvt|^L(AI6ZN{&494Uy!(n>tz>QarEtb9!Ck2>SfH1!lShuIlT?V7?s z{#TQ69|!i{gx7;@i$``}Xxe2HK!^qYIg|{Tjr3H8IgwdVM9nqk(<8q2FBen>;e!OS zHfCz;8uac2+c$&M3-W{1slu#mU)$IwvI^-TsOGo{o)^6FbA#QP6*jg~<$G6cZM&Tp zejiDKurJK7eB7?8Q>0EsxSG~p_JlGmJC*f3PO#?$yd&~$s8bsP?XDpMn3EvHi%P9d zMIy);dyPf~ZDM(pbESy`|A0+J*u+lvVNnZ>(ao5Ow|E1RP6IDzqX(v5|9<QR%u@Ux zuL$@uK?nC+(BQGL$II^1pDm4IA1*U!yoK#$bD-GB`+$9YvgW<h%XxO{1bfF%QP{n8 zM)Z5>th(x8TaQ&>uJz*C`fQQ5E^IG_Oo=gCbbMAgEbcBKdl*pP*iW(IGyinsK-27l zc3|eCp#?M6<rImO9~o1H#G96@b$$HjmCYEd5047CtlGyPmRs;#pPnYJz%R-BC7Dt| zgQH%Fw0}%*JSKZ;0J1HN;d?+y6;f&u+7wcHSKAn(XyVy?xP3opkP)v@mokm+_mXF* zs7I;l2JN?s?2_+%Yi9DzPrev>76`A~@6<OaC`jci%9`wX7K^jYj^&)M>9F3^S)G(* zB@j9f4pO9wG-)J<8Jy<_d%g`(1vPC~gL{_Uth!IRQ?Yk)`dMoDA6fe80hq2PRj97E z?yqqnK2P#*88`pm$-jQWfm_TV>X3~hhk8+<aiku=#(o}}g%8%D@HORENF`JgnWUAN z{f{iRzLyBt&6T^3iv|T8%-IR}%&>iM8*y;OY`xY$+A$5LdIY-KSyuDHg?9ya4f_TJ zGOssd#=GOGLh5G0Y+Y4$sNvPv0?SM6$=<7K#|E8le3F#sbZvVCkdBiwMYF`@!z~)6 z_O-0=*g)AVDmqh17;ghOL;6$j_8W?Vj+>-jHM3U@h`0SGM2I4Cg8HsE%z`bMw*kQ{ zETs$5e@=<Jfn82UK*+S6cZ2()r<T(#M#9E%kVSB!DdhN(`Cx7ivvyJJ1Sb-Q1db)U z*1!w4Y>~^)9mEt!`9TTGo$M1IuhJnN-h{69i$k%^WY@Sg%Shnk3^lrG8<kJBf-z$> zo`W}E^Y7%agsUUTOO=0ej)wMqk}H-V9{-130iNyx7z>yW+Tt~LQLG8(qn&ydwHcJV z^xhAh)TiCU;(vbHv5jbmrI%7&dH8}ys4MQLHB&2V2X|L@EK%zF*8}l%*1aOE8!k0+ zHcXHG{ajqsB-nlOyh$i#0}c)d^FK8_)fUWqT%VlsPMH+du@-%&{{tt`2%7JChU$x- zK2JGH4PunaKVHIXe3{7x?M(f7tUps-pPAEgJgk6{6zn3P2%lV)u~y3#-tR@_6dyWD zh=`b?-;q875B67EnpU(@x@4v~PX7EPw^p><&H@bot@D%GC0ZSw5;*pIq8+ARVSE&K zn~ut=aiv4VP>LyYvB>YsOJ82`*d~bqJN-MU$q~{V70rIXJ~ePzUUKIvn!%w#<sv7n zO|yqvW7D7P62(z|eRO)2&0iwK>3Z$upU1}XpRE85v_7*NpeBpWmG{UY7e>De8aGA- zT~H}~UVOE`!!}L89;(iCL(XSYE^qyg)^g@sAlKQQ8@3Uj6CZhf-uQ~kvC^cqchuv# zrm3mZmo!iF?3An(J)*w8KAOmB^TOki^Uc+xl=oOcom&00^OfvcF457^eJ@$NT#r#c z(NR&o3xX4_<LKpyw<QgsRtX=noigH8{DJR1rm7D=OUMm=<)DagI;*u&iOq~`=r-7` z%$_KNDs9%7RJJjpYhQ6y1c;8cX^NeDw<7e`ea#A&HZC1(_6GV?<|*8J6C)eMJ2m?Q zOe*WrrTx*9n`3szZoldpKG4cpbMac72-e@f6m&|-^4Q|lbV5I4=-3RF<{<2=)#?mq zfDjRmv5#^S7_(W*HODiLl1-E#iF|)6WqE@#DioOV$7%R&j9Y=4iMz3~-opuCI)cW? zEl9dM;xvc3Mr;j)FfWX$ANjGFyzhbV@=tk*QSo(Aqw-nnaOm85T}k-gk5Hq>N%!V+ zGdM$t(83e;;<3j`c(pWgq0twukD8IXj1htqX2F0DJ1~1$qW7==<d`p49H2S6dh1su z^hwIvwF&v%^Gt|$U8`AbFKWPU?~{fV;a-93?*-{I*S>H8=RIfrPrs$bJWg{yULCeZ z94}lucoDO16;A&7yoLhwdF68ILps*s_-rHg$18Je;iOydssgBnNBpi9si|@T&Zd-J z*UVprO#kX_tnY64HDV;ttowCk!cOnQ`t;a4L1%`uGpSc+NigepC-jc;2Wht~Xx1ph z^VsLd2CDQDluDInhs_n~j_ry!N$Jc`jaRmD3@J|1SKoW*+{BXSHbJHP;qkP{x9KJQ z1HYQ$=O5>e6TuG++EU6!O7LG)2=at`zi4wB4yzn74zEa+KGcrrEA_lGpVe>eGVDpU zX{Xr5Tp(|LcvRaR^=K9kcW10{@QgBTSAQ3F=(*B3rQ?06c14S*TpmcPSMOBKxCeHc zVV*8+7wNjUIn;uu9K-BxKgd!ezT)MM#%ClVf&d|mV#=M&EQeFJ($!JY4aNJLigK9& zMU3sHP7W}opuuoP1KwuO-NcOzTT{H?lX+M>y`<M*=aJTAt^2yBn8()p*v+B^n_q9) zT@e>=<;Bi92LXBi#4`ni0-fFumVk#ucX(^eDO=}yQlSgT0asL?$TYqS?a6R~G6q*j zW2G<MIy8#*<QpDC!&x=JwC^Hp7Tjrc>nE4G{A}Hy@tSGLX|6@TKSL}D^X4nTNRe9V z!GY^g15n~Rc)0WXSCZw=1eQ96OiAzI`UZ;oDhY|Kiiidd)r`G|JMZ}|Ud=SP2v;1Z z20ZjB*D6%`>;g<HSkikzBko?@`<}m}Q_B4*c=D|JHod5NMLWjFF#DQ+ucl6+Fg*y+ zz-^E_)Aa-O4FC<F%HL`5xY>Guaek@vz5V{u58<5(<7U6;vU^C&o|Gw{TDMhI;9H>O zulP(gF0ixZJM&8$scY@%u}b?Hhv~XAYCRmO<4KESjN??*kBNqO$%6>{`L@~4<HwJH zyI^D8H!1aifH-D$)4{*Zby>+zw%A+7))*43)mbK2jEPP2E+Pww+Z85(V@4n4x>I8V zo_TpBwKsWRNH%=1r;}y$0&iBfT+W6(Xz&)Y3%FW~omQ?}(w;j;660oDS#5Ng>z|tg zbf-RBD57~EmHxJ;Zw6ydk#!7~J%IG)!SXSxQo0_63P*qN+X=Whbvauv%J#d2JB`Q8 z2K)aE!Xw&>=dU%{pOGrH*#6R|BJIid@L{dLrLdIe#K}ThS{g}9aAY*CQ|C_Jlh~>Z z@13moW3}{sb)TCjSVmp6Omi3V?lf}`)v3}j`Ok=j)D=ngQ5Z8$|C%Q~EzpIb^=_vq zD5v%WN=f{lKFT&&?UhI=PYCw)q)YtGWf*FF>2|MU<vuFUuo|@n_kV&bHZ1OK`)xqC zjg>=<{?^$1zE(vN&BXkxp1~{ay9PEhEZjGVKc5@j*2Ap1ByGDN`Rx7P&wEnotRHr} ztOlY+<=u9IR(>K=+G%DEn{2-L)n30`oUlJ!K&1Or%-yTNTRP3cD)x0sd+Twyx{t#^ zbc!_3CHGB)q_yhUd%@>$H!)vyvfsg+DbDhTgG{;Nl=5W;u<K8gYKkOVjM_X*D%qsR zljmf2j)kr*VnFk8!{*q;hAw(<<VwL5vE1b(ooYM{Q*F8$QkgrLIhG=o;%q%nMhNJ* zmtmVqc|wiHEc?3amq#P>j-jL@;Zl3=DMB#H^4`^}d<oY<XiDt-uknptdmkrBXeyKh zE>Z6gCsrZRvTlI~PwdJYsU~J}x`a!sGl^^YtDkIY4{)%{7DokWKV2#M`Hb!ZVVrhx zvCE|&v#!2AXKih*uF6VL67m6RQM27hhb%oCdy<Ux$qTGf;Evyru%CS7o9P3)j45M! zR4e`32%Nclla$e(0rIXoJIItOdA*WBs3JSg9_QxmPb6<?C)D7d38xPA7HRdSlT4M{ z11>*~WRG)$k-*?S{l)UmBNSGpwEO5Jt=_qEJ2jM0V;uP1l<u3Uaa2@P!WC_NXSK!3 zt2wg4Jl0^OEuCgy^_(dfZwnoLwb&6p-LN{4eL_@yDN|`~QLYz2xLWHp&L7?><*Qo+ z^okFe7R510j01lo8mCp9>a$(zfyB=DND2p|%f;C2=m77WiPDqFfl$B!)l^$;RjASQ z<yv@V_OIrb450NDitbjCz^$6RG6ke3B4kk+%r)U31lo$fH-w}-jbAd+6m$EWOq>oN z%-N;Sl1c6_;KISM1AHXNXNFLguI~;W*~kz4BESoF8hQ?<EIhuAdNuENEO<3t(0g&J z&{du3vemXXwW?sOZg*VT1L@+4nUx5$Ff4fF;S{ajMQ^<|$4{Tc$v)k=gPu|WUv2mX zXA=Q_5Z>qJ2@~CP^YkBwZ%3@|tXuTEog98XO6hg;<cmFQ?VoWw30UzZ_TMQVFgQ%g z_1vxy@oAr?Mr9O;-Z!0OsoA~=q^xLJ6FOVW!_D0coo+iu!*>!WVw|fx2_D5WiM_;4 zXCK!%7>z`_9`0dYzLAk!1(iyeULo5N);zYNE~~BF`A+SwVl}XM=z;qx9VleZrss4a zRxe+8vfn)oP3PD;vIFZ7)x7D>Bz7>EboC<&+-`FInbWRbD$6>{i5IrC`|$ya8%gdn z`F-%9l?2|OsC6RYl4ph4c$|chVfK&wU=lDDRpju2>1ta@0@~!5oc4M*RdTO*wta&E z+3&5|ms}C$Mlgo63HF~S<bqy-SVddChoporrz_iR@QRvD&to6;u<FVf*r0*%S`xbS z-pu<;zRO8pxdXp|<FNY&QD_)dO+f38Mc%+54GMc80EZJ0+<-3F*f;9^i1ZZtu?f2! zAt{tO-R{|?dxqJB`b=hd`h}|}zF~Hc0?NKq;G2MUoLC5AsSmb@iUXQJJ3j91YoE`{ z;2tQtx=EU!DHb9{j}&h;pvhYc18Q`@+MqP9_GDZ@sr=I)3FnPKo_iq55QJjzF*NQn zQA-9?nyXF~yaK=!fxoFJ5$%3dA!*@FXHO^*9V!f_V&}Z;B7XOR26;+_XV*20_|7t8 z+RRGqF>81wQZPtb%-x!yreG5?3F8)3ntA)eXc0$JJ;Ot_A36JYa4wSVkI{q>ZmzOd zKmrUceo#|w!nca|&$Q-?6ShgX`s!hFcT>)TEzrh+Sh#w9AL3_1uN;r4y90@;O2hs% zUrcm?_&R=t_15=wE8Bg;-$7(r0f+stuw#jEb?Bxa>K%+|dtiX?(MT))jFev6QK^wA zOxW!5eY<nyhbsS%Ylo}T!Z4?vA$>X{8d@!jW)u*n1xOg51K7p8?R40ubMCI^$&DiE zQ~G`GyG(6Is}Ww0of^gn<a+Ya;ZDDMODKd}-3WB7heb@!PSnvYEk@pZ`|b7L#?<C~ zjr+4IZvBAnOLQQ_LN;0vjpgF*7(o90ZRr#$e`S*FY%9-mD}W=HScQhkgJ5j)y_>}( zi+hjSd}z1Jwm0S*kcY&Tr(47|yE|SkAQw52d>P7q^Ab3Uo^v19+e_k&&o8+dPAWf3 zZ-LV6&j9s4#$^i9KT>l&)*Ll>P&7rT0e>-+0Llx;dqQc83*P|V?jWOr)y63>*0U&v zNuyT%W?UwF*;P9%S=l<Sap{FzjxwkLvv|cTSJ<1%G>2&~SnU4s>F%h?o%wuobtwMW z`cmwJ!_ubUFkJY<<6BlCxZ2s1?!%T}*0}WddQq;frnMja7aLW5QZlIjNWm+VW$@tT z5+NI_3jPPfXaj5WNNGk70tYEm!(~@Ve$Ug>fiAI-3e3VoG5-Uv;<=N`yDy6@+Wpqw zwp?{)L(aR@N~*Lvcjv3Mgx{~PwQj%Kh-uUuu1>Yb?9-JryH3#)84cvHu=Wb>1Q-0g z$eDeJ)7O^O>Wgp81^81h28gNedbZqIZN?PJ=}5(m@hRA4L!~6`vFY(H=7-8^oA|xf zla?88(eS|$4z-^fq;nBZ+YjMITW23J3(q?iL!_8eE2}?ddU+n<JEgmgmMr4<g~!sf zip+VW0(cp(owVP`j-K_%DG6MUxF<Bs(hm5-axRD+fGWo6Ar|NDPs7zS;R9Db^t%CW zj5GKf3iiGQ#%zvvh@nTECs+SJFe{E(8=SNHo|8uBZd!V}H2^`%7Q5UUdySOhg`*Wh z#R1SmHFd~sCnbCCN@VH^$#E;lg#bG<{>#!8QlWfbvvmWEZ9Zt@sm@K{P!%*tEn?h4 z7K#Y>eR88Tz5$}l0iYsc_Xvq+IZb2$^z45ihHK8(7g4Xl%}2?~uqY;8b(bEJ^sDl9 zE~?rg%e91rwc$&i+a5vmZ2<emxOlf81iE%rg|jmgTv`-MON0*s(8a~p=i2l244GN~ zcoV0o(@gtvOn=HQl;Coc2_u%<hFd6>O`Eo2gnXVxO&t=@8~~L^d#xgY6lw2l{oF}D z-xNJK!YI48KX}xHiq*@S2axlSQm+&3zR!baU4)Pyy=*!>q>{%HD2t7<19bK`61b1l z5Dzr_1dXlL8K3l=Zk&!|xX(v)$?K4n_Ve@qY5{nBWEONmuU*)8^cY?hla7X~jN=1t zcI=b@Qae`l$76USV~+imBND!f+#s<%UcSp;jM3L9b|KI)11^?O?%)N02S-7Od{er6 zEw9ou#A=MkuNF@tEnFzEGb6ryDiQg$6`qxZlz=7pgDc(^=G?9M)eyYlz7Jf*{+w}^ z(Wc;1x#F`C4nZq~moX$reRcwmnuBn*X~l8~{1>>nb{9L51YV(X`SGsLlDSEJ8_<HZ zh6J+E(H^4THd2gT?{4_LEzf4qH_eb+OCz1HyVoHzbq|OYX@Ty#prAo;=#H8U$R{s^ z0b(3Cd9H`3Z|Ct|(BM|Ou-#U4JsiY*aidRS&q7-Jg%8S{?GFQ~xaVEKqH;f<X+uhc z-0UWm2zer2a5HY>>hr{eZcCgrBlWHi14Kc4jn;#}zbDIe69`(mPtS;6x&LI*p4Elk zGBuR}ZO}t-g4B2S*ABTPw>4g{anA?n@MeMUh;DK6iAzl@G569j?&V0kXB`6r=(Tt8 zg9c^zI<NlcVZ1(<gVb15Z<+*ELAFJ)q*0JqIRJ<EF^2Trp%RC-V08KocyKy<6Z*JK z6I$oX-Pu34J;4?e)01m&si*y325)wcWlxe4;WKqS55Lm$Ii}+=l=gqdn<4q&Ik8^M zgTpAQU<29WCg2|%Eg1sf6X)T<jpc7|>@-C|?2dJtpZ^7W4PxQ8kigOW8ZcP;Kb(~~ zw4!7Vf4E53X}Y5)^0vs;X}SIUg<Nr_N`|oVHA;@MR_g)MXmSM8HYlx_p*PL5EGAv> zImy-(`@A2<MP4GqbJLuL-^VBfX)Oud!GM~wf*mFeG1Y6(2%5^*(+s<mlRp#TGvI1f z?s<i*-&_V7_Kz=+Vj)dH0&opuaayZ}GC*ec`#;420o1CHpSD-GSLT7qW*{xmM=vy! zy%Tz9`igUPAoJFKqxBg@*mKjf{qzCLiQ{a!bK^^m!fua|Fh0w!@oe+|IHG|UvTLQD zn0u=#{x0YGMeUa!VLre7SEN0Qd?sH2j_mny6DY*v%a_c1|F`CFjl5zJyu!mmQL?iM zjZQqdU8**`RkQy@w21WZAV#@Zl`fl(GIZq*C0+(3_EC+Z3)6?DwF%A1GVM0lCG2OS zgQ+!Mox5?q^eCW1l4RBq_7mvj&o3!?)^>%#7xO--$&m8pH#Vy-ouOmAY`vZKNRh<B zgrm#zk{Nnu;I6DqrVAc%LvY<)ew*q`yBhPs6f0Ij9X)lFc4s-&drZot^x}jN(Ldc3 z@Od8w0;c-1rdZA3t?4EY&(L}Y3H(=TlC9Bjkw^9dbh{-(uGoXx>GBLuLM~byI_Evl zI^3*QLO;_y9VKS$dly`wWMu?P>SC!mI(`K0Bo<h1nsU9?&VXIAQ?NL<u}k^(R8gtf z{RA*tW3Ec2e_u9w2vMx_IYtp?-2-4pN@A|g*$=G;6UbaQmwdM|r1R)%qbBFMo%mf4 z?E~-@2wk|Ae6Zs)-Q4Eb|6Cy8z`fCH@#9DRvpB)`Oj6C8PP>RmPd(TIl;FB})Dbi& z#MDL;<u=dS{x0_|`Bb$<85yjiI=(jpRrZXHi%|;jxDQ$-DoY-(q@4?Qxt`iDPY)00 zK1Sh+{xIY-K!1WJUoNl4YF-9QhqoR3(D0Ege|zUM<Rg*q@2+xgQ(UGtIsXG0!#v7t z5%}-7{(sGAAR9q3_7KnU6;NojL3pv=?jw$Z0M5+1Fm7O^1R4Boa5(Ut0HzavyDihS zg<WF2-7$lj?k$1pTN`W~{Np4lZpOF3wA3f`292%-9}SL5GQ`|i%)xZODDH}SgsHS$ z-r{7CG_dt>N)CQ<ZBwDOtNCK0I({+)wllKnz3%%@2oOt!(eXf&C-PDAnwqEnALmns zx$G7Y9xxC9L1*tZB9J*J7IHtybVPpDdRlUmQ1YOPU~x#yOEeK)Hes!nnOt>1SkHN# z9f*0ZyegJfln7}G+266&zjD?0o3FGdhwe-I*km7Wo3dPX<2Tqlh+lu79#SjaXX=#- z{W<D>1hp#sP-w-uKGtJZCg$;CyuxRqZ5$Q73bG|ly8=!Z#k0=ltXG>(KoBl@I6B{) z%bFoN+kc$QG{B<W6HOIz6P=xy*VWU4=ZQ!Fus#5wDW^Kyh)c6jybaK+`7Z~2rTx!C zcc;fv%7!P4u}i*GVMb2M)ko7!LMs87(VXJJmDS^pc;1J74-e`mFfUDgM>a|7Boxo< zMZ(Wz{RJ;AsxA#Zo!Z4M{Yoz>g48FH{>~e|;|687WipTY(Tgzs1gZ>Tjg8|hf+Bi& z@W2wJ98h8lr9S)w<Pzb>J7Dc6eJvxZ9py_+*Nza?b&~$NfsZ3@U<ee*eaH6IN`tHS zp&mEnyoC?bg}RE*a9m@TQtEDL_RfrWQ_{iikN@=SC+D!|&%5adKgaogPsZ)y#N`xL zjGMLriH!*!+_CgD1&D0?qP-vG!MHGjFst&)E%TGcziSl<vAE6zV?|2nXCH*<$tQxF znfzu{nc7}=_|TY$nRMgdt}+fIJa0R9jisNdf1Jx0N)Pe8x<}XcV#G>{PTKaP5o~08 z#oMy&Vf|;gEXnq~gY|+2%i_NwkT-J0dQZ;~%2)JcB3W`a3m-~e@S87BK7-$-wb|`= z@aRU}M;ZO94QA=!US3f+`B_5I6=Sz*2*31c$zy=r;b~O6Ny2Dl)zrwvJ%6T#)btU) zXX3lDJWG1B&_`{YMZ41O@bs6!NQ4mT0Ht#Ar3yBnx_K8dTKui=^vGiG6Pc!vZ(DN8 zk>}Kw&$i;OQr|u+|JnWL>70NfVCvf}DWFw#zH5BxGZ^FlhRDe1>IWiw|27Gn%vhA} zFIT&Wdruzz1h;gWMZ+fRciL;D+d(hlV#$apr4%%(%H%WyLB-x3<tAr%v?9*Q#1AA! zIHc|p7e>u4fxcj0*&WkI&pYFpm`=DR$_+W@nfj8tH+Q3bGcwC5Yy`9{GbQR2Rktu2 zyxCwHmK!><#hLf#8W5#rZwMik!XjNR_(G(3I(Ij|x#RG!y&2Nc)X--)oM_pqh!XnK zR_8zc0$|uoR7ancxHhpLQ$gK*Rzb9_+1sOMg+Ab?*#?)b)FyZ<jpiCIpN&L4g_k{& z9yq!@<71uVS=)1M-LACWom&xm-8O0>>6FD$>%Mm8i)3q3b!wU`@GrQoAFY*&6@vE* zmFrsUo)umAQc@dW0-5wj94kYM=1=0iM)fwzz`Mb*M?ehrYMOxr&Pg)-jO3pgEnr^O zIQ#%I!S?2hRCNje7F7l(j5g&)VM6^kbyiSc52<Jv!ojU2r=f`QgjXbJ2%`0gEP3Uo zv7%5^r!#->=EoJLmQz%7Tf)4wPh||fm;i5FWjn?|H=q_64U<K6;#IeEThueHY@+D? z$^b@dvCu`K!PZEcuEHpp`;IEjkhPY~-s>6KG@XknZKsJ?XzCW_J{EuRhx>CXR8CA; z)9Xvk^%BK|kUNs77OD_nV8A%_cc|PyvVjSu8{z8yx%uGiSH$=mSle#`{=1kz1l$N8 zv||WNV){q=07Pu$6nhZ@M|n*ePPDCs-j*2GEjtltOBbi$d;|oT1ZM;pV;Los4~A$! z$!og~y#|d(Q}QYuKE7EP&C+(B&V0GhFx_1Non61I`n|IGYVljB3*SWnxRqy`#5nnF z6^l!}bx(i7jZqV48QfC7+2oxjk_MK${MLi(S_)Z#VB`(N=v~UNv|6%o;W1O;&C~vz z4fB@@g}U||F8IW_HP(Y;SLv?jTDdST0tHIM*zWz+e^yvu1-${l;vW<7iN}eG&WgF) z7eH!W+ykURvmUt@X<^7;sIWUnp~n#4XLYc`_MCsMr+Tm<^*caX-}WT6Zi5aezN-FR zxcvp%^UEbBfE-<Y0>~s`wERU>o*M`irtG9LRX7)c&VCWan>1)!lng4C(<c9}HcA{` zLJ^8^ZCzazBi-Gx_b&hTy=?WQ=jv=eJ1HtEDxznu&A++jbeLOHoqz6SPpoLlJ390$ zwuVVPk7)wgJ_Mm$Y(n)9)kG@lP!k;>d$|n|8ZxQpZpqX2xgTt>A<Vz$ak15&^%`UY zNm=bM!HQbFvAXf}`#A>VlcR6e+4WjomA?C8B~ym8P`;4I;s12;30T;XK&*LdokPig zj=YsxL)PXHGeeKUQAnTrxJ8~h;NV6tt@LFqd`;*m7%h95<1c?g<d}{L1QtsY&>9KR ziUq2CM}{9+{>xVR&mUkG!)D~SlA)w^za`X&vy;`t;7mBSj&yT%>wu_plM_eFj4=o% zbVfeK@y=w{R(p!Vr>1+jV!8KnyquOdGcmWU*7d4Y*%GL_48QswZCGcY_^wPkv5Xyu zt-V-poIld{D|$wt23NSEZY|Yvg)E9t2-@u>;ozr!*R}{IVy?zbDD+Kj^{(x)h)^*= z4JR#hIr}J4TrX+pv)As(b`zRfkgIMy-nN%9{o@aE#0}iH%s}up79Wj{V1Ead)tF=% zU3MdZJElW7jI>%w^VQ1dz7d%mDgz7WdTsCG({5e|ny&JowC`Us*#5N1AO)H11>Dp( z4+I}sw(LZag_eqriih5m&sN3VFp}5Vd#wP6bFS)8+A7uZOKNpCzX@cXEC1*}JN$wJ zPwcs70-ZBX<M1o>-&BSZV;@7w{TI<8Y_^{v>Z`wB#~CJCKfPPJlZ`c0<aPtPG?>yE zmg6;=e=F)~hxy<_^8VZl7js`y&#?)dCwls+>V>hprJf0>e?o}@3e2B@>S(}N5l?v~ zib&Z!4#*x22PSkI>D77}r1zeU@|G|GB|ZFI@b!MzxA18<HY_?}4nKhI<%G}wH(vaY z@syDuz*T^wHNXZf>+*1{A~h<%o0trO(F!@<T&^2Jyg7xE+_!6&@f6@+tNO_NshV*I zSdMS_R;9^cvlM3bN_k2C2HCzW*(^d-g@j~nVB5-PvuG4gjdT7kzaa=DBBWA%*(;t4 z6dA$F-~M`cRnao{!qpWaVATy|+*&Ooz@x}J0WghM2X<*|7mn!{SlDrjhqTgr8rVZe z-yF2S#lX%G;hUtn6NjXSq!S46TJrwTQMW_AZaXMMT;T76Ere|%Tf9;;ZKgPo@N<L^ zjFYRzL0LQU1)o9I4XWgIZh*R36fR<^pE{`Cc>kGQ?%-&D+Niz?Guy;y5xdnbfP!n6 zDL&UycW>~E-~n2Eftq$wx9A#kVWua3M@AWpaJW=UeHruR=G8w9`Sq!icP98D+bj9# z(Q_;k=N!gCk~RE)ueY1{&IHR^xPN-tQ)T7{gkw4Ap)p>#zpO@4l*Canh4;-L*=ceL z*{Y(pNw_1S(XItewGzOi+-A=qMQB&jyucUfo#3}J^!#u+&OXXGG0_BOrY4S-b%3Pn zjBJvhf<N_a6FGhk`ll@c48RNQNP5T-vOrq%Sq<+4+gTPtjkdb(WiRucy+G3KvTaPH z2Sfs%IjFjZKRiMzI9|U4LKrl6uq2Z*{ds4c9dHk|0-SHP3rK8X-mHx<3Qwc?KAGx$ zeZ6$_(j74<oj!uGMumxZ;{_()dNEWak}Cc17B5y{@s#lSmWeSPOR@1*+X8<&$DVld zMC)L!)XwZzJA%KC>YEz?4)FUa7+M{MbM(eT2K?WZpaOm)_aK|?uZ-g{u4JydCpKDR z?v@k|-*(o5wEw7CUGN!Xq{H{5F=$3y^|oh(5boQ1AolT5#W)X_dur8Wt|UU|5bWWX zZFh%k9{Ji(=>*uoL}2_c-QV$et$6F7EV*e`*Egk`K1{{`X9SXSd5u(5P+mS*_y3(} zBhmio8dzU_uU7eMWkNEtdsWo0nCVx&B7$i9+MO+4>iK^A0`Yxn>T6R>BoT7IhYq+5 ze+OSibXdD1@h_j5X%z1e01T%kwbTa<U@yrf!<d)kx}c}rSpEi<DS&QFG!1!VX>{Z> zI`X$oxWaBTt#g#K=%Xl@!9$J{ogT!Utj+O>Uq5935pjSTsIf&uHXbuyUi8wEjN%}$ zE^qv2$p~Bm4}3xZeV^~olj=^76)&m{9uB7-2=E6Aha|-8f_(ReeMt9cu^Hr<AK=F7 z8T=$3Ww;&rWR|2_HDNx9i`zszb+Fvdg!=OD)b+(R1bX?jBwkum{|+Sps2EGHf;Q^( zi~5De&SlV}8xLC+6x!w6<^T1@2QdLT|3c+?6am1uYjNwZR5*?~N6kAo<-HWy>wEF{ zDWjjf>$#7MkYnDuSa;p591f0_*5BtzG+dDxbx|As=-&ub=n8VHqDqj}bvK<C|8cry z3~>Rxlea*cXg^>{r6tq!1$mORNYM`}tMNz_;BQMtMK*|zLeX#iu2g*Ct%D{ue2ZB6 zU3K9=3==)*0n`l`xc`Q<_HpmwW-l%sA#Hnyl@RaUx8BG!5qu}}_F=LJ{nq*Hj(Ey4 z-+OWC9_|~d{|uJc*nsROvFAN00!<JhL|=?wgM-HU&kC$N$D)T&kW`8h{PSGiz^36y zkU6B$V@unoSi!A|lrh>b!pTu9e1Txg(dyc~KF)nLKtt$1UUP!O_0IdJIBTJkEP@$) z3n1D#;bHRNcMhWYx};&ZnK&)(tGq*55B&T^p}0J`csa_7NG!oA8Dkyph(2MOgRSF> zw=od_=i0R0nLx<N>|a#J|9M3dWU+6_=h8@WL#!WTeb1iqJ8AUz<JP}sz20x+-Avp# z=gjzy?<J25Z2K?NXXl&U(*3T+tY`Q(xVOjFD%a}Q>b>W48D3Q}ePIz4lw8F7$6f$) z=gx^80cjrH-WcbIzFT8BZ^BFRe;E4;uqd<lZAAnG1w=x+M5HC88vzAr6loRdmTn0F zMM5c&?h-~ihY$(r?jAa&VTOV4jJxXMZ})$F*Rp%fEH5+fd(Ly7xX=CE=WHS^e)G~a z{>SQ~$ae`Jw545Z`R7Oo@0m_{a@l{q6~p6EYI$?B_JpxQ$L$?LT!hEr<^DadyIv23 zlBiqMf%nOxjQURf=Xd_;!TuiLB)hBk=I~cKi|=4EizMhjrhutO8~ABWPL14bg&5ih z*Yw2~_FNRDYoqeia^2rx)A!haM=@F(08bYzpO!=>A-~$aN%KU<>J~MjE)E^Gy(_zm zcg4YhDO5@IBUepY+Fd}*NJ~zxkB4xq&dxqFwX*7bJl@nKbKy~B=LxH{v~+K8FWm^C z#*rCxJKXN+(`0CJ%$wW;jm;u``A{m6_9r6EgSi%g+D|+iqoSjYS0fXLVzbME)@F*A z7Nod#*BtQ6v@(m?OlxNtjY%{%AvJi2lDUP2H~Wzz1R98Yly*zVy>Pn>*a~t$OpJt5 zjermNm9#1|u|Vd@+6JC+)5eKSXTP!kgWzOtBEGSv?wCqFK7XUNgvk?nwM0@b5r6Po zjHd2ou(|DeQ!Mv8V2K@o>o#Jjq|EhWUN0g?HQ-0W{AvbE4;nlAua7nk01lqr$b(?R zA~vhi;0ar-#;5r#{OxQ2QQL)FH$hL%o%`-RVU*P5v5I{VjJPY3o-q65zF4;i!n;_= zPxSbEL!sM&)nDWnn$x-qcrEq5n3g(XVoN{^2Kr)cn3G&X>^Zz-+~Z*41$@_(ysV~X z67u=QV!Tu9D;yGwalusfee;U-(y_ZRQ)s38QKA0R`6z9x0z7}74WRJp9^?iK=F>_} zX?i&6$!53MSt7|8?6xs3Q2V9zlV(v`adH3T6t{8bA#qtwjvO;Hv&Fbtrs5H?Q)>hh zm;I75d09qA(_c&{&^JbYaJU7lX`}>N7A6*X8nU5Odu$t7DyerhH8eEZ0A<1>2*u!t z;WY3M&fS#vWb?(iLSWh83Pl`j9<Y3hh>mXS;G435ZA?@R{h&ETj#<}E4Cacs?SmUA zJ1SXy&meCxu3O~@_)F4Rf(zY_#Rko|fZF7{8$jjS(li0eAZy#bHbgU(TYJ+)r9-Z@ z?VavxO2SW9ce=z**UZOdsXf$7Eys)u5TyKI;@YCvu6f|~mqxG|90ZYYv}P#8u00-w zR7P4F931T|j!{pTK%W7+l2+5Oh=&D~!Psog6SjS-i*fln%~b8_=GzG<Uz)Ezx8vrM zq&&jrVJOjU(_!RXxYDRCCEz&L?GeUgFK&zoIu3EpN3v)LJJCQnfWbvWSmGBqLhiZ_ z2|Foq=xe}R-q9Pb^k?sr5_e5dMuzA!zQD5V(CnzHP0DfzP_`D_xjyKgX#Je_(@b}W zt4-H-bGnmh%qQKw{5XEs53YBGQYcGODSJ-0MZY3$!@@#VKrNx|gk_nRkUobm2xMYe zZtSSdr8TVC!kR`nrg-ubYi|v3$eWp)OT*wtL8?R!%f87#>J|={mqjLJ){YL8dkUro zU+4VN)K~85=>qw}o~MMO-3UEz!B@e&5}aF&)It$&YTF0PJ+Xr^aVbnFF#z*M(tyZ! zh2)MF<k-D?6e!s)59CGzLS9`3(9#44;!u-oAlPb&i8dl5BS+wC#yZeP$S-PO9s5`j zU0#VEA=8lN_hbXpO=#Hw$UB^GN@<Ds&4mQLPO9~z%@LOY>kWL}@POvih^yr0<3+|@ za7QRS$m_?#=FYDnSsqq|!I_3NK8N2_D74+fyzR*j9;?(Ad7S)GZwjz))<Zptt&S>g z1Fk18AiD8a6;zedi#2z@oj_L9g8$5{HEoQB98*YjRU<S>-TLSYX9iB?aV?O8i9vT% zu2&&^wI&t|ZP)VGTe>)Tz$dH}$vg$V!#tr1(IC**H34S#KornNq3*iNF`gS62g6{d zH##$&C>^mJY~S{_KRBY&Bd}g1iTaw>)v?ui?*o7BJEO02N(`b$MZQxWp~sKjv_H^K ztUjoW6Is7g_bp^qsZ^wwDhqEn@msA|4~XN%(fei5AdI^Om{p0U-@tNcEIeC^{fcGc zT<FB2h-!t>{a1_c?<<o15yU2<KprK7>(i%~=`^kbu<Nu>UKQ>I@C#t`!7Eb?sLeh} zr5I&IhQG5UizlU+78hC4S*KaYO`dR^>v;7?zV>&NPJ2}cAiKipm_@_YkJ0P;T0Y9R z&W0avc5xbzf5yeDYJ&=~;Rp>kdB&3d>FpcC{!)n5`n2UDL-;3h6jNg47*^J+tZ%*2 z|FGgk#bEn!Hun?N+ARHbh<-oyDBgO&zLZ#)mKZ+Rd=jO`P1c?EJ)ikMDF!NQs^mKF za49eA)B7Q>+AJ1*jKx17y6KNt)A(l3#bq#kcOEY;C@U+|T+8ST%jV7x&D#7K<q*cZ z{QU-_oXG1x;IBFe3it|~!TKNc^e+%2-uq>xpJD})>FMZUrwt!J=9$5#Q9~s$8<*qy z)Lx7M@X!YSQp&`6?4JF&zh>qm(dC)+y-fFMd`+80kN44ujDWrV9$v{(s`&8qdt{uD z-5WR&ow!GGZF23EJD8K!m$6qa?A2dg8a@E&9q8$S;jcU{_z#;nn@oXwO?!>90?-L= zlIb)Ib{;9YUbLf+6wly{6qHo<B|`LZEx#rsd%b3`?t?Tp>^17Kd9JiG5uZ&{{^xUl zCK)ztKk_vJ=6~dCxcm_XS1=uN%=JaeH{VUksoZ~!qFJDbA60Fp*6!OSn>F=Utajbj zHxEq2iW57>J;88&n_{GpgcyER)H^elXX}Q4W{ctk0PVl?0*69U2nt((vUsvFKwU{C zmb72wEIf`~94peWW@2A9)UG?SGfvAcNI*Ogu`ZgNF23j0g7AKqOy)sNG2Q)01Y~Zn zC3r!X+n?#{nT-6`0C-*l$z>_=h=5q57$CS@q3-HXk>x47(MjznZnvB|G>6CUCv9gt zHd1fA<x%^-2H83vq$C!W8gNh*#J74l<Ta*?C)b!n%RAu)f!@$yA9CGx)^F;fR;;&O zNh}k?8G2jCvVsYqw5t=$U&e|<H^xQ5;LHe79Wn2p!0^fL2eSR^_;~YNyMDV8MDg6` zU}^SIyq^67Z?mTYB^Ox*M(6AJ$*Wd9y#Z<Nbu>o{J4$nP3GtV@G^A5l5>8~led~A! zlf`WXbaD9)35d{ZdfJN46q$)>iAjk`)PZXNLs%C!aOp(<SNljJLc>Y)m;{GCo+48o zseUFudzJVBJy^~Tii+8J01TUPuMl9vP<IxZ4r}D4=)cdg=Nc9p%ZMj(bns}p0^U$I zR~*A7sv-aw4&PGJ+bj)q5j|C^cE>hfcE_XlCrHZc*;ItmJe2`HlU+OIknVU5xQY`& zTdWePYbU<HChVWn_&w;QpdUM^rv)k?S}15w|LN-sXXl+J6=hZ1IH_axN4Yz;*Lm#I z9(?4LV#dHNdDflFrZL~4aiR;zI@cy4OhX-!U{AGWepG(@WYuf#M78$XAjzm0k{ao? zxO!?D9bc<Ei~q4_MyURex%hkayl1#Qzn^*cbP#$N*T0sh$V<NbhZ)*yqCE{zcDmHL z%v7-sRzJT7FxP6!w3}89pVU|qcSqXHXZvGR?-<wQ36+2p2R^Xb_R7w<!*LdF4+uNy z`bXzR4%bNv%@GT{ePs`+ny>Bn<GEYVmz)|}dl5aU+v}r#uH=Q96cO>UJ?a4E=Kn<k ze%$O!2>PSrJ^<@`Feu(WV}LZ_KL(5_b*hdwf5*0k<5mRF(EpYmMDmu!#6b0ciH}3+ zkuVZ3#VW<R7EayUAE6_<;k8(GS}YG|7-d+0@0ql-e}Ui6=*6z0#hFk3x%9mwu(G7G z6sfz4x0s&NbDjw}-O`r;-zC0C&1@AlJo-O|=LVZU?wHN@Osa_eb;jV}U_e^I=@%M} z&%SWdFEm>hb09FeSd{&vO7xnt#ad6HCg!*N*RMWn8{meP1ku43t`KaHVnj<wPg&g} zo*de2_x2o1CB^2JbBI0#9)94$Lg~x}YKG|*sojq#MBKAmGzD?7OyN}gG%Vz#UcfIy z6$Kn>{SR>X^MU{TR#Q4KLzen4kH5@*cv~T`LfDM<`;u<TOFaDp>F%0Xl4>$C82^8d z@lxCj^&EBt@($}1lF*}B0YBH!W;?9;@)(oBC6k$R|J(;kDmu8(Ny`(t1HkLi>+${n zTv7jY$bT$3fVzRHv3sEBu>ZjF2ZX`Fm&q{yLN!<(-oJ6JdU_yXUjdptscfz4rJpv@ zZB>{&0iLA!?dfQx6?!rT>Hm;7eg>UCTs2Mn4-32)Ct!Q?4xC}d`&~i3x2w3<Jy0em zQQu2J73cA(cCG&&Gw-o@zv>dTZqrA6u`>;pIt#>{L+6qJ`OK)6{o*dssWX5i+=cmM zDF?>i^Lq{7???~T3m2O%C+cXlY`_$q++SFErTrIPJVf?vs2Fb)bDh3aw_N1)m@L>L z09Q9JG}H2YygK&pLr6+{#vgRH2;dX`;Xx}=xy;;c89X0ux<7!JIXD1f`tERL>ioM6 z=wp34%p5?GIHw6#aIAdJj;1I9h--q3nf-&Pgr#OYj@a%Z$lTbTkW*#gD&v`u?Be{5 z`gm`2<AI3{uTA9lgY(xh5}sL*Kj#K8>&pFZE;Oi|xtXL@0vnJSVaIVlDjd5g-8lHP zFIKb}`xMPhl`V8v=4Og(_B0xvvof`gIhGJfrO1yVY$(AP#YB47BE9Xf9PZpEJpu3* z+yB5@e|>Sh&pGzw_*q^Xf-^%bj`<^OL#F>;LR8WQ?JAD{7y)}jG!&STDC*N4@$TLE zv!njEVFNn@UA~~B2DobD>wlWlG|PDaAA-bJ7toZgMIEsAta5$H$IdUZ^VfOri+lZq zjk^aB;KGls4nPysAyDb8GyN|QfZrm6Rsd)i7`}q;G7!r67Ua<@hWgzZ7zm;F7x9`e zEuu<{31kbdr+`2Om&M$5CTjl2t_SRqc>_~z%i+xvjm_CkBaMvXRO1d+B~o*xsY5?p z)$AEp)lviRQP!|#A6><wo?st$Ecc+na$tS@zq<^7gsHy=viEKsAgRr2+qs&pm)l|O zQH6pL5uxfL4ThaZ%ieZcF7np5(n}b3UF0I=cm7bNbF@+vI@f(7l5E*b@48NKchhMM z+?9=R%FuW3P=dgG606uB4)nP`0+>y9@hPCWgOeD-78Z;Ui#~@1?6+=(MMXuu{R%ac zX;Dy6@YFH7<*#Rb6Sg8Uc<9Lnz$0El+dg?=wYZ)3fL9dUf?J3av+?n9oiWfX(iIbu z8&?8=C6y-Ej;{(FTMujpOLIK}rVcb<D{jd(r*O?gT^*<%a>x9ex(>AFLGXIus?vHZ zAqq+w*FT7nK(L543&xPC5ey}|HV#jSbqdz(xV??c=47x1O@*M~gninq_w+LA&TZ=- z4k<tB&wF5Nw6@WOGz3NEq6MP8Zg?Wmbw1&M+fj*Q6THV?#s5e)g|gAh6#^$75b|qr zloL_njdjT{#wPHFS94EJ%mr-M!T{T$)1As}t#qjUXQd`c@9ke%8KK0&AhARrDoj`9 zxc*)tmiGvr4%?n@o3Hb}z?<*6v~6B-T7#HdDlp)+GD!8h5bL&4A*+}uY6%-0%3aSD zYnyEhng{>#ZU8{z_>x%)t5)&7l?U?!t`D`fSGC74;t)Sh?r91pp9ej4HI2~ajMz_g za*%`LqaE;W#-BVbt^6@BayJ}s_Xf+#K@A_(+pFs4)fFcI(;i>VuTlgs(;Q>N(OkA# zO=G;EL(XMN!G1k!1%+U1ThK{&3t$Jl8(Xl+Y_eWAY@p8>dFk5C;c@^rDOlfDT6WEA z)f(#oSd)lsFXF}U=cioegRJBS)Ahc~R~DE&x|1ZhtS5!ZolZ}X3G~Yz*_KNvILsC( zuf|S3N$72BZC!Kooty&42f2l$9T;v-Lg#@#Yw1uG0LOMQb|UmF;0uV&+Nmbwe6+qW z_zu<DFs*`TuY#*UTOI0x{AaHi6P&*^O%xgiE#8Cci|+NSZHK#1k5{|c!F^rmgh88I zp~LDU^06YJb&ev-HP1xSDr$qZgnidY&=Xg~w`5FQOAPEM4A=plF`whm!54bjki%>J zcS-M<MLdY*?WjRn1GQrlXunxyiz%pzmT}b=L+<vKPCZ#m(CdtG-RK+^fgTJ*dLq`# zf;?-J03IpqWW2Vqao2SaJ`M%P45dt2Kku`1qHIgxx1|$NhDCf_Ufg&)0UM9N9ghT% zRVQt~@Xb3@Wn~*S1EaQotx?*3w`vjFmZ=&<UwpYjoS<6HLVGl*1lkr8tH<);qw&t& zA*!2@^3z>#GT7)9q<Hf#Dk&gJV7er^=NpRs?fCd}It&i_0o#rMHmM^vVGYd0_?3?< z)Kf4bn3P*aJuKT6Nm!Rtmjh7ZroBEQhq;z_gBKMW?L4uq!I3ytuR6#yAY%~ei-yep zJpc;w47g1C>{S+-#>H|^Hn?}?=g-}!_AcD(x5ee;<iJ;4?3|yUPf6jjtc~W--^VH1 z-JF`jwgyNQp7X=<A&9=18`Kj!_u=zT`pzqRa3<^?w_FD;nBHv^I{6n4HR_2Zd~Z(P zD=H65;I4)5nlmvo6Z{0C-Wl#L_3(mE>m8_<Y^DIX*W29M>Y;-8rUB}d!;&SpY0wjD zIj$y(y}t)nzn4Gcwmo;hmVKzH2lPV^2(B-F8J?gXMd42E;6>+OFQ6WAm_RDxBXr=t zap&ByuGft$1Kg&K`&0Q5*SH*&NHwdtK${jxsiWbuwMljhCS!n5auqrnJ~{KQ+jxq( zdZ2)^Yhvux?d%65ypDjpO(va{1=KB8kH5VC0BS2iI~}w6Fe*LEf!f!7pc!{xSm3j9 zfIDE~KY*G8&!uKPRV)t@aDPWBQ2OITs7<oexWDy}%F6jDs|o&B^5ssS^sE~QU2p)> zmc^+%&I6h=0UoH6K0pP5XYqY%g=4_bV)X!f<h_|o8sGMt+oA?54W%$nqc4Opb}&f+ z$EBC<C(DS^H}wJVnU#P?SODnohEGp=nBwhXF0bQ(3^mvG+jklK6_ft}JeDGUU|{M% z8)CIqzn`fqH2jY}=djD(pfKRdHt3rpA#Pr4@7TIUW)M<%0d1ZBwnhKr3fmb1froZF zgYJh$2y9xCpSjEskd~z^5~s(48m$rj%xTUhxM7NDoeaBkHD;P1b6mbGJ#r@FIEUUV z0|r9SI8%KHH4I9WlF+f0;U>qrY#iM6#oJb@k?qwpMyOslcj!L-Y8rOe{!W^1FGr83 z$qx?2`Z{KulnL|ERlf{Nex>?9KK$<hOetMg;g|n6dq<^F75C@;|9$xs)>lElj{>bC zYBl89cfSCZ)B6vooFQRHDXuneTOa{?jO|J&r=p^yb}|2>!|QnV8YfeIdJN=nprq5q z)yMnWm(ESY)s`VkN?9EJBoJRdFhSiX@NQ)dYVH&BAVweMuJ0eB|NZ9T!%?}|PL0i@ zGrt$#MwK3IGjR#;CmeB;CwEuySEAJ0=%Me_{I*`jJuG^+m}ZLFadUTCc7y`*=Ez># zBU4kW(y@M$6`Xa3@w+B?#h9ZP*6Xj{bMgXB_iC>LX!?7eUHM%T{bRzqvoB@cB6R9j z^a$b?IAf|b=e-{Xbfdq9QM7WfhK$6TQ&4Ks*yN%6Uxq~3hG&dDdm}HGR{W`!hnJ_N zc@uiGgrrN~d~5C}KPo969dKM!J73fA7(Rdgf7C%|ZY%AFQ7~rz(<mMTqo}||8HL?O z9+%ZN&yfoZW8@JM>Y)0{7~#^(z8no;GHS5l>b<}JqY^s?wH|qD`2SE_{yi<D3FAUN zDTSIpDtXlHd4~WD9-rlgW;;8r_np)s*E$pB+_$R`J;<ZrEDFXQV%8`%#*l9xBdud` z_6I@7CDNP4{y@mJW3NH>(%m7PG6hi6DvOy==ZgjEl|j4!L!6Hk8uGnozH;3LwfBtk zAIbR{Ge9jSZ=S0lugfFaj7!uK+5m-UGgPgqEHV)$qc}65<}C4X==-G|(mRem<2G%S zFJ_mXYF4f2%43T@6;*kNDGG)JFI6_xD|m<-eZR*`RV+W<19W%|nmw+D0}0|gT5CV& z>Oa%!zo*R}A2mSz*qy)-zD*PPHBmgrL?6BX<alnG%V2};g~_Sak?VSnW0WNCPZP?I z6qnM~ZI!0MQXnNI{r7?AQg{Md=duAb4JZhmyV>}Euqee<e-z;_<o+tcU$hgZOj<f5 zJrkO~ixIGI_d(SG`XJr~x;9p#iy|#MocR8yIfRpa49EJ{swIj0s<OnAk8wh@1BWQ% z9k*PezHD3eKc-@qAP_V>*{zXD7(3tp#W!Gx(<NBsQ+@m4IQuK3*Zz0%J4Pw(P{VgR z`Oq6+Te|rP$S_v%bim<Or(uAJaC+|t7s1Cm%4ncqJvljlG9rddA3b@ds-ETNu$>tw zZ}z)SW{SGBqq|YiJx=_cOzbm@<@t}2Dt}5qgrN!rQ!E2cDO+}wtSdbP3x*?*`$a7+ zO~bmvsj-h(+D@$~PT2OY=j35toTw)3dtKX-N0wOfinvFTXg8I!*d+(7w;Y1|NC3YG z)_waf{eQ$1Fe|9;m9G9R-IYPx!ha4`iyW#9%YNE|GL*1bm`c)D3UGk^s8SNZ^DCB1 ztI|H-Qr@hAuh9#*KT$&9++h6akeRN5Wg;(t_kYxvNk#Z|48=v`mpU(;nMsqNm!R<N zRLsr?{r`N4f_|=L0|jC0FS$u51pTEXB)oUm9x_L>$46&0$*gE}UwRK8;M~Irv6=G- zI<SO$e?@{nS9o>Jr@K^Up5ot^#8NSM=A}xK#Bh+9#Dh$M^N$GwPc5Ul5VJjTPm;h2 zwZBEt)nU5zQsGAw!L3zap$BvnNslc-9yMxI*WLG*e*4|;P(~nXbUgG3c!h?NcN+g3 z%KyV-dKcpZx3JC@#{?Ub4{`tXS4rjsKsqbmWs3iQd0J;<^WOnrX#xEirKDQb6ZGWl zExO+X^Q9GZ(9X0c)N=IySK5k~-2idMuKhudMn`*Lc0SIVHwN4DAD9-^*aCLfVR(Gy zC7Bbd)>(Z0vHeq<CVU`6Rw$p~860W^VAgCy#MZ=iDmuv@&Cvhll;TlaHu1|=Hoq5X z($2Ly+Txul`cC{amhqosSQv&fndl9Q?z1u0qt5$^=Xtb#%oSh3&WcxX)aoc7#CJ5a zcLS^rJh8C=U@GY+Gylix*hbG7KZ&?}U6;XJ-#NccABW^0(H6Y4=K%W*ju6DRfnV^7 zw>`Lho(VsuKm6a{bC#rbO_}qDhXQW0)GHqTT;PvLor}c}+QwO<RzTCW>rS>*mXkJR z?l3NYbp3hUVodl?Hj;P#BpwI=>iHJ}x<A1K>l-EiWsJQylu%AbUfM9S`UD|8ar&%6 z@{F-3wc!J2*ulBD5tFL<Yctn$OUG;WScZTgTrBc%E`WNE;b>p(C-;ia1f(O$$RFlD zzeVoTZL<m^`nmFdnJ?;Q#QA8ZbtIKGazUH6m5o>h`ai$go&*hOo<WDg0XrxB4qr3N z?LdKc+}~Gtvp;yr1grYq0qg(w4F$oFx`?qv0;KlebU_$)#*N>9{=>Jf-#~#3S`r|G zfJvME`epaPB^s>t6+ry`4+n7@jTs9xuFm7Kg!-Iu;^(n+a8Q7jfl&z5?5&8JE(sw~ z^IRLJT4$yf?($eOwUv#zQdK&Dd9(PyK247mSRS!9Ri>XHL8PZ8jg09`FgrUtg_pVe zn_3u^*$WI-IbTPkMP=R8i<U(8?P&AGif6`=0;?kXRiT3)zAAW400aigHR;>u<u$X0 zXTaYepo`8mpafJb*xyd{IT&A<|000epdV?`gn;@v_x!RZ3(R8n6Cw9r!rk%R{+T7! z8486PB4aj+ctcC7MZtEwAr89<JDLoY3r=gidpQO!rH`~qgPAHO2hAj#H$bbOfr~2H zujyRcK=0kahcBguwt}NR4Pi~eHJ%^54rc-k0qnqQWoK7peX!+?NfnkV8w#Y+f`e-s zbxSQ`;shO9oAMx#1GWuarwIlUcAd|*5S@xy?Qx-va;cS-6^rrFQLU+2cAau%EiEmJ zaX?vt6CW(ZT1OopyOaL~vBdLPCqzU<43~ogKLPy)bjziIdY^Y=xi1sYZw#6)liq&v zG8k2dPR%OD@+$aWAvb~n6yQr)IUr5E?@5&%D+dQC@33uvvyGq6etpfm0Yr@4gS3G> z3(z|%v&EwnoYNi$2us`ANRj3E1x(ySVyExZ^;}*j?&MBWH6AsMd0AOu00$Z_2M4EY zu~}(q#w@ut0B+6mp?uw%#`TG+GFIKne!Wc~kODqfM~4+p^e`#7$^+pzHA_rL*aFb0 zX9Cs}l>=qv@Fz#vjEND!3nwSXJ6$!61LCzOdyw_LM;Ztr!?o}Q*D{NdX9ftDdQdwj zcl<7*e^t}qg`i{YPN&fN<ER2R_y=RyFp{tKC^!$GZ+#k&;^ztf%f}G}ES%T7`9L;E zuO@%@fryh6K%@cW2q&MuJ1K&y)j0SVE%y5!d4@19;7}jlF!Mq&+xHp}8`q9L?n@v* zi$m$9&B2mVJOG`r^&tfHj+g}e6cdC|(oyuyq0|RavHL^Jys9&CKp;jV9o8Hg4Ni0( zDQBkGH76Bv{FFtb1{qR1zXn93?q19yE6dLQ25BQAax@hNbeivu02V0~H68c&$C%33 zsEz2=uQvx+N3B!<E)fz<0+w@b2qG<}OhQcj<_$fPkPOI3bz?udvoYTmjjYsBIUoO~ z6>xyi7J+sZ6VOevEt%RjUmk)aE+y(g62F)ck+PTv=Sn)(Mgirv79f9v0#YWRu6uy2 z1>lgkxpdn=lV`Ymu`_-Lr-+b<XygC`st1H|0Pu?Mfj3w#IJsrsLw)JNhuZ+fQR7dH zj%H-O2Otj=CXxkC(d^F#n2C=ERFvrb>fT^~|Di(#Z-^d5FLz(l6RgnxoJbTt@~P{c zy+!k|2(tPw=H7G#P;hhE0B9*tff_n^>S2viaNBSID=ly4m1y4#c*k!%HcT1Ny*t1l zw{eW#*RJ9IQwsp&3MFlLMn{C&oo3MAI@MZN-(dJ+fK|W_9-;1aY8ap+{#2!aFnafW zB}>UdSM6q?(A=B?tw&p{rOIL_`~a#T8;AI6TJ-#T_e|kxG}{a3Q>&QUhW?jHMOF7V z#&2ToZ>$C4-;Y+&=~c5TpT{+|GK<>For+GGC5Ulajc;OI%RvdlVRfh|S|zZS+iB(+ z=5{`5fGcT<B&>N?SEDWKjt(t7B}>~+@ydAmEn%u8gZz$Tiuy;f&6L?XAmsLVWtAYd zClZf>F11;KKzH@U%2$<<D)TB`w>J6tJ%^zwRzoCQ_IZUx`!cWlmg!f~iSA7{64n|H zqtkc3i06x?Uop7jvnQK%&Iwf#p*V+*ELu93e>t-M%pQLD_c}%pjQRxx(koF#y1T%O z0W2-F&gY+IlIT8lYT-30C9nh46zR@*0fyX3WbWH|{)a|g`ZOe6iII3yt|d~PZgK;A zTn-C+g~rXqM>DGqgSKTw+ksAcp~nlgBa=&c8Yh{Y17>Sni(We^=`!a8<vfpE!qou< zX54vf0&HQJ0E&v1Cwv1(ACjtM8Z!<(wr$&jcpVxq4`ixGiSDLNcp^hj%{tQ=>_wUT z-Iua!%bI7AnJFmQmNI4deksIr-ghp<5Ra5|N&0BILrmAsWnCoHeqQbTf*;dEPArWY zWRB%jULz&%i%;1s^3S%HLm;Avd!X^NFW++}ACoFt)T52Ab-?YYsqK_lE#U;qsS$F@ zNtb6uA!IKudb*4Jusi8E7PA7hMYLcYC!L6s;Xx$m1F}rtp;G!r%ad{O^Vv-P_lWDh zL`CZN=Tmc^uj6z%co)qRo(bvR8}|ScXTw>eBIEfbFt!Rt9aRrI$z;2gCK0)|7vos% ztSS{c6Gbf1Q@rk%e(+mPxtM(En$s8G9-nSS=Qp-W%QxxIQJSGAjq#n{iuF2{LCJH{ zGdF+Z(Dq!peK1t+4O?3q5n?^(v7YV~^<EEZIMSdr76Cck(tmrsFDpYte#-N_#vmPx z-Oaq4Y1@GQ@nCvmpe#pR)xPFMgvRCf(((;H%1Vb;TZMlqpAuC@NgoKax*WO8q9-R` zNFZ5hKl-kAdP|@J@Xe;!>2FS&Y<2+pm&HVd8HMhYv*yL>cL!~1cXK)9UH7A?jEJ>$ zcdOOQm%l={xb~RV87o{W_PYaHx42B<4_L4s28`U5H_kbD-5GA6n+Wn_-BYR;7tdr? zzX||Tnj|0Pzf&N-tq3JkETwmUT=^BK!jVXOKDB)?I3CyE$i?{zOq3}8bph8nBJ4p5 zA-Y75-H^)=o{g6{(Zfu08eZRx#<_1T^+>rMEVBp_9nX&j$q19NbgIp0cuL^KdiD0l zpl}<O+lXZZM9Q#~hp#zaQcv16-RR@Kq*_yq8e@CJWN@<ZBndM!k5TnLg}qnp_W6`f zpT*9&QpJSH;_RAILrm_8s##Nn*?^GOI}C1UxwiV{w2)-7qLv4*^NGWw%{eL>l1mJF zhP!Ekuk1N`ndp1OLq!cWjqZk#aKwd=%9BXBz@ST}`BkAYE63w8+@NvXvigSO)Oak( z{rU3OGMC|b7P%)9`FxU7Jut>LR9kaYVvYHo#3+58UU|ht;Ys!x{c4{*IF<97Q;N@* zp||IXlVp=wy=FCI)hVU4we8K05Y$M^FZMmG4=o;R4eEE2_bkodigIx|QhiLZRD_7e zD^~pAV1y0MxOJwVCCvYTxURJO;WFa*7;RZa{l*H_u*FLG8*R4_tCs7NuB!!u$Tt#F zZ<Pzb4Z;={G6HwDt!_2cEC`l}4C+<Wki5;cuC{Y@L20Mbx3B_v8=S??%89G1g(nxD zZ+Wn+dAP22onAGQbya9fJQQLfL>^j0NY5XA3v{nk(Hc~8-Cr?SarQNwdXisTBHfsL zAi7-)l#HcJw1Wn<^J>A~g$jIs5OLS=#bvZDaIQtL>?@By%;aaDd;15-JpWSu*1*}F z;TCkT<<2b;{~`WW-QS|i-+<?xYr8}JBiPixN6Zb+_@Wy0pDEh4IvBwZNQY$CG|mri z=zEs3M{Lx+ttbmrlua~wy@;D-sFVQq!n<@Pi!v2kM?<Hsyk>H!CCU;Z{qRU9Gr1N> zLCC!PXkBDdq3^k$Od<B7+bR>T4?+t{@?*5U6or&lSQZi0vw0jc#h#=(op3inUjU+4 zx>sXecmMiFo53q(t;6^Go;OFCkv>6xX*f@m>@1779t|1twD{`xPI6d6O7?oe#zgXJ z|K&Xgjkz7A$sm?)J2n5)!;Z!M6ZMW7m79vOwB)r&Nk_}j)70^&^v&1b&FSvzXysl= zzNRoEt*H0}`+G*yO&2gVRvE{goN7L&Lw>mLw5qWsHxUnk09j|s7IE_nphRNW9rm<$ z?19o|R0HOI>(HtwFIuxvRoj4*Q+0_$agY)7TC-A(qES2aK98ejE%x%r7*@2ed`Ce; zhLtB_PTQnw^>P)5A-vsXuu(0Xg~c+QF@*BPh}3F~oplM_YB%nVWMhjy1lfY+qU7up zpeOeI@nd|~o<La`TPN+~F1z&{C;Inqsl*x!C#vjn<6lftvaagW@=Z8~+In26dbd(A zvEXUJV|gRt-ouN@ZEw2;^{A&?_wreS?Fz2>mCoajCb(_}H=1MJkUuA3QkZt{kFKo0 z3(qhIbo<uBU)v>xxaUYNpM|(0K%ssIeuG};D!sa#rS6ls&6J(_>tkO~;>u>@GXtE} zK(~Om&xj-Xf}3LtU!s?pU(f{*)mY}HCnsd?l`*GKxn&{;nW~iH`EQCKBFL^T-yZWH z^4!hurx3FkYv8AMRJV;`XAV>|mLw{7d1bQKB=LPm>4ex!%*xEA!*2UfCHoFq*^Eq5 zR-cZ3js1O5=T#z>wZ8pF%1<b8e0`K1;?+6FN4Oacm%huf+#K-OJf8O=ozd0JI$m-d z9W8wW&e^3?EaNXgof({-UeUPD%N{$`)+6O+dE_)%^+hlu=a$OF5BZ+Nq<pQweZ+U{ z%)~-k(t{Q);Uha}KFT6Wx*=4fj2>^t46GZ}qSs^j<o59N16oOMJ7<kM4qezEZp2xX zzUbzfC(Xbx*kL<R<yDaXmOVc|-uYmxOYGeRH>ZzGWyWp0ntUz(9FN*gCThRVznF+S zlHn2TYqFi~801f{JbgEH%%{TF>2wOmbkhYyM@SQ8iQc5=USqd|YnOf-2k+1#P*A%; z=Al!DYBtt#q2gqfN9=RYHrDJ5mRi8|N>7UG^{kIxdf!B%Rf$arPGC6aE)7??S2(8I zPE?z~9$q$!MS|n|B!!!mIDNWTt+9zRzjRBgoemtSkyvU^x!1I2gyuI#v+Ldtsmhev zdwemxHTRp%i|Tk2wYFYIRSl;ff}!hoH%F%Q+?eQ~Z6s`14hZNfP~mKeBPY@FJ|xK% zd)qb^E6Ty46eV(8oEP9~9UAMZIS8qBFd5jfvnuX6P>6p7Zl6*zHB{CbNSDY^A6S)T zPP*;5K|+y~JEDEM0y#J~IeQG5)z!k?tL8XQ|LkJ>+c?GfZYHrO6Gc9JKsgn>$r+9D z52N+&1bPA8ovz0G9Aa2Y>XX;rzRX)Vyt<mD`Pig1Q_r|DK^AW_l>g!)+~v51mSxT! z6G*ACFyH9oDz!eTU&qah@3!j-VN0<}$YWFdoU!pCV1Kjln|fTu@$1^MXJvmYtrj4l zoo@t#i|wxTBk@G3K8$J^n6{his<&jdX+w^?x(P>n3`4Gq=q*gRW;zI~V?J1X!~eAP za>WLmJkLC<GN7`N<(d1Um_8vNbbnWKC)ma=Bi<3Qu5sGH5bc6P+!oksTWCGxc(l=W zCmD$oHEw;eqE$E3|9~AuH|@R}d9#bdsB*jL(dQi8B!tuE?Y^MIt(#Y%gUf@9lkNe8 zv3o%J{jIaD@zQWfjq;)f(aVj|ni1l3w1I^TjMc(Xt&wbgKQmItk2_5m+T-oyZ-X+f z-pu`Qh~zslIdC~u69qc2&tD}{S06EMCI=2>7am|siaEm!v+vL;>B@W*eBql-Us=7C z=@kk1Em)b_i@mwc55@D=C88ms+J@sYPGdvxDJi8BUrW46$M?}B3BAbNlAFjA8`tQb z5{Suxnv>4Z3fc;=X~oe&E|eG^7u_==CW!0eLpOzw#z0+9CQLZ<;r_BcmZcKZdR0&W z*V`G;=>Cr=_#IzaC9BeJa4lHOGKP_f<4nE^aKGyS1sa`1+X;Tj?)t5x{*>wCr7_*a zmx|}bcKo%0sv?;taz?v}jr;Z$Cv6ShhnX>nE>I$4&(nd*@^3-%u4IfYQ;!3hLGtdz zt?OF8=2FHU_0FJ)G`e|n4is-A$9l%nL|O@A<iz}wFPc?Jx!fb-Lp=JN^uGp2w|43y zd0DY6E#Ag8O+<#9#}P!6)8<shcow*(+ddvQ$D06Ri@llFF}7E9sR<-kz@6swjY1HI zA{uXZIs4+LKy6WlgeRV`eLeJU_GDz9)GgY!T*Y4IQV)qWL5~Ys<C#gUIrSf#upDH@ zf}Zw(gA-Qp$*gTz9o`_i#4Z)}XSb6~9R%j7L6BSe&kA#FzK0kBWqyQpF*}w*BosSm z00^du3?O^iCIzdu^E85Q9a#AFS=x)}Ictco<w{Oj4ire5YvgNBIgb&L0x6nfpu9Z- z1Wv{%wZsmcodJP$aaM^GNQ#lQ#?9g^p34cz+ZxWL*`1UHYIts>CB!#JX^Ttj*8*N9 z0%#k4Klbr|<TAfYkiRbQzJY2$ym9Njp@_E+)vG3t9nCh0S9&X0uKT<9xhIZJ!t9KN z?|tL<+g~Ta-^>o&P>!gY37etMGnh@F)DCV~(jxz|iVq)^e<xZ0?V;WC(e?_Nmt>06 zo7L|pqTd3&;(4Is>uc6EnO?Y?%xHNXVwcXh9;Gm)nVhRYc+5e#*>(Cf;{>-{cV%}4 zRVzH{Vuy*6C6ww>iZ}=!4eH#6&O0^^fyE?K^gvxULK+C?W)}j)263<!ab;EPb)Bd~ z(_YH6mJTsbdIt@5S#aSHY-ESz`r<^k<LF-Az<JtY7K`q%8Aoh*N(w=lJO(Z)t%%z} zNUR<>lmRF%Jxtzi>4BwbByxRg>ln~|9~;e)+NAgyk_wS`%IKr*$CwwxIut)Ak%bp_ z>HI(3;F*_queN-9Jz9Sgo$Ky2Gp;5nozml<&?=CMppbMp{L~PoK6Fv$^0jyl+58M0 z#T?uFfNOKH%p8^>S-f1dII`Rf?O6R%!3~l{ytUooo^@I+{2$>cHnpsMX$S}}1CKK} zw!h$@t-g3~(<7MJuPJCzVQO$IOq`qF(A|95Ew?Ju7JWcE)0<kXqE#_=J)`(w!7e|F z`B}f%=8@m*#f!-wH{a=Fq9^w}7~hwM@g61H#_vYKf`=X+fP)UMFAf8yMD?nTqz)sN z!w@AZ{7h8pyA0C;HBl!->X0I6JduSASnkks>sJ01o?v;`24E%<S#4roPwOu6z=g;6 ziD4<ZXsXeZc|c>&s@91Tm~3bnp9c`os<0cOfAnc>Ru+;vmbu`Bo6;#=lE>eJs+SiJ zOZIlY%ZKP2c<4S5+9?fnrm$;mYg4%P<UaxwsP?*X(ct~>E~3T1ekn?1{QJ82Fx39Z z_Y51k_=J~&YWr*~(e({7S|m~q5c|##J&e?BSiz$CfSa;A`)a2UEXG-e*TW=i`#V&Z zn|Fq2)w`a8#tp6^Rrc5#gz8P?=~_IdGsQr~iP+wxMa2P~M#X^-xz%v_w2uPkbxHZ@ zjCX#1H`2nw<|bp^>`rNKB=eEqp2p5EH}K_Tf3c|>#VQ~5d~fCSAigIip6$J(<+|3q zNLP^%_hOvDV!L(cCz+zn8vSQp*oG1?N8(n!8v9PcyL}2kA9;cmYr;@t;TBe;B%9oE zv1Ei<nUu(6mI6R43!Mo49yz$?d~6Q!Q8s0~S<0zUvnWasFi9CZbjwL^{rYN7O^LOv zL{`ZIh|^kcI7aaG+|h}XMq@#3HhQ|$NHy!uJ=VSo$9})%W76&@_BfII_p6*#J@rCz zv<GbOU*z{=e&C!7L!ZXiYF480$3vq5$EE+<PsvAdqFJRDN%OrJb9~=iRSfKRCcYmo z@ZbCUywa?7^veO=tW+6$8=4!>#>a3kUxHQc?;(*n)i^op_il^ge-9L|Qq(mWUV5$G zZe=P@0^KOKel{f$ktJD9oiZU*${#5&gQYKfApD*3;mt2q@81(d@7ZSzXxcX9gfhWa zh;;87Fm}y-AITFSM3mXvYz`TnbmX^t?UI({3c*%}(}k{3xiCf&lBg-$+{RgyTkbnJ zP1l+GgftK6&drWCg{`C;y~6?@5tmsSU5&(!j3%F%$?Vc@p{u+%TItD<u2cDB5VFZa zhZXOV*g@Mruw@oCYWZ-ep!2G2w*3PFF3&H;0uj;SQn{j@%>~2b`w<>PUu*iUzu$Ww zwxw@ff=6o2t5u*na2+$an-=EOLraV+Pj=gahrxbRdQmAc4N{t)Yts1mGBx+>(HBem zAZy;sBQgRq>;(4tx}_fqxgeTB?R&&|quopQx%-ci5tL?mru)81Sprap32eFhHTMP! zAcxaz%(q&&bK7)?tlsMskVq`;y5NO7Vm&0vb?(DWrW2g{f*i8nhAo<LQQwG+AXIT= za5t;eFV-l#blpc8fe>p(NHkmW`wgz!h4i4#SpT=5aXRk|N`&%gOq#Yudcua@>vR2Z zhZ2{$$o@NR1!!oKH>V|cHM51kNb6eT;jRDUQz#@kF~#dd&{7!{GjDci+48SU*zeYr zEhtX7T~U5I$6#?xMjR`U!MlFV<Z$>tYhi2B9*Y>4bV$gaQM;SEfSKH5=Sz4T-=@S~ zW^`&MeGR)m<tg2ayP<OQ2=hz(Ztia5B6Y4~$xf2`Xz5d;AjYm-c-c1Zx}MbAWkt7> z6K@{l`fwwx^>A6d-bqNApaY~KPQHFIu|}z4t@2h%u>^gSN`T+S4cci94&CP)d~+d} z`fKbo<Z5i994)ju(9~eHIkc%aRo#f^^zK*O)gEN$Tb){yvwuMUeusj9Oa^m*ZEnZL zEwRlR#EQ}V!bS*-L=lTsLg7J%l*zMN+Wzp1gGVkE#b)2e7Hsl%^a9@U;jlbvni7eY z4(&K38<oF5wPDZtXl!tGg@w3CkKayme%1UjZ?RfV`;Z%nXkM$6m7Xj~SG@Y_Ht&P4 z*^~tR`?A&oLAFRF(sZL?N^L&YWin&yIFI>`)u>OCQhXkI8ZgD!H`q8iIUBYyHdjU8 zcg71UR+U<RFTb+Z{T&NzB+ta#QE>kIEWfN{!^h`b`p|qL7r<AOt5?>ojvO0!Ro8bP z%S@xWMxAPy-uorrr6rD=mL8Ai5^;T$<X@*(w&`dK42pvTGynQY^1pN)H+jS;_eC3P z8C!_FZat@1;48JkF$$&wgJ$Js*T{9qYVtTuk_$h3{o?-4a(_DJ6*2+ED|gXNhl;qE z@@=aL@Z=UkI?R`QE7TN+LaBoU7ulWVUf*EoFX8`wztaAMRmk6p2a>3$8*DS6`shPq z7rXy*|E-$r7kApbIt*v#+*skPYI}JbeyaL$7?7es)h7D;<8m5oE&+1#^6mQ!Q?`1` zmQ3MkWqEB>9vCVdj{K1~)>nWnB{I)s!1g%6s#%FBb{xTUryzADDe_o1%f>0pPgi@c z&Uz8!Vh-!oMk+D)x=$RP$>AAOxz}3@GSY9VJ-5Fsy(Rx4U#~Q<nJz`OOSIPBLnP!x zN>(MB3ObO%;-scKWt)BA)IKzp5D}E+*mV5LmBiu1P^u`pqRgr>U_rQVR?8Mkr23Sc zdy3hrpFPr5*p-Q{;n0XpUB42Ul0v7|VW)6P8Ti^R6v{)lE{H8jxfo!j5jE(-YS0gR zpMD4Zm{yeiK36Sc#|OyZJXwYN7HcD~RZ9M~$Zyu*IInft*177|qsSX}S)bZ%X4p7b zGUuslvbWQx>gk(zj@L6v?}Sf>DVKQcg0Tk$kA%|5+E%3=3iPL7MHP~IfBlrkbr zh~~EVdg@4<|K6cYaH0&`XtA2ZSCxaND-Bopf_3e#^c3aQVBKSwgO+c|^}^ooHLNAL z9)G6QDJ|V}K2|L&-2PhQJJRU<SzTqw!ts&6%Sk$AJo6a!JchS_Q>Nc3m9wDOG=2_* z_+Jj~{>#3|Zg?j+iQe5F-G&$0V3%~BbE$0o<_x)&)xuqRe0aW0*HoH8!(lz>^F_2f zl67#N@ni`pR<k4CM%AJB=I*}BpE-CXe%WfBHi&X7Y=tIeq6GJrCSzUku3KS~>~SUt ze^LBURl1^(X}A+*`S{k5((7jXr><JemdD4}pxIHB1g^@PCsL7WS?po1Ri&0m;}o$r zW6b+w1*UAFrnjUUzvk$^$T!aIUU7+nSLVfPVq64T_g_8Nx<?cQB3F284%e>XK1|Hv z>oAO_qTq6Gx%b{S@N@UWUb-d&%mWqY=Qm1Bhein95`;g__11Id+p3ZAx~90q_2viC z*G^Pa$iyk9Lk~09mOHbHo=3(Lgcclb;*@N(mA!mP^Kqv;JkpWrSrgbS$kd$l!U|B? z><KESQ73u47|0SWmrX)+O?JX<&zP@`J?-M#_nVKWuI*>g^k4BDG;mn!W5yDRdyXY( zXfZp;lw!a}Ma(%bP5dl6DvG%$lh{vAxA}%uO}xNdwV!?2&E^^eZjCQEz1ei1c6OSZ zF*FP-GVi`$iqr`75spy$0}8iB-A9sL@-Hm34Yy;<PjP2qCIcBzrrn6b_P6$q1SiTG z+|jc>g7*FB38`!kF!h)|wQlu~S9~N|Ptekw8dwXN#gGH}+?Cz0u+P6|?br1EcwrS> zH{5)hLpFRoT2wZutUEO(F`Ye^V3a_6Vo%`xD56NXpWn5}Q#ZhSYlsw$=vf!}T1fhP z>0_obUPcw|iuhao!#K0b9p~kLow5$OdYeMBs2usm%{W@*Tl|~V?Ygkr>_r?02X-#T zutLM<KH|DJXe2*>{%+&A!jj^1tRsp(G}PQd6<bVAMRnh$Vr1=>89%Ak*bDnd5nNZ_ zzNXI_5K-RbcHDk|ZS>UmAycf>q*uq6K5YdxBN6eQ0^8!#SW5e~Zqeut(*WhGoUQT{ z_>Eg?IvTn1=g)h~wLGo^r@p0pyb{mmfulW>7#*uNp-iSzQ8wa+rhYfx+dUuP{Rze9 zdE`{2>G^9b3qEh9C;$_oFsr|D&&;x@G!KD5^o$48WZuotx71Wx?25~>l=|vy#&jO& zvw58^3acYt@cRi~lf?axupoYc{$=-;`xi>TxD^)ONMrXABW#I0G-asOMw^2}0*r!d zm7n+tIr{jG>07(~RoU)RSWn-G33RE@m_@w~l#E=(V3iLLWo3AL@z=rZ-r6?-j-)LU zh#_9Lh(2!XRSY!PeU)OY0tVezU2y~A^r80e?zF)iY~u(eveM>*rn1yOX9F!?T{e*o zd?Apej1zbt8m+w%G9<>)ls6rj)yS?M$I|tZ4}P|YA%QZQSsob8e;G}+g!q>xdBN%} zU27iws<Gn0I|>k@NaIU5U1O~mi&OFw6KlcIgs)OFOw>aLdoxBqJtc`Ay+W9}gK=JM zI-9&Kkgo%OOKXX#jhsk7KTB8&ow-VD^`pWvQL2vv&g$=MS=j)U0$VubvX*hZXj}Wn zb&IDxyN3F7xUtVsd5U{udf>B*fS})C)KRzB(k-G=A+~t}ORb{n-mdeq0o|iqo^Mm1 z>T6utIzCttivXwX-jjX5-(3&Y7eI;}%y}l!ZkxBb%e}!CN#L--$}1Ri;z9rSR2|%r z!`qj_68ZNQKDKiEC1otBZ9hK8?4!gT+R*;3lEAxFb+l)!vg1uDGjIIe6eH0`;z@#N z+jCP7==O^(a~+7iaws%k!~{xM2afscMZ<{<1(iP95eAHEJ$z*p`uP0sdHqNO=JjJz zAg_2`8TyT{uHaPa?CMW(`x~PxW1YE6d2W#N6oNt@JL3dpNKKg{2Q3Cl=}RmoLf8|8 zq_d+r9%<oS1UEa$fKNuONK5-PBGxUwRZhNoj8qGvfhtKLij24zKXZ+<o&W@h>IdSN zcr`P#tFrNJMlXp;&zZgvV;OWA!}o%Ry9;EzN|Iw`wq%+yk<K-uHsYw{W0k0*M+3Rq ztLy#vK4b)n4CnIjJ>-V(%;Rd&ViP^hQn3oW8_X5!Ym%PQ{l-(6><07i+*X|EM|qR~ z6v`KcAU!C(pn30$m}x=o=BDvMI5E2v+OI)5kDtYXepw<5iA;nclVH3QlB;ba&9?uV zX`}w<%S`kK1vfWMuKLPxpPcuOW5iA3y_!|)b<X(WXRL^5X8qD&K{<5hqr#$5$gI5O z)qqitdNv}~>}QvwC_eGpqdmmpjJ(yah~G-5wBV3h{dUTtGpUg6OV<m{?Y75S7n7qL zM)XU*vE3*C=p`-YdhNI8!!Lh`W`hNN=rc_Hdx*)1&-457e%c96{k?qu@o4A3YwuXb zCs&`5(RIGD`97W!C?bu|JgdZTd>MZxlNNp8NQ)u|J-IT0I8<&d$HTNp^12yicCN6w z%LOeCrgB9UTGku}VvhG>^F-G!`pfXrpbb#u;HYH8JFB6`IpQYKog*s9B=+7e{M1kA zKY5GWFGcHi&OxNghtA|d+KQzqt+x)pTsQuAzn3%kA1ycN${Gj9f>%m?B^%;a5F?(Q zDOU9&rjW`L<&BY}^Bmv5>=}Dj&+N1`bUC?uPVZbuq&__EMyC+N1FI>1<1{B0ET{pV z0`xx~^!Hnw9|ci3uE)~%CMULvz2^@TSRdUeF|yt54@o*XFX2xUK`Cyi>z71__RvRp zwK7nG<!cIWnX>JbJMb`J`Vsi2*4yf#xiuWtpYL&I&5`XXtSQp_<E6H=+>^V1Klx$q zkm_iO&UTQA>eSV8C-)59Om#lqyI-?Q=a$R-UO}*~KqkZ^ud4@%8)_EhM;S6?Jn1jF zRh*(QAk`SV$5eHP`X63snikBUE7s+K-|jHU`x=lXxsZ5{-~IRCp{DZJkM~D<8lP^? zP-J9&f<j6ML`2)}%G5?w+}9+!ex5jC>C4Q#RCM1jV_i1d$_x~_z$A{I<V}a2hwtCY zrHr1kMyExKyBA(L%i4<AR+Pgx3zV$zFzs_4kK%k>MBWPzj%(ww-F-sFXBF4?^mOwS z`hcVt=N3;!jGCrz+vh;eND6gJc@t+CgO4%;I+4n)Dz6*|hI59rCfR`!@?a^k%FAcH zZL^AyuelRZ%`omRjd%uq(xSu?n0t&NOO1K?ryEpm__0W>23(z3ohUafG5!5)brJ9r zPe&)VJHpR+Fn?YM01g|Wvla1VC%1pi?;$(uY|QTbm4^~Uce}y}&8`~cK;lb;vI2Pr z1cL;m@yTxZbkh*~KMEArq{+FPQcanJp8Uu=YlbREI=Qfx*w-vm`RXgJ+9lo{^S#U8 zJE!G>aF&`%uI=^))jEvSHwKX`&wD{7lGzaDtk$<;WK5hrijSwOBqOq=npjkB1^8>= zCKEkVZn#)1iIsz%L?qeW`;6FcJLqDvVH}5~ikf)N*b|j(0v|asy4wS~Z3I5bELT4L za$Vy$fIUp!;9tY|{q$EN&QVB`FTHMl`s=bk{F8I&OX62nE=Wx|q3}6qi2J~cS2X?j zzGQR?SB+Pcx8&Nt`E=GzE2|xvVo7KM-_!H54qu9u8LbC9vpaBjfsleY+b_-HYU_y% z5iRWD4w1*<%HfK*Sj95Qt%Uqur$pzp-X2jz(PH*Mm<!RDQMueZS!hy8nzWZ6;@ECI zzseX8MxtV3YD+tBte^hgN5>J3h}MKNGV*(F@jbSIQUd=sYFUAj()`RJ$^P0|ekmHW zsthMQa8CI1c6>kHtO|eMpYF^0M}F6_Fh8hc#Z3`i>0{ujZ1Q2k@^u7ai}IZTv~_=I zPuU8;TbLqx`huurovq%IWjh|~A02A=PMz)RE=+e(XV^}1T2}Y+38WGMaOaiWtN(DL zzgN+Z<&@=zN>Sw$1b35NX|W?S2jcpxQiX2eNHEM*@q2*UV9YO(^+X*_<*GLZ=qU1| zoBgN~lrMFw)J2wvw}!yJ#ZiK0r<apixaQ*t?At4LIcY*0U(fwxYm*iyJx9%u_MA&| zy7K9}JD8C#t3KWRnsoe(HVaF}&=gx5#DNhe^@Kte64hM!EMZgG-%-h1`vxinLl2zz zZR1JaS5a<3Xog?sAFkk+Pe8qx!Z#T$0B0@?9(~?&e5U-)?+terR4A)fEWiuza#~Ut zmfA5>XOg$X+V$4_<U)%Pj}NYwkh^vPOt9mt%;gDIflKaoax3;6d{z-Elj+c1#8izq zCnu+3GfEE+?%arhZPwqDeVH`!8WwhN(l9ZlW|r`WMKi4V4930@k=0uqERn5`FKcOi z@lm$DToQPf!QS14N$6FSd8Tr?-y_BA48O9kGPyb%td3cuIXxXun`fY%gK}q&SLMmQ z4}G2|o`*AUZU!*?PI&wI(EMrCr|<;X_j*Pb!QOyfJ;Co7_#58%;RRNN&SOes`Mr+j zca|x4ZlLTJ6Gu;$AfzBO#gb&Xn>340cAY{d@7iY4f-Z&%e8kuOISgUH-uoJ^*um1g zxasBVy1ynPc36MF#KY6S1_MW*>~6PlsJXiG;#fq6=4$#A9bg68RE#aTMBILMGa$4p z^_q}qa@?+bE3Nu#$wsFCN7q}2Mb&=a!=fVHCDI7epfn6Qlypmjh_rOKz#u6|OEX9} z(hQ+=H_{B<A)N#7;q!g!_gwFD`Og63VL0c$@4ffhYpq??$}?_{S%OMcCec;NmA{0q zTQNT0O0N<7<FC)W_yX+*&B=?O#(aN2NQ=zJjR1KWFdG~`T<NkLN*5MERNpmv9B%@A z-$MBJ@0=NF3p$l%BLJIJvxT0XUS!9uGlJClbX#-6PE|E}=qioV6y2c3*Wgr7MP;$l zmq9UEax|mA2xus0oPuoU_;1(z>bLnA7`n_6k<WO-9!{G|MDl?^v*rkFDv{LIwl>X{ zq@*O^vH+`?kc_P8YM{IOw@wS<`aBOH5f}Qdc^+wQ&%^ktZRd4Qz1Dh(j9laa>S-oG zCoc3=S5x!;Wd}6T^Yv<N3*oD)?=wzWx|oXrg||Gws0OYB2(@$kAE6Zb4yA!MDt+~; zEX1EnN2nmu1)Ms~5xqRzX8lhm4hq!sfa~b%llQ=%2(&OLUF?UiZRRsJc+GH698{Qe zdAl|m8yj<*50bl3PzX4-n<JheBaZ++jCqmg!%zv49j&=MVs`!f$a6qg4}2x)c`_n1 zakvj}f#)IaogJNjFN#mpa)3IUV0IQO3-f%b4hQ>WgLQ7_SR1oG2khtCz#nlm@Y9h4 z>u|vbVEk?y=KWfLg<q%-eMG0pCx(RKjz_h1pp-x5{wUFg5+udM2Sh<r23owpFfNNC z%IBM{w~AG6mv9$(?FBj{ydYjHLWw{WoMqGJzE3<OBPoUDl9B{)0~hGjcWiC0BA`g; zHy_czk8KT;*i}+jeUuBoFl`7TCS6()i1602L>tI(<Dd|6{&KcAYt4Wf&l`V`koXu{ z=T@7}b5O!p<#irPz@P@0c3!1GHue`%wA}aig=>B#oI1JHCj!O`)x;hy6zBVf9oD?Q zpbQv1(#x%lX8a664Ug$NQ0dp0cLk1Dbsy0cC8=uF`CG>G2tIfJM21O#U=92WzuZuu z<PktLJ*4_Q^7y5w5KQF0qG?VSazs^BP@uzezEt}_7b||d)Z5#uhFB=~1#c!T=j4Q> zN-{Ebv9yXC(c@{aZEO_z`Y?Umo2%yveV`dx8`nm6$=s7DF`99*Bfn&GeYSTQP!GLg zV`o2`Sc<tZ**Q2UyE@HT)RGLNy~P_mq8`>N0OsrVu7QE?4%c}cuM^_oK_j(F8Gy?V zOUcLpm%q&95fMq3lw?Q+Gz&g1b=!T+TYA9T%XK9?2a<Wz5n*5EwPXiN`Yi<vjnEsO zKYv~Tm@pI?U3zU6(a}0y7RRle=6umlbr5vf&c14L-AgXRP6UI&j5IXjr#tdUu$$MJ zUg!IT@RJkg3<^6ju?H}`Qw_wG8b3#Xibn{?F*6(6fO!`S#p&Lx7VsZ_HlMI`v2<<s zaj<3^&A~8`4;bcPUKtf9)P7O+2m3z1fCVRFvW0ZM-@Fvzb<WIxjCd*<%ZCpOoN4Vq zVSJSj2|LF@U8bshqHgbAg)YkkR`Cdu?o`WQQdaE@`c{P)*l=Od{`CM!6#KX*Pg-~` zh{XcyK{a>u%Xh@5spTKlA`h}KWK*zIErMQZ1V?h10!L!x!ALa=9A*R^kL&Yhr*jr6 z;%aA$i{(SUUrwnnIZBro=ELpT6oFmwur0g@_=L3j@?*fye*9jxl7P7dOXQO<5F|hO zGvw4+K0JDUsAS-&qM<;(H(NdMfq{`9@+m4w66M@iadOn>dcIU3<Vo|gOYrcML6YGA z?pD3HPuTNMtGHy%3`-30I%pUe#2H0@#|Jmx9=&r@p-06+hkglpO8*!O6D^P0x0>o@ z^ztnIXr!BAW)V3%CEDX{bbkjpf=sHN=bKMP6PRA=au>gs125<eB$8w8_aQARn|b1} zBL%Nv0??NecuVz+c$}#DDZ`&3=HoSxiK@KveR<I9Vc_ezyx>X=ItTqa-=HF)@qd6i zPW3PicAE8dO4|9R+qbvL)`_s0Lgpz2nGD*zm-?@2XF;Ev-+NCVUI8V^ZzQD4xh;^z zV;OYx?2{^utK~}NenW}2R?BstsKPy<wY4>yrpE~CbIVm)7kl<Qj0V-^&R12%V-<Ji z<Mi_I-|_zc&1uDcME;eiu$76*R~r)Q(maWOq+-NVWaEml#`wZ_m$0xsa~G2t8;Z{^ zB8CPwnWh19z$ZbU)XPHQ(3@iJnVT}IS%)7_P{&;#&HxJeK~o^`Ie=92UD4PG5>+DK zWkaBHn7!CgW-CHbdvm<}wEl-#!!y3UuSEU`RQc8Sb-uMS)|2%|4+r$BbM~9ASSq6V z2Y9SUJ^>hr*d*sDljV7$?dMF{$~SRScDM%hsR}aKyROz54H^XwvFqHo`vO+o1wAAd zJ6G{Myf+VSGentRBz&tORyU0F0*`_62B8<Z0~1ewvFa&I+Qp)NnA(&jd`8>}t}2Oh z5{~^8k^CBe%l@ErFD@-f)g)rhGXKST1qd;oCm1NRAd45RTD8;nIW8mR8+M7RxB51h zecik<FnGA~ktuVI!+2!XxV)?B!rbQ9;lLTU{Wmrw!RH6LEKDRtB^!mUo1t76(<vDZ zxjv-OnF($3W8K;v$>*K(Ip52&^|KJL=ImmH>->b){GrSxLUMe$4)W%kO67%bG0U`F zx&1c!3@^f->G(@ne10qW{I68!-!B#^{RN++yU?H#(d2uEGHT3;$5V5^Mgu<rDihMY zRPjLYb4H<Drc?4e+_d?ogcy;#S}%JQjK@}J{?a9|;ry;!aAMSM_UboKLH5fb+#mq{ zCxt=6B=TucO@mcj(VL+9VEXrshXK@838}9!AN`RjZ6hH8C;yA~m|5o?JH^6TY;-KA z!UOZmEjgF#Z*yD=i<*^<4JVt5h~P*y+gR&Fzt?Fn`q3XtCfe`6$`&QEFW)H_9h4d5 zm2v}+u$*FZcWWV$(qtgRWGo1A+0eAO_;Rg%qpP*z8_Jo=XsuCFhIg%?A)ag46gLM$ z@Ef0-NWI2uOmgvg&(oE=#9oWFnA-gO^778km7c?Xo!@be>~r5;k#h{2UsqCj2LuEJ zdSDl4#mR3>d=;`(cCDMfTK+2d_Nxu0+jjEnK<Nl*>aN1o&6ree&`=3^6y@)MBYX2Z zul+mhpx^K^o<5P6$c-hlF4@s;N+U23+fbT_xRITK-MW{P_fb7vhVn#Clabvw0vYUl ztMo|o+iyeph=j&tW#4@j*Rf;esFAT-mLe)r3JG;{;qD=1#AXrO&gM;0Y&>O#kOaA~ zrC|7_UZn+J5^sr4Zt#k;5YM5<!OjqTX3hvL++LW1iX1$!6-xAXa`&&A<9Di98g5W; ztdY{TK}jWuk;4X<z1T((Ck0)`W-cqw?<DLV%=}4%M>tW%=0f@^CDQilW2BwlvH;yK z(!<(lF>M7Q#j6o(=hJ@QhA&k~^Q=etRWIQ$(22zJ8#Sgb)QBOy@)+O_vhknv<5z^Y zyp$vje;wNSUiUlre2`RH-{~<O-Qh>$Z%nZi;*(`JU)mjPhr1o*sB2s=QEXjS8BtxV z^90|>-YVmuVdb<2+&rVGccmf{-!#u?TydSnXV)c1vdb%4+C{+<&qp0D&3!Q@d@h=s zie6jF^*GCxN&!owIMhS`QCq&cVt?ZhE~M9`l)^x-n&&Z;HXGr%^2|ku_SxQ4HAD5m zxRedm94tJN&I697eT9XF#+7tN`7%zo;_d0lp+xQj4Rnm7%dQ;Fx_8M~b=pbiA&R)n zqTmuX%hoC{**RT~RN;ch(239T+KMT#FMbM?Vz3%N@eL&kr+SQ1f<$dwK>}<j-)C%4 z!2e(W{Jxa_F@OhDPB@=EcFU|b=!c?O>ZKc-wBms>1_g+mjdc1#Ee``Fm{^mMq2!z* z{H!rq$yGTJV=f)shOfv_Dle}p3#!LfaUYpbg}pwqQ_g`qkn1&c+XGaHgA#!K$pnKu z!-kUcHY>DthErEX%#%nGqKjL6PRHc_v&_dzy$8oS2v_rT9W$1u)%J%d&X3PyUB8HB zo)K6~zViR{IqAJ%U-i~-l5|8`F}U0BaGHS~N30dn#bcYLFL}&A?tDEsLv!-bMrT7X zwE>bFxjww9Gg+!CE}q`!>XKVUMMTlHA1fZ$0iM|2?rZrqQt_FImNv{$V7CFn;dbg( z@se2e3i+|U`|b|;wot&kcPket7N*RYPL{F-`wRM?WF&ug`HR}kijc^Kh4D9fh3d$Z zJr0KCA8tG1S#<DN^_%p(Fn+IyB=iZFGQ4V*zgZwSC|x1w^0Mld>k^u`cbrGf(Ipf~ zZHCwGquqB&Mb|r2l$>+IBJ|CK?A0|UJ#D<uwR2$W^KTyfe05FwYs^#G9xS(h#Tt9+ zop<5&a$Fmta!_(wUqmlGxth@DT=@0NS^_DuH$GreEp>yvtfTk)A!H`5Sf3is-&6K^ zqpj4dz0u8@)BI+#%<t?;w^muMlIrmJj?Vz6rNM)|^H@#^8bU}fFo@9;>p^ck6^@Sa zeoR69Ck-Llq;`Gr`kH>;&h%vevE7?xA~YQKNog7*86<KYY4={trt~CPDXBA?ZN!ZL zx}t*qbNceQ@`JTCKJ6bxz(~1D>XYO`=73>Pr}EOVThP!qgj23REBV1(6G?bi*=Pe1 z)mH96PDBx2lvHHXcPAFn0PNTmb2mK!U$Sd&vy7!9cNY|i{km8|I_JDcUd?9W+qHtQ zQPb1o^zIRW%AoBoNtYon%KUE7vAI|_^7*GBO;oDm>Jt`rQGyef(v2kjw!;D4Bc9F; zUY4R!j~B=47@CRfo}bI6dnJD8b<vgn^g;QY>68~$8NTzxI=X(p$K~BVBwp?wV4H=b zz=q8G;8@lo<Y1PL(1-tdhS^7b3<0M&Z%P1cgq;QK=2CiP4}k)=YQrVmUYrxnI(IZu zDho(>EgE=5wzZ>A{xLKySP8I$ncToaOrRpMp(={HeL;rmeg1MN_(2FmqMCXfldKHf zSxF64(QE#omJ1Ad9rd0GzUBtZf1o${0RR+M^f55YQ#n&=$CcZvmHVC%LV+Z@QQl@_ z(Z5PcesulNW_FQve^=C*o4%Y;fx!Vk&Ifk7ZTBf+()9a>g+ewAg}nUXWkDmzTfNCL zjaJSy26?=a`B%Qr4#ql@OIiyUd|<Kc>0GTZ^f0{6C=`URG1Z{)F226D?0a+Hr9)?& z(^)rIFG?MUL3QRNr_Wxtvo`!(t}ElmtTDMh7rCi2fPD{ygp0b`N7a`ZlK#+_WOchy z!RN7Ah*@4v*`#goJ|y3pYgCdF{h}~1SFF<0yPXrR(e1>vzhM2~l#;){G&jPl2Xt57 zS82;_H~r~TRFga!rb{!ba;>d)2Y9GRF#O?VecLXwa^ap2+*`Kk8b>{qP6G0slcO|S zpL=H~o~e>DlaW~6_qNANE(GMU?juK6Yc~CkxRLasgn<?Z3MQEZ5Zns~d|)4-%`TS% z>^$mj>kgRV!3=PxjASh!iOW~KRx+!l4Vmxst}@AGw|AqoP8_GVyOTh>m5xXO<9|YI z`Zuw@pOZx*M=~3fP-aHj`=nX(ngbF1+hwB7)j+m|Ga)S{ZQ~F~sm6I6Z-#x%ru&4n zz7M?`l%GN56ely)s1iJDqv?>!W|dtBh9=66H=B^qZ@R4E0y~F8q>==}`v-4k=3=uX zaYAjORYxDRU#dfhM`XGly6AK;skhbN^V}@Gcn;HR{>Nf8ulVN3qj(JPeA@YFDY2nk zw|kL<MHjoU<#0$v*aQ%?`Zii$Y%A^Zef2!4x$EyyXpu{qWUF;zp0F)yRU6E4Nnd+! zUg7Q=WB|M3H(B9N?(M&ndkMP<<a2hd?)#&oKi*i{Z!;CUlRbm2>c5B^Cj0+KzWXEN zTU7W#AkG*Zj)RG9609PDTSyLGnN`W7ag)H^Rv;E|`S?*0*Yhho#X>GQSY8`fw->r1 zKQ$q#57z9yP;*5!-AHgm1@HXO>A?M?D9E(MzwdstP&CffXr0c+QWH<}C+JativW^_ z<}otW!BHql^yGY{K>JHrugz>`E1`0#FNXoYQ<zk|b-Z47I-CM_lKGQd$uK5F@IIq` zV<VUxP&K|9F1MP^k(<k=TcNT`S<R%BWLfBoLJuZTO-3ClW)Df@XRQt?r!!D&>Niaq zuao5pvmq>l==5om(;JVvY)@o#WU`{h)6otW_dC;D#FzKbzScWE1PKj#A77<jf1yF0 zIF(wR%Jm6gdP8LVEkd1v)JsLBe1>qz<gH33K7>;EEdgZvvS6NIt&Yy1**zg^TKpxm zptMamwTfg(%<9)C-(RY!y|pjbk&!h0Rpm2gHOdaP0nwE+R<$sn;o#PGb%uokbNwzW z_N`4hSlcTq(O?8um)8Y|IMgpvDV)AV)Eb=`eb|MxAs;4k0}DB>IqrTdf+|u@c#LQC zU0C}>-DBsOkGBSyD)Z68jkUZlcwv(3g-aG9o*!b(`psqf+^dtkZ!vv(x;CwD8k$#b z1&ugdJY0^oClhnZVF0i%NE*;YZZ%&M3uL?lrTRy_cR0GX4LNW4qN<<gnw4AifO<(? zw|7x=xXFqh|Dp{bu7Iz4t&b%U$ZP_d_JT9ei<3-oqRIIFYjAoco1UA>WIU>)miI{l z#r3OLpraCD)JtFNUP)Y{D4q+!6!jWKC8hk~WO~&oqwp$U|J!mZ(`1~Ib;5LTM6Dpi zs#&d8K1yWVZ<C^T;LUm<ou&2mF0!1nzM8&5;$X&lP0H5|Z4vd=N+|~AsXmckw*~?b z-M*Y=*s<ZZoZ}kqv!2P_aUGn(`nRS`rv#VBFQP@==z8jV383aMrEUEl^I`Lh=Daz( z9DP_xFAx1>+BnI%4(T^KhY2Fp@|Bx|ah_;Vl&-H2y}BH$oq&edMTfrz225&^nZ5Cs z<8;TNpygaPlTlaAkeBuB_x(E2-aWLq-|**dLs1JrnwI=H;ok+*Ut3JY2(3F-Q)xSG z5~0B;xIk>K;(MtHiJ08aEPV{6Kv4#~rJ4t$Jbm?ZsZVZ<<wP3i2_XsP)Kyhr+(HBJ zeM}-V0$Ru1_=1M%jcqhl>B@xUlz(6V7axJZ;`4yiU|jd%^0TRGb0ax<MKh#PWFoZ& z>vu+SEivFkj<iaEUT`V-+BG!D8>HRMAbacAbiQ;@=z^DGW6%OF=}UoRk<hG6$I9s3 z)Qt2~ae3t8mxuOcK>U8mf|T}WPwCV6zZ;pWm)%Ne=g;V`<>(9Aty}YTX3vABNgc|1 z6A$|v?i}bBoId~>p)fIdkhR?A#@4w=mSo{f$H?xF?^*L`MNX*k1_^Aw^Ms{9j-1+X zzCRSu{7$!FQ&T>?(`x0#fX)|Co}89wR~j96EjjFzpL_?CY+x7kf>AYzzbOn?F`Y}n zY(pHcR<3(5&<pGAjd;H6RNR@f==WxmF<p^^y!R6dj<%*E<a6g(Q8@~s)>6A37I52J z_<+mT%Uo9dUC~u+;-dI5J`nG;KzuS5ygiKJSX(l5*^GJX_pP0KBRv1K{5L9ndfev$ z>qHi$%3?3hsOrG|H?DpPP2SOEpYw9ie(~8G{^<okB<(ZNWyd1j)}{3Byx+$8XcI{@ zcO$2++bU7l0Y43~NFevR&V;K{)ZC<u0&Hr&C&|1cA)}b-n%2NM=8U~{3w>X4QO@&u zZAWp`oYj4>@73d>dHiej4DmM2W)L>?P%(uoxM%mtGuaJ#Ispt}dY8AaibC3@FqyjY z)cIVi_SzzAXdMk(dWj7e)Xy4(b+)ZlM7Wp%m)!RY@+pzn&?66Or{{t-(T->LL~XSJ zg)6Qu&(c+$g<X^kjjUnCqQsF2mY%|9SJZecOPSOFdYxl>I&WEr=-x>?;i*Gykz*w) zQ3d$^Tyf>Ru9embz?aFS^(|fgDpapTe%?{T1vLBwxy1jA;B(J8U=hEE@i4%ToV(q_ zCGzV4P)AzLIhJ?ac$g3s{3>>v4oIj5`27I)=US)k3w;Sf=A~uLZub-k3sMkdVK_jA z?eT=X&xz(V=cb_c-&N2@jEA2jEQk}km8(}bUZk@<l4wmks}zJd(%_?xV?1;s7RrGn z06EgTE{T+vPCtfK$@d#-@#AcD%rUt9h7fSoK?s?aU+=y<(~CfCw0VneiWSY*=U7D_ zj0<F(Hm`_nW*lCh`BeFxV^=m85#b#Se7y@{zB=*oQjO>T9Hy<BN+6|?1Ms1N8*7E_ z<R@*e4)7fd$im-Bl~>yYHa$?#8r_G`qE-t+wjK9ynX`0R0xgJ*l#&o13u{)>wBm5p z4xY>7L6p9lp@KU`#?$D)13EZQ1BRpcx$BFvr$Lm~lj%PBR2ZBcMCz5bf*5jj=er7e zb>|gsq{{|)OQ9iEBH3T-q<3*WTLj~j;eMK-E~gHmPWd6k=ia6SAFLAg`%U)t83TsG z&6P(Yt|>K-Rn}L6?QYZXud$XU-_F!|7C1(XBN}ugpYkOkXQypZSBicx=2(K$Lz-5+ zZAJ>QKAW8bv;xFxlV8CLf`;>4q&Df1(_#f(fI&5_X#W~@2--DKbDFAWz~)ri2bWs5 z2FkdnM8=gNicU_YMVxqVYj)y(w9d@4zFf#TT2+L68{1&hn5ETaE3+R2Zp)iNUzTuF zoI;-xyQ^o#R_xnOYlDr6H53$JFPx`x!dI9YPT<n6W!e;NE>@vbQ71}G@CQKag`Fxm z&F|etj_F@v#)J){YPnW6h!2|fWu^9!Lwh}0rk|Ax9snC|O}kNJfP)K%O6NgB1du_! ziz<9P4!zZT!Yay6t<n!%h^H$Y6=+m$L=yk24fpq#KSIUEB7H3SP5g4&$l#hBdFajk z_IkL_z(_x(yDxr!eL+ZwFV6hF5fzNGfGlo-m0lT{2DATK2TYmTc>xQ)=9xlHE~I{o z4(1>S9MCyf+B3=7)7v@|EyNi?rg8DT7y&8@<bTyE2SAfF|9mQI?Mmv0bH>W>9zLfb zg9AOG8(1b-#o+E{EA~^HUk5fVacp@{WN84KUTq%e#axM%ti=U4c13D@w9>{9xj9lY zTO3T|OZhku|J9d@w7lO#%1SMoM}u77>Hl0RrEjlSa`>F020k~Gqw~1Bph>f-9p8sc z2tdMSv+I;}V_j(ok@PKPE2;rpuW1RlaGJx7Ukks~_h#N{ZwkoRL_K#*VTV}cBA+Bz z-9w~ogJXB2`bytX%nY-~a|Q=R@r2r|w<q8`8i0iF7vV~$qTcXDL1ldAtE4pNE`@Y) zBU^pN=Le}^d}%{{fgBte^X_%FS(VszHYRQj-?wkG&DdrOU;d8Vtp-jD=P<oIB}dBZ zN`6>f{#WFQS<M>Phr({#Ph~Hhtdl)5Xr^4(aIH!hcCI|<YOO(Q8*5)XmPMG^vzHl* zrLr2zeKtz1CIk<JP0MCua+;qMIigN%^rm@m*lb&?>qpU7+fJ&o@s$||PmRl48MJH- zk>j}=R`J)!dB_ug4_BGp-Y!TJlk!G%nDZ{d##_7w+qUO?0r29cK9*U>>?l_ve;VzL z6$A*s){mb*`<t%zXPe;<@XSA%=H=O8iyA=dv29_jVg?!-;pZEPuBg@M;AskOe(r!Q zD}U-LpRPe}_bF87RSvZ`z75%CO2al*IWq6;uBI{kRGKn`eB*8}b~bFsUZ=uNaWHc~ zW~#*eHeGm)@7q+I9nvem*OGc7ZPcKcLVc2t$7u-3+{NE!MP*xeh|m8L$n4zc`eZ_W zo|+uYgZ4KIT8Qo~KV+ue%(2h3sXE^l6e3{@^bXXOzI6gr5305F!zX34U470OvG(mm zK=*+PXhHZ0Z(hY4#4PL9nN8CkUq#=!0Vm2b?inJUpuj~GZ&G&i!zEm5hQ#65YSJfa zX8{Z$mqIIFi{Rlks;S3N&51-_mwHzQXJ^!jwHX?X$56q?^$SMfgH<n;j-wsXI!chC z%dhiiV>x9@YXSpou%Yq`m%%+lx<-{G#`?zoij8FO@fWBD^{X}$&EKfU1)Ig6e+U?s zV0Cukp|nOcs8CYOex(2(s5@z2Xz=;6acVHMcce`CsDtbb>%Bt_EQTDXPL`^cztzTl z+o?ItsGxR!Dka(Z6+6Av`a-)6ZCq-M87TN$4P(ad^-w&w`%MwUziI6OeDd_hf^Pu8 zXWwc~VNNuo(n>WZ_ked5trJL3`}zv9v)gy!GjM*_FGS)Nnt&we)@Gk-HopSBhdqO; z@4~5Fkkay7p^XogF2FY3*#VZIJXM1}WSAhA6l{@nE(lO`o7noucFh``E9$UGQm*Bj zZ~LQ9dq+c`qr9ForU!mrL{>&QR8`L_ZsC~>ponYpUO03P%)oBo(j7*yW59PX-g z9@&v=FS;IE>2MvmMX)YWv1?z#+DNdWp1kkS{^sTUD^WG19zGrner;Gg0kAp<9n}sR z_cu1m(tZ0z;X%~gjQpvsfMTybQ=FVvgxQcB%m9BAJY0V+DyeQhsM=NWdtu8;M+@$G zx+ODod*o(yk;tC<%?Po2&5mQbJig@&50KHEetvA;P^_+=M2m_YY6bS+Mc)b6(2bLq znj8~|fF7Dg@m*IDKqxO`<k0!wdj~OARpozd@$SjvT*E~d&*vL@H1ih@OdTM>XoiGZ zCnJWjLOy>!<HigAp|uVa-{oqz(TNO9VG<=>6`Pd?dF;E*lZJZ@c9V(D%iX@!rSn(l z$HzMM<^+&86EddQP|dw>eN`Z17oh7Tcs(qlxA|cju8LbY8<zuQ<^jV)A|7438e0wg zcneM~z>azhEsYV;inPtA6>D<ZCg+C?R-u6n4lCC!6ZQv9lFX%UL~X2&knzu_0kIKX z4}q%rJNVDIw0Lo{H>9HZpN$>1-xKGBp*nP}^DtvJhNRPLt`BZAqE<&vILxmDR5px~ zHBG?g+OfK7zi9IQ^vir<^irSKC8@|%6PU)kt#^FFvl$6Yia9o%`8Bx?fN@7rDt(r_ z*-3=G(4`KnvmKkA;`x-)c7e%C%hSAr>G=AYI-TI*8VcBO`3M2ij;>%&-JBv`^oaBM zT(E|VOrDY3teEf+Am171GR(*N&($G9lPLw&8hSCZKAC<ho8g*P$AXHrY=3x!j$Mfg z{-%vP^DICGlv&&SDr8<suj+$T%4AtiYU&O<S*q)hezFhzq2{_G-iHSn$K`czRyU{? z9myAZ8&wUZFDGNB<14%M4Dwy~kf9)L!10d(m5q&k(LRiv-B2L0m1ew%;nC+msAFP} z7=Y8t>=p850kIyAXB{SHG;Yh;_2qRBFOc(bJp7~kV%e3;L(X|w<zYWlul(3v?^g-; zF`&MCjZf(d^zGstw*rz5wtBySUZ?~^N}ZQ>WoPQAaV>)&kP%@VZyBDo&4B63Au+3r z;WT#p{L8@I()?IJq_@DMo~Ttl-nF@jTX+uWOSJ_05uFqAE;VUIi!rr#KeM>5ho0Q$ zbG*Svj*hR%!Sk|!zVGxS4JsmJ);T^G-naYw`PLuJ7_<0vKp<3xB*Dl=uOlEPC$6G` zJ7D-byAbfPIHOqsa-e}v+rQ-ic-9paU}e7!i+%6XGv*;=*(pREzzenBvfOR-P!w<q zdv0lH8;Q@CA>jM9dAdT!1@v~xLT+<G9u2o;#)?CXT*M5&dhe@P@Y(*mS6jADUrk;p zY8meVEw;MXZ{h`A6zK$lFYD{yyL4h2n_D}e##aGQ8;_EX+nN>AWjwu)0l8?L<aP73 zp$-ohV7fb#HbHT=9@&_6{ap~KyG`d~JvdIQzpcl(-S&t^u$go%5VH=avq@E%?m5bt z=4quBX9&ERR5#eKD-O#6Qsweqf%lF56iE`a_@%p7uDkLzvsDN8P0Ef9!Qi24uMGOq z8HachGLoSo`aliU&Iv-0mV(wPnOr&zYIVz<QzI2H?M}i%B8h@u>94Hm|9#;<D<>ui zjcI2Z-`FxkhVEiNYd2o83kJ?G#;=~IdwI?6Gc6lrg@98}WSjr#7T?Fg43LDma)u+A zoRp}el;iU>TW*|M3`g__9CD!ur3^uFz<kW9u#8K$##Vv?bV%|X<xKUBS?-{#u3Qr~ z`7guPnmu@B3TGLq@o+Iw3b(Zq6bk*t>bl#VwKGkNUY^6dH&gVvVufL>PF8*Vs?5*I z32&n7?d;00U#(=ds>aK9CGjb<u;T15M_lX+bAz_~Yjmh<_J<w2__}IwGC4t)kHzy- zs8zB4B#kX5+!L@8@Ur}$fZqZ=qynH*O#gx~+Fao7s{+JynVYd^ghm6Bx(9O8gjTu% zazk2L2~2_a<C0cGgBe<C>`Wu^yE{Xokd#+<c!p1ykt{uDOncr~_OB+5N3^L?0kiYY z4sO-V$09tUnE6D}#!}lamwz*?M0<<+i4rwju8ur1^D*Lw$eCyOU5Tt24lOfM`?7YD zb-nww9~YZ;-)`mX%F+V2wO-bJMkVYcCNoZV(J_^GBL@f1y-J^UQ_pH=+j(RE_!DhR z^NTs3GW;;g_t8tybQ143pK>nD*qO4&{}N0uuH74!5-{qWn+CK`22C-$J^MUrlj@0b z%Ig)FDwT;Ydsd(iC+~^fToYJf9!XPnsHeN2kfUQq%Xs=Y4|hpIMcBEqrV+T4O6faV zim(IfXS<mg3S;vES3o4Cj{hnnyEzcRUP67wuV&V2z8q@S>lXH6+szDo|0-c#_6{%0 z^`R|1)F4pivsRXLQFuw-ZpnQ83XlzlKdo&5g10rGsDboGnb2<uaCFhN_Hu^9DsKCy zYpuy(L}VFkN7FnmgAeI_YXwoYI*#@QfZsr>Wka))bNBH9u|?&Dp-5Ye-(iyD=lGlL z(R{SA0;!!E=lv73nQ9v!mx+1y#G#6M93SP+T?8^_wV?bqq9t72U}efr*#CJ(8$Mqw zppHummXsvve>c2X&KUM#t~U>eY-)($Aw<|i{)qbQ(HML|*Hv74$7Z@By<F5+ey7WR zM~@uz@{h@G*5*(Sn#Fgt%G6FLM3jXTO*)m;aHgWDgu%j|q(-k_PX|?+Z}N%WMPKGX zS3i>rM^btmky1)zekjz?&=&K%+T+-oBTZT4W@b+Iy)(MYYtL;+XI+y|Aj<HT+z~M{ zHfy=*yk?dB%v)=^ws<RiGMVUWz8;!+GI1`#TWe4fM@TZZf-~r#x$2M(3Bd`s*4grX z{$eO6r2DDvUC&PZulk9Afc`#>-em<ldcdRNXe5pG2~n^LF~o5S=+_wOwr8mTWONMC znLp{cxaGLzZm89SQzcd?j4*|2yl$l+UKGY}<iIHK)GzAiXck={0epwDzD8F~G|4qT z8V773$qOlWRx_T@gBL+0O><1+5|S07M_v(ja7_E1ewSj6t3u$~U1+;X?taO1ZAo|e zjLuKq$bW|X_(^`_8dalg5b|Y%$LM}=49(ug`uk|qLAsu=?GBZ7`LAvIQh*@ROi>wN znxj6MK5oAFl8YhaIe&<l{1p=m%d2D2JLhP}<Z*0hgIKGujC9BMqV&U-orN-+hnxm7 z99_z3ia=*9Ul9)~%e?I12XsfQ3>GBqiCgayx9PG}WE+}+v(_Wx%+CbXtYjC^$kBGZ z(ugV7soZ^Zd&e(hb3IDq+*^#oD9EdHn&Q30im@P~wbSf<Jf6c@qbj7BEHWf?H!-jR zY={E*V)XWxnqq^W?6D*TD6n4uRHv~P8d%KVBNtNxh}z43Nqum;T-FIQ4StPhxU;&8 zD;P)tp0$5{yiEbl`CL#*m7V`pdm>fej^-v%J4uu?ac!mncvSWK2gx2(LnVw8%^{6} zffCa+Jcc%YO)gFNjD=Xf$8k*y$_e|xGvqwTQ3ML!9s-8`E!IvV>AdLy7q_64qPU)o z)=WGT)A&_yQ`6icCwiB)XV<9`J&6@#`E&@6B!^RU8*S4&p#tV$mFZfoJ18q5W%rPz zTzqBGsbHcbuuHF@-(KYJ4vF|Ciei+3-qdf|?7d^f0<7@UBB_)bwraL#BhRp*@f2wY z#B-pBxB~ev5yStKH``yRHOP@}PX~j0ZKQsO*NqLq;rj{};uhNO{7pAkUb*(%AePvy zOAJwiyNQ-NoV~ebsXbUZ0O*p%;2PW_btd}6FtiTfEW85Ool?BiMpJV!*m?TC#XQ5e z-}wQe!_FwHR2W}V6E9-@IPgmQc2qWMi9w$aVU^5W#w4CEl<D{GXOpLUZ53wdyCplw zA$?NOC(5hCj0=p!r&p5)EgOY((nn#(oCS;wP0}-f)h3X?=2PSR6-ZohKT2%wWk!nZ zF#EF$UzNiaU(-j%n&9GD$Q^mme~c~vJ<D5M1*)p+RI~)F6mqE*(bs}K0Wh%Ncy(xY zf+$beVS~U;9M1??fcZ_Zno$5dc1frwJRcBw6r6HbBqx3qPe_yt;$xUCW?)GN47|52 z2~MYZmdk0Dw%7ayktAf)&9M<ayNe?9jUUEJD+Qn6&8G<^OWv#3r^aeX{}7bE`aq(7 z!<<9aWQqYV?XsQi^H6LJKA36#$@*n@Z24=x)OXs}ri5r|P;IfMagK|W16}Iz7d#-8 z<i%OnO3Eb4HI?P{?Kj!9b{ovLEPmPO60%uN*ujYEVSRVfzBe4}G1q8H_DtCF)3c2* z9IV3%*YLRhoNz<+$dW6unJQ&!hnDeWW4v8MAvZZGwXM-_e8CZJd$?B<2`)DhGqDXx za*4~lo?RCOa^B4Gt93Ygcc1XDvEN|BtP|MRQJE&kZ`o$6HX7WDtSwfym04`YbgAEU z<C!q1$C{P&@4Q)WI&s)tq_o!LApuYdvSGl5#_iy3?V^w1tmW2zV<LrO@Nlis64mp> z6q|ddTBic<;L=sRJY#GbS(+f-N#+9U6egfu5X{NT`OdQOg!tWzh*RM^op_1-DyGt= zu?^uY$QkW2k!m3epTTAS?pz6p--{@fU1ciK_KXs}?9gZO!XD{#XJo~aH-J-yRl|>8 z+9)n{G=Ou%Qfx5vzf>FC3J*ol=#7Gb<^{+NN@93r$3q%PA@cg}G?-*+f<F27?7*!b z4KNTjVNl2ors9KyaWwsQ5_H%_4Q@ow^lL`=FO}OL@i$%cWfUTc1BQj+-co*d2coK# z>uj4te%B15{bw7$UA4Bl;9RXZx2uUQTyw7l`WLBu5#(ut4iPVulBCZ$y!ES3w@?m7 z3Qa%lw8D+Nw0xG_>DM@g&ja%`>LK2K%SluPr}PIciYT$EEn*jbjYixcbACZcVH)#7 z?mSnb8N61^X3L}|VY1y*`vVs`X0;nzyerD*G-+X!$Ym4j>{cAZp<b2wy_veyQFV^d zag^IY?EM)9ubPrKm+Jd|$;-)AnNoM>EYG-nkg7i<iiO|Z1~8{^AIQ3Hgnm;tx~`mY z^=4tKQ$n8BBCoXm0Jyk6lL-Cv7r<y6{jo_R-__C4kF;CqmN!_%L=;faEX)4fuTK7W z!$L+d`}N4NOdU-o(w+hN@#Dg~5%i;@Da7~883W1o7lZN0yQsQppmc6xLh@Es!YqCb zuQMAwR?FaFiR)$>kIUn`%H>a<X@1lmuvpP>Yl$}6X$PKQDZ5%PZpowPqu;+f)(-CM zQI#N2h0?mtytJC=@!mWP3~1stm#iawM!a5qcDey}aKIa?7J^xDXj0X?$u$Wd1ev?L z)A?bW!4LwQJ<$NSX0+Y%Jh@dJuN~NwBOkzXIsk-BjGrOk0BOE-jMXtzXCatg(;>GW zO6Bj}o4qP4INNaq0SE2=JYOqK6?=w51jV0+(!YqKnS%tl;`y3{p6)9!9zO+_*SO5a z^HPmWTOCHX5Wrm&`m5!<En{?X$t~Z7E%N5)kgqJ_3AQTqGTmcW{Agq4<oG!6;O@j@ zJAEoe;)ZBg9cPH^tb!jRg6KxXbt%1y@jcFKQ43Z=+s2^XYp^8ncv0DX3@c1;Y&yGT z=;aZI6#CuHb7<ZU`5mKd|ElB*7GufbDN2YAQ5<qYy>fKrEn%K;6}LEmuH<)pR3(I` zrty8<nurVEwm<uTNL8Y|O=v!!^(!#BJr7qtPJ9x5(#~;5MV|qJN@h4^c&8^<?~8HV z&EO8RUyR5T{-nWVaUum8frUu{oPSm|{#TNK==gN&!?L#6;u$V*JkUN7?jH<!eGlN+ z7d@7Ap7Hwea=W`Un{-RdVr3)atKz3mL1Ph@QH3g}!LhEDZ;#7$-l(%2&223Nb>cKn zvYIeVv$o0B<j$1s>;b+Vr5WN8|4?QAwKVTvWQybeZeXm!Mgw;#r_14pSV6vA@3$St z%ZDq3{fYh@OJIQg&UQ!&WXeS12a}ewXt7H~Ca3bixXep%swB~|xh`hr2^S-eok#dx z5hlSn<-d=$%naBFg`Kt^BW^D<@^9GZJ%^85++3R#n%&chn@*om2sr<kgEy$nB11ik z<AWkvj?;sB#ZOkW?j%bDopY85%tkiC@5J+P4roxpJi>p&nPNZE0V<jm#xZm4)*F(h zKXsbX1E0zo5UTs6cE2BTD3>Z54Uhl{27+m&=yoJS{6XX4r3#rSmF?+O9PZI8htK?B zgG9%^610chVRo(1Rdtu+o3^E7;XB34uI`oGIf*3Ftt!A-gJ;24F=}#h4+wI}8xl$Z zAOt{CO)dS0YWd$V@T|NThC~N_m2?_-O_qk?ba3aW&&$J;QQi+bZ{uKy!*`Pmm#~#4 z^P&UFNZ;>jZ$XC~ET#H><;}S9;(Ngc%U2X|Gl<+daX|_mBn*@n!c8$YQN?ny-FH{? z-KF~?88A{<QB3Cjc1`6Qkw(~{qM^afh4u1ND;&G=_Z-evYkg&B&013_2Kd{`vgMNU zZLCto1MX%IvapEIZ(6?BaaYn00#y1!JDL9^mp?$&z6XaMJ}Ls18=q9B=4|BncNb8D z`#xcgAup}KoQA{{+Wp?OLPNsKl9rTKH#Qa_A|f7-QJqLP23FTuDn`WP|Jd9Q9@{40 zw^tbV6h;*f4ysHj@F9`U`1~n-wnfKjZqBLvB!Xkk3S9P9naBWgV3N6?(xYI^yqM*8 zo;Tuc!Wx>chq=g5wUagms@&p!1<f6tJH%bS%j}u!(%Rht+>S*C5>aG%vO5-)RMJ%8 z&IDxXqkxg_mZ*{U&we+3Qh{|P&s{H3m1(2m%ZLTP9nCIgz)cP6D28*hh(kyMk3jA~ za$f$yu6sSwGT|_&eZg8SzbSj?zJ$ii!jf}vbK@8P`m9>i7akh;p89>GJS*Vb`^UD; z5B{G#RD|=gAc5b@vnGzpR$zbQz#JHc45s^YYXfig_mupr5#`B^WY737SoZHP7h}YL z+f!o$_xHWaxXZ&{84YrhbDu8uz_?lq_j`q-hyj*~cqShudi*^8ZoTDJX_nXKtwWY5 z;?nLed#a9q?OCBjvmP>(WH1dXidgs99&JKFY}OmG)Q*R542=S~L<qm%?=d5_7I6s; znBCpA=}h&pJkfVwfbhU<cL*vQyBgB^-d0G3INrmNZ8hJ!b|q;#s=ZTN+Y@xoh9lPO ztbC3?@th1=-(@d;!L=Is9^V^VyJ2|o4PD;vs{NJc`Ded^o5nePIdi;1C1I~qXvXc8 zRCT@87XD1N2~3xRu{$r~q^MzrjZGqJ3XPYxbd)z&<Ts|v_J|RhNG0d!W_S9YvoM#z z#KEh#be}U$^K}7NceuLlkwA6&7ygj_h69-M`jl_)@=Fthj8c{atadba?I7Q=t9H3j zfw}k9j5BLdsEq?G?d01e<4UV)gSRsS5y`5GgE4zN;kv4&DpJc)K^4$PxzIG!1+T5; zXbggXk@5A$uN2ge2Uc_jjd}dD`%HH4@bZLC<DWgBmjE&|F6sZ@<mr6B$NN587+i6U zGBj&DK>08@(`VPH98K;y&mM|ew#;AkKb-3J_R0&84dfPvsasHk+^;GaHAXZ1u7xN- zj;~8n0#xKdg_+BHZRZQw)kO(KCHKcChwpXFbY!)mgDM{4!YfM6Rw=Ug_84vmU1!rP zIz9T%$-MUal=e1`ZfWao<q)}W{|8OmCa|NRM=W0Qvdxk$7bEApVRwjq8A5z3@(lLZ z@`}(5pF7zm5Ba_yUr^D3dfbxia6oTt9>+I>PO>rEMcy!HMb@au#gDjE<oJ3rpPD1s z(5nY`tni_T@|b&us{wwzm&JAtSC!|8DC2p<Qg7k5_5wO5M<pZIP>gzI5iaBxt8&$! z6k=-o<Hx%cU>DHLVZqhj58Qf=sxq8ll>w}O@+bZ7-*S354X7y>s9FIjW$RzE#DC=t zz=0(2e>&<>SxKlZnuMJ=uIIO^uQxjHr?VxfT(yOyH_p7Fu=5!;a^4KDC7j(%(t@k* zp24hdth?kwAF^kD1*lAighfK(jVEwIcy<BJQ`4Ut)z({bbM?Nb%$i0Efb)%H3aG{= zYsoD_a|te0b$J?Tb8^sf7p0^1#*^+WH_z{eTuM@!^m5czS`cv<lrdZH^eHHq<<Mf* zl|tcX;jT)jQY;mlr;I>ouPYoWa0`HG<le9-CUO{LZus3q7<gpdKC(Q!LFt+CRYV8p zREBigbc-^Mmm0b8&baL<j!|9F>`ga+axF$oS(4yIJbCg&)j<`io;1kF!jydz5$-w0 z(pSDIJ?_=(#&|sH&?~E)`#|Iu{yxwPGW_{j`Y*Lk;k~s6^I|6IKhEhNp+HYF?F;pv z3XK0JE%@Kf-ljrgLyynV%Nk||<|lWty<JiC6RdDz!dL?{su<EN5#m!y8T%p=Qjy6D zH@M`e5v%Ph3%+LRNn`?nCK*&xxNSO(VRlnNs%6C{Xz+z*6NU!orHA2&u(U!;fRir< zqz>vqUI)WW{)6cv1y}YnP+AH7*8srU+2vEv>#1hYIv*S?qhck;X=>Ww+E!egF`LA( zAzZA=Vurss@^b%jP`N;{YyV{3D7|DHQCaxJE-q13-0VC0grR1W9@FtLOuRxU7;q7m zQhe4ix)!G~1GA%w4{nb(dW#C~grn>4V*H7<ExtnX3Rc<MUA<M?AG5($>GZWnJ-y(5 zi}LqLp3il^Hs@h>5DAMkfu^U7e@D=N$=&%0?%Db|X|j&YU%X`ti_yh5H)Za?hkN+b zh-1{WD^Fz8ymZZg;mt3O$*)}SvB-G)OZR}m$)mv79VV_ZzAP2kpq+7>Dy}Dn;zXYT z;!z+I9dr7S4`jBBWHLJl9WBFQEoSz5GYp@$nQ?<TzB0D3HYP-Tw$@Hj^!6}{k)3^T zcfB{B<>cJ25j=S$5H3}oBXy~vtCkHZ;7mU{Qe);|F-5W$*kQAsN@zeH{D<F&e+mt- zQc|FweEwbznM?S9vb#<AoZs>9yYVm0Ncr={r-rxN({v4<XAg1?SrBvLL<{<<;?>j| z2i~5O%!0=Q%+G|YIOhG}-T)iHdn+e#b8|$UHSV~wuWWz}7t-M7D*2R|X2R6g{ds7& zT5Ee^q9Cm9m$ToiS4$^zPNF$AHnV~}KFjTBo@*0)1yda40V)iF-G6<I-^BM1a6a7C zQ>}K>JIZF3*J|p279PDN07nW`Ug!%8aQyxAW_U8~7=IF_6cFSOP6iGBJoVpsG#T9d zEH!bV`CdH=IC!{AABjb5e3!nfu_2`aw#X2H<KH*qU#C!}Auev>@C%A5rnn4D-o0VH z5<8ETJWX`)+K+SgTV%FW(ZB}ht>u`u^?{VFy;?X!B(0IT8c+Z;LiKQ%nb>m;K(&)e z4knp=GGdgUYON<@Zhr@pu-Git8wn{-h6l!RG2S!XA%8L5{h!@S@1H?JbN~KKnSidZ zL1Gh-8Ot2?Z8dqFzhPANk#GUVJUQ>*cud$O;bd?EleA;i)YLQpZT13&!Rl*=nJUY; z^z_mzHfCl=*0Y&2V7QbNF#A{w0nA!*FQMxC#Gvq1EGQ_b5DrMRyWW-AL{Wee+C<}v ze4QK}8-GQaw7JbWpddeaV&8Jj2<WscrmSc27ubY^^eODj%yK8M96q=MgS80>2qXXr z?YD>s%=?R+?@UG|^IC$v>fuFNWuEuH$Ln-kszkS1t&3%;%R9co?Ldh_&{+l1yOB_) z1_nQL*_+WkO-W7NEwY`cK(|iPBH=JR0rc84RZyfyPx5_-j^Xr1ImB$b`M94Q8}H{^ zY1WQI%Aml^s-hKPpR1~&tN53`N44US=fK!yksUr}=CE9)w8AT4-<!IjD=Z9*Z}-E@ zPrXhyRWnY>2ni*R*9X)Qlp@|!=7_7q+V2>|tSSgfq3%)*5d{nqb`8V}dVyf|)Ag5> z-oM51J2~@wk^spp+06fGv|SEF99LhXqocPs!4j2^_Zr=RkyX`KRA}e>3!t7OKlJ_i zrY^18-Cs?x{1yHJH$$!cC?a{FR4H)#9Sa|I+t1h<)luzr+n<xQYdpjzB8u;Q0dzhE zA9AjN+-zl9E(b8;BljjbZW2b1+bgQHgCa6s*)UUxx<X>k4vsCmFD{QaithXl=bA>< zHJ>A6?JfA+TpZqz8f5RgJZp7wTUii(O<^bDu}=wT=p$>PLT+76UZ1@Q-%O+K&lk0; zn*;$>`%F&{!u0KH{-}=T<_Vgr&L?4i;~BuRvqJvE{>xo-vu-b1!U7nG9<Oo;%!?Nh zUTkld#79awvDRL_&b_7-_GI+WKDn1#=h)6SL<?m`-)pUjx=`@U<XUoan!Ucl>z&N; zf0l$yg-t_$YRTSK#9z_@!e#f_W*@KH(GKNPmPVg=$?g}+RQJ+oUnz3ZXU__+28M>t z0W~)2CM_+kMoY8z;&ibNS6F1^8^rqh`qi%cT3?Iw=jDqlMH(aYR5Cu>V#&axdYS-7 zK$4BgYqy~9V|aOlR2GtAs?>LAW@>u5Z7&N6!(UJWG})PYr8I!x+PBOAwqChH<ez<G z^94|Fi-G9qB+*iHj4q8omBo1T_6Y|A`fwyKF|m*M=C4~UEiIb`^-uZAfw^ki?mkS- z(T{fFEiK!8JUkQk!t0*H-ij-=at_4uf&rtu=+eUcbwp0^tjobdsR8Kd{9J3216Ubu zrFI)lusL`S91Q5O4{iX-v)6|bHkiu+`!>JQ1RA)^KuFr$)laFupv!VU0yZAiUhK1V z$_Mw3<+Zzw$tZccx9!a;4tPF=gk$90U1r>=4z-ha(GR`yMWn!0W#2}SayIKF4#v6C z2dS|$Tx;NldAW5}uB7eVn+x0#T#dSCsplze3$&cf;-<p@sWoWwvk9QJcB5NPvk`JT z$JXk4V>jvK25>^t(LVeWIn<E)u>;U8Tq6I%HU6xW{yL){`0L93z!JCU40>L*v(lpS zRt5DE<%!z+x1_>%#~DXBxMb55<A968X2#tO$=%03H$1J~Cm*qJ?PJBXwO=Zx@<*-@ zrtMU9!48^~jSfVmSE)XrgZt#d@*Arl14wc{%&0EU$T|Ny7q{8&LxRuO|I0VOyexfh zL<KOo$^OCMR=Go+0ztM{DqE<W8%0mLX*Bpk|KaTMueknEjDl9>Gc`A0M{=JAA(uY| zxJ~$Bqi}c>?M=AF`bhy@v?*Oy|Ia`a%Z{*kavmCz{Y;n56TVd_uPwQRu$OC|!%AmN z9kD!J7#11dSp6zDbCwEmAh8(k%ohgK$Uh-6Zv_Ak5_uB2|JR=)mKk|(x<n!B|5W5Z z(+>9_F@V5z_fd@4?}+7Pw-VrkMleLPZm05+Nc};)s<yMx+WPuf&~vXwPD$=<uZRBP z-h+=Wb`uf~izET2L1b$bSG!Xtd){!j#rCKj|C+gD_x|!bW=0HZQDTuq!gX<6-9%;9 z&OiH;E?@o!Y`Qx7F^SH77Z4-8{z+4Xt@Id^^zS5dj_&ON?)HKY&}x~7gj@Yx>;6TW z|GyV1=4f--)BsB^CZH{g@w)@d_Dg$CiutOqk#^U0gYWFg-6gdjNCly&s${m9sD8Fh zj7v~gGDKS=N|>;K@y%*$Ycsm+O()K{DP^J(%dhJX<yPh?k8FKIVH6d;GfOF{z7DPP z7>zRg7J(tN1@sj)^uI`S1l0ot$_&>he?UWj|D?qb^gw96^0IY!Ppv}gKREBH3fT1u z`o0%kTYInd2J7;VkG~iLB+dr!#9#d9li;?0hzprfe1P|nHw-QJ_m>ASZ#%nHqJ+`v z-FM7vu|~)6^!vEzHstXUAKz;%XH15Xk0tP3N>yeZaoScply0WZ@s+9RL>UJY4Qi9H z6IRn-q-cHupyLV9>bCA{sDG^K>NK(NUp57L@BHMH<#PhPT5|6hm*)$Q7>{)Y_wLr^ zYd^uOJmj~h-CpM8(2Xr+&)e>)Yvy$4TcEFQBGn7%0O_Hc=0pm!^<1`q;#Xdoy_95c z|C(Yw%SokRi0yc@Iv=c&%sbSt+Jyo@f2P*-_^bkQs+(r&C};E0PRWE?EiGhXnsZo~ z5>t;1DjR>YPHOdx7SI3_&Vf72_8eFTg(&8JqMPo3t*FWX+CPfEzt4yNof!FLkEv0s zL6rk5fTDG(_MM9G$ABzOxdF!U6cK|b$WKPkGrZF0#l40Z>{`HZ-^xVvaCt@{?ypa9 z$iOQbBI4I>ds*}V2^u{-%tw&d&h-gEE!)bn`73o*zK6RAbo2G#sC3Q;_k!{)R^pGP zdWSSFfOz{+aMZnc`<?-<7d%_4uJugN>S4|cqwJC$t-5Mu&G`LP!QkZ--aLwwU&A@a zc}0bu>ZUZT%jvJ~RF96&zXAI4VSizx>FP1J-xN1@I#e*BX<W+IobMV0tSr{Bn$=p1 z`ABKSuj|~YKCqe*fccktWS5YmF7dxq)?Zf|*!tDGkhlE*y;p#t*tk~dpEg6Nt@`&f zYq?C{lZ>1;s%79nwaU^@+BW>;WsUsA%WkjPm|3erMcT9puf}oGp)dBXN0$9=lCUWR zcaIM}iX>3MO;W^rf2}NcxgQHO8X!=83nnJo2K(L)0CkR4e!Qv;{|Hf{;`in@HPb)^ z0}3ti=Uu9<W=~3V&iwV-tl&Br;Iw((d8;4L(F+)s_+}!R79cT=kgy#1aCLpH4zS3H zi1|UjPl6$8fHc(Yz5XP@e!4u8Kaw1-r*M&@xW&f*N7+|^McHj_OQV3Ih&Y6RC@L*O zNDm<xl!A0A-O^oxpi&~;j6sKVhax!)3=G{$w=i_gzsFZS=Y2oV_nrTGE|G_cXUB?r zt#$9cZ35#2pi^wYmJ)9j1wR#;gmrr4k(SR7-db%?T8G@SB5xA3l=P@~<|O6Ukd52A z1{fvU9JMZlW<M;AMIg=&9eJf4AAGF<u^IBC9Q%JBJdcyO!AlfCe!js2t5eS2+eGy` zJ1#!qS`mHyz<BDa@r>ibD@?3kk&1@v6{jWW7aQ!eLs8+F%~IWQq*X@}G;uXvBvsF6 z&&T{=J6TbIobF~w%I;T(&Gl+q^x`8#a{H16F(KS(tK;$L&sG2@14#c?*%e|gK;OBl zitJB9)-+bZ0(K6UWx|<g47J+il$bbJGKTtiGg7r;2P{l^u(X|Q{$;R@E#R=ZOX7h_ zUxbgD#;qoSSJ%$U&Cbmz13qJskBrgz+bv&;P@A;W=ck8s@gH8ga7pnPPxi|Tk=3N^ z;R#HiT9xNQlH2k9SHW5DD_U*zhhBf*D6|XJoP@|%4iu<#Zpn|!eG!uFnNq*36hx*; zD0Y=TW}I1jEjwms6nn9pK?|IjY%{?S<4mD$wY#@h%wj>tzxw%^X9io9SH`nf<mxl_ zVF9L+dR)=56KSA`9Cbo1$sjm*`JG|wvAwrVRIEsHVnYMdsc(j{USKcIq(CMIxv*+p zP`v*nEUwOxH>1dKYdPHB6n}EAwo499Q8%eQG(oa=@O9zPm#KFTN-p%DXR@ADRrG-> z?;X8ZZog^m>zex{gm3C4>UR;bCywp!*G-<AV?NtW6eRobsmA%+0yei9h!xY{C0;<U zTQd%53gFSF7g#&kJ=#!oU(ZUpN4eh#_5oU%^t7nkFX||K;%dYPmKHqRF3nkHip+U6 z6Wm&YprOxkFIib*gkdh}z+6J;<gf=l!H&nHMB>;$9<>V_29>%8&YOq9+}#<B9B=#B z)s?{+w{l*+{q4Zd^h5pae)*Y37T{+BT{`W)2;iT?_uTwq9tG$c*4S?j7UM+}q4A>r z54m(Fzpt@dQ}+#1&^Cm<1{=A=?_E9>?NtrdQ!!5^Ge~Kd*dXxHHvk<qty1a1V?1lk z@;AD73liMG#=y#7Ma@lw^4{x+DrpPm79kjUz*6JQkD+p>Fq(unzRp7P^$K%DY`>z} z<ZJk$3nK-pgRYh-Q~S9jfMyMrfhKE~nZ!WZADb^3503`jxXgCX_yv1TMe(>rg0Lyx z?b|J=Ph!=XmQNV3n)O+;u8iN6JG0uCc>Wyiz018dpFVeoj~@rcbF%|e%4{btR^dX! zFkU~BFE0=n4mMYa777qvo#eXTa`f>B8?`hPcvH&}$S2o-p`CvQ*#eZP&k~UP&nZ>8 z>E1A@CP_9imlS~|^DU+6r#V?ND@u~;ggis*MpN^%x#M~ipC@1kdZ4Nrs<PqomP%l2 zyv;SGE>tbmurm6V^Q)Jqo}8VbXm1nz{HeZlkzV%zyF6wZMXw$+xuZP}pRC;(_wZ$s zMEPF1^AYcjL9xdma(vaCedSW<`!m(1+ev+Q7~iuLJ2>*a(?yz%WM)uQFVy0lJm<)C z=j}-)SMBTPvrnkPYX+*h7eC~*%gCx3M;(wTGJDp%(2w(scf-_ae>E9mLDZVzOKBrx z4GMK1tUrw}cg#x?a&GVxnu}hEY3po@5DHdnL%!m@aY^7apHD2yhuF^H$p_`~>R#3s zwtGdD8~#39FU-R}-Z|dr8(>XTNH&l9sGE?WZ8e#3`pgZs*KWbHtxXQIh_0P8h?nTA z1edQz`J(dkPo*>ap1>z%@`c%wn!(Hz6dtwTwhTlKatTPAEqgYfooB~?*DU|~;F6mT zSCSC(!FC-xi(Gj0LAk^Yny!?XE^$wG3qjKk&Fhv54jaSf%BGqN#_@c{>DKr<8M>U6 zM$hi(_e+k1LL~04x|wA#<kvkiE_%JV0lhL|M9??cSgyD4Kbl?jc&=c~v7l{3X!x|{ zb~ibu@l#-xaCZzuM_tSQ6CrD$wt1ZSwp?QQK>pz(wYgbl<jY9Y!6WmVYgu}G=z_Pr zuWa0&W=u~{56^7XCK6{^z~%*m>Wk3djs_m7A@XCk3>W$b7@w`W&zjn(^06;2*q#n* zR$&<2uArgJUa~ZKMDe|Eax_V2ZFj<BiYV)P^%KF=SbZjuIt+`4;_+r;CmtN6XJP;c z6w&Q}AhM#!3z1?`zt_)e&k%SPcEYdj`Xo`n{s@#eU?20uoRAWB0`H$c_ES&b!MBsf zxR^E}=;?#y8`9H*mD;@aX89qE(cIk2HzMkjGE2}-$8Ev<_N({uL_c~bdAg=aIj?a? zwv@>iArvnrb0b+_m%QyE!#_G2q4`i$<)GqH1zpFN3L<CVa)`~1UTi<r#(nWgT~uyF zPq69xe!|n!)3a@F_!CzORV|d~pHiGcMi=bVUb8+~<Yr<#)2ifM^XM4Kz51H{>Wm4a z0+X3x@kwIqH<mNe4>jHHPx&i+H%;t~=awYfklFU~zCS+X(8Fgw=mg6dd+EKAN=901 zw-*paf25JHnDZeEE}KAS@!><^_+j7O`ws6)66yuee2=xYgV(KYYO0J_E%qu;FchS# ze31S%I1eeAq|Hv5<?<R3@?Cs7{&wb}p4t<CVvM{mrltv@@X;X4I$vx9#yGkjW!-+X z@w|9r-NSZJbC_c<vg>g4bc+e8(-2EnvM^Vw&!(Q4iuR>p<|$VB7(<f!C~5uY1fpa3 zJ7OCF=2BiYoYII_11tN-UDn^{GCan4CDKH!Y0#{lj6VC<1kyGm#cbO@IG9%Hc!7oZ z?IA*BR<QYX>6Zjjkw}ZIc!4=it%xY&X+@-Nh5RhH-eai0_x>mCvXcj`ds*bw<nXH1 z;`)7(k^KNSmaAtZ*1ZyiHkV(%E5b&KIip|l2IN;x+1TO1<?2@HDHl6$LZnoL?t{%! z@sBip$>B$P%Y|z|`+lh4-+sKm@9V3~?osWF!hWB6ZeN>y_ZaP9dPPJ;`gmKP^R-H> z#dNu?t3rc>+k}xOuSeX`vz(%I&EsR2YsdkeuboPf>vjZE3fpm&!2@w?UBmKfiNU-k zhv&_C6`C237N?Y9X19loD_!^F$oVy`O}KS#%qh~BXi7JFoD+MG2bb7AzOdJ%)PKiq zPl0N4$f$1DGU5VNjeDLHfm9CiW;{LI+M4xZkwVdX&26s-S3ShV@53^yDjJkq?!hu6 z)53ptMVzRWa*-;ei6pOKN(tLx(l=_EV9i~-T+65u61`MoGGYh~u;{-_aVxY#HVdJY zDyH8*=9X&R<F#W6)*n9@t3Bg^2OnNOm?6_8ag@!YgcjK${CSP{twwbA3#F42g$A1V zS+dATSF*_AH><mI-f>CK`*|_8WlfPtAMpCLv1yjM`@89DU-lSx?^e7fZT2Dvr<qLV zc)S6u3Qf=cIP&C|@#^v5l|&DDtU{&}$fJe!q~E)in9F)?`P&~JO(0&@CfCa>ala`i zBRyjDscJQ84-vgYF?u)`+c|XCO(=e2&8MW{G<J52%zl_KSk_tWEM>yu&POI{&T@h6 z&hfXb!$JfV&R!bF`ptKm2cq6!u6@Xzu4!Y^x<aNt&K%oGFNnOL_i8jc^Dzaf*Q`h4 zjHyrUFk!&>>tQVlQtRjok!MS4-nR24XSTro!dKFDs3;S<FQ>-)qRxnmLnra2xEc3a zjfnGZYDy^tqGThPd-=8odyI+#-Z1kmCOjtmX_|UiUf+_=WLNRAH|i-LNxgJ?`*scM z)Vn#fQH{epiPwa6NB-D$lX1`W9c+O)ZymFv8QQcfa?54RHX)?`I%CbzJ+mt9<InMt zvvM|HQ&*@dAeRB>>rGVxEjkw*T#Tspdm4xuApt8R_-@OFCtfEadT!$c5wy!bPfaso z(plXOPC-d$kv20U*>CVgP*xq3&VBf%Wo{PEs+4#&an-9;zM8FC-=p*2XU*>mL~6|B z?NSgQWKCzD<qulJ#G_2~kb)G+{U+(zH%ThWeHRJM>#+rWhyr~w)3k1GDs7QE<x%s5 zKz$+G3M6HAybQKWBIQ-p6ZgwZd?MEd2;DCvaC77!Y2x#EAGgcL^Ej2!@7Whn4EFk> zirzS|Uf+z``({_sp<UH)Le`fHUl&ef-t=%mboo>%vIztKt)*p+*`8sQHhc7_^rhf; z@D~L%hL*C**5^YFw)+GHX{kc<z;J`|h><8Be4UFS{bw&=XUJYNmjp=;jrvl%Za}!1 zHa|!l(oj)qF$#B@7wf?0ukL+Mj+I7_ZNN|7B<Oo2I1@_aO#^*&Wr!TkMP-&j;V6I0 z-k_ddY_KxkAz4g=Hvl-nt&MqKl*lS@5fdI`L;)9Gm^fGqt`%Z86MaIy>D1mFHS$!C zp<DHarFX`$`0RNWB#Am-w;0Qpb@tvVxGKm!k>VnTb;N-KA8KH$eZ(A=1#wv6Z-xqY zyKw%F02n{@B72NHdiuFupfVk0Hbg3e`~^8YJ|4nr?RhKlKF41D@wOF2j6+>-NSn>A zGTt6rWmmKVt6bj8)=5|5c*7_rtlJsW+y1;qc&NxdWO>|2L7WexyR+uaqkn3nXc{d& z@S%;brEV_R@_|Bo^-M{@h}qKH1-sa-;zij?)$t0}$c(NCHL4o7>6~yWh32Jw8YRZ) z2UDy=X(%cZiP+%;w&YRcIo^Qd%>s3xFZUvuOS%PUCalV|?T%u8OpT>-W*cRKc|r*G z5TTvkw(ED1vPwNc!5Nhv_!MU5ppsl7z)$<7K_uwcUI25f=9cCHeifVy;Diogd}EP) z#qwaxtVy@rFE@AY<dsfJ+T@`uhd`8bB(v&LQ8yxjnJ?lNW<SRlu?tCLc5J3EIJxTz zkxIp`U=6i|%oO)OHbSH(codpbSL|!QvdFgii_{zXUz89~K*Z{a+m1uyc1Y8;9fHW7 z9O{I5jyXlH8<8pi+czvy*4p6~{LMsrtfGmGRO};nr7x=Gn2ny&J?D)tii_KZ^|CLl zp7Kr-Ghdb7{NhU^(iNJFm}Hq>js17%l17IwP9RmR5V`(5z0K8{OMHOv`GxRqUsTY( zSL8a|EjKH_X(LnTmqP7?{>QE@@;BI8i!@=}(S2>)K)Yg(%2@}Y*Vv_x2PRTp?+2>w zt)--}O9$Ieule}gvr!WqGFFRQhL5bqG3~L&saDq?%o(O@Iv*JbRG|&J&qcoE)63A5 zY*9>N)f^IfsISc#+m-x!F`EwVi&|`7;x(FoiVk=ZGs*+e=u8!*w^ebj0@iYv4}vwM zqO{<BHteemEE#b?%!n_Fw&1}-s`<feh2}(Gl$XA0f-kC=Ag68j{<Z0ih%?fk(vY<0 zhv)erN-(p}_g}>S6mXO@z8=gh+EKM@MQy8`wJppta=j2?8h4`GXkY-}aKKd5&FAo0 zwfCiioYc4-iOP+(1KW5Q61}b0jnyeb=3E6@;BsW+vqADugYSzGg;$fnwKcV;n5j^J zTvjah9?=OPl8Ek4e~wq#7z(nL8?&VoNMve?cd(*-XHvRsaWv?QlG({YSnTx&`@Cku z)D`5cT-B-<j0v~q9bZWmfG{R9t4=hE?{##oEVyExPb3=FoZT4R9z;JBwW{#k8Va)C zr&|-VnO+Zo26$WX26$VSpB2)-CKJ}8)!Z4*&~;NHj6vAyy<&_zhDFM{l*Qho5COfk z5=wGulzSmZ$qbgMua7Ual_eAOKlB|8Q3LG4{H`pkba*$psZpG7Q<!)9o)jw!`v@_k ziSZn;N?@XDF1K;=K%_{NVe_cBRuFy;?yp0gf|}xqi6c@9P1kg8J&&i34t=OQU_ZiN z+~H<!O~IX^uWTN<esxZ%y_B6s9%h!_W;En}uoBl%AmKS{10adoy`jFuhbkimz`}hu zt=7rOeAC{p%{roxgS3Bl)B!YXGlzC>G7{5c#A~{}lMhzLxlP(0Yuf1SHxmY|MzdmO z^c>!qkcLu__Hn?GtoO6qC@tuRN7^WdC%VKA{z5J=eJWt@^hH>fmGr3x_va&d?Kd?l z)wG`Yi)teeQfdzIhxW!8hjhb`lvS&p2gc1wZV&C(lROW{>C(B@j(BX3W)F<3%uVWY zH;b0tFdr0ymY(q|T@txYvhCGtUZ<#0^CWpso5iV=f1|y){xqkGhAKz6_W?x2dm7ed zL|W@}zy;wqNurl<gq`-@GtbOTq17ndR*bKHnx#5Xb)y>O@)Bt2s%jSoeU-~4DM&%^ zG+=bNIsiPl(|A~h_w~K!&+TWwJlNYniwU%v_r|T;W4Es@XRp=mo1!;8<45E(>y2i# zG)p}5l6Q{jx<(kGfhNv3DXsTjYJT><+|%@C{QYf2f9sn^OUtdMjCF?izDmuyF%X<9 zc#UsFO7SqU=-*AnZ}TzFjenOUAc1wtVMB&MiYzd!>{D98RmV)?fczl=Cf<O>8+3xa zN~bWz9y7~*{pGEtiCILNvOrCvla$=C#R}=&2dkQF?6IdK69p}-S>AE7ikMm659W*G zKEbb&Nqo{Yq23w2`M9htz&-!{qq^P2nv?Vjb(Kx;T|fxNLLkM>aEu~+2A*#X4EgCr z-T(m)g=Q*B+V5@fTYrUQ#PPnw0sXzIA3S>#nEA@ywe4!0Z6<lv@IDKw!1GnW<NAvA z%B+`sB?YCmjYUzsv@X!%+(x;uh`f<hK8XYb&zghQ%y8d1boOVS8v;I6$8_AsiWWAH zs+y#|DC%m~B6OW=K4|(}oIPS%o1{r?A5v*q-u%jfJkwFP6~$nanG?w#+u8R0f#LFT z1qFl9M}0oP1c%OXy)n)atk8U8IBr;dWMNHFU++HkeB!R5v?JmyZEoe0F<oa}vD4+A ziRNuz0U(47o3IfK+kc^&z?z%&waO>KU)~_M@;LP9ciXY-N;E+Bpe|IkQ)WXmI^)>s zR>v)3RWSSeTGe%ms1uW7wzXz+O~*8Dmo}3G5_g{Fj~uM#BtN@zq*Y~ASf#{$e4M&` zhIMRv0f$X$Z%pE1TY~>JwuPzV!Dp|7HR`d<d}x!CNd8Z0dqDr`(K<CqN@4d`9Xbc! zeNU1i`Sj0)(I&7<pYo?T1pumKfja+PZMibqdYXrULg8*iw|lILoGm7w@Fvf~x<_yA z?Nz=>j5H2X2-85@mTU4j<=Y6dH0qG0ukkLJy*BgmQsKS5xn~T}(N=IvE$bBj#a<JV zJbpa*R!`4+AByJQB)Uijz9{BiqmzAHQ8#)S%wNrQ<tZ}$guuiFCxCHUtDI{+BJ0pD zk&h`DGV;ISWKDt3$yv$7-f#1!2SH;!i)zgayU4F5kN}gmb{=&~=)(jnZO6@USKLkB zFP`I$(8OG5C#9NRs2U7ORWa=lK=w^s8xk7#xI-ad>v&vpgf}+&93daM{*@e#xxih1 zFkePpfc>_Hy`SN$tn2)>=MnD+-vfOOzAF}e62}{iZ<|#hLmQ~m5W&ax*YA{7Wjnmi zomde+tQhF6CJs>32|i7_!Uj^XKK(Vfi+Tv_OpHyRzY?+KwfEi{BAGq1IoyF$u_{3a zzQ{rZo~W9MF$`j6gIUR}?)LIA-0goT!7{jYq(5lMsXL@8vO-S<)Y$I|2-6a-oDcq) z3i`Xb=gA~|f-hB?#Yab&iEMHqd~TA!@kTS<e7%(%9sv>~{bIZZ_)=q)UC6emZ_lhE z3$LOT7We92Hd|SkM^+I>h8DN+Nh;D*z>Hw?9~v25ha?qXX2Ak<6K5%hha;Fvkc7j- zv)@YFTt}>G0?`Q)RD{{mWh_j+cTv9}RO$~$oE6-n{~(8UeHNFV+AD-e8P2uDd^y~$ zvv{sFvC+MgCmj?THJqR=u*(~di8S_JzU8<}nkD!tEi}E3ap-EXcg=xPxr*|wFNDu$ z?>O}#Takf*)zwE#{R6&3!wD7#-q%W|7ziYz7#DIR3x>}Hx|hZGvpwUQV8G57Qq7D9 zF&4io5ItbeD#&pve!E{=>SJ^5dcCb!>ZqHYSLd0zYa4sB`D6oYom+4`xc9&{>CDKC zUt#dyAaetZdVW`?$9v5xh>9>EAx#;14j>atSmrTxcsyJ2fQ0ZQz7)yg!feG`qR$|2 zF!Sw`n4Yt8B6BZ4WX+hQ6e4w!G_=`}{HGARtAaFwSbJMDZXz$8)hM!0=FqB3M|2d6 zJv>1t2`qCJGb|8gGg-*?N9azV#spxFY<euv66J2zDJZ|R*>xtzD5E)Q_aA^sh|Zak zQrLj|wV#&(7gNc%XT$+@rqF;gzW(?IWnfx|bUfuE*UN@n6B$&Yy|0-a4fGl48UA|K zr&B+3iq9?nEvJY}kDVj48auXz+;k7Au}w(ft{<*k*!yA$r2)OVQ{YQOxmZB{zgwKo zzRDD*`V3H3ZSB0s=DF#i`e#o-By5W>g^>?L6{3w+8Nz{tVLL@VtO%v>;B#jX&+C_C zSkbDU!63^eAPJ|Xv`7pdV4c!da9HQ$Oka>WLK`UKOI7;*^lWLSM4v&t>C7Kge|+L< z*~FIT(WjVlPgij~c;_>5(81RU4X@#iS4Ix+lx@sz^$Ij-;rt^l?o+zVg<G<Mmr|5q znZ>`eA^$@RNJ;w|(cz>`A*N<OCfN8S@}?x5l}Qx}<N{U0%yOm0^J}g`Gfn74Zn8Bu ztxnJFY<lm5*eW7n!u`;OkLyq)MAz}VDDPq=F2<ulOWtuJe*ykG!NmR+&dSzY<Al%4 zES*sx)sy(L(yaV3Eu5fCQ(D@P{C5O*m&_Fp{z!}AjFVRB9cLYAKr>hBV+*#n{DSAr z`CYCX$Gdh%PwSu7f3Kma^;~!VHno*Fa7Is}3bX&0kTdZe<~||)DT#jr1b%)uDAz-u zlacc4PsMel=_#9;n8cX#>Vce8jIy-=_&+#sgmtPm4JD;nw=!p6JRRk^nN!aYCrjVw zOC49ZkEgla+BLmAS8%V=TwvUU*a;6#OHO-=)VdH4Zk=E0N_F0%uIS^0mf3i|az^TB z^l#gBgB0N1DtX1@ePD3EZUM{o`K1B%uY7D;D(>aLC7*3fwjyM<5p$As<=VB_u|G#Z z+V~8T@cr(z6{|oXZO9@Yf$P0Sq<sxQ8%tXG;$yTaPn}AxJ$?WYw=1jJ3CGH2Be*N` zP;zNqWLEC8K5x3A7Z852SZ?#&oHsFH2B-^>466Q;{L+~w#6XX&ot2T47RP+w(R_3T zy4MM#3{c`ebwiSWwROMfCs3&g#sL);CO)+QyI<Wfm?Q-=cN`YvtL&r45&CzF4ZJo^ z5V6!elJI1FDeK~O&<j@SObSwdO(_SmpacV;x7m{LS=+&i1zsPF%0v5Us=aTO*jg;@ z<|nb8Cr%2*(<|^|G|5hp&uY)~RhPEK7OX5=Cu0osKtDDn-F>Z~a2!FN4RUijH6SOo zV8*5AFF7Ji5^#=uSb`gWoA!rD_=#TumPg!?cko}s&qxZ=9E?d<oX`GIYV@dHoFe7_ z1ZTb2duVzPuQ?>3ViwbOE*oh0`SXeL@IBlMPK1Gq`gZjp@s;vn^;i#~!kw;K=Zgd% zCPPA;A>y7dLTLH#y$TMV87{T#863Pa0D^skFMjq>wJ!85*SexZCk_}pwvw6UoUvMq z+3gC>%AgB8nv%84X&|Rr(s#^1Vt^`BqdK3hDTzG!)JqKCL-^K>S{L3MkiWWxcNst; zHFIjwUUO=eA-lZTmj5rU*@<R$9LU$T-`_Of+g~bBko@Fzxzb@yvC?(@EoXUYDgUA4 zT?`7Gp7NojMD_F~(d$Sg(wvL4W)GZ412%Df&RJeqcvF-GDGko8i3a~P=ZGC{67`Ka zbcwy4S#Wx!u1=#`Xg{Fqa#veBk+VEKo%N%=pv~y@A=Z(=O9k#EeR;aoYNyD^tSh6V zqa~@?+12y;^sUItXT~e+SU%dva%!_0His0>EFPU#EjR1F9DmF1ZLQDN{iL)iaF9tG zI9|xQQoTTrB3mP#-!i5nS!5svoUIex(B5bG$kIz(zs^(Wqy06d6b^Q^+>bMhOAnRD zcGstj$EuvR-Rquyl~QhMpYMJ*(<!o6GLs_W!u8R9E8cFcc4ycg5K^f$D)(oW{LhG+ zA;g>(RSjkU^|bX}xVQyE%<h|v!OZORbn8^OgiXQRR7*ICTE6$$gXA^OmKZ?Q>;^4( zmg2o{uD$xTF^GUuJ$z@d(6DToi7b8fQiy=ljzLmZHOZ9d#ii9qR)wO1PrYMkmr!t~ zioykMJu7{Ztm=%eA@#Ab-j<SLY~t5eUuA3IaREA8d}*kHvYE{>Wm|h&BnJiQ3Mo0V zjs1-fol85)4m~}3EI89DQeXDTaY3ZYuiBV`IGN8p_i!^qA{qiU2{^r3+$A14m*OGW z-u~=>{;KNk3EH@-!%LcFD=pjmVn;)Whjw4@+@dUbm*vV$*=s~%eG}P~2t&0nj@)Po zXN;5%Iz4R5rUNsZkNnQ(#VQV3zrPplEiN1<?lRC-xVW@LP>t9f6}xC5?sBGCQv!50 z#oKambwRNITq`AQ2=v2mYfo^SX$1oI1l8x7nVA8^-*cHmJs<AhCTQauyhE`2Y;lt! zop{;2Qj+*Bm&Qk!g{7r+B_}7RBtyO#ES@-RagSodze31iHqIno%c>Hb)Z@ocKwM;Q zP<+YuwO{!@o)4h-atgLKXnU~~e3Edlk@WtaiRVbW{C<WQ@;yf~UOJHcFQLlt^JomD zC<cEv6LntJfC$;Y=Pa+P63%Qa#q+0i&6yKyJz*-QR@!f$lbww(qDP_Hd*;%X(^tTk z_KVK(d*yz{5;RhchMKyqBOYoESPS}RmR+~v;eO5T-u~qCod^V#7e&8)X&senMwbc2 z8<f32xolf|`#Fk~0eb|P2Votmr>`ITaHN4YG~|<1xNli_)a?7GGKo-p>@n=~xYZg~ z<-s%DJLYePlO5<U*%?iA+F3|n)$55}TD=Jz5^c5<9|bv*Cso}QhbIy^X%Ec_r1-D% zT^k^NZt>Ji?~`(T**9^BkZ~Q~z(8x0KV#f%Mfl)yupVubL8pTc_tlK8^h2Z%reNiu zcF@obXu))!;Le@m^Itlji2A`4z2PBod~&Q@8r&@M&ZwAXVWFKq5&NT_o8}b8MEnp* z1?c)V_uH)9va$=BWsWa(_}6Iib#5%&d0`+WiJS{sI8FLtHD&Vs_9VyRs@RKo1C`z} zs>#iN+~s~!a~beOB_#CL51`25g}Ui`@UuS=9B?EJ4d+1~`-ma2fE<1-fb97jjQQ<( zP;-QDpGhfDW`+elld9!_LuN^2gesp4Q1YSq7z-3yEz#@4E|PEGrE^8S#Pr`kMn!n7 zQXcG|exxhL@<?Ce8g|9!Xo}0_kx$2g3l@WA&Z);TRws3R`GTLYt^$1)LyuLZwPch2 zlsc?hV?tDl{34jxfKo$}TO_;^%zIc`S~|=+C9NsRPnT&fEPh@uB7HgSWq3g4&&Kkg z2ys|0!hH2uIkb`d+NG{e_T>_gg718TLvlYnCeQaD?<u^R&pk|dBK>YwD{Z=V<nNdN z@~Xf3OD^M=>8pHnqR=5m8F}7<09I^og;}R1wIGmINJr&baT{hb2x5X|6<)vnI(4<& z;02OzktjB>eCce0U9yZR5g(JI42EcL;Y>ZF`gA?E+=m@ne)z@2I<y*PkoZ`lZCkvv zKgNg8_477=aK_2;-rUr!a<=!2i*D%44-9X8N0XN)oKU6%kCRFic_yiCrLw-lb+NZZ z@xzGShycOY@8lqc80l+Xzx<=wuP@+nGnL%S>3}QdKtLuFfbkiqeCXAoty0wB7DQ>B zQ6islm1dPDr5qm1t*7K!<Bi$>v?S-EC&9<myS5K9J`y<>r2uaK8_2`m8cdSOczQgp z59*)&=9I8ktk5ztJ5^k>oW?o{Wgh|2qB(R!FC8ThP3b#(Jt500Im3tbg(@`I9oc26 zU%pC|sMQ;0N3x7`G4#)w7**zU+q$~Jczje3j>#_2ujj%du-iKiZy7&|#gHImxOvbF zd3))%C5{A=$p(AtC}w+^I>R_pt@HF=XP6xK%EJZ7GPgx|{TC0_pyQ8BP!&UxwNlnY zhrd&pj&e*%AP3*BRlYuO#uS`;Di?@aO*-3u`&Zxg%V4*p%_@#vs@<>P!MzTWid_CE z@SC~56djuaS&S?eOqpb(y#|U51LSZ4_Do_|6O+Uf08{i$D$$&<*dL-LOt5|idk1hI zPj9Xfft0Rt=2*J=`yHPz;xdYt<Qww@JKJ5Hz3!{!YIHAr#C~rvok1AJUU8WpRKyUE z1%xpvi`mefwrHvCNE6`cCG;Pj9cOtaHNt<r1bJ9$_B4~&$9dD**DUzt<vzQDm#zb< zHN=U{$kP^NJI@}FBukKl{FGTSwlq9AKmVf}wkiPVI}-*pzv2n|Ezk5oLV73GDJd7b zf<FLK!~dM>c`W`}ym)}*<zkc_jPwG#<mnEx{NRxI@s9}nY%dx@i=Y)=O_232&ZSYM zEfj+ym#pq46N{LfuF|`2m~7M=)1KeL7#T_@IDBF_JDbhzV8vy+A%dzWNyHu%@95U> zz%Fk5PPO^4t_TgK1%VKfk7hJWQ`(v`Q-ElVH?Yc_6{b3HhM%gy?>&P;lbkg=2DPp) zqiiv4gd<`6)!ur10Qzkd@>nbxu7QZ{5Y!;Qd*Vwa6M6lTT!YDzxcI;@I8U%@{U5LW zt<%Jju#7b2f;QKysI*Syj~ONhCGk?VX-a})0%s@A@>meaS*wArQk>#vlo?);<AX4o z(>-vWp8g~-YLalX%RDExzgepdiHMGg5im!xy$-4Q_DS|E*0q|n*-@d{LsP7D$d#%$ z%P(1=LIvvMj+e>><2z4pK_G+lEZ4~Z5Zx?0UW#QWJ$Qq~6#8opNq5fzTbxsqZQt@r zYwShtrE9c6>IyMX`4yZpxBf$!8MyUoQog${zB~TBWV)j#!4H&q=?otHt0}egAEla{ z%P-89chhCM5HE>#cY*(C{OoiVXhyN0#feN1%r#ckF_e$+Vn)la2#=YFzLH*w35I%n zl<#M{ov7>dk^br1sa~bZDMtaFVngri(wUlSQ18WzDZP&wfN9eRPgABKF7<n>H<<}p zZ&uK+&SX*%X;O5Q-uyLzAcH|_O6nA!bhrg7V2w*4KP0C}L3a{n@Oe6m*Ju4+R)zDU z(~N`HrmiVQ5ZzaAL()-iY#oQc+cxM6tTvke7*T|s+Ie4=E^ILlqk{;!=W=M*KFE*N zkCxV7xxe^bSX)sbCa1bY-ooGODf}x-OY5sr7sugZ9BHJe2a%lK6$jN%?@D0g*PWR2 z+WsoiGpNkSpL`ZbPnrZya6sfZ8_D57y50f;M<vrZuiu8_-z{U>H6r0>H@BvwM~3XU zb20`tt4~+wrqWIi-ONQcEPdQVkW+Xds#au(0~SjHB;n3ikENLkVb@(uRbiPlq80>_ zPE{26p(po4+L;z--9Ek3dSb+r$>37m{UD6)!|$w;qdJU0%F-hH;dltBXRMHthEhMS zDLB=k`B7CZzy3a(!io72ZtMdSq&7#6;5g^6qH*cS$R%vb`7;cxpuRp%crQj4NoU3q zncf+tuww&l(KHF$>AK61!iab;Fi1j^z~h!M>V(}^^w^H=+G%ey3=&q*uLHG+ax=}0 z1$XNOep#Gx-BkZ!KgOy4v;DYu3;*np5EaSugi3)EB`ovk+&O-V0!cUnM)iXsgk+lN zd0?5<tQ>ju^LdunAu!da>BJ6!#V>{*BX_e#y6fWSc%LWW*=oeD#m%J;HLW;1rj(1V zVZ#^}<N9yPp7~|3=5h8rb0n%ioW+vqWy=pK0*&IB-*NIA!o<d%Kq2ih?v*D=z0^#Q zy4NiQ##kN+79bM2kC`oY7`-pkQ)<~+Pm%<TyQR`l?yu&w5e0OntP!DE4IxA$;j)`8 z8PY-Fq$}%-6fTH)JF4l1x_kmD2h7HYkD1p?4v)e`Z)gr_3JmV5UB!blUnB-{Gx5XV z=ql;xmq;KO#oQf@DA0L#e<h*+U2K5;A@+yKyf(S6gH}XODnXkL7bHf?rx+yDgdfet zGbtyV(ky6n7P{fVg)a;jCEjNAJ5MBxFUaE0kNkLCTzFo18DGLFn`^FF8sQ{g&K@_0 zGA=R2uI&~WYE57HZ6*CyPvO!&W&62(6nV1Mbt7eT;VVm?NJ?4}vBusfjw1QN$!pE= zYbS4o4~mN~?8FNjhFMh{;DG|z<+b{~xAO5fwO0o>(yXP9YX?$`MJ7A%TLq%vjdJ%S zB_l?8KD|dOP$aMV=jPJ4vA5DtB$!)TMn7hey>DZaqB@ZsRW<xH5@U0apPC$btx+!I z&1j8IR;AN@n<4o(``*m^#~kzzttNQPFk65*z{+FywxqNyZnA$Vz2<_;o}(5w6slcv zcQ-eeqk5%kI3e>u4-qfQ34A)31$+DDZKwKLSo3!g>T>;v7{K~@z^W8p=7^}Q8<F(F zin}ipR&$3^IKhvcFk>5qM=}_)Q)E}s^!l|zWo2c~kee1E>m9}M26V{17hdAj5JXwO z0H7O&vPp=D$l1#)o2nT4m+%S737PM?ugu#497QE}#wnoyA+x$;=(9CoBC%l&%S2Gp z3QeqG1f36Z8MoEwR=$UN+uS;%M8Y_CcBG|+>V=ctl(4$~$f&E8L<%EBDw+JZbNagj z%7J|m<b$uhS>fZJM;FD}9nIAt?flZ5_Xdrfp1-gje(+3+lLh;M%b+eMR8LcaXl=eG z1a2}aU#wDUi!W(yn&F0V6t&m1#e*U#T*+j@ZvOTnn{hZL>3>^F{H=fyuoLvcK@Qp3 zeNZ`psO02#q;~oJwkC&-?$rU~!!I}Ncl;-Cm5`Z67rmEkW5%R?R<AKM1yP@wrKK=u zyw|hnB|Mq<@(C~(IAf`o1lnfEOpU*^-h8NPg?~$=s)#*yW@=}G&+mPx&6|}1t$NK_ z4xMxS3IqZYHxU+?m3;IGWJ~>yipYNK5hJ1}n$BwWZZfK8^fF`pPL)oreH*9E0pLuh zLw4nifDd?pcG4F=_|9OSSwN-$lDD8d$LZ6)sR?)MdlbF#X@onN_^qsSGUqiq1cR=Y zY<{`d0F;>y*`7V5Sq)a=JMHFl8ION679d4iL$`HiX66u`YsaN~NWE&&gC!rw;GyHi zP?um6aWT(H2ci8fng<wg+VTZ%CO$oYnW--mNVQSY<}}^{2Jae<;Pmup&3Wq%zSIF4 zVxs@ciTyG~KS0TySXjmhMHOYaw2TXo_U)lU9ml7c-b`7^3qi9UDpJ@MD+#`2Ji<5B zFGx@HJ(7}4a;_4bCGuF5ryyU^cxzaT5>2+5b9wNzrAW8q@ws2I3d$Off;2BH>z<p) z@8sZm9UG5D2Vvz}M9a+Qc&Z}XwQ2!}*`txvC~y`)B2!!WTjvpDQfq7TQi=I0`-P;F z`AMuom6gtwI}@%C+7b7pmOA=;QKgAeQ?wToOpBz$AQIh*!JLM(^aYjf$(r6<uiDf! zuTfp=`skNJJH6MQ>P>x$sjnlNi%FFLVrgMB1Z%&mVsa<2?Lzj!?nZGvIcLELt{%#3 z(Q%6*ijI1qgO?NTRq0kMHSRVO?woR*fd@}q9W(L2;^d)zpKHj_`P2KV<LZLr8znWT zcMBcaEy{XyyAC@-+tpLOu4mK>(bFn9H6*j^)>me=5r0^$t9IBLr{lhJdx*Mv4_A_3 zPi7o4JVM)oJY;7=xkecMAU9y4ua$^IK%_wNz1Q4(nUF#>6xg|H@+km+U_O+%z;@*Z z$fVY2{FSr(+uVVQiXS^ARf&0{s5H7JtxMS;Q)NERmO#cy%gn9(gco5C$0WYEAwEnu z>w#aIaD;)fob)fdj(-6MWQZ&oL^h!`&?Hw;K)I>;Q9AI}r$HIOkkeUyNeR?^U4*x~ zv};BQ$zs~05p(aHCybMOUv?cYGM1dO^?ASBP+rFgs?Ra^=eh>vQ*5q|g^`hZt#n~i zdsQcjiUCq#UlOz}ZS>jS!dfTr4X6<g8xQRGJocB67{kQqh`J1SmiwhDVZRW&c^Azo zR64GPBwvbW?ll(dgV0)KYF;lz=f15{8m7KL5g}K9L?Ck5f8VD>A!F%<fC6^1bIld4 ze^{$JUh_BzC>jHxps9x<iA21<MJ?D59EA^lrSabB0tbIS4OBsv2-$YOde5;!EPR|; z{WQL<qH2_|Wc;;~gzNnT1@!XRN|ms>#o>3ViP&%O#_697*|Inph}6KST{&<_sfe?@ z0nR%mA3?}X&IGhe*+u!~Qm|I)6{cRzfAOjQ<}m#}!mgvBqBn#G(Vi7?*LR`aH7^u+ zU%=Y&0mnHo;kgT4xP}1l^k?tl<rw&H;PhUzfs8coRkoUkpUu~2RG~jAiV)WCY(<L< zBB|^16S-fzB}8@l+XxQC->9^l3AA|)phJ5$g-csrJKaY5$!y0tG|mntcO7j@JdX+Q zSlw&J)JJMR5~LdErYjdUOR!4i96sDDl-3_yks}VUh}SAxDv95=qhdwC3%1+VrS*qr zOgrMamsV?(N-B2-Qn9g_8)^wAi$N5Ok7KYM0{06JB-|2WnN^{Y;yWW)dM$A%6it|P z+)JEq5ePC4i(v!N6Ew}bu*{U4$d=QA+dju{9u-+_5#yXRj_tZ?pYIm9a%cmB+38~( zbqP?`=Uk$h`Rwa1PzsGd3PiQ;{owlq;rKpYb6XDyq>fU~0K(6WAAs(6*6?3(LN|WI z1e;4o_e83k(*uM|m%>F3+S0SKly?-ElJ?MfjAs|KQlsP?yA2tjPhwtbBuV~?xtKU{ zVHsC(WW6_Nj;k1NK>CboVYl9er;>pHgX*#6M+a`pCO)nQyqBz;ECFN{mX!}U8nFUo zx6iJPS=DI{&B}hG6^<BxeQ#EZEho4e6{|u-_Y^BY9FNV0D_*L6$gCLT)4tUC5CWmx zZ_z)NHq&I7XUmy+c9q?DC?V8-wO4RsPp<l1%$mg)W71kZJ(Gk$tV8eR5f?GTow+gf zR4Cqi^>?#d(Gjk578@m+MGNCW9eaOKP*-j2AyPYu<nZLgkco;JJRBaPA1ogrOc}3h zb489X0!Hj3x8j1Qg^XaZd~N@73`)?OmaHgOFMKI4XfggT!~M4`lD`ZS+a%PpW8cm& zgR8SBImQqlf8pHx<%VJeOqsJj*ce@^ZG|g|{_rGE@o6cwR8(eUy>>v2)=Ca$mP<NC zAlU!}(HFHe(~Sq;5qk?F{Efv_9|mF7SmsS|{_DoxxQi5eilRb3ZGDyz&`rWFZ9Y54 zQXFD|*^1Cr*W{ez-o}^eN>vnRazxjTST&=@3T~4Gh;IadfDJF&i=T;ayt3zSEYjAn zH4q4mcLK(NI!>rsPUFPN8PW{=pWiXlB#thk0_>_=A>W^Stqzvbn8r%J^;dtzvPR@r z@hP>6o6vXrGX#F;XIEcPU%!5lxvx)*fs$7H)D-Ea4_>U(M(u$9>y09IH~}rcNlhE& zegH^F!lOlq1H&)gIJ0_~Us7I*Xj!Yj@gcVBZX%>VKPMzL%al~?F>ipZDf`Oc^UU!E zz(zqEk&eYU2BJz`tRmtu7j7Gyh6Y@gHhWLpNXo<$N0>mn=*7r|2d`@bJG%W6;J@KA zDg2YqWTX*1I4TsO4{?@)=1&!wly0!(t-DupD(}&S`lF=7XNpV4ajeouHbFBgm=?}k zf}js5^@HhMIJfk`(c#q`qoGBLhfbKPnyns2DKy~1vsTrsSr2kJT;PZxGgWK@%qs>b ztks}Z!qrrv7Q(CU5U^DesDDNbNP>c-u3jSlz+@9xDdV;fULP`ioV+7;xv5ch(4?y% zM&~2We$d=nNC*iZtwT@(iorx?1+DP7Obbn=HzlHSQWrU^WtYe2Sfi321X%ciDq{s9 z9UN{O@u?#jvx(e?df9BY0Wm(i>lvZQr+bUVXJ&fW8{rG9u7GZ%FOJnvq(7=cdzL3H zRkKb_xUZjiXr50anC$&@$BuG<aV`=gbTPkpwwZXM%LizHDoXSGpGWiW=3JGq2Jmu= zvu6heLIRHWhuoeg_rGg%m6j5mB-D8gzch%!ZYcGP_8JjD%e|Lw+_)qx&y^x}HP!pO z^L)h|ryduMwSzHLY^^&(Wjdn+Xm%WWg|dqlmebLuJ`BUMj~^6@?ys0G??w=$A|eXv zKQ@Blgj09G)FHPq@{`*D47PG@cdrV*pZ=yPBKo-J{Lo=T`SSOrE^M%>`a&`63Shmk z_mAIjpy7&M)D|=@Z>t+NdKOXzu;mP3!EBeb?JDd5;soGxEIEsANw;xxL_x2Gwa2Ee z7D_5|VRE=It2sUwD8fQlVsdrZgJTLxG2=c{fct12tb4`c@OY)#s5j1zHEq|I{&0f| zs+3y(gMf{t`koN7qi%bJsRWyBx_$Bg9?Z1VlRwB8N`jCAJbXt|at=Je@nGmfzOu*B zF6g4m^}W^DY@>pjf#3z2INs#(oHlc7AdIod&=f7u5eBhYXhg)jmcSTiAa+vD&S9d* zwW-5yIBX(?n1=xg6m*!136g;tSqzvZ>;qsK4MKwD-O?UJ(Oh2a@<$xN8!o9LmR$8- z>4Gl^PvS`FGg})u4={&eMqmLF)x=Ji^gy3{5&(D0F3{u{m$`ccQU$TGfwPvdd4?ST z3f5{bohD5dHq5uG@W|24|4?~*ammv9b%A09U}IY8kz2FaGi}tAmY3dla4<C69(U0% z^@g7`ff4Ad=T;*WA7cCaO+-db@bBt-Z^uRi^Ws9FP4geQ3a)eiIal#3pZL=NeCJeF z=8?+Ip&|;X3W4j|3Ii}FDiQhK<uQ36afq*__+dj`ZTiWbN?!M6R}U-N!E)@>nEfXs z($Hm{K$Js9T|}3}FoRGI;%gSv1Tn0zbJtn1EV%%3hdWsUl-R(!uBY+lyyD<n!LB)k z505*zc3SjhNdlasnN?|eO>)&iI>)3#?;@LYMyi;b{4sE4ta06BioNDk8S=5!t3EPp z?e`S=JLjApW{%V?L2j^d31~vA8Nr^@V}DV}ip*d$fT_q8^?^^-;1t2Cj#DS)B2kr^ zPPGNTZX+IC>r1X%un{=tuA{y4RJ7}H$6c!T`+i7bx`c~Rp2r(}nsp}cz`ABOi_R25 z{U}~4dD;<9ovM2#+!~jk-`3;FPh|ACH0HPuEFnz)JV3so<u1uf;ULFc{6zSVUf`$A z{N)eDu1xYn^o{+Sh83PDQP4VgsR!Va5{Jt*&rlmS-ht-sdgW8Xf?{P|o=QypO`%ih zaxZ(##_hx>!1Y2G>lRk`*ss$+!546lH<(r+n=GRl`9Z}>*3=w&X!-0@X*{O&U_2U~ z3<_=7;2;XH7{G$+{4lM13ZbU=^m>kujb6%@t)(T|O1OjF(fF`L`;K15_Z`B~Ey|G= z5=QD5wOpgFN;-_Rl#Af2!^?qN|4(uI-z*v)Vb2xd5!Dj%s(({bc%f#z;tCnT$iIp+ zU<hitC_xmSkBz5PE!O{$THHc6pZ)^``^Tt}k~{v01Ml`4c|O4dfz8NSN?P{MAUyq9 z(s~N)sj^RtG%QnsJ4W_zZtNe;{CV3@4wL*u3Luzl#;_9uWx`^=#_!Y`EUkcykH7a| z{5G)Yk%fQ8H2-MfF(Ke2=Uizl`95T$wZA#i)y6$vq8Dh++q>qhA<pkJ`t<%x-AvV2 zaIItATf7><MO9~;Ku0Ishqajl4X+m#zW>LiKEFig=MyKjaiE@WDgK2rRqH}|YO?yE zxmZGo;1!(x{88p;I6?i(h&uY}k<(xJHBA5brhmJIO`JvHDx)bjfVxCD|F1D4ev2ci zt+jd*1+0!%s&pm^u>_rA6v(>cpQB+jQK!qLTV;f>wzl4K2^iCGA1Sl8-&%Ym<T(Gj zq@={r@cywHphM)0R@gPhjP&+S+s@c<cy)>G#0fI-myHGn2HIG)O=Kqu+FsyPD#!t) zaUXUU!v&Gm3hCkDVXI2_QAwwzfr40&1k+y4(=LC;rlwY)$4Nv)w0)@d=)<urdhIl~ zew_~3JI(}A>xN06=u^@2ZhLc;*S+_;kWqdP_;5xILp?jL&d(|zXV7Kq=2rRRO`s#Z z*28?-Hkz86?IY{e-#~@kczbzh`*0D%n`yJyn^goT9NMd3YdY{$R>h?2h*-|zfxa0; zECIU<=+O41{<-O<U>a~2RT(5;Gip|lgPH3BR1I<HYTbd2_UcfvDR{B<HM2eTXlKM; zDOH?7qjGO=Z{vI8nUSTv-YgZr=)}lKmWloT^5ylETXvT}+C$~(&Qv?E6wO?c@D}}O zUz(PY;@<v)^Z`TkO&!f9+FA`sUXULXyJ)?*`svn=+L#v@;5W(1)@%BJ3n4N9xE`i- zmB#8W1H#5&U#98YlG7?KO6;~~Q@vx-=gk@B^Qx-rF02d|Hr8Ew;G(p2qipR_W_8AZ zMtSGl4=x4q$lNO6161Z`0uBl8L388#qqS)U0jla5$6~t9dXm@D<P`+&{jM}J@h}nn z0F$u=z9=wU?WL9jS6Z1aie09-jy`dhQ~d|!A~er?Ap@`wUQ_|5O;%oBDem`dKP%d} ztd^0{QTp@69f^WnF}Sy!$dLq|C3i+ZK!Dh}W?vQ;5ysFNO|Ae&&TW66P1mKRBMFX& zL_OTtO)@IL#PFTC8_9at&aM#myFLXmgScnK52D2*fr+IB5}JLYG+`1*E{{paMTm%V zE_fHr5J$V1%FFFdfAnl&b5t>&w+TGL7lVRJ-(GtxrspL_o{c`yrdez<1UMDB{gX~! z*4J;`h+rbf!E9{E===9+E{RAw?OW+pIcDJ=Yjb4x`4u(06(DRl;yA8%B$ELbqZ?D{ zyrQLT^V)A(EB{i#r5ru9q%#=Z?!U1uL<bjFS>(b>By!)bri?G;2rjJxNh)^&T!saa zmLD@(ZM?WzczFRb=diO`(QM;1K3dshbxwEQk|(vgWpQ)V)z*7e&p_Oz-l&s1wR&fC zFJ$bT(~d^OX2_Yct}#Fv&-vmhZ&8=QN6~z+JU2}P^2?2UVrwoU9H|KsHWeT2C+x?b zf@{~OcB<|xxK2(8B10*X7k$bx>(qcmXGII6F>+S+vXrb`aPz0G?)x;KODstYv2>#) zediOEL%Xn)<bdkpXjhmJ3LWL`jG$ehTjewrd;4iO;ru4%)ZFHnNTK$udl>cIu3Mc8 zcA;Yyw!%ww%0XdbEMg+7dIuf|q4@zRmzt=N;(-Ut{ZDakbhQ;)>fW5Slt%4vw<M-4 zmFMKy>pFet!Jwb6x}o#SZe2|7gO@KxMUC`58gxLr=X6R|in*MovOYN{BP=G^MMl~u zL&JKmm(U(y;W=t&kW^=>h!Iox`rA~Vz{fk~s?dCpBe;Kxl;XR_^;7>h;5^}n$MU1X zEH;K)+(<Bs{}uMjVESDIkc-dco3-po(pn12;y<QJ{{gz@^MAOa{M7|t)aokr(!ZR* z<|DuKK=+AiSy-kmui8I8_Q%_XRNnz3&uF8#3sWNiVEm5G(&v6gXB6eCtJ?W=xC>k0 zGAf9>z8<7k7Q$6PidUR4U_a+w@t@<_U+(2+DZP#egfQ(dw&aa6PtKFOylg4PoD$(o zjXzuI?Rua5H%%26(-yStw(jeF+kboVDmBO<W8LORe+}6GGTi%sN*{=FtzP66cjA{2 z;Z9=iMTS$5DwNdaH+phU#bAP0ru@TASVIQvyq<#VOSOor*VnxU46|+*2?gnvhZg_| zSj{H-KSsCzXn?y++7BD2{-K7G0u&i}sR#?2xDdDS*9pN{3|wISLk#d!(_lf&japCU z@wf6nN&9{$ei*;l|DP!W-BONSXj787CIR}C7yCFNX&^+^CaEqKPEO);8dq~LwiF2D znlHW<-bRyJ)8%BWH9vFwZSONC;J|n8M3>*V=V?B{KoCb)GZF>_X7l}v>HLpL`uE${ zmw;@R^jPVj-4pIWQ;(KwJ}WLY2|#Qwq)7ufq-c{*&LAMgs*ub~7MoYLRKKxo`#!&H zc&F@lg1}u7mKd_5jSJ4^4u^gx>HiH!{FIG<Jy!QGW;`x9^ZVdPx|!6#q9n?4yQvAc zjIbS)P~2@Atxo=w0#6)G`&bPuib$pTFR{>Hv$o?1%Ul~XAR;_>YP8-fHiQ=1NgSXj z3>G>{ahUw12ATZOrK^DWkr+b#kJ<m@?e}XqsD2fa-n8o+If%BExK^Krc88=dxYdt5 z-s?Ri2_9(-$3~w|oSoXK*?JSDTiLM{{q0CtBcCQLa_%l$fvJul?rIw#Nc;)&<o<tQ zUWi7QOhUYG=hIkMQIUd==#6+t#8OcC7fSrBQ_HT4L2vXmuG`ihUaNFF(#lSJEcR2> z=kfkc=lFrnd7Ma5_%C2K)9X7-ywaK4oyM;BOhkXE9OoKuK$V@u2--<w9D#bCdM$0B zkzm68o@@v$C*z8@!ac5(a){LW$pbSJ=>a?JQ|wc*ifX?BsuU8@Gd)|PC|~9J0U8_= zgX+&rjDNK1{q5g+X#GPES)QSv%-sxwvEc)$jQNn$k0|XqC$!lo4t99xvryMpX92X~ zEUPz8Kj)C?_K$hC1VQhAJ<pW5N`d<Zx~mOPrJR;30k!6bG6wZB*xTD1jenyqa%~#U zBs+}UnTWzwC_>=7>8m-A>F<r<OTK@pq){pXayAI!T|k2`^)8zy<RAU|<1Gq<Tk&K3 zC6K>1!~C5{@72HPdY&e-)BA9Wo4z2u56&rjU0uHVI(L}^RFrgRMR+5<j+!zay1lzR zvr=^VC$itAL9tF9_0lM0<^lPhi0A*KK>x#Ie7_3%%{Tsvi$GN!Q`)u*78~IKij848 zu&ykcxxsWTaz>3{edlnl+eU1c#2b))7p5I)30jha4mT}Th~utrV+>zIGS_1!kG~$j zbisnPv=dAIz9?50TJ(SA`k4KmJv;kQS2ud2pO1;J3?WaML|x4>MS6wh<5U*oJ@3cx znuEpE8Fx2L-kgu*KYRQBZdw+LN_i?0@EzoWjsKem-I2t}HDYr*BQ+8ff*1P{JPMtb zvNr@3ClEY=(u=3V#rFtq(DTYT*KgOT>af?YY(3(8uW{;B(4ffM?2SNB47V36%UJC? z>cE7|(d?Y*!b>5D=<=Iw2F20Vg_rqM_);pFnZ>96?@;RmzBi#wDzhruE8$MjEdZz= zXY7Fliht8|6wxKH2Mjr*kk)n`CPAmt^SErJ(q+UD>}MiVJ&N!-{LnklH|ER$4)p$E zF#V<pK^jhGOPWe*%p8>5+J=hG|2NOSK_&zCl%cbWPk93YrDeld7l>4(rhe>8-Bden zDhBTqU%fN9EoDM3iRj^K(3MYT-_!CFs{Yyw0L0&2a{kIGp0hM>9$$_GN!X|}|Bth; z0E=?l-WNeZkrD;zP(d0A=@w9t?p6UQ$zkYFK~Yj^>5%SbNCoN6p;Lw&Y8X2Hd(OG% zoO|!@oO{pxKl42BF?{pQ7kjV0*1O*Iu4(VPb2|oh26nOiJx~K8%xlAT7X4*O>SXE7 zWxpdd13jOo-F?6Rvq1c@RQ$FjsfW2gd=ABwAkGI00$P6C8+fK>X6>Xi$6pAxCQqw@ zPDdW!96~NM;n~GUbd{Z;^jMaKjpK2ZDDOW8!?z?!b(y+`n_UCIqhT_+^mkl<lPj3p z@+I+P7qSEBG*j-JGFeoan`Wbp-;<gS9uJy+54*|pa`Fz<wejf@PE1Bj!P+{v`+%Ys z(!T=*7J@Q6H~+Kt`R<y^<x<y9-l-Y4@AzEZ$?ec@;Kg0F>HEuQF-^k#|M0TPkfwuj zE?EkqiI{l*uq&tyEkpI<0+#Vb{HQ#PyD$#5%FQ*~+|d5Iis+uOHCanj?r7^bS^QQX z`txV8Z>3@4rGFR&;3Gbu*vzcAkOmqMtD!M}&Vu}RW5I<nlKDs;vAetliY=|2U|C=) zt>+}I5nnJuz`k!phsQC$dnfZX<0z{A>iD|gbt5i{FS<KFc_qlZnT{Ga&mQaP#gcON zqFYZ}QSX<6K_h4yI5v3s)=*t<5XV@K<9F9mp6kE=JGSdl;IBS&!4tO$%rG&N!-4<k zF`<65d$p`{izMwU45n*koinI^L;qJt?b)ZR7)VFl@qdAI7%>G_%HBsphZwe{7=V?7 z2lsgj=3e{0Ok5D2#k|C4g}y<O8zgXJslb>CuS@j0<R^Z#oaVT<t}Eoeo8>n5YG1ns z*)jm2k?_uFt_eqdef^r^yFR%<6I!#tu!c!Kg!=wUxBf}~WN<p*#T&r@j=@oxot;gk zzf%dGWn6kjMvJgOj8mx;H12^-SWAnXM=ctCmKLi%)l0AjSBQg!pif3dMlz}?X4dj` zOXW&RO7b93sC`3(9?UWQ=hD&yC@@evUrd?7#)PZaXoQD^wY9X448;}~$vO1yq{#)B z{8ALNh&(WKn67o>@jh`~Gf-8Xb3sQ$Cy9AfJjKMvxxM+x)hmOUoq4*Yc@Th`s5Fn8 zB-rJJ#=z4C1_r)$Byc}}%dT7ULpVAj0!M2D6s3Sz+UvB=X+`bXVhFX6+DM#vdX&dp z<GXJiG!I_IEL;0Vd7Pabg1>l7KfD?%0pEd6NEY^bAhGogyJB)MQyEt?56GRh-3}Ff zu{i_OnJ;PCzRz~poP-V1Zhj<XzR0GH?>_&L`4tS5kPwv_h)stxE034>vbzG^b;pfK zA$?Bh{%$)!K$2{JMrGH?TYa^QsrEQ}b-QwM0_u`)Vp!+S?-)HGoVTeCP>{$`+j#Kb z;Bvp|oEmd;^S}}0E-xku7sKExYikt%BT-_k^AdMaE-ai7PO1_4xVz_pgt(x__9L>@ zk>+f5+b|LEjYisaXqX6v^q_~)*zd3~;N}k`GB}}Ehf}d6q%Nl|Kh`UL@y<drI%;BM zglyDJzv9&wi`jU9L(u13rm}pNf!SJZe1|WKEmdxyxJIiX;B)4wp9tQ*us#WcAkkkJ ztt72f5-#_ey?i-NzeJDy%)Bdx0S1HZos0|*UxHR)ATE}B_lf|j;ws{{W6HwGS@BD~ zlK?0(0wrWszal2m!xwbw5`kFz&7rxDnENKC0N<*Zq{j|M>o6<EnsyFfI7+T2xh>bG zfx^N-DHeHB!@TA_Ws61)-eRD8E5WCDUh@!etP4wxjne(_%TP`(E=;LPry&6o+|l>s zwKg^{sK>GGB;XO0@rf_88fL|SBEvv12!oGcP_~OA4SJt03$7g!@FeR4tZUe0m`?E0 z*vjFswQi`;pVX-sP8k}9zC9_X1w|`8T)$PW<fW^C@}O8y)zb5St6KDvYUF{5mtXyJ zE5>&sidBGHfZP3bNf`6H0M|ue>!=--yq}1x$Q0FE*Z>l-N7haRZP$42soUjGvul^K zT%x$P8d53HG5n1f`BvOWyTC|W>SbZ7fk*Y=S<>YOt1aceB^U0fILml_?E26l-Sb@- z$<NaX{l(Styxx%HP<SNOt~K{!#wiH}YqG$au#~a3dmpvk@=zx98MpAh9F1OrN1B0l zG^lSGnP{uuu)ZP%H=8;nsGj_;<G+!=&k9DS$L8vYp<z_DhkJekqz~FL&8p3hYeK`e zj0%LfZ%(x3?<3HYR8Wi6jnC#1UQKnH(KN3X)m)Tk_^DxL$3lGIwuF5(8y!YoR+vh- zmSPDD8ol?MBf<Ub5@ytX>u`AX+XDTc*6^>Y|K?~xz)C9U#pO#*C>RrSrXt7x>z&=e zO~e+(Jz9e#!|sE0(Ae6j_WM4*5wEeog*{+v&F)c1m**q)s#<>mz`b8SMD;jmn_m9y zw1N814d4@#4ihP6*<~K$`0aJ7cPsq?t^BD!jtOJ|-wmWbZ5s?TGEG}bQJ89)9gHKe zUn*wkzgQP!dEj;t<K4tc_5PEe6SgF^DMkcpND3uDtm6^Q@OK=J0Y>}dYbhTC0j*&) z1hCr~gML3hZ29Eul<EEN|4X#`=TXded|Yb2QU*8{=CzN7@mo(%h59CM^$^Fdqo4jS z0VycJ|7W(qts$V0LNk<^<ola|!GE~u<=l%vBe2>lB>M325{9OOGJM}>Is!L)0e4x* z`v(R0AWL!`I@bdGCA6rLm_*T6^rGG@$+#pDDH3dhVvd@6ptEQC=Za|mhC+N|0@`;O zSE9NzWF_N96L`ETf@lS#0s{R(I()wq^o%NtXqNkjx0q}V_H{-!YW#-__}w3J*Q8ek z9Lb;;hQUieTYHJehj%FZTAfI!PmFPa$5Z0A=8TcFSWZ0rc`C3taLDWGXuNG;xddy3 z44*?H;U*h#8k=#5mIDgu1w(C6ainQ{>-U5I2j7QY4BT$TP&R#}+Z)huy+dq*wv<Zz zpgGeK-65M@kP{IV6_v;p#PN@0o1_7_rk3>Qf6R#gFm3CYF@p#Bu5&%2%hlNkaAx(z zWyWn<<ODX%J*o*iJt#W2P#{Oh4Js3WKyI1=2aKJr9u>AQ=f2W*ZACJke4!ssMC+P$ zVIQ}UE0$ysOpR<~@X&zY)b1X}F!cB@cy0d*|IsUAE|HWy4(ad)Q}j?WO!f0*<rjvh zSiRSS5C6D=_A8$l_=K3F{<Kc(-*SKSDp*`H7U}KFmOJjwrj)M)u-ikHegY!g@*)_3 zTYI(FG=dhAzl^EX5x(p(ykosF%9tL_*)}~@m{FUc(P=JLFtcIMU?72cWlNmHe2%71 z2Pd;Aq|?@XDye|dVzQ>^)!(H)|N8OW3c^g5AoDsOT2ur@>CX@jqQ}kUxg|gj@MuY@ z2>z>Wy><aSG{V>6Z47^DpZ@sAHBro^MYiL`>LED6mJ@;%Fn~Nhjpdn(BG`49*x)lP z(LG6@mG^5Ozwc9#n@QYW6mGsh496N#K%z+nCXY}Mcqd5i!N9-Y?2Es>{WostvI)2j zqzSX%)Rikhb@j27RLaK(B<;Oi+}+JHx0{>Z$35$-{5cOBOj-PGRKIx!WLGHj1k(S& zg#WmRf8Y8P@j9BJY`_$c-%V`cCH1RNkr_ZU7fS*qnLIC>UFaq!zBIU|v+I}MUll&< zT=G~P5|B@?%jf@&7OtAAN)$XdO}vA;gRyce-h@DE4m_rW!kT4Fgq<_M{q{f1f!|g) z$zF^?;gaImi(b5^@jNmJPJ<T$S}Hnsn&g7Q5E)Ev$C2jg2rbRt8;x4OMO2kQVCU~< zidV?Dc>J`!TuV;i&4CSwn0xN<*sWml|4Lsd|6voa+aw(xRIQmTR(JEX;(VYL--1z# z9K5Tkt@55X-GfT@aQ#jX@D06MwZ>4<i~f$jKUx@?@&5A6J}y|Z!hQdupz~jExaD=A z{}uae>{TfK(>Njd2a~6-H6kKAHsDE{+sIO>f7ruEO2G8D3l;w@bm7faaNF4w->Zbf zc_S=~#8!Y_7(fi_YQb}yPK;KKaG<X4co?@1y>Bxjs_O$kRdZe}@!Z-|Ac3<LOgSiy zBrR6;RQ1f8Ut2tzJ-hX4U;Y;vvx4&}+xTlKZ?v{qev^dx>)9sxjQc83XyHyopSg8m zC`^%Sn_&t|%AL%c4Z{*dZ|i=g2t2z0R2rNv#6Kkb3kuvne*-9}0~eP2RIYD`C&>+r zEM8v#PK^6@IfyM3Ta4-H);i|Li$D$bd`@U7q-7|)JX)gHZnSC`<LYpg!<8rP*Lr&m zxn!~$mY9GV)ATW3$e#*|{HsMw4h6QZEHr`GnYrLw&=|^W%5eX&p-ph@&U#^&BU8W^ z!hh_4ksLh8*`GIlvys1z=06+o-;kGTucbr+rHuA9iVnfi#N8^OXXWk&glIZrZaSOS zJ)}Tg4G385XHF_6hgvJ-Mnp%p&0!z^bU!SQ;GJY*4MjDg+t5;%<uN{-E1}aVH5k%z z@bmF*g77SHU`;0Aow^tQ^-BKDF8>3;vc+7UzGohF(YKgwQqw`Pi91{k-=a={0&*Kj z4q#8?2mESWpV@<1>13be6GHpnkDDBd8H2fMJw?lAc)>mNa_{>rh<Litwem_zAvXvp zm_T*tz`$hu4q5?GS-{&c_ZGi5Khek5!Pvo=;OlV*#wdz!1NjOJpW*DpO$fBhdi>nZ zaTIsR1{1X#z!+OYXu?4$&Fd#OVL&t;Bf+uf3Pe#avh8owY(@b_!?<HyZ0togm%$80 zHGO^S1S%>jzy<(%0KaHg7#q{+z^vf~<?!&R+*TTmN)Qo&!u^sSetv!+RnZy65q@&w zx#qbxlK-v4u-3Ks208hJBO4nVIL7z4mv}&*t+l*;b(N`jAqXZ~+j9j)#Izd4JP^dw zFQRTG9F?GRS&?I=VjL6i%dVoHquOgZaeS6x7!?cJ&EZ^aUskP_P&%$aGLDj8r7J+# zw=R~~n;RAumK+d?3HD<7t=G2!V8Bx3=ub$)3OY%FfUnA)p~x{XAmC>~%|^+hl^1iC zQc}0xQXk4=@0EQ+UUyGV6fT{oO8p^%O33TTNyXn<9S+(;P0?;<t7l@u_3>g8e~$eh zA4%VU3(swF5Aj{6bzf%a_juF^LL6~75N($RPdeHSPicb+|D)QW{nVkU+r5u^0gqu* zx7SraZG%=h!#wj}|FSLx75^S1eA6SBO{NSb8U}>PKw`XxwOYcf$ZJDss_q#6lt({# zBQZ#AKRRV&+N;U5qE8>{0zfQ?v(Dw;ht#(#U&UmyysrRw<Yj}7^SjIa@C(8OdDX7l zFF?u{2z?L}JR8J7IKF}!`Yk|J0vR2wXZwlngXL`EWm79yHzHb03JO}Kl`UF+lm3KN z1RAQ9ekXCSB_JTc2uyr?2E-$vggh6turEU(5E~d23VlPf378G($;sb9WIu}pybv4u zoWz;c)zxny+SoVwKPZ8)dCR%c?+TCmu7$p>rl#hzRuLd5L3Jm)mCE!RpcUw0gd$E> zVZ+vXVBcAn(W2)()+6ltwz9Ia&z7`(Vf<TKp@8<V@N0Pf3j>0n%b>*cBZu}cJciEk z$c!F`@6>jmuzHRba0m`G0h$nxR#-%@!6yI%!hQsm4OAy;K@d;4x;yT;>x$OF@FL!# zPgcclm_93yQkXKlIWPk#B@M+<l7Kd1j^U@MgIN3vCKj(VbZM~g=GNBJqwM+~s|L6w zayB!$DqwGcq?*hb9fiJcVyE73dI8GP4R|PMsQwbfh57TeIQTCdZZp+MtY_&XSST?+ z(yP$q6@AF!Tunv3jmb<ED!*4*lAAG5Lbx|#;TB+TVE<~Au6<nso&;YtC~!YY193a$ z(~*}KZ|h&Y7BbXP%mFvO44{Y-fld&%i#bTq6|!mj8ke6WSA)=*_tb)XcYE(8>?lL6 zFqgi!kk-yFBG)*|V%Dawb**D~jkPVP#W&wTtkS(pO2iuGuDAxh`STE&?}(lrHh21E zA5^Z76cp?O9TwK=>(R3{GD@r~cq`gar)z5`r0xPQO+4!vHbd)Dm@ah;AKz=MHnfYJ zh=W>)6-tThYtd9!c0i%JU@CY82%6U=`WAG8XnhkloL^JT4&Ws{5%apxU9PqQz<mHH zhfpU5>Tv$$185+NVq{ziUBv)Zr_(ty`&uwh46CHw21NEszvy}W8YxNgT(s;Zj_p$V zJ+JbwuED>rAP?~|Hnl6XTCH<(5o1<QFlIIMO#})1@R}c5t){`gDR>FDSN)g3ejf4- zV@1z)0cF0ry5rXGarvJuNWZ;z=Qhw$GL86``S=M055Ftq{ipaWnFzB!|LS;@PMNt| zDZ5Vsm!nP;R36^;;+gn7#(82e=RN!DONd(~5gXLHEgu=zz`l_VGXq^z|LF@wZ2x}Q ze{xf>)qy*NjjDCj_-$v&Y8;@y#i?5n_}iuY$;W=*SpWwtznV;G>Q-MSz*av+1MrWL zQgVW#YIP03Sy;MmTeUs82<wE<*YyG)KdWIa=vvBDR#EAnOWXZ+;D3B~J8)|FDDFmK zH1U3QVC;d<4!!<+@%e9W|5?<z4Ag+%17^BBlWbNQ5Tasg$b6hnHW4ZSvfq82bV3ce z*HSh<EtsvpsAqny45#SEWC1l^ANwAs<#r{B(z%LndE%WNf_2vh&Rlx9W?<)pZfgVX zH+n<!>~G)%lJqv17?6sC>US3B<*yj|%Ab~t$^Yz*{eE%(dKE`CnD~L!7E$L)aX7gf ze{wOLflruM6I5_a?d;a{Z29AJ7VncR^!MHr-I^6@hlDLj&bVPBg;OLEh4k@p&E>t{ zO56U!*8$-(;JEZAF*+0fb~l&RF_Dparf<R@H~wGG?VpeLjT93F#9m8bD$IObw^*4J zZVYIgzj$D>ExfgDo{tbs=Sg*_t<d>LgXg*Ns*)FR@ZLx=KB6b4cjOtn9E9ZycdPSQ zt3#g()l^pQPe63ID!${GIQ9qySUBEb#JizNc2s9aFg!TO8}`yKga5knp$XJQ6Z+0h zbN@z~yz-pb>-`JU*serQ8T+q6yQ@klXjHddNc4NJu>ZcX#fv5AcSOM*KxA<-+v~Xh zQ@kjv_s$2<KSi$2eLp?rc8f|o>V>e&yh=@(7$Z+uYN|<g#?hs7<0($FaotDKFI+SY zEg#>f^Igw7Dqp1(e!&-NPB~TO{$^vQzI{P7K<zkV5V;n;($j5Ss98}cATKLFc#sWQ zHZ!@6oPm92OiQ(BEQ7IU_&h##Iq8r$|M;EZJD<%Sqq*N=UhGnT@u>($alJ8MnX0|2 z#Idr-%Iw(TnZd`~61i+jm~w{gUsa}dwmi3dhSbnkU$|~p_h=>Z3B{MDdoXjmNR5*N zhN*cs)UN7h{#-abtwoXHlzk;G$EY@W0e7lZ%`ateiF0HuXrbx2a%DCB;==ggWBP6n zWu%M&9MeX)fH!LS%3n6gd;M$Iz2ARIk~7ZQ<8hdn_f-f%-pR_b+z&tW7W8Xe=@F#^ z^`7k3qj()u5|K}IFH|F=lPvl^QB(3d2x~?}_94pICQb}uj*}ANl!e?5olvW?!cfzs z3DosA&#!cB-si@v!IgW*KKHhtC0A-!+I8ez8WPEuICY+5zZoz5G<@1U?kL0jp41eK z6xW8v-*y{hGzLO%v?kS`vEkoeu>4-4JcM4gJCB>#8L&`(^yn7H@vcH7VnyCW=63$! z*`}QLl(BK><Muj>y+*U$)YP{R@((x4J5lA{K1-m-^}EaFOVtdMCAR7A^>Mikc1QGy zY^ceN@DEKM6$7Ut0u#GNG3V3rLmUkK=>`;?N#|&n%AL2qzLtkeJ`U|{<g)vltWw2? zr!SE5^xel%<CZo81tK%m-{*)|^=rV7J)PSZasIM-(!cdmx_~nt4H_;@L^D5GGr%dk zQ1)|7eZ2npO@b~;ITPjvS+l#n7Hdaz6Y0D{w_8i-o#L<iC5qM-8g`VGej=*3zbd`9 zwuZzE)mR>s&1%!dzRpXTD<>;yd=4!Js{-`l-ZicTQ<KJZHp#jdSV4>HQ60Uq6A1J; z$L=iWM@&yi>^%k+T|+rp*}*c{W)8}x5Me@E_w!J9zH7Rrbc01nYPlt;!P<o7jfXzy z76;m!-6DvudnWYG@BJq{=}rYP6e-riPH%l`@+^I|%dxPbkWXxD#>e-IA*ztf6*JQO zvkkkbZJr#9?K{FuQH{45H4>L&1Y?Cgut3N0OsWOaFb$DA9AYNiBIHXHZ&j2Jp$1^Y zPBBr}FoAjO-0dJ+`1kLmun?BNj0Ma7+eq%AA4+EHn=D#}Mn?X!ay5(O{wg$OGPeq7 zaK1IoJ*2vaVi3C>&h#`%K{?>6W$y?&;=$`FEb}gP{kF~VvKIRd-bw*J_V{_%TqK@z zYrebc_xai-;juQZ#=^cLZnP*tJFV=O^@pq%OW&%?2Q4j{nPA%)8X_4v!w(r4#P7dj zWNch<+a~1+<nBhvKYS;3G85Qw6{@V}NOW}$*`knmvoze?H-L938qcmU&16Baa>1); z>$F!xcVgsR5?xzY<t_2yR@UcZ`puxWWu2b0u$)|@Dl{{@fsUWLmmv-R5Io1JJF~t* z3$3if#01T}I$dqBp9$Lwh$+zbyjW8$o)#O+T6r|I7HCH+U`EznF}I{lXQH~y(qW!r zcNyQCD^W;^yf;C^Yv+edN9g>SsK@FktwA*0z@zqY^jY2h9NjHd()^=6yE{*#yie$I z2pEZnzmL<7S)EN0)pPIY4la(*>g%Ub8>FS0xSrin*InVw@m5`FWWIAJf-#j5R!hIy zgEAY?M`dT^1USDs`mzwed2Th`VbC(BXMC*}ANP3ajy_y}a8Yj+Z|N++W41FoIEQ0y zawcF$4PC3_CR?l$7W-C1j~oAfxJL$S%GavBj?<BoBfTRI{UZN&{-vFkLXpuzi82YC zeO*ZXoqjsqPZTSRc>I|P-fr&^?0N+)mdJ+l6hvhNIVEMw0-E`5-(>w9Gdf7OGgSHM z*U>81oZNhqz1~Be1+@mDhOS~+HgO24pWZ}Tq1klD$uMDxS`<#gMwO$%lCpEpG24=I z|LEf$H=pcT(^AnmwnCxAw#_f+aX&i2d_Q$p6=$0z&Pmdumij-l`!7fDH)Whz;#KCN zdll-$kC1Ung9!GojUJUNvjyElxh}@B;&%6MidD&jRb+L<KVEfxPsl1>p%#y(&={6% za1SayeZUgI-{n<fmB1;lyz#op&HC{9?H0<O*I|WU4_J_6d-(XQ){AQIxXI!;Pk$8W zS5<|CbR>19cB(`&Ez*vc>Z0c~os$T^v$GnWeK~d>9+B>$7LkN=CYh5M2<UB{WwN@h z9Yxx1M3$1*D^IDr=j2cHCWz+k1b^WUzDz))q|HU95^7Y(a$l(V-r3x#h?D8&1DT;5 zQnlkflj4mDIf=VQIOWF`cHB7b!iCiG!H`!J-m7im)!P%YltRwe2`Iz^z!$b5Il0aG zd(0FL+t=AyQ(!{RO!wv<`h|2#oCjPgCa9e{KEKB>6EJfg-755q^8pWQ0NMEl5o-rK zHUCmSd!ezAvY7gyLB(Ak`+YK+vC3eh2lX12%ZIl0GlQ^U*+KF>-%@#=<MumHW~OwN zr|};GY38<@a76_wDIsH`p}mQM4{z@<-6L0S=g1>OPCpDiptZgw=ACn0H*O(U=Y&<i zTEgRKl-efV<GLT^$3mqtnW8cXmjndCZEydextVz&-#XfWT8^}ac`)M3X3<)1X_Dg% zfj4u!c*TtD>+DX_%04i#?4T#mGXzeG@RaAP&RT()Pzdo>6fpXAci(x^|6AwX|M2It z+gM2ARr||OVI#|>`>m=Xt*}i0V$%4g$4c?CG%NGOtL_cQUXRSxYqe|b*Q*XcPu`Av z$)WSa#m*=_?bxNnz8JULs49MAsyfBb3E@WBwBdsv<&~P)<LG>W-P_>d{i%6fKe%=? za_R_Q%xzaP*qSMjhL^`b+m+<UN8S{?N!E^deNA|Y@9Y^3Rgl}Kgt%V)&ZS(BL~-{= zTspH+QQ|6Be&(>USq_eGj=}ooVl2o8KCXTJF!v-;B3;}PJ0eD7e&qbXhrKrUe#gFZ zA}E;|T)(uL>)b}fF~ZK~_7V^Ou93k5y6J}W4svZ-2kjfr*H_YxgO_I5EhcLb9yxjU z)DO!)sE)JD&7DpR1y9i~#AN!hE%n|L(CluIx1w{mKJ#EzG<cYFLrldh*c4Zh#bkm@ zf>m2C!8l~>_LrqBPvHt5+^ITfaOEVe%x$^Q%mrMR{sgK9x1T(Y>kr9-5!vphwIgT8 zKB5R-_yUJE$?E{BB`yiC{DZcUpXl|NYTHE#0;3VfI0zp95|!9ALqVh*exUSAmWklj z;=mj~p!G434r$et2=mze+P{#Qd!XVxGE^(;^or1b&-OXI`|5DP4YKumexHv|Y(wg* zAXbFBjCf{mN(dRR1<xLGEk62WzP5a3!`c9))z*qCO+NT?KU8*-y|%10Z&yDtw|G5@ zMJkch;UjX&<+Zz#Y1f0iqn5i3wr~$RVH)X0p`mR=+6VDgrP$|qKf2?)51l+8J;M9w zThJv#F(EI?)x7c`aj68HrP3C}vC+n}*%CVEm?@LRSqMwhBpx>q|7h1)YnLsmd@ASd z>D^h|N0?HdM>j{DV?=O(b@A_y)HBdeQ{}u^w*8GVJ#euvqoOvH>Bw2-+dh1_M;Xtn z_EC&ZylP<p3wd;ywMAQ_COjoGctmuEBuwnNL+mR>TLPimW14(+)Kj(YpMP|h!jfsO zHPfJ`kyNa4f(o3c2fa_$E6h8EHU00zymLa>e7)=4E+3MgpU#<$f;QnApG331K<=O0 zkJV2b*dBdjkL9)raTGN`oz6s#Dh>Gm3`+fc7(S}HEo3)*>gy0l=J&)8kI~G-<_gS& zsl=XF*@mlOZ{|FA{%7kC3P|?M>!;OTr&sZXU8uHs5nNht4dyuYo8n~lBaDVJXD-}5 zV>ic2#?v}3`KI-wp(MRBlbD87W+AhOVXWn3erzFrKQ{A~O+aT4jSO^shc#9AO4C7g zO1%;+?}2u*uF6ELOjZO#*N<YY#^o#PsX|UTnwnaDg}Dvt4hXtNbKt)|Ie=}O?>i;H zwfA;TETon*lFpkMXB{;)d=hUjxP?5@IYrL6iX1RSyX6_W-!;S;+7W$34DUU5vCV-* z&GjEAoym$3A(rm&@kJzsb4A`d+A`|#(O>d`Qqk?A%%Bob1U;HM?otYZPkBj3S2Vtp z@dZhTt>L~MfFGDS8|RrOsXcsz{VL3+JyxmaC6oCt+T+;-6A%0kOI<QHo`t{4p2g4c za?-5hV%TocBj;t(+<SwkiPX|icr1iWrrP(y0Z8)V`yGDfe}%KStu@>mnABI$Z9T?O z+HZemo$WOSQ<BPhWxUbuz{M*3e)@EFzhh1nClZv}@$Gz+RK<yo9<|(>A0uU*X2;{V z8{|Zp{~A^_jE9^~w)?XBC?M5CH*>EgB9a94pAbt~$jH?7b3!EEj<Gnf;H%0=%Ph|p zaJ+{sizrB_O}t7HcYpov(-MJ^9<kL;xbp%BYzE<RC+}>d;fud-lLy_|8^+OKSAoD6 z%{SF5A(9AUN&&N|aVF2DmZV_Ugo4l<$+MKg{8mZD{bmM@iYkdBl(vcI^{_C@jT*Wv zq4I!BG1t3Jm7X}0`^65T4?3dIgnQPJDtfK^bU!<cq6=xHo2LfZ2_N*1aae1IeQ@e_ z;0f(iNa#rzToU?G&$v4*ZLN$PM7aPL>t6mjeKq}IZxWB-vQsJj&?<4x>{ciO+*x-8 z^xBz%*Ep62wLZUx@=D|KlFz9j`6iEeY$>%q1aJ6~>x8(q_~a)2v&GcH@2$LaNMb9a zb0M0AgtbQyVQOjgt45OlA(A<H2%2SkUkrRvL;Ys!1D-R*3DGbYiMtpAnWB(Ho{Q{h zrYVEX2Fql9*hg@jNeaEPasfgLN!}|R_qP$l3hUg}+Ol!)oV1qO)<2tAxK6t|<3bT| zv+~7d@R<nfn{F*Tdz+XgD+jbYi$w+CXTGwuC6x1Gs_eX(<dZ&0*IcX~e;4}UBM?_r zsuyCGODHU*^>V-~FL`Adf=6UWBkg4d?(Aj}=^BW`4f}|CJ2OEv!YT;$v&zXe#7P-( z6LlVNBC|kjRji51_rQ8S4~bhwOqL1&$eL<#e|;+S#r(V|BKqk|sVqEYu}iu1L+Z5F z@m1F{%}tB9gx*NMmC<livA;*qMxED_e;W1(Oq`r2+t@g-=z&F~qEEZ}-vNx@yRUCW z0tlomPuH9W`Du9aevsjDm(t6y-f$TU$o}o=(%Vc`I84z-b%Pc^Vy;*nJD^IL^i%0Z zVIPe&qH4s5#$boFW}fxq^>sr2eLpr>))7^W245!*%R84=*0-^9Jab2TMUCGV-(ImV zmp#1;;`2g%kGx)MYxuOF-;+~^`3=~QxeeHvzmM8TkwFKoP?V{{F?P*^>N1%?`Q~kn zGl9yPtk6p-CF#{=%7NFXl4}EqTU)!=g)KQV%e|rRos6la-=$Txt$M$E|Gs}fxn9Fh z%L}D_+bA=RMp_{Mu!qBNN&oDNOwa_i2Z?q-O=iz8PsVls;0=vjt&#wper&4=la<uH z{oHS3g&%pTm6E&5m*y$pC)M@);wNd>wK}7=wft4Y?DF2K(9BHUFZST+)~%BM7#U|B ziYJ}FZ;h)8Mzdn2Q6+-dsylS;+sVnY)xx|z9u!JD(yI@V7xa6upCKvwxABk*cx9v@ zCaoZSbOR+v@*(77ir<eL)3IWPuv?ce;eLi5LkND5`}YOx@Qedz#ml->KKD~ke7|l| z9bwAAWe1^~m4l_sg#CT-q5(l68Q3BZ`kQcFx)k{)A2!oy#B+3~+y!m8E9&d(r{n7s zl5oRhvMSg09r4NuwG>dJ;HgW$xZdrtDRbR_TE?S7t>1Uzy-8l?3F64p0nf|zJmk7- z+-7L-n%wg_5)EBdIW!c?YB?S+8!=u<H&x@6a8-%+xs>a>0K07CFf789tEApej=+*5 ze`0??`2LRh{I?A!#fyageQF!j`X{QSB43IdGId1U+-S%skWt`yL;FH>s+@^05cxy& zwPT4+U2B%F3?ftvZ3sT{X1IGAK~-8r;%G!55hhEr&g+v)QJjaj8B>?*LeG*nc<?u4 z<G5aaAL|L07%a^U9j*)WIht%9Ds?e-4is&E(P=`v<}obV1S?9nZYmCMjv*jGb3{dd z=8xE6?$vk;E84PmK6sT}ett%`KsyBt+0kyl%y^wyLP27eQg&{`Db)=8>xN<3JmhZb zM8kqM8P+uK&f1RCL+u*d9)_XG8Q`w<C#N(;7VTZP%P&|;atm3u>Fo9twV|+6iWdIq zKIh2nK_a%c1Fc4nuMoRO(nz~h#LSG#K~58K64RKUraj$Qn~H<40O*br<ubnu8{)b> zXx8ZpanmENwmBuz(AzO!;LB9-dnGGjvr%JpR$%6@2RRxDJ^%5x&wjkY3;X8HX_v7L z@21ch6(}12b_1E`lP|G#@oPJ&Ci52k0lkkz{GP=A5|kQQ`}fHkY>F2c`y+OaV>y$K zdFbv8!QpDUyq@E^;>HvHCxjQ@w9Nb(oFXVNmb~Tv*OIS5$W@>8ir0FfA6FaLcP!5o zk)#bj*eS6&iGoVp+CKU^^IGc8gh6zqXd05rn}?bJS}o3bHjBTQ<%Sr=3PaA__4C6S z6zLS#e9;Ebl9i6bL9j-DWn=MvA|jBlwYZv}b$1X%6uP2r+KWC3bV~PMt}VXv^Xn^7 zK6~{l#p|@gP1tMqwLV&pnDt5={4B0=i&OK3AW1l38}*YC0@y2Lep3}osgO7K&FLCN ze`mV~58p3jc4wSn;|?yQ;+DCraS40Gg>GMY%xBXh9q=sW_;65uc{Tm*gXm#efuli& zA%nf@$a9UiFO_ZfBI_h*q#<&_Y4NHi*{ia5Ppen;(_k;L5Y4=;D~$Li`Oo(n_jPwi z!;?~YHSAZpnM*<SWmoTT|EY+MtGnW2UvH6t#rKL2s4|_SwrQL7>+VhWZ04_#WfHb& zz`?4~L);b82!h)uh8t5AiL?^zLyu`g+r`u^M?@4NU+=<)XH;wygIhgqf^Tp($~Pj{ z*nf&umFCON)eC$85#vnqcRB69?|BIm{TeY2hnSADl%dy(+U}8=!w>)rLz=!zYa$_~ z7MAc2>Uz%SyraK5M<x~ffeeJQrLNphnrU9Dj{1rSMCkO3PD_3NzI(=ep8YVaWVl># z>#?i-{(}V3+KINL&yk8I%-t%M{F<8UFLa(hZWp^J$`}S>KwZH@u|RPuP47~BMO)&` z?>jv59zXRap9mD3_1N$7RmxM-c)@6+MBam{i|7}I4}EJM6q+%Zf9&@*EFgnEIkQ>w z`ROeR$?avwi}XzI4P<%1`9``)dNHhrR^r=>E>w%xJhia-oMW@f(a}#;hGyr$Ou;kb z7+c+j0piG2=d#5Z0dH0_0;-hpXU+oXciQ@^{LysO)Y6;YTOPxg4;dD?q*^rIc8QUY zt+~e&rZA7*-18%$qokBmkT_JEDqW3hpxizZ7Yb-o`SJd`(;<+!FMGEX+e4ld^Atj! zZ}Hj7%q{!#46p;izz*Ent`<f0i-yVyS|Ye#&O1>G3d#$42jJ;%gfD@qPM5XU!Mn3{ zX7gkg#?bqbkP&bAuzS&rkVcB9{<<k4|AUcSRCF+xWm8UJuroqAv3Y)hfmM;W-^Bj- z=q8!M#)I+f%AjbWgQ~1gR-<>zlpC<_nB2Nfnc7jbr?Bs{n)fauHmYE!jciv@k!$#n z)VW)%);zGEww<nP^Al#Nbi5b&wA#>-v!DphNnZ6+U|$~&AGmLBDqeGINx|Wy>rB#& z-PVq!p>XlG&(iq`ijS^LCW~tgERZ>^*ChTD!1}*X*Eb&wq$N>t54J|~37G?Feahn( zSnUrV>qqStpL(`eyrF&jN;V-<3}jLEc+~=vL{kmpMsC3!8TDmCpKY6&I+c-d9h$Bo z!5VtIhyANatvOrpEwMNA^Q0QpKg08f*%S|hsV3~N(~?N}!jOz%q|9<O&b9_k`oj_f z(Jc=4dygpjZA+{+5&Sb$+Xzw)yi;VOZa_$IdfBej`&pGAHkUF(VoGx%B=0&ZW~-B~ zv&^@J`|no-lIBE250naYdzycYFHxVjV?Ucn??IlBP*OhW6hDQOYU))|&~-DmJ|!zQ z67B4XF;D1;K?u#ar>n6pM}2xE<G)lKXBNfqEW*{=8?<-xSFt<za%`19I2g4(@W5g1 zMt=qwQDDrDIDRn|<*II1+**lQ?N0$*2NcbZ-Xb?zg=*_5MNyy30ZB58HQ|TUo@+;U zukRb7csZr}uc(_-H7c<vP<u2RYE>jg&+1hKs7d@dbzv8o-+N5;3{UKC3P>4{ofJ5} z^n@~PG%Uahe$r;I>h@Dmhk)9^K&5>|)|7f&?nE5Vr=@NXnfD3Ub@iM4ZAbKtlBA@F zpYTJ@@s)6n_K2c<^cwfHo-Va?_aTUEaFN&ETjOGF*$bva_YUc$i?WvQUB4gu;d=R_ zyt)?cj?j>h6!OT$^?R%KJU3oedn&(>Ac65ecu@6<IUm&<QyJ6PbeqpPRNmoIM1Jb< z!U?N?z46R!H~M5>Tur(O!c17&xuhGqXeqVaT*|ET!&O>{K42l!nc5~>+`%{~P&w5K zVWh2%EIx_au*>(E&to|;*3i0DYn)j-vby$kg@GaE$tV5O`AfqiS}ymJmSb*_y?W1< zY2*{E{M3NDq3BWfrR+4=Asr=I3t{Cp#&05^P1N=&FNEzKs3ld2)Ss^yN3`uO*V8!d zkMMhWl%5V#jHjLT@SXP?!+Y(1372%?z9ZI2)Y3~`Uu5<-rqXh?3+wePR<oC87Jz<B zc2?F0?_;Du#iFy${!yizJSbLD&dgL!$~k+$$2Y2)o@3Gx!J?t3mvdxoWPebDMauLv znf&BA;{w~rkn=l0xH1wQ%68Aq%|+%!$%WG9BZ|9nSu3h4*n+5q!lb38HGJ5{Y^Mvj zJrC>yf`T+qIjO0Q1y+`TZF8g3$MOy;C$H-;TI0<2+w6B=mizZr(b|d3HI3iiU}wvR z(&jYuCP`H0>aET+NT62+<%jhR3|{Hi0g;x7#KeNt$o+J~6qn6uK8rqj%}vns2b_cZ z-uh!1|7$5TuHq8zKhLyX&#WFfI`XvhviJ5k`4gRL2J6Fiu7M^u+3s|ruCq}ag&zcs zwnC`{l!iUd&%Asz7qo2o&Mz(hY!BD01ILCc>}x;7IWN!56EgJ#MN|ugR+Al(4LI?j z#4BfiBwycl<t*Tn&w5vqgz>Nk3ZV#&P&|1FpDfEA(91UNPZ@t6Fn-Q%^2+?%_!RB9 zPR&FUg|&-^pb65J$UJWfITuu3BNkumT<Om{vax3$rB?(t`RiT4-+%UlS{i&pPcm8T zitBa13XL<26H-O<AiA)&%&#I(f6SmndwX8+a4#%WV;aqNuKe+`*wpe3zv^l|_l`h8 zFssf<FezK!8KVyAk;{6OJ9K$IkTWU&b1T_)!#A&IXryK185*fXny>P3o3w`9B@z56 z*aVP`56>PxVnPFXnehkm54rEX70ZSzG30nUFuZ+=*|KtWuE<edEm!{_>?doO`YFoY z+REw`)eKM}LTd}C6((HNIc&TmDr$g+D`Bm_#=<_}%1PC%bH4ga_UpZdnCm@Y3zVPb z`emtM<c4fePwKNs`jdyOGq<$7+hb#6b5=Q6Sc>x-SXfx@<fS(_U$qMl3yV0=TJ_pn z|6+3*92%;9Q3ssK_d-#boq`DWBvH2)`aUKHg<U^OrNDV@v85hPP8w(yCcHIv6X=YU zIb!p<V50p=Lg|w+VLRRN4KlKEO^)+-`oxOyf~w#wQ;MBba2guNz{U{UX=MFXYY}Yl zfciGt%4_nTFV4KwsX4aGD|X(e(}%R{)WlgHY}TfYZ0u~_IF#Wzyw);4<+Bi2UUNPg zs{pp2HZ?4P4@4Jo(#+IYUq2J;*xx8|j_#GdFIU7i>*3<@XaW2XbQ#*;*qN&IB<JYe zjN7Uc0bkf<jwEKC2@g;wryJU2tE(dn407z`B1{Q!baE4X>))h%je6YQSCg3Xo&n1B zc!zzu{^W5#_IN2e`sEBZcyLV&d@AFi&SkwPIITTR4gj^F1a1u{d3*-Gc}VOC*1=?p zsI40<W>J6#HV!iTtB^eTAla|<IhDOG-H60={4h`{TbsJ(;p=f#^i^~J^xU$RbE*2F zsH5qx?(cGf3KQbZzTYFVlpkhWu+A*f+lOzsH;dgC6x8W97df8zj1H($Ktv{-Mhd6* z(r6ZTv58zdu5NIKvq5wd(?hbf#i>j^kw4i_kssU_r`VHDo%gljYzu7X6BNRZHgJ;D z;DC7!GgoonQXg}Qk&Uy(T!q$g!|<(5)k3qyseY34m!I|{6Rbt1jBD1AZTwF1r*T-; zF6ZI^wFP^dv-3a0%(LuEV9Mz<SYqaxxzTgXTXj**<5LZ_-~n<O8&(^2c6e5xX8fv+ z4h2#YW^i3J>S)k)A|Nn%+Dd{WO!Q`UmY!1Vxp19ncgKne?!}Bbz-&k4m)7*>Q#Gh= zFCYlyk9Iqg#VuEy&U*-0S<CFq>tAENd;ePUHSvd^GwO0Xbj})DQEbtD7c9wQ`>)^~ zOfi&^a!Zo_eP>yce)bob(q_J*U8}f2-X9h2QQJdKMsd0ts+F*R30J5xewa|d8%n)G zh|9UP{qzC~8zmDT7>6H2hk132(VVb!({;{bi+7W_P93JuNTc2R+;_{ociS1tVDE3! z{tr-^WU?CuXH6tf`)B`a^2#@cp@b_h{`tNCvYp&?1z@lK;o;6VtY4TH(=W0k>_1tZ z2?tfi+PuK$fq2&s+A|FjY6uF)kAK6W{!vVXx`280V*SFyLF~VF#^WzM&s&pP<ENGu z@f-`i{O?4OzXlDQpcsf@^>)zNo8NbmJih)~#^QMAq2=FCDA4zD;*`nU^C)+4!+0Ur z>T1C()gnwmoXivF_&Q5AmHXN~&Sv>d3*dsQAoUzK9XFld1&rdy{HH8z1m@-IO?mPB zG>b;MdriUt$7Y3+Fr{a^nc{yyfDYb)Anf$*&3AAAxSr^{z^CFADO<kvw+|#~&Xi0e z?K>K)7?eOwX!Bcawn%JM4`7t?L8=hKRss79xJ#(?BsS`8gIU-&$QukZDgh-1K8QJe z(D)IYe!`*gH4Bly|Gk+2hwM|j|J35nAD_Xw22icbT`I{8|HA{By#vl;EG_8v7~@c@ ziN(XR39s)`YJ?a6IP7al?K>*k>P=HSHdA<jdn8EO$Ncx5&ex3KL|;=9@?HL8(p*dg zHy_*kL!ac|KaRj>Fn$THUQGp^7<bUO08r&j7yV<g$nFM}{2U%(45$>?qW$J^VH2ws zcZ+seZ}ZL3IAtZ;*m~`epkMvR^8v}Ezi&Zafsvr1?~umx%lW-glA{;A6T`#fuUhH% zP#ChF+<yd6@mN7G&IAdsphR+s`|}PLFWOxMx4-^MT9Ei3W9s+8^%omX!WYLGBI-J; zkV;@#DJ+-C5^50iTTQ0<ZjYqDKuhS)*X7^CPj{JsLTQ`*eT_TpHEU<*?iK~%H{r&R zLmKj=M_***RaQ_hs-eg*N5+a$Z-?rk?;&TY;))Y1McF3nvl909gu&DzUqH;Aeb9b; zyxcOlATc#HZ3XZ@RN%^VVqewe_f&OPAh)MN_MUs+suKC>wabKpgE@�wAdqfxpU< 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 From 2f24a5b1e245317ee5b5db13e881348de6306d1a Mon Sep 17 00:00:00 2001 From: scab24 <git.seco@protonmail.com> Date: Sat, 16 May 2026 21:53:53 +0200 Subject: [PATCH 08/10] chore: delete ilold-core source tree Removes every file under crates/ilold-core/ now that the workspace member is gone and no live import references it. --- crates/ilold-core/Cargo.toml | 16 - crates/ilold-core/src/callgraph/builder.rs | 167 ---- crates/ilold-core/src/callgraph/mod.rs | 2 - crates/ilold-core/src/callgraph/types.rs | 28 - crates/ilold-core/src/cfg/builder.rs | 675 ------------- crates/ilold-core/src/cfg/error.rs | 16 - crates/ilold-core/src/cfg/mod.rs | 4 - crates/ilold-core/src/cfg/modifier.rs | 302 ------ crates/ilold-core/src/cfg/types.rs | 121 --- .../ilold-core/src/classify/entry_points.rs | 351 ------- crates/ilold-core/src/classify/mod.rs | 1 - .../src/exploration/add_step_solidity.rs | 80 -- crates/ilold-core/src/exploration/commands.rs | 691 ------------- crates/ilold-core/src/exploration/mod.rs | 4 - crates/ilold-core/src/exploration/timeline.rs | 196 ---- crates/ilold-core/src/lib.rs | 16 - crates/ilold-core/src/model/common.rs | 56 -- crates/ilold-core/src/model/contract.rs | 28 - crates/ilold-core/src/model/expression.rs | 153 --- crates/ilold-core/src/model/function.rs | 44 - crates/ilold-core/src/model/mod.rs | 7 - crates/ilold-core/src/model/modifier.rs | 21 - crates/ilold-core/src/model/project.rs | 264 ----- crates/ilold-core/src/model/statement.rs | 79 -- crates/ilold-core/src/narrative/formatter.rs | 153 --- crates/ilold-core/src/narrative/function.rs | 356 ------- crates/ilold-core/src/narrative/mod.rs | 5 - crates/ilold-core/src/narrative/sequence.rs | 147 --- crates/ilold-core/src/narrative/trace.rs | 936 ------------------ crates/ilold-core/src/narrative/types.rs | 163 --- crates/ilold-core/src/parse/error.rs | 26 - crates/ilold-core/src/parse/mod.rs | 14 - crates/ilold-core/src/parse/solar_frontend.rs | 826 ---------------- crates/ilold-core/src/pathtree/config.rs | 21 - crates/ilold-core/src/pathtree/mod.rs | 3 - crates/ilold-core/src/pathtree/types.rs | 83 -- crates/ilold-core/src/pathtree/walker.rs | 425 -------- crates/ilold-core/src/sequence/analysis.rs | 457 --------- crates/ilold-core/src/sequence/builder.rs | 75 -- crates/ilold-core/src/sequence/mod.rs | 3 - crates/ilold-core/src/sequence/types.rs | 33 - crates/ilold-core/src/slicing/backward.rs | 85 -- crates/ilold-core/src/slicing/extract.rs | 267 ----- crates/ilold-core/src/slicing/flatten.rs | 164 --- crates/ilold-core/src/slicing/forward.rs | 61 -- crates/ilold-core/src/slicing/mod.rs | 104 -- crates/ilold-core/src/slicing/types.rs | 75 -- crates/ilold-core/src/util.rs | 39 - crates/ilold-core/tests/callgraph_test.rs | 86 -- crates/ilold-core/tests/integration_test.rs | 108 -- crates/ilold-core/tests/pathtree_test.rs | 123 --- crates/ilold-core/tests/sequence_test.rs | 119 --- crates/ilold-core/tests/slicing_test.rs | 191 ---- 53 files changed, 8470 deletions(-) delete mode 100644 crates/ilold-core/Cargo.toml delete mode 100644 crates/ilold-core/src/callgraph/builder.rs delete mode 100644 crates/ilold-core/src/callgraph/mod.rs delete mode 100644 crates/ilold-core/src/callgraph/types.rs delete mode 100644 crates/ilold-core/src/cfg/builder.rs delete mode 100644 crates/ilold-core/src/cfg/error.rs delete mode 100644 crates/ilold-core/src/cfg/mod.rs delete mode 100644 crates/ilold-core/src/cfg/modifier.rs delete mode 100644 crates/ilold-core/src/cfg/types.rs delete mode 100644 crates/ilold-core/src/classify/entry_points.rs delete mode 100644 crates/ilold-core/src/classify/mod.rs delete mode 100644 crates/ilold-core/src/exploration/add_step_solidity.rs delete mode 100644 crates/ilold-core/src/exploration/commands.rs delete mode 100644 crates/ilold-core/src/exploration/mod.rs delete mode 100644 crates/ilold-core/src/exploration/timeline.rs delete mode 100644 crates/ilold-core/src/lib.rs delete mode 100644 crates/ilold-core/src/model/common.rs delete mode 100644 crates/ilold-core/src/model/contract.rs delete mode 100644 crates/ilold-core/src/model/expression.rs delete mode 100644 crates/ilold-core/src/model/function.rs delete mode 100644 crates/ilold-core/src/model/mod.rs delete mode 100644 crates/ilold-core/src/model/modifier.rs delete mode 100644 crates/ilold-core/src/model/project.rs delete mode 100644 crates/ilold-core/src/model/statement.rs delete mode 100644 crates/ilold-core/src/narrative/formatter.rs delete mode 100644 crates/ilold-core/src/narrative/function.rs delete mode 100644 crates/ilold-core/src/narrative/mod.rs delete mode 100644 crates/ilold-core/src/narrative/sequence.rs delete mode 100644 crates/ilold-core/src/narrative/trace.rs delete mode 100644 crates/ilold-core/src/narrative/types.rs delete mode 100644 crates/ilold-core/src/parse/error.rs delete mode 100644 crates/ilold-core/src/parse/mod.rs delete mode 100644 crates/ilold-core/src/parse/solar_frontend.rs delete mode 100644 crates/ilold-core/src/pathtree/config.rs delete mode 100644 crates/ilold-core/src/pathtree/mod.rs delete mode 100644 crates/ilold-core/src/pathtree/types.rs delete mode 100644 crates/ilold-core/src/pathtree/walker.rs delete mode 100644 crates/ilold-core/src/sequence/analysis.rs delete mode 100644 crates/ilold-core/src/sequence/builder.rs delete mode 100644 crates/ilold-core/src/sequence/mod.rs delete mode 100644 crates/ilold-core/src/sequence/types.rs delete mode 100644 crates/ilold-core/src/slicing/backward.rs delete mode 100644 crates/ilold-core/src/slicing/extract.rs delete mode 100644 crates/ilold-core/src/slicing/flatten.rs delete mode 100644 crates/ilold-core/src/slicing/forward.rs delete mode 100644 crates/ilold-core/src/slicing/mod.rs delete mode 100644 crates/ilold-core/src/slicing/types.rs delete mode 100644 crates/ilold-core/src/util.rs delete mode 100644 crates/ilold-core/tests/callgraph_test.rs delete mode 100644 crates/ilold-core/tests/integration_test.rs delete mode 100644 crates/ilold-core/tests/pathtree_test.rs delete mode 100644 crates/ilold-core/tests/sequence_test.rs delete mode 100644 crates/ilold-core/tests/slicing_test.rs diff --git a/crates/ilold-core/Cargo.toml b/crates/ilold-core/Cargo.toml deleted file mode 100644 index 8a0df36..0000000 --- a/crates/ilold-core/Cargo.toml +++ /dev/null @@ -1,16 +0,0 @@ -[package] -name = "ilold-core" -version.workspace = true -edition.workspace = true -authors.workspace = true -license.workspace = true -repository.workspace = true -description = "Core library for Ilold: smart contract execution path analysis engine" - -[dependencies] -ilold-session-core = { path = "../ilold-session-core" } -solar = { workspace = true } -petgraph = { workspace = true } -serde = { workspace = true } -serde_json = { workspace = true } -thiserror = { workspace = true } diff --git a/crates/ilold-core/src/callgraph/builder.rs b/crates/ilold-core/src/callgraph/builder.rs deleted file mode 100644 index 5f3ee38..0000000 --- a/crates/ilold-core/src/callgraph/builder.rs +++ /dev/null @@ -1,167 +0,0 @@ -use std::collections::HashMap; - -use petgraph::stable_graph::NodeIndex; - -use crate::cfg::builder::CfgBuilder; -use crate::cfg::types::CfgStatement; -use crate::model::contract::ContractDef; -use crate::model::function::{Mutability, Visibility}; -use crate::model::project::Project; -use crate::util::is_type_cast; - -use super::types::*; - -pub fn build_call_graph(project: &Project, contract: &ContractDef) -> CallGraph { - let mut graph = CallGraph::new(); - let mut node_index: HashMap<String, NodeIndex> = HashMap::new(); - - // Add a node for each function in the contract - for func in &contract.functions { - let key = format!("{}::{}", contract.name, func.name); - let idx = graph.add_node(CallNode { - contract: contract.name.clone(), - function: func.name.clone(), - visibility: func.visibility, - mutability: func.mutability, - is_external: false, - }); - node_index.insert(key, idx); - } - - // Build CFG for each function and extract call edges - for func in &contract.functions { - let caller_key = format!("{}::{}", contract.name, func.name); - let caller_idx = match node_index.get(&caller_key) { - Some(idx) => *idx, - None => continue, - }; - - let cfg = match CfgBuilder::build(func, contract) { - Ok(cfg) => cfg, - Err(_) => continue, - }; - - // Scan all blocks for call statements - for block in cfg.node_weights() { - for stmt in &block.statements { - match stmt { - CfgStatement::InternalCall { function, .. } => { - if !is_type_cast(function) { - let callee_idx = resolve_internal( - function, - contract, - project, - &mut graph, - &mut node_index, - ); - add_or_increment_edge(&mut graph, caller_idx, callee_idx); - } - } - CfgStatement::ExternalCall { target, function, .. } => { - if !is_type_cast(function) && !is_type_cast(target) { - let callee_idx = resolve_external( - target, - function, - &mut graph, - &mut node_index, - ); - add_or_increment_edge(&mut graph, caller_idx, callee_idx); - } - } - _ => {} - } - } - } - } - - graph -} - -fn resolve_internal( - function_name: &str, - contract: &ContractDef, - project: &Project, - graph: &mut CallGraph, - index: &mut HashMap<String, NodeIndex>, -) -> NodeIndex { - // 1. Check current contract - let key = format!("{}::{}", contract.name, function_name); - if let Some(&idx) = index.get(&key) { - return idx; - } - - // 2. Check parent contracts (inheritance) - for parent_name in &contract.inherits { - if let Some(&parent_idx) = project.contract_index.get(parent_name) { - let parent = &project.contracts[parent_idx]; - if let Some(func) = parent.functions.iter().find(|f| f.name == function_name) { - let key = format!("{parent_name}::{function_name}"); - if let Some(&idx) = index.get(&key) { - return idx; - } - // Add inherited function as a node - let idx = graph.add_node(CallNode { - contract: parent_name.clone(), - function: function_name.to_string(), - visibility: func.visibility, - mutability: func.mutability, - is_external: false, - }); - index.insert(key, idx); - return idx; - } - } - } - - // 3. Unresolved — create external node - let key = format!("?::{function_name}"); - if let Some(&idx) = index.get(&key) { - return idx; - } - let idx = graph.add_node(CallNode { - contract: "?".to_string(), - function: function_name.to_string(), - visibility: Visibility::Public, - mutability: Mutability::NonPayable, - is_external: true, - }); - index.insert(key, idx); - idx -} - -fn resolve_external( - target: &str, - function_name: &str, - graph: &mut CallGraph, - index: &mut HashMap<String, NodeIndex>, -) -> NodeIndex { - let key = format!("{target}::{function_name}"); - if let Some(&idx) = index.get(&key) { - return idx; - } - let idx = graph.add_node(CallNode { - contract: target.to_string(), - function: function_name.to_string(), - visibility: Visibility::External, - mutability: Mutability::NonPayable, - is_external: true, - }); - index.insert(key, idx); - idx -} - -fn add_or_increment_edge(graph: &mut CallGraph, from: NodeIndex, to: NodeIndex) { - // Check if edge already exists - if let Some(edge) = graph.find_edge(from, to) { - graph[edge].call_count += 1; - } else { - let kind = if graph[to].is_external { - CallKind::External - } else if graph[from].contract != graph[to].contract { - CallKind::Inherited - } else { - CallKind::Internal - }; - graph.add_edge(from, to, CallEdge { kind, call_count: 1 }); - } -} diff --git a/crates/ilold-core/src/callgraph/mod.rs b/crates/ilold-core/src/callgraph/mod.rs deleted file mode 100644 index ab228e5..0000000 --- a/crates/ilold-core/src/callgraph/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod builder; -pub mod types; diff --git a/crates/ilold-core/src/callgraph/types.rs b/crates/ilold-core/src/callgraph/types.rs deleted file mode 100644 index 33f4546..0000000 --- a/crates/ilold-core/src/callgraph/types.rs +++ /dev/null @@ -1,28 +0,0 @@ -use petgraph::stable_graph::StableDiGraph; -use serde::{Deserialize, Serialize}; - -use crate::model::function::{Mutability, Visibility}; - -pub type CallGraph = StableDiGraph<CallNode, CallEdge>; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct CallNode { - pub contract: String, - pub function: String, - pub visibility: Visibility, - pub mutability: Mutability, - pub is_external: bool, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct CallEdge { - pub kind: CallKind, - pub call_count: usize, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -pub enum CallKind { - Internal, - External, - Inherited, -} diff --git a/crates/ilold-core/src/cfg/builder.rs b/crates/ilold-core/src/cfg/builder.rs deleted file mode 100644 index 2c64005..0000000 --- a/crates/ilold-core/src/cfg/builder.rs +++ /dev/null @@ -1,675 +0,0 @@ -use petgraph::stable_graph::NodeIndex; - -use crate::model::contract::ContractDef; -use crate::model::expression::{Expression, ExpressionKind}; -use crate::model::function::FunctionDef; -use crate::model::project::Project; -use crate::model::statement::{Statement, StatementKind}; - -use super::error::CfgError; -use super::modifier::inline_modifiers; -use super::types::*; - -pub struct CfgBuilder { - graph: CfgGraph, - next_block_id: usize, - current_block: NodeIndex, - /// Modifier name attached to statements being processed right now, or - /// `None` when processing the function's own body. - /// - /// Limitation: only top-level statements of a modifier are tagged. If a - /// modifier contains a compound statement (if/for/while) with the - /// placeholder inside, statements nested under that compound node lose - /// their modifier provenance because `process_statement` does not save/ - /// restore this field across recursive descent. This is acceptable for - /// common modifiers (onlyOwner, nonReentrant, lock, whenNotPaused). - current_modifier: Option<String>, -} - -impl CfgBuilder { - /// Build a CFG without cross-contract context. Modifier resolution is - /// limited to the current contract — inherited modifiers will be skipped. - pub fn build(function: &FunctionDef, contract: &ContractDef) -> Result<CfgGraph, CfgError> { - Self::build_with_project(function, contract, None) - } - - /// Build a CFG with an optional `Project` reference, enabling modifier - /// resolution through the inheritance chain. - pub fn build_with_project( - function: &FunctionDef, - contract: &ContractDef, - project: Option<&Project>, - ) -> Result<CfgGraph, CfgError> { - let mut builder = CfgBuilder { - graph: CfgGraph::new(), - next_block_id: 0, - current_block: NodeIndex::new(0), // will be replaced - current_modifier: None, - }; - - let entry = builder.add_block(BlockKind::Entry); - builder.current_block = entry; - - let body = match &function.body { - Some(body) => body.clone(), - None => return Ok(builder.graph), // interface/abstract — no body - }; - - // Inline modifiers if any - let tagged_body: Vec<super::modifier::TaggedStatement> = if function.modifiers.is_empty() { - body - .into_iter() - .map(|s| super::modifier::TaggedStatement { stmt: s, provenance: None }) - .collect() - } else { - let modifier_defs: Vec<&_> = function - .modifiers - .iter() - .filter_map(|mref| { - if let Some(proj) = project { - proj.resolve_modifier(contract, &mref.name) - } else { - contract.modifiers.iter().find(|m| m.name == mref.name) - } - }) - .collect(); - if modifier_defs.len() == function.modifiers.len() { - inline_modifiers(&body, &modifier_defs)? - } else { - body - .into_iter() - .map(|s| super::modifier::TaggedStatement { stmt: s, provenance: None }) - .collect() - } - }; - - for tagged in &tagged_body { - builder.current_modifier = tagged.provenance.clone(); - builder.process_statement(&tagged.stmt); - } - builder.current_modifier = None; - - // If current block has no outgoing edges and is NOT already terminal, add implicit return - if !builder.block_is_terminal(builder.current_block) - && builder - .graph - .edges(builder.current_block) - .next() - .is_none() - { - let ret = builder.add_block(BlockKind::Return); - builder.add_edge(builder.current_block, ret, BranchEdge::Unconditional); - } - - Ok(builder.graph) - } - - fn add_block(&mut self, kind: BlockKind) -> NodeIndex { - let id = self.next_block_id; - self.next_block_id += 1; - self.graph.add_node(BasicBlock { - id, - kind, - statements: Vec::new(), - span: None, - }) - } - - fn add_edge(&mut self, from: NodeIndex, to: NodeIndex, edge: BranchEdge) { - self.graph.add_edge(from, to, edge); - } - - fn add_stmt_to_current(&mut self, stmt: CfgStatement) { - if let Some(block) = self.graph.node_weight_mut(self.current_block) { - block.statements.push(stmt); - } - } - - fn process_statement(&mut self, stmt: &Statement) { - match &stmt.kind { - StatementKind::If { condition, then_body, else_body } => { - self.process_if(condition, then_body, else_body.as_deref()); - } - StatementKind::For { init, condition, increment, body } => { - self.process_for(init.as_deref(), condition.as_ref(), increment.as_ref(), body); - } - StatementKind::While { condition, body } => { - self.process_while(condition, body); - } - StatementKind::DoWhile { body, condition } => { - self.process_do_while(body, condition); - } - StatementKind::Return { value } => { - self.process_return(value.as_ref()); - } - StatementKind::Revert { .. } => { - let revert = self.add_block(BlockKind::Revert); - self.add_edge(self.current_block, revert, BranchEdge::Unconditional); - } - StatementKind::Emit { event_name, .. } => { - let from_modifier = self.current_modifier.clone(); - self.add_stmt_to_current(CfgStatement::EmitEvent { - event: event_name.clone(), - span: None, - from_modifier, - }); - } - StatementKind::Block { statements } => { - for s in statements { - self.process_statement(s); - } - } - StatementKind::UncheckedBlock { statements } => { - for s in statements { - self.process_statement(s); - } - } - StatementKind::ExpressionStmt { expression } => { - self.process_expression_stmt(expression); - } - StatementKind::VariableDeclaration { name, initial_value, .. } => { - if let Some(val) = initial_value { - let from_modifier = self.current_modifier.clone(); - self.add_stmt_to_current(CfgStatement::Assignment { - target: name.clone(), - value: expr_to_string(val), - operator: crate::model::expression::AssignOperator::Assign, - span: None, - from_modifier, - }); - } - } - StatementKind::TryCatch { expression, clauses } => { - self.process_try_catch(expression, clauses); - } - StatementKind::Assembly { .. } => { - let from_modifier = self.current_modifier.clone(); - self.add_stmt_to_current(CfgStatement::AssemblyBlock { span: None, from_modifier }); - } - StatementKind::Break | StatementKind::Continue | StatementKind::Placeholder => { - // Break/Continue handled by loop processors - // Placeholder should have been replaced by modifier inlining - } - } - } - - fn process_if( - &mut self, - condition: &Expression, - then_body: &[Statement], - else_body: Option<&[Statement]>, - ) { - let cond_str = expr_to_string(condition); - let before = self.current_block; - - // Then branch - let then_block = self.add_block(BlockKind::Normal); - self.add_edge( - before, - then_block, - BranchEdge::ConditionalTrue { condition: cond_str.clone() }, - ); - self.current_block = then_block; - for s in then_body { - self.process_statement(s); - } - let then_end = self.current_block; - - // Else branch - let else_end = if let Some(else_stmts) = else_body { - let else_block = self.add_block(BlockKind::Normal); - self.add_edge( - before, - else_block, - BranchEdge::ConditionalFalse { condition: cond_str }, - ); - self.current_block = else_block; - for s in else_stmts { - self.process_statement(s); - } - self.current_block - } else { - // No else: false branch goes directly to join - let join = self.add_block(BlockKind::Normal); - self.add_edge( - before, - join, - BranchEdge::ConditionalFalse { condition: cond_str }, - ); - join - }; - - // Join block: connect non-terminal branches - let then_needs_join = !self.block_is_terminal(then_end); - let else_needs_join = else_body.is_some() && !self.block_is_terminal(else_end); - - if then_needs_join || else_needs_join || else_body.is_none() { - let join = if else_body.is_none() { - else_end // already created as the join - } else { - self.add_block(BlockKind::Normal) - }; - - if then_needs_join { - self.add_edge(then_end, join, BranchEdge::Unconditional); - } - if else_needs_join { - self.add_edge(else_end, join, BranchEdge::Unconditional); - } - self.current_block = join; - } - } - - fn process_for( - &mut self, - init: Option<&Statement>, - condition: Option<&Expression>, - increment: Option<&Expression>, - body: &[Statement], - ) { - // Init - if let Some(init_stmt) = init { - self.process_statement(init_stmt); - } - - // Condition check - let cond_block = self.add_block(BlockKind::LoopCondition); - self.add_edge(self.current_block, cond_block, BranchEdge::Unconditional); - - let exit_block = self.add_block(BlockKind::Normal); - let body_block = self.add_block(BlockKind::Normal); - - if let Some(cond) = condition { - let cond_str = expr_to_string(cond); - self.add_edge( - cond_block, - body_block, - BranchEdge::ConditionalTrue { condition: cond_str.clone() }, - ); - self.add_edge( - cond_block, - exit_block, - BranchEdge::ConditionalFalse { condition: cond_str }, - ); - } else { - // for(;;) — always enters body - self.add_edge(cond_block, body_block, BranchEdge::Unconditional); - } - - // Body - self.current_block = body_block; - for s in body { - self.process_statement(s); - } - - // Increment + loop back - if let Some(_incr) = increment { - // increment is an expression, not a separate block for simplicity - } - if !self.block_is_terminal(self.current_block) { - self.add_edge(self.current_block, cond_block, BranchEdge::LoopBack); - } - - self.current_block = exit_block; - } - - fn process_while(&mut self, condition: &Expression, body: &[Statement]) { - let cond_str = expr_to_string(condition); - - let cond_block = self.add_block(BlockKind::LoopCondition); - self.add_edge(self.current_block, cond_block, BranchEdge::Unconditional); - - let body_block = self.add_block(BlockKind::Normal); - let exit_block = self.add_block(BlockKind::Normal); - - self.add_edge( - cond_block, - body_block, - BranchEdge::ConditionalTrue { condition: cond_str.clone() }, - ); - self.add_edge( - cond_block, - exit_block, - BranchEdge::ConditionalFalse { condition: cond_str }, - ); - - self.current_block = body_block; - for s in body { - self.process_statement(s); - } - if !self.block_is_terminal(self.current_block) { - self.add_edge(self.current_block, cond_block, BranchEdge::LoopBack); - } - - self.current_block = exit_block; - } - - fn process_do_while(&mut self, body: &[Statement], condition: &Expression) { - let cond_str = expr_to_string(condition); - - let body_block = self.add_block(BlockKind::Normal); - self.add_edge(self.current_block, body_block, BranchEdge::Unconditional); - - self.current_block = body_block; - for s in body { - self.process_statement(s); - } - - let cond_block = self.add_block(BlockKind::LoopCondition); - if !self.block_is_terminal(self.current_block) { - self.add_edge(self.current_block, cond_block, BranchEdge::Unconditional); - } - - let exit_block = self.add_block(BlockKind::Normal); - self.add_edge( - cond_block, - body_block, - BranchEdge::ConditionalTrue { condition: cond_str.clone() }, - ); - self.add_edge( - cond_block, - exit_block, - BranchEdge::ConditionalFalse { condition: cond_str }, - ); - - self.current_block = exit_block; - } - - fn process_return(&mut self, value: Option<&Expression>) { - if let Some(expr) = value { - for s in classify_expression(expr, &self.current_modifier) { - self.add_stmt_to_current(s); - } - } - let ret = self.add_block(BlockKind::Return); - self.add_edge(self.current_block, ret, BranchEdge::Unconditional); - self.current_block = ret; - } - - fn process_try_catch( - &mut self, - expression: &Expression, - clauses: &[crate::model::statement::CatchClause], - ) { - let before = self.current_block; - for s in classify_expression(expression, &self.current_modifier) { - self.add_stmt_to_current(s); - } - - let join = self.add_block(BlockKind::Normal); - - for (i, clause) in clauses.iter().enumerate() { - let clause_block = self.add_block(BlockKind::Normal); - let edge = if i == 0 { - // First clause is the success case (returns) - BranchEdge::ExternalCallSuccess - } else { - BranchEdge::CatchClause { - kind: clause.name.clone().unwrap_or("default".into()), - } - }; - self.add_edge(before, clause_block, edge); - - self.current_block = clause_block; - for s in &clause.body { - self.process_statement(s); - } - if !self.block_is_terminal(self.current_block) { - self.add_edge(self.current_block, join, BranchEdge::Unconditional); - } - } - - self.current_block = join; - } - - fn process_expression_stmt(&mut self, expr: &Expression) { - // Check if this is a require/assert call - if let ExpressionKind::FunctionCall { callee, arguments } = &expr.kind { - if let ExpressionKind::Identifier { name } = &callee.kind { - match name.as_str() { - "require" => { - self.process_require(arguments); - return; - } - "assert" => { - self.process_assert(arguments); - return; - } - _ => {} - } - } - } - - // Not require/assert — classify and add to current block - for s in classify_expression(expr, &self.current_modifier) { - self.add_stmt_to_current(s); - } - } - - fn process_require(&mut self, arguments: &[Expression]) { - let condition = arguments.first().map(expr_to_string).unwrap_or_default(); - let message = arguments.get(1).map(expr_to_string); - - let cond_str = condition.clone(); - let from_modifier = self.current_modifier.clone(); - self.add_stmt_to_current(CfgStatement::RequireCheck { - condition, - message, - span: None, - from_modifier, - }); - - let before = self.current_block; - - // True branch: continues - let next = self.add_block(BlockKind::Normal); - self.add_edge( - before, - next, - BranchEdge::ConditionalTrue { condition: cond_str.clone() }, - ); - - // False branch: reverts - let revert = self.add_block(BlockKind::Revert); - self.add_edge( - before, - revert, - BranchEdge::ConditionalFalse { condition: cond_str }, - ); - - self.current_block = next; - } - - fn process_assert(&mut self, arguments: &[Expression]) { - let condition = arguments.first().map(expr_to_string).unwrap_or_default(); - let cond_str = condition.clone(); - let from_modifier = self.current_modifier.clone(); - - self.add_stmt_to_current(CfgStatement::AssertCheck { - condition, - span: None, - from_modifier, - }); - - let before = self.current_block; - - let next = self.add_block(BlockKind::Normal); - self.add_edge( - before, - next, - BranchEdge::ConditionalTrue { condition: cond_str.clone() }, - ); - - let revert = self.add_block(BlockKind::Revert); - self.add_edge( - before, - revert, - BranchEdge::ConditionalFalse { condition: cond_str }, - ); - - self.current_block = next; - } - - fn block_is_terminal(&self, idx: NodeIndex) -> bool { - let kind = self.graph[idx].kind; - kind == BlockKind::Return || kind == BlockKind::Revert - } -} - -// ============================================================================ -// Expression classification -// ============================================================================ - -/// Classify an expression into CfgStatements for the detection engine. -/// Returns multiple statements when an expression contains embedded calls -/// (e.g., `x = foo()` produces both an Assignment and an InternalCall). -fn classify_expression(expr: &Expression, from_modifier: &Option<String>) -> Vec<CfgStatement> { - let mut stmts = Vec::new(); - collect_calls(expr, &mut stmts, from_modifier); - - if let ExpressionKind::Assignment { target, operator, value } = &expr.kind { - stmts.push(CfgStatement::Assignment { - target: expr_to_string(target), - value: expr_to_string(value), - operator: *operator, - span: None, - from_modifier: from_modifier.clone(), - }); - } - - stmts -} - -/// Recursively scan an expression for function calls and add them as CfgStatements. -fn collect_calls(expr: &Expression, stmts: &mut Vec<CfgStatement>, from_modifier: &Option<String>) { - match &expr.kind { - ExpressionKind::FunctionCall { callee, arguments } => { - // Classify this call - match &callee.kind { - // `uint32(x)` etc. — type conversion, not a call. - ExpressionKind::TypeMeta { .. } => {} - ExpressionKind::Identifier { name } => { - if !crate::util::is_type_cast(name) { - stmts.push(CfgStatement::InternalCall { - function: name.clone(), - span: None, - from_modifier: from_modifier.clone(), - }); - } - } - ExpressionKind::MemberAccess { object, member } => { - if let ExpressionKind::Identifier { name } = &object.kind { - if name == "this" || name == "super" { - stmts.push(CfgStatement::InternalCall { - function: member.clone(), - span: None, - from_modifier: from_modifier.clone(), - }); - } else { - stmts.push(CfgStatement::ExternalCall { - target: name.clone(), - function: member.clone(), - span: None, - from_modifier: from_modifier.clone(), - }); - } - } else { - stmts.push(CfgStatement::ExternalCall { - target: expr_to_string(object), - function: member.clone(), - span: None, - from_modifier: from_modifier.clone(), - }); - } - } - _ => { - stmts.push(CfgStatement::InternalCall { - function: expr_to_string(callee), - span: None, - from_modifier: from_modifier.clone(), - }); - } - } - // Also recurse into arguments (they might contain calls too) - for arg in arguments { - collect_calls(arg, stmts, from_modifier); - } - } - // Recurse into sub-expressions - ExpressionKind::Assignment { target, value, .. } => { - collect_calls(target, stmts, from_modifier); - collect_calls(value, stmts, from_modifier); - } - ExpressionKind::BinaryOp { left, right, .. } => { - collect_calls(left, stmts, from_modifier); - collect_calls(right, stmts, from_modifier); - } - ExpressionKind::UnaryOp { operand, .. } => { - collect_calls(operand, stmts, from_modifier); - } - ExpressionKind::MemberAccess { object, .. } => { - collect_calls(object, stmts, from_modifier); - } - ExpressionKind::IndexAccess { base, index } => { - collect_calls(base, stmts, from_modifier); - if let Some(idx) = index { - collect_calls(idx, stmts, from_modifier); - } - } - ExpressionKind::Ternary { condition, true_expr, false_expr } => { - collect_calls(condition, stmts, from_modifier); - collect_calls(true_expr, stmts, from_modifier); - collect_calls(false_expr, stmts, from_modifier); - } - _ => {} - } -} - -fn expr_to_string(expr: &Expression) -> String { - match &expr.kind { - ExpressionKind::Identifier { name } => name.clone(), - ExpressionKind::Literal { value, .. } => value.clone(), - ExpressionKind::MemberAccess { object, member } => { - format!("{}.{}", expr_to_string(object), member) - } - ExpressionKind::FunctionCall { callee, arguments } => { - // Render type conversion `uint32(x)` as `uint32(x)` instead of - // `type(uint32)(x)` when the callee is a TypeMeta expression. - if let ExpressionKind::TypeMeta { type_name } = &callee.kind { - let args: Vec<String> = arguments.iter().map(expr_to_string).collect(); - return format!("{type_name}({})", args.join(", ")); - } - let args: Vec<String> = arguments.iter().map(expr_to_string).collect(); - format!("{}({})", expr_to_string(callee), args.join(", ")) - } - ExpressionKind::BinaryOp { left, operator, right } => { - format!("{} {} {}", expr_to_string(left), operator.as_str(), expr_to_string(right)) - } - ExpressionKind::UnaryOp { operator, operand } => { - let (sym, postfix) = operator.format_parts(); - let s = expr_to_string(operand); - if postfix { format!("{}{}", s, sym) } else { format!("{}{}", sym, s) } - } - ExpressionKind::IndexAccess { base, index } => { - let idx = index.as_ref().map(|e| expr_to_string(e)).unwrap_or_default(); - format!("{}[{}]", expr_to_string(base), idx) - } - ExpressionKind::Assignment { target, operator, value } => { - format!("{} {} {}", expr_to_string(target), operator.as_str(), expr_to_string(value)) - } - ExpressionKind::Ternary { condition, true_expr, false_expr } => { - format!("{} ? {} : {}", expr_to_string(condition), expr_to_string(true_expr), expr_to_string(false_expr)) - } - ExpressionKind::TypeCast { type_name, expression } => { - format!("{type_name}({})", expr_to_string(expression)) - } - ExpressionKind::TypeMeta { type_name } => { - format!("type({type_name})") - } - ExpressionKind::New { type_name, arguments } => { - let args: Vec<String> = arguments.iter().map(expr_to_string).collect(); - format!("new {type_name}({})", args.join(", ")) - } - } -} - diff --git a/crates/ilold-core/src/cfg/error.rs b/crates/ilold-core/src/cfg/error.rs deleted file mode 100644 index 218d689..0000000 --- a/crates/ilold-core/src/cfg/error.rs +++ /dev/null @@ -1,16 +0,0 @@ -use crate::model::common::SourceSpan; - -#[derive(Debug, thiserror::Error)] -pub enum CfgError { - #[error("Modifier '{name}' not found in contract '{contract}'")] - ModifierNotFound { name: String, contract: String }, - - #[error("Modifier '{name}' has no _ placeholder")] - ModifierMissingPlaceholder { name: String }, - - #[error("Unsupported statement at {span:?}: {kind}")] - UnsupportedStatement { - kind: String, - span: Option<SourceSpan>, - }, -} diff --git a/crates/ilold-core/src/cfg/mod.rs b/crates/ilold-core/src/cfg/mod.rs deleted file mode 100644 index 025d9fd..0000000 --- a/crates/ilold-core/src/cfg/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub mod builder; -pub mod error; -pub mod modifier; -pub mod types; diff --git a/crates/ilold-core/src/cfg/modifier.rs b/crates/ilold-core/src/cfg/modifier.rs deleted file mode 100644 index d44307a..0000000 --- a/crates/ilold-core/src/cfg/modifier.rs +++ /dev/null @@ -1,302 +0,0 @@ -use crate::model::modifier::ModifierDef; -use crate::model::statement::{Statement, StatementKind}; - -use super::error::CfgError; - -/// A statement tagged with the name of the modifier it came from, or `None` -/// when the statement is part of the function's own body. -#[derive(Debug, Clone)] -pub struct TaggedStatement { - pub stmt: Statement, - pub provenance: Option<String>, -} - -/// Inline a chain of modifiers around a function body. -/// -/// Modifiers wrap last-to-first: `[onlyOwner, nonReentrant]` means the result -/// of inlining `nonReentrant` around the body is then inlined into `onlyOwner`. -pub fn inline_modifiers( - body: &[Statement], - modifier_defs: &[&ModifierDef], -) -> Result<Vec<TaggedStatement>, CfgError> { - let mut current: Vec<TaggedStatement> = body - .iter() - .cloned() - .map(|s| TaggedStatement { stmt: s, provenance: None }) - .collect(); - - for modifier in modifier_defs.iter().rev() { - if !has_placeholder(&modifier.body) { - return Err(CfgError::ModifierMissingPlaceholder { - name: modifier.name.clone(), - }); - } - current = replace_placeholder_tagged(&modifier.body, ¤t, &modifier.name); - } - - Ok(current) -} - -fn has_placeholder(stmts: &[Statement]) -> bool { - stmts.iter().any(|s| match &s.kind { - StatementKind::Placeholder => true, - StatementKind::Block { statements } => has_placeholder(statements), - StatementKind::UncheckedBlock { statements } => has_placeholder(statements), - StatementKind::If { then_body, else_body, .. } => { - has_placeholder(then_body) - || else_body.as_ref().is_some_and(|e| has_placeholder(e)) - } - StatementKind::For { body, .. } => has_placeholder(body), - StatementKind::While { body, .. } => has_placeholder(body), - StatementKind::DoWhile { body, .. } => has_placeholder(body), - StatementKind::TryCatch { clauses, .. } => { - clauses.iter().any(|c| has_placeholder(&c.body)) - } - _ => false, - }) -} - -/// Replace each `_` placeholder in `modifier_body` with `function_body`, -/// tagging modifier statements with `modifier_name`. Inner tags in the -/// function body (from prior nested inlining) are preserved. -fn replace_placeholder_tagged( - modifier_body: &[Statement], - function_body: &[TaggedStatement], - modifier_name: &str, -) -> Vec<TaggedStatement> { - let mut result: Vec<TaggedStatement> = Vec::new(); - - for stmt in modifier_body { - match &stmt.kind { - StatementKind::Placeholder => { - result.extend(function_body.iter().cloned()); - } - StatementKind::Block { statements } => { - let inner = replace_placeholder_tagged_block(statements, function_body, modifier_name); - result.push(TaggedStatement { - stmt: Statement { - kind: StatementKind::Block { statements: inner }, - span: stmt.span, - }, - provenance: Some(modifier_name.to_string()), - }); - } - StatementKind::UncheckedBlock { statements } => { - let inner = replace_placeholder_tagged_block(statements, function_body, modifier_name); - result.push(TaggedStatement { - stmt: Statement { - kind: StatementKind::UncheckedBlock { statements: inner }, - span: stmt.span, - }, - provenance: Some(modifier_name.to_string()), - }); - } - StatementKind::If { condition, then_body, else_body } => { - let then_body = replace_placeholder_tagged_block(then_body, function_body, modifier_name); - let else_body = else_body - .as_ref() - .map(|e| replace_placeholder_tagged_block(e, function_body, modifier_name)); - result.push(TaggedStatement { - stmt: Statement { - kind: StatementKind::If { - condition: condition.clone(), - then_body, - else_body, - }, - span: stmt.span, - }, - provenance: Some(modifier_name.to_string()), - }); - } - StatementKind::For { init, condition, increment, body } => { - let body = replace_placeholder_tagged_block(body, function_body, modifier_name); - result.push(TaggedStatement { - stmt: Statement { - kind: StatementKind::For { - init: init.clone(), - condition: condition.clone(), - increment: increment.clone(), - body, - }, - span: stmt.span, - }, - provenance: Some(modifier_name.to_string()), - }); - } - StatementKind::While { condition, body } => { - let body = replace_placeholder_tagged_block(body, function_body, modifier_name); - result.push(TaggedStatement { - stmt: Statement { - kind: StatementKind::While { - condition: condition.clone(), - body, - }, - span: stmt.span, - }, - provenance: Some(modifier_name.to_string()), - }); - } - StatementKind::DoWhile { body, condition } => { - let body = replace_placeholder_tagged_block(body, function_body, modifier_name); - result.push(TaggedStatement { - stmt: Statement { - kind: StatementKind::DoWhile { - body, - condition: condition.clone(), - }, - span: stmt.span, - }, - provenance: Some(modifier_name.to_string()), - }); - } - StatementKind::TryCatch { expression, clauses } => { - let new_clauses = clauses.iter().map(|c| crate::model::statement::CatchClause { - name: c.name.clone(), - params: c.params.clone(), - body: replace_placeholder_tagged_block(&c.body, function_body, modifier_name), - }).collect(); - result.push(TaggedStatement { - stmt: Statement { - kind: StatementKind::TryCatch { - expression: expression.clone(), - clauses: new_clauses, - }, - span: stmt.span, - }, - provenance: Some(modifier_name.to_string()), - }); - } - _ => result.push(TaggedStatement { - stmt: stmt.clone(), - provenance: Some(modifier_name.to_string()), - }), - } - } - - result -} - -/// Placeholder replacement inside compound statements. Drops the tag layer -/// because compound statements store plain `Statement`, not `TaggedStatement`. -fn replace_placeholder_tagged_block( - modifier_body: &[Statement], - function_body: &[TaggedStatement], - modifier_name: &str, -) -> Vec<Statement> { - replace_placeholder_tagged(modifier_body, function_body, modifier_name) - .into_iter() - .map(|t| t.stmt) - .collect() -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::model::common::SourceSpan; - use crate::model::expression::{Expression, ExpressionKind}; - - fn span() -> SourceSpan { - SourceSpan { file_index: 0, start_line: 0, start_col: 0, end_line: 0, end_col: 0 } - } - - fn make_require(name: &str) -> Statement { - Statement { - kind: StatementKind::ExpressionStmt { - expression: Expression { - kind: ExpressionKind::FunctionCall { - callee: Box::new(Expression { - kind: ExpressionKind::Identifier { name: "require".into() }, - span: span(), - }), - arguments: vec![Expression { - kind: ExpressionKind::Identifier { name: name.into() }, - span: span(), - }], - }, - span: span(), - }, - }, - span: span(), - } - } - - fn make_placeholder() -> Statement { - Statement { kind: StatementKind::Placeholder, span: span() } - } - - fn make_return() -> Statement { - Statement { kind: StatementKind::Return { value: None }, span: span() } - } - - #[test] - fn test_single_modifier_inlining() { - // modifier onlyOwner { require(isOwner); _; } - let modifier = ModifierDef { - name: "onlyOwner".into(), - params: vec![], - body: vec![make_require("isOwner"), make_placeholder()], - span: span(), - }; - - // function body: return; - let body = vec![make_return()]; - - let result = inline_modifiers(&body, &[&modifier]).unwrap(); - - // Expected: require(isOwner) tagged with "onlyOwner", return (no tag) - assert_eq!(result.len(), 2); - assert!(matches!(result[0].stmt.kind, StatementKind::ExpressionStmt { .. })); - assert_eq!(result[0].provenance.as_deref(), Some("onlyOwner")); - assert!(matches!(result[1].stmt.kind, StatementKind::Return { .. })); - assert_eq!(result[1].provenance, None); - } - - #[test] - fn test_chained_modifiers() { - // modifier A { require(a); _; } - let mod_a = ModifierDef { - name: "A".into(), - params: vec![], - body: vec![make_require("a"), make_placeholder()], - span: span(), - }; - - // modifier B { require(b); _; } - let mod_b = ModifierDef { - name: "B".into(), - params: vec![], - body: vec![make_require("b"), make_placeholder()], - span: span(), - }; - - let body = vec![make_return()]; - - // function foo() A B { return; } - // Expected: require(a) [tagged A], require(b) [tagged B], return [no tag] - let result = inline_modifiers(&body, &[&mod_a, &mod_b]).unwrap(); - - assert_eq!(result.len(), 3); - assert_eq!(result[0].provenance.as_deref(), Some("A")); - assert_eq!(result[1].provenance.as_deref(), Some("B")); - assert_eq!(result[2].provenance, None); - } - - #[test] - fn test_modifier_missing_placeholder() { - let modifier = ModifierDef { - name: "broken".into(), - params: vec![], - body: vec![make_require("x")], // no placeholder! - span: span(), - }; - - let body = vec![make_return()]; - let result = inline_modifiers(&body, &[&modifier]); - - assert!(result.is_err()); - assert!(matches!( - result.unwrap_err(), - CfgError::ModifierMissingPlaceholder { .. } - )); - } -} diff --git a/crates/ilold-core/src/cfg/types.rs b/crates/ilold-core/src/cfg/types.rs deleted file mode 100644 index 046c145..0000000 --- a/crates/ilold-core/src/cfg/types.rs +++ /dev/null @@ -1,121 +0,0 @@ -use petgraph::stable_graph::StableDiGraph; -use serde::{Deserialize, Serialize}; - -use crate::model::common::SourceSpan; -use crate::model::expression::AssignOperator; - -pub type CfgGraph = StableDiGraph<BasicBlock, BranchEdge>; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct BasicBlock { - pub id: usize, - pub kind: BlockKind, - pub statements: Vec<CfgStatement>, - pub span: Option<SourceSpan>, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -pub enum BlockKind { - Entry, - Normal, - LoopCondition, - Return, - Revert, - Assembly, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub enum BranchEdge { - Unconditional, - ConditionalTrue { condition: String }, - ConditionalFalse { condition: String }, - ExternalCallSuccess, - ExternalCallFailure, - LoopBack, - LoopExit, - CatchClause { kind: String }, -} - -impl BranchEdge { - /// Canonical ordering used to make CFG traversal deterministic. - /// Lower values are visited first; tiebreaker is the target node index. - pub fn variant_order(&self) -> u8 { - match self { - BranchEdge::Unconditional => 0, - BranchEdge::ConditionalTrue { .. } => 1, - BranchEdge::ConditionalFalse { .. } => 2, - BranchEdge::ExternalCallSuccess => 3, - BranchEdge::ExternalCallFailure => 4, - BranchEdge::LoopBack => 5, - BranchEdge::LoopExit => 6, - BranchEdge::CatchClause { .. } => 7, - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub enum CfgStatement { - Assignment { - target: String, - value: String, - operator: AssignOperator, - span: Option<SourceSpan>, - #[serde(default)] - from_modifier: Option<String>, - }, - ExternalCall { - target: String, - function: String, - span: Option<SourceSpan>, - #[serde(default)] - from_modifier: Option<String>, - }, - InternalCall { - function: String, - span: Option<SourceSpan>, - #[serde(default)] - from_modifier: Option<String>, - }, - EmitEvent { - event: String, - span: Option<SourceSpan>, - #[serde(default)] - from_modifier: Option<String>, - }, - StateRead { - variable: String, - span: Option<SourceSpan>, - #[serde(default)] - from_modifier: Option<String>, - }, - StateWrite { - variable: String, - span: Option<SourceSpan>, - #[serde(default)] - from_modifier: Option<String>, - }, - EthTransfer { - to: String, - span: Option<SourceSpan>, - #[serde(default)] - from_modifier: Option<String>, - }, - RequireCheck { - condition: String, - message: Option<String>, - span: Option<SourceSpan>, - #[serde(default)] - from_modifier: Option<String>, - }, - AssertCheck { - condition: String, - span: Option<SourceSpan>, - #[serde(default)] - from_modifier: Option<String>, - }, - AssemblyBlock { - span: Option<SourceSpan>, - #[serde(default)] - from_modifier: Option<String>, - }, -} diff --git a/crates/ilold-core/src/classify/entry_points.rs b/crates/ilold-core/src/classify/entry_points.rs deleted file mode 100644 index 80ce6a9..0000000 --- a/crates/ilold-core/src/classify/entry_points.rs +++ /dev/null @@ -1,351 +0,0 @@ -pub use ilold_session_core::exploration::access::AccessLevel; - -use crate::model::contract::ContractDef; -use crate::model::expression::{BinaryOperator, Expression, ExpressionKind}; -use crate::model::function::{FunctionDef, FunctionKind, Visibility}; -use crate::model::statement::{Statement, StatementKind}; - -// Modifiers that restrict WHO can call (access control) -const ACCESS_MODIFIERS: &[&str] = &[ - "onlyowner", "onlyadmin", "onlyrole", "onlygovernance", - "onlyminter", "onlypauser", "onlyoperator", "onlyguardian", - "onlyauthorized", "onlymanager", "onlycontroller", - "onlybridge", "onlyrelayer", "onlyvault", - "auth", "restricted", -]; - -// Modifiers that restrict WHEN you can call (state guards, not access control) -// These do NOT make a function "Restricted" — anyone can call when the condition is met -#[allow(dead_code)] -const STATE_GUARD_MODIFIERS: &[&str] = &[ - "whennotpaused", "whenpaused", - "nonreentrant", "noreentrant", - "initializer", "reinitializer", -]; - -pub fn classify_function(func: &FunctionDef, _contract: &ContractDef) -> AccessLevel { - // Constructor, fallback, receive are special — not normal entry points - match func.kind { - FunctionKind::Constructor => { - return AccessLevel::Special { kind: "constructor".into() }; - } - FunctionKind::Fallback => { - return AccessLevel::Special { kind: "fallback".into() }; - } - FunctionKind::Receive => { - return AccessLevel::Special { kind: "receive".into() }; - } - FunctionKind::Function => {} - } - - // Internal/Private → not externally callable - if matches!(func.visibility, Visibility::Internal | Visibility::Private) { - return AccessLevel::Internal; - } - - // Check modifiers for access control patterns (not state guards) - for modifier in &func.modifiers { - let lower = modifier.name.to_lowercase(); - for pattern in ACCESS_MODIFIERS { - if lower.contains(pattern) { - return AccessLevel::Restricted { - role: modifier.name.clone(), - }; - } - } - // State guards are noted but don't change classification - } - - // Check for require(msg.sender == owner) or similar comparison in function body - if let Some(body) = &func.body { - if let Some(role) = find_sender_access_check(body) { - return AccessLevel::Restricted { role }; - } - } - - AccessLevel::Public -} - -pub fn classify_all(contract: &ContractDef) -> Vec<(String, AccessLevel)> { - contract - .functions - .iter() - .map(|f| (f.name.clone(), classify_function(f, contract))) - .collect() -} - -/// Walks the AST looking for access control checks: require/assert/if where -/// msg.sender is COMPARED (==, !=) with another value. This is stricter than -/// just "mentions msg.sender" — balances[msg.sender] is NOT an access check. -fn find_sender_access_check(stmts: &[Statement]) -> Option<String> { - for stmt in stmts { - if let Some(role) = check_statement_for_access(stmt) { - return Some(role); - } - } - None -} - -fn check_statement_for_access(stmt: &Statement) -> Option<String> { - match &stmt.kind { - StatementKind::If { condition, then_body, else_body } => { - if expr_is_sender_comparison(condition) { - return Some("msg.sender check".into()); - } - if let Some(role) = find_sender_access_check(then_body) { - return Some(role); - } - if let Some(else_stmts) = else_body { - if let Some(role) = find_sender_access_check(else_stmts) { - return Some(role); - } - } - } - StatementKind::ExpressionStmt { expression } => { - if is_require_with_sender_comparison(expression) { - return Some("msg.sender check".into()); - } - } - StatementKind::Block { statements } | StatementKind::UncheckedBlock { statements } => { - if let Some(role) = find_sender_access_check(statements) { - return Some(role); - } - } - _ => {} - } - None -} - -/// Checks if an expression is require(...) or assert(...) where the condition -/// contains a msg.sender COMPARISON (==, !=), not just a mention. -fn is_require_with_sender_comparison(expr: &Expression) -> bool { - if let ExpressionKind::FunctionCall { callee, arguments } = &expr.kind { - let is_require = matches!(&callee.kind, - ExpressionKind::Identifier { name } if name == "require" || name == "assert" - ); - if is_require { - return arguments - .first() - .map_or(false, |arg| expr_is_sender_comparison(arg)); - } - } - false -} - -/// Returns true if the expression compares msg.sender with something using == or !=. -/// This catches: msg.sender == owner, owner == msg.sender, msg.sender != address(0) -/// But NOT: balances[msg.sender] (that's indexing, not a comparison) -fn expr_is_sender_comparison(expr: &Expression) -> bool { - match &expr.kind { - ExpressionKind::BinaryOp { left, operator, right } => { - match operator { - BinaryOperator::Eq | BinaryOperator::Neq => { - // One side must be msg.sender - expr_is_msg_sender(left) || expr_is_msg_sender(right) - } - BinaryOperator::And | BinaryOperator::Or => { - // Recurse into && and || chains - expr_is_sender_comparison(left) || expr_is_sender_comparison(right) - } - _ => false, - } - } - ExpressionKind::UnaryOp { operand, .. } => expr_is_sender_comparison(operand), - _ => false, - } -} - -/// Returns true only for the exact expression `msg.sender` -fn expr_is_msg_sender(expr: &Expression) -> bool { - if let ExpressionKind::MemberAccess { object, member } = &expr.kind { - if member == "sender" { - if let ExpressionKind::Identifier { name } = &object.kind { - return name == "msg"; - } - } - } - false -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::model::common::SourceSpan; - use crate::model::function::Mutability; - use crate::model::modifier::ModifierRef; - - fn span() -> SourceSpan { - SourceSpan { file_index: 0, start_line: 0, start_col: 0, end_line: 0, end_col: 0 } - } - - fn make_func(name: &str, vis: Visibility, kind: FunctionKind, modifiers: Vec<&str>, body: Option<Vec<Statement>>) -> FunctionDef { - FunctionDef { - name: name.into(), - kind, - visibility: vis, - mutability: Mutability::NonPayable, - modifiers: modifiers - .into_iter() - .map(|m| ModifierRef { name: m.into(), arguments: vec![] }) - .collect(), - params: vec![], - returns: vec![], - body, - is_virtual: false, - is_override: false, - span: span(), - } - } - - fn contract() -> ContractDef { - ContractDef { - name: "Test".into(), - kind: crate::model::contract::ContractKind::Contract, - functions: vec![], modifiers: vec![], state_vars: vec![], - structs: vec![], enums: vec![], events: vec![], errors: vec![], - inherits: vec![], span: span(), - } - } - - fn msg_sender_expr() -> Expression { - Expression { - kind: ExpressionKind::MemberAccess { - object: Box::new(Expression { - kind: ExpressionKind::Identifier { name: "msg".into() }, - span: span(), - }), - member: "sender".into(), - }, - span: span(), - } - } - - fn owner_expr() -> Expression { - Expression { - kind: ExpressionKind::Identifier { name: "owner".into() }, - span: span(), - } - } - - fn require_sender_eq_owner() -> Statement { - Statement { - kind: StatementKind::ExpressionStmt { - expression: Expression { - kind: ExpressionKind::FunctionCall { - callee: Box::new(Expression { - kind: ExpressionKind::Identifier { name: "require".into() }, - span: span(), - }), - arguments: vec![ - Expression { - kind: ExpressionKind::BinaryOp { - left: Box::new(msg_sender_expr()), - operator: BinaryOperator::Eq, - right: Box::new(owner_expr()), - }, - span: span(), - }, - ], - }, - span: span(), - }, - }, - span: span(), - } - } - - #[test] - fn internal_function() { - let f = make_func("_update", Visibility::Internal, FunctionKind::Function, vec![], Some(vec![])); - assert_eq!(classify_function(&f, &contract()), AccessLevel::Internal); - } - - #[test] - fn private_function() { - let f = make_func("_helper", Visibility::Private, FunctionKind::Function, vec![], Some(vec![])); - assert_eq!(classify_function(&f, &contract()), AccessLevel::Internal); - } - - #[test] - fn public_no_modifier() { - let f = make_func("deposit", Visibility::External, FunctionKind::Function, vec![], Some(vec![])); - assert_eq!(classify_function(&f, &contract()), AccessLevel::Public); - } - - #[test] - fn restricted_only_owner() { - let f = make_func("setRate", Visibility::External, FunctionKind::Function, vec!["onlyOwner"], Some(vec![])); - assert_eq!( - classify_function(&f, &contract()), - AccessLevel::Restricted { role: "onlyOwner".into() } - ); - } - - #[test] - fn when_not_paused_is_still_public() { - // whenNotPaused is a state guard, NOT access control - let f = make_func("deposit", Visibility::External, FunctionKind::Function, vec!["whenNotPaused"], Some(vec![])); - assert_eq!(classify_function(&f, &contract()), AccessLevel::Public); - } - - #[test] - fn nonreentrant_is_still_public() { - let f = make_func("withdraw", Visibility::External, FunctionKind::Function, vec!["nonReentrant"], Some(vec![])); - assert_eq!(classify_function(&f, &contract()), AccessLevel::Public); - } - - #[test] - fn constructor_is_special() { - let f = make_func("", Visibility::Public, FunctionKind::Constructor, vec![], Some(vec![])); - assert_eq!( - classify_function(&f, &contract()), - AccessLevel::Special { kind: "constructor".into() } - ); - } - - #[test] - fn receive_is_special() { - let f = make_func("", Visibility::External, FunctionKind::Receive, vec![], Some(vec![])); - assert_eq!( - classify_function(&f, &contract()), - AccessLevel::Special { kind: "receive".into() } - ); - } - - #[test] - fn require_msg_sender_eq_owner() { - let body = vec![require_sender_eq_owner()]; - let f = make_func("admin_func", Visibility::External, FunctionKind::Function, vec![], Some(body)); - assert_eq!( - classify_function(&f, &contract()), - AccessLevel::Restricted { role: "msg.sender check".into() } - ); - } - - #[test] - fn auth_modifier() { - let f = make_func("mint", Visibility::External, FunctionKind::Function, vec!["auth"], Some(vec![])); - assert_eq!( - classify_function(&f, &contract()), - AccessLevel::Restricted { role: "auth".into() } - ); - } - - #[test] - fn msg_sender_is_exact() { - assert!(expr_is_msg_sender(&msg_sender_expr())); - - // "sender" without "msg" is NOT msg.sender - let not_msg = Expression { - kind: ExpressionKind::MemberAccess { - object: Box::new(Expression { - kind: ExpressionKind::Identifier { name: "tx".into() }, - span: span(), - }), - member: "sender".into(), - }, - span: span(), - }; - assert!(!expr_is_msg_sender(¬_msg)); - } -} diff --git a/crates/ilold-core/src/classify/mod.rs b/crates/ilold-core/src/classify/mod.rs deleted file mode 100644 index 965bd9d..0000000 --- a/crates/ilold-core/src/classify/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod entry_points; diff --git a/crates/ilold-core/src/exploration/add_step_solidity.rs b/crates/ilold-core/src/exploration/add_step_solidity.rs deleted file mode 100644 index adc6cfc..0000000 --- a/crates/ilold-core/src/exploration/add_step_solidity.rs +++ /dev/null @@ -1,80 +0,0 @@ -use std::collections::HashMap; - -use ilold_session_core::exploration::session::{ - ExplorationSession, ExplorationStep, MutationScope, StateMutation, TraceConfig, -}; -use ilold_session_core::journal::types::JournalEntry; - -use crate::cfg::types::CfgGraph; -use crate::model::common::StateVar; -use crate::model::contract::ContractDef; -use crate::model::function::FunctionDef; -use crate::model::project::Project; -use crate::narrative::trace::{build_flow_tree_with_mutations, FlowConfig}; - -#[allow(clippy::too_many_arguments)] -pub fn add_solidity_step<'a>( - session: &'a mut ExplorationSession, - function: &FunctionDef, - cfg: &CfgGraph, - state_vars: &[StateVar], - project: &Project, - owning_contract: &ContractDef, - all_cfgs: &HashMap<(String, String), CfgGraph>, - timestamp: &str, - trace_config: TraceConfig, -) -> &'a ExplorationStep { - let step_index = session.steps.len(); - - let flow_config = FlowConfig { - max_depth: trace_config.depth, - include_reverts: trace_config.include_reverts, - expand_set: trace_config.expand_set.iter().copied().collect(), - }; - - let (flow_tree, raw_mutations) = build_flow_tree_with_mutations( - owning_contract, - function, - cfg, - project, - all_cfgs, - &flow_config, - ); - - let mutations: Vec<StateMutation> = raw_mutations - .into_iter() - .filter_map(|fm| { - let base = crate::util::target_base_name(&fm.target); - if !state_vars.iter().any(|sv| sv.name == base) { - return None; - } - Some(StateMutation { - variable: fm.target, - operator: fm.operator, - value_expr: fm.value, - step_index, - via: fm.via, - flow_step_id: Some(fm.flow_step_id), - scope: MutationScope::State, - }) - }) - .collect(); - - let flow_tree_value = serde_json::to_value(&flow_tree).ok(); - - session.steps.push(ExplorationStep { - function: function.name.clone(), - mutations, - flow_tree: flow_tree_value, - trace_config, - runtime_trace: None, - call_payload: None, - }); - - session.journal.record(JournalEntry::SequenceExplored { - steps: session.steps.iter().map(|s| s.function.clone()).collect(), - timestamp: timestamp.into(), - }); - - session.steps.last().unwrap() -} diff --git a/crates/ilold-core/src/exploration/commands.rs b/crates/ilold-core/src/exploration/commands.rs deleted file mode 100644 index 8a94bce..0000000 --- a/crates/ilold-core/src/exploration/commands.rs +++ /dev/null @@ -1,691 +0,0 @@ -use std::collections::HashMap; - -use serde::{Deserialize, Serialize}; - -use crate::cfg::types::CfgGraph; -use crate::classify::entry_points::{classify_all, AccessLevel}; -use crate::journal::types::{Finding, ReviewStatus, Severity}; -use crate::model::contract::ContractDef; -use crate::model::project::Project; -use crate::narrative::function::build_function_narrative; -use crate::narrative::sequence::{build_sequence_narrative, compute_flow_summary}; -use crate::narrative::trace::{build_flow_tree, FlowConfig, FlowTree}; -use crate::narrative::types::{FunctionNarrative, SequenceNarrative}; -use crate::pathtree::types::PathTree; -use crate::journal::export::export_markdown; -use crate::sequence::analysis::{FunctionBehavior, SequenceAnalysis, TransitionInfo}; - -use super::session::{ExplorationSession, TraceConfig, VariableSummary}; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct FunctionEntry { - pub name: String, - pub access: AccessLevel, - pub writes_state: bool, - pub has_external_calls: bool, - pub is_read_only: bool, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct AccessibleFunctionEntry { - pub name: String, - pub access: AccessLevel, - pub origin: String, - pub is_inherited: bool, - pub writes_state: bool, - pub has_external_calls: bool, - pub is_read_only: bool, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct AccessibleStateVarEntry { - pub name: String, - pub type_name: String, - pub is_constant: bool, - pub is_immutable: bool, - pub origin: String, - pub is_inherited: bool, -} - -pub struct AnalysisData<'a> { - pub project: &'a Project, - pub contract: &'a ContractDef, - pub cfgs: &'a HashMap<(String, String), CfgGraph>, - pub path_trees: &'a HashMap<(String, String), PathTree>, - pub behaviors: &'a [FunctionBehavior], - pub transitions: &'a [TransitionInfo], - pub classifications: &'a [(String, AccessLevel)], - pub all_sequence_analyses: &'a HashMap<String, SequenceAnalysis>, - pub all_classifications: &'a HashMap<String, Vec<(String, AccessLevel)>>, -} - -pub use ilold_session_core::exploration::canvas::CanvasPatch; -pub use ilold_session_core::exploration::scenario::{ - validate_scenario_name, ScenarioAction, ScenarioEvent, ScenarioInfo, -}; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub enum SessionCommand { - Call { - func: String, - #[serde(default)] - trace_config: Option<TraceConfig>, - }, - Back, - Clear, - State, - Functions, - Finding { severity: Severity, title: String, description: String }, - Note { text: String }, - Status { func: String, status: ReviewStatus }, - Session, - Who { variable: String }, - Export, - SaveSession, - LoadSession { json: String }, - FunctionsAll, - StateVarsAll, - Scenario { sub: ScenarioAction }, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub enum CommandResult { - StepAdded { - step_index: usize, - function: String, - access: AccessLevel, - state_changed: Vec<String>, - }, - StepRemoved { - remaining: usize, - }, - Cleared, - StateView { - summary: Vec<VariableSummary>, - }, - FunctionList { - functions: Vec<FunctionEntry>, - }, - FunctionListAll { - functions: Vec<AccessibleFunctionEntry>, - }, - StateVarListAll { - state_vars: Vec<AccessibleStateVarEntry>, - }, - FindingAdded { - id: String, - }, - NoteAdded, - StatusUpdated, - SessionView { - contract: String, - steps: Vec<String>, - findings_count: usize, - }, - VariableInfo { - variable: String, - writers: Vec<(String, AccessLevel)>, - readers: Vec<(String, AccessLevel)>, - }, - Exported { - markdown: String, - }, - SessionSaved { - json: String, - }, - SessionLoaded { - contract: String, - steps: Vec<String>, - }, - Error { - message: String, - }, - ScenarioList { items: Vec<ScenarioInfo> }, - ScenarioCreated { name: String }, - ScenarioSwitched { from: String, to: String }, - ScenarioForked { from: String, to: String, at_step: usize }, - ScenarioDeleted { name: String }, -} - -pub fn canvas_patch_from(result: &CommandResult, active_scenario: &str) -> Option<CanvasPatch> { - match result { - CommandResult::StepAdded { function, access, step_index, .. } => { - Some(CanvasPatch::AddNode { - scenario: active_scenario.to_string(), - function: function.clone(), - access: access.clone(), - step_index: *step_index, - runtime: None, - }) - } - CommandResult::StepRemoved { .. } => Some(CanvasPatch::RemoveLastNode { - scenario: active_scenario.to_string(), - }), - CommandResult::Cleared => Some(CanvasPatch::ClearAll { - scenario: active_scenario.to_string(), - }), - CommandResult::ScenarioCreated { name } => { - Some(CanvasPatch::ScenarioEvent(ScenarioEvent::Created { name: name.clone() })) - } - CommandResult::ScenarioSwitched { from, to } => { - if from == to { - // idempotent no-op: suppress WS broadcast - None - } else { - Some(CanvasPatch::ScenarioEvent(ScenarioEvent::Switched { - from: from.clone(), - to: to.clone(), - })) - } - } - CommandResult::ScenarioDeleted { name } => { - Some(CanvasPatch::ScenarioEvent(ScenarioEvent::Deleted { name: name.clone() })) - } - CommandResult::ScenarioForked { from, to, at_step } => { - Some(CanvasPatch::ScenarioEvent(ScenarioEvent::Forked { - from: from.clone(), - to: to.clone(), - at_step: *at_step, - })) - } - CommandResult::SessionLoaded { contract: _, steps: _ } => { - Some(CanvasPatch::ScenarioEvent(ScenarioEvent::Reloaded { - active: active_scenario.to_string(), - })) - } - _ => None, - } -} - -pub fn execute_command( - cmd: SessionCommand, - session: &mut ExplorationSession, - data: &AnalysisData, - timestamp: &str, -) -> CommandResult { - match cmd { - SessionCommand::Call { func, trace_config } => { - execute_call(session, data, &func, trace_config, timestamp) - } - SessionCommand::Back => execute_back(session), - SessionCommand::Clear => { session.clear(); CommandResult::Cleared } - SessionCommand::State => CommandResult::StateView { - summary: session.variable_summary(), - }, - SessionCommand::Functions => { - let classifications = classify_all(data.contract); - let functions: Vec<FunctionEntry> = classifications.into_iter().map(|(name, access)| { - let behavior = data.behaviors.iter().find(|b| b.name == name); - let writes_state = behavior - .map(|b| !b.effective_state_writes().is_empty()) - .unwrap_or(false); - let has_external_calls = behavior - .map(|b| !b.effective_external_calls().is_empty()) - .unwrap_or(false); - let is_read_only = !writes_state && !has_external_calls; - FunctionEntry { - name, - access, - writes_state, - has_external_calls, - is_read_only, - } - }).collect(); - CommandResult::FunctionList { functions } - } - SessionCommand::Finding { severity, title, description } => { - execute_finding(session, severity, title, description, timestamp) - } - SessionCommand::Note { text } => execute_note(session, &text, timestamp), - SessionCommand::Status { func, status } => { - execute_status(session, &func, status, timestamp, data) - } - SessionCommand::Session => CommandResult::SessionView { - contract: session.contract.clone(), - steps: session.current_sequence().into_iter().map(|s| s.to_string()).collect(), - findings_count: session.journal.findings.len(), - }, - SessionCommand::Who { variable } => execute_who(data, &variable), - SessionCommand::Export => { - let md = export_markdown(&session.journal, data.contract.functions.len()); - CommandResult::Exported { markdown: md } - } - SessionCommand::SaveSession | SessionCommand::LoadSession { .. } => CommandResult::Error { - message: "Save/Load commands must be dispatched at the store level, not through execute_command".into(), - }, - SessionCommand::FunctionsAll => execute_functions_all(data), - SessionCommand::StateVarsAll => execute_state_vars_all(data), - SessionCommand::Scenario { .. } => CommandResult::Error { - message: "Scenario commands must be dispatched at the store level, not through execute_command".into(), - }, - } -} - -fn execute_state_vars_all(data: &AnalysisData) -> CommandResult { - let state_vars: Vec<AccessibleStateVarEntry> = data.project - .accessible_state_vars(data.contract) - .into_iter() - .map(|(sv, origin, is_inherited)| AccessibleStateVarEntry { - name: sv.name, - type_name: sv.type_name, - is_constant: sv.is_constant, - is_immutable: sv.is_immutable, - origin, - is_inherited, - }) - .collect(); - - CommandResult::StateVarListAll { state_vars } -} - -fn execute_functions_all(data: &AnalysisData) -> CommandResult { - use crate::classify::entry_points::classify_function; - - let functions: Vec<AccessibleFunctionEntry> = data.project - .accessible_functions(data.contract) - .into_iter() - .map(|af| { - let access = if af.is_inherited { - if let Some(parent) = data.project.contracts.iter().find(|c| c.name == af.origin) { - classify_function(af.function, parent) - } else { - classify_function(af.function, data.contract) - } - } else { - classify_function(af.function, data.contract) - }; - - let behavior = if af.is_inherited { - data.all_sequence_analyses.get(&af.origin) - .and_then(|sa| sa.functions.iter().find(|b| b.name == af.function.name)) - } else { - data.behaviors.iter().find(|b| b.name == af.function.name) - }; - - let writes_state = behavior - .map(|b| !b.effective_state_writes().is_empty()) - .unwrap_or(false); - let has_external_calls = behavior - .map(|b| !b.effective_external_calls().is_empty()) - .unwrap_or(false); - let is_read_only = !writes_state && !has_external_calls; - - AccessibleFunctionEntry { - name: af.function.name.clone(), - access, - origin: af.origin, - is_inherited: af.is_inherited, - writes_state, - has_external_calls, - is_read_only, - } - }) - .collect(); - - CommandResult::FunctionListAll { functions } -} - -fn execute_call( - session: &mut ExplorationSession, - data: &AnalysisData, - func: &str, - trace_config: Option<TraceConfig>, - timestamp: &str, -) -> CommandResult { - let accessible = data.project.accessible_functions(data.contract); - let resolved = match accessible.iter().find(|a| a.function.name == func) { - Some(af) => af, - None => return CommandResult::Error { - message: format!("Function '{}' not found. Use 'functions' or 'funcs-all' to see available.", func), - }, - }; - - let owning_contract = if resolved.is_inherited { - match data.project.contracts.iter().find(|c| c.name == resolved.origin) { - Some(c) => c, - None => return CommandResult::Error { - message: format!("Parent contract '{}' not found", resolved.origin), - }, - } - } else { - data.contract - }; - - let key = (owning_contract.name.clone(), func.to_string()); - let cfg = match data.cfgs.get(&key) { - Some(c) => c, - None => return CommandResult::Error { - message: format!("No CFG available for {}::{}", owning_contract.name, func), - }, - }; - - let function_def = resolved.function; - - // Session steps model real external transactions. Internal/private - // functions cannot be invoked from outside the contract, so accepting - // them as entry points would produce an execution sequence that is - // impossible in practice — biasing every downstream analysis. - use crate::model::function::Visibility; - if matches!(function_def.visibility, Visibility::Internal | Visibility::Private) { - return CommandResult::Error { - message: format!( - "'{}' is {} and cannot be called from outside the contract — \ - not a valid session entry point. Use `tr {}` to view its flow, \ - or `c <public_caller>` to trace a real entry point.", - func, - match function_def.visibility { - Visibility::Internal => "internal", - Visibility::Private => "private", - _ => unreachable!(), - }, - func, - ), - }; - } - - let access = crate::classify::entry_points::classify_function(function_def, owning_contract); - - let combined_state_vars = data.project.inherited_state_vars(data.contract); - crate::exploration::add_step_solidity::add_solidity_step( - session, - function_def, - cfg, - &combined_state_vars, - data.project, - owning_contract, - data.cfgs, - timestamp, - trace_config.unwrap_or_default(), - ); - - let state_changed: Vec<String> = session.steps.last() - .map(|s| { - let mut vars: Vec<String> = s.mutations.iter().map(|m| m.variable.clone()).collect(); - vars.sort(); - vars.dedup(); - vars - }) - .unwrap_or_default(); - - CommandResult::StepAdded { - step_index: session.steps.len() - 1, - function: func.to_string(), - access, - state_changed, - } -} - -fn execute_back(session: &mut ExplorationSession) -> CommandResult { - if session.remove_last_step() { - CommandResult::StepRemoved { remaining: session.steps.len() } - } else { - CommandResult::Error { message: "No steps to undo".into() } - } -} - -fn execute_finding( - session: &mut ExplorationSession, - severity: Severity, - title: String, - description: String, - timestamp: &str, -) -> CommandResult { - let affected_sequence = if session.steps.is_empty() { - None - } else { - Some(session.current_sequence().into_iter().map(|s| s.to_string()).collect()) - }; - - let finding = Finding { - id: String::new(), - severity, - title, - affected_function: session.steps.last().map(|s| s.function.clone()).unwrap_or_default(), - affected_sequence, - description, - notes: vec![], - created_at: String::new(), - // Solidity flow does not yet capture step_index or recommendation; - // SDD-02 only wires the new fields on the Solana side. Setting None - // here preserves backward-compat with existing Solidity callers. - affected_step_index: None, - recommendation: None, - }; - - session.journal.add_finding(finding, timestamp); - let id = session.journal.findings.last().map(|f| f.id.clone()).unwrap_or_default(); - CommandResult::FindingAdded { id } -} - -fn execute_note(session: &mut ExplorationSession, text: &str, timestamp: &str) -> CommandResult { - let anchor = session.current_sequence().join(" → "); - session.journal.record(crate::journal::types::JournalEntry::NoteAdded { - anchor, - content: text.into(), - timestamp: timestamp.into(), - }); - CommandResult::NoteAdded -} - -fn execute_status( - session: &mut ExplorationSession, - func: &str, - status: ReviewStatus, - timestamp: &str, - data: &AnalysisData, -) -> CommandResult { - if data.project.resolve_function(data.contract, func).is_none() { - return CommandResult::Error { - message: format!("Function '{}' not found in {} or its ancestors", func, data.contract.name), - }; - } - session.journal.record(crate::journal::types::JournalEntry::StatusChanged { - function: func.into(), - status, - timestamp: timestamp.into(), - }); - CommandResult::StatusUpdated -} - -fn execute_who(data: &AnalysisData, variable: &str) -> CommandResult { - let var_lower = variable.to_lowercase(); - - let mut contract_set: std::collections::HashSet<String> = std::collections::HashSet::new(); - contract_set.insert(data.contract.name.clone()); - for af in data.project.accessible_functions(data.contract) { - if af.is_inherited { - contract_set.insert(af.origin); - } - } - let mut contract_names: Vec<String> = contract_set.into_iter().collect(); - contract_names.sort(); - - let access_for = |func_name: &str, contract_name: &str| -> AccessLevel { - data.all_classifications.get(contract_name) - .and_then(|classifs| classifs.iter().find(|(n, _)| n == func_name).map(|(_, a)| a.clone())) - .unwrap_or(AccessLevel::Internal) - }; - - // Match a normalized path against the requested var. - // The path equals the var (`var`), or ends with `.var`, or equals `var[]`, - // or starts with `var[]` followed by `.` or end. - let path_matches = |path: &str| -> bool { - let p = path.to_lowercase(); - if p == var_lower { - return true; - } - if p == format!("{}[]", var_lower) { - return true; - } - if p.ends_with(&format!(".{}", var_lower)) { - return true; - } - // base[].suffix or base.suffix where base == var - let base = p - .split(|c| c == '[' || c == '.') - .next() - .unwrap_or(&p); - base == var_lower - }; - - let mut writers: Vec<(String, AccessLevel)> = Vec::new(); - let mut readers: Vec<(String, AccessLevel)> = Vec::new(); - let mut seen_writers: std::collections::HashSet<(String, String)> = - std::collections::HashSet::new(); - let mut seen_readers: std::collections::HashSet<(String, String)> = - std::collections::HashSet::new(); - - for contract_name in &contract_names { - let behaviors = match data.all_sequence_analyses.get(contract_name) { - Some(sa) => &sa.functions, - None => continue, - }; - - for b in behaviors { - let eff_write_paths = b.effective_state_write_paths(); - let eff_read_paths = b.effective_state_read_paths(); - let eff_writes = b.effective_state_writes(); - let eff_reads = b.effective_state_reads(); - - let writes = eff_write_paths.iter().any(|p| path_matches(p)) - || eff_writes.iter().any(|w| { - let base = w - .split(|c| c == '[' || c == '.') - .next() - .unwrap_or(w) - .to_lowercase(); - base == var_lower - }); - let reads = eff_read_paths.iter().any(|p| path_matches(p)) - || eff_reads.iter().any(|r| { - let base = r - .split(|c| c == '[' || c == '.') - .next() - .unwrap_or(r) - .to_lowercase(); - base == var_lower - }); - - let key = (contract_name.clone(), b.name.clone()); - if writes && seen_writers.insert(key.clone()) { - writers.push((b.name.clone(), access_for(&b.name, contract_name))); - } - if reads && !writes && seen_readers.insert(key) { - readers.push((b.name.clone(), access_for(&b.name, contract_name))); - } - } - } - - if writers.is_empty() && readers.is_empty() { - return CommandResult::Error { - message: format!("Variable '{}' not found in any function", variable), - }; - } - - CommandResult::VariableInfo { variable: variable.to_string(), writers, readers } -} - -pub fn get_step_narrative( - session: &ExplorationSession, - step_index: usize, - data: &AnalysisData, -) -> Result<FunctionNarrative, String> { - let step = session.steps.get(step_index) - .ok_or_else(|| format!("Step {} not found", step_index))?; - - let (owning, func) = data.project - .resolve_function(data.contract, &step.function) - .ok_or_else(|| format!("Function '{}' not found", step.function))?; - - let key = (owning.name.clone(), step.function.clone()); - let cfg = data.cfgs.get(&key).ok_or("No CFG")?; - let pt = data.path_trees.get(&key).ok_or("No paths")?; - - let behaviors: &[FunctionBehavior] = if owning.name == data.contract.name { - data.behaviors - } else { - data.all_sequence_analyses.get(&owning.name) - .map(|sa| sa.functions.as_slice()) - .unwrap_or(&[]) - }; - - Ok(build_function_narrative(owning, func, pt, cfg, behaviors, data.project, data.all_sequence_analyses)) -} - -pub fn get_function_info( - func_name: &str, - data: &AnalysisData, -) -> Result<FunctionNarrative, String> { - let (owning, func) = data.project - .resolve_function(data.contract, func_name) - .ok_or_else(|| format!("Function '{}' not found", func_name))?; - - let key = (owning.name.clone(), func_name.to_string()); - let cfg = data.cfgs.get(&key).ok_or_else(|| format!("No CFG for {}::{}", owning.name, func_name))?; - let pt = data.path_trees.get(&key).ok_or("No paths")?; - - let behaviors: &[FunctionBehavior] = if owning.name == data.contract.name { - data.behaviors - } else { - data.all_sequence_analyses.get(&owning.name) - .map(|sa| sa.functions.as_slice()) - .unwrap_or(&[]) - }; - - Ok(build_function_narrative(owning, func, pt, cfg, behaviors, data.project, data.all_sequence_analyses)) -} - -pub fn get_flow_tree( - func_name: &str, - data: &AnalysisData, - max_depth: usize, - include_reverts: bool, - expand_set: std::collections::HashSet<usize>, -) -> Result<FlowTree, String> { - let (owning, func) = data.project - .resolve_function(data.contract, func_name) - .ok_or_else(|| format!("Function '{}' not found", func_name))?; - - let key = (owning.name.clone(), func_name.to_string()); - let cfg = data.cfgs.get(&key) - .ok_or_else(|| format!("No CFG for {}::{}", owning.name, func_name))?; - - let config = FlowConfig { - max_depth, - include_reverts, - expand_set, - }; - Ok(build_flow_tree(owning, func, cfg, data.project, data.cfgs, &config)) -} - -pub fn get_sequence_narrative( - session: &ExplorationSession, - data: &AnalysisData, -) -> Result<SequenceNarrative, String> { - if session.steps.len() < 2 { - return Err("Need at least 2 steps for a sequence narrative".into()); - } - - let names: Vec<&str> = session.current_sequence(); - let mut narrative = build_sequence_narrative( - &session.contract, &names, - data.behaviors, data.transitions, data.classifications, - ); - - // Enrich each narrative step with the FlowSummary derived from the - // session's persisted flow_tree. Legacy steps without a tree get - // None, which the renderer treats as "no summary available". - for (i, step) in narrative.steps.iter_mut().enumerate() { - if let Some(session_step) = session.steps.get(i) { - step.flow_summary = session_step.flow_tree.as_ref() - .and_then(|v| serde_json::from_value::<FlowTree>(v.clone()).ok()) - .map(|tree| compute_flow_summary(&tree, i)); - } - } - - Ok(narrative) -} - -pub fn get_session_state(session: &ExplorationSession) -> Vec<VariableSummary> { - session.variable_summary() -} diff --git a/crates/ilold-core/src/exploration/mod.rs b/crates/ilold-core/src/exploration/mod.rs deleted file mode 100644 index 46633d4..0000000 --- a/crates/ilold-core/src/exploration/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub use ilold_session_core::exploration::session; -pub mod commands; -pub mod timeline; -pub mod add_step_solidity; diff --git a/crates/ilold-core/src/exploration/timeline.rs b/crates/ilold-core/src/exploration/timeline.rs deleted file mode 100644 index 9b838cf..0000000 --- a/crates/ilold-core/src/exploration/timeline.rs +++ /dev/null @@ -1,196 +0,0 @@ -// Cross-step variable timeline. -// -// `build_variable_timeline` walks an `ExplorationSession` and collects -// every mutation of a target variable, annotating each one with the -// path conditions that had to be true to reach the write (extracted -// from the persisted FlowTree of the corresponding session step). - -use serde::{Deserialize, Serialize}; - -use crate::model::expression::AssignOperator; -use crate::narrative::trace::collect_path_conditions; -use crate::util::target_base_name; - -use super::session::{ExplorationSession, MutationScope}; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct VariableTimeline { - pub variable: String, - pub state_entries: Vec<TimelineEntry>, - pub local_entries: Vec<TimelineEntry>, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct TimelineEntry { - pub session_step_index: usize, - pub function: String, - pub flow_step_id: Option<usize>, - pub target: String, - pub operator: AssignOperator, - pub value_expr: String, - pub reached_when: Vec<String>, - pub via: Option<String>, - pub scope: MutationScope, -} - -/// Build a chronological timeline of every write to `variable` across the -/// session. Matches by base name (`balances[user]` matches `balances`). -/// Path conditions are extracted from each step's persisted FlowTree. -pub fn build_variable_timeline( - session: &ExplorationSession, - variable: &str, -) -> VariableTimeline { - let mut state_entries: Vec<TimelineEntry> = Vec::new(); - let mut local_entries: Vec<TimelineEntry> = Vec::new(); - - for (idx, step) in session.steps.iter().enumerate() { - for mutation in &step.mutations { - if !matches_variable(&mutation.variable, variable) { - continue; - } - - let reached_when = match (mutation.flow_step_id, &step.flow_tree) { - (Some(flow_id), Some(tree_value)) => { - serde_json::from_value::<crate::narrative::trace::FlowTree>(tree_value.clone()) - .ok() - .and_then(|tree| collect_path_conditions(&tree, flow_id)) - .unwrap_or_default() - } - _ => Vec::new(), - }; - - let entry = TimelineEntry { - session_step_index: idx, - function: step.function.clone(), - flow_step_id: mutation.flow_step_id, - target: mutation.variable.clone(), - operator: mutation.operator, - value_expr: mutation.value_expr.clone(), - reached_when, - via: mutation.via.clone(), - scope: mutation.scope, - }; - - match mutation.scope { - MutationScope::State => state_entries.push(entry), - MutationScope::Local => local_entries.push(entry), - } - } - } - - // Stable order: by session step index then by flow_step_id (None last - // so legacy mutations without ids appear after the resolved ones). - let sort_key = |e: &TimelineEntry| (e.session_step_index, e.flow_step_id.unwrap_or(usize::MAX)); - state_entries.sort_by_key(sort_key); - local_entries.sort_by_key(sort_key); - - VariableTimeline { - variable: variable.to_string(), - state_entries, - local_entries, - } -} - -/// True if `target` (a mutation target like `balances[msg.sender]` or -/// `config.fee`) refers to the variable `query`. -fn matches_variable(target: &str, query: &str) -> bool { - target_base_name(target) == query -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::exploration::session::{ExplorationStep, StateMutation, TraceConfig}; - - fn make_mutation(var: &str, op: AssignOperator, val: &str, flow_id: Option<usize>) -> StateMutation { - StateMutation { - variable: var.into(), - operator: op, - value_expr: val.into(), - step_index: 0, - via: None, - flow_step_id: flow_id, - scope: MutationScope::State, - } - } - - fn make_step(func: &str, mutations: Vec<StateMutation>) -> ExplorationStep { - ExplorationStep { - function: func.into(), - mutations, - flow_tree: None, - trace_config: TraceConfig::default(), - runtime_trace: None, - call_payload: None, - } - } - - #[test] - fn timeline_collects_state_writes_in_session_order() { - let mut session = ExplorationSession::new("C", "p"); - session.steps.push(make_step("deposit", vec![ - make_mutation("balances[user]", AssignOperator::AddAssign, "amount", Some(7)), - make_mutation("totalStaked", AssignOperator::AddAssign, "amount", Some(8)), - ])); - session.steps.push(make_step("withdraw", vec![ - make_mutation("balances[user]", AssignOperator::SubAssign, "amount", Some(14)), - ])); - - let tl = build_variable_timeline(&session, "balances"); - assert_eq!(tl.variable, "balances"); - assert_eq!(tl.state_entries.len(), 2); - assert!(tl.local_entries.is_empty()); - - // Order: session step 0 first, then 1. - assert_eq!(tl.state_entries[0].session_step_index, 0); - assert_eq!(tl.state_entries[0].function, "deposit"); - assert_eq!(tl.state_entries[0].target, "balances[user]"); - assert_eq!(tl.state_entries[1].session_step_index, 1); - assert_eq!(tl.state_entries[1].function, "withdraw"); - } - - #[test] - fn timeline_matches_by_base_name() { - let mut session = ExplorationSession::new("C", "p"); - session.steps.push(make_step("f", vec![ - make_mutation("balances[a]", AssignOperator::Assign, "1", None), - make_mutation("balancesOther", AssignOperator::Assign, "2", None), - make_mutation("config.fee", AssignOperator::Assign, "3", None), - ])); - - let tl = build_variable_timeline(&session, "balances"); - // Only `balances[a]` matches; `balancesOther` is a different name. - assert_eq!(tl.state_entries.len(), 1); - assert_eq!(tl.state_entries[0].target, "balances[a]"); - - let tl_config = build_variable_timeline(&session, "config"); - assert_eq!(tl_config.state_entries.len(), 1); - assert_eq!(tl_config.state_entries[0].target, "config.fee"); - } - - #[test] - fn timeline_returns_empty_when_no_matches() { - let mut session = ExplorationSession::new("C", "p"); - session.steps.push(make_step("f", vec![ - make_mutation("balances[a]", AssignOperator::Assign, "1", None), - ])); - - let tl = build_variable_timeline(&session, "totalSupply"); - assert!(tl.state_entries.is_empty()); - assert!(tl.local_entries.is_empty()); - } - - #[test] - fn timeline_legacy_mutation_has_empty_reached_when() { - let mut session = ExplorationSession::new("C", "p"); - // No flow_tree (legacy session) and no flow_step_id on the mutation. - session.steps.push(make_step("f", vec![ - make_mutation("balances", AssignOperator::Assign, "1", None), - ])); - - let tl = build_variable_timeline(&session, "balances"); - assert_eq!(tl.state_entries.len(), 1); - assert!(tl.state_entries[0].reached_when.is_empty()); - assert!(tl.state_entries[0].flow_step_id.is_none()); - } -} diff --git a/crates/ilold-core/src/lib.rs b/crates/ilold-core/src/lib.rs deleted file mode 100644 index 2276609..0000000 --- a/crates/ilold-core/src/lib.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Ilold Core — Smart contract execution path analysis engine. -//! -//! Pipeline: .sol files → Parser → Model Types → CFG Builder → CFG Graph - -pub mod model; -pub mod parse; -pub mod cfg; -pub mod callgraph; -pub mod pathtree; -pub mod sequence; -pub mod classify; -pub mod narrative; -pub mod slicing; -pub use ilold_session_core::journal; -pub mod exploration; -pub mod util; diff --git a/crates/ilold-core/src/model/common.rs b/crates/ilold-core/src/model/common.rs deleted file mode 100644 index e5b3dbc..0000000 --- a/crates/ilold-core/src/model/common.rs +++ /dev/null @@ -1,56 +0,0 @@ -use serde::{Deserialize, Serialize}; - -/// Points to a location in source code. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -pub struct SourceSpan { - pub file_index: usize, - pub start_line: u32, - pub start_col: u32, - pub end_line: u32, - pub end_col: u32, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Param { - pub name: String, - pub type_name: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct StateVar { - pub name: String, - pub type_name: String, - pub visibility: super::function::Visibility, - pub is_constant: bool, - pub is_immutable: bool, - pub initial_value: Option<super::expression::Expression>, - pub span: SourceSpan, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct StructDef { - pub name: String, - pub fields: Vec<Param>, - pub span: SourceSpan, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct EnumDef { - pub name: String, - pub variants: Vec<String>, - pub span: SourceSpan, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct EventDef { - pub name: String, - pub params: Vec<Param>, - pub span: SourceSpan, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ErrorDef { - pub name: String, - pub params: Vec<Param>, - pub span: SourceSpan, -} diff --git a/crates/ilold-core/src/model/contract.rs b/crates/ilold-core/src/model/contract.rs deleted file mode 100644 index 228a24f..0000000 --- a/crates/ilold-core/src/model/contract.rs +++ /dev/null @@ -1,28 +0,0 @@ -use serde::{Deserialize, Serialize}; - -use super::common::{EnumDef, ErrorDef, EventDef, SourceSpan, StateVar, StructDef}; -use super::function::FunctionDef; -use super::modifier::ModifierDef; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ContractDef { - pub name: String, - pub kind: ContractKind, - pub functions: Vec<FunctionDef>, - pub modifiers: Vec<ModifierDef>, - pub state_vars: Vec<StateVar>, - pub structs: Vec<StructDef>, - pub enums: Vec<EnumDef>, - pub events: Vec<EventDef>, - pub errors: Vec<ErrorDef>, - pub inherits: Vec<String>, - pub span: SourceSpan, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -pub enum ContractKind { - Contract, - Interface, - Library, - Abstract, -} diff --git a/crates/ilold-core/src/model/expression.rs b/crates/ilold-core/src/model/expression.rs deleted file mode 100644 index 17788b0..0000000 --- a/crates/ilold-core/src/model/expression.rs +++ /dev/null @@ -1,153 +0,0 @@ -use serde::{Deserialize, Serialize}; - -use super::common::SourceSpan; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Expression { - pub kind: ExpressionKind, - pub span: SourceSpan, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub enum ExpressionKind { - FunctionCall { - callee: Box<Expression>, - arguments: Vec<Expression>, - }, - MemberAccess { - object: Box<Expression>, - member: String, - }, - BinaryOp { - left: Box<Expression>, - operator: BinaryOperator, - right: Box<Expression>, - }, - UnaryOp { - operator: UnaryOperator, - operand: Box<Expression>, - }, - Identifier { - name: String, - }, - Literal { - value: String, - literal_type: LiteralType, - }, - IndexAccess { - base: Box<Expression>, - index: Option<Box<Expression>>, - }, - Assignment { - target: Box<Expression>, - operator: AssignOperator, - value: Box<Expression>, - }, - Ternary { - condition: Box<Expression>, - true_expr: Box<Expression>, - false_expr: Box<Expression>, - }, - /// uint256(x), address(y) — explicit type conversion - TypeCast { - type_name: String, - expression: Box<Expression>, - }, - /// type(uint256) — builtin that returns type metadata (min, max) - TypeMeta { - type_name: String, - }, - New { - type_name: String, - arguments: Vec<Expression>, - }, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -pub enum BinaryOperator { - Add, - Sub, - Mul, - Div, - Mod, - Pow, - Eq, - Neq, - Lt, - Gt, - Lte, - Gte, - And, - Or, - BitAnd, - BitOr, - BitXor, - Shl, - Shr, -} - -impl BinaryOperator { - /// Solidity source-form symbol for the operator (e.g. `Add` → `"+"`). - pub fn as_str(self) -> &'static str { - match self { - BinaryOperator::Add => "+", - BinaryOperator::Sub => "-", - BinaryOperator::Mul => "*", - BinaryOperator::Div => "/", - BinaryOperator::Mod => "%", - BinaryOperator::Pow => "**", - BinaryOperator::Eq => "==", - BinaryOperator::Neq => "!=", - BinaryOperator::Lt => "<", - BinaryOperator::Gt => ">", - BinaryOperator::Lte => "<=", - BinaryOperator::Gte => ">=", - BinaryOperator::And => "&&", - BinaryOperator::Or => "||", - BinaryOperator::BitAnd => "&", - BinaryOperator::BitOr => "|", - BinaryOperator::BitXor => "^", - BinaryOperator::Shl => "<<", - BinaryOperator::Shr => ">>", - } - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -pub enum UnaryOperator { - Not, - Neg, - BitNot, - PreIncrement, - PreDecrement, - PostIncrement, - PostDecrement, -} - -impl UnaryOperator { - /// Returns `(symbol, is_postfix)`. Prefix ops use `is_postfix = false` - /// so the caller can format `op + operand`; postfix increments use - /// `true` for `operand + op`. - pub fn format_parts(self) -> (&'static str, bool) { - match self { - UnaryOperator::Not => ("!", false), - UnaryOperator::Neg => ("-", false), - UnaryOperator::BitNot => ("~", false), - UnaryOperator::PreIncrement => ("++", false), - UnaryOperator::PreDecrement => ("--", false), - UnaryOperator::PostIncrement => ("++", true), - UnaryOperator::PostDecrement => ("--", true), - } - } -} - -pub use ilold_session_core::exploration::assign_operator::AssignOperator; - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -pub enum LiteralType { - Number, - String, - Bool, - HexString, - Address, -} diff --git a/crates/ilold-core/src/model/function.rs b/crates/ilold-core/src/model/function.rs deleted file mode 100644 index 6a9d005..0000000 --- a/crates/ilold-core/src/model/function.rs +++ /dev/null @@ -1,44 +0,0 @@ -use serde::{Deserialize, Serialize}; - -use super::common::{Param, SourceSpan}; -use super::modifier::ModifierRef; -use super::statement::Statement; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct FunctionDef { - pub name: String, - pub kind: FunctionKind, - pub visibility: Visibility, - pub mutability: Mutability, - pub modifiers: Vec<ModifierRef>, - pub params: Vec<Param>, - pub returns: Vec<Param>, - pub body: Option<Vec<Statement>>, - pub is_virtual: bool, - pub is_override: bool, - pub span: SourceSpan, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -pub enum FunctionKind { - Function, - Constructor, - Fallback, - Receive, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -pub enum Visibility { - Public, - External, - Internal, - Private, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -pub enum Mutability { - Pure, - View, - Payable, - NonPayable, -} diff --git a/crates/ilold-core/src/model/mod.rs b/crates/ilold-core/src/model/mod.rs deleted file mode 100644 index 4206d92..0000000 --- a/crates/ilold-core/src/model/mod.rs +++ /dev/null @@ -1,7 +0,0 @@ -pub mod common; -pub mod contract; -pub mod function; -pub mod modifier; -pub mod statement; -pub mod expression; -pub mod project; diff --git a/crates/ilold-core/src/model/modifier.rs b/crates/ilold-core/src/model/modifier.rs deleted file mode 100644 index 45337a0..0000000 --- a/crates/ilold-core/src/model/modifier.rs +++ /dev/null @@ -1,21 +0,0 @@ -use serde::{Deserialize, Serialize}; - -use super::common::{Param, SourceSpan}; -use super::expression::Expression; -use super::statement::Statement; - -/// Modifier definition (the declaration with its body) -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ModifierDef { - pub name: String, - pub params: Vec<Param>, - pub body: Vec<Statement>, - pub span: SourceSpan, -} - -/// Modifier reference on a function (the usage, e.g. `onlyOwner` in function header) -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ModifierRef { - pub name: String, - pub arguments: Vec<Expression>, -} diff --git a/crates/ilold-core/src/model/project.rs b/crates/ilold-core/src/model/project.rs deleted file mode 100644 index c595476..0000000 --- a/crates/ilold-core/src/model/project.rs +++ /dev/null @@ -1,264 +0,0 @@ -use std::collections::HashMap; -use std::collections::HashSet; - -use serde::{Deserialize, Serialize}; - -use super::common::StateVar; -use super::contract::{ContractDef, ContractKind}; -use super::function::{FunctionDef, Visibility}; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Project { - pub source_files: Vec<SourceFile>, - pub contracts: Vec<ContractDef>, - #[serde(skip)] - pub contract_index: HashMap<String, usize>, -} - -impl Project { - /// Rebuild the contract_index from the contracts vec. - /// Call this after deserialization since serde(skip) means the index is empty. - pub fn rebuild_index(&mut self) { - self.contract_index = self - .contracts - .iter() - .enumerate() - .map(|(i, c)| (c.name.clone(), i)) - .collect(); - } - - /// Find a contract by exact name, or auto-pick the best candidate when - /// `filter` is None. Returns Err with a user-facing message on failure. - /// - /// Auto-pick rules (when filter is None): - /// 1. Skip interfaces. - /// 2. Prefer "top-level" contracts — those NOT inherited by any other. - /// 3. If there's still ambiguity, return Err listing the candidates. - pub fn find_contract(&self, filter: Option<&str>) -> Result<&ContractDef, String> { - if let Some(name) = filter { - return self - .contracts - .iter() - .find(|c| c.name == name) - .ok_or_else(|| format!("Contract '{}' not found", name)); - } - - let non_interface: Vec<&ContractDef> = self - .contracts - .iter() - .filter(|c| c.kind != ContractKind::Interface) - .collect(); - - match non_interface.len() { - 0 => Err("No contracts found".into()), - 1 => Ok(non_interface[0]), - _ => { - // Collect names that appear as a parent in `inherits` of any contract. - let inherited: HashSet<&str> = non_interface - .iter() - .flat_map(|c| c.inherits.iter().map(|s| s.as_str())) - .collect(); - - let top_level: Vec<&&ContractDef> = non_interface - .iter() - .filter(|c| !inherited.contains(c.name.as_str())) - .collect(); - - match top_level.len() { - 1 => Ok(*top_level[0]), - _ => { - let names: Vec<&str> = non_interface.iter().map(|c| c.name.as_str()).collect(); - Err(format!( - "Multiple contracts, specify one: {}", - names.join(", ") - )) - } - } - } - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct SourceFile { - pub path: String, - pub content: String, -} - -pub struct AccessibleFunction<'a> { - pub function: &'a FunctionDef, - pub origin: String, - pub is_inherited: bool, -} - -impl Project { - pub fn accessible_functions<'a>(&'a self, contract: &'a ContractDef) -> Vec<AccessibleFunction<'a>> { - let mut seen: HashSet<String> = HashSet::new(); - let mut result: Vec<AccessibleFunction<'a>> = Vec::new(); - - for f in &contract.functions { - if f.name.is_empty() { continue; } - if seen.insert(f.name.clone()) { - result.push(AccessibleFunction { - function: f, - origin: contract.name.clone(), - is_inherited: false, - }); - } - } - - let mut visited: HashSet<String> = HashSet::new(); - let mut queue: Vec<&str> = contract.inherits.iter().map(|s| s.as_str()).collect(); - - while let Some(parent_name) = queue.pop() { - if !visited.insert(parent_name.to_string()) { continue; } - - let parent_idx = match self.contract_index.get(parent_name) { - Some(&i) => i, - None => continue, - }; - let parent = &self.contracts[parent_idx]; - - for f in &parent.functions { - if f.name.is_empty() { continue; } - if !matches!(f.visibility, Visibility::Public | Visibility::External) { continue; } - if seen.insert(f.name.clone()) { - result.push(AccessibleFunction { - function: f, - origin: parent.name.clone(), - is_inherited: true, - }); - } - } - - for grand in &parent.inherits { - queue.push(grand.as_str()); - } - } - - result - } - - pub fn resolve_function<'a>( - &'a self, - contract: &'a ContractDef, - func_name: &str, - ) -> Option<(&'a ContractDef, &'a FunctionDef)> { - let accessible = self.accessible_functions(contract); - for af in accessible { - if af.function.name == func_name { - if af.is_inherited { - return self.contracts.iter() - .find(|c| c.name == af.origin) - .map(|parent| (parent, af.function)); - } else { - return Some((contract, af.function)); - } - } - } - None - } - - pub fn accessible_state_vars(&self, contract: &ContractDef) -> Vec<(StateVar, String, bool)> { - let mut result: Vec<(StateVar, String, bool)> = contract - .state_vars - .iter() - .cloned() - .map(|sv| (sv, contract.name.clone(), false)) - .collect(); - - let mut seen_names: HashSet<String> = contract.state_vars.iter() - .map(|sv| sv.name.clone()) - .collect(); - - let mut visited: HashSet<String> = HashSet::new(); - let mut queue: Vec<&str> = contract.inherits.iter().map(|s| s.as_str()).collect(); - - while let Some(parent_name) = queue.pop() { - if !visited.insert(parent_name.to_string()) { continue; } - - let parent_idx = match self.contract_index.get(parent_name) { - Some(&i) => i, - None => continue, - }; - let parent = &self.contracts[parent_idx]; - - for sv in &parent.state_vars { - if seen_names.insert(sv.name.clone()) { - result.push((sv.clone(), parent.name.clone(), true)); - } - } - - for grand in &parent.inherits { - queue.push(grand.as_str()); - } - } - - result - } - - /// Resolve a modifier by name on `contract`, walking the inheritance chain. - /// Returns the first matching `ModifierDef` (current contract first, then - /// parents BFS). - pub fn resolve_modifier<'a>( - &'a self, - contract: &'a ContractDef, - mod_name: &str, - ) -> Option<&'a crate::model::modifier::ModifierDef> { - // Try current contract first - if let Some(m) = contract.modifiers.iter().find(|m| m.name == mod_name) { - return Some(m); - } - - // Walk inheritance chain BFS - let mut visited: HashSet<String> = HashSet::new(); - let mut queue: Vec<&str> = contract.inherits.iter().map(|s| s.as_str()).collect(); - - while let Some(parent_name) = queue.pop() { - if !visited.insert(parent_name.to_string()) { - continue; - } - - let parent_idx = match self.contract_index.get(parent_name) { - Some(&i) => i, - None => continue, - }; - let parent = &self.contracts[parent_idx]; - - if let Some(m) = parent.modifiers.iter().find(|m| m.name == mod_name) { - return Some(m); - } - - for grand in &parent.inherits { - queue.push(grand.as_str()); - } - } - - None - } - - pub fn inherited_state_vars(&self, contract: &ContractDef) -> Vec<StateVar> { - let mut result: Vec<StateVar> = contract.state_vars.clone(); - - let mut visited: HashSet<String> = HashSet::new(); - let mut queue: Vec<&str> = contract.inherits.iter().map(|s| s.as_str()).collect(); - - while let Some(parent_name) = queue.pop() { - if !visited.insert(parent_name.to_string()) { continue; } - - let parent_idx = match self.contract_index.get(parent_name) { - Some(&i) => i, - None => continue, - }; - let parent = &self.contracts[parent_idx]; - - result.extend(parent.state_vars.iter().cloned()); - - for grand in &parent.inherits { - queue.push(grand.as_str()); - } - } - - result - } -} diff --git a/crates/ilold-core/src/model/statement.rs b/crates/ilold-core/src/model/statement.rs deleted file mode 100644 index 460bcde..0000000 --- a/crates/ilold-core/src/model/statement.rs +++ /dev/null @@ -1,79 +0,0 @@ -use serde::{Deserialize, Serialize}; - -use super::common::{Param, SourceSpan}; -use super::expression::Expression; - -/// A single clause in a try/catch statement -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct CatchClause { - /// None for the success clause (returns), Some("Error"), Some("Panic"), or custom - pub name: Option<String>, - pub params: Vec<Param>, - pub body: Vec<Statement>, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Statement { - pub kind: StatementKind, - pub span: SourceSpan, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub enum StatementKind { - If { - condition: Expression, - then_body: Vec<Statement>, - else_body: Option<Vec<Statement>>, - }, - For { - init: Option<Box<Statement>>, - condition: Option<Expression>, - increment: Option<Expression>, - body: Vec<Statement>, - }, - While { - condition: Expression, - body: Vec<Statement>, - }, - DoWhile { - body: Vec<Statement>, - condition: Expression, - }, - Block { - statements: Vec<Statement>, - }, - UncheckedBlock { - statements: Vec<Statement>, - }, - Return { - value: Option<Expression>, - }, - Emit { - event_name: String, - arguments: Vec<Expression>, - }, - Revert { - error_name: Option<String>, - arguments: Vec<Expression>, - }, - ExpressionStmt { - expression: Expression, - }, - VariableDeclaration { - name: String, - type_name: String, - initial_value: Option<Expression>, - }, - /// try expr returns (...) { ok_body } catch Error(...) { err_body } catch (...) { ... } - TryCatch { - expression: Expression, - clauses: Vec<CatchClause>, - }, - Assembly { - span: SourceSpan, - }, - Break, - Continue, - /// The _ placeholder in modifiers - Placeholder, -} diff --git a/crates/ilold-core/src/narrative/formatter.rs b/crates/ilold-core/src/narrative/formatter.rs deleted file mode 100644 index afce27c..0000000 --- a/crates/ilold-core/src/narrative/formatter.rs +++ /dev/null @@ -1,153 +0,0 @@ -use std::fmt::Write; - -use crate::pathtree::types::TerminalKind; - -use super::types::*; - -pub fn function_to_markdown(n: &FunctionNarrative) -> String { - let mut md = String::new(); - - writeln!(md, "## {} ({})", n.name, n.access).unwrap(); - writeln!(md, "**Contract**: {} | **Paths**: {} ({} return, {} revert)", - n.contract, n.total_paths, n.happy_paths, n.revert_paths).unwrap(); - - if !n.modifiers.is_empty() { - writeln!(md, "**Modifiers**: {}", n.modifiers.join(", ")).unwrap(); - } - let has_writes = !n.state_writes.is_empty(); - let has_calls = !n.external_calls.is_empty(); - if has_writes && has_calls { - writeln!(md, "**Writes**: {} | **Calls**: {}", - n.state_writes.join(", "), n.external_calls.join(", ")).unwrap(); - } else if has_writes { - writeln!(md, "**Writes**: {}", n.state_writes.join(", ")).unwrap(); - } else if has_calls { - writeln!(md, "**Calls**: {}", n.external_calls.join(", ")).unwrap(); - } - - // Happy paths first, then revert paths - let mut happy: Vec<&PathNarrative> = n.paths.iter() - .filter(|p| p.terminal == TerminalKind::Return) - .collect(); - let mut revert: Vec<&PathNarrative> = n.paths.iter() - .filter(|p| p.terminal != TerminalKind::Return) - .collect(); - happy.sort_by_key(|p| p.id); - revert.sort_by_key(|p| p.id); - - for path in happy.iter().chain(revert.iter()) { - writeln!(md).unwrap(); - format_path(&mut md, path); - } - - if !n.observations.is_empty() { - writeln!(md).unwrap(); - writeln!(md, "### Observations").unwrap(); - for obs in &n.observations { - writeln!(md, "- **{}**: {}", obs.kind, obs.description).unwrap(); - } - } - - md -} - -pub fn sequence_to_markdown(n: &SequenceNarrative) -> String { - let mut md = String::new(); - - let names: Vec<&str> = n.steps.iter().map(|s| s.function.as_str()).collect(); - writeln!(md, "## Sequence: {}", names.join(" → ")).unwrap(); - writeln!(md, "**Contract**: {}", n.contract).unwrap(); - - for (i, step) in n.steps.iter().enumerate() { - writeln!(md).unwrap(); - writeln!(md, "### Step {} — {} ({})", i + 1, step.function, step.access).unwrap(); - - if !step.requires.is_empty() { - writeln!(md, "**Requires**: {}", step.requires.join(", ")).unwrap(); - } - if !step.effects.is_empty() { - writeln!(md, "**Effects**: {}", step.effects.join(", ")).unwrap(); - } - if !step.external_calls.is_empty() { - writeln!(md, "**Calls**: {}", step.external_calls.join(", ")).unwrap(); - } - if !step.events.is_empty() { - writeln!(md, "**Events**: {}", step.events.join(", ")).unwrap(); - } - - for dep in &step.dependencies { - writeln!(md, "- *Dependency*: {}", dep.relationship).unwrap(); - } - } - - if !n.observations.is_empty() { - writeln!(md).unwrap(); - writeln!(md, "### Observations").unwrap(); - for obs in &n.observations { - writeln!(md, "- **{}**: {}", obs.kind, obs.description).unwrap(); - } - } - - md -} - -pub fn overview_to_markdown( - contract_name: &str, - narratives: &[FunctionNarrative], -) -> String { - let mut md = String::new(); - - let total_paths: usize = narratives.iter().map(|n| n.total_paths).sum(); - let total_ext: usize = narratives.iter().map(|n| n.external_calls.len()).sum(); - writeln!(md, "## {} — {} functions, {} paths, {} external calls", - contract_name, narratives.len(), total_paths, total_ext).unwrap(); - - writeln!(md).unwrap(); - writeln!(md, "| Function | Access | Paths | Writes | Calls |").unwrap(); - writeln!(md, "|----------|--------|-------|--------|-------|").unwrap(); - - for n in narratives { - let writes = if n.state_writes.is_empty() { "—".into() } else { n.state_writes.join(", ") }; - let calls = if n.external_calls.is_empty() { "—".into() } else { n.external_calls.join(", ") }; - writeln!(md, "| {} | {} | {} ({}/{}R) | {} | {} |", - n.name, n.access, n.total_paths, n.happy_paths, n.revert_paths, - writes, calls).unwrap(); - } - - let all_obs: Vec<&Observation> = narratives.iter() - .flat_map(|n| n.observations.iter()) - .collect(); - - if !all_obs.is_empty() { - writeln!(md).unwrap(); - writeln!(md, "### Key Observations").unwrap(); - for obs in all_obs { - writeln!(md, "- **{}**: {}", obs.kind, obs.description).unwrap(); - } - } - - md -} - -fn format_path(md: &mut String, path: &PathNarrative) { - let terminal = match path.terminal { - TerminalKind::Return => "Return", - TerminalKind::Revert => "Revert", - TerminalKind::DepthCutoff => "DepthCutoff", - TerminalKind::LoopCutoff => "LoopCutoff", - }; - writeln!(md, "### Path #{} → {}", path.id, terminal).unwrap(); - - for (i, step) in path.steps.iter().enumerate() { - let branch_str = match step.branch { - Some(BranchDirection::True) => " [✓]", - Some(BranchDirection::False) => " [✗]", - None => "", - }; - write!(md, "{}. {} {}{}", i + 1, step.step_type.icon(), step.description, branch_str).unwrap(); - if let Some(detail) = &step.detail { - write!(md, " — {}", detail).unwrap(); - } - writeln!(md).unwrap(); - } -} diff --git a/crates/ilold-core/src/narrative/function.rs b/crates/ilold-core/src/narrative/function.rs deleted file mode 100644 index a5bf6f7..0000000 --- a/crates/ilold-core/src/narrative/function.rs +++ /dev/null @@ -1,356 +0,0 @@ -use std::collections::{HashMap, HashSet}; - -use crate::cfg::types::{BlockKind, BranchEdge, CfgGraph, CfgStatement}; -use crate::classify::entry_points::{classify_function, AccessLevel}; -use crate::model::contract::ContractDef; -use crate::model::function::FunctionDef; -use crate::model::project::Project; -use crate::narrative::types::TransitiveEffect; -use crate::pathtree::types::{PathTree, TerminalKind}; -use crate::sequence::analysis::{FunctionBehavior, SequenceAnalysis}; - -use super::types::*; - -pub fn build_function_narrative( - contract: &ContractDef, - function: &FunctionDef, - path_tree: &PathTree, - cfg: &CfgGraph, - all_behaviors: &[FunctionBehavior], - project: &Project, - all_sequence_analyses: &HashMap<String, SequenceAnalysis>, -) -> FunctionNarrative { - let access = classify_function(function, contract); - - let paths: Vec<PathNarrative> = path_tree - .paths - .iter() - .map(|path| { - let mut steps = Vec::new(); - - for node in &path.nodes { - let block = cfg - .node_indices() - .find(|&n| cfg[n].id == node.block_id) - .map(|n| &cfg[n]); - - let block = match block { - Some(b) => b, - None => continue, - }; - - let branch = branch_direction(&node.branch_taken); - - match block.kind { - BlockKind::Entry => { - steps.push(NarrativeStep { - step_type: StepType::Entry, - description: format!("{}()", function.name), - detail: None, - branch: None, - }); - } - BlockKind::Return => { - steps.push(NarrativeStep { - step_type: StepType::Return, - description: "return".into(), - detail: None, - branch, - }); - } - BlockKind::Revert => { - steps.push(NarrativeStep { - step_type: StepType::Revert, - description: "revert".into(), - detail: None, - branch, - }); - } - _ => { - for stmt in &block.statements { - if let Some(step) = statement_to_step(stmt, branch) { - steps.push(step); - } - } - } - } - } - - PathNarrative { - id: path.id, - terminal: path.terminal, - steps, - } - }) - .collect(); - - let observations = generate_observations( - &access, path_tree, cfg, all_behaviors, &function.name, - ); - - let mut writes = HashSet::new(); - let mut reads = HashSet::new(); - let mut calls = HashSet::new(); - let mut internal = HashSet::new(); - let mut events_set = HashSet::new(); - for p in &path_tree.paths { - for w in &p.annotations.state_writes { writes.insert(w.clone()); } - for r in &p.annotations.state_reads { reads.insert(r.clone()); } - for c in &p.annotations.external_calls { - calls.insert(format!("{}.{}", c.target, c.function)); - } - for ic in &p.annotations.internal_calls { - internal.insert(ic.clone()); - } - for ev in &p.annotations.events_emitted { - events_set.insert(ev.clone()); - } - } - - let mut writes: Vec<String> = writes.into_iter().collect(); - writes.sort(); - let mut reads: Vec<String> = reads.into_iter().collect(); - reads.sort(); - let mut calls: Vec<String> = calls.into_iter().collect(); - calls.sort(); - let mut internal: Vec<String> = internal.into_iter().collect(); - internal.sort(); - let mut events: Vec<String> = events_set.into_iter().collect(); - events.sort(); - - // Look up this function's behavior to get internal_calls - let root_behavior = all_behaviors.iter().find(|b| b.name == function.name); - - let (transitive_writes, transitive_reads, transitive_external, transitive_events) = - if let Some(root) = root_behavior { - collect_transitive_effects( - root, - contract, - project, - all_sequence_analyses, - &writes, - &reads, - &calls, - ) - } else { - (Vec::new(), Vec::new(), Vec::new(), Vec::new()) - }; - - FunctionNarrative { - contract: contract.name.clone(), - name: function.name.clone(), - access, - total_paths: path_tree.stats.total_paths, - happy_paths: path_tree.stats.happy_paths, - revert_paths: path_tree.stats.revert_paths, - paths, - observations, - state_writes: writes, - state_reads: reads, - external_calls: calls, - internal_calls: internal, - modifiers: function.modifiers.iter().map(|m| m.name.clone()).collect(), - events, - transitive_state_writes: transitive_writes, - transitive_state_reads: transitive_reads, - transitive_external_calls: transitive_external, - transitive_events, - } -} - -fn collect_transitive_effects( - root: &FunctionBehavior, - _root_contract: &ContractDef, - _project: &Project, - _all: &HashMap<String, SequenceAnalysis>, - _direct_writes: &[String], - _direct_reads: &[String], - _direct_external: &[String], -) -> (Vec<TransitiveEffect>, Vec<TransitiveEffect>, Vec<TransitiveEffect>, Vec<TransitiveEffect>) { - // Read pre-computed transitive sets from FunctionBehavior. - // Chain info is lost in this simpler form — will be restored in Phase 3. - fn map_to_effects(items: &[String]) -> Vec<TransitiveEffect> { - items - .iter() - .map(|item| TransitiveEffect { - via: vec!["(transitive)".to_string()], - item: item.clone(), - origin_contract: String::new(), - }) - .collect() - } - - ( - map_to_effects(&root.transitive_state_writes), - map_to_effects(&root.transitive_state_reads), - map_to_effects(&root.transitive_external_calls), - map_to_effects(&root.transitive_events), - ) -} - -fn statement_to_step(stmt: &CfgStatement, branch: Option<BranchDirection>) -> Option<NarrativeStep> { - match stmt { - CfgStatement::RequireCheck { condition, .. } => Some(NarrativeStep { - step_type: StepType::Condition, - description: format!("require({})", condition), - detail: None, - branch, - }), - CfgStatement::AssertCheck { condition, .. } => Some(NarrativeStep { - step_type: StepType::Condition, - description: format!("assert({})", condition), - detail: None, - branch, - }), - CfgStatement::ExternalCall { target, function, .. } => Some(NarrativeStep { - step_type: StepType::ExternalCall, - description: format!("{}.{}()", target, function), - detail: None, - branch: None, - }), - CfgStatement::InternalCall { function, .. } => Some(NarrativeStep { - step_type: StepType::InternalCall, - description: format!("{}()", function), - detail: None, - branch: None, - }), - CfgStatement::StateWrite { variable, .. } => Some(NarrativeStep { - step_type: StepType::StateWrite, - description: variable.clone(), - detail: None, - branch: None, - }), - CfgStatement::StateRead { variable, .. } => Some(NarrativeStep { - step_type: StepType::StateRead, - description: variable.clone(), - detail: None, - branch: None, - }), - CfgStatement::EthTransfer { to, .. } => Some(NarrativeStep { - step_type: StepType::EthTransfer, - description: format!("transfer ETH to {}", to), - detail: None, - branch: None, - }), - CfgStatement::EmitEvent { event, .. } => Some(NarrativeStep { - step_type: StepType::Event, - description: format!("emit {}", event), - detail: None, - branch: None, - }), - CfgStatement::AssemblyBlock { .. } => Some(NarrativeStep { - step_type: StepType::Assembly, - description: "assembly block".into(), - detail: None, - branch: None, - }), - CfgStatement::Assignment { .. } => None, - } -} - -fn branch_direction(edge: &Option<BranchEdge>) -> Option<BranchDirection> { - match edge { - Some(BranchEdge::ConditionalTrue { .. }) => Some(BranchDirection::True), - Some(BranchEdge::ConditionalFalse { .. }) => Some(BranchDirection::False), - _ => None, - } -} - -fn check_cei_violation( - path: &crate::pathtree::types::ExecutionPath, - cfg: &CfgGraph, -) -> Option<(String, String)> { - let mut seen_external_call: Option<String> = None; - - for node in &path.nodes { - let block = cfg - .node_indices() - .find(|&n| cfg[n].id == node.block_id) - .map(|n| &cfg[n]); - - let block = match block { - Some(b) => b, - None => continue, - }; - - for stmt in &block.statements { - match stmt { - CfgStatement::ExternalCall { target, function, .. } => { - if seen_external_call.is_none() { - seen_external_call = Some(format!("{}.{}", target, function)); - } - } - CfgStatement::StateWrite { variable, .. } => { - if let Some(call) = &seen_external_call { - return Some((call.clone(), variable.clone())); - } - } - _ => {} - } - } - } - None -} - -fn generate_observations( - access: &AccessLevel, - path_tree: &PathTree, - cfg: &CfgGraph, - all_behaviors: &[FunctionBehavior], - function_name: &str, -) -> Vec<Observation> { - let mut obs = Vec::new(); - - for path in &path_tree.paths { - if path.terminal == TerminalKind::Revert { continue; } - - if let Some((call, write)) = check_cei_violation(path, cfg) { - obs.push(Observation { - kind: ObservationKind::CeiViolation, - description: format!( - "Path #{}: {} called BEFORE writing {}", - path.id, call, write, - ), - }); - break; - } - } - - let my_writes: HashSet<&str> = path_tree - .paths - .iter() - .flat_map(|p| p.annotations.state_writes.iter().map(|s| s.as_str())) - .collect(); - - for behavior in all_behaviors { - if behavior.name == function_name { continue; } - let shared: Vec<&str> = behavior - .state_reads - .iter() - .filter(|r| my_writes.contains(r.as_str())) - .map(|s| s.as_str()) - .collect(); - if !shared.is_empty() { - obs.push(Observation { - kind: ObservationKind::SharedState, - description: format!( - "{} reads {} which this function writes", - behavior.name, - shared.join(", "), - ), - }); - } - } - - if access.is_unrestricted() && !my_writes.is_empty() { - obs.push(Observation { - kind: ObservationKind::NoAccessControl, - description: format!( - "Public function writes state ({}) without access control", - my_writes.into_iter().collect::<Vec<_>>().join(", "), - ), - }); - } - - obs -} diff --git a/crates/ilold-core/src/narrative/mod.rs b/crates/ilold-core/src/narrative/mod.rs deleted file mode 100644 index e0b68a1..0000000 --- a/crates/ilold-core/src/narrative/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -pub mod types; -pub mod function; -pub mod sequence; -pub mod formatter; -pub mod trace; diff --git a/crates/ilold-core/src/narrative/sequence.rs b/crates/ilold-core/src/narrative/sequence.rs deleted file mode 100644 index ccc38e4..0000000 --- a/crates/ilold-core/src/narrative/sequence.rs +++ /dev/null @@ -1,147 +0,0 @@ -use crate::classify::entry_points::AccessLevel; -use crate::sequence::analysis::{FunctionBehavior, TransitionInfo}; - -use super::trace::{FlowKind, FlowNode, FlowTree}; -use super::types::*; - -pub fn build_sequence_narrative( - contract_name: &str, - function_names: &[&str], - behaviors: &[FunctionBehavior], - transitions: &[TransitionInfo], - classifications: &[(String, AccessLevel)], -) -> SequenceNarrative { - let mut steps = Vec::new(); - let mut observations = Vec::new(); - - for (i, func_name) in function_names.iter().enumerate() { - let behavior = behaviors.iter().find(|b| b.name == *func_name); - let access = classifications - .iter() - .find(|(name, _)| name == func_name) - .map(|(_, a)| a.clone()) - .unwrap_or(AccessLevel::Public); - - let (requires, effects, ext_calls, events) = match behavior { - Some(b) => ( - b.preconditions.clone(), - b.state_writes.clone(), - b.external_calls.clone(), - b.events.clone(), - ), - None => (vec![], vec![], vec![], vec![]), - }; - - let mut deps = Vec::new(); - - for prev_idx in 0..i { - let prev_name = function_names[prev_idx]; - let transition = transitions - .iter() - .find(|t| t.from == prev_name && t.to == *func_name); - - if let Some(t) = transition { - for cond in &t.conditions_affected { - deps.push(Dependency { - from_step: prev_idx, - variable: extract_variable_from_condition(cond), - relationship: cond.clone(), - }); - } - } - } - - steps.push(SequenceStep { - function: func_name.to_string(), - access, - requires, - effects, - external_calls: ext_calls, - events, - dependencies: deps, - flow_summary: None, - }); - } - - for i in 0..steps.len() { - if steps[i].external_calls.is_empty() { continue; } - for j in (i + 1)..steps.len() { - if steps[j].dependencies.iter().any(|d| d.from_step == i) { - observations.push(Observation { - kind: ObservationKind::SharedState, - description: format!( - "Step {} ({}) makes external calls, step {} ({}) depends on its state — cross-step interaction", - i + 1, steps[i].function, - j + 1, steps[j].function, - ), - }); - } - } - } - - SequenceNarrative { - contract: contract_name.to_string(), - steps, - observations, - } -} - -fn extract_variable_from_condition(cond: &str) -> String { - // conditions_affected format: "deposit writes 'balances' → withdraw needs require(...)" - if let Some(start) = cond.find('\'') { - if let Some(end) = cond[start + 1..].find('\'') { - return cond[start + 1..start + 1 + end].to_string(); - } - } - String::new() -} - -/// Walk a persisted FlowTree and aggregate counts + mutation refs into -/// a compact `FlowSummary` for use in sequence narratives. -pub fn compute_flow_summary(tree: &FlowTree, session_step_index: usize) -> FlowSummary { - let mut summary = FlowSummary { - total_steps: 0, - mutation_count: 0, - external_call_count: 0, - internal_call_count: 0, - depth_limited_count: 0, - mutation_refs: Vec::new(), - }; - walk_for_summary(&tree.root, session_step_index, &mut summary); - summary -} - -fn walk_for_summary(node: &FlowNode, session_step_index: usize, summary: &mut FlowSummary) { - summary.total_steps += 1; - match &node.kind { - FlowKind::Write { target, .. } => { - summary.mutation_count += 1; - summary.mutation_refs.push(MutationRef { - variable: target.clone(), - flow_step_id: node.step_id, - session_step_index, - }); - } - FlowKind::StateWrite { variable } => { - summary.mutation_count += 1; - summary.mutation_refs.push(MutationRef { - variable: variable.clone(), - flow_step_id: node.step_id, - session_step_index, - }); - } - FlowKind::ExternalCall { .. } | FlowKind::EthTransfer { .. } => { - summary.external_call_count += 1; - } - FlowKind::InternalCall { depth_limited, .. } => { - summary.internal_call_count += 1; - if *depth_limited { - summary.depth_limited_count += 1; - } - } - _ => {} - } - for child in &node.children { - walk_for_summary(child, session_step_index, summary); - } -} diff --git a/crates/ilold-core/src/narrative/trace.rs b/crates/ilold-core/src/narrative/trace.rs deleted file mode 100644 index 70d9875..0000000 --- a/crates/ilold-core/src/narrative/trace.rs +++ /dev/null @@ -1,936 +0,0 @@ -// FlowTree builder: walks a function CFG and produces a temporal execution -// trace with inlined internal calls and per-arm branch walks. - -use std::collections::{HashMap, HashSet}; - -use petgraph::visit::EdgeRef; -use petgraph::Direction; -use serde::{Deserialize, Serialize}; - -use crate::cfg::types::{BlockKind, BranchEdge, CfgGraph, CfgStatement}; -use crate::model::contract::ContractDef; -use crate::model::expression::AssignOperator; -use crate::model::function::FunctionDef; -use crate::model::project::Project; - -/// Hard cap on the canonical walk's call-nesting depth. Cycle detection -/// via `visited_calls` is the primary safeguard; this is a second line of -/// defence against pathological cases and ensures termination. -const CANONICAL_WALK_SAFETY_CAP: usize = 32; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct FlowTree { - pub contract: String, - pub function: String, - pub signature: String, - pub modifiers: Vec<String>, - pub max_depth: usize, - pub root: FlowNode, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct FlowNode { - pub step_id: usize, - pub depth: usize, - pub kind: FlowKind, - #[serde(default)] - pub from_modifier: Option<String>, - pub children: Vec<FlowNode>, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub enum FlowKind { - Entry { - signature: String, - }, - Require { - condition: String, - message: Option<String>, - }, - Assert { - condition: String, - }, - Write { - target: String, - value: String, - op: AssignOperator, - }, - StateWrite { - variable: String, - }, - StateRead { - variable: String, - }, - InternalCall { - function: String, - origin: String, - depth_limited: bool, - ops_count: usize, - }, - ExternalCall { - target: String, - function: String, - }, - EmitEvent { - name: String, - }, - BranchTrue { - condition: String, - }, - BranchFalse { - condition: String, - }, - LoopHeader { - kind: String, - }, - Return, - Revert { - reason: Option<String>, - }, - EthTransfer { - to: String, - }, - AssemblyBlock, - DepthLimit, -} - -pub struct FlowConfig { - pub max_depth: usize, - pub include_reverts: bool, - pub expand_set: HashSet<usize>, -} - -impl Default for FlowConfig { - fn default() -> Self { - FlowConfig { - max_depth: 2, - include_reverts: false, - expand_set: HashSet::new(), - } - } -} - -/// A raw write event harvested from a FlowTree, decoupled from any -/// session-level mutation model. The caller filters and converts. -#[derive(Debug, Clone)] -pub struct FlowMutation { - pub flow_step_id: usize, - pub target: String, - pub value: String, - pub operator: AssignOperator, - pub via: Option<String>, -} - -pub fn build_flow_tree( - contract: &ContractDef, - function: &FunctionDef, - cfg: &CfgGraph, - project: &Project, - all_cfgs: &HashMap<(String, String), CfgGraph>, - config: &FlowConfig, -) -> FlowTree { - let canonical = build_canonical_tree(contract, function, cfg, project, all_cfgs, config); - filter_for_render(canonical, config) -} - -/// Build a FlowTree and harvest every write event in one canonical walk. -/// Mutations are harvested from the pre-filter tree so writes inside -/// depth-limited calls are still reported. -pub fn build_flow_tree_with_mutations( - contract: &ContractDef, - function: &FunctionDef, - cfg: &CfgGraph, - project: &Project, - all_cfgs: &HashMap<(String, String), CfgGraph>, - config: &FlowConfig, -) -> (FlowTree, Vec<FlowMutation>) { - let canonical = build_canonical_tree(contract, function, cfg, project, all_cfgs, config); - let mutations = harvest_mutations_from_tree(&canonical); - let rendered = filter_for_render(canonical, config); - (rendered, mutations) -} - -/// Internal: run the canonical walk with stable step_ids but no depth -/// filtering. Used by both `build_flow_tree` and `build_flow_tree_with_mutations`. -fn build_canonical_tree( - contract: &ContractDef, - function: &FunctionDef, - cfg: &CfgGraph, - project: &Project, - all_cfgs: &HashMap<(String, String), CfgGraph>, - config: &FlowConfig, -) -> FlowTree { - let signature = build_signature(function); - - let mut counter: usize = 0; - let mut root = FlowNode { - step_id: next_id(&mut counter), - depth: 0, - kind: FlowKind::Entry { signature: signature.clone() }, - from_modifier: None, - children: Vec::new(), - }; - - let mut visited_calls: HashSet<(String, String)> = HashSet::new(); - visited_calls.insert((contract.name.clone(), function.name.clone())); - - walk_cfg( - cfg, - contract, - project, - all_cfgs, - config, - 0, - &mut counter, - &mut visited_calls, - &mut root.children, - ); - - FlowTree { - contract: contract.name.clone(), - function: function.name.clone(), - signature, - modifiers: function.modifiers.iter().map(|m| m.name.clone()).collect(), - max_depth: config.max_depth, - root, - } -} - -/// Second pass over a canonical FlowTree: collapses internal calls whose -/// children exceed `config.max_depth`, unless their canonical `step_id` is -/// in `config.expand_set`. Step_ids on remaining nodes are preserved, so -/// references (e.g. `tr <func> +N`) stay valid across different configs. -fn filter_for_render(mut tree: FlowTree, config: &FlowConfig) -> FlowTree { - filter_node_children(&mut tree.root, config); - tree -} - -fn filter_node_children(node: &mut FlowNode, config: &FlowConfig) { - if let FlowKind::InternalCall { - ref mut depth_limited, - ref mut ops_count, - .. - } = node.kind - { - let children_depth = node.depth + 1; - let should_collapse = children_depth > config.max_depth - && !config.expand_set.contains(&node.step_id); - if should_collapse && !node.children.is_empty() { - *ops_count = count_subtree_nodes(&node.children); - *depth_limited = true; - node.children.clear(); - return; - } - } - - for child in &mut node.children { - filter_node_children(child, config); - } -} - -fn count_subtree_nodes(children: &[FlowNode]) -> usize { - let mut total = 0; - for child in children { - total += 1; - total += count_subtree_nodes(&child.children); - } - total -} - -/// Pre-order DFS over the tree collecting one FlowMutation per write. -/// Dedupes by (target, operator, value); first occurrence wins so the -/// flow_step_id is the earliest in pre-order. -fn harvest_mutations_from_tree(tree: &FlowTree) -> Vec<FlowMutation> { - let mut out = Vec::new(); - let mut seen: HashSet<(String, AssignOperator, String)> = HashSet::new(); - harvest_node(&tree.root, &[], &mut out, &mut seen); - out -} - -fn harvest_node( - node: &FlowNode, - call_chain: &[String], - out: &mut Vec<FlowMutation>, - seen: &mut HashSet<(String, AssignOperator, String)>, -) { - match &node.kind { - FlowKind::Write { target, value, op } => { - let key = (target.clone(), *op, value.clone()); - if seen.insert(key) { - out.push(FlowMutation { - flow_step_id: node.step_id, - target: target.clone(), - value: value.clone(), - operator: *op, - via: call_chain_via(call_chain), - }); - } - } - FlowKind::StateWrite { variable } => { - let key = (variable.clone(), AssignOperator::Assign, String::new()); - if seen.insert(key) { - out.push(FlowMutation { - flow_step_id: node.step_id, - target: variable.clone(), - value: String::new(), - operator: AssignOperator::Assign, - via: call_chain_via(call_chain), - }); - } - } - FlowKind::InternalCall { function, .. } => { - // Recurse with the extended chain, then early-return so the - // fallthrough loop below doesn't re-walk children. - let mut new_chain: Vec<String> = call_chain.to_vec(); - new_chain.push(function.clone()); - for child in &node.children { - harvest_node(child, &new_chain, out, seen); - } - return; - } - _ => {} - } - for child in &node.children { - harvest_node(child, call_chain, out, seen); - } -} - -fn call_chain_via(chain: &[String]) -> Option<String> { - if chain.is_empty() { - None - } else { - Some(chain.join(" -> ")) - } -} - -/// Find the chain of branch conditions that had to be true to reach the -/// node with `target_step_id` in the tree. Returns `None` if the target -/// is not present. -/// -/// `BranchTrue { cond }` contributes `cond`; `BranchFalse { cond }` -/// contributes `!(cond)`. The branch node's own conditions are NOT -/// included in its own path — they describe the path TO its children. -pub fn collect_path_conditions( - tree: &FlowTree, - target_step_id: usize, -) -> Option<Vec<String>> { - let mut path: Vec<String> = Vec::new(); - if collect_path_rec(&tree.root, target_step_id, &mut path) { - Some(path) - } else { - None - } -} - -fn collect_path_rec(node: &FlowNode, target: usize, path: &mut Vec<String>) -> bool { - if node.step_id == target { - return true; - } - - let pushed = match &node.kind { - FlowKind::BranchTrue { condition } => { - path.push(condition.clone()); - true - } - FlowKind::BranchFalse { condition } => { - path.push(format!("!({})", condition)); - true - } - _ => false, - }; - - for child in &node.children { - if collect_path_rec(child, target, path) { - return true; - } - } - - if pushed { - path.pop(); - } - false -} - -fn next_id(counter: &mut usize) -> usize { - let id = *counter; - *counter += 1; - id -} - -fn build_signature(function: &FunctionDef) -> String { - let params: Vec<String> = function - .params - .iter() - .map(|p| format!("{} {}", p.type_name, p.name)) - .collect(); - format!("{}({})", function.name, params.join(", ")) -} - -#[allow(clippy::too_many_arguments)] -fn walk_cfg( - cfg: &CfgGraph, - contract: &ContractDef, - project: &Project, - all_cfgs: &HashMap<(String, String), CfgGraph>, - config: &FlowConfig, - depth: usize, - counter: &mut usize, - visited_calls: &mut HashSet<(String, String)>, - out: &mut Vec<FlowNode>, -) { - let entry = match cfg - .node_indices() - .find(|&n| cfg[n].kind == BlockKind::Entry) - { - Some(n) => n, - None => return, - }; - - let mut visited_blocks: HashSet<petgraph::stable_graph::NodeIndex> = HashSet::new(); - walk_block( - cfg, - entry, - contract, - project, - all_cfgs, - config, - depth, - counter, - visited_calls, - &mut visited_blocks, - out, - ); -} - -#[allow(clippy::too_many_arguments)] -fn walk_block( - cfg: &CfgGraph, - node: petgraph::stable_graph::NodeIndex, - contract: &ContractDef, - project: &Project, - all_cfgs: &HashMap<(String, String), CfgGraph>, - config: &FlowConfig, - depth: usize, - counter: &mut usize, - visited_calls: &mut HashSet<(String, String)>, - visited_blocks: &mut HashSet<petgraph::stable_graph::NodeIndex>, - out: &mut Vec<FlowNode>, -) { - if !visited_blocks.insert(node) { - return; - } - - let block = &cfg[node]; - - match block.kind { - BlockKind::Return => { - out.push(FlowNode { - step_id: next_id(counter), - depth, - kind: FlowKind::Return, - from_modifier: None, - children: Vec::new(), - }); - return; - } - BlockKind::Revert => { - if config.include_reverts { - out.push(FlowNode { - step_id: next_id(counter), - depth, - kind: FlowKind::Revert { reason: None }, - from_modifier: None, - children: Vec::new(), - }); - } - return; - } - _ => {} - } - - for stmt in &block.statements { - if let Some(node) = stmt_to_flow_node( - stmt, - depth, - counter, - contract, - project, - all_cfgs, - config, - visited_calls, - ) { - out.push(node); - } - } - - let mut edges: Vec<(petgraph::stable_graph::NodeIndex, BranchEdge)> = cfg - .edges_directed(node, Direction::Outgoing) - .map(|e| (e.target(), e.weight().clone())) - .collect(); - - // Deterministic edge ordering so step_ids are stable across runs, - // independent of petgraph's iteration order. - edges.sort_by_key(|(target, edge)| (edge.variant_order(), target.index())); - - if edges.is_empty() { - return; - } - - if edges.len() == 1 { - walk_block( - cfg, - edges[0].0, - contract, - project, - all_cfgs, - config, - depth, - counter, - visited_calls, - visited_blocks, - out, - ); - return; - } - - // Drop arms going to Revert blocks (require/assert/if-then-revert). - let mut useful: Vec<(petgraph::stable_graph::NodeIndex, BranchEdge)> = Vec::new(); - for (target, edge) in edges { - if cfg[target].kind == BlockKind::Revert && !config.include_reverts { - continue; - } - useful.push((target, edge)); - } - - if useful.is_empty() { - return; - } - - // One useful arm: collapse require/assert into linear walk. - if useful.len() == 1 { - let (target, _edge) = useful.into_iter().next().unwrap(); - walk_block( - cfg, - target, - contract, - project, - all_cfgs, - config, - depth, - counter, - visited_calls, - visited_blocks, - out, - ); - return; - } - - // Real if/else: clone visited_blocks per arm so each branch is complete. - // Post-merge code is duplicated under each arm. - for (target, edge) in useful { - let mut arm_visited = visited_blocks.clone(); - let mut children = Vec::new(); - walk_block( - cfg, - target, - contract, - project, - all_cfgs, - config, - depth + 1, - counter, - visited_calls, - &mut arm_visited, - &mut children, - ); - - if children.is_empty() { - continue; - } - - let kind = match edge { - BranchEdge::ConditionalTrue { condition } => FlowKind::BranchTrue { condition }, - BranchEdge::ConditionalFalse { condition } => FlowKind::BranchFalse { condition }, - BranchEdge::ExternalCallSuccess => FlowKind::BranchTrue { - condition: "<call success>".to_string(), - }, - BranchEdge::ExternalCallFailure => FlowKind::BranchFalse { - condition: "<call failure>".to_string(), - }, - BranchEdge::CatchClause { kind: catch_kind } => FlowKind::BranchTrue { - condition: format!("catch {}", catch_kind), - }, - BranchEdge::LoopBack => FlowKind::LoopHeader { kind: "back".to_string() }, - BranchEdge::LoopExit => FlowKind::LoopHeader { kind: "exit".to_string() }, - BranchEdge::Unconditional => { - out.extend(children); - continue; - } - }; - out.push(FlowNode { - step_id: next_id(counter), - depth, - kind, - from_modifier: None, - children, - }); - } -} - -#[allow(clippy::too_many_arguments)] -fn stmt_to_flow_node( - stmt: &CfgStatement, - depth: usize, - counter: &mut usize, - contract: &ContractDef, - project: &Project, - all_cfgs: &HashMap<(String, String), CfgGraph>, - config: &FlowConfig, - visited_calls: &mut HashSet<(String, String)>, -) -> Option<FlowNode> { - match stmt { - CfgStatement::Assignment { target, value, operator, from_modifier, .. } => { - Some(FlowNode { - step_id: next_id(counter), - depth, - kind: FlowKind::Write { - target: target.clone(), - value: value.clone(), - op: *operator, - }, - from_modifier: from_modifier.clone(), - children: Vec::new(), - }) - } - CfgStatement::RequireCheck { condition, message, from_modifier, .. } => { - Some(FlowNode { - step_id: next_id(counter), - depth, - kind: FlowKind::Require { - condition: condition.clone(), - message: message.clone(), - }, - from_modifier: from_modifier.clone(), - children: Vec::new(), - }) - } - CfgStatement::AssertCheck { condition, from_modifier, .. } => { - Some(FlowNode { - step_id: next_id(counter), - depth, - kind: FlowKind::Assert { condition: condition.clone() }, - from_modifier: from_modifier.clone(), - children: Vec::new(), - }) - } - CfgStatement::InternalCall { function, from_modifier, .. } => { - Some(build_internal_call_node( - function, - from_modifier.clone(), - depth, - counter, - contract, - project, - all_cfgs, - config, - visited_calls, - )) - } - CfgStatement::ExternalCall { target, function, from_modifier, .. } => { - Some(FlowNode { - step_id: next_id(counter), - depth, - kind: FlowKind::ExternalCall { - target: target.clone(), - function: function.clone(), - }, - from_modifier: from_modifier.clone(), - children: Vec::new(), - }) - } - CfgStatement::EmitEvent { event, from_modifier, .. } => { - Some(FlowNode { - step_id: next_id(counter), - depth, - kind: FlowKind::EmitEvent { name: event.clone() }, - from_modifier: from_modifier.clone(), - children: Vec::new(), - }) - } - CfgStatement::StateWrite { variable, from_modifier, .. } => { - Some(FlowNode { - step_id: next_id(counter), - depth, - kind: FlowKind::StateWrite { variable: variable.clone() }, - from_modifier: from_modifier.clone(), - children: Vec::new(), - }) - } - CfgStatement::StateRead { variable, from_modifier, .. } => { - Some(FlowNode { - step_id: next_id(counter), - depth, - kind: FlowKind::StateRead { variable: variable.clone() }, - from_modifier: from_modifier.clone(), - children: Vec::new(), - }) - } - CfgStatement::EthTransfer { to, from_modifier, .. } => { - Some(FlowNode { - step_id: next_id(counter), - depth, - kind: FlowKind::EthTransfer { to: to.clone() }, - from_modifier: from_modifier.clone(), - children: Vec::new(), - }) - } - CfgStatement::AssemblyBlock { from_modifier, .. } => { - Some(FlowNode { - step_id: next_id(counter), - depth, - kind: FlowKind::AssemblyBlock, - from_modifier: from_modifier.clone(), - children: Vec::new(), - }) - } - } -} - -#[allow(clippy::too_many_arguments)] -fn build_internal_call_node( - callee_name: &str, - from_modifier: Option<String>, - depth: usize, - counter: &mut usize, - contract: &ContractDef, - project: &Project, - all_cfgs: &HashMap<(String, String), CfgGraph>, - config: &FlowConfig, - visited_calls: &mut HashSet<(String, String)>, -) -> FlowNode { - let resolved = resolve_callee_cfg(callee_name, contract, project, all_cfgs); - - let (owning_name, callee_cfg) = match resolved { - Some(x) => x, - None => { - return FlowNode { - step_id: next_id(counter), - depth, - kind: FlowKind::InternalCall { - function: callee_name.to_string(), - origin: String::new(), - depth_limited: false, - ops_count: 0, - }, - from_modifier, - children: Vec::new(), - }; - } - }; - - let call_key = (owning_name.clone(), callee_name.to_string()); - let already_visited = visited_calls.contains(&call_key); - // Canonical walk always inlines up to the safety cap. Depth-based - // pruning is applied by `filter_for_render` as a second pass so that - // step_ids remain stable across different max_depth / expand_set values. - let depth_exhausted = depth + 1 > CANONICAL_WALK_SAFETY_CAP; - - if already_visited || depth_exhausted { - let ops_count = count_statements(callee_cfg); - return FlowNode { - step_id: next_id(counter), - depth, - kind: FlowKind::InternalCall { - function: callee_name.to_string(), - origin: owning_name, - depth_limited: true, - ops_count, - }, - from_modifier, - children: Vec::new(), - }; - } - - visited_calls.insert(call_key.clone()); - - let owning_contract = project - .contracts - .iter() - .find(|c| c.name == owning_name) - .unwrap_or(contract); - - let mut children = Vec::new(); - walk_cfg( - callee_cfg, - owning_contract, - project, - all_cfgs, - config, - depth + 1, - counter, - visited_calls, - &mut children, - ); - - // Pop the call stack so sibling calls to the same function still inline. - visited_calls.remove(&call_key); - - let ops_count = children.len(); - - FlowNode { - step_id: next_id(counter), - depth, - kind: FlowKind::InternalCall { - function: callee_name.to_string(), - origin: owning_name, - depth_limited: false, - ops_count, - }, - from_modifier, - children, - } -} - -fn resolve_callee_cfg<'a>( - callee: &str, - starting_contract: &ContractDef, - project: &Project, - all_cfgs: &'a HashMap<(String, String), CfgGraph>, -) -> Option<(String, &'a CfgGraph)> { - let key = (starting_contract.name.clone(), callee.to_string()); - if let Some(cfg) = all_cfgs.get(&key) { - return Some((starting_contract.name.clone(), cfg)); - } - - let mut visited: HashSet<String> = HashSet::new(); - let mut queue: Vec<String> = starting_contract.inherits.clone(); - - while let Some(parent_name) = queue.pop() { - if !visited.insert(parent_name.clone()) { - continue; - } - - let key = (parent_name.clone(), callee.to_string()); - if let Some(cfg) = all_cfgs.get(&key) { - return Some((parent_name, cfg)); - } - - if let Some(parent_idx) = project.contract_index.get(&parent_name) { - let parent = &project.contracts[*parent_idx]; - for grand in &parent.inherits { - queue.push(grand.clone()); - } - } - } - - None -} - -fn count_statements(cfg: &CfgGraph) -> usize { - cfg.node_indices() - .map(|n| cfg[n].statements.len()) - .sum() -} - -#[cfg(test)] -mod tests { - use super::*; - - fn leaf(step_id: usize, kind: FlowKind) -> FlowNode { - FlowNode { - step_id, - depth: 0, - kind, - from_modifier: None, - children: Vec::new(), - } - } - - fn parent(step_id: usize, kind: FlowKind, children: Vec<FlowNode>) -> FlowNode { - FlowNode { - step_id, - depth: 0, - kind, - from_modifier: None, - children, - } - } - - /// Build a synthetic tree: - /// Entry (0) - /// ├── BranchTrue("a > 0") (1) - /// │ └── BranchFalse("b > 0") (2) - /// │ └── Write reserve0 (3) - /// └── BranchFalse("a > 0") (4) - /// └── Write balance (5) - fn fixture_tree() -> FlowTree { - let inner_write = leaf(3, FlowKind::Write { - target: "reserve0".into(), - value: "x".into(), - op: AssignOperator::Assign, - }); - let bf_inner = parent(2, FlowKind::BranchFalse { condition: "b > 0".into() }, vec![inner_write]); - let bt_outer = parent(1, FlowKind::BranchTrue { condition: "a > 0".into() }, vec![bf_inner]); - - let other_write = leaf(5, FlowKind::Write { - target: "balance".into(), - value: "y".into(), - op: AssignOperator::Assign, - }); - let bf_outer = parent(4, FlowKind::BranchFalse { condition: "a > 0".into() }, vec![other_write]); - - let root = parent( - 0, - FlowKind::Entry { signature: "f()".into() }, - vec![bt_outer, bf_outer], - ); - - FlowTree { - contract: "C".into(), - function: "f".into(), - signature: "f()".into(), - modifiers: Vec::new(), - max_depth: 4, - root, - } - } - - #[test] - fn collect_path_conditions_inside_nested_branches() { - let tree = fixture_tree(); - // Step 3 lives inside BranchTrue(a>0) → BranchFalse(b>0) → Write - let path = collect_path_conditions(&tree, 3).expect("step 3 should be reachable"); - assert_eq!(path, vec!["a > 0".to_string(), "!(b > 0)".to_string()]); - } - - #[test] - fn collect_path_conditions_in_other_branch() { - let tree = fixture_tree(); - // Step 5 lives inside BranchFalse(a>0) → Write - let path = collect_path_conditions(&tree, 5).expect("step 5 should be reachable"); - assert_eq!(path, vec!["!(a > 0)".to_string()]); - } - - #[test] - fn collect_path_conditions_root_returns_empty_path() { - let tree = fixture_tree(); - // Root itself has step_id 0 — no conditions on the path TO the root. - let path = collect_path_conditions(&tree, 0).expect("root should be reachable"); - assert!(path.is_empty()); - } - - #[test] - fn collect_path_conditions_branch_node_itself() { - let tree = fixture_tree(); - // Step 2 IS the BranchFalse(b>0) node. Its own condition is NOT - // included; only the parent BranchTrue(a>0) is. - let path = collect_path_conditions(&tree, 2).expect("step 2 should be reachable"); - assert_eq!(path, vec!["a > 0".to_string()]); - } - - #[test] - fn collect_path_conditions_returns_none_for_unknown_step() { - let tree = fixture_tree(); - assert!(collect_path_conditions(&tree, 9999).is_none()); - } -} diff --git a/crates/ilold-core/src/narrative/types.rs b/crates/ilold-core/src/narrative/types.rs deleted file mode 100644 index 4c9fd3c..0000000 --- a/crates/ilold-core/src/narrative/types.rs +++ /dev/null @@ -1,163 +0,0 @@ -use serde::{Deserialize, Serialize}; -use std::fmt; - -use crate::classify::entry_points::AccessLevel; -use crate::pathtree::types::TerminalKind; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct FunctionNarrative { - pub contract: String, - pub name: String, - pub access: AccessLevel, - pub total_paths: usize, - pub happy_paths: usize, - pub revert_paths: usize, - pub paths: Vec<PathNarrative>, - pub observations: Vec<Observation>, - pub state_writes: Vec<String>, - pub state_reads: Vec<String>, - pub external_calls: Vec<String>, - #[serde(default)] - pub internal_calls: Vec<String>, - pub modifiers: Vec<String>, - #[serde(default)] - pub events: Vec<String>, - #[serde(default)] - pub transitive_state_writes: Vec<TransitiveEffect>, - #[serde(default)] - pub transitive_state_reads: Vec<TransitiveEffect>, - #[serde(default)] - pub transitive_external_calls: Vec<TransitiveEffect>, - #[serde(default)] - pub transitive_events: Vec<TransitiveEffect>, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct TransitiveEffect { - pub via: Vec<String>, - pub item: String, - pub origin_contract: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct PathNarrative { - pub id: usize, - pub terminal: TerminalKind, - pub steps: Vec<NarrativeStep>, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct NarrativeStep { - pub step_type: StepType, - pub description: String, - pub detail: Option<String>, - pub branch: Option<BranchDirection>, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -pub enum StepType { - Entry, - Condition, - StateWrite, - StateRead, - ExternalCall, - InternalCall, - EthTransfer, - Event, - Return, - Revert, - Assembly, -} - -impl StepType { - pub fn icon(&self) -> &str { - match self { - StepType::Entry => "▶", - StepType::Condition => "▸", - StepType::StateWrite => "✏", - StepType::StateRead => "◇", - StepType::ExternalCall => "→", - StepType::InternalCall => "○", - StepType::EthTransfer => "Ξ", - StepType::Event => "◆", - StepType::Return => "✓", - StepType::Revert => "✗", - StepType::Assembly => "⚙", - } - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -pub enum BranchDirection { - True, - False, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Observation { - pub kind: ObservationKind, - pub description: String, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub enum ObservationKind { - CeiViolation, - SharedState, - NoAccessControl, - ExternalCallRisk, -} - -impl fmt::Display for ObservationKind { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - ObservationKind::CeiViolation => write!(f, "CEI violation"), - ObservationKind::SharedState => write!(f, "Shared state"), - ObservationKind::NoAccessControl => write!(f, "No access control"), - ObservationKind::ExternalCallRisk => write!(f, "External call"), - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct SequenceNarrative { - pub contract: String, - pub steps: Vec<SequenceStep>, - pub observations: Vec<Observation>, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct SequenceStep { - pub function: String, - pub access: AccessLevel, - pub requires: Vec<String>, - pub effects: Vec<String>, - pub external_calls: Vec<String>, - pub events: Vec<String>, - pub dependencies: Vec<Dependency>, - #[serde(default)] - pub flow_summary: Option<FlowSummary>, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Dependency { - pub from_step: usize, - pub variable: String, - pub relationship: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct FlowSummary { - pub total_steps: usize, - pub mutation_count: usize, - pub external_call_count: usize, - pub internal_call_count: usize, - pub depth_limited_count: usize, - pub mutation_refs: Vec<MutationRef>, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct MutationRef { - pub variable: String, - pub flow_step_id: usize, - pub session_step_index: usize, -} diff --git a/crates/ilold-core/src/parse/error.rs b/crates/ilold-core/src/parse/error.rs deleted file mode 100644 index 4831b5a..0000000 --- a/crates/ilold-core/src/parse/error.rs +++ /dev/null @@ -1,26 +0,0 @@ -use crate::model::common::SourceSpan; - -#[derive(Debug, thiserror::Error)] -pub enum ParseError { - #[error("Failed to parse {path}: {message}")] - SyntaxError { - path: String, - message: String, - span: Option<SourceSpan>, - }, - - #[error("Unsupported Solidity version in {path}: requires 0.8.x or higher")] - UnsupportedVersion { path: String }, - - #[error("File not found: {path}")] - FileNotFound { path: String }, - - #[error("Import resolution failed: {import_path} (from {source_path})")] - ImportResolution { - import_path: String, - source_path: String, - }, - - #[error("Internal parser error: {message}")] - Internal { message: String }, -} diff --git a/crates/ilold-core/src/parse/mod.rs b/crates/ilold-core/src/parse/mod.rs deleted file mode 100644 index 38b049e..0000000 --- a/crates/ilold-core/src/parse/mod.rs +++ /dev/null @@ -1,14 +0,0 @@ -pub mod error; -pub mod solar_frontend; - -use std::path::PathBuf; - -use crate::model::project::Project; -use error::ParseError; - -/// Abstraction over the concrete parser implementation. -/// Enables swapping solar-parse for another backend (e.g. solang-parser) -/// without changing any downstream code. -pub trait ProjectParser { - fn parse(&self, paths: &[PathBuf]) -> Result<Project, ParseError>; -} diff --git a/crates/ilold-core/src/parse/solar_frontend.rs b/crates/ilold-core/src/parse/solar_frontend.rs deleted file mode 100644 index 2b1c583..0000000 --- a/crates/ilold-core/src/parse/solar_frontend.rs +++ /dev/null @@ -1,826 +0,0 @@ -use std::path::{Path, PathBuf}; - -use solar::ast::{self, ExprKind, ItemKind, LitKind, StmtKind}; -use solar::interface::{ColorChoice, Session}; -use solar::parse::Parser; - -use crate::model::common::*; -use crate::model::contract::*; -use crate::model::expression::*; -use crate::model::function::*; -use crate::model::modifier::*; -use crate::model::project::*; -use crate::model::statement::{CatchClause, Statement, StatementKind}; - -use super::error::ParseError; -use super::ProjectParser; - -pub struct SolarParser; - -impl ProjectParser for SolarParser { - fn parse(&self, paths: &[PathBuf]) -> Result<Project, ParseError> { - let mut all_contracts = Vec::new(); - let mut source_files = Vec::new(); - - for path in paths { - if !path.exists() { - return Err(ParseError::FileNotFound { - path: path.display().to_string(), - }); - } - - let src = std::fs::read_to_string(path).map_err(|e| ParseError::Internal { - message: format!("Failed to read {}: {e}", path.display()), - })?; - - let file_index = source_files.len(); - // Canonicalize to absolute so downstream consumers (the canvas - // source viewer → `vscode://` deep link) can open the file - // regardless of the user's cwd. Falls back to the original - // display path on error (e.g. network filesystems) so the - // parse never fails over a cosmetic concern. - let stored_path = std::fs::canonicalize(path) - .map(|p| p.display().to_string()) - .unwrap_or_else(|_| path.display().to_string()); - source_files.push(SourceFile { - path: stored_path, - content: src.clone(), - }); - - let contracts = parse_single_file(path, &src, file_index)?; - all_contracts.extend(contracts); - } - - let mut project = Project { - source_files, - contracts: all_contracts, - contract_index: Default::default(), - }; - project.rebuild_index(); - Ok(project) - } -} - -fn parse_single_file( - path: &Path, - src: &str, - file_index: usize, -) -> Result<Vec<ContractDef>, ParseError> { - let sess = Session::builder() - .with_buffer_emitter(ColorChoice::Never) - .build(); - - let mut contracts = Vec::new(); - - let result = sess.enter(|| -> Result<(), ParseError> { - let arena = ast::Arena::new(); - let mut parser = Parser::from_source_code( - &sess, - &arena, - solar::interface::source_map::FileName::Real(path.to_path_buf()), - src.to_string(), - ) - .map_err(|_| ParseError::SyntaxError { - path: path.display().to_string(), - message: "Failed to initialize parser".into(), - span: None, - })?; - - let source_unit = parser.parse_file().map_err(|e| { - e.emit(); - ParseError::SyntaxError { - path: path.display().to_string(), - message: "Syntax error".into(), - span: None, - } - })?; - - // Solar's BytePos starts at 1 (not 0). For multi-file parsing, each file - // has an offset. We detect it from the source_map. - let base_offset = sess.source_map() - .files() - .first() - .map(|f| f.start_pos.to_usize()) - .unwrap_or(1); - - let mapper = SpanMapper::new(file_index, src, base_offset); - - for item in source_unit.items.iter() { - if let ItemKind::Contract(ref contract) = item.kind { - contracts.push(convert_contract(contract, &mapper)); - } - } - - Ok(()) - }); - - result?; - Ok(contracts) -} - -// ============================================================================ -// Contract -// ============================================================================ - -fn convert_contract(contract: &ast::ItemContract<'_>, mapper: &SpanMapper) -> ContractDef { - let mut functions = Vec::new(); - let mut modifiers = Vec::new(); - let mut state_vars = Vec::new(); - let mut structs = Vec::new(); - let mut enums = Vec::new(); - let mut events = Vec::new(); - let mut errors = Vec::new(); - - for item in contract.body.iter() { - match &item.kind { - ItemKind::Function(f) => { - if f.kind == ast::FunctionKind::Modifier { - modifiers.push(convert_modifier_def(f, mapper)); - } else { - functions.push(convert_function(f, mapper)); - } - } - ItemKind::Variable(v) => { - state_vars.push(convert_state_var(v, mapper)); - } - ItemKind::Struct(s) => { - structs.push(convert_struct(s, mapper)); - } - ItemKind::Enum(e) => { - enums.push(convert_enum(e, mapper)); - } - ItemKind::Event(ev) => { - events.push(convert_event(ev, mapper)); - } - ItemKind::Error(er) => { - errors.push(convert_error(er, mapper)); - } - _ => {} - } - } - - let inherits: Vec<String> = contract - .bases - .iter() - .map(|base| path_to_string(&base.name)) - .collect(); - - ContractDef { - name: contract.name.as_str().to_string(), - kind: convert_contract_kind(contract.kind), - functions, - modifiers, - state_vars, - structs, - enums, - events, - errors, - inherits, - span: mapper.convert(contract.name.span), - } -} - -fn convert_contract_kind(kind: ast::ContractKind) -> ContractKind { - match kind { - ast::ContractKind::Contract => ContractKind::Contract, - ast::ContractKind::Interface => ContractKind::Interface, - ast::ContractKind::Library => ContractKind::Library, - ast::ContractKind::AbstractContract => ContractKind::Abstract, - } -} - -// ============================================================================ -// Functions and modifiers -// ============================================================================ - -fn convert_function(f: &ast::ItemFunction<'_>, mapper: &SpanMapper) -> FunctionDef { - let name = f - .header - .name - .as_ref() - .map(|n| n.as_str().to_string()) - .unwrap_or_default(); - - let kind = match f.kind { - ast::FunctionKind::Constructor => FunctionKind::Constructor, - ast::FunctionKind::Fallback => FunctionKind::Fallback, - ast::FunctionKind::Receive => FunctionKind::Receive, - _ => FunctionKind::Function, - }; - - // FunctionHeader has helper methods that extract from Spanned - let visibility = f - .header - .visibility() - .map(convert_visibility) - .unwrap_or(Visibility::Internal); - - let mutability = convert_mutability(f.header.state_mutability()); - - let modifiers: Vec<ModifierRef> = f - .header - .modifiers - .iter() - .map(|m| convert_modifier_ref(m, mapper)) - .collect(); - - let params = convert_param_list(&f.header.parameters); - let returns = convert_returns(&f.header); - let body = f - .body - .as_ref() - .map(|block| convert_block_stmts(block, mapper)); - - // Span covers the full function (signature + body) so consumers like - // the canvas source viewer can slice the complete declaration. When - // there's no body (interfaces, abstract) the header span is all we - // have and stays correct. - let header_span = mapper.convert(f.header.span); - let span = match f.body.as_ref().and_then(|b| b.last()) { - Some(last_stmt) => { - let end = mapper.convert(last_stmt.span); - crate::model::common::SourceSpan { - file_index: header_span.file_index, - start_line: header_span.start_line, - start_col: header_span.start_col, - end_line: end.end_line, - end_col: end.end_col, - } - } - None => header_span, - }; - - FunctionDef { - name, - kind, - visibility, - mutability, - modifiers, - params, - returns, - body, - is_virtual: f.header.virtual_(), - is_override: f.header.override_.is_some(), - span, - } -} - -fn convert_modifier_def(f: &ast::ItemFunction<'_>, mapper: &SpanMapper) -> ModifierDef { - let name = f - .header - .name - .as_ref() - .map(|n| n.as_str().to_string()) - .unwrap_or_default(); - - let params = convert_param_list(&f.header.parameters); - let body = f - .body - .as_ref() - .map(|block| convert_block_stmts(block, mapper)) - .unwrap_or_default(); - - ModifierDef { - name, - params, - body, - span: mapper.convert(f.header.span), - } -} - -fn convert_modifier_ref(m: &ast::Modifier<'_>, mapper: &SpanMapper) -> ModifierRef { - let name = path_to_string(&m.name); - let arguments: Vec<Expression> = m.arguments.exprs().map(|e| convert_expression(e, mapper)).collect(); - ModifierRef { name, arguments } -} - -fn convert_visibility(v: ast::Visibility) -> Visibility { - match v { - ast::Visibility::Public => Visibility::Public, - ast::Visibility::External => Visibility::External, - ast::Visibility::Internal => Visibility::Internal, - ast::Visibility::Private => Visibility::Private, - } -} - -fn convert_mutability(m: ast::StateMutability) -> Mutability { - match m { - ast::StateMutability::Pure => Mutability::Pure, - ast::StateMutability::View => Mutability::View, - ast::StateMutability::Payable => Mutability::Payable, - ast::StateMutability::NonPayable => Mutability::NonPayable, - } -} - -fn convert_param_list(params: &ast::ParameterList<'_>) -> Vec<Param> { - params - .iter() - .map(|p| Param { - name: p - .name - .as_ref() - .map(|n| n.as_str().to_string()) - .unwrap_or_default(), - type_name: type_to_string(&p.ty), - }) - .collect() -} - -fn convert_returns(header: &ast::FunctionHeader<'_>) -> Vec<Param> { - header - .returns() - .iter() - .map(|p| Param { - name: p - .name - .as_ref() - .map(|n| n.as_str().to_string()) - .unwrap_or_default(), - type_name: type_to_string(&p.ty), - }) - .collect() -} - -// ============================================================================ -// State vars, structs, enums, events, errors -// ============================================================================ - -fn convert_state_var(v: &ast::VariableDefinition<'_>, mapper: &SpanMapper) -> StateVar { - let visibility = v - .visibility - .map(convert_visibility) - .unwrap_or(Visibility::Internal); - - let is_constant = v - .mutability - .map(|m| m == ast::VarMut::Constant) - .unwrap_or(false); - - let is_immutable = v - .mutability - .map(|m| m == ast::VarMut::Immutable) - .unwrap_or(false); - - let initial_value = v - .initializer - .as_ref() - .map(|expr| convert_expression(expr, mapper)); - - StateVar { - name: v - .name - .as_ref() - .map(|n| n.as_str().to_string()) - .unwrap_or_default(), - type_name: type_to_string(&v.ty), - visibility, - is_constant, - is_immutable, - initial_value, - span: mapper.convert(v.span), - } -} - -fn convert_struct(s: &ast::ItemStruct<'_>, mapper: &SpanMapper) -> StructDef { - let fields = s - .fields - .iter() - .map(|f| Param { - name: f - .name - .as_ref() - .map(|n| n.as_str().to_string()) - .unwrap_or_default(), - type_name: type_to_string(&f.ty), - }) - .collect(); - - StructDef { - name: s.name.as_str().to_string(), - fields, - span: mapper.convert(s.name.span), - } -} - -fn convert_enum(e: &ast::ItemEnum<'_>, mapper: &SpanMapper) -> EnumDef { - EnumDef { - name: e.name.as_str().to_string(), - variants: e.variants.iter().map(|v| v.as_str().to_string()).collect(), - span: mapper.convert(e.name.span), - } -} - -fn convert_event(ev: &ast::ItemEvent<'_>, mapper: &SpanMapper) -> EventDef { - let params = ev - .parameters - .iter() - .map(|p| Param { - name: p - .name - .as_ref() - .map(|n| n.as_str().to_string()) - .unwrap_or_default(), - type_name: type_to_string(&p.ty), - }) - .collect(); - - EventDef { - name: ev.name.as_str().to_string(), - params, - span: mapper.convert(ev.name.span), - } -} - -fn convert_error(er: &ast::ItemError<'_>, mapper: &SpanMapper) -> ErrorDef { - let params = er - .parameters - .iter() - .map(|p| Param { - name: p - .name - .as_ref() - .map(|n| n.as_str().to_string()) - .unwrap_or_default(), - type_name: type_to_string(&p.ty), - }) - .collect(); - - ErrorDef { - name: er.name.as_str().to_string(), - params, - span: mapper.convert(er.name.span), - } -} - -// ============================================================================ -// Statements -// ============================================================================ - -fn convert_block_stmts(block: &ast::Block<'_>, mapper: &SpanMapper) -> Vec<Statement> { - block - .stmts - .iter() - .map(|s| convert_statement(s, mapper)) - .collect() -} - -fn convert_statement(stmt: &ast::Stmt<'_>, mapper: &SpanMapper) -> Statement { - let kind = match &stmt.kind { - StmtKind::If(condition, then_stmt, else_stmt) => StatementKind::If { - condition: convert_expression(condition, mapper), - then_body: wrap_stmt_as_body(then_stmt, mapper), - else_body: else_stmt - .as_ref() - .map(|s| wrap_stmt_as_body(s, mapper)), - }, - - StmtKind::For { init, cond, next, body } => StatementKind::For { - init: init - .as_ref() - .map(|s| Box::new(convert_statement(s, mapper))), - condition: cond - .as_ref() - .map(|e| convert_expression(e, mapper)), - increment: next - .as_ref() - .map(|e| convert_expression(e, mapper)), - body: wrap_stmt_as_body(body, mapper), - }, - - StmtKind::While(condition, body) => StatementKind::While { - condition: convert_expression(condition, mapper), - body: wrap_stmt_as_body(body, mapper), - }, - - StmtKind::DoWhile(body, condition) => StatementKind::DoWhile { - body: wrap_stmt_as_body(body, mapper), - condition: convert_expression(condition, mapper), - }, - - StmtKind::Block(block) => StatementKind::Block { - statements: convert_block_stmts(block, mapper), - }, - - StmtKind::UncheckedBlock(block) => StatementKind::UncheckedBlock { - statements: convert_block_stmts(block, mapper), - }, - - StmtKind::Return(value) => StatementKind::Return { - value: value.as_ref().map(|e| convert_expression(e, mapper)), - }, - - StmtKind::Emit(path, args) => StatementKind::Emit { - event_name: path_to_string(path), - arguments: args.exprs().map(|e| convert_expression(e, mapper)).collect(), - }, - - StmtKind::Revert(path, args) => StatementKind::Revert { - error_name: Some(path_to_string(path)), - arguments: args.exprs().map(|e| convert_expression(e, mapper)).collect(), - }, - - StmtKind::Expr(expr) => StatementKind::ExpressionStmt { - expression: convert_expression(expr, mapper), - }, - - StmtKind::DeclSingle(var) => StatementKind::VariableDeclaration { - name: var - .name - .as_ref() - .map(|n| n.as_str().to_string()) - .unwrap_or_default(), - type_name: type_to_string(&var.ty), - initial_value: var - .initializer - .as_ref() - .map(|e| convert_expression(e, mapper)), - }, - - StmtKind::Try(try_stmt) => { - let expression = convert_expression(&try_stmt.expr, mapper); - let clauses = try_stmt - .clauses - .iter() - .map(|c| CatchClause { - name: c.name.as_ref().map(|n| n.as_str().to_string()), - params: convert_param_list(&c.args), - body: convert_block_stmts(&c.block, mapper), - }) - .collect(); - StatementKind::TryCatch { expression, clauses } - } - - StmtKind::DeclMulti(vars, expr) => { - // (uint a, uint b) = foo() — we store the whole thing as a single - // VariableDeclaration with a combined name for simplicity in v1. - let names: Vec<String> = vars - .iter() - .map(|v| match v { - solar::interface::SpannedOption::Some(var) => var - .name - .as_ref() - .map(|n: &ast::Ident| n.as_str().to_string()) - .unwrap_or("_".into()), - solar::interface::SpannedOption::None(_) => "_".into(), - }) - .collect(); - StatementKind::VariableDeclaration { - name: format!("({})", names.join(", ")), - type_name: "tuple".into(), - initial_value: Some(convert_expression(expr, mapper)), - } - } - - StmtKind::Assembly(..) => StatementKind::Assembly { - span: mapper.convert(stmt.span), - }, - - StmtKind::Break => StatementKind::Break, - StmtKind::Continue => StatementKind::Continue, - StmtKind::Placeholder => StatementKind::Placeholder, - }; - - Statement { - kind, - span: mapper.convert(stmt.span), - } -} - -fn wrap_stmt_as_body(stmt: &ast::Stmt<'_>, mapper: &SpanMapper) -> Vec<Statement> { - match &stmt.kind { - StmtKind::Block(block) => convert_block_stmts(block, mapper), - _ => vec![convert_statement(stmt, mapper)], - } -} - -// ============================================================================ -// Expressions -// ============================================================================ - -fn convert_expression(expr: &ast::Expr<'_>, mapper: &SpanMapper) -> Expression { - let kind = match &expr.kind { - ExprKind::Call(callee, args) => ExpressionKind::FunctionCall { - callee: Box::new(convert_expression(callee, mapper)), - arguments: args.exprs().map(|e| convert_expression(e, mapper)).collect(), - }, - - ExprKind::CallOptions(callee, _options) => ExpressionKind::FunctionCall { - callee: Box::new(convert_expression(callee, mapper)), - arguments: Vec::new(), - }, - - ExprKind::Member(object, member) => ExpressionKind::MemberAccess { - object: Box::new(convert_expression(object, mapper)), - member: member.as_str().to_string(), - }, - - ExprKind::Index(base, index_kind) => ExpressionKind::IndexAccess { - base: Box::new(convert_expression(base, mapper)), - index: match index_kind { - ast::IndexKind::Index(Some(expr)) => { - Some(Box::new(convert_expression(expr, mapper))) - } - _ => None, - }, - }, - - ExprKind::Binary(left, op, right) => ExpressionKind::BinaryOp { - left: Box::new(convert_expression(left, mapper)), - operator: convert_binop_kind(op.kind), - right: Box::new(convert_expression(right, mapper)), - }, - - ExprKind::Unary(op, operand) => ExpressionKind::UnaryOp { - operator: convert_unop_kind(op.kind), - operand: Box::new(convert_expression(operand, mapper)), - }, - - ExprKind::Assign(target, op, value) => ExpressionKind::Assignment { - target: Box::new(convert_expression(target, mapper)), - operator: op - .as_ref() - .map(|o| convert_assign_op_kind(o.kind)) - .unwrap_or(AssignOperator::Assign), - value: Box::new(convert_expression(value, mapper)), - }, - - ExprKind::Ternary(cond, true_expr, false_expr) => ExpressionKind::Ternary { - condition: Box::new(convert_expression(cond, mapper)), - true_expr: Box::new(convert_expression(true_expr, mapper)), - false_expr: Box::new(convert_expression(false_expr, mapper)), - }, - - ExprKind::Ident(ident) => ExpressionKind::Identifier { - name: ident.as_str().to_string(), - }, - - ExprKind::Lit(lit, _sub) => ExpressionKind::Literal { - value: format!("{lit}"), - literal_type: convert_literal_type(&lit.kind), - }, - - ExprKind::New(ty) => ExpressionKind::New { - type_name: type_to_string(ty), - arguments: Vec::new(), - }, - - ExprKind::Type(ty) => ExpressionKind::TypeMeta { - type_name: type_to_string(ty), - }, - - _ => ExpressionKind::Identifier { - name: "/* unsupported expr */".into(), - }, - }; - - Expression { - kind, - span: mapper.convert(expr.span), - } -} - -// ============================================================================ -// Operator conversion -// ============================================================================ - -fn convert_binop_kind(kind: ast::BinOpKind) -> BinaryOperator { - use ast::BinOpKind::*; - match kind { - Add => BinaryOperator::Add, - Sub => BinaryOperator::Sub, - Mul => BinaryOperator::Mul, - Div => BinaryOperator::Div, - Rem => BinaryOperator::Mod, - Pow => BinaryOperator::Pow, - Eq => BinaryOperator::Eq, - Ne => BinaryOperator::Neq, - Lt => BinaryOperator::Lt, - Gt => BinaryOperator::Gt, - Le => BinaryOperator::Lte, - Ge => BinaryOperator::Gte, - And => BinaryOperator::And, - Or => BinaryOperator::Or, - BitAnd => BinaryOperator::BitAnd, - BitOr => BinaryOperator::BitOr, - BitXor => BinaryOperator::BitXor, - Shl => BinaryOperator::Shl, - Shr | Sar => BinaryOperator::Shr, - } -} - -fn convert_unop_kind(kind: ast::UnOpKind) -> UnaryOperator { - use ast::UnOpKind::*; - match kind { - Not => UnaryOperator::Not, - Neg => UnaryOperator::Neg, - BitNot => UnaryOperator::BitNot, - PreInc => UnaryOperator::PreIncrement, - PreDec => UnaryOperator::PreDecrement, - PostInc => UnaryOperator::PostIncrement, - PostDec => UnaryOperator::PostDecrement, - } -} - -fn convert_assign_op_kind(kind: ast::BinOpKind) -> AssignOperator { - use ast::BinOpKind::*; - match kind { - Add => AssignOperator::AddAssign, - Sub => AssignOperator::SubAssign, - Mul => AssignOperator::MulAssign, - Div => AssignOperator::DivAssign, - Rem => AssignOperator::ModAssign, - BitAnd => AssignOperator::BitAndAssign, - BitOr => AssignOperator::BitOrAssign, - BitXor => AssignOperator::BitXorAssign, - Shl => AssignOperator::ShlAssign, - Shr | Sar => AssignOperator::ShrAssign, - _ => AssignOperator::Assign, - } -} - -fn convert_literal_type(kind: &LitKind<'_>) -> LiteralType { - match kind { - LitKind::Number(_) => LiteralType::Number, - LitKind::Str(..) => LiteralType::String, - LitKind::Bool(_) => LiteralType::Bool, - LitKind::Address(_) => LiteralType::Address, - _ => LiteralType::String, - } -} - -// ============================================================================ -// Helpers -// ============================================================================ - -/// Converts a solar Type to a readable string like "uint256", "address", -/// "mapping(address => uint256)", "uint256[]", or the custom type name. -/// Solar's Type doesn't implement Display, so we need our own conversion. -fn type_to_string(ty: &ast::Type<'_>) -> String { - match &ty.kind { - ast::TypeKind::Elementary(elem) => format!("{elem}"), - ast::TypeKind::Custom(path) => path_to_string(path), - ast::TypeKind::Array(arr) => { - let elem = type_to_string(&arr.element); - match &arr.size { - Some(size) => format!("{elem}[{size:?}]"), - None => format!("{elem}[]"), - } - } - ast::TypeKind::Mapping(m) => { - let key = type_to_string(&m.key); - let value = type_to_string(&m.value); - format!("mapping({key} => {value})") - } - ast::TypeKind::Function(_) => "function".into(), - } -} - -fn path_to_string(path: &ast::AstPath<'_>) -> String { - path.segments() - .last() - .map(|s| s.as_str().to_string()) - .unwrap_or_default() -} - -/// Pre-computed line offset table for converting byte positions to line/column. -struct SpanMapper { - file_index: usize, - /// Byte offset where each line starts. line_offsets[0] = 0 (first line). - line_offsets: Vec<usize>, - /// Offset added to solar's BytePos to get the position relative to this file. - /// Solar uses absolute positions across all files in the SourceMap. - base_offset: usize, -} - -impl SpanMapper { - fn new(file_index: usize, src: &str, base_offset: usize) -> Self { - let mut line_offsets = vec![0usize]; - for (i, ch) in src.char_indices() { - if ch == '\n' { - line_offsets.push(i + 1); - } - } - Self { file_index, line_offsets, base_offset } - } - - fn convert(&self, span: solar::interface::Span) -> SourceSpan { - let lo = span.lo().to_usize().saturating_sub(self.base_offset); - let hi = span.hi().to_usize().saturating_sub(self.base_offset); - let (start_line, start_col) = self.offset_to_line_col(lo); - let (end_line, end_col) = self.offset_to_line_col(hi); - SourceSpan { - file_index: self.file_index, - start_line: start_line as u32, - start_col: start_col as u32, - end_line: end_line as u32, - end_col: end_col as u32, - } - } - - fn offset_to_line_col(&self, offset: usize) -> (usize, usize) { - let line = self.line_offsets.partition_point(|&o| o <= offset).saturating_sub(1); - let col = offset.saturating_sub(self.line_offsets.get(line).copied().unwrap_or(0)); - (line + 1, col + 1) // 1-based - } - -} diff --git a/crates/ilold-core/src/pathtree/config.rs b/crates/ilold-core/src/pathtree/config.rs deleted file mode 100644 index 5cd1198..0000000 --- a/crates/ilold-core/src/pathtree/config.rs +++ /dev/null @@ -1,21 +0,0 @@ -/// Controls how aggressively the walker prunes paths. -/// Without limits, complex functions could generate millions of paths. -#[derive(Debug, Clone)] -pub struct PruningConfig { - /// Stop exploring a path after this many blocks. Default: 32. - pub max_depth: usize, - /// Stop unrolling a loop after this many iterations per path. Default: 3. - pub max_loop_unroll: usize, - /// Stop enumerating paths for a function after this many. Default: 10_000. - pub max_paths: usize, -} - -impl Default for PruningConfig { - fn default() -> Self { - Self { - max_depth: 32, - max_loop_unroll: 3, - max_paths: 10_000, - } - } -} diff --git a/crates/ilold-core/src/pathtree/mod.rs b/crates/ilold-core/src/pathtree/mod.rs deleted file mode 100644 index 0ae8170..0000000 --- a/crates/ilold-core/src/pathtree/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod config; -pub mod types; -pub mod walker; diff --git a/crates/ilold-core/src/pathtree/types.rs b/crates/ilold-core/src/pathtree/types.rs deleted file mode 100644 index a479fdf..0000000 --- a/crates/ilold-core/src/pathtree/types.rs +++ /dev/null @@ -1,83 +0,0 @@ -use serde::{Deserialize, Serialize}; - -use crate::cfg::types::{BlockKind, BranchEdge, CfgGraph}; - -/// All paths through a single function, plus statistics. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct PathTree { - pub contract: String, - pub function: String, - pub paths: Vec<ExecutionPath>, - pub stats: PathTreeStats, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct PathTreeStats { - pub total_paths: usize, - pub paths_pruned: usize, - pub max_depth_reached: usize, - pub revert_paths: usize, - pub happy_paths: usize, -} - -/// One complete route from Entry to a terminal block. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ExecutionPath { - pub id: usize, - pub nodes: Vec<PathNode>, - pub terminal: TerminalKind, - pub annotations: PathAnnotations, - pub depth: usize, -} - -/// A single step in a path — which block we visited and how we got there. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct PathNode { - pub block_id: usize, - pub block_kind: BlockKind, - /// The edge taken to reach this block. None for the Entry node. - pub branch_taken: Option<BranchEdge>, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -pub enum TerminalKind { - Return, - Revert, - DepthCutoff, - LoopCutoff, -} - -/// What happens along a path — accumulated during DFS traversal. -#[derive(Debug, Clone, Default, Serialize, Deserialize)] -pub struct PathAnnotations { - pub state_writes: Vec<String>, - pub state_reads: Vec<String>, - /// Fully qualified write paths with normalized indices, e.g. - /// `proposals[].executed`, `balances[]`. Preserved for accurate - /// who-style queries that distinguish nested fields. - #[serde(default)] - pub state_write_paths: Vec<String>, - #[serde(default)] - pub state_read_paths: Vec<String>, - pub external_calls: Vec<ExternalCallInfo>, - pub internal_calls: Vec<String>, - pub events_emitted: Vec<String>, - pub require_checks: Vec<String>, - pub eth_transfers: Vec<String>, - pub has_assembly: bool, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ExternalCallInfo { - pub target: String, - pub function: String, -} - -/// Groups a function's CFG with its path tree for easy access. -#[derive(Debug, Clone)] -pub struct FunctionAnalysis { - pub contract: String, - pub function: String, - pub cfg: CfgGraph, - pub path_tree: PathTree, -} diff --git a/crates/ilold-core/src/pathtree/walker.rs b/crates/ilold-core/src/pathtree/walker.rs deleted file mode 100644 index 75e727c..0000000 --- a/crates/ilold-core/src/pathtree/walker.rs +++ /dev/null @@ -1,425 +0,0 @@ -use std::collections::HashMap; - -use petgraph::stable_graph::NodeIndex; -use petgraph::visit::EdgeRef; -use petgraph::Direction; - -use crate::cfg::types::*; -use crate::model::common::StateVar; - -use super::config::PruningConfig; -use super::types::*; - -/// One item on the DFS stack. Represents a path being explored. -struct WalkState { - node: NodeIndex, - path: Vec<PathNode>, - annotations: PathAnnotations, - depth: usize, - /// How many times each LoopCondition block has been visited in THIS path. - /// Each path gets its own copy so loops don't interfere between paths. - loop_counts: HashMap<usize, usize>, - /// The edge that led to this node (for recording in PathNode). - edge_taken: Option<BranchEdge>, -} - -pub fn build_path_tree( - cfg: &CfgGraph, - contract_name: &str, - function_name: &str, - state_vars: &[StateVar], - config: &PruningConfig, -) -> PathTree { - let mut paths: Vec<ExecutionPath> = Vec::new(); - let mut paths_pruned: usize = 0; - let mut max_depth_reached: usize = 0; - let mut next_path_id: usize = 0; - - // Find the Entry node - let entry = match cfg.node_indices().find(|&n| cfg[n].kind == BlockKind::Entry) { - Some(n) => n, - None => { - // No entry = interface function with no body - return PathTree { - contract: contract_name.to_string(), - function: function_name.to_string(), - paths: Vec::new(), - stats: PathTreeStats { - total_paths: 0, - paths_pruned: 0, - max_depth_reached: 0, - revert_paths: 0, - happy_paths: 0, - }, - }; - } - }; - - // DFS stack — each item is a path being explored - let mut stack: Vec<WalkState> = vec![WalkState { - node: entry, - path: Vec::new(), - annotations: PathAnnotations::default(), - depth: 0, - loop_counts: HashMap::new(), - edge_taken: None, - }]; - - while let Some(state) = stack.pop() { - // Budget check: stop if we've found enough paths - if paths.len() >= config.max_paths { - paths_pruned += 1; - continue; - } - - let block = &cfg[state.node]; - let mut path = state.path; - let mut annotations = state.annotations; - let mut loop_counts = state.loop_counts; - let depth = state.depth; - - // Track max depth - if depth > max_depth_reached { - max_depth_reached = depth; - } - - // Pruning: max depth - if depth > config.max_depth { - paths_pruned += 1; - path.push(PathNode { - block_id: block.id, - block_kind: block.kind, - branch_taken: state.edge_taken, - }); - let id = next_path_id; - next_path_id += 1; - paths.push(ExecutionPath { - id, - nodes: path, - terminal: TerminalKind::DepthCutoff, - annotations, - depth, - }); - continue; - } - - // Pruning: loop detection - if block.kind == BlockKind::LoopCondition { - let count = loop_counts.entry(block.id).or_insert(0); - *count += 1; - if *count > config.max_loop_unroll { - paths_pruned += 1; - path.push(PathNode { - block_id: block.id, - block_kind: block.kind, - branch_taken: state.edge_taken, - }); - let id = next_path_id; - next_path_id += 1; - paths.push(ExecutionPath { - id, - nodes: path, - terminal: TerminalKind::LoopCutoff, - annotations, - depth, - }); - continue; - } - } - - // Add this block to the path - path.push(PathNode { - block_id: block.id, - block_kind: block.kind, - branch_taken: state.edge_taken, - }); - - // Collect annotations from this block's statements - collect_annotations(&block.statements, state_vars, &mut annotations); - - // Terminal check - match block.kind { - BlockKind::Return => { - let id = next_path_id; - next_path_id += 1; - paths.push(ExecutionPath { - id, - nodes: path, - terminal: TerminalKind::Return, - annotations, - depth, - }); - continue; - } - BlockKind::Revert => { - let id = next_path_id; - next_path_id += 1; - paths.push(ExecutionPath { - id, - nodes: path, - terminal: TerminalKind::Revert, - annotations, - depth, - }); - continue; - } - _ => {} - } - - // Get outgoing edges and push successors onto stack - let edges: Vec<_> = cfg - .edges_directed(state.node, Direction::Outgoing) - .map(|e| (e.target(), e.weight().clone())) - .collect(); - - if edges.is_empty() { - // Entry with no edges = interface/abstract function (no body) → skip - if block.kind == BlockKind::Entry && block.statements.is_empty() { - continue; - } - // Dead end in a normal block → treat as implicit return - let id = next_path_id; - next_path_id += 1; - paths.push(ExecutionPath { - id, - nodes: path, - terminal: TerminalKind::Return, - annotations, - depth, - }); - continue; - } - - // For each outgoing edge, clone the state and push - for (i, (target, edge)) in edges.into_iter().enumerate() { - let (p, a, lc) = if i == 0 { - // First edge reuses the original (avoids one clone) - (path.clone(), annotations.clone(), loop_counts.clone()) - } else { - (path.clone(), annotations.clone(), loop_counts.clone()) - }; - stack.push(WalkState { - node: target, - path: p, - annotations: a, - depth: depth + 1, - loop_counts: lc, - edge_taken: Some(edge), - }); - } - } - - let revert_paths = paths.iter().filter(|p| p.terminal == TerminalKind::Revert).count(); - let happy_paths = paths.iter().filter(|p| p.terminal == TerminalKind::Return).count(); - - PathTree { - contract: contract_name.to_string(), - function: function_name.to_string(), - paths, - stats: PathTreeStats { - total_paths: next_path_id, - paths_pruned, - max_depth_reached, - revert_paths, - happy_paths, - }, - } -} - -/// Normalize a state-var access path by collapsing array indices to `[]` -/// while preserving struct/field accesses. E.g. -/// `proposals[id].executed` -> `proposals[].executed`, -/// `balances[msg.sender]` -> `balances[]`. -fn normalize_path(target: &str) -> String { - let mut out = String::with_capacity(target.len()); - let mut depth = 0i32; - for ch in target.chars() { - match ch { - '[' => { - if depth == 0 { - out.push('['); - } - depth += 1; - } - ']' => { - depth -= 1; - if depth == 0 { - out.push(']'); - } - } - _ if depth > 0 => {} - _ => out.push(ch), - } - } - out -} - -/// Check whether an expression mentions a variable name as an identifier -/// (not as a substring of a larger identifier). -fn expression_mentions_var(expr: &str, name: &str) -> bool { - if name.is_empty() { - return false; - } - let bytes = expr.as_bytes(); - let nb = name.as_bytes(); - let mut i = 0; - while i + nb.len() <= bytes.len() { - if &bytes[i..i + nb.len()] == nb { - let before_ok = i == 0 || !is_ident_char(bytes[i - 1]); - let after_idx = i + nb.len(); - let after_ok = after_idx >= bytes.len() || !is_ident_char(bytes[after_idx]); - if before_ok && after_ok { - return true; - } - } - i += 1; - } - false -} - -fn is_ident_char(b: u8) -> bool { - b.is_ascii_alphanumeric() || b == b'_' -} - -/// Extract qualified state-var paths from an expression for a known var name. -/// Returns normalized forms like `proposals[].executed` for matches. -fn extract_qualified_paths(expr: &str, name: &str) -> Vec<String> { - let mut out = Vec::new(); - if name.is_empty() { - return out; - } - let bytes = expr.as_bytes(); - let nb = name.as_bytes(); - let mut i = 0; - while i + nb.len() <= bytes.len() { - if &bytes[i..i + nb.len()] == nb { - let before_ok = i == 0 || !is_ident_char(bytes[i - 1]); - let after_idx = i + nb.len(); - let after_ok = after_idx >= bytes.len() || !is_ident_char(bytes[after_idx]); - if before_ok && after_ok { - // Walk forward consuming `[...]` and `.field` chains. - let mut end = after_idx; - loop { - if end < bytes.len() && bytes[end] == b'[' { - let mut depth = 1i32; - end += 1; - while end < bytes.len() && depth > 0 { - match bytes[end] { - b'[' => depth += 1, - b']' => depth -= 1, - _ => {} - } - end += 1; - } - continue; - } - if end < bytes.len() && bytes[end] == b'.' { - let mut j = end + 1; - while j < bytes.len() && is_ident_char(bytes[j]) { - j += 1; - } - if j > end + 1 { - end = j; - continue; - } - } - break; - } - let raw = &expr[i..end]; - let norm = normalize_path(raw); - if !out.contains(&norm) { - out.push(norm); - } - i = end; - continue; - } - } - i += 1; - } - out -} - -/// Scan a block's statements and accumulate annotations. -fn collect_annotations( - statements: &[CfgStatement], - state_vars: &[StateVar], - annotations: &mut PathAnnotations, -) { - for stmt in statements { - match stmt { - CfgStatement::ExternalCall { target, function, .. } => { - annotations.external_calls.push(ExternalCallInfo { - target: target.clone(), - function: function.clone(), - }); - } - CfgStatement::InternalCall { function, .. } => { - annotations.internal_calls.push(function.clone()); - } - CfgStatement::EmitEvent { event, .. } => { - annotations.events_emitted.push(event.clone()); - } - CfgStatement::EthTransfer { to, .. } => { - annotations.eth_transfers.push(to.clone()); - } - CfgStatement::RequireCheck { condition, .. } => { - annotations.require_checks.push(condition.clone()); - for sv in state_vars { - if expression_mentions_var(condition, &sv.name) { - if !annotations.state_reads.contains(&sv.name) { - annotations.state_reads.push(sv.name.clone()); - } - for path in extract_qualified_paths(condition, &sv.name) { - if !annotations.state_read_paths.contains(&path) { - annotations.state_read_paths.push(path); - } - } - } - } - } - CfgStatement::AssertCheck { condition, .. } => { - annotations.require_checks.push(format!("assert: {condition}")); - } - CfgStatement::AssemblyBlock { .. } => { - annotations.has_assembly = true; - } - CfgStatement::Assignment { target, value, .. } => { - let base_name = crate::util::target_base_name(target); - if state_vars.iter().any(|sv| sv.name == base_name) { - annotations.state_writes.push(target.clone()); - let normalized = normalize_path(target); - if !annotations.state_write_paths.contains(&normalized) { - annotations.state_write_paths.push(normalized); - } - } - // Detect state reads in the value expression and target - for sv in state_vars { - if expression_mentions_var(value, &sv.name) { - if !annotations.state_reads.contains(&sv.name) { - annotations.state_reads.push(sv.name.clone()); - } - for path in extract_qualified_paths(value, &sv.name) { - if !annotations.state_read_paths.contains(&path) { - annotations.state_read_paths.push(path); - } - } - } - } - } - CfgStatement::StateWrite { variable, .. } => { - annotations.state_writes.push(variable.clone()); - let normalized = normalize_path(variable); - if !annotations.state_write_paths.contains(&normalized) { - annotations.state_write_paths.push(normalized); - } - } - CfgStatement::StateRead { variable, .. } => { - annotations.state_reads.push(variable.clone()); - let normalized = normalize_path(variable); - if !annotations.state_read_paths.contains(&normalized) { - annotations.state_read_paths.push(normalized); - } - } - } - } -} diff --git a/crates/ilold-core/src/sequence/analysis.rs b/crates/ilold-core/src/sequence/analysis.rs deleted file mode 100644 index 2527262..0000000 --- a/crates/ilold-core/src/sequence/analysis.rs +++ /dev/null @@ -1,457 +0,0 @@ -use std::collections::{HashMap, HashSet}; - -use serde::{Deserialize, Serialize}; - -use crate::model::contract::ContractDef; -use crate::model::project::Project; -use crate::pathtree::types::{PathTree, TerminalKind}; -use crate::util::is_type_cast; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct FunctionBehavior { - pub name: String, - pub preconditions: Vec<String>, - pub state_writes: Vec<String>, - pub state_reads: Vec<String>, - /// Fully qualified, normalized write paths (e.g. `proposals[].executed`). - #[serde(default)] - pub state_write_paths: Vec<String>, - #[serde(default)] - pub state_read_paths: Vec<String>, - pub external_calls: Vec<String>, - #[serde(default)] - pub internal_calls: Vec<String>, - pub events: Vec<String>, - pub can_revert: bool, - pub always_reverts: bool, - pub read_only: bool, - - #[serde(default)] - pub transitive_state_writes: Vec<String>, - #[serde(default)] - pub transitive_state_reads: Vec<String>, - #[serde(default)] - pub transitive_state_write_paths: Vec<String>, - #[serde(default)] - pub transitive_state_read_paths: Vec<String>, - #[serde(default)] - pub transitive_external_calls: Vec<String>, - #[serde(default)] - pub transitive_events: Vec<String>, - #[serde(default)] - pub transitive_internal_calls: Vec<String>, -} - -impl FunctionBehavior { - /// Returns direct + transitive state writes (base names) - pub fn effective_state_writes(&self) -> Vec<String> { - let mut out: Vec<String> = self.state_writes.clone(); - for w in &self.transitive_state_writes { - if !out.contains(w) { out.push(w.clone()); } - } - out.sort(); - out - } - - pub fn effective_state_reads(&self) -> Vec<String> { - let mut out: Vec<String> = self.state_reads.clone(); - for r in &self.transitive_state_reads { - if !out.contains(r) { out.push(r.clone()); } - } - out.sort(); - out - } - - pub fn effective_state_write_paths(&self) -> Vec<String> { - let mut out: Vec<String> = self.state_write_paths.clone(); - for p in &self.transitive_state_write_paths { - if !out.contains(p) { out.push(p.clone()); } - } - out.sort(); - out - } - - pub fn effective_state_read_paths(&self) -> Vec<String> { - let mut out: Vec<String> = self.state_read_paths.clone(); - for p in &self.transitive_state_read_paths { - if !out.contains(p) { out.push(p.clone()); } - } - out.sort(); - out - } - - pub fn effective_external_calls(&self) -> Vec<String> { - let mut out: Vec<String> = self.external_calls.clone(); - for c in &self.transitive_external_calls { - if !out.contains(c) { out.push(c.clone()); } - } - out.sort(); - out - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct TransitionInfo { - pub from: String, - pub to: String, - pub shared_state: Vec<String>, - pub conditions_affected: Vec<String>, - pub has_external_in_from: bool, - pub has_external_in_to: bool, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct SequenceAnalysis { - pub functions: Vec<FunctionBehavior>, - pub transitions: Vec<TransitionInfo>, -} - -pub fn analyze_sequences( - path_trees: &HashMap<(String, String), PathTree>, - contract_name: &str, -) -> SequenceAnalysis { - let mut behaviors: Vec<FunctionBehavior> = Vec::new(); - - for ((c, f), pt) in path_trees { - if c != contract_name || f.is_empty() { continue; } - - let mut preconditions = HashSet::new(); - let mut state_writes = HashSet::new(); - let mut state_reads = HashSet::new(); - let mut state_write_paths = HashSet::new(); - let mut state_read_paths = HashSet::new(); - let mut external_calls = HashSet::new(); - let mut internal_calls = HashSet::new(); - let mut events = HashSet::new(); - let mut has_return = false; - let mut has_revert = false; - - for path in &pt.paths { - for check in &path.annotations.require_checks { - preconditions.insert(check.clone()); - } - for write in &path.annotations.state_writes { - let base = write - .split(|c| c == '[' || c == '.') - .next() - .unwrap_or(write) - .to_string(); - state_writes.insert(base); - } - for write in &path.annotations.state_write_paths { - state_write_paths.insert(write.clone()); - } - for read in &path.annotations.state_reads { - let base = read - .split(|c| c == '[' || c == '.') - .next() - .unwrap_or(read) - .to_string(); - state_reads.insert(base); - } - for read in &path.annotations.state_read_paths { - state_read_paths.insert(read.clone()); - } - for call in &path.annotations.external_calls { - external_calls.insert(format!("{}.{}", call.target, call.function)); - } - for ic in &path.annotations.internal_calls { - internal_calls.insert(ic.clone()); - } - for event in &path.annotations.events_emitted { - events.insert(event.clone()); - } - match path.terminal { - TerminalKind::Return => has_return = true, - TerminalKind::Revert => has_revert = true, - _ => {} - } - } - - let read_only = state_writes.is_empty() && external_calls.is_empty(); - - let mut state_writes: Vec<String> = state_writes.into_iter().collect(); - state_writes.sort(); - let mut state_reads: Vec<String> = state_reads.into_iter().collect(); - state_reads.sort(); - let mut state_write_paths: Vec<String> = state_write_paths.into_iter().collect(); - state_write_paths.sort(); - let mut state_read_paths: Vec<String> = state_read_paths.into_iter().collect(); - state_read_paths.sort(); - let mut external_calls: Vec<String> = external_calls.into_iter().collect(); - external_calls.sort(); - let mut internal_calls: Vec<String> = internal_calls.into_iter().collect(); - internal_calls.sort(); - let mut preconditions: Vec<String> = preconditions.into_iter().collect(); - preconditions.sort(); - let mut events: Vec<String> = events.into_iter().collect(); - events.sort(); - - behaviors.push(FunctionBehavior { - name: f.clone(), - preconditions, - state_writes, - state_reads, - state_write_paths, - state_read_paths, - external_calls, - internal_calls, - events, - can_revert: has_revert, - always_reverts: !has_return && has_revert, - read_only, - transitive_state_writes: Vec::new(), - transitive_state_reads: Vec::new(), - transitive_state_write_paths: Vec::new(), - transitive_state_read_paths: Vec::new(), - transitive_external_calls: Vec::new(), - transitive_events: Vec::new(), - transitive_internal_calls: Vec::new(), - }); - } - - behaviors.sort_by(|a, b| a.name.cmp(&b.name)); - - // For each pair A→B, describe what conditions connect them - let mut transitions = Vec::new(); - - for a in &behaviors { - for b in &behaviors { - let shared: Vec<String> = a.state_writes.iter() - .filter(|w| b.state_reads.contains(*w) || b.state_writes.contains(*w)) - .cloned() - .collect(); - - let mut conditions_affected: Vec<String> = Vec::new(); - for w in &a.state_writes { - for p in &b.preconditions { - if p.contains(w.as_str()) { - conditions_affected.push(format!("{} writes '{}' → {} needs require({})", a.name, w, b.name, p)); - } - } - } - - // Only include transitions that have something interesting - if !shared.is_empty() || !conditions_affected.is_empty() || !a.external_calls.is_empty() || !b.external_calls.is_empty() { - transitions.push(TransitionInfo { - from: a.name.clone(), - to: b.name.clone(), - shared_state: shared, - conditions_affected, - has_external_in_from: !a.external_calls.is_empty(), - has_external_in_to: !b.external_calls.is_empty(), - }); - } - } - } - - SequenceAnalysis { - functions: behaviors, - transitions, - } -} - -// ============================================================================ -// Project-level transitive effect computation -// ============================================================================ - -pub fn analyze_project( - project: &Project, - analyses: &mut HashMap<String, SequenceAnalysis>, -) { - let all_funcs: Vec<(String, String)> = analyses - .iter() - .flat_map(|(c, sa)| sa.functions.iter().map(move |b| (c.clone(), b.name.clone()))) - .collect(); - - // Read-only snapshot so we can mutate `analyses` in place. - let snapshot: HashMap<String, SequenceAnalysis> = analyses.clone(); - - for (contract_name, func_name) in all_funcs { - let root_contract = match project.contracts.iter().find(|c| c.name == contract_name) { - Some(c) => c, - None => continue, - }; - - let root_behavior = match snapshot - .get(&contract_name) - .and_then(|sa| sa.functions.iter().find(|b| b.name == func_name)) - { - Some(b) => b, - None => continue, - }; - - let effects = collect_transitive_for_function( - root_behavior, - root_contract, - project, - &snapshot, - ); - - if let Some(sa) = analyses.get_mut(&contract_name) { - if let Some(b) = sa.functions.iter_mut().find(|b| b.name == func_name) { - b.transitive_state_writes = effects.state_writes; - b.transitive_state_reads = effects.state_reads; - b.transitive_state_write_paths = effects.state_write_paths; - b.transitive_state_read_paths = effects.state_read_paths; - b.transitive_external_calls = effects.external_calls; - b.transitive_events = effects.events; - b.transitive_internal_calls = effects.internal_calls; - } - } - } -} - -struct TransitiveSets { - state_writes: Vec<String>, - state_reads: Vec<String>, - state_write_paths: Vec<String>, - state_read_paths: Vec<String>, - external_calls: Vec<String>, - events: Vec<String>, - internal_calls: Vec<String>, -} - -fn collect_transitive_for_function( - root: &FunctionBehavior, - root_contract: &ContractDef, - project: &Project, - all: &HashMap<String, SequenceAnalysis>, -) -> TransitiveSets { - let mut writes: HashSet<String> = HashSet::new(); - let mut reads: HashSet<String> = HashSet::new(); - let mut write_paths: HashSet<String> = HashSet::new(); - let mut read_paths: HashSet<String> = HashSet::new(); - let mut external: HashSet<String> = HashSet::new(); - let mut events: HashSet<String> = HashSet::new(); - let mut internal: HashSet<String> = HashSet::new(); - - let mut visited: HashSet<(String, String)> = HashSet::new(); - let mut stack: Vec<(String, String)> = root - .internal_calls - .iter() - .filter(|c| !is_type_cast(c)) - .map(|c| (root_contract.name.clone(), c.clone())) - .collect(); - - while let Some((ctx_name, callee_name)) = stack.pop() { - let ctx = match project.contract_index.get(&ctx_name) { - Some(&i) => &project.contracts[i], - None => continue, - }; - - let (callee_behavior, owning) = match resolve_internal_callee(&callee_name, ctx, project, all) { - Some(x) => x, - None => continue, - }; - - let key = (owning.clone(), callee_name.clone()); - if !visited.insert(key) { - continue; - } - - for w in &callee_behavior.state_writes { - if !root.state_writes.contains(w) { - writes.insert(w.clone()); - } - } - for r in &callee_behavior.state_reads { - if !root.state_reads.contains(r) { - reads.insert(r.clone()); - } - } - for p in &callee_behavior.state_write_paths { - if !root.state_write_paths.contains(p) { - write_paths.insert(p.clone()); - } - } - for p in &callee_behavior.state_read_paths { - if !root.state_read_paths.contains(p) { - read_paths.insert(p.clone()); - } - } - for c in &callee_behavior.external_calls { - if !root.external_calls.contains(c) { - external.insert(c.clone()); - } - } - for e in &callee_behavior.events { - if !root.events.contains(e) { - events.insert(e.clone()); - } - } - internal.insert(callee_name.clone()); - - for ic in &callee_behavior.internal_calls { - if is_type_cast(ic) { - continue; - } - stack.push((owning.clone(), ic.clone())); - } - } - - fn to_sorted(s: HashSet<String>) -> Vec<String> { - let mut v: Vec<String> = s.into_iter().collect(); - v.sort(); - v - } - - TransitiveSets { - state_writes: to_sorted(writes), - state_reads: to_sorted(reads), - state_write_paths: to_sorted(write_paths), - state_read_paths: to_sorted(read_paths), - external_calls: to_sorted(external), - events: to_sorted(events), - internal_calls: to_sorted(internal), - } -} - -/// Resolve an internal callee name to its `FunctionBehavior`, walking the -/// inheritance chain of `starting_contract`. Returns the behavior plus the -/// name of the contract that owns the resolved function. -pub fn resolve_internal_callee<'a>( - callee: &str, - starting_contract: &'a ContractDef, - project: &'a Project, - all: &'a HashMap<String, SequenceAnalysis>, -) -> Option<(&'a FunctionBehavior, String)> { - // 1. Try starting_contract first - if let Some(sa) = all.get(&starting_contract.name) { - if let Some(b) = sa.functions.iter().find(|b| b.name == callee) { - return Some((b, starting_contract.name.clone())); - } - } - - // 2. Walk the inheritance chain (BFS) - let mut visited: HashSet<String> = HashSet::new(); - let mut queue: Vec<&str> = starting_contract - .inherits - .iter() - .map(|s| s.as_str()) - .collect(); - - while let Some(parent_name) = queue.pop() { - if !visited.insert(parent_name.to_string()) { - continue; - } - - let parent_idx = match project.contract_index.get(parent_name) { - Some(&i) => i, - None => continue, - }; - let parent = &project.contracts[parent_idx]; - - if let Some(sa) = all.get(&parent.name) { - if let Some(b) = sa.functions.iter().find(|b| b.name == callee) { - return Some((b, parent.name.clone())); - } - } - - for grand in &parent.inherits { - queue.push(grand.as_str()); - } - } - - None -} diff --git a/crates/ilold-core/src/sequence/builder.rs b/crates/ilold-core/src/sequence/builder.rs deleted file mode 100644 index 5322f21..0000000 --- a/crates/ilold-core/src/sequence/builder.rs +++ /dev/null @@ -1,75 +0,0 @@ -use crate::model::contract::ContractDef; -use crate::model::function::{FunctionKind, Mutability}; -use crate::pathtree::types::PathTree; - -use super::types::*; - -pub fn build_sequence_tree( - contract: &ContractDef, - path_trees: &[PathTree], - max_depth: usize, -) -> SequenceTree { - // Collect public/external functions, excluding constructors (REQ-SEQ-6) - let functions: Vec<SequenceFunction> = contract - .functions - .iter() - .filter(|f| f.kind != FunctionKind::Constructor) - .filter(|f| matches!(f.visibility, crate::model::function::Visibility::Public | crate::model::function::Visibility::External)) - .map(|f| { - let path_count = path_trees - .iter() - .find(|pt| pt.function == f.name) - .map(|pt| pt.stats.total_paths) - .unwrap_or(0); - - SequenceFunction { - name: f.name.clone(), - visibility: f.visibility, - read_only: matches!(f.mutability, Mutability::Pure | Mutability::View), - path_count, - } - }) - .collect(); - - let n = functions.len(); - let mut sequences: Vec<TransactionSequence> = Vec::new(); - - // Depth 1: each function alone - for i in 0..n { - sequences.push(TransactionSequence { - steps: vec![i], - depth: 1, - path_count: functions[i].path_count as u64, - has_state_change: !functions[i].read_only, - }); - } - - // Depth 2..D: extend previous depth by appending each function - for d in 2..=max_depth { - let prev: Vec<TransactionSequence> = sequences - .iter() - .filter(|s| s.depth == d - 1) - .cloned() - .collect(); - - for seq in &prev { - for i in 0..n { - let mut steps = seq.steps.clone(); - steps.push(i); - sequences.push(TransactionSequence { - steps, - depth: d, - path_count: seq.path_count * functions[i].path_count as u64, - has_state_change: seq.has_state_change || !functions[i].read_only, - }); - } - } - } - - SequenceTree { - contract: contract.name.clone(), - functions, - sequences, - max_depth, - } -} diff --git a/crates/ilold-core/src/sequence/mod.rs b/crates/ilold-core/src/sequence/mod.rs deleted file mode 100644 index 9d7948f..0000000 --- a/crates/ilold-core/src/sequence/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod analysis; -pub mod builder; -pub mod types; diff --git a/crates/ilold-core/src/sequence/types.rs b/crates/ilold-core/src/sequence/types.rs deleted file mode 100644 index fa649da..0000000 --- a/crates/ilold-core/src/sequence/types.rs +++ /dev/null @@ -1,33 +0,0 @@ -use serde::{Deserialize, Serialize}; - -use crate::model::function::Visibility; - -/// All possible function call sequences for a contract. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct SequenceTree { - pub contract: String, - pub functions: Vec<SequenceFunction>, - pub sequences: Vec<TransactionSequence>, - pub max_depth: usize, -} - -/// A callable function in the sequence tree. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct SequenceFunction { - pub name: String, - pub visibility: Visibility, - pub read_only: bool, - pub path_count: usize, -} - -/// One specific ordering of function calls (e.g., deposit → withdraw → claim). -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct TransactionSequence { - /// Indices into SequenceTree.functions - pub steps: Vec<usize>, - pub depth: usize, - /// Product of each step's path_count - pub path_count: u64, - /// false only if ALL steps are read_only - pub has_state_change: bool, -} diff --git a/crates/ilold-core/src/slicing/backward.rs b/crates/ilold-core/src/slicing/backward.rs deleted file mode 100644 index 8f832d2..0000000 --- a/crates/ilold-core/src/slicing/backward.rs +++ /dev/null @@ -1,85 +0,0 @@ -use std::collections::{BTreeSet, HashSet}; - -use super::extract::extract_statement_uses_defs; -use super::flatten::FlatStatement; -use super::types::StatementPath; - -/// Intraprocedural backward dataflow slice for `variable`. -/// -/// Iterates to a fixed point over the flattened body: -/// 1. Any statement whose def set intersects the `relevant` set is added -/// to the slice and its uses are merged into `relevant`. -/// 2. Every included statement drags in its lexical ancestors -/// (If / For / While / Block / TryCatch wrappers) and *their* uses -/// are merged into `relevant` — that is how `If (c)` conditions pull -/// the data deps of `c` into the backward slice. -/// The loop re-runs until neither set grows, so transitive and -/// control-dependency-driven propagation are both handled. -/// -/// Returns the selected paths in program order. -pub fn backward_slice(flat: &[FlatStatement<'_>], variable: &str) -> Vec<StatementPath> { - let mut relevant: HashSet<String> = HashSet::new(); - relevant.insert(variable.to_string()); - - let mut included: BTreeSet<usize> = BTreeSet::new(); - - loop { - let mut changed = false; - - for i in (0..flat.len()).rev() { - if included.contains(&i) { - continue; - } - let (uses, defs) = extract_statement_uses_defs(flat[i].statement); - if defs.is_disjoint(&relevant) { - continue; - } - included.insert(i); - changed = true; - for u in uses { - if relevant.insert(u) { - changed = true; - } - } - } - - if add_ancestors_with_uses(flat, &mut included, &mut relevant) { - changed = true; - } - - if !changed { - break; - } - } - - included.into_iter().map(|i| flat[i].path.clone()).collect() -} - -/// Walk up the parent chain of every currently-included statement and -/// add every enclosing control-flow statement to the set, plus fold that -/// ancestor's own uses into `relevant`. Returns `true` if either set -/// grew — callers use that as a "re-run fixed-point pass" signal. -pub(super) fn add_ancestors_with_uses( - flat: &[FlatStatement<'_>], - included: &mut BTreeSet<usize>, - relevant: &mut HashSet<String>, -) -> bool { - let mut changed = false; - let seeds: Vec<usize> = included.iter().copied().collect(); - for idx in seeds { - let mut cur = flat[idx].parent; - while let Some(p) = cur { - if included.insert(p) { - changed = true; - let (uses, _defs) = extract_statement_uses_defs(flat[p].statement); - for u in uses { - if relevant.insert(u) { - changed = true; - } - } - } - cur = flat[p].parent; - } - } - changed -} diff --git a/crates/ilold-core/src/slicing/extract.rs b/crates/ilold-core/src/slicing/extract.rs deleted file mode 100644 index fb84eea..0000000 --- a/crates/ilold-core/src/slicing/extract.rs +++ /dev/null @@ -1,267 +0,0 @@ -use std::collections::HashSet; - -use crate::model::expression::{Expression, ExpressionKind}; -use crate::model::statement::{Statement, StatementKind}; - -/// Walk an expression tree and collect every `Identifier` leaf name. -/// Used to build the "uses" set of a statement. -pub fn walk_expr_identifiers(expr: &Expression, out: &mut HashSet<String>) { - match &expr.kind { - ExpressionKind::Identifier { name } => { - out.insert(name.clone()); - } - ExpressionKind::MemberAccess { object, .. } => { - walk_expr_identifiers(object, out); - } - ExpressionKind::IndexAccess { base, index } => { - walk_expr_identifiers(base, out); - if let Some(idx) = index { - walk_expr_identifiers(idx, out); - } - } - ExpressionKind::BinaryOp { left, right, .. } => { - walk_expr_identifiers(left, out); - walk_expr_identifiers(right, out); - } - ExpressionKind::UnaryOp { operand, .. } => { - walk_expr_identifiers(operand, out); - } - ExpressionKind::FunctionCall { callee, arguments } => { - // The callee may itself mention identifiers (e.g. `someFn`, or - // `obj.method` — we care about `obj`). Skip the callee if it's - // just a plain Identifier that names the function being called, - // since that's not a data read. - match &callee.kind { - ExpressionKind::Identifier { .. } => { - // Pure function name — not a data use. - } - _ => walk_expr_identifiers(callee, out), - } - for arg in arguments { - walk_expr_identifiers(arg, out); - } - } - ExpressionKind::Assignment { target, value, .. } => { - // Used when an assignment is part of a bigger expression. The - // target's side effects (e.g. `a[i] = x` reads `i`) need to be - // captured; the value is a normal use. - walk_assignment_target_uses(target, out); - walk_expr_identifiers(value, out); - } - ExpressionKind::Ternary { condition, true_expr, false_expr } => { - walk_expr_identifiers(condition, out); - walk_expr_identifiers(true_expr, out); - walk_expr_identifiers(false_expr, out); - } - ExpressionKind::TypeCast { expression, .. } => { - walk_expr_identifiers(expression, out); - } - ExpressionKind::New { arguments, .. } => { - for arg in arguments { - walk_expr_identifiers(arg, out); - } - } - ExpressionKind::Literal { .. } | ExpressionKind::TypeMeta { .. } => {} - } -} - -/// Collect the USE identifiers inside an assignment target. `x = …` has -/// no uses on the target, but `arr[i] = …` uses `i`, and `obj.field = …` -/// uses `obj`. The base identifier itself (`x`, `arr`, `obj`) is the DEF -/// and is NOT added here. -fn walk_assignment_target_uses(target: &Expression, out: &mut HashSet<String>) { - match &target.kind { - ExpressionKind::Identifier { .. } => { - // The target identifier is the def, not a use. - } - ExpressionKind::IndexAccess { base, index } => { - walk_assignment_target_uses(base, out); - if let Some(idx) = index { - walk_expr_identifiers(idx, out); - } - } - ExpressionKind::MemberAccess { object, .. } => { - walk_assignment_target_uses(object, out); - } - _ => { - // Anything else on the LHS is unusual (tuple? deref?) — treat - // sub-expressions as uses to stay safe. - walk_expr_identifiers(target, out); - } - } -} - -/// Collect the DEF identifier of an assignment target. Returns the base -/// variable name being written (e.g. `balances` for `balances[to] = x`). -pub fn extract_assignment_def(target: &Expression) -> Option<String> { - match &target.kind { - ExpressionKind::Identifier { name } => Some(name.clone()), - ExpressionKind::IndexAccess { base, .. } => extract_assignment_def(base), - ExpressionKind::MemberAccess { object, .. } => extract_assignment_def(object), - _ => None, - } -} - -/// Use/def sets for a single `Statement`. Does NOT recurse into nested -/// statements — callers handle block walking separately so they can -/// associate sub-statements with their own paths. -/// -/// For statements that only introduce control flow (If/For/While), the -/// returned uses are the condition's uses; defs is empty. -pub fn extract_statement_uses_defs(stmt: &Statement) -> (HashSet<String>, HashSet<String>) { - let mut uses: HashSet<String> = HashSet::new(); - let mut defs: HashSet<String> = HashSet::new(); - - match &stmt.kind { - StatementKind::ExpressionStmt { expression } => { - match &expression.kind { - ExpressionKind::Assignment { target, value, .. } => { - if let Some(def) = extract_assignment_def(target) { - defs.insert(def); - } - walk_assignment_target_uses(target, &mut uses); - walk_expr_identifiers(value, &mut uses); - } - _ => walk_expr_identifiers(expression, &mut uses), - } - } - StatementKind::VariableDeclaration { name, initial_value, .. } => { - defs.insert(name.clone()); - if let Some(val) = initial_value { - walk_expr_identifiers(val, &mut uses); - } - } - StatementKind::If { condition, .. } => { - walk_expr_identifiers(condition, &mut uses); - } - StatementKind::For { init: _, condition, increment, .. } => { - // `init` is a Statement of its own and will be handled as a - // separate flat entry; same for the body. Increment is an - // Expression evaluated each iteration — its uses count for - // the loop header. - if let Some(c) = condition { - walk_expr_identifiers(c, &mut uses); - } - if let Some(inc) = increment { - walk_expr_identifiers(inc, &mut uses); - } - } - StatementKind::While { condition, .. } | StatementKind::DoWhile { condition, .. } => { - walk_expr_identifiers(condition, &mut uses); - } - StatementKind::Return { value } => { - if let Some(v) = value { - walk_expr_identifiers(v, &mut uses); - } - } - StatementKind::Emit { arguments, .. } => { - for arg in arguments { - walk_expr_identifiers(arg, &mut uses); - } - } - StatementKind::Revert { arguments, .. } => { - for arg in arguments { - walk_expr_identifiers(arg, &mut uses); - } - } - StatementKind::TryCatch { expression, .. } => { - walk_expr_identifiers(expression, &mut uses); - } - StatementKind::Block { .. } - | StatementKind::UncheckedBlock { .. } - | StatementKind::Assembly { .. } - | StatementKind::Placeholder - | StatementKind::Continue - | StatementKind::Break => {} - } - - (uses, defs) -} - -/// Render a statement as a short human-readable string for slice output. -/// This is NOT a full Solidity pretty-printer — just enough to identify -/// the statement in a slice result. -pub fn statement_text(stmt: &Statement) -> String { - match &stmt.kind { - StatementKind::ExpressionStmt { expression } => expr_to_text(expression), - StatementKind::VariableDeclaration { name, type_name, initial_value } => { - match initial_value { - Some(v) => format!("{} {} = {}", type_name, name, expr_to_text(v)), - None => format!("{} {}", type_name, name), - } - } - StatementKind::If { condition, .. } => format!("if ({}) {{ … }}", expr_to_text(condition)), - StatementKind::For { .. } => "for (…) { … }".to_string(), - StatementKind::While { condition, .. } => format!("while ({}) {{ … }}", expr_to_text(condition)), - StatementKind::DoWhile { condition, .. } => format!("do {{ … }} while ({})", expr_to_text(condition)), - StatementKind::Return { value } => match value { - Some(v) => format!("return {}", expr_to_text(v)), - None => "return".to_string(), - }, - StatementKind::Emit { event_name, arguments } => { - let args = arguments.iter().map(expr_to_text).collect::<Vec<_>>().join(", "); - format!("emit {}({})", event_name, args) - } - StatementKind::Revert { error_name, arguments } => { - let args = arguments.iter().map(expr_to_text).collect::<Vec<_>>().join(", "); - match error_name { - Some(n) => format!("revert {}({})", n, args), - None => format!("revert({})", args), - } - } - StatementKind::TryCatch { expression, .. } => { - format!("try {} {{ … }} catch {{ … }}", expr_to_text(expression)) - } - StatementKind::Block { .. } => "{ … }".to_string(), - StatementKind::UncheckedBlock { .. } => "unchecked { … }".to_string(), - StatementKind::Assembly { .. } => "assembly { … }".to_string(), - StatementKind::Placeholder => "_".to_string(), - StatementKind::Continue => "continue".to_string(), - StatementKind::Break => "break".to_string(), - } -} - -/// Minimal expression-to-text for slice entries. Keeps the output -/// compact — full fidelity is not the goal. -fn expr_to_text(expr: &Expression) -> String { - match &expr.kind { - ExpressionKind::Identifier { name } => name.clone(), - ExpressionKind::Literal { value, .. } => value.clone(), - ExpressionKind::MemberAccess { object, member } => { - format!("{}.{}", expr_to_text(object), member) - } - ExpressionKind::IndexAccess { base, index } => { - let idx = index.as_ref().map(|e| expr_to_text(e)).unwrap_or_default(); - format!("{}[{}]", expr_to_text(base), idx) - } - ExpressionKind::BinaryOp { left, operator, right } => { - format!("{} {} {}", expr_to_text(left), operator.as_str(), expr_to_text(right)) - } - ExpressionKind::UnaryOp { operator, operand } => { - let (sym, postfix) = operator.format_parts(); - if postfix { - format!("{}{}", expr_to_text(operand), sym) - } else { - format!("{}{}", sym, expr_to_text(operand)) - } - } - ExpressionKind::FunctionCall { callee, arguments } => { - let args = arguments.iter().map(expr_to_text).collect::<Vec<_>>().join(", "); - format!("{}({})", expr_to_text(callee), args) - } - ExpressionKind::Assignment { target, operator, value } => { - format!("{} {} {}", expr_to_text(target), operator.as_str(), expr_to_text(value)) - } - ExpressionKind::Ternary { condition, true_expr, false_expr } => { - format!("{} ? {} : {}", expr_to_text(condition), expr_to_text(true_expr), expr_to_text(false_expr)) - } - ExpressionKind::TypeCast { type_name, expression } => { - format!("{}({})", type_name, expr_to_text(expression)) - } - ExpressionKind::TypeMeta { type_name } => format!("type({})", type_name), - ExpressionKind::New { type_name, arguments } => { - let args = arguments.iter().map(expr_to_text).collect::<Vec<_>>().join(", "); - format!("new {}({})", type_name, args) - } - } -} diff --git a/crates/ilold-core/src/slicing/flatten.rs b/crates/ilold-core/src/slicing/flatten.rs deleted file mode 100644 index cbb395c..0000000 --- a/crates/ilold-core/src/slicing/flatten.rs +++ /dev/null @@ -1,164 +0,0 @@ -use crate::model::contract::ContractDef; -use crate::model::function::FunctionDef; -use crate::model::project::Project; -use crate::model::statement::{Statement, StatementKind}; - -use super::types::{StatementOrigin, StatementPath}; - -/// A single statement in the flattened walk of a function body, carrying -/// its position in program order, nesting depth, a back-link to its -/// lexical parent (another `FlatStatement` index) when nested inside -/// If/For/While/Block/TryCatch, and an `origin` tag distinguishing -/// statements lifted from the function body vs from a wrapping modifier. -pub struct FlatStatement<'a> { - pub path: StatementPath, - pub statement: &'a Statement, - pub depth: usize, - pub parent: Option<usize>, - pub origin: StatementOrigin, -} - -/// Walk a function and every applied modifier in execution order, emitting -/// one `FlatStatement` per encountered statement. -/// -/// Order: for `function f() mod1 mod2 { body }` we walk -/// `mod1.before` → `mod2.before` → `body` → `mod2.after` → `mod1.after` -/// where each modifier body is split at the first top-level `_;` -/// placeholder. Modifiers without an explicit placeholder are treated -/// entirely as `before` (over-inclusive but lossless). -/// -/// Modifier resolution walks the inheritance chain via -/// `Project::resolve_modifier`; unresolved modifiers are silently -/// skipped (the slicer should not crash on a partially-parsed project). -/// -/// Paths are single-element `[global_index]` values that match the -/// position of the entry in the returned vector — the same scheme as -/// before, just over a larger flat list. -pub fn flatten_function<'a>( - project: &'a Project, - contract: &'a ContractDef, - function: &'a FunctionDef, -) -> Vec<FlatStatement<'a>> { - let mut out: Vec<FlatStatement<'a>> = Vec::new(); - - let body: &'a [Statement] = function.body.as_deref().unwrap_or(&[]); - - // Resolve every applied modifier and split its body at the first - // top-level Placeholder. We collect the references up-front so we - // can iterate the "after" parts in reverse order at the end. - struct ModifierParts<'a> { - name: &'a str, - before: &'a [Statement], - after: &'a [Statement], - } - let mut modifier_parts: Vec<ModifierParts<'a>> = Vec::new(); - for m_ref in &function.modifiers { - let Some(m_def) = project.resolve_modifier(contract, &m_ref.name) else { - continue; - }; - let split = m_def - .body - .iter() - .position(|s| matches!(s.kind, StatementKind::Placeholder)); - let (before, after) = match split { - Some(idx) => (&m_def.body[..idx], &m_def.body[idx + 1..]), - None => (m_def.body.as_slice(), &[][..]), - }; - modifier_parts.push(ModifierParts { - name: m_def.name.as_str(), - before, - after, - }); - } - - // Walk every modifier's "before" half, in declaration order. - for parts in &modifier_parts { - let origin = StatementOrigin::Modifier(parts.name.to_string()); - for stmt in parts.before { - walk(stmt, 0, None, &origin, &mut out); - } - } - - // Walk the function body itself. - for stmt in body { - walk(stmt, 0, None, &StatementOrigin::FunctionBody, &mut out); - } - - // Walk every modifier's "after" half in reverse — innermost modifier - // unwraps first, matching Solidity's call stack semantics. - for parts in modifier_parts.iter().rev() { - let origin = StatementOrigin::Modifier(parts.name.to_string()); - for stmt in parts.after { - walk(stmt, 0, None, &origin, &mut out); - } - } - - out -} - -fn walk<'a>( - stmt: &'a Statement, - depth: usize, - parent: Option<usize>, - origin: &StatementOrigin, - out: &mut Vec<FlatStatement<'a>>, -) { - let idx = out.len(); - out.push(FlatStatement { - path: StatementPath(vec![idx]), - statement: stmt, - depth, - parent, - origin: origin.clone(), - }); - - let child_parent = Some(idx); - let child_depth = depth + 1; - - match &stmt.kind { - StatementKind::If { then_body, else_body, .. } => { - for s in then_body { - walk(s, child_depth, child_parent, origin, out); - } - if let Some(eb) = else_body { - for s in eb { - walk(s, child_depth, child_parent, origin, out); - } - } - } - StatementKind::For { init, body, .. } => { - if let Some(init_stmt) = init { - walk(init_stmt, child_depth, child_parent, origin, out); - } - for s in body { - walk(s, child_depth, child_parent, origin, out); - } - } - StatementKind::While { body, .. } | StatementKind::DoWhile { body, .. } => { - for s in body { - walk(s, child_depth, child_parent, origin, out); - } - } - StatementKind::Block { statements } | StatementKind::UncheckedBlock { statements } => { - for s in statements { - walk(s, child_depth, child_parent, origin, out); - } - } - StatementKind::TryCatch { clauses, .. } => { - for clause in clauses { - for s in &clause.body { - walk(s, child_depth, child_parent, origin, out); - } - } - } - StatementKind::ExpressionStmt { .. } - | StatementKind::VariableDeclaration { .. } - | StatementKind::Return { .. } - | StatementKind::Emit { .. } - | StatementKind::Revert { .. } - | StatementKind::Assembly { .. } - | StatementKind::Placeholder - | StatementKind::Continue - | StatementKind::Break => {} - } -} diff --git a/crates/ilold-core/src/slicing/forward.rs b/crates/ilold-core/src/slicing/forward.rs deleted file mode 100644 index b82526f..0000000 --- a/crates/ilold-core/src/slicing/forward.rs +++ /dev/null @@ -1,61 +0,0 @@ -use std::collections::{BTreeSet, HashSet}; - -use super::backward::add_ancestors_with_uses; -use super::extract::extract_statement_uses_defs; -use super::flatten::FlatStatement; -use super::types::StatementPath; - -/// Intraprocedural forward dataflow slice for `variable`. -/// -/// Iterates to a fixed point over the flattened body: -/// 1. Any statement whose use set intersects the `tainted` set is added -/// to the slice and its defs are merged into `tainted` — taint -/// spreads along data dependencies. -/// 2. Every included statement drags in its lexical ancestors so that -/// enclosing control-flow headers appear in the rendered slice. The -/// ancestor's own uses are merged into `tainted` too, which keeps -/// parity with `backward_slice` and handles nested propagation. -/// The loop re-runs until neither set grows. -/// -/// Returns the selected paths in program order. -pub fn forward_slice(flat: &[FlatStatement<'_>], variable: &str) -> Vec<StatementPath> { - let mut tainted: HashSet<String> = HashSet::new(); - tainted.insert(variable.to_string()); - - let mut included: BTreeSet<usize> = BTreeSet::new(); - - loop { - let mut changed = false; - - for i in 0..flat.len() { - if included.contains(&i) { - continue; - } - let (uses, defs) = extract_statement_uses_defs(flat[i].statement); - if uses.is_disjoint(&tainted) { - continue; - } - included.insert(i); - changed = true; - for d in defs { - if tainted.insert(d) { - changed = true; - } - } - } - - // Known: ancestor merge may over-taint — an enclosing `if (c)` - // adds `c` to the tainted set even if the sliced variable has no - // data relationship with `c`. Acceptable for v1; a precise - // control-dep analysis would require a separate CDG pass. - if add_ancestors_with_uses(flat, &mut included, &mut tainted) { - changed = true; - } - - if !changed { - break; - } - } - - included.into_iter().map(|i| flat[i].path.clone()).collect() -} diff --git a/crates/ilold-core/src/slicing/mod.rs b/crates/ilold-core/src/slicing/mod.rs deleted file mode 100644 index 627ff55..0000000 --- a/crates/ilold-core/src/slicing/mod.rs +++ /dev/null @@ -1,104 +0,0 @@ -// Intraprocedural backward + forward dataflow slicing over the model AST. -// -// Operates on `FunctionDef.body: Vec<Statement>` directly, not on the CFG. -// The CFG stringifies expressions and discards structure; we need the -// structured AST to extract variable use/def sets. -// -// Known extraction limitations (v1, documented so callers can reason -// about false negatives in the slice): -// -// * Only `Assignment` expressions produce DEFs. Solidity mutations that -// are not modelled as Assignment — `x++`, `--x`, `delete x`, -// `arr.push(v)`, `arr.pop()` — are captured as USEs of `x`/`arr` but -// NOT as DEFs. A slice on a variable mutated only through `.push()` -// will miss the mutating call as a def. -// * Tuple destructuring assignments `(a, b) = foo()` may not hit the -// top-level `Assignment` arm depending on how the frontend lowers -// them; in that case sub-expressions are treated as USEs only. -// * Function-call side effects on state are not tracked — this is an -// intraprocedural slice, not a whole-program one. -// -// These are acceptable for the audit use case (the 80% that relies on -// plain `x = …` assignments works correctly). Lifting them requires an -// effects database, which is out of scope for Phase 2b. - -pub mod backward; -pub mod extract; -pub mod flatten; -pub mod forward; -pub mod types; - -use crate::model::contract::ContractDef; -use crate::model::function::FunctionDef; -use crate::model::project::Project; - -pub use types::{SliceDirection, SliceEntry, SliceResult, StatementOrigin, StatementPath}; - -/// Compute a dataflow slice for `variable` in `function`. -/// -/// The slicer walks both the function body AND the bodies of every -/// applied modifier (resolved through the inheritance chain), so writes -/// hidden inside `nonReentrant` / `updateReward` / etc. show up in the -/// slice. Each entry carries an `origin` tag distinguishing -/// function-body statements from modifier statements. -/// -/// `Backward`: statements whose values feed into reads of `variable`. -/// `Forward`: statements whose values derive from writes of `variable`. -/// `Both`: union of backward and forward, annotated per entry. -pub fn build_slice_result( - project: &Project, - contract: &ContractDef, - function: &FunctionDef, - variable: &str, - direction: SliceDirection, -) -> SliceResult { - let flat = flatten::flatten_function(project, contract, function); - - if flat.is_empty() { - return SliceResult { - function: function.name.clone(), - variable: variable.to_string(), - direction, - backward: Vec::new(), - forward: Vec::new(), - }; - } - - let backward = if matches!(direction, SliceDirection::Backward | SliceDirection::Both) { - backward::backward_slice(&flat, variable) - } else { - Vec::new() - }; - - let forward = if matches!(direction, SliceDirection::Forward | SliceDirection::Both) { - forward::forward_slice(&flat, variable) - } else { - Vec::new() - }; - - SliceResult { - function: function.name.clone(), - variable: variable.to_string(), - direction, - backward: backward.into_iter().map(|p| entry_for(&flat, p)).collect(), - forward: forward.into_iter().map(|p| entry_for(&flat, p)).collect(), - } -} - -fn entry_for(flat: &[flatten::FlatStatement<'_>], path: StatementPath) -> SliceEntry { - let entry = flat.iter().find(|f| f.path == path); - match entry { - Some(f) => SliceEntry { - path, - span: Some(f.statement.span), - text: extract::statement_text(f.statement), - origin: f.origin.clone(), - }, - None => SliceEntry { - path, - span: None, - text: String::new(), - origin: StatementOrigin::FunctionBody, - }, - } -} diff --git a/crates/ilold-core/src/slicing/types.rs b/crates/ilold-core/src/slicing/types.rs deleted file mode 100644 index efa52a4..0000000 --- a/crates/ilold-core/src/slicing/types.rs +++ /dev/null @@ -1,75 +0,0 @@ -use serde::{Deserialize, Serialize}; - -use crate::model::common::SourceSpan; - -/// Direction of a dataflow slice. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -pub enum SliceDirection { - /// Statements whose values feed the target variable (sources → var). - Backward, - /// Statements whose values derive from the target variable (var → sinks). - Forward, - /// Both directions, reported separately in the result. - Both, -} - -/// Where a sliced statement was lifted from. The slicer walks both the -/// function body and the bodies of every applied modifier; entries carry -/// this tag so renderers can distinguish "real" function code from -/// modifier code that wraps it. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(tag = "kind", content = "name")] -pub enum StatementOrigin { - FunctionBody, - /// `name` is the modifier identifier (e.g. `updateReward`). - Modifier(String), -} - -impl Default for StatementOrigin { - fn default() -> Self { - StatementOrigin::FunctionBody - } -} - -/// Unique identifier for a statement inside a flattened function body. -/// -/// The slicing pipeline pre-walks the function body in program order and -/// assigns every statement — including nested ones inside If / For / -/// While / Block / TryCatch — a single global index. A `StatementPath` -/// wraps that index as `vec![global_index]`. We keep it as a `Vec<usize>` -/// (rather than a bare `usize`) so the serialized shape stays open for a -/// future hierarchical scheme without breaking on-disk session formats. -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] -pub struct StatementPath(pub Vec<usize>); - -impl StatementPath { - pub fn new(indices: Vec<usize>) -> Self { - StatementPath(indices) - } -} - -/// One statement in a slice result. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct SliceEntry { - pub path: StatementPath, - #[serde(default)] - pub span: Option<SourceSpan>, - /// Rendered source-like text of the statement (e.g. `reserve0 = uint112(balance0)`). - pub text: String, - /// Function-body statement vs modifier-body statement. Defaults to - /// `FunctionBody` so older serialized payloads keep deserializing. - #[serde(default)] - pub origin: StatementOrigin, -} - -/// Full slice result for a (function, variable) query. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct SliceResult { - pub function: String, - pub variable: String, - pub direction: SliceDirection, - /// Backward slice entries in program order. Empty if direction is Forward. - pub backward: Vec<SliceEntry>, - /// Forward slice entries in program order. Empty if direction is Backward. - pub forward: Vec<SliceEntry>, -} diff --git a/crates/ilold-core/src/util.rs b/crates/ilold-core/src/util.rs deleted file mode 100644 index 2185c5e..0000000 --- a/crates/ilold-core/src/util.rs +++ /dev/null @@ -1,39 +0,0 @@ -//! Shared utility helpers used across multiple analysis passes. - -/// Filter out type casts that look like function calls -/// (e.g. `IERC20(addr)`, `address(0)`, `uint256(x)`). -/// -/// Used by the call graph builder and by transitive-effect analysis -/// to avoid treating type casts as real internal calls. -pub fn is_type_cast(name: &str) -> bool { - let name = name.trim(); - // Solidity elementary types - if name.starts_with("type(") - || name.starts_with("address") - || name.starts_with("uint") - || name.starts_with("int") - || name.starts_with("bytes") - || name.starts_with("bool") - || name.starts_with("string") - { - return true; - } - // Interface type casts: starts with I + uppercase (IERC20, IUniswapV2Pair) - if name.starts_with('I') - && name.len() > 1 - && name.chars().nth(1).is_some_and(|c| c.is_uppercase()) - { - return true; - } - false -} - -/// Extract the base variable name from an assignment target expression. -/// -/// Strips any indexing (`balances[user]` → `balances`) and field access -/// (`config.fee` → `config`). Used by mutation harvesters and path -/// analyzers to match target expressions against state variable names. -pub fn target_base_name(target: &str) -> &str { - let base = target.split('[').next().unwrap_or(target); - base.split('.').next().unwrap_or(base) -} diff --git a/crates/ilold-core/tests/callgraph_test.rs b/crates/ilold-core/tests/callgraph_test.rs deleted file mode 100644 index a29de6a..0000000 --- a/crates/ilold-core/tests/callgraph_test.rs +++ /dev/null @@ -1,86 +0,0 @@ -use std::path::PathBuf; - -use ilold_core::callgraph::builder::build_call_graph; -use ilold_core::callgraph::types::CallKind; -use ilold_core::parse::solar_frontend::SolarParser; -use ilold_core::parse::ProjectParser; - -fn fixture_path(name: &str) -> PathBuf { - PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .parent().unwrap() - .parent().unwrap() - .join("tests/fixtures") - .join(name) -} - -#[test] -fn test_staking_call_graph() { - let parser = SolarParser; - let mut project = parser.parse(&[fixture_path("staking.sol")]).unwrap(); - project.rebuild_index(); - - let staking = project.contracts.iter().find(|c| c.name == "Staking").unwrap(); - let cg = build_call_graph(&project, staking); - - println!("Call graph nodes:"); - for idx in cg.node_indices() { - let node = &cg[idx]; - println!(" {}::{} (external={})", node.contract, node.function, node.is_external); - } - - println!("\nCall graph edges:"); - for edge_idx in cg.edge_indices() { - let (src, dst) = cg.edge_endpoints(edge_idx).unwrap(); - let edge = &cg[edge_idx]; - println!(" {}::{} → {}::{} ({:?}, count={})", - cg[src].contract, cg[src].function, - cg[dst].contract, cg[dst].function, - edge.kind, edge.call_count); - } - - // deposit calls stakingToken.transferFrom (external) - let has_deposit_to_transfer = cg.edge_indices().any(|e| { - let (src, dst) = cg.edge_endpoints(e).unwrap(); - cg[src].function == "deposit" - && cg[dst].function == "transferFrom" - && cg[dst].is_external - }); - assert!(has_deposit_to_transfer, "deposit should call stakingToken.transferFrom"); - - // claimRewards calls rewardToken.transfer (external) - let has_claim_to_transfer = cg.edge_indices().any(|e| { - let (src, dst) = cg.edge_endpoints(e).unwrap(); - cg[src].function == "claimRewards" - && cg[dst].function == "transfer" - && cg[dst].is_external - }); - assert!(has_claim_to_transfer, "claimRewards should call rewardToken.transfer"); - - // deposit calls rewardPerToken (internal, from inlined modifier) - let has_deposit_to_reward = cg.edge_indices().any(|e| { - let (src, dst) = cg.edge_endpoints(e).unwrap(); - cg[src].function == "deposit" - && cg[dst].function == "rewardPerToken" - }); - assert!(has_deposit_to_reward, "deposit should call rewardPerToken (from updateReward modifier)"); - - // setRewardRate has NO external calls - let set_rate_external = cg.edge_indices().any(|e| { - let (src, _) = cg.edge_endpoints(e).unwrap(); - cg[src].function == "setRewardRate" && cg[e].kind == CallKind::External - }); - assert!(!set_rate_external, "setRewardRate should have no external calls"); -} - -#[test] -fn test_simple_storage_no_calls() { - let parser = SolarParser; - let mut project = parser.parse(&[fixture_path("simple_storage.sol")]).unwrap(); - project.rebuild_index(); - - let contract = &project.contracts[0]; - let cg = build_call_graph(&project, contract); - - // SimpleStorage has no function calls — graph should have 0 edges - assert_eq!(cg.edge_count(), 0, "SimpleStorage should have no call edges"); -} diff --git a/crates/ilold-core/tests/integration_test.rs b/crates/ilold-core/tests/integration_test.rs deleted file mode 100644 index 8152af8..0000000 --- a/crates/ilold-core/tests/integration_test.rs +++ /dev/null @@ -1,108 +0,0 @@ -use std::path::PathBuf; - -use ilold_core::cfg::builder::CfgBuilder; -use ilold_core::cfg::types::BlockKind; -use ilold_core::model::function::{FunctionKind, Visibility, Mutability}; -use ilold_core::parse::solar_frontend::SolarParser; -use ilold_core::parse::ProjectParser; - -fn fixture_path(name: &str) -> PathBuf { - PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .parent() - .unwrap() - .parent() - .unwrap() - .join("tests/fixtures") - .join(name) -} - -#[test] -fn test_parse_simple_storage() { - let parser = SolarParser; - let project = parser.parse(&[fixture_path("simple_storage.sol")]).unwrap(); - - // Should have 1 contract - assert_eq!(project.contracts.len(), 1); - let contract = &project.contracts[0]; - assert_eq!(contract.name, "SimpleStorage"); - - // Should have 2 functions: get and set - assert_eq!(contract.functions.len(), 2); - - let get_fn = contract.functions.iter().find(|f| f.name == "get").unwrap(); - assert_eq!(get_fn.visibility, Visibility::Public); - assert_eq!(get_fn.mutability, Mutability::View); - assert_eq!(get_fn.kind, FunctionKind::Function); - assert_eq!(get_fn.returns.len(), 1); - assert_eq!(get_fn.returns[0].type_name, "uint256"); - - let set_fn = contract.functions.iter().find(|f| f.name == "set").unwrap(); - assert_eq!(set_fn.visibility, Visibility::Public); - assert_eq!(set_fn.mutability, Mutability::NonPayable); - assert_eq!(set_fn.params.len(), 1); - assert_eq!(set_fn.params[0].name, "newValue"); - assert_eq!(set_fn.params[0].type_name, "uint256"); - - // Should have 1 state variable - assert_eq!(contract.state_vars.len(), 1); - assert_eq!(contract.state_vars[0].name, "value"); - - // Should have 1 event - assert_eq!(contract.events.len(), 1); - assert_eq!(contract.events[0].name, "ValueChanged"); -} - -#[test] -fn test_cfg_simple_get() { - let parser = SolarParser; - let project = parser.parse(&[fixture_path("simple_storage.sol")]).unwrap(); - let contract = &project.contracts[0]; - let get_fn = contract.functions.iter().find(|f| f.name == "get").unwrap(); - - let cfg = CfgBuilder::build(get_fn, contract).unwrap(); - - // get() just returns — should have Entry + Return (2 nodes minimum) - let node_count = cfg.node_count(); - assert!(node_count >= 2, "Expected at least 2 nodes, got {node_count}"); - - // Should have exactly one Entry and one Return - let entries: Vec<_> = cfg.node_weights().filter(|b| b.kind == BlockKind::Entry).collect(); - let returns: Vec<_> = cfg.node_weights().filter(|b| b.kind == BlockKind::Return).collect(); - assert_eq!(entries.len(), 1); - assert_eq!(returns.len(), 1); -} - -#[test] -fn test_cfg_set_with_require() { - let parser = SolarParser; - let project = parser.parse(&[fixture_path("simple_storage.sol")]).unwrap(); - let contract = &project.contracts[0]; - let set_fn = contract.functions.iter().find(|f| f.name == "set").unwrap(); - - let cfg = CfgBuilder::build(set_fn, contract).unwrap(); - - // set() has require(newValue > 0) which creates a branch: - // Entry → [require check] → True path (assignment + emit + return) - // → False path (revert) - let node_count = cfg.node_count(); - assert!(node_count >= 4, "Expected at least 4 nodes, got {node_count}"); - - // Must have at least one Revert node (from require failing) - let reverts: Vec<_> = cfg.node_weights().filter(|b| b.kind == BlockKind::Revert).collect(); - assert!(!reverts.is_empty(), "Expected at least one Revert node"); - - // Must have at least one Return node - let returns: Vec<_> = cfg.node_weights().filter(|b| b.kind == BlockKind::Return).collect(); - assert!(!returns.is_empty(), "Expected at least one Return node"); - - // Must have branch edges (from require) - let edge_count = cfg.edge_count(); - assert!(edge_count >= 3, "Expected at least 3 edges, got {edge_count}"); -} - -#[test] -fn test_parse_file_not_found() { - let parser = SolarParser; - let result = parser.parse(&[PathBuf::from("nonexistent.sol")]); - assert!(result.is_err()); -} diff --git a/crates/ilold-core/tests/pathtree_test.rs b/crates/ilold-core/tests/pathtree_test.rs deleted file mode 100644 index 05eb4f2..0000000 --- a/crates/ilold-core/tests/pathtree_test.rs +++ /dev/null @@ -1,123 +0,0 @@ -use std::path::PathBuf; - -use ilold_core::cfg::builder::CfgBuilder; -use ilold_core::parse::solar_frontend::SolarParser; -use ilold_core::parse::ProjectParser; -use ilold_core::pathtree::config::PruningConfig; -use ilold_core::pathtree::types::TerminalKind; -use ilold_core::pathtree::walker::build_path_tree; - -fn fixture_path(name: &str) -> PathBuf { - PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .parent().unwrap() - .parent().unwrap() - .join("tests/fixtures") - .join(name) -} - -// REQ-PT-5: set() must produce exactly 2 paths (1 happy + 1 revert) -#[test] -fn test_set_produces_2_paths() { - let parser = SolarParser; - let project = parser.parse(&[fixture_path("simple_storage.sol")]).unwrap(); - let contract = &project.contracts[0]; - let set_fn = contract.functions.iter().find(|f| f.name == "set").unwrap(); - let cfg = CfgBuilder::build(set_fn, contract).unwrap(); - - let pt = build_path_tree(&cfg, &contract.name, "set", &contract.state_vars, &PruningConfig::default()); - - println!("set() paths:"); - for p in &pt.paths { - let nodes: Vec<String> = p.nodes.iter().map(|n| format!("{:?}", n.block_kind)).collect(); - println!(" [{}] {:?} → {}", p.id, p.terminal, nodes.join(" → ")); - } - - assert_eq!(pt.stats.total_paths, 2, "set() should have exactly 2 paths"); - assert_eq!(pt.stats.happy_paths, 1); - assert_eq!(pt.stats.revert_paths, 1); -} - -// REQ-PT-6: transferFrom() must produce exactly 5 paths (1 happy + 4 revert) -#[test] -fn test_transfer_from_produces_5_paths() { - let parser = SolarParser; - let project = parser.parse(&[fixture_path("erc20.sol")]).unwrap(); - let contract = &project.contracts[0]; - let func = contract.functions.iter().find(|f| f.name == "transferFrom").unwrap(); - let cfg = CfgBuilder::build(func, contract).unwrap(); - - let pt = build_path_tree(&cfg, &contract.name, "transferFrom", &contract.state_vars, &PruningConfig::default()); - - println!("transferFrom() paths:"); - for p in &pt.paths { - let nodes: Vec<String> = p.nodes.iter().map(|n| format!("{:?}", n.block_kind)).collect(); - println!(" [{}] {:?} → {}", p.id, p.terminal, nodes.join(" → ")); - } - - assert_eq!(pt.stats.total_paths, 5, "transferFrom() should have exactly 5 paths"); - assert_eq!(pt.stats.happy_paths, 1); - assert_eq!(pt.stats.revert_paths, 4); -} - -// REQ-PT-4: paths should have annotations (external calls, state writes, events) -#[test] -fn test_path_annotations() { - let parser = SolarParser; - let project = parser.parse(&[fixture_path("simple_storage.sol")]).unwrap(); - let contract = &project.contracts[0]; - let set_fn = contract.functions.iter().find(|f| f.name == "set").unwrap(); - let cfg = CfgBuilder::build(set_fn, contract).unwrap(); - - let pt = build_path_tree(&cfg, &contract.name, "set", &contract.state_vars, &PruningConfig::default()); - - // The happy path should have: state write (value) + event (ValueChanged) - let happy = pt.paths.iter().find(|p| p.terminal == TerminalKind::Return).unwrap(); - println!("Happy path annotations: {:?}", happy.annotations); - - assert!(!happy.annotations.state_writes.is_empty(), "Happy path should write state"); - assert!(!happy.annotations.events_emitted.is_empty(), "Happy path should emit event"); - assert!(!happy.annotations.require_checks.is_empty(), "Happy path should have require check"); - - // The revert path should have: require check but no state write - let revert = pt.paths.iter().find(|p| p.terminal == TerminalKind::Revert).unwrap(); - assert!(revert.annotations.state_writes.is_empty(), "Revert path should NOT write state"); -} - -// REQ-PT-8: loops should be unrolled max 3 times -#[test] -fn test_loop_unrolling() { - let parser = SolarParser; - let project = parser.parse(&[fixture_path("uniswap_v2_pair.sol")]).unwrap(); - let contract = project.contracts.iter().find(|c| c.name == "UniswapV2Pair").unwrap(); - let sqrt_fn = contract.functions.iter().find(|f| f.name == "_sqrt").unwrap(); - let cfg = CfgBuilder::build(sqrt_fn, contract).unwrap(); - - let config = PruningConfig { max_loop_unroll: 3, ..PruningConfig::default() }; - let pt = build_path_tree(&cfg, &contract.name, "_sqrt", &contract.state_vars, &config); - - println!("_sqrt() paths: {} total, {} pruned", pt.stats.total_paths, pt.stats.paths_pruned); - for p in &pt.paths { - println!(" [{}] {:?} depth={}", p.id, p.terminal, p.depth); - } - - // Should have some LoopCutoff paths (loop was stopped) - let loop_cutoffs = pt.paths.iter().filter(|p| p.terminal == TerminalKind::LoopCutoff).count(); - println!("Loop cutoff paths: {loop_cutoffs}"); - - // _sqrt has a while loop — with max_unroll=3, we should get cutoffs - assert!(loop_cutoffs > 0, "_sqrt should have loop cutoff paths"); -} - -// Interface functions should produce 0 paths -#[test] -fn test_interface_function_no_paths() { - let parser = SolarParser; - let project = parser.parse(&[fixture_path("staking.sol")]).unwrap(); - let ierc20 = project.contracts.iter().find(|c| c.name == "IERC20").unwrap(); - let transfer = ierc20.functions.iter().find(|f| f.name == "transfer").unwrap(); - let cfg = CfgBuilder::build(transfer, ierc20).unwrap(); - - let pt = build_path_tree(&cfg, "IERC20", "transfer", &ierc20.state_vars, &PruningConfig::default()); - - assert_eq!(pt.stats.total_paths, 0, "Interface function should have 0 paths"); -} diff --git a/crates/ilold-core/tests/sequence_test.rs b/crates/ilold-core/tests/sequence_test.rs deleted file mode 100644 index 84f5756..0000000 --- a/crates/ilold-core/tests/sequence_test.rs +++ /dev/null @@ -1,119 +0,0 @@ -use std::path::PathBuf; - -use ilold_core::cfg::builder::CfgBuilder; -use ilold_core::parse::solar_frontend::SolarParser; -use ilold_core::parse::ProjectParser; -use ilold_core::pathtree::config::PruningConfig; -use ilold_core::pathtree::walker::build_path_tree; -use ilold_core::sequence::builder::build_sequence_tree; - -fn fixture_path(name: &str) -> PathBuf { - PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .parent().unwrap() - .parent().unwrap() - .join("tests/fixtures") - .join(name) -} - -fn build_staking_path_trees() -> (ilold_core::model::project::Project, Vec<ilold_core::pathtree::types::PathTree>) { - let parser = SolarParser; - let mut project = parser.parse(&[fixture_path("staking.sol")]).unwrap(); - project.rebuild_index(); - - let staking = project.contracts.iter().find(|c| c.name == "Staking").unwrap(); - let config = PruningConfig::default(); - - let path_trees: Vec<_> = staking - .functions - .iter() - .map(|f| { - let cfg = CfgBuilder::build(f, staking).unwrap(); - build_path_tree(&cfg, &staking.name, &f.name, &staking.state_vars, &config) - }) - .collect(); - - (project, path_trees) -} - -// REQ-SEQ-4: 8 functions, depth 2 → 72 sequences (8 + 64) -#[test] -fn test_staking_sequence_count_depth_2() { - let (project, path_trees) = build_staking_path_trees(); - let staking = project.contracts.iter().find(|c| c.name == "Staking").unwrap(); - - let st = build_sequence_tree(staking, &path_trees, 2); - - println!("Functions in sequence tree ({}):", st.functions.len()); - for (i, f) in st.functions.iter().enumerate() { - println!(" [{}] {} (read_only={}, paths={})", i, f.name, f.read_only, f.path_count); - } - - let depth_1 = st.sequences.iter().filter(|s| s.depth == 1).count(); - let depth_2 = st.sequences.iter().filter(|s| s.depth == 2).count(); - println!("\nSequences: {} total (depth 1: {}, depth 2: {})", st.sequences.len(), depth_1, depth_2); - - assert_eq!(st.functions.len(), 8, "Should have 8 public/external functions (excluding constructor)"); - assert_eq!(depth_1, 8); - assert_eq!(depth_2, 64); - assert_eq!(st.sequences.len(), 72, "Total should be 8 + 64 = 72"); -} - -// REQ-SEQ-1: read_only flag correct -#[test] -fn test_read_only_flags() { - let (project, path_trees) = build_staking_path_trees(); - let staking = project.contracts.iter().find(|c| c.name == "Staking").unwrap(); - - let st = build_sequence_tree(staking, &path_trees, 1); - - let read_only: Vec<_> = st.functions.iter().filter(|f| f.read_only).collect(); - let state_changing: Vec<_> = st.functions.iter().filter(|f| !f.read_only).collect(); - - println!("Read-only: {:?}", read_only.iter().map(|f| &f.name).collect::<Vec<_>>()); - println!("State-changing: {:?}", state_changing.iter().map(|f| &f.name).collect::<Vec<_>>()); - - // rewardPerToken (view) and earned (view) should be read_only - assert_eq!(read_only.len(), 2, "Should have 2 read-only functions"); - assert_eq!(state_changing.len(), 6, "Should have 6 state-changing functions"); -} - -// REQ-SEQ-6: constructors excluded -#[test] -fn test_constructor_excluded() { - let (project, path_trees) = build_staking_path_trees(); - let staking = project.contracts.iter().find(|c| c.name == "Staking").unwrap(); - - let st = build_sequence_tree(staking, &path_trees, 1); - - let has_constructor = st.functions.iter().any(|f| f.name.is_empty() || f.name == "constructor"); - assert!(!has_constructor, "Constructor should not appear in sequences"); -} - -// REQ-SEQ-3: has_state_change correct -#[test] -fn test_has_state_change_flag() { - let (project, path_trees) = build_staking_path_trees(); - let staking = project.contracts.iter().find(|c| c.name == "Staking").unwrap(); - - let st = build_sequence_tree(staking, &path_trees, 2); - - // A sequence of only read-only functions should have has_state_change = false - let earned_idx = st.functions.iter().position(|f| f.name == "earned").unwrap(); - let reward_idx = st.functions.iter().position(|f| f.name == "rewardPerToken").unwrap(); - - let read_only_seq = st.sequences.iter().find(|s| { - s.depth == 2 && s.steps == vec![earned_idx, reward_idx] - }); - - if let Some(seq) = read_only_seq { - assert!(!seq.has_state_change, "earned → rewardPerToken should have no state change"); - } - - // A sequence with deposit should have has_state_change = true - let deposit_idx = st.functions.iter().position(|f| f.name == "deposit").unwrap(); - let deposit_seq = st.sequences.iter().find(|s| { - s.depth == 1 && s.steps == vec![deposit_idx] - }).unwrap(); - - assert!(deposit_seq.has_state_change, "deposit should have state change"); -} diff --git a/crates/ilold-core/tests/slicing_test.rs b/crates/ilold-core/tests/slicing_test.rs deleted file mode 100644 index f29df4e..0000000 --- a/crates/ilold-core/tests/slicing_test.rs +++ /dev/null @@ -1,191 +0,0 @@ -use std::path::PathBuf; - -use ilold_core::parse::solar_frontend::SolarParser; -use ilold_core::parse::ProjectParser; -use ilold_core::slicing::{ - build_slice_result, SliceDirection, SliceEntry, SliceResult, StatementOrigin, -}; - -fn fixture_path(name: &str) -> PathBuf { - PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .parent().unwrap() - .parent().unwrap() - .join("tests/fixtures") - .join(name) -} - -fn slice( - contract: &str, - function: &str, - variable: &str, - direction: SliceDirection, -) -> SliceResult { - let parser = SolarParser; - let mut project = parser.parse(&[fixture_path("staking.sol")]).unwrap(); - project.rebuild_index(); - - let c = project - .contracts - .iter() - .find(|c| c.name == contract) - .unwrap_or_else(|| panic!("contract {contract} not found")); - let f = c - .functions - .iter() - .find(|f| f.name == function) - .unwrap_or_else(|| panic!("function {function} not found")); - - build_slice_result(&project, c, f, variable, direction) -} - -fn texts(entries: &[SliceEntry]) -> Vec<String> { - entries.iter().map(|e| e.text.clone()).collect() -} - -fn modifier_origin(entries: &[SliceEntry], substr: &str) -> Option<String> { - entries.iter().find(|e| e.text.contains(substr)).and_then(|e| match &e.origin { - StatementOrigin::Modifier(name) => Some(name.clone()), - StatementOrigin::FunctionBody => None, - }) -} - -#[test] -fn backward_slice_picks_up_transitive_assignment() { - // deposit(): totalStaked += amount. The backward slice on totalStaked - // should include that line and the `require(amount > 0)` can be - // skipped (require is an ExpressionStmt/FunctionCall, not an - // assignment, so it's not a def of totalStaked — but it does USE - // `amount`, so it won't appear in backward slice). - let res = slice("Staking", "deposit", "totalStaked", SliceDirection::Backward); - let lines = texts(&res.backward); - assert!( - lines.iter().any(|l| l.contains("totalStaked")), - "expected a totalStaked assignment, got {lines:?}" - ); - // Forward is not requested — must stay empty. - assert!(res.forward.is_empty()); -} - -#[test] -fn forward_slice_propagates_taint_across_assignments() { - // withdraw(): amount flows into balances[..] -= amount, totalStaked -= - // amount, stakingToken.transfer(..., amount), and emit Withdrawn(...). - let res = slice("Staking", "withdraw", "amount", SliceDirection::Forward); - let lines = texts(&res.forward); - assert!( - lines.iter().any(|l| l.contains("balances")), - "expected balances update in forward slice, got {lines:?}" - ); - assert!( - lines.iter().any(|l| l.contains("totalStaked")), - "expected totalStaked update in forward slice, got {lines:?}" - ); - assert!(res.backward.is_empty()); -} - -#[test] -fn backward_slice_pulls_in_control_dependency() { - // claimRewards(): - // uint256 reward = rewards[msg.sender]; - // if (reward > 0) { - // rewards[msg.sender] = 0; // ← def of `rewards` inside If - // ... - // } - // Backward slice on `rewards` should: - // 1. include `rewards[msg.sender] = 0` (direct def hit) - // 2. drag the enclosing `if (reward > 0)` via ancestor merge - // 3. propagate `reward` into the relevant set - // 4. pick up `uint256 reward = rewards[msg.sender]` (def of reward) - let res = slice("Staking", "claimRewards", "rewards", SliceDirection::Backward); - let lines = texts(&res.backward); - assert!( - lines.iter().any(|l| l.starts_with("if (")), - "expected enclosing if(...) in backward slice, got {lines:?}" - ); - assert!( - lines.iter().any(|l| l.contains("uint256 reward")), - "expected `uint256 reward = rewards[..]` pulled in via control dep, got {lines:?}" - ); -} - -#[test] -fn slice_on_unknown_variable_is_empty() { - let res = slice("Staking", "deposit", "nonexistent_var", SliceDirection::Both); - assert!(res.backward.is_empty(), "backward should be empty"); - assert!(res.forward.is_empty(), "forward should be empty"); -} - -#[test] -fn both_direction_populates_both_sides() { - let res = slice("Staking", "withdraw", "amount", SliceDirection::Both); - assert!(!res.forward.is_empty(), "forward should have entries"); - // `amount` is a parameter, never reassigned inside withdraw → backward - // data-dep pass finds nothing, backward stays empty. This is the - // expected behavior for parameter slicing. - assert!(res.backward.is_empty(), "backward on parameter should be empty"); -} - -#[test] -fn backward_slice_pulls_writes_from_modifier() { - // deposit() applies updateReward(msg.sender), which writes - // lastUpdateTime, rewardPerTokenStored, rewards[account], and - // userRewardPerTokenPaid[account] before the function body runs. - // None of these are touched by deposit's body itself, so without - // modifier walking the slice would be empty. - let res = slice("Staking", "deposit", "lastUpdateTime", SliceDirection::Backward); - let lines = texts(&res.backward); - assert!( - lines.iter().any(|l| l.contains("lastUpdateTime") && l.contains("block.timestamp")), - "expected `lastUpdateTime = block.timestamp` from updateReward, got {lines:?}" - ); - assert_eq!( - modifier_origin(&res.backward, "lastUpdateTime"), - Some("updateReward".to_string()), - "the lastUpdateTime entry should be tagged as coming from updateReward" - ); -} - -#[test] -fn modifier_writes_appear_before_function_body_in_program_order() { - // updateReward writes happen BEFORE deposit's own writes in the - // execution timeline. Forward slice on the modifier-defined - // `rewardPerTokenStored` should put its def first, and any later - // function-body statements that read it (none in deposit, but the - // ordering invariant must hold for the entries that do exist). - let res = slice("Staking", "deposit", "rewardPerTokenStored", SliceDirection::Forward); - let lines = texts(&res.forward); - if let Some(idx) = lines.iter().position(|l| l.contains("rewardPerTokenStored")) { - // Anything function-body that follows must come AFTER the - // modifier write in the flat order. - let modifier_entry = &res.forward[idx]; - assert!( - matches!(modifier_entry.origin, StatementOrigin::Modifier(ref n) if n == "updateReward"), - "rewardPerTokenStored entry should be tagged as updateReward, got {:?}", - modifier_entry.origin - ); - } else { - panic!("expected rewardPerTokenStored in forward slice, got {lines:?}"); - } -} - -#[test] -fn slice_respects_program_order() { - // deposit() body order: require → transferFrom → balances += → - // totalStaked += → emit. Forward slice on `amount` should keep them - // in that program order inside the returned vec. - let res = slice("Staking", "deposit", "amount", SliceDirection::Forward); - let lines = texts(&res.forward); - let find = |needle: &str| lines.iter().position(|l| l.contains(needle)); - if let (Some(i_balances), Some(i_total), Some(i_emit)) = ( - find("balances"), - find("totalStaked"), - find("emit"), - ) { - assert!( - i_balances < i_total && i_total < i_emit, - "program order broken: balances@{i_balances}, total@{i_total}, emit@{i_emit} — {lines:?}" - ); - } else { - panic!("expected balances / totalStaked / emit in forward slice, got {lines:?}"); - } -} From b80d388f90ae2e72bb0a8a9c0b35388c07de6797 Mon Sep 17 00:00:00 2001 From: scab24 <git.seco@protonmail.com> Date: Sun, 17 May 2026 19:37:46 +0200 Subject: [PATCH 09/10] docs(book): purge solidity from concepts, reference, roadmap and repl pages Rewrites the six primary pages (architecture, overview, api-endpoints, websocket, limitations, cross-cutting) as Solana-only and removes every Solidity link or comparative aside from the solana/* and roadmap/solana pages. mdbook build is clean; no broken cross-references remain. --- docs/guide/src/concepts/architecture.md | 62 ++---- docs/guide/src/concepts/overview.md | 17 +- docs/guide/src/reference/api-endpoints.md | 196 ++++-------------- docs/guide/src/reference/limitations.md | 7 +- docs/guide/src/reference/mcp.md | 2 - docs/guide/src/reference/websocket.md | 13 +- docs/guide/src/roadmap/cross-cutting.md | 1 - docs/guide/src/roadmap/solana.md | 8 +- docs/guide/src/solana/limitations.md | 3 +- docs/guide/src/solana/overview.md | 34 +-- docs/guide/src/solana/repl/analysis.md | 4 +- docs/guide/src/solana/repl/findings.md | 3 +- docs/guide/src/solana/repl/help.md | 2 +- docs/guide/src/solana/repl/programs.md | 1 - docs/guide/src/solana/repl/runtime.md | 2 +- docs/guide/src/solana/repl/scenarios.md | 3 +- docs/guide/src/solana/repl/session.md | 2 +- .../src/solana/workflows/audit-walkthrough.md | 14 +- 18 files changed, 118 insertions(+), 256 deletions(-) diff --git a/docs/guide/src/concepts/architecture.md b/docs/guide/src/concepts/architecture.md index 3dfefb4..334b7a7 100644 --- a/docs/guide/src/concepts/architecture.md +++ b/docs/guide/src/concepts/architecture.md @@ -1,64 +1,44 @@ # Architecture -ilold is split into a handful of crates with clear responsibilities. The diagram below shows the pipeline for each backend; both meet at the shared web layer (`ilold-web`) and the REPL frontend (`ilold-cli`). +ilold is a small Rust workspace organised around one pipeline (Solana via LiteSVM), one HTTP/WS layer, one REPL, and one MCP server. The diagram below shows how a typed program model flows from the Anchor IDL down to the canvas and the LLM client. -## Solidity pipeline +## Pipeline ``` -.sol files - │ - ▼ -ilold-core::parse::solar_frontend (SolarParser) - │ - ▼ -ilold-core::model (Project, ContractDef, FunctionDef, ...) - │ - ▼ -ilold-core::cfg::builder (CfgBuilder) - │ - ▼ -ilold-core::pathtree::walker (build_path_tree) - │ - ▼ -ilold-core::sequence::analysis (analyze_sequences, analyze_project) +Anchor.toml + idls/<program>.json + target/deploy/<program>.so │ ▼ -ilold-core::narrative + slicing (info, trace, slice, timeline) -``` - -`analyze` and `context` run this pipeline once and print to stdout. `serve` and `explore` keep the model in memory and expose it through the HTTP/WS API in `ilold-web`. - -## Solana pipeline - -``` -Anchor.toml + idls/<program>.json + target/deploy/<program>.so +ilold-solana-core::ingest (detect, DetectedProject, AnchorProject) │ ▼ -ilold-solana-core::ingest (detect, AnchorProject) +ilold-solana-core::idl + model (IDL → ProgramDef → ProgramView) │ ▼ -ilold-solana-core::runtime (LiteSVM-backed engine) +ilold-solana-core::execute (VmHost: LiteSVM boot, snapshots, fork) │ ▼ -ilold-solana-core::exploration (SolanaCommand → SolanaCommandResult) - │ per-step CU, logs, account diffs, decoded timelines +ilold-solana-core::exploration (SolanaCommand → SolanaCommandResult) + │ per-step CU, logs, account diffs, decoded timelines ▼ -ilold-web (shared HTTP/WS layer) (/api/cmd, /api/program/*, /ws) +ilold-web (HTTP/WS + REST routes) (/api/cmd, /api/program/*, /api/scenarios, /ws) │ - ▼ -ilold-cli::explore (REPL, parses input, prints results) + ├─▶ ilold-cli::explore (REPL, parses input, prints results) + ├─▶ frontend (Svelte canvas) (subscribes to /ws, paints state) + └─▶ ilold-mcp (stdio JSON-RPC, exposes 30 tools) ``` -There is no static CFG or path tree on Solana today: programs are bytecode at this point. Anything that requires control-flow analysis (`slice`, `trace`, `sequence` narrative) is listed in [Solana: Limitations](../solana/limitations.md) and tracked under Phase 2 in the [Roadmap](../roadmap/solana.md). +There is no static CFG or path tree on Solana today: handlers are bytecode at this point. Anything that requires control-flow analysis (slicing, structural narratives, detector engine) is listed in [Limitations](../solana/limitations.md) and tracked under Phase 2 in the [Roadmap](../roadmap/solana.md). -## Shared layer +## Crates | Crate | Role | | --- | --- | -| `ilold-cli` | Argument parsing, REPL, output formatting, key bindings (`crates/ilold-cli/src/main.rs`, `explore.rs`, `help.rs`) | -| `ilold-web` | HTTP + WebSocket API consumed by both the REPL (via `--attach`) and the web canvas | -| `ilold-session-core` | Shared session abstractions (steps, scenarios, canvas patches) | -| `ilold-core` | Solidity model, CFG, slicer, narrative, sequence analysis | +| `ilold-cli` | Argument parsing, REPL, output formatting (`src/main.rs`, `explore.rs`, `help.rs`) | +| `ilold-web` | HTTP + WebSocket API consumed by the REPL (via `--attach`) and the web canvas | +| `ilold-session-core` | Backend-agnostic session abstractions (steps, scenarios, canvas patches, access levels, journal) | | `ilold-solana-core` | Anchor IDL ingest, LiteSVM runtime, instruction execution, timeline reconstruction | +| `ilold-mcp` | Model Context Protocol server exposing the REPL commands as 30 typed tools | +| `ilold-render` | Pretty printers shared by CLI and MCP (byte-identical to legacy CLI prints) | +| `ilold-help` | `SOLANA_HELP_BLOCKS` registry shared by REPL inline help and MCP tool descriptions | -The web canvas (`crates/ilold-web/frontend`) subscribes to the `/ws` stream and stays in sync with whatever the REPL does. +The web canvas (`crates/ilold-web/frontend`) subscribes to the `/ws` stream and stays in sync with whatever the REPL or MCP client does. diff --git a/docs/guide/src/concepts/overview.md b/docs/guide/src/concepts/overview.md index 4ddf8b9..8bc5014 100644 --- a/docs/guide/src/concepts/overview.md +++ b/docs/guide/src/concepts/overview.md @@ -1,20 +1,19 @@ # What ilold does -ilold is a smart-contract audit explorer. It loads a project, builds an internal model, and exposes that model through an interactive REPL. The auditor adds entry-point calls to a **session**, the tool tracks accumulated effects, and analysis commands answer questions about the code without requiring a separate run. +ilold is a Solana program audit explorer. It loads an Anchor project, builds an internal model from the IDL, boots a LiteSVM with the compiled program, and exposes that model through an interactive REPL. The auditor adds instruction calls to a **session**, the tool tracks accumulated effects, and analysis commands answer questions about the program without leaving the shell. -## Two backends, one shell +## Input and execution model -| Backend | Input | Execution model | What you get | -| --- | --- | --- | --- | -| Solidity | `.sol` sources (file or directory) | Symbolic (parser, CFG, path tree, slicer) | Function narratives, execution trees with modifier inlining, backward/forward dataflow slices, cross-step timelines | -| Solana | Project root with `Anchor.toml`, `idls/<program>.json`, `target/deploy/<program>.so` | Concrete (in-process execution via LiteSVM) | Per-call CU and logs, account diffs, decoded timelines, scenario-isolated VMs, time-warp on the `Clock` sysvar | +| Input | Execution model | What you get | +| --- | --- | --- | +| Project root with `Anchor.toml`, `idls/<program>.json`, `target/deploy/<program>.so` | Concrete (in-process execution via LiteSVM) | Per-call CU and logs, account diffs, decoded timelines, scenario-isolated VMs, time-warp on the `Clock` sysvar | -The REPL command surface is the same shell. Backend-specific commands are documented in [Solana: Solana runtime](../solana/repl/runtime.md) and [Solana: Scenarios](../solana/repl/scenarios.md). +Solana-specific commands are documented in [Solana runtime](../solana/repl/runtime.md) and [Scenarios](../solana/repl/scenarios.md). ## Sessions and scenarios -A **session** is the active scenario inside the active project. Adding a step means calling an entry point and recording its effects. A **scenario** is a named branch of the session timeline; scenarios can be created from scratch (`scenario new`) or forked from an existing one at a step boundary (`scenario fork`). On Solana, each scenario owns its own VM and user keypairs, so forks produce independent state. +A **session** is the active scenario inside the active program. Adding a step means calling an instruction and recording its effects. A **scenario** is a named branch of the session timeline; scenarios can be created from scratch (`scenario new`) or forked from an existing one at a step boundary (`scenario fork`). Each scenario owns its own VM and user keypairs, so forks produce independent state. ## What the tool does not do -ilold has no built-in vulnerability detectors. There is no checklist that fires "this is a reentrancy" or "this is a missing access control" automatically. The auditor uses `who`, `info`, `trace`, `slice`, `timeline`, `state`, `step` to investigate, and records findings via `finding` and `note`. See [Roadmap](../roadmap/solana.md) for the Phase 2 detector engine and AST extractor. +ilold has no built-in vulnerability detectors. There is no checklist that fires "this is a missing signer" or "this is account confusion" automatically. The auditor uses `who`, `info`, `state`, `timeline`, `step`, `coverage` to investigate, and records findings via `finding` and `note`. See [Roadmap](../roadmap/solana.md) for the Phase 2 detector engine and AST extractor. diff --git a/docs/guide/src/reference/api-endpoints.md b/docs/guide/src/reference/api-endpoints.md index c5d41be..dc2eff0 100644 --- a/docs/guide/src/reference/api-endpoints.md +++ b/docs/guide/src/reference/api-endpoints.md @@ -1,191 +1,87 @@ # HTTP API Reference -ilold exposes an HTTP API on the configured port (default 3001). All endpoints return JSON. The API is split into three groups: command bus, session queries, and contract queries. +ilold exposes an HTTP API on the configured port (default `8080`). All endpoints return JSON. The routes are defined in `crates/ilold-web/src/lib.rs::build_router`. ## Command bus ### POST /api/cmd -Execute a session command. This is the single entry point for all state-mutating session operations. +Execute a session command against the active program and scenario. This is the single entry point for every state-mutating session operation. **Request body:** -``` +```json { - "contract": "Staking", // optional, defaults to session contract - "command": { "Call": { "func": "deposit" } } + "contract": "staking", + "command": { "Call": { "ix": "stake", "args": { "amount": 1000 }, "accounts": { "pool": "pool", "user_stake": "alice_stake", "user": "alice" }, "signers": ["alice"] } } } ``` -**Supported commands:** - -| Command | Payload | Description | -|---|---|---| -| `Call` | `{ "func": "deposit", "trace_config": null }` | Add a function call as a session step | -| `Back` | `"Back"` | Remove the last step | -| `Clear` | `"Clear"` | Remove all steps | -| `State` | `"State"` | Return accumulated state variable summary | -| `Functions` | `"Functions"` | List functions of the current contract | -| `FunctionsAll` | `"FunctionsAll"` | List all accessible functions including inherited | -| `StateVarsAll` | `"StateVarsAll"` | List all accessible state variables including inherited | -| `Who` | `{ "variable": "totalStaked" }` | Find all writers and readers of a variable | -| `Finding` | `{ "severity": "High", "title": "...", "description": "..." }` | Record an audit finding | -| `Note` | `{ "text": "..." }` | Record a free-text note | -| `Status` | `{ "func": "deposit", "status": "Reviewed" }` | Set review status for a function | -| `Session` | `"Session"` | Return session overview (contract, steps, finding count) | -| `Export` | `"Export"` | Export session journal as markdown | -| `SaveSession` | `"SaveSession"` | Serialize session to JSON | -| `LoadSession` | `{ "json": "..." }` | Restore session from serialized JSON | - -**Response:** A `CommandResult` variant matching the command type. Key variants: - -- `StepAdded`: `step_index`, `function`, `access`, `state_changed` (list of variable names) -- `StateView`: `summary` (list of variable summaries with writers per step) -- `FunctionList`: `functions` (name, access level, writes_state, has_external_calls) -- `VariableInfo`: `variable`, `writers` (name + access), `readers` (name + access) -- `Error`: `message` +`contract` is the program name. It defaults to the first program in the workspace when omitted. -## Session query endpoints +`command` carries a `SolanaCommand` variant (see `crates/ilold-solana-core/src/exploration/commands.rs`). Headline variants: -These endpoints read from the active session. They return 404 if no session exists (no `Call` command has been issued). +| Variant | Payload | Description | +| --- | --- | --- | +| `Call` | `{ ix, args, accounts, signers }` | Run an instruction against the LiteSVM and append a step | +| `Back` | `"Back"` | Remove the last step and rewind the VM | +| `Clear` | `"Clear"` | Reset scenario steps and the underlying VM | +| `Funcs` / `Vars` / `Info` / `Coupling` / `Coverage` | metadata | Inspect the typed model and runtime aggregates | +| `State` / `Session` / `Step` / `Timeline` / `Inspect` | session queries | Decoded views of the active scenario | +| `Users` / `UsersNew` / `Airdrop` / `TimeWarp` / `Pda` | runtime | LiteSVM controls | +| `Scenario` | `{ sub: New | List | Switch | Fork | Delete }` | Scenario management | +| `Finding` / `Note` / `Status` / `Findings` / `Export` | deliverable | Audit journal and Markdown export | +| `SaveSession` / `LoadSession` | persistence | JSON scenario store under `~/.ilold/sessions/` | -### GET /api/session/state +The response is a `SolanaCommandResult` variant (`StepAdded`, `CallFailed`, `StateView`, `Timeline`, `Coverage`, `Error`, …). -Returns the accumulated state variable summary across all session steps. +## Project endpoints -**Response:** Array of `VariableSummary` objects with variable name, type, and per-step write details. - -### GET /api/session/sequence - -Returns a sequence narrative describing the relationship between session steps. Requires at least 2 steps. +| Endpoint | Description | +| --- | --- | +| `GET /api/project` | Project summary: `kind: "solana"`, list of programs with instruction + account-type counts | +| `GET /api/project/map` | Full project map consumed by the web canvas (programs, instructions, account types) | -**Response:** `SequenceNarrative` with per-step summaries and flow summaries derived from persisted flow trees. +## Program endpoints -### GET /api/session/step/{index}/narrative +| Endpoint | Description | +| --- | --- | +| `GET /api/program/{name}/view` | Full `ProgramView` for the named program: instructions (typed args, accounts with flags, signers, PDAs, admin-gated flag, coupling hints), account types, discriminators | +| `GET /api/program/{name}/overlay` | Runtime overlay aggregated over the active scenario: calls-per-instruction, failures, CU stats, CPI edges (`?scenario=<name>` overrides the default) | +| `GET /api/program/{name}/{ix}/source` | Anchor handler source slice from `programs/<program>/src/lib.rs`, with `file_path`, `source`, and `span` (line/column range) | -Returns the function narrative for a specific session step. +## Users + scenarios -**Path params:** `index` -- zero-based step index. +| Endpoint | Description | +| --- | --- | +| `GET /api/users/{scenario}/labels` | Returns the keypair labels (pubkey → name) for the given scenario; used by the canvas to render `users new <name>` aliases on signer/payer pubkeys | +| `GET /api/scenarios` | Scenario list for the active program (name, active flag, step count) | +| `GET /api/scenarios/all` | Scenarios with their step lists across every program in the workspace | -**Response:** `FunctionNarrative` with paths, state writes, external calls, and conditions. +## Session step trace ### GET /api/session/step/{index}/trace -Returns the persisted FlowTree of a session step. This is the tree captured when `Call` was executed, not a recomputation. - -**Path params:** `index` -- zero-based step index. - -**Response:** `FlowTree` with step nodes, each containing operation type, target, conditions, and child steps. Returns 404 if the step has no persisted tree (pre-Phase-2a sessions). - -### GET /api/session/timeline/{variable} - -Returns a chronological timeline of every write to `variable` across all session steps. Matches by base name (e.g., `balances` matches `balances[msg.sender]`). - -**Path params:** `variable` -- state variable name. - -**Response:** `VariableTimeline` with `state_entries` and `local_entries`. Each entry contains: `session_step_index`, `function`, `target`, `operator`, `value_expr`, `reached_when` (path conditions), `via` (modifier name if applicable), `scope`. - -### GET /api/session/slice/{function}/{variable} - -Returns a dataflow slice for `variable` inside `function` of the session's current contract. - -**Path params:** `function` -- function name, `variable` -- variable name. - -**Query params:** - -| Param | Values | Default | Description | -|---|---|---|---| -| `direction` | `backward`, `forward`, `both`, `b`, `f`, `all` | `both` | Slice direction | - -**Response:** `SliceResult` with `backward` and `forward` arrays. Each entry contains: `path`, `span` (source location), `text` (rendered statement), `origin` (`FunctionBody` or `Modifier(name)`). - -### GET /api/session/trace/{contract}/{func} - -Returns a FlowTree for the given function, computed on demand from the current analysis data. - -**Path params:** `contract` -- contract name, `func` -- function name. - -**Query params:** +Returns the persisted runtime trace of a session step (logs, CU, inner instructions). 404 if the step has no trace recorded. -| Param | Type | Default | Description | -|---|---|---|---| -| `depth` | integer | 2 | Maximum inline depth for internal calls | -| `reverts` | boolean | false | Include revert paths in the tree | -| `expand` | string | empty | Comma-separated step IDs to force-inline beyond max depth (e.g., `17,24`) | - -**Response:** `FlowTree` with nested step nodes representing the execution flow. - -### GET /api/session/function/{contract}/{func} - -Returns a function narrative without requiring a session step. Useful for inspecting functions that have not been called in the session. - -**Path params:** `contract` -- contract name, `func` -- function name. - -**Response:** `FunctionNarrative` with paths, state effects, and behavioral summary. - -## Contract query endpoints - -These endpoints read from the static analysis data and do not require an active session. - -### GET /api/project - -Returns a project summary with file count and a list of contracts (name, kind, function count, state variable count, inheritance). - -### GET /api/project/map - -Returns the full project map with all contracts, their functions (with path counts and external call flags), state variables, and cross-contract relationships extracted from call graphs. - -### GET /api/contract/{name} - -Returns contract detail: name, kind, inheritance chain, functions (with path stats), state variables, and inherited functions and state variables. - -### GET /api/contract/{name}/callgraph - -Returns the call graph for a contract in Cytoscape-compatible JSON format. Nodes represent functions (with contract, type, external flag). Edges represent calls (with kind and count). - -### GET /api/contract/{name}/{func}/cfg - -Returns the control flow graph for a function in Cytoscape-compatible JSON format. Nodes represent basic blocks (entry, normal, return, revert, assembly, loop). Edges represent control flow transitions. - -### GET /api/contract/{name}/{func}/paths - -Returns the path tree for a function. Contains all execution paths with stats (total, happy, revert) and per-path annotations (state writes, external calls, events). - -### GET /api/contract/{name}/sequences - -Returns the sequence tree for a contract, showing function interaction patterns. - -### GET /api/contract/{name}/analysis - -Returns the sequence analysis for a contract: per-function behavior summaries (state writes, state reads, external calls, conditions) and inter-function transition information. - -### GET /api/contract/{name}/suggestions - -Returns search suggestions for the contract: function names, state variable names, event names, external call targets, and predefined categories (revert, return, assembly). - -## Solana-specific endpoints - -Solana shares `/api/cmd`, `/api/project`, `/api/project/map`, `/ws`, and most session endpoints with the Solidity backend. The following routes are Solana-only: +## Annotations | Endpoint | Description | | --- | --- | -| `GET /api/program/{name}/view` | Full `ProgramView` for the named program: instructions (with args, accounts, signers, PDAs, admin-gated flag, coupling hints), account types, discriminators. | -| `GET /api/program/{name}/overlay` | Runtime overlay aggregated over the active scenario: calls-per-instruction, failures, CU stats, CPI edges. | -| `GET /api/users/{scenario}/labels` | Returns the keypair labels for a given scenario (used by the web canvas to render `users new <name>` aliases). | -| `GET /api/scenarios` | Scenario list for the active program (active marker, step counts). | -| `GET /api/scenarios/all` | Scenario list across every program in the workspace. | - -`POST /api/cmd` carries a `SolanaCommand` payload (`Call`, `Users`, `UsersNew`, `Airdrop`, `TimeWarp`, `Pda`, `Inspect`, `Scenario`, `SaveSession`, `LoadSession`, …; see `crates/ilold-solana-core/src/exploration/commands.rs`). The response is a `SolanaCommandResult` variant (`StepAdded`, `CallFailed`, `StateView`, `Timeline`, `Coverage`, etc.). +| `GET /api/annotations` | List all canvas annotations | +| `POST /api/annotations` | Create an annotation | +| `PUT /api/annotations/{id}` | Update an annotation | +| `DELETE /api/annotations/{id}` | Delete an annotation | ## WebSocket `GET /ws` upgrades to a WebSocket connection. See [WebSocket events](./websocket.md) for the full event vocabulary and payload shapes. -A second WebSocket route `GET /ws/pty` provides a PTY bridge used by the embedded REPL in the web canvas. +`GET /ws/pty` provides a PTY bridge used by the embedded REPL in the web canvas. Binary passthrough; the wire format is documented inline in `crates/ilold-web/src/ws/pty.rs`. ## Related pages - [WebSocket events](./websocket.md) -- [Solidity REPL: Session](../solidity/repl/session.md) -- [Solana REPL: Session](../solana/repl/session.md) +- [MCP server](./mcp.md) +- [REPL: Session](../solana/repl/session.md) - [Known Limitations](./limitations.md) diff --git a/docs/guide/src/reference/limitations.md b/docs/guide/src/reference/limitations.md index 54fd247..9a82acb 100644 --- a/docs/guide/src/reference/limitations.md +++ b/docs/guide/src/reference/limitations.md @@ -1,8 +1,5 @@ # Known Limitations -Limitations are documented per backend, since the boundaries are very different: +See [Solana: Limitations](../solana/limitations.md) for the backend-specific list: no static CFG (so no `slice` / structural `trace` yet), heuristic `who`, `time-warp` semantics, keypair persistence model, CPI visibility. -- [Solidity: Limitations](../solidity/limitations.md): intraprocedural slicing, assignment-only DEF extraction, modifier placeholder split, tuple destructuring, etc. -- [Solana: Limitations](../solana/limitations.md): no static CFG (no `slice` / `trace` yet), heuristic `who`, `time-warp` semantics, keypair persistence, CPI visibility. - -For the planned remediations, see the [Roadmap](../roadmap/solidity.md). +For the planned remediations, see the [Roadmap](../roadmap/solana.md). diff --git a/docs/guide/src/reference/mcp.md b/docs/guide/src/reference/mcp.md index 52be72a..871ad83 100644 --- a/docs/guide/src/reference/mcp.md +++ b/docs/guide/src/reference/mcp.md @@ -238,7 +238,6 @@ Every step also fires a WebSocket patch from `ilold serve`, so a browser tab poi ## Limitations -- **Solana only.** The MCP server refuses to start when the backend is a Solidity project. Solidity support is in the [cross-cutting roadmap](../roadmap/cross-cutting.md). - **Single active program at a time.** The handler tracks one active program. Call `ilold_use <program>` to switch — the MCP subprocess does not need to be restarted to point at a different program in the same workspace. - **Static tool registry.** Tools are derived from `SOLANA_HELP_BLOCKS` once at startup. Reloading the backend project does not change the tool set; only the data behind the tools. - **No sandbox over the LLM.** Every tool that mutates the VM (`ilold_call`, `ilold_clear`, `ilold_back`, `ilold_scenario`) is invocable without confirmation from the server. Sandboxing is delegated to the MCP client: mature clients prompt the human before destructive tools (those whose names contain `clear`, `delete`, `reset`). @@ -250,7 +249,6 @@ Every step also fires a WebSocket patch from `ilold serve`, so a browser tab poi | Symptom | Likely cause | | --- | --- | | `Cannot reach Ilold server at <url>` on startup | `ilold serve` is not running, or `--server-url` points to the wrong port. | -| `Server at <url> is not a Solana project (kind=solidity)` | The backend was started against a Solidity workspace. Point `ilold serve` at a Solana project. | | Tools do not appear in the client | The client could not spawn `ilold`. Check that the binary is on `PATH` or use an absolute path in `command`. Inspect the client log (`~/Library/Logs/Claude/mcp-server-ilold.log` for Claude Desktop on macOS). | | `No active contract` from every tool but `ilold_programs` | The session has no active program. Call `ilold_use <program>` (or restart the subprocess with `--contract <name>`). | | Tool call returns `Error: ...` | The backend rejected the `SolanaCommand`. The error text is the same as the REPL would print; check the active program (`ilold_programs`) and the instruction arguments. | diff --git a/docs/guide/src/reference/websocket.md b/docs/guide/src/reference/websocket.md index 62a809d..6451157 100644 --- a/docs/guide/src/reference/websocket.md +++ b/docs/guide/src/reference/websocket.md @@ -6,10 +6,10 @@ The `/ws` route emits `ServerMessage` events (JSON, tagged on a `type` field) wh | Event | Fields | Trigger | | --- | --- | --- | -| `session_add_node` | `scenario`, `function`, `access`, `step_index`, optional `runtime` (CU + diffs + logs excerpt on Solana) | `call` adds a step | +| `session_add_node` | `scenario`, `function`, `access`, `step_index`, optional `runtime` (CU + diffs + logs excerpt) | `call` adds a step | | `session_remove_node` | `scenario` | `back` rewinds the last step | | `session_clear` | `scenario` | `clear` wipes the scenario | -| `session_highlight` | `scenario`, `function` | Auditor selects a step (web canvas) | +| `session_highlight` | `scenario`, `function` | Auditor selects a step from the web canvas | ## Scenario events @@ -21,7 +21,7 @@ The `/ws` route emits `ServerMessage` events (JSON, tagged on a `type` field) wh | `scenario_forked` | `from`, `to`, `at_step` | `scenario fork` | | `scenario_store_reloaded` | `active` | After `load`, when the entire scenario tree is rehydrated | -## Solana-only events +## Runtime events | Event | Fields | Trigger | | --- | --- | --- | @@ -30,14 +30,13 @@ The `/ws` route emits `ServerMessage` events (JSON, tagged on a `type` field) wh ## Client → server -The only message the client can send is a `search` query consumed by `crates/ilold-web/src/ws/search.rs`. Responses come back as `search_result` (one per match) and `search_complete` (`total` count). +The current `/ws` socket is server-push only: clients receive events, the only thing they may send back is a `Close` frame. Programmatic interaction with the backend goes through `POST /api/cmd`. ## PTY bridge -`GET /ws/pty` opens a PTY for the embedded REPL in the web canvas. The protocol is binary-passthrough; the wire format is documented inline in `crates/ilold-web/src/ws/pty.rs`. +`GET /ws/pty` opens a PTY for the embedded REPL in the web canvas. The protocol is binary passthrough; the wire format is documented inline in `crates/ilold-web/src/ws/pty.rs`. ## Related pages - [HTTP API](./api-endpoints.md) -- [Solana REPL: Scenarios](../solana/repl/scenarios.md): the source of the scenario events. -- [Solidity REPL: Scenarios](../solidity/repl/scenarios.md): same events on the Solidity side. +- [Scenarios](../solana/repl/scenarios.md): the source of the scenario events. diff --git a/docs/guide/src/roadmap/cross-cutting.md b/docs/guide/src/roadmap/cross-cutting.md index 2cae727..bbe62e0 100644 --- a/docs/guide/src/roadmap/cross-cutting.md +++ b/docs/guide/src/roadmap/cross-cutting.md @@ -6,5 +6,4 @@ Elozer is our in-house static analyzer. It produces a typed AST for smart-contra ## Related -- [Solidity: future work](./solidity.md) - [Solana: future work](./solana.md) diff --git a/docs/guide/src/roadmap/solana.md b/docs/guide/src/roadmap/solana.md index c3bf976..a7b7163 100644 --- a/docs/guide/src/roadmap/solana.md +++ b/docs/guide/src/roadmap/solana.md @@ -8,7 +8,7 @@ Plug Elozer, our in-house static analyzer, into ilold to produce a typed AST for ## CFG on top of the AST -Build the control-flow graph layer on the Elozer AST. Brings Solana to parity with the Solidity CFG view and unlocks `slice`, `trace`, and structural narratives. +Build the control-flow graph layer on the Elozer AST. Unlocks `slice`, `trace`, and structural narratives over Anchor handlers. ## Detector engine @@ -18,9 +18,9 @@ Detectors for known Sealevel attack patterns (missing signer checks, missing own Record concrete values at each VM instruction boundary so the dynamic trace can confirm or refute hypotheses produced by the static layer. -## CFG visual parity on the canvas +## CFG visual on the canvas -The web canvas renders Solana state today as a flat bipartite graph (instructions ↔ accounts). The redesigned view will mirror the Solidity CFG: per-instruction control flow, branch nodes, constraint annotations. +The web canvas renders Solana state today as a flat bipartite graph (instructions ↔ accounts). The redesigned view adds a per-instruction control-flow layer with branch nodes and constraint annotations once the CFG layer above is in place. ## CPI graph in the UI @@ -28,7 +28,7 @@ The runtime already records CPI edges (`coverage` surfaces them in text). A dedi ## Sequence narrative -`sequence` is aliased to `session` on Solana today. A true narrative engine reuses the existing `coupling` aggregate plus a renderer mirroring the Solidity output. +`sequence` is aliased to `session` today. A true narrative engine on top of the CFG + `coupling` aggregate is tracked under Phase 2. ## Open to ideas diff --git a/docs/guide/src/solana/limitations.md b/docs/guide/src/solana/limitations.md index 7d28382..46583b2 100644 --- a/docs/guide/src/solana/limitations.md +++ b/docs/guide/src/solana/limitations.md @@ -38,10 +38,9 @@ Cross-program CPI calls are exercised correctly by the VM and surface in logs, b ## Flat bipartite CFG visual -The web canvas renders Solana state as a flat bipartite graph (instructions ↔ accounts). The Solidity-style CFG visual is not implemented; See [Solana roadmap](../roadmap/solana.md) for CFG visual parity. +The web canvas renders Solana state as a flat bipartite graph (instructions ↔ accounts). A per-instruction control-flow view is not implemented; see [Solana roadmap](../roadmap/solana.md) for the planned CFG layer. ## Related pages - [Roadmap: Solana Phase 2](../roadmap/solana.md) -- [Solidity limitations](../solidity/limitations.md) - [Reference: HTTP API](../reference/api-endpoints.md) diff --git a/docs/guide/src/solana/overview.md b/docs/guide/src/solana/overview.md index 00ff96a..d4fa812 100644 --- a/docs/guide/src/solana/overview.md +++ b/docs/guide/src/solana/overview.md @@ -18,25 +18,25 @@ Two CLI entry points cover Solana: `ilold explore <project>` (REPL + API) and `i The committed fixtures live under `tests/fixtures/solana/staking` (single program) and `tests/fixtures/solana/cpi` (two programs that talk to each other through CPI). Both ship pre-built `bin/<program>.so` binaries so the suite runs without the Anchor toolchain. -## Solidity vs Solana mental model - -| Concept | Solidity | Solana | -| --- | --- | --- | -| Entry point | function on a contract | instruction on a program | -| Persistent state | contract state variables | accounts owned by the program | -| Caller identity | `msg.sender` (implicit) | signers passed by the client | -| `who <X>` | reads/writes of a state variable (CFG-based) | instructions that touch an account type (IDL heuristic) | -| `timeline <X>` | mutation history of a state variable | mutation history of an account pubkey, decoded | -| `step <i>` | re-renders the persisted flow tree | re-prints CU, logs, account diffs | -| `slice` / `trace` | full CFG-based analysis | not implemented (Phase 2) | -| `sequence` | narrative with cross-step dependencies | aliased to `session` (no narrative engine yet) | -| Execution | symbolic (CFG + paths) | concrete (in-process LiteSVM execution) | -| `back` | drops the step from the timeline | drops the step AND rewinds the VM to the pre-call snapshot | -| `save` / `load` | step list + persisted paths | step list + replay-driven VM reconstruction | +## Mental model + +| Concept | How ilold handles it | +| --- | --- | +| Entry point | instruction on a program | +| Persistent state | accounts owned by the program | +| Caller identity | signers passed by the client | +| `who <X>` | instructions that touch an account type (IDL heuristic for fields) | +| `timeline <X>` | mutation history of an account pubkey, decoded | +| `step <i>` | re-prints CU, logs, and account diffs of the step | +| `back` | drops the step AND rewinds the VM to the pre-call snapshot | +| `save` / `load` | step list + replay-driven VM reconstruction | +| Execution | concrete (in-process LiteSVM execution) | + +Structural commands (`slice`, `trace`, `sequence` narrative) are not implemented and are tracked in Phase 2. ## REPL command groups -The REPL command surface mirrors the Solidity one with backend-specific extensions. Each group has its own page: +Each group has its own page: - [Session](./repl/session.md): `c/call`, `b/back`, `cl/clear`, `s/session`, `state`, `st/step`. - [Programs and IDL](./repl/programs.md): `ct/programs`, `use`, `f/funcs`, `fa/funcs-all`, `i/info`, `v/vars`, `va/vars-all`. @@ -49,5 +49,5 @@ The REPL command surface mirrors the Solidity one with backend-specific extensio ## Workflows -- [Audit walkthrough](./workflows/audit-walkthrough.md): staking program end-to-end, paralleling the Solidity walkthrough. +- [Audit walkthrough](./workflows/audit-walkthrough.md): staking program end-to-end. - [Scenarios and forks](./workflows/scenarios.md): branching VMs, rewinding the clock, persisting bundles. diff --git a/docs/guide/src/solana/repl/analysis.md b/docs/guide/src/solana/repl/analysis.md index 6b1b72d..2ea4f36 100644 --- a/docs/guide/src/solana/repl/analysis.md +++ b/docs/guide/src/solana/repl/analysis.md @@ -127,5 +127,5 @@ Coverage is the closest current surrogate for "have I exercised every instructio ## Notes -- See [Solana: Limitations](../limitations.md) for the static-analysis gap (no CFG → no `slice` / `trace` yet). -- The Solidity counterparts of `who` and `timeline` work on state variables; the Solana versions work on account types and pubkeys respectively. The mental shift is the same ("what touches this piece of state?") but the units of state are different. +- See [Limitations](../limitations.md) for the static-analysis gap (no CFG → no `slice` / `trace` yet). +- `who` works against account types (and fields with a snake_case → PascalCase heuristic). `timeline` works against decoded account pubkeys. diff --git a/docs/guide/src/solana/repl/findings.md b/docs/guide/src/solana/repl/findings.md index 2187d29..0d1d2df 100644 --- a/docs/guide/src/solana/repl/findings.md +++ b/docs/guide/src/solana/repl/findings.md @@ -69,7 +69,7 @@ ilold[staking]> status claim_rewards finding ✓ status updated ``` -Note: Solana statuses are intentionally narrower than the Solidity equivalent: only `open`, `reviewed`, `finding` (alias `found`). Solidity supports `reviewed`, `suspicious`, `vulnerable`, `clean`, `inprogress`, `notreviewed`. +Note: the supported statuses are `open`, `reviewed`, `finding` (alias `found`). **Returns:** `StatusUpdated`. @@ -103,4 +103,3 @@ ilold[staking]> export --auditor="Alba S." --version=v1.2 --date=2026-05-09 ## Notes - Findings are scoped to the scenario they were recorded in but the export merges all of them. -- The Solidity equivalent (see [Solidity: Findings](../../solidity/repl/findings.md)) does not support the `--rec=`, `--auditor=`, `--version=`, `--date=` flags; the report there is simpler. diff --git a/docs/guide/src/solana/repl/help.md b/docs/guide/src/solana/repl/help.md index cf2adaa..105c47d 100644 --- a/docs/guide/src/solana/repl/help.md +++ b/docs/guide/src/solana/repl/help.md @@ -75,4 +75,4 @@ Exits the REPL. `Ctrl+D` and `Ctrl+C` also work. ## Notes - The full list of registered command aliases is enforced by the test `every_solana_command_has_a_help_block` in `crates/ilold-cli/src/help.rs`. New commands without a corresponding HelpBlock break the build. -- The Solidity REPL uses a flat one-line inline-help table (see `print_inline_help`); Solana uses the structured HelpBlock format above. Both respond to the `<cmd>?` trailing syntax. +- Append `?` to any command for its structured HelpBlock (syntax, flags, examples, return shape, related commands). diff --git a/docs/guide/src/solana/repl/programs.md b/docs/guide/src/solana/repl/programs.md index 57df9a2..123fe5d 100644 --- a/docs/guide/src/solana/repl/programs.md +++ b/docs/guide/src/solana/repl/programs.md @@ -116,5 +116,4 @@ Aliased to `vars` at the dispatcher level: same command, same output. ## Notes -- The Solidity counterpart of these commands is documented under [Contract](../../solidity/repl/contract.md). The shapes line up so an auditor moving between backends sees the same structure. - `use` clears the displayed step list for the previous program. The underlying scenario state for that program is preserved and reappears when switching back. diff --git a/docs/guide/src/solana/repl/runtime.md b/docs/guide/src/solana/repl/runtime.md index 42f781e..7e62413 100644 --- a/docs/guide/src/solana/repl/runtime.md +++ b/docs/guide/src/solana/repl/runtime.md @@ -1,6 +1,6 @@ # Solana Runtime Commands -These commands operate directly on the LiteSVM owned by the active scenario. They have no Solidity counterpart. +These commands operate directly on the LiteSVM owned by the active scenario. ## users diff --git a/docs/guide/src/solana/repl/scenarios.md b/docs/guide/src/solana/repl/scenarios.md index a495cbd..be66579 100644 --- a/docs/guide/src/solana/repl/scenarios.md +++ b/docs/guide/src/solana/repl/scenarios.md @@ -76,5 +76,4 @@ ilold[staking]> sc delete attack - `back` and `clear` rewind the VM of the **active scenario only**, never the fork's parent. Diverging via `fork` is the only way to keep both timelines side by side. - `time-warp` is a per-scenario side effect on the `Clock` sysvar, but is not reverted by `back`. -- See [Solana: Scenarios and forks](../workflows/scenarios.md) for an end-to-end workflow. -- The Solidity counterpart is documented at [Solidity: Scenarios](../../solidity/repl/scenarios.md); the command surface is identical, but Solidity scenarios share the parsed model (no VM clone needed). +- See [Scenarios and forks](../workflows/scenarios.md) for an end-to-end workflow. diff --git a/docs/guide/src/solana/repl/session.md b/docs/guide/src/solana/repl/session.md index ff2c3a6..db851a1 100644 --- a/docs/guide/src/solana/repl/session.md +++ b/docs/guide/src/solana/repl/session.md @@ -130,5 +130,5 @@ ilold[staking → initialize_pool → stake]> step 1 ## Notes -- The Solidity equivalent (`seq` / `sequence`) is currently aliased to `session` on Solana; there is no cross-step narrative engine yet. See [Roadmap](../../roadmap/solana.md). +- `seq` / `sequence` is currently aliased to `session`; a cross-step narrative engine is tracked in the [Roadmap](../../roadmap/solana.md). - `call` is the only command that drives the VM forward; everything else inspects or rewinds. diff --git a/docs/guide/src/solana/workflows/audit-walkthrough.md b/docs/guide/src/solana/workflows/audit-walkthrough.md index 2dd6411..1948104 100644 --- a/docs/guide/src/solana/workflows/audit-walkthrough.md +++ b/docs/guide/src/solana/workflows/audit-walkthrough.md @@ -1,6 +1,6 @@ # Solana Audit Walkthrough -This walkthrough mirrors the [Solidity audit walkthrough](../../solidity/workflows/audit-walkthrough.md) against the canonical Solana staking fixture under `tests/fixtures/solana/staking`. The program exposes five instructions — `initialize_pool`, `stake`, `unstake`, `add_rewards`, `claim_rewards` — and ships with a pre-built `bin/staking.so` so the suite runs without the Anchor toolchain. +This walkthrough exercises the canonical Solana staking fixture under `tests/fixtures/solana/staking`. The program exposes five instructions — `initialize_pool`, `stake`, `unstake`, `add_rewards`, `claim_rewards` — and ships with a pre-built `bin/staking.so` so the suite runs without the Anchor toolchain. ## Starting the session @@ -165,18 +165,16 @@ ilold[staking]> load my-audit `--with-keypairs` is mandatory when the audit relies on deterministic PDAs (which depend on signer pubkeys): without it, `load` regenerates fresh keypairs and PDAs come back at different addresses. -## Parallel to the Solidity walkthrough +## Flow recap -The flow is structurally identical to the Solidity one: - -1. `f` / `v` to map the surface (instructions and account types instead of functions and state variables). -2. `users new` + `call` to push the VM forward (vs. `c <func>` on a parsed CFG). +1. `f` / `v` to map the surface (instructions and account types). +2. `users new` + `call` to push the VM forward. 3. `state`, `step`, `timeline` to inspect what changed. -4. `who` to navigate cross-instruction relationships (vs. cross-function in Solidity). +4. `who` to navigate cross-instruction relationships. 5. `finding`, `note`, `status` to record observations. 6. `export`, `save`, `load` to ship the deliverable and resume later. -What is **not** available yet on Solana: `slice` and `trace`. Both require the Anchor handler AST and are tracked in [Roadmap: Solana Phase 2](../../roadmap/solana.md). +What is **not** available yet: `slice` and `trace`. Both require the Anchor handler AST and are tracked in [Roadmap: Solana Phase 2](../../roadmap/solana.md). ## Related pages From fa7360ea0ef2d881e250fe15256bf206d5d88d99 Mon Sep 17 00:00:00 2001 From: scab24 <git.seco@protonmail.com> Date: Sun, 17 May 2026 20:19:44 +0200 Subject: [PATCH 10/10] fix(frontend): bug in FunctionSidebar derived; drop solidity-only paths Fixes FunctionSidebar.svelte::allRows which used the legacy $derived<T>(callback) form (Svelte 5 incompatible) and was called with parens, causing a runtime crash whenever the sidebar rendered. Switches to $derived.by, drops the now-impossible Solidity branch in contract/[name]/+page.svelte onMount, prunes contracts and relationships from the ProjectMap TS interface (the backend no longer returns them), and removes the orphaned petgraph workspace dependency. --- Cargo.toml | 1 - crates/ilold-web/frontend/src/lib/api/rest.ts | 2 - .../contract/FunctionSidebar.svelte | 11 ++-- .../src/routes/contract/[name]/+page.svelte | 50 ++++++------------- 4 files changed, 19 insertions(+), 45 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 89258a3..e4f1a1c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,7 +20,6 @@ homepage = "https://github.com/scab24/ilold" description = "Solana program execution path analyzer and interactive security workbench" [workspace.dependencies] -petgraph = "0.8" serde = { version = "1", features = ["derive"] } serde_json = "1" thiserror = "2" diff --git a/crates/ilold-web/frontend/src/lib/api/rest.ts b/crates/ilold-web/frontend/src/lib/api/rest.ts index 02739d5..3174d3c 100644 --- a/crates/ilold-web/frontend/src/lib/api/rest.ts +++ b/crates/ilold-web/frontend/src/lib/api/rest.ts @@ -77,9 +77,7 @@ export interface SearchSuggestions { export interface ProjectMap { kind: string; - contracts: MapContract[]; programs: MapProgram[]; - relationships: MapRelationship[]; } export interface MapProgram { diff --git a/crates/ilold-web/frontend/src/lib/components/contract/FunctionSidebar.svelte b/crates/ilold-web/frontend/src/lib/components/contract/FunctionSidebar.svelte index bd79693..5ec8f1a 100644 --- a/crates/ilold-web/frontend/src/lib/components/contract/FunctionSidebar.svelte +++ b/crates/ilold-web/frontend/src/lib/components/contract/FunctionSidebar.svelte @@ -50,7 +50,7 @@ hasPdas?: boolean; signers?: string[]; }; - const allRows = $derived<Row[]>(() => { + const allRows = $derived.by<Row[]>(() => { if (kind === 'solana' && program) { return (program.instructions ?? []).map((ix): Row => ({ name: ix.name, @@ -85,8 +85,7 @@ const filtered = $derived.by(() => { const q = query.trim().toLowerCase(); - const rows = allRows(); - return rows.filter((r) => { + return allRows.filter((r: Row) => { if (q && !r.name.toLowerCase().includes(q)) return false; if (kind === 'solana') { if (onlyPdas && !r.hasPdas) return false; @@ -99,9 +98,9 @@ }); }); - const ownFiltered = $derived(filtered.filter((r) => r.source === 'own')); - const inheritedFiltered = $derived(filtered.filter((r) => r.source === 'inherited')); - const totalCount = $derived(allRows().length); + const ownFiltered = $derived(filtered.filter((r: Row) => r.source === 'own')); + const inheritedFiltered = $derived(filtered.filter((r: Row) => r.source === 'inherited')); + const totalCount = $derived(allRows.length); const visibleCount = $derived(filtered.length); const canvasCount = $derived(canvasFuncs.size); const canClear = $derived(mode !== 'session' && canvasCount > 0); diff --git a/crates/ilold-web/frontend/src/routes/contract/[name]/+page.svelte b/crates/ilold-web/frontend/src/routes/contract/[name]/+page.svelte index c9540d1..6e8d853 100644 --- a/crates/ilold-web/frontend/src/routes/contract/[name]/+page.svelte +++ b/crates/ilold-web/frontend/src/routes/contract/[name]/+page.svelte @@ -984,47 +984,25 @@ clearUserLabels(); setSearchContext(contractName); try { - const pm = await getProjectMap(); + await getProjectMap(); if (!stillFresh()) return; kind = 'solana'; - if (kind === 'solana') { - try { - const prog = await getProgramView(contractName); - if (!stillFresh()) return; - solanaProgram = prog; - } catch { - if (stillFresh()) error = `Program "${contractName}" not found`; - return; - } - projectMap = []; - await refreshSolanaUsers(); - const scenario = getActiveScenario(); - await loadRuntimeOverlay(contractName, scenario); - if (scenario) await loadUserLabels(scenario); - paintCpiEdges(); - return; - } - projectMap = pm.contracts ?? []; - const ctr = await getContract(contractName); - if (!stillFresh()) return; - contract = ctr; - const callgraphData = await getCallGraph(contractName); - if (!stillFresh()) return; - callgraphRaw = callgraphData; try { - const tree = await getSequences(contractName); - if (stillFresh()) seqTree = tree; - } catch (e) { - if (stillFresh() && kind !== 'solana') console.warn('getSequences failed:', e); - } - try { - const analysis = await getSequenceAnalysis(contractName); - if (stillFresh()) seqAnalysis = analysis; - } catch (e) { - if (stillFresh() && kind !== 'solana') console.warn('getSequenceAnalysis failed:', e); + const prog = await getProgramView(contractName); + if (!stillFresh()) return; + solanaProgram = prog; + } catch { + if (stillFresh()) error = `Program "${contractName}" not found`; + return; } + projectMap = []; + await refreshSolanaUsers(); + const scenario = getActiveScenario(); + await loadRuntimeOverlay(contractName, scenario); + if (scenario) await loadUserLabels(scenario); + paintCpiEdges(); } catch (e) { - if (stillFresh()) error = `Contract "${contractName}" not found`; + if (stillFresh()) error = `Program "${contractName}" not found`; } });