diff --git a/dsl/extdeps/render/glyphs.dag b/dsl/extdeps/render/glyphs.dag index a5584701641..37662b4dd8d 100644 --- a/dsl/extdeps/render/glyphs.dag +++ b/dsl/extdeps/render/glyphs.dag @@ -67,7 +67,14 @@ data standard_symbols: List = [ { id: ConnectorTeeDown, glyphs: glyphs_for_tiers(emoji: glyph(text: "┬"), unicode: glyph(text: "┬"), ascii: glyph(text: "+")), color: Dim }, { id: ConnectorTeeUp, glyphs: glyphs_for_tiers(emoji: glyph(text: "┴"), unicode: glyph(text: "┴"), ascii: glyph(text: "+")), color: Dim }, { id: ConnectorCornerBottomLeft, glyphs: glyphs_for_tiers(emoji: glyph(text: "╰"), unicode: glyph(text: "╰"), ascii: glyph(text: "+")), color: Dim }, - { id: ConnectorCornerTopLeft, glyphs: glyphs_for_tiers(emoji: glyph(text: "╭"), unicode: glyph(text: "╭"), ascii: glyph(text: "+")), color: Dim } + { id: ConnectorCornerTopLeft, glyphs: glyphs_for_tiers(emoji: glyph(text: "╭"), unicode: glyph(text: "╭"), ascii: glyph(text: "+")), color: Dim }, + + { id: BoxHorizontal, glyphs: glyphs_for_tiers(emoji: glyph(text: "─"), unicode: glyph(text: "─"), ascii: glyph(text: "-")), color: Dim }, + { id: BoxVertical, glyphs: glyphs_for_tiers(emoji: glyph(text: "│"), unicode: glyph(text: "│"), ascii: glyph(text: "|")), color: Dim }, + { id: BoxCornerTopLeft, glyphs: glyphs_for_tiers(emoji: glyph(text: "┌"), unicode: glyph(text: "┌"), ascii: glyph(text: "+")), color: Dim }, + { id: BoxCornerTopRight, glyphs: glyphs_for_tiers(emoji: glyph(text: "┐"), unicode: glyph(text: "┐"), ascii: glyph(text: "+")), color: Dim }, + { id: BoxCornerBottomLeft, glyphs: glyphs_for_tiers(emoji: glyph(text: "└"), unicode: glyph(text: "└"), ascii: glyph(text: "+")), color: Dim }, + { id: BoxCornerBottomRight, glyphs: glyphs_for_tiers(emoji: glyph(text: "┘"), unicode: glyph(text: "┘"), ascii: glyph(text: "+")), color: Dim } ] fn lookup_symbol_entry(id: SymbolId) -> SymbolEntry? { diff --git a/dsl/extdeps/render/terminal.dag b/dsl/extdeps/render/terminal.dag index c704c0683c1..08097d6e3c4 100644 --- a/dsl/extdeps/render/terminal.dag +++ b/dsl/extdeps/render/terminal.dag @@ -1,7 +1,8 @@ module extdeps.render.terminal import extdeps.render.glyphs { resolve_symbol } -import std.render { Frame, Line, RenderCapability, Span, Viewport, cursor_action_under } +import extdeps.render.ansi { ansi_code } +import std.render { Frame, Line, RenderCapability, Span, Viewport, cursor_action_under, repeat_string } import std.symbols { Tier } import std.unicode { string_display_width } import std.width { truncate_text } @@ -88,3 +89,39 @@ fn project_frame(frame: Frame, viewport: Viewport, cap: RenderCapability) -> Fra Absent => none } } + +fn serialize_span(span: Span, cap: RenderCapability) -> String { + let glyph = match span.style.symbol { + null => "" + sym => match resolve_symbol(id: sym, tier: cap.tier) { + Present { value: s } => s + Absent => "" + } + } + let body = concat(glyph, span.text) + if cap.color { + match span.style.color { + null => body + c => match ansi_code(c: c) { + Present { value: code } => concat(code, concat(body, "\x1b[0m")) + Absent => body + } + } + } else { + body + } +} + +fn serialize_line(line: Line, cap: RenderCapability) -> String { + let pad = repeat_string(s: " ", n: line.indent * 4) + let body = fold(line.spans, + init: "", + f: (acc, span) => concat(acc, serialize_span(span: span, cap: cap)) + ) + concat(pad, body) +} + +fn serialize_frame(frame: Frame, cap: RenderCapability) -> String { + let rendered = map(frame.lines, line => serialize_line(line: line, cap: cap)) + join(rendered, "\n") +} diff --git a/dsl/gunbc/ci_render.dag b/dsl/gunbc/ci_render.dag new file mode 100644 index 00000000000..53edf049015 --- /dev/null +++ b/dsl/gunbc/ci_render.dag @@ -0,0 +1,123 @@ +module gunbc.ci_render + +import std.render { Frame, Line, Span, SpanStyle, RenderCapability, repeat_string } +import std.symbols { + SemanticColor, SymbolId, Tier, Unicode, + BoxHorizontal, BoxVertical, BoxCornerTopLeft, BoxCornerTopRight, BoxCornerBottomLeft, BoxCornerBottomRight, +} +import std.unicode { string_display_width } +import std.width { truncate_text } +import std.measure { Nanosecond, nanosecond, nanosecond_count } +import extdeps.render.glyphs { resolve_symbol } +import extdeps.render.terminal { serialize_frame } + +fn glyph_for(id: SymbolId, tier: Tier) -> String { + match resolve_symbol(id: id, tier: tier) { + Present { value: s } => s + Absent => "" + } +} + +fn pad_to(text: String, width: Int) -> String { + let w = string_display_width(s: text) + if w >= width { + text + } else { + concat(text, repeat_string(s: " ", n: width - w)) + } +} + +fn plain_span(text: String) -> Span { + Span { text: text, style: SpanStyle { color: none, bold: false, italic: false, symbol: none } } +} + +fn dim_span(text: String) -> Span { + Span { text: text, style: SpanStyle { color: Present { value: Dim }, bold: false, italic: false, symbol: none } } +} + +fn line_of(span: Span) -> Line { + Line { spans: [span], indent: 0, max_width: none } +} + +fn box_top(title: String, width: Int, tier: Tier) -> Line { + let hz = glyph_for(id: BoxHorizontal, tier: tier) + let tr = glyph_for(id: BoxCornerTopRight, tier: tier) + let prefix = concat(glyph_for(id: BoxCornerTopLeft, tier: tier), concat(hz, concat(" ", concat(title, " ")))) + let dashes = width - string_display_width(s: prefix) - string_display_width(s: tr) + line_of(span: dim_span(text: concat(prefix, concat(repeat_string(s: hz, n: dashes), tr)))) +} + +fn box_bottom(width: Int, tier: Tier) -> Line { + let hz = glyph_for(id: BoxHorizontal, tier: tier) + let bl = glyph_for(id: BoxCornerBottomLeft, tier: tier) + let br = glyph_for(id: BoxCornerBottomRight, tier: tier) + let dashes = width - string_display_width(s: bl) - string_display_width(s: br) + line_of(span: dim_span(text: concat(bl, concat(repeat_string(s: hz, n: dashes), br)))) +} + +fn box_content_line(text: String, width: Int, tier: Tier) -> Line { + let v = glyph_for(id: BoxVertical, tier: tier) + let inner_budget = width - (2 * string_display_width(s: v)) - 2 + let inner = pad_to(text: truncate_text(text: text, max_width: inner_budget), width: inner_budget) + line_of(span: plain_span(text: concat(v, concat(" ", concat(inner, concat(" ", v)))))) +} + +fn boxed_frame(title: String, body: List, width: Int, tier: Tier) -> Frame { + let content = map(body, l => box_content_line(text: l, width: width, tier: tier)) + let all = concat([box_top(title: title, width: width, tier: tier)], concat(content, [box_bottom(width: width, tier: tier)])) + Frame { lines: all, cursor_action: Append } +} + +fn format_duration(d: Nanosecond) -> String { + let ns = nanosecond_count(n: d) + if ns < 1000 { + concat(to_string(ns), "ns") + } else if ns < 1000000 { + concat(to_string(ns / 1000), "us") + } else if ns < 1000000000 { + concat(to_string(ns / 1000000), "ms") + } else { + concat(to_string(ns / 1000000000), "s") + } +} + +fn timing_cell(label: String, d: Nanosecond) -> String { + concat(label, concat(": ", format_duration(d: d))) +} + +fn percentile_row(p50: Nanosecond, p90: Nanosecond, p95: Nanosecond, p99: Nanosecond, p100: Nanosecond) -> String { + concat(timing_cell(label: "p50", d: p50), + concat(" ", concat(timing_cell(label: "p90", d: p90), + concat(" ", concat(timing_cell(label: "p95", d: p95), + concat(" ", concat(timing_cell(label: "p99", d: p99), + concat(" ", timing_cell(label: "max", d: p100))))))))) +} + +fn percentile_box_frame(title: String, p50: Nanosecond, p90: Nanosecond, p95: Nanosecond, p99: Nanosecond, p100: Nanosecond, width: Int, tier: Tier) -> Frame { + boxed_frame(title: title, body: [percentile_row(p50: p50, p90: p90, p95: p95, p99: p99, p100: p100)], width: width, tier: tier) +} + +fn render_capability(color: Bool) -> RenderCapability { + RenderCapability { color: color, tier: Unicode, cursor_addressable: false } +} + +fn percentiles_box_frame_ns(title: String, p50: Int, p90: Int, p95: Int, p99: Int, p100: Int, width: Int, tier: Tier) -> Frame { + percentile_box_frame( + title: title, + p50: nanosecond(count: p50), p90: nanosecond(count: p90), p95: nanosecond(count: p95), + p99: nanosecond(count: p99), p100: nanosecond(count: p100), + width: width, tier: tier + ) +} + +fn render_percentile_box(title: String, p50: Int, p90: Int, p95: Int, p99: Int, p100: Int, width: Int, color: Bool) -> String { + let cap = render_capability(color: color) + serialize_frame( + frame: percentiles_box_frame_ns(title: title, p50: p50, p90: p90, p95: p95, p99: p99, p100: p100, width: width, tier: cap.tier), + cap: cap + ) +} + +fn timing_histogram_frame(p50: Int, p90: Int, p95: Int, p99: Int, p100: Int, width: Int) -> Frame { + percentiles_box_frame_ns(title: "WITNESS TIMING HISTOGRAM", p50: p50, p90: p90, p95: p95, p99: p99, p100: p100, width: width, tier: Unicode) +} diff --git a/dsl/std/symbols.dag b/dsl/std/symbols.dag index 58f27f447b9..fa0f189dcc3 100644 --- a/dsl/std/symbols.dag +++ b/dsl/std/symbols.dag @@ -53,3 +53,10 @@ type SymbolId | ConnectorTeeUp | ConnectorCornerBottomLeft | ConnectorCornerTopLeft + + | BoxHorizontal + | BoxVertical + | BoxCornerTopLeft + | BoxCornerTopRight + | BoxCornerBottomLeft + | BoxCornerBottomRight diff --git a/dsl/std/unicode.dag b/dsl/std/unicode.dag index 0e4112587e1..ce092636fab 100644 --- a/dsl/std/unicode.dag +++ b/dsl/std/unicode.dag @@ -82,7 +82,7 @@ fn in_block(cp: Int, block: UnicodeBlock) -> Bool { } fn char_display_width(c: Char) -> DisplayWidth { - let cp = code_point(c: c) + let cp = c let is_zero_width = contains(zero_width_codepoints, item: cp) || any(zero_width_blocks, predicate: b => in_block(cp: cp, block: b)) if is_zero_width { return ZeroWidth } if any(wide_blocks, predicate: b => in_block(cp: cp, block: b)) { return Wide } diff --git a/dsl/std/width.dag b/dsl/std/width.dag index d4fe82ecda3..7bbfe92515f 100644 --- a/dsl/std/width.dag +++ b/dsl/std/width.dag @@ -10,7 +10,7 @@ fn truncate_text(text: String, max_width: Int) -> String { f: (acc, c) => if acc.used + char_width(c: c) > max_width { acc } else { - { result: concat(acc.result, c), used: acc.used + char_width(c: c) } + { result: concat(acc.result, from_code_point(c)), used: acc.used + char_width(c: c) } } ) state.result diff --git a/dsl/test/claim/ci_render_histogram_width_test.dag b/dsl/test/claim/ci_render_histogram_width_test.dag new file mode 100644 index 00000000000..09e2a01d71f --- /dev/null +++ b/dsl/test/claim/ci_render_histogram_width_test.dag @@ -0,0 +1,56 @@ +module test.claim.ci_render_histogram_width + +import gunbc.ci_render { timing_histogram_frame, percentiles_box_frame_ns, render_percentile_box } +import extdeps.render.terminal { serialize_line, serialize_frame } +import std.render { Frame, RenderCapability } +import std.symbols { Ascii, Unicode } +import std.unicode { string_display_width } + +fn no_color_cap() -> RenderCapability { + RenderCapability { color: false, tier: Unicode, cursor_addressable: false } +} + +fn sample_frame(width: Int) -> Frame { + timing_histogram_frame(p50: 12, p90: 3400, p95: 56000, p99: 7800000, p100: 920000000, width: width) +} + +fn all_lines_have_width(width: Int) -> Bool { + fold(sample_frame(width: width).lines, + init: true, + f: (acc, line) => acc && (string_display_width(s: serialize_line(line: line, cap: no_color_cap())) == width) + ) +} + +fn has_three_lines(width: Int) -> Bool { + count(sample_frame(width: width).lines) == 3 +} + +fn exact_small_box() -> Bool { + render_percentile_box(title: "X", p50: 1, p90: 2, p95: 3, p99: 4, p100: 5, width: 30, color: false) == + "┌─ X ────────────────────────┐\n│ p50: 1ns p90: 2ns p95: 3 │\n└────────────────────────────┘" +} + +fn ascii_box(width: Int) -> Frame { + percentiles_box_frame_ns(title: "X", p50: 1, p90: 2, p95: 3, p99: 4, p100: 5, width: width, tier: Ascii) +} + +fn ascii_width_holds() -> Bool { + fold(ascii_box(width: 48).lines, + init: true, + f: (acc, line) => acc && (string_display_width(s: serialize_line(line: line, cap: no_color_cap())) == 48) + ) +} + +fn tier_swaps_glyphs() -> Bool { + serialize_frame(frame: ascii_box(width: 48), cap: no_color_cap()) != + serialize_frame(frame: timing_histogram_frame(p50: 1, p90: 2, p95: 3, p99: 4, p100: 5, width: 48), cap: no_color_cap()) +} + +test fn ci_render_histogram_width_witnesses() -> Bool { + all_lines_have_width(width: 80) && + all_lines_have_width(width: 64) && + has_three_lines(width: 80) && + exact_small_box() && + ascii_width_holds() && + tier_swaps_glyphs() +} diff --git a/src/v1/stage0/src/bin/claim_executor.rs b/src/v1/stage0/src/bin/claim_executor.rs index a1e7f7ca7fd..af050ef9062 100644 --- a/src/v1/stage0/src/bin/claim_executor.rs +++ b/src/v1/stage0/src/bin/claim_executor.rs @@ -7,11 +7,12 @@ use std::thread; use std::time::Instant; use v1_compiler::cli_run::{ - make_eval_context, resolve_entry_graph, run_claim, run_discovery_corpus_with_options, - run_value, ClaimOutcome, DiscoveryCorpusOptions, + compute_histogram_data, make_eval_context, resolve_entry_graph, run_claim, + run_discovery_corpus_with_options, run_value, ClaimOutcome, DiscoveryCorpusOptions, + HistogramData, TimingPercentiles, }; use v1_compiler::v1_interpreter::{ - paint, run_in_context_with_args, sgr, ExecutionMode, InterpContext, Value, + color_enabled, paint, run_in_context_with_args, sgr, ExecutionMode, InterpContext, Value, }; #[derive(Clone)] @@ -415,6 +416,93 @@ fn run_shared_entry_claims( .collect() } +/// The medium's `Viewport.width` for boxed output. The seed sources the runtime value from the host +/// (`COLUMNS` when present) and passes it into the `.dag` render model; the model owns how width +/// shapes the box. Conservative 88-col default fits common CI-log and chat viewers; clamped so a +/// hostile `COLUMNS` cannot produce a degenerate box. +fn histogram_output_width() -> i64 { + std::env::var("COLUMNS") + .ok() + .and_then(|v| v.trim().parse::().ok()) + .filter(|w| *w >= 48) + .unwrap_or(88) + .min(120) +} + +fn clamp_nanos_to_i64(n: u128) -> i64 { + i64::try_from(n).unwrap_or(i64::MAX) +} + +/// Render the timing histogram boxes through `dsl/gunbc/ci_render.dag` (`render_percentile_box`), +/// the single authority for boxed-Frame width. The seed only supplies measured data + the host +/// viewport width; all layout (borders, padding, duration formatting) lives in `.dag`. +fn render_timing_histogram( + source_roots: &[String], + data: &HistogramData, +) -> Result { + let entry = "dsl/gunbc/ci_render.dag"; + let (graph, indices) = resolve_entry_graph(source_roots, entry) + .map_err(|m| format!("resolve failed for {entry}:\n{m}"))?; + let ctx = make_eval_context(&graph, indices, ExecutionMode::Wet); + let width = histogram_output_width(); + let color = color_enabled(); + + let render_box = |title: &str, p: &TimingPercentiles| -> Result { + let value = run_in_context_with_args( + &ctx, + "render_percentile_box", + &[ + (Some("title".to_string()), Value::Str(title.to_string())), + ( + Some("p50".to_string()), + Value::Int(clamp_nanos_to_i64(p.p50)), + ), + ( + Some("p90".to_string()), + Value::Int(clamp_nanos_to_i64(p.p90)), + ), + ( + Some("p95".to_string()), + Value::Int(clamp_nanos_to_i64(p.p95)), + ), + ( + Some("p99".to_string()), + Value::Int(clamp_nanos_to_i64(p.p99)), + ), + ( + Some("p100".to_string()), + Value::Int(clamp_nanos_to_i64(p.p100)), + ), + (Some("width".to_string()), Value::Int(width)), + (Some("color".to_string()), Value::Bool(color)), + ], + false, + ) + .map_err(|e| format!("render_percentile_box eval failed: {e}"))?; + match value { + Value::Str(s) => Ok(s), + other => Err(format!( + "render_percentile_box returned non-string: {other}" + )), + } + }; + + let mut out = String::new(); + out.push_str(&format!( + "Total witnesses: {} (included in histogram); {} skipped (no entry-resolve timing)\n", + data.included, data.skipped + )); + out.push_str( + "Note: Resolve times are per-entry-amortized (witnesses in an entry share its resolve cost); eval times are per-witness.\n\n", + ); + out.push_str(&render_box("TOTAL TIME (Resolve + Eval)", &data.total)?); + out.push('\n'); + out.push_str(&render_box("RESOLVE TIME", &data.resolve)?); + out.push('\n'); + out.push_str(&render_box("EVAL TIME", &data.eval)?); + Ok(out) +} + fn run_discovery_batch_node( source_roots: Vec, scan_dirs: Vec, @@ -448,8 +536,13 @@ fn run_discovery_batch_node( summary.total_measured_nanos as f64 / 1.0e6, summary.total_measured_nanos, ); - let histogram = v1_compiler::cli_run::generate_witness_timing_histogram(&summary); - eprintln!("{}", histogram); + match compute_histogram_data(&summary) { + Ok(data) => match render_timing_histogram(&source_roots, &data) { + Ok(histogram) => eprintln!("{histogram}"), + Err(e) => eprintln!("[histogram] render failed (timings unaffected): {e}"), + }, + Err(msg) => eprintln!("{msg}"), + } ClaimResult { function: format!("{label} ({} witnesses)", summary.total), ok: true, diff --git a/src/v1/stage0/src/cli_run.rs b/src/v1/stage0/src/cli_run.rs index 175c563f557..f6ffa1720a4 100644 --- a/src/v1/stage0/src/cli_run.rs +++ b/src/v1/stage0/src/cli_run.rs @@ -1986,21 +1986,28 @@ pub fn compute_percentiles(mut values: Vec) -> TimingPercentiles { } } -// SCAFFOLD (§7 hand-Rust shrink-to-zero, dissolution named): witness timing histogram measures v1 -// evaluator execution characteristics (resolve+eval per-witness percentiles). Seed-side justified -// (evaluator cannot measure itself without circularity). Dissolution: ROADMAP lane "CI observability" -// adds machine-readable timing-data emission to claim_executor (dsl/gunbc/ci_spec.dag TimingRecord -// rows), then dsl/ .dag witness consumes and histograms natively. At full dissolution, delete this -// hand-Rust output and hand_generated_percentiles_test.dag coverage. -pub fn generate_witness_timing_histogram(summary: &DiscoverySummary) -> String { +// SCAFFOLD (§7 hand-Rust shrink-to-zero, dissolution named): the v1 evaluator measures its own +// per-witness resolve+eval percentiles here — seed-side justified (the evaluator cannot measure +// itself without circularity). The *rendering* of these timings now lives in `dsl/gunbc/ci_render.dag` +// (boxed Frames over `std.render`, width-parameterized by the medium's `Viewport.width`); this Rust +// only produces the measured data. Full dissolution: ROADMAP lane "CI observability" emits the +// `TimingPercentiles` rows as a substrate value so a .dag witness measures + histograms natively, +// at which point this measurement struct collapses too. +pub struct HistogramData { + pub included: usize, + pub skipped: usize, + pub total: TimingPercentiles, + pub resolve: TimingPercentiles, + pub eval: TimingPercentiles, +} + +pub fn compute_histogram_data(summary: &DiscoverySummary) -> Result { if summary.performance_receipts.len() != summary.witness_outcomes.len() { - let msg = format!( + return Err(format!( "[histogram] SKIPPED: mismatched vector lengths (performance_receipts={}, witness_outcomes={}) — timings unreliable", summary.performance_receipts.len(), summary.witness_outcomes.len() - ); - eprintln!("{}", msg); - return format!("╔════════════════════════════════════════════════════════════════════════════╗\n║ {:<74} ║\n╚════════════════════════════════════════════════════════════════════════════╝\n", msg); + )); } let mut entry_resolve_map: HashMap = HashMap::new(); @@ -2035,92 +2042,13 @@ pub fn generate_witness_timing_histogram(summary: &DiscoverySummary) -> String { eval_times.push(eval_nanos); } - let included_witnesses = total_times.len(); - let total_percentiles = compute_percentiles(total_times); - let resolve_percentiles = compute_percentiles(resolve_times); - let eval_percentiles = compute_percentiles(eval_times); - - let mut output = String::new(); - output.push_str( - "╔════════════════════════════════════════════════════════════════════════════╗\n", - ); - output.push_str( - "║ WITNESS TIMING HISTOGRAM ║\n", - ); - output.push_str( - "║ Per-Witness Resolve+Eval Percentiles ║\n", - ); - output.push_str( - "╚════════════════════════════════════════════════════════════════════════════╝\n\n", - ); - - output.push_str(&format!( - "Total witnesses: {} (included in histogram); {} skipped (no entry-resolve timing)\n", - included_witnesses, skipped_missing_entry_resolve - )); - output.push_str( - "Note: Resolve times are per-entry-amortized (all witnesses in an entry share the\n", - ); - output.push_str("entry's resolve cost). Eval times are per-witness measurements.\n\n"); - - output.push_str( - "┌─ TOTAL TIME (Resolve + Eval) ───────────────────────────────────────────────┐\n", - ); - output.push_str(&format!( - "│ p50: {:>12} | p90: {:>12} | p95: {:>12} | p99: {:>12} | max: {:>12} │\n", - format_nanos(total_percentiles.p50), - format_nanos(total_percentiles.p90), - format_nanos(total_percentiles.p95), - format_nanos(total_percentiles.p99), - format_nanos(total_percentiles.p100), - )); - output.push_str( - "└─────────────────────────────────────────────────────────────────────────────┘\n\n", - ); - - output.push_str( - "┌─ RESOLVE TIME ──────────────────────────────────────────────────────────────┐\n", - ); - output.push_str(&format!( - "│ p50: {:>12} | p90: {:>12} | p95: {:>12} | p99: {:>12} | max: {:>12} │\n", - format_nanos(resolve_percentiles.p50), - format_nanos(resolve_percentiles.p90), - format_nanos(resolve_percentiles.p95), - format_nanos(resolve_percentiles.p99), - format_nanos(resolve_percentiles.p100), - )); - output.push_str( - "└─────────────────────────────────────────────────────────────────────────────┘\n\n", - ); - - output.push_str( - "┌─ EVAL TIME ─────────────────────────────────────────────────────────────────┐\n", - ); - output.push_str(&format!( - "│ p50: {:>12} | p90: {:>12} | p95: {:>12} | p99: {:>12} | max: {:>12} │\n", - format_nanos(eval_percentiles.p50), - format_nanos(eval_percentiles.p90), - format_nanos(eval_percentiles.p95), - format_nanos(eval_percentiles.p99), - format_nanos(eval_percentiles.p100), - )); - output.push_str( - "└─────────────────────────────────────────────────────────────────────────────┘\n", - ); - - output -} - -fn format_nanos(nanos: u128) -> String { - if nanos < 1_000 { - format!("{}ns", nanos) - } else if nanos < 1_000_000 { - format!("{:.1}µs", nanos as f64 / 1_000.0) - } else if nanos < 1_000_000_000 { - format!("{:.1}ms", nanos as f64 / 1_000_000.0) - } else { - format!("{:.1}s", nanos as f64 / 1_000_000_000.0) - } + Ok(HistogramData { + included: total_times.len(), + skipped: skipped_missing_entry_resolve, + total: compute_percentiles(total_times), + resolve: compute_percentiles(resolve_times), + eval: compute_percentiles(eval_times), + }) } pub const WET_HERMETIC_EQUIVALENCE_WITNESS_ENTRY: &str = diff --git a/src/v1/stage0/src/compiler_tests.rs b/src/v1/stage0/src/compiler_tests.rs index d66f6b26d61..6d188e3361e 100644 --- a/src/v1/stage0/src/compiler_tests.rs +++ b/src/v1/stage0/src/compiler_tests.rs @@ -1776,12 +1776,14 @@ mod compiler_tests { sources, crate::v1_compiler_artifact::RenderTarget::Rust, ); + // AnthropicChatMessage is declared in extdeps.llm.anthropic, so its `tag = "role"` + // contract must merge into THAT module's emitted file. Match it exactly: the loose + // `contains("extdeps_llm_anthropic")` glob also matched the sibling `_errors`/`_rest` + // split-out modules (which don't declare it), and `.find()` grabbed the wrong one. let anthropic_file = result .files .iter() - .find(|f| { - f.path.contains("extdeps_llm_anthropic") && !f.path.contains("_contracts") - }) + .find(|f| f.path.ends_with("extdeps_llm_anthropic.rs")) .expect( "emitted file for extdeps.llm.anthropic not found — \ module must be present in dsl/extdeps/llm source closure", diff --git a/src/v1/tests/src/interp_string_family_cast_test.rs b/src/v1/tests/src/interp_string_family_cast_test.rs index fd52b7ac92d..f4d4865e050 100644 --- a/src/v1/tests/src/interp_string_family_cast_test.rs +++ b/src/v1/tests/src/interp_string_family_cast_test.rs @@ -36,18 +36,16 @@ fn run_witness(src: &str, witness_fn: &str) -> Value { fn string_family_alias_casts_are_identity_at_runtime() { let src = r#"module test.string_family_cast -import std.types { NonEmptyStr, Secret, SecretValue, Url } +import std.types { NonEmptyStr, Secret, SecretValue } fn string_to_secret(s: String) -> Secret { s as Secret } fn string_to_nonempty(s: String) -> NonEmptyStr { s as NonEmptyStr } fn string_to_secret_value(s: String) -> SecretValue { s as SecretValue } -fn string_to_url(s: String) -> Url { s as Url } test fn string_family_cast_holds() -> Bool { string_to_secret("secret-token") == "secret-token" && string_to_nonempty("bmc-host") == "bmc-host" && string_to_secret_value("rotated") == "rotated" - && string_to_url("https://example.com") == "https://example.com" } "#; match run_witness(src, "string_family_cast_holds") {