From daa512da86632242f2b7dec2b2ec21295d6f643f Mon Sep 17 00:00:00 2001 From: Ralf Anton Beier Date: Sat, 27 Jun 2026 11:21:38 +0200 Subject: [PATCH 1/3] FEAT-040 (v2.6): machine-readable analysis-gap records + report + viz panel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stop emitting top as silence. AnalysisResult gains a library-only gaps field — every site where an unsupported operator degraded a function to top is now an explicit Gap{func_index, pc, op, kind} record (the qualification scope/ limitation signal + the structured channel an AI agent needs per TE-011), sorted by (func, pc), emitted INDEPENDENTLY of emit_diagnostics. - Gap collection lives on FuncCtx (threaded everywhere), pushed at the interpret_op unsupported-op fallback; the degraded early-return means one gap per function at the first unmodeled op (the give-up point). Drained by run_function_body's new emit_gaps sink in phase 2 only (phase-1 summary + context-sensitive re-eval pass None, no double-count). - op_report_name names ANY op (curated op_name, else the Debug variant), e.g. F64Add, not a generic placeholder. - scry-viz: an "Analysis gaps" summary count + a structured gaps section (per TE-011: gaps as DATA beside the SVG, not silence). Library-only: WIT + frozen v1 JSON contract unchanged (contract test green). Tests: feat040_unsupported_op_recorded_as_gap, _modelled_function_has_no_gaps, _gaps_independent_of_emit_diagnostics. 41 core + 17 viz tests; clippy + fmt clean. FEAT-040 accepted (release v2.6.0). Co-Authored-By: Claude Opus 4.8 --- artifacts/roadmap-3.0.yaml | 2 +- crates/scry-analyze-core/src/lib.rs | 131 ++++++++++++++++++++++++++++ crates/scry-viz/src/lib.rs | 35 +++++++- 3 files changed, 166 insertions(+), 2 deletions(-) diff --git a/artifacts/roadmap-3.0.yaml b/artifacts/roadmap-3.0.yaml index 865513d..a561cc2 100644 --- a/artifacts/roadmap-3.0.yaml +++ b/artifacts/roadmap-3.0.yaml @@ -127,7 +127,7 @@ artifacts: - id: FEAT-040 type: feature title: "v2.6 — Machine-readable gap records + aggregated gap report" - status: proposed + status: accepted release: v2.6.0 description: > Stop emitting top as silence. Emit a positive Gap{function, pc, op, kind, diff --git a/crates/scry-analyze-core/src/lib.rs b/crates/scry-analyze-core/src/lib.rs index 136b073..281f2ce 100644 --- a/crates/scry-analyze-core/src/lib.rs +++ b/crates/scry-analyze-core/src/lib.rs @@ -372,6 +372,38 @@ pub struct AnalysisResult { /// the frozen v1 JSON contract), like [`AnalysisResult::function_meta`]. /// Only non-⊤ facts are emitted (a ⊤ fact carries no information). pub bit_facts: Vec, + /// FEAT-040 (REQ-017): explicit, machine-readable record of every place the + /// analysis was conservative — an unsupported operator that degraded the + /// function's abstract state to ⊤. Where the rest of the result emits ⊤ as + /// *silence* (an unanalyzed point produces no record), this enumerates the + /// "scry gave up here" sites so an assessor (the qualification scope/ + /// limitation statement) or an AI agent can see them directly. Sorted by + /// `(func_index, pc)`. Library-only, and emitted regardless of + /// `emit_diagnostics`. (Slice-1 covers the unsupported-op fallback — the + /// primary degradation site; future slices add operand-stack-havoc and + /// memory-fallback gap kinds.) + pub gaps: Vec, +} + +/// FEAT-040: one analysis-gap record — a site where scry was conservative. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Gap { + /// Absolute function index where the gap occurred. + pub func_index: u32, + /// Operator index (pc) of the conservative site. + pub pc: u32, + /// The operator name that triggered the gap (e.g. `f64.add`, `select`). + pub op: String, + /// What kind of conservative step this is. + pub kind: GapKind, +} + +/// FEAT-040: the category of an analysis [`Gap`]. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum GapKind { + /// An operator outside scry's modelled set: the function's abstract state + /// is scrubbed to ⊤ (sound, but no further facts are learned in it). + UnsupportedOp, } /// FEAT-037: a known-bits / congruence fact about one local at one program @@ -755,6 +787,10 @@ struct FuncCtx { /// become uninformative (all-top) and further records would just /// be noise. degraded: bool, + /// FEAT-040: analysis-gap records collected during this function's walk + /// (every unsupported-op fallback), drained by the caller in phase 2. + /// Independent of `emit_diagnostics` so the gap report is always available. + gaps: Vec, } impl FuncCtx { @@ -765,6 +801,7 @@ impl FuncCtx { operand_stack: Vec::new(), octagon, degraded: false, + gaps: Vec::new(), } } @@ -1538,6 +1575,7 @@ pub fn analyze( &mut sink_edges, /*emit_diagnostics=*/ false, /*depth=*/ 0, + /*emit_gaps=*/ None, )?; extract_results(&defined_funcs[defined].results, &result_state) }; @@ -1577,6 +1615,7 @@ pub fn analyze( let mut points: Vec = Vec::new(); let mut call_graph: Vec = Vec::new(); + let mut gaps: Vec = Vec::new(); for func in &defined_funcs { let init_locals = top_input_locals(func); run_function_body( @@ -1588,8 +1627,10 @@ pub fn analyze( &mut call_graph, config.emit_diagnostics, /*depth=*/ 0, + /*emit_gaps=*/ Some(&mut gaps), )?; } + gaps.sort_by_key(|g| (g.func_index, g.pc)); // ─────────────────────────────────────────────────────────── // Assemble the per-function-summary output records (FEAT-007). @@ -1880,6 +1921,7 @@ pub fn analyze( function_meta, verified_premises, bit_facts, + gaps, }) } @@ -3138,6 +3180,7 @@ fn run_function_body( call_graph: &mut Vec, emit_diagnostics: bool, depth: u32, + emit_gaps: Option<&mut Vec>, ) -> Result, AnalyzeError> { let mut ctx = FuncCtx::new(init_locals); let ops = &func.ops; @@ -3159,6 +3202,9 @@ fn run_function_body( if let Some(out) = emit_points { out.extend(interp.points); } + if let Some(g) = emit_gaps { + g.append(&mut ctx.gaps); + } Ok(ctx.operand_stack) } @@ -3899,6 +3945,16 @@ fn interpret_op( ), }); } + // FEAT-040: record the gap unconditionally (the function is about to + // degrade to ⊤). The `degraded` early-return means this fires once + // per function — at the first unsupported op, the point where scry + // gave up — which is exactly the scope/limitation signal we want. + ctx.gaps.push(Gap { + func_index, + pc, + op: op_report_name(other), + kind: GapKind::UnsupportedOp, + }); ctx.scrub_to_top(); } } @@ -4382,6 +4438,7 @@ fn handle_call( &mut sink_edges, /*emit_diagnostics=*/ false, depth.saturating_add(1), + /*emit_gaps=*/ None, )?; ( extract_results(&callee.results, &final_stack), @@ -5102,6 +5159,22 @@ fn run_taint_analysis( /// (full payloads) and tends to balloon diagnostic strings. The set /// below is the one we expect to see most often via the fallback /// path; anything else falls through to a debug-ish label. +/// FEAT-040: a human-readable name for ANY operator, for gap records. Uses the +/// curated [`op_name`] when known, else falls back to the operator's Debug +/// variant name (e.g. `F64Add`, `V128Const`) so an unsupported op is still +/// identified in the gap report rather than shown as a generic placeholder. +fn op_report_name(op: &Operator<'_>) -> String { + let n = op_name(op); + if n != "" { + return n.to_string(); + } + let dbg = alloc::format!("{op:?}"); + dbg.split([' ', '(', '{']) + .next() + .unwrap_or("") + .to_string() +} + fn op_name(op: &Operator<'_>) -> &'static str { match op { Operator::Unreachable => "unreachable", @@ -5188,6 +5261,7 @@ mod tests { function_meta: alloc::vec![], verified_premises: FusionPremises::default(), bit_facts: alloc::vec![], + gaps: alloc::vec![], }; assert_eq!(res.invariants.points.len(), 1); assert!(matches!(rp, AbstractValue::RegionPointer(_))); @@ -5985,6 +6059,63 @@ mod tests { ); } + /// FEAT-040: an unsupported operator that degrades the function to ⊤ is + /// recorded as an explicit Gap — not emitted as silence — so an assessor / + /// AI agent can enumerate where scry gave up. + #[test] + fn feat040_unsupported_op_recorded_as_gap() { + // f64.add is outside scry's modelled set ⇒ the function degrades to ⊤. + let r = analyze_default( + "(module (func (param f64 f64) (result f64) \ + local.get 0 local.get 1 f64.add))", + ); + let g = r + .gaps + .iter() + .find(|g| g.func_index == 0) + .expect("a gap for the unsupported f64.add"); + assert_eq!(g.kind, GapKind::UnsupportedOp); + assert!( + g.op != "" && g.op.to_lowercase().contains("f64"), + "gap should name the unsupported op (f64.*), got {:?}", + g.op + ); + } + + /// FEAT-040: a fully-modelled function produces NO gaps (no false "gave up"). + #[test] + fn feat040_modelled_function_has_no_gaps() { + let r = analyze_default( + "(module (func (param i32) (result i32) local.get 0 i32.const 1 i32.add))", + ); + assert!( + r.gaps.is_empty(), + "a fully-modelled i32 function must report no gaps, got {:?}", + r.gaps + ); + } + + /// FEAT-040: gaps are emitted even with the default config + /// (emit_diagnostics = false) — the gap report is independent of verbose + /// diagnostics. + #[test] + fn feat040_gaps_independent_of_emit_diagnostics() { + let r = analyze( + wat::parse_str("(module (func (result f64) f64.const 1 f64.const 2 f64.add))") + .expect("assemble"), + AnalysisConfig::default(), // emit_diagnostics = false + ) + .expect("analyze"); + assert!( + !r.gaps.is_empty(), + "gaps must populate even with default (no-diagnostics) config" + ); + assert!( + r.diagnostics.is_empty(), + "default config emits no diagnostics" + ); + } + /// FEAT-034: scry determines its OWN fusion premises (verify-not-trust): /// bounded_memory = no `memory.grow`; closed_world = no functional imports. #[test] diff --git a/crates/scry-viz/src/lib.rs b/crates/scry-viz/src/lib.rs index 6a02d83..7d1627e 100644 --- a/crates/scry-viz/src/lib.rs +++ b/crates/scry-viz/src/lib.rs @@ -31,7 +31,7 @@ use core::fmt::Write as _; use scry_analyze_core::{ AbstractValue, AnalysisResult, Diagnostic, DiagnosticSeverity, FunctionMeta, FunctionStack, - Interval, Region, SecurityLabel, SoundnessTag, StackBound, TaintFindingKind, + GapKind, Interval, Region, SecurityLabel, SoundnessTag, StackBound, TaintFindingKind, }; /// FEAT-027: metadata for one function index, if scry resolved any. @@ -156,6 +156,7 @@ pub fn render_html(result: &AnalysisResult, title: &str) -> String { render_functions(&mut s, result); render_call_graph(&mut s, result); render_diagnostics(&mut s, &result.diagnostics); + render_gaps(&mut s, result); render_taint(&mut s, result); render_provenance(&mut s, result); render_points(&mut s, result); @@ -247,6 +248,7 @@ fn render_header(s: &mut String, r: &AnalysisResult) { kv(s, "recursive functions", &recursive.to_string()); kv(s, "call-graph edges", &r.call_graph.len().to_string()); kv(s, "diagnostics", &r.diagnostics.len().to_string()); + kv(s, "analysis gaps", &r.gaps.len().to_string()); kv(s, "program points", &points.to_string()); // FEAT-034: scry's own verified fusion premises (independent of meld). let vp = &r.verified_premises; @@ -616,6 +618,37 @@ fn truncate_label(label: &str, max: usize) -> String { } } +/// FEAT-040: analysis gaps — the explicit "scry was conservative here" sites +/// (an unsupported op that degraded a function to ⊤). Rendered as a structured +/// table so an assessor (the qualification scope/limitation statement) or an AI +/// agent reads the gaps as DATA, not as the silence of an absent fact. +fn render_gaps(s: &mut String, r: &AnalysisResult) { + s.push_str("

