diff --git a/experiments/atomic-codemode/README.md b/experiments/atomic-codemode/README.md index cd24661b02..d81dfcb3ac 100644 --- a/experiments/atomic-codemode/README.md +++ b/experiments/atomic-codemode/README.md @@ -291,5 +291,7 @@ repeat, answer both local guards correctly without a whole-report fetch. Code Mode uses fewer MCP requests, but whole-task costs are mixed and a large capture time variation remains unexplained. Both clients already provide JavaScript orchestration. Source fidelity is not machine equivalence, provider parity or a -general speedup. Case labels on the 149-way recovered dispatch remain a fact-export -question; they cannot be inferred from edge ordinals. +general speedup. The [native switch experiment](switch-study.md) now exports and independently checks +182 labels over those 149 targets, exposes selected case-to-edge relations and +keeps them readable when a 2 MiB native output budget omits the full graph. +New real-model case/default verification is pending. diff --git a/experiments/atomic-codemode/api.mjs b/experiments/atomic-codemode/api.mjs index e1e0d21b2e..8731d0e9b4 100644 --- a/experiments/atomic-codemode/api.mjs +++ b/experiments/atomic-codemode/api.mjs @@ -50,6 +50,14 @@ const definition = (description, input, method, example) => ({ }); const operationSeeds = z.array(z.string()).min(1).max(128); const relationInput = z.discriminatedUnion("kind", [ + z.strictObject({ + snapshot, + kind: z.literal("cases"), + seeds: operationSeeds, + case_values: z.array(hex).max(128).optional(), + case_indices: z.array(z.number().int().nonnegative()).max(128).optional(), + ...page, + }), z.strictObject({ snapshot, kind: z.literal("operands"), @@ -71,6 +79,18 @@ const relationInput = z.discriminatedUnion("kind", [ }), ]); export const methods = { + switches: definition( + "Inspect recovered indirect branches and their independent native jump-table metadata, without returning all case rows. Selector is the current recovered BRANCHIND input, which may be normalized rather than the machine jump address. Native consumed mask and signed rendering hint are not complete runtime/signedness proofs. Null tables/defaults stay unknown. The native model default out-edge is separate from any machine out-of-range guard destination. Use relations kind:cases for selected raw table rows, labels and their native mapping to recovered edges; row/edge ordinals are not case values. Available even when the full graph is unavailable if this independent facet was captured.", + { + snapshot, + ids: z.array(z.string()).max(128).optional(), + origin_addresses: z.array(hex).max(128).optional(), + fields: z.array(z.enum(["selector", "table"])).optional(), + ...page, + }, + "switches", + 'await rea.switches({snapshot:s,origin_addresses:["0x1000"]})', + ), functions: definition( "Search retained native callable entries by literal name/alias, exact name, or entry addresses. Names can match several entries, including PLT/import stubs and implementations; preserve all matches and select by address. A name does not establish runtime symbol binding.", { @@ -158,7 +178,7 @@ export const methods = { ), relations: { description: - "Read native operands or definitions/uses for operation IDs, incoming/outgoing CFG edges or derived controllers for block IDs. operands pages every selected input slot, including constants, function inputs and null slots; input_indices optionally selects slots in caller order. Identity is snapshot + operation + input_index; value_id/definition and exact phi predecessor/edge bindings remain separate. Out-of-range slots of retained operations are absent; unretained_seeds remain unknown. A bounded call-rooted projection supports operands/definitions/uses but no CFG; uses cover retained selected operations only. Diagnostic SSA lacks native operands and has partial CFG/printed occurrences. controllers requires a complete recovered graph and concerns finite paths to recovered exits, not runtime feasibility.", + "Read native operands, definitions/uses or case rows for operation IDs; incoming/outgoing CFG edges or derived controllers for block IDs. cases pages one relation per case/out-edge, with a stable case_id; an unmapped case has one explicit unknown-edge row. total counts relations, not table rows. cases filters optional case_values (native labels tagged value) and case_indices (original row ordinals); both filters intersect and paging spans deduplicated seeds in caller order. Rows preserve missing/no-label status, original recovered target, and separate recovered edges. Partial/unavailable tables and unmapped rows are explicit; no runtime feasibility claim. operands pages every selected input slot, including constants, function inputs and null slots; input_indices optionally selects slots in caller order. Identity is snapshot + operation + input_index; value_id/definition and exact phi predecessor/edge bindings remain separate. Out-of-range slots of retained operations are absent; unretained_seeds remain unknown. A bounded call-rooted projection supports operands/definitions/uses but no CFG; uses cover retained selected operations only. Diagnostic SSA lacks native operands and has partial CFG/printed occurrences. controllers requires a complete recovered graph and concerns finite paths to recovered exits, not runtime feasibility.", input: relationInput, method: "relations", example: diff --git a/experiments/atomic-codemode/api.test.mjs b/experiments/atomic-codemode/api.test.mjs index 327463cfe2..598052d02f 100644 --- a/experiments/atomic-codemode/api.test.mjs +++ b/experiments/atomic-codemode/api.test.mjs @@ -29,6 +29,17 @@ test("relation identity and operand-only selectors agree in runtime and advertis [{ kind: "operands", seeds: ["0x1:0"], input_indices: [0, 155] }, true], [{ kind: "operands", seeds: ["0x1:0"], input_indices: [] }, true], [{ kind: "uses", seeds: ["0x1:0"] }, true], + [ + { + kind: "cases", + seeds: ["0x1:0"], + case_values: ["0xffffffffffffffff"], + case_indices: [0, 155], + }, + true, + ], + [{ kind: "cases", seeds: [0] }, false], + [{ kind: "cases", seeds: ["0x1:0"], case_values: ["0X1"] }, false], [{ kind: "incoming", seeds: [0] }, true], [{ kind: "incoming", seeds: ["0"] }, false], [{ kind: "operands", seeds: [0] }, false], diff --git a/experiments/atomic-codemode/kuna-native-ir.patch b/experiments/atomic-codemode/kuna-native-ir.patch index 1e10aa667e..2ec9a073ca 100644 --- a/experiments/atomic-codemode/kuna-native-ir.patch +++ b/experiments/atomic-codemode/kuna-native-ir.patch @@ -167,10 +167,10 @@ index 00000000..48631d39 +} diff --git a/decompiler/crates/kuna-cli/src/atomic_json.rs b/decompiler/crates/kuna-cli/src/atomic_json.rs new file mode 100644 -index 00000000..7077bc56 +index 00000000..6005472b --- /dev/null +++ b/decompiler/crates/kuna-cli/src/atomic_json.rs -@@ -0,0 +1,359 @@ +@@ -0,0 +1,456 @@ +//! Opt-in atomic documents share the ordinary record producer and JSON writer. +//! A graph that cannot fit must not discard otherwise readable reconstructed C. + @@ -291,6 +291,25 @@ index 00000000..7077bc56 + text.push('\n'); + Some(text) +} ++// Reclaim a saved independent facet when other omissions have made room. ++// Only bounded encoded strings coexist here, never cloned graph/facet trees. ++fn bounded_reclaiming( ++ document: &mut Json, ++ budget: usize, ++ name: &str, ++ saved: &mut Vec, ++) -> Option { ++ let base = bounded(document, budget)?; ++ if saved.is_empty() { ++ return Some(base); ++ } ++ restore_facet(document, std::mem::take(saved)); ++ if let Some(complete) = bounded(document, budget) { ++ return Some(complete); ++ } ++ *saved = take_facet(document, name, budget + 1, Omission::Capacity); ++ Some(base) ++} +pub(crate) fn render(mut document: Json, limit: usize) -> Result { + let budget = limit + .checked_sub(1) @@ -307,7 +326,7 @@ index 00000000..7077bc56 + return Ok(text); + } + let originals = compress_graphs(&mut document, budget); -+ let alternatives = if originals.is_empty() { ++ let mut alternatives = if originals.is_empty() { + Vec::new() + } else { + if let Some(text) = bounded(&document, budget) { @@ -315,24 +334,50 @@ index 00000000..7077bc56 + } + exchange_graphs(&mut document, originals) + }; -+ let calls = take_facet(&mut document, "atomic_calls", limit, Omission::Capacity); -+ if !calls.is_empty() { ++ // New switch rows must not displace previously available graph/call facts. ++ // Retry without that independent facet before weakening existing coverage. ++ let mut switches = take_facet(&mut document, "atomic_switches", limit, Omission::Capacity); ++ if !switches.is_empty() { + if let Some(text) = bounded(&document, budget) { + return Ok(text); + } + if !alternatives.is_empty() { -+ drop(exchange_graphs(&mut document, alternatives)); ++ let originals = exchange_graphs(&mut document, alternatives); + if let Some(text) = bounded(&document, budget) { + return Ok(text); + } ++ alternatives = exchange_graphs(&mut document, originals); ++ } ++ } ++ let calls = take_facet(&mut document, "atomic_calls", limit, Omission::Capacity); ++ if !calls.is_empty() { ++ if let Some(text) = ++ bounded_reclaiming(&mut document, budget, "atomic_switches", &mut switches) ++ { ++ return Ok(text); ++ } ++ if !alternatives.is_empty() { ++ drop(exchange_graphs(&mut document, alternatives)); ++ if let Some(text) = ++ bounded_reclaiming(&mut document, budget, "atomic_switches", &mut switches) ++ { ++ return Ok(text); ++ } + } + } + restore_facet(&mut document, calls); + take_facet(&mut document, "atomic_facts", limit, Omission::Capacity); + restore_facet(&mut document, projection); ++ restore_facet(&mut document, std::mem::take(&mut switches)); + if let Some(text) = bounded(&document, budget) { + return Ok(text); + } ++ switches = take_facet(&mut document, "atomic_switches", limit, Omission::Capacity); ++ if !switches.is_empty() { ++ if let Some(text) = bounded(&document, budget) { ++ return Ok(text); ++ } ++ } + // Independent calls retain priority when the combined fallback is too big. + let projection = take_facet( + &mut document, @@ -340,12 +385,14 @@ index 00000000..7077bc56 + limit, + Omission::Capacity, + ); -+ if let Some(text) = bounded(&document, budget) { ++ if let Some(text) = bounded_reclaiming(&mut document, budget, "atomic_switches", &mut switches) ++ { + return Ok(text); + } + restore_facet(&mut document, projection); + take_facet(&mut document, "atomic_calls", limit, Omission::Capacity); -+ if let Some(text) = bounded(&document, budget) { ++ if let Some(text) = bounded_reclaiming(&mut document, budget, "atomic_switches", &mut switches) ++ { + return Ok(text); + } + take_facet( @@ -354,12 +401,62 @@ index 00000000..7077bc56 + limit, + Omission::Capacity, + ); -+ bounded(&document, budget).ok_or_else(|| { ++ bounded_reclaiming(&mut document, budget, "atomic_switches", &mut switches).ok_or_else(|| { + format!("atomic JSON exceeds {limit} byte capacity even without recovered facets") + }) +} + +#[cfg(test)] ++mod switch_facet_tests { ++ use super::*; ++ fn document(graph: usize, calls: usize, projection: usize, switches: usize) -> Json { ++ Json::Object(vec![( ++ "functions".into(), ++ Json::Array(vec![Json::Object(vec![ ++ ("code".into(), Json::Str("retained C".into())), ++ ("atomic_facts".into(), Json::Str("g".repeat(graph))), ++ ("atomic_calls".into(), Json::Str("c".repeat(calls))), ++ ( ++ "atomic_projection".into(), ++ Json::Str("p".repeat(projection)), ++ ), ++ ("atomic_switches".into(), Json::Str("s".repeat(switches))), ++ ])]), ++ )]) ++ } ++ #[test] ++ fn oversized_switches_do_not_displace_existing_graph_and_calls() { ++ let text = render(document(600, 20, 10, 5000), 1500).unwrap(); ++ let d: serde_json::Value = serde_json::from_str(&text).unwrap(); ++ let f = &d["functions"][0]; ++ assert_eq!(f["code"], "retained C"); ++ assert_eq!(f["atomic_facts"].as_str().unwrap().len(), 600); ++ assert_eq!(f["atomic_calls"].as_str().unwrap().len(), 20); ++ assert!(f.get("atomic_switches").is_none()); ++ assert!(f["atomic_switches_error"].is_string()); ++ } ++ #[test] ++ fn reclaim_switches_after_a_large_projection_is_omitted() { ++ let text = render(document(5000, 100, 3000, 100), 2000).unwrap(); ++ let d: serde_json::Value = serde_json::from_str(&text).unwrap(); ++ let f = &d["functions"][0]; ++ assert_eq!(f["atomic_calls"].as_str().unwrap().len(), 100); ++ assert_eq!(f["atomic_switches"].as_str().unwrap().len(), 100); ++ assert!(f.get("atomic_projection").is_none()); ++ assert!(f.get("atomic_switches_error").is_none()); ++ } ++ #[test] ++ fn independent_switch_rows_survive_when_all_other_facets_overflow() { ++ let text = render(document(5000, 5000, 5000, 1200), 2000).unwrap(); ++ let d: serde_json::Value = serde_json::from_str(&text).unwrap(); ++ let f = &d["functions"][0]; ++ assert_eq!(f["code"], "retained C"); ++ assert_eq!(f["atomic_switches"].as_str().unwrap().len(), 1200); ++ assert!(f.get("atomic_switches_error").is_none()); ++ } ++} ++ ++#[cfg(test)] +mod tests { + use super::*; + @@ -668,7 +765,7 @@ index ad249b04..62074996 100644 failure = Some(format!("--assert directive rejected in {}", args.binary)); } diff --git a/decompiler/crates/kuna-cli/src/decompile_all.rs b/decompiler/crates/kuna-cli/src/decompile_all.rs -index 698c7243..330c0f3f 100644 +index 698c7243..21147ba5 100644 --- a/decompiler/crates/kuna-cli/src/decompile_all.rs +++ b/decompiler/crates/kuna-cli/src/decompile_all.rs @@ -744,6 +744,7 @@ fn run_jobs_worker(args: &Args) -> Result<(), String> { @@ -724,19 +821,20 @@ index 698c7243..330c0f3f 100644 /// [`render_result_json`] plus the inventory `total` a NARROWED whole-binary run /// answers with. /// -@@ -2152,6 +2168,11 @@ fn result_json( +@@ -2152,6 +2168,12 @@ fn result_json( // Always present, `[]` unless the option is on. ("types".into(), types), ]; + if let Some(detail) = f.detail.as_ref() { + append_atomic_facet(&mut record, "atomic_facts", detail.atomic_facts.as_ref()); + append_atomic_facet(&mut record, "atomic_calls", detail.atomic_calls.as_ref()); ++ append_atomic_facet(&mut record, "atomic_switches", detail.atomic_switches.as_ref()); + append_atomic_facet(&mut record, "atomic_projection", detail.atomic_projection.as_ref()); + } if !f.stack_objects.is_empty() { record.push(("stack_objects".into(), Json::Array( f.stack_objects.iter().map(stack_object_json).collect(), -@@ -3519,3 +3540,16 @@ mod triage_tests { +@@ -3519,3 +3541,16 @@ mod triage_tests { } } @@ -1053,20 +1151,21 @@ index e40b18c9..b8cae233 100644 }; diff --git a/decompiler/crates/kuna-console/src/inspect.rs b/decompiler/crates/kuna-console/src/inspect.rs -index 74024b10..72e4ec24 100644 +index 74024b10..a41465a6 100644 --- a/decompiler/crates/kuna-console/src/inspect.rs +++ b/decompiler/crates/kuna-console/src/inspect.rs -@@ -24,6 +24,9 @@ pub const FUNCTION_ROW_CAP: usize = 4096; +@@ -24,6 +24,10 @@ pub const FUNCTION_ROW_CAP: usize = 4096; pub struct FuncDetail { pub tokens: Vec, pub tokens_error: Option, + pub atomic_facts: Option>, + pub atomic_calls: Option>, ++ pub atomic_switches: Option>, + pub atomic_projection: Option>, } impl FuncDetail { -@@ -40,13 +43,13 @@ impl FuncDetail { +@@ -40,13 +44,13 @@ impl FuncDetail { ) -> Self { let mut tokens = kuna_srcmap::resolve(fd, var_refs, raw, untrimmed, variables); if let Err(error) = kuna_srcmap::verify(&tokens, code) { @@ -1083,7 +1182,7 @@ index 74024b10..72e4ec24 100644 } diff --git a/decompiler/crates/kuna-console/src/project.rs b/decompiler/crates/kuna-console/src/project.rs -index bc401db2..1b53ccf1 100644 +index bc401db2..7b7c7c24 100644 --- a/decompiler/crates/kuna-console/src/project.rs +++ b/decompiler/crates/kuna-console/src/project.rs @@ -245,6 +245,8 @@ pub struct DecompileOptions { @@ -1119,7 +1218,7 @@ index bc401db2..1b53ccf1 100644 want_pointer_arguments, .. } = *opts; -@@ -838,11 +843,21 @@ pub fn decompile_pulled( +@@ -838,11 +843,23 @@ pub fn decompile_pulled( } } let var_refs = provenance.apply_to_variables_with_refs(&fd, &mut variables); @@ -1134,6 +1233,8 @@ index bc401db2..1b53ccf1 100644 + let membership = kuna_decomp::atomic_membership::inspect(&fd); + detail.atomic_calls = Some(membership.as_ref().map_err(Clone::clone) + .and_then(|m| kuna_decomp::atomic_calls::collect_all(&fd, m))); ++ detail.atomic_switches = Some(membership.as_ref().map_err(Clone::clone) ++ .and_then(|m| kuna_decomp::atomic_switches::collect(&fd, m))); + detail.atomic_facts = Some(membership.as_ref().map_err(Clone::clone) + .and_then(|m| kuna_decomp::atomic_facts::collect(prog.arch(), &fd, m))); + detail.atomic_projection = Some(membership.as_ref().map_err(Clone::clone) @@ -1159,6 +1260,59 @@ index 40b093b6..8f03b654 100644 toml = { version = "1.1", default-features = false, features = ["parse", "serde", "std"] } [build-dependencies] +diff --git a/decompiler/crates/kuna-decomp/src/p2_lift/jumptable.rs b/decompiler/crates/kuna-decomp/src/p2_lift/jumptable.rs +index b2677f6c..5e913130 100644 +--- a/decompiler/crates/kuna-decomp/src/p2_lift/jumptable.rs ++++ b/decompiler/crates/kuna-decomp/src/p2_lift/jumptable.rs +@@ -5481,6 +5481,22 @@ impl JumpTable { + self.label[index as usize] + } + ++ /// Borrow observed rows for read-only analysis without indexing absent labels. ++ /// Row order, labels and recovered out-edge positions are separate identities. ++ pub fn case_rows(&self) -> KunaResult)>> { ++ if self.label.len() > self.addresstable.len() { ++ return Err(KunaError::lowlevel("jump-table labels exceed address rows")); ++ } ++ Ok(self.addresstable.iter().enumerate().map(|(index, address)| { ++ (index, address, self.label.get(index).copied()) ++ })) ++ } ++ ++ /// Borrow the native row-to-out-edge mapping; never infer it from addresses. ++ pub fn case_edge_rows(&self) -> impl ExactSizeIterator + '_ { ++ self.block2addr.iter().map(|pair| (pair.address_index, pair.block_position)) ++ } ++ + /// Force a given basic-block to be a switch destination + /// (C++ `JumpTable::addBlockToSwitch`, `jumptable.cc:2663`). + pub fn add_block_to_switch(&mut self, fd: &Funcdata, bl: BlockId, lab: uintb) -> KunaResult<()> { +diff --git a/decompiler/crates/kuna-decomp/src/p2_lift/jumptable/tests.rs b/decompiler/crates/kuna-decomp/src/p2_lift/jumptable/tests.rs +index 8aad0172..2f69df2d 100644 +--- a/decompiler/crates/kuna-decomp/src/p2_lift/jumptable/tests.rs ++++ b/decompiler/crates/kuna-decomp/src/p2_lift/jumptable/tests.rs +@@ -725,3 +725,21 @@ fn guard_range_retry_keeps_both_wrapped_bounds() { + model.calc_range(&fd, input, &mut range); + assert_eq!((range.get_min(), range.get_end(), range.get_size()), (58, 62, 4)); + } ++ ++#[test] ++fn atomic_case_rows_keep_ordinals_labels_and_edges_separate() { ++ let manage = build_manager(); ++ let ram = Rc::clone(manage.get_space_by_name("ram").unwrap()); ++ let mut table = JumpTable::new(Address::new(Rc::clone(&ram), 0x1000)); ++ table.kuna_push_entry(Address::new(Rc::clone(&ram), 0x3000), 2, 0x77); ++ table.kuna_push_entry(Address::new(Rc::clone(&ram), 0x4000), 0, NO_LABEL); ++ table.kuna_finalize(0); ++ let rows: Vec<_> = table.case_rows().unwrap() ++ .map(|(index, address, label)| (index, address.get_offset(), label)).collect(); ++ assert_eq!(rows, vec![(0, 0x3000, Some(0x77)), (1, 0x4000, Some(NO_LABEL))]); ++ assert_eq!(table.case_edge_rows().collect::>(), vec![(1, 0), (0, 2)]); ++ table.label.pop(); ++ assert_eq!(table.case_rows().unwrap().nth(1).unwrap().2, None); ++ table.label.extend([1, 2]); ++ assert!(table.case_rows().is_err()); ++} diff --git a/decompiler/crates/kuna-decomp/src/substrate/atomic_calls.rs b/decompiler/crates/kuna-decomp/src/substrate/atomic_calls.rs new file mode 100644 index 00000000..2b7ced64 @@ -1545,21 +1699,23 @@ index 00000000..6b9afe69 +} diff --git a/decompiler/crates/kuna-decomp/src/substrate/atomic_membership.rs b/decompiler/crates/kuna-decomp/src/substrate/atomic_membership.rs new file mode 100644 -index 00000000..6628ddd7 +index 00000000..be11e472 --- /dev/null +++ b/decompiler/crates/kuna-decomp/src/substrate/atomic_membership.rs -@@ -0,0 +1,64 @@ +@@ -0,0 +1,76 @@ +//! One validated membership scan for independent recovered fact facets. +use crate::{context::OpId, funcdata::Funcdata}; +use kuna_num::opcodes::OpCode; +use std::collections::HashSet; + +pub const MAX_CALLS: usize = 12_000; ++const MAX_SWITCHES: usize = 12_000; +// Bound the optional membership index separately from call discovery. Crossing +// this resource budget disables projections, without discarding known calls. +const MAX_INDEXED_OPERATIONS: usize = 65_536; +pub struct Membership { + pub calls: Vec, ++ pub switches: Option>, + pub members: Option>, + pub blocks_examined: i32, + pub operations_examined: usize, @@ -1570,6 +1726,7 @@ index 00000000..6628ddd7 + } + let mut result = Membership { + calls: Vec::new(), ++ switches: Some(Vec::new()), + members: Some(HashSet::new()), + blocks_examined: fd.bblocks_get_size(), + operations_examined: 0, @@ -1606,6 +1763,15 @@ index 00000000..6628ddd7 + } + result.calls.push(key); + } ++ if op.code() == OpCode::CPUI_BRANCHIND { ++ if let Some(switches) = result.switches.as_mut() { ++ if switches.len() == MAX_SWITCHES { ++ result.switches = None; ++ } else { ++ switches.push(key); ++ } ++ } ++ } + } + if examined != expected { + return Err("native block operation count mismatch".into()); @@ -1861,6 +2027,210 @@ index 00000000..097b153a + }, + }) +} +diff --git a/decompiler/crates/kuna-decomp/src/substrate/atomic_switches.rs b/decompiler/crates/kuna-decomp/src/substrate/atomic_switches.rs +new file mode 100644 +index 00000000..e8992d5d +--- /dev/null ++++ b/decompiler/crates/kuna-decomp/src/substrate/atomic_switches.rs +@@ -0,0 +1,198 @@ ++//! Read-only native jump-table facts; row ordinals are not case values. ++use crate::{ ++ atomic_membership::Membership, ++ atomic_values::{self, operation_id, ValueFact}, ++ funcdata::Funcdata, ++ jumptable::NO_LABEL, ++}; ++use kuna_base::address::Address; ++use serde::Serialize; ++use std::collections::{BTreeMap, BTreeSet}; ++ ++const MAX_ROWS: usize = 100_000; ++const MAX_MAPPINGS: usize = 100_000; ++#[derive(Debug, Clone, Serialize)] ++pub struct SwitchFacts { ++ pub schema_version: u32, ++ pub items: Vec, ++ pub coverage: Coverage, ++} ++#[derive(Debug, Clone, Serialize)] ++pub struct Coverage { ++ pub scope: &'static str, ++ pub blocks_examined: i32, ++ pub operations_examined: usize, ++ pub machine_semantic_equivalence: &'static str, ++} ++#[derive(Debug, Clone, Serialize)] ++pub struct SwitchFact { ++ pub id: String, ++ pub origin_address: String, ++ pub block: i32, ++ pub selector: Option, ++ pub table: Option, ++} ++#[derive(Debug, Clone, Serialize)] ++pub struct Table { ++ pub partial: bool, ++ pub labelled: bool, ++ pub consumed_mask: String, ++ pub signed_render_hint: bool, ++ pub default_raw_index: i32, ++ pub default_edge: Option, ++ pub default_is_folded: bool, ++ pub rows: Vec, ++} ++#[derive(Debug, Clone, Serialize)] ++pub struct Edge { ++ pub id: String, ++ pub index: i32, ++ pub target: i32, ++} ++#[derive(Debug, Clone, Serialize)] ++pub struct Row { ++ pub index: usize, ++ pub label: Option