Analysis gaps

"); + if r.gaps.is_empty() { + s.push_str("

No gaps — every analyzed function stayed within scry's modelled set.

"); + return; + } + let _ = write!( + s, + "

{} site(s) where scry degraded a function to \u{22a4} (gave up).

    ", + r.gaps.len(), + ); + for g in &r.gaps { + let kind = match g.kind { + GapKind::UnsupportedOp => "unsupported-op", + }; + let _ = write!( + s, + "
  • {kind} \ + fn{}:{} {}
  • ", + g.func_index, + g.pc, + esc(&g.op), + ); + } + s.push_str("
"); +} + fn render_diagnostics(s: &mut String, diags: &[Diagnostic]) { s.push_str("

Diagnostics

"); if diags.is_empty() { From 38cc15e36e1587501eee696badf84a8440db736f Mon Sep 17 00:00:00 2001 From: Ralf Anton Beier Date: Sat, 27 Jun 2026 11:24:40 +0200 Subject: [PATCH 2/3] =?UTF-8?q?FEAT-040:=20enforce=20gap=20completeness=20?= =?UTF-8?q?=E2=80=94=20scrub=5Fto=5Ftop=20requires=20a=20Gap=20(all=204=20?= =?UTF-8?q?sites)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Audit found scry degrades to top at FOUR sites (unsupported op, br_table, 2x non-i32 memory-address fallback), but slice-1 only recorded the first — so the "no conservative site silently omitted" AC was violated for the other 3. Fix: scrub_to_top now takes a Gap by signature, so degradation CANNOT be silent (the compiler enforces it). Added GapKind::UnmodeledBranch (br_table) and UnmodeledMemoryAddress (non-i32 address). Zero bare scrub_to_top calls remain. Test feat040_br_table_recorded_as_gap. 42 core tests; clippy + fmt clean. Co-Authored-By: Claude Opus 4.8 --- crates/scry-analyze-core/src/lib.rs | 67 ++++++++++++++++++++++++----- 1 file changed, 57 insertions(+), 10 deletions(-) diff --git a/crates/scry-analyze-core/src/lib.rs b/crates/scry-analyze-core/src/lib.rs index 281f2ce..e84a90e 100644 --- a/crates/scry-analyze-core/src/lib.rs +++ b/crates/scry-analyze-core/src/lib.rs @@ -404,6 +404,11 @@ pub enum GapKind { /// An operator outside scry's modelled set: the function's abstract state /// is scrubbed to ⊤ (sound, but no further facts are learned in it). UnsupportedOp, + /// An unmodelled multi-target branch (`br_table`): control flow scrubbed. + UnmodeledBranch, + /// A memory access on a non-i32-shaped address operand: region state + /// scrubbed to ⊤ (the address could alias anywhere — sound fallback). + UnmodeledMemoryAddress, } /// FEAT-037: a known-bits / congruence fact about one local at one program @@ -809,7 +814,15 @@ impl FuncCtx { /// we hit any operator outside the v0.2 AC#1 supported set — /// soundness over precision (REQ-001 / DD-005). The octagon is reset to /// `top` too (all relations forgotten — sound). - fn scrub_to_top(&mut self) { + /// + /// FEAT-040: every degradation MUST record a [`Gap`] (passed in), so no + /// function can silently degrade to ⊤ — the gap report is complete. The + /// `degraded` early-return elsewhere means only the FIRST scrub records a + /// gap (subsequent ops are skipped), which is the give-up point we want. + fn scrub_to_top(&mut self, gap: Gap) { + if !self.degraded { + self.gaps.push(gap); + } for slot in self.locals.iter_mut() { *slot = AbstractValue::I32Interval(domain::top()); } @@ -2755,7 +2768,12 @@ impl Interp<'_, '_> { } Operator::BrTable { .. } => { // Unmodelled multi-target branch: sound fallback. - ctx.scrub_to_top(); + ctx.scrub_to_top(Gap { + func_index: self.func_index, + pc: pc as u32, + op: "br_table".to_string(), + kind: GapKind::UnmodeledBranch, + }); return Ok(Flow::Diverged); } _ => {} @@ -3945,17 +3963,15 @@ fn interpret_op( ), }); } - // FEAT-040: record the gap unconditionally (the function is about to - // degrade to ⊤). The `degraded` early-return means this fires once - // per function — at the first unsupported op, the point where scry - // gave up — which is exactly the scope/limitation signal we want. - ctx.gaps.push(Gap { + // FEAT-040: scrub_to_top records the gap (the function degrades to + // ⊤ here). The `degraded` early-return means this fires once per + // function — at the first unsupported op, the give-up point. + ctx.scrub_to_top(Gap { func_index, pc, op: op_report_name(other), kind: GapKind::UnsupportedOp, }); - ctx.scrub_to_top(); } } Ok(StepOutcome::Continue) @@ -4092,7 +4108,12 @@ fn handle_memory_load( ), }); } - ctx.scrub_to_top(); + ctx.scrub_to_top(Gap { + func_index, + pc, + op: op_label.to_string(), + kind: GapKind::UnmodeledMemoryAddress, + }); return Ok(()); }; @@ -4193,7 +4214,12 @@ fn handle_memory_store( ), }); } - ctx.scrub_to_top(); + ctx.scrub_to_top(Gap { + func_index, + pc, + op: op_label.to_string(), + kind: GapKind::UnmodeledMemoryAddress, + }); return Ok(()); }; @@ -6082,6 +6108,27 @@ mod tests { ); } + /// FEAT-040 completeness: a non-unsupported-op degradation (an unmodelled + /// `br_table`) is ALSO recorded — degradation can't be silent (scrub_to_top + /// requires a Gap by signature). + #[test] + fn feat040_br_table_recorded_as_gap() { + let r = analyze_default( + "(module (func (param i32) (block (block \ + local.get 0 br_table 0 1 0))))", + ); + let g = r.gaps.iter().find(|g| g.func_index == 0); + if let Some(g) = g { + assert_eq!( + g.kind, + GapKind::UnmodeledBranch, + "br_table ⇒ UnmodeledBranch gap, got {g:?}" + ); + } else { + panic!("br_table must record a gap; gaps={:?}", r.gaps); + } + } + /// FEAT-040: a fully-modelled function produces NO gaps (no false "gave up"). #[test] fn feat040_modelled_function_has_no_gaps() { From 3751131c1ccc58fd324283506a6806ea4e1cccff Mon Sep 17 00:00:00 2001 From: Ralf Anton Beier Date: Sat, 27 Jun 2026 11:31:00 +0200 Subject: [PATCH 3/3] FEAT-040: record write-set-havoc give-ups + honestly scope the gap claim (clean-room) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Clean-room refuted the "every conservative site" wording: havoc_region (write-set havoc of an unmodelled control-flow region — a typed `if` / non-empty block-type) widened written locals to ⊤ but recorded NO gap. It's a PARTIAL give-up (the rest of the function stays precise), distinct from the full-function scrub_to_top sites. Fix: - New GapKind::UnmodeledControlFlow; havoc_region pushes a gap when it actually widens a local (written set non-empty). - Field doc now scopes the claim precisely: gaps cover the interval/region INTERPRETER's conservative sites (full-function scrubs + control-flow havoc), and explicitly NOT ordinary loop widening (normal abstraction), the separate bits/taint passes, or imported functions (sound but out of scope). - scry-viz render_gaps handles the new kinds. Test feat040_control_flow_havoc_recorded_as_gap. 43 core + 17 viz tests; clippy + fmt clean. Co-Authored-By: Claude Opus 4.8 --- crates/scry-analyze-core/src/lib.rs | 61 ++++++++++++++++++++++++----- crates/scry-viz/src/lib.rs | 3 ++ 2 files changed, 55 insertions(+), 9 deletions(-) diff --git a/crates/scry-analyze-core/src/lib.rs b/crates/scry-analyze-core/src/lib.rs index e84a90e..a434588 100644 --- a/crates/scry-analyze-core/src/lib.rs +++ b/crates/scry-analyze-core/src/lib.rs @@ -372,16 +372,24 @@ pub struct AnalysisResult { /// the frozen v1 JSON contract), like [`AnalysisResult::function_meta`]. /// Only non-⊤ facts are emitted (a ⊤ fact carries no information). pub bit_facts: Vec, - /// FEAT-040 (REQ-017): explicit, machine-readable record of every place the - /// analysis was conservative — an unsupported operator that degraded the - /// function's abstract state to ⊤. Where the rest of the result emits ⊤ as + /// FEAT-040 (REQ-017): explicit, machine-readable records of the places the + /// interval/region interpreter was CONSERVATIVE — every site where it either + /// degraded a whole function to ⊤ (an unsupported op, a `br_table`, or a + /// non-i32-shaped memory address — the [`FuncCtx::scrub_to_top`] sites, + /// enforced complete by that method's signature) OR fell back to write-set + /// havoc of an unmodelled control-flow region (a typed `if` / non-empty + /// block-type — a partial give-up). Where the rest of the result emits ⊤ as /// *silence* (an unanalyzed point produces no record), this enumerates the - /// "scry gave up here" sites so an assessor (the qualification scope/ - /// limitation statement) or an AI agent can see them directly. Sorted by - /// `(func_index, pc)`. Library-only, and emitted regardless of - /// `emit_diagnostics`. (Slice-1 covers the unsupported-op fallback — the - /// primary degradation site; future slices add operand-stack-havoc and - /// memory-fallback gap kinds.) + /// "scry was conservative here" sites so an assessor (the qualification + /// scope/limitation statement) or an AI agent can see them directly. Sorted + /// by `(func_index, pc)`. Library-only, emitted regardless of + /// `emit_diagnostics`. + /// + /// SCOPE (honest bounds): this covers the interval/region INTERPRETER. It + /// does NOT enumerate (a) ordinary loop widening to ⊤ — that is normal sound + /// abstraction, not a give-up; (b) the separate `bit_facts` / taint passes' + /// own conservative stops; (c) imported functions (never analyzed). Those + /// are sound but out of this report's scope. pub gaps: Vec, } @@ -409,6 +417,11 @@ pub enum GapKind { /// A memory access on a non-i32-shaped address operand: region state /// scrubbed to ⊤ (the address could alias anywhere — sound fallback). UnmodeledMemoryAddress, + /// An unmodelled control-flow region (a typed `if`, or a non-empty + /// block-type `block`) handled by write-set havoc: the locals the region + /// writes are widened to ⊤ (the rest stay precise — a PARTIAL give-up, + /// unlike the full-function scrubs above). FEAT-016 fallback. + UnmodeledControlFlow, } /// FEAT-037: a known-bits / congruence fact about one local at one program @@ -3147,6 +3160,17 @@ impl Interp<'_, '_> { let _ = ctx.operand_stack.pop(); } let written = region_write_set(self.ops, opener + 1, end); + // FEAT-040: an unmodelled control-flow region that actually widens a + // local to ⊤ is a (partial) give-up — record it so the gap report + // reflects write-set havoc, not only the full-function scrubs. + if !written.is_empty() { + ctx.gaps.push(Gap { + func_index: self.func_index, + pc: opener as u32, + op: op_report_name(&self.ops[opener]), + kind: GapKind::UnmodeledControlFlow, + }); + } for idx in &written { if let Some(slot) = ctx.locals.get_mut(*idx as usize) { *slot = AbstractValue::I32Interval(domain::top()); @@ -6129,6 +6153,25 @@ mod tests { } } + /// FEAT-040 completeness (clean-room finding): write-set havoc of an + /// unmodelled control-flow region (a typed `if`) is now recorded — it is a + /// partial give-up that previously left no gap. + #[test] + fn feat040_control_flow_havoc_recorded_as_gap() { + let r = analyze_default( + "(module (func (param i32) (local i32) \ + i32.const 5 local.set 1 \ + local.get 0 (if (then i32.const 9 local.set 1))))", + ); + assert!( + r.gaps + .iter() + .any(|g| g.kind == GapKind::UnmodeledControlFlow), + "write-set havoc of the if-region must record an UnmodeledControlFlow gap; got {:?}", + r.gaps + ); + } + /// FEAT-040: a fully-modelled function produces NO gaps (no false "gave up"). #[test] fn feat040_modelled_function_has_no_gaps() { diff --git a/crates/scry-viz/src/lib.rs b/crates/scry-viz/src/lib.rs index 7d1627e..91ae855 100644 --- a/crates/scry-viz/src/lib.rs +++ b/crates/scry-viz/src/lib.rs @@ -636,6 +636,9 @@ fn render_gaps(s: &mut String, r: &AnalysisResult) { for g in &r.gaps { let kind = match g.kind { GapKind::UnsupportedOp => "unsupported-op", + GapKind::UnmodeledBranch => "unmodeled-branch", + GapKind::UnmodeledMemoryAddress => "unmodeled-memory-address", + GapKind::UnmodeledControlFlow => "unmodeled-control-flow", }; let _ = write!( s,