Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 82 additions & 3 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ pub use ingest::fileset::{FilesetInput, FilesetInputKind};
pub use ingest::format::Format;
pub use order::types::{ArrayBias, ArraySamplerStrategy};
pub use order::{
NodeId, NodeKind, PriorityConfig, PriorityOrder, RankedNode, build_order,
DEFAULT_SAFETY_CAP, NodeId, NodeKind, PriorityConfig, PriorityOrder,
RankedNode, build_order,
};
pub use utils::extensions;
pub use utils::templates::map_json_template_for_style;
Expand Down Expand Up @@ -75,9 +76,17 @@ pub fn headson(
grep: &GrepConfig,
budgets: Budgets,
) -> Result<RenderOutput> {
let crate::ingest::IngestOutput { arena, warnings } =
crate::ingest::ingest_into_arena(input, priority_cfg, grep)?;
let crate::ingest::IngestOutput {
arena,
mut warnings,
} = crate::ingest::ingest_into_arena(input, priority_cfg, grep)?;
let mut order_build = order::build_order(&arena, priority_cfg)?;
if order_build.safety_cap_hit {
warnings.push(format!(
"warning: input truncated (exceeded {} node safety cap)",
priority_cfg.safety_cap
));
}
let out = find_largest_render_under_budgets(
&mut order_build,
config,
Expand All @@ -89,3 +98,73 @@ pub fn headson(
warnings,
})
}

#[cfg(test)]
mod tests {
use super::*;

fn test_render_config() -> RenderConfig {
RenderConfig {
template: OutputTemplate::Pseudo,
indent_unit: " ".to_string(),
space: " ".to_string(),
newline: "\n".to_string(),
color_mode: ColorMode::Off,
color_enabled: false,
style: serialization::types::Style::Default,
prefer_tail_arrays: false,
string_free_prefix_graphemes: None,
debug: false,
primary_source_name: None,
show_fileset_headers: false,
fileset_tree: false,
count_fileset_headers_in_budgets: false,
grep_highlight: None,
}
}

#[test]
fn safety_cap_warning_emitted_when_exceeded() {
// Use a tiny safety cap so we can trigger it with minimal input.
// An array [1,2,3,4,5] generates: 1 root array + 5 children = 6 nodes.
// With safety_cap=5, we should hit the cap.
let mut priority_cfg = PriorityConfig::new(usize::MAX, usize::MAX);
priority_cfg.safety_cap = 5;

let result = headson(
InputKind::Json(b"[1,2,3,4,5]".to_vec()),
&test_render_config(),
&priority_cfg,
&GrepConfig::default(),
Budgets::default(),
)
.expect("headson should succeed");

assert!(
result.warnings.iter().any(|w| w.contains("safety cap")),
"expected safety cap warning, got: {:?}",
result.warnings
);
}

#[test]
fn no_safety_cap_warning_when_not_exceeded() {
// With default (2M) cap, a small input should not trigger warning.
let priority_cfg = PriorityConfig::new(usize::MAX, usize::MAX);

let result = headson(
InputKind::Json(b"[1,2,3]".to_vec()),
&test_render_config(),
&priority_cfg,
&GrepConfig::default(),
Budgets::default(),
)
.expect("headson should succeed");

assert!(
!result.warnings.iter().any(|w| w.contains("safety cap")),
"unexpected safety cap warning: {:?}",
result.warnings
);
}
}
7 changes: 5 additions & 2 deletions src/order/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -789,6 +789,7 @@ pub fn build_order(
arena_index: Some(root_ar),
}));

let mut safety_cap_hit = false;
while let Some(Reverse(entry)) = heap.pop() {
let mut scope = Scope {
arena,
Expand All @@ -800,7 +801,7 @@ pub fn build_order(
nodes: &mut nodes,
scores: &mut scores,
heap: &mut heap,
safety_cap: SAFETY_CAP,
safety_cap: config.safety_cap,
object_type: &mut object_type,
index_in_parent_array: &mut index_in_parent_array,
arena_to_pq: &mut arena_to_pq,
Expand All @@ -809,7 +810,8 @@ pub fn build_order(
duplicate_counts: &duplicate_counts,
};
scope.process_entry(&entry, &mut order);
if next_pq_id >= SAFETY_CAP {
if next_pq_id >= config.safety_cap {
safety_cap_hit = true;
break;
}
}
Expand Down Expand Up @@ -863,6 +865,7 @@ pub fn build_order(
object_type,
code_lines,
fileset_render_slots,
safety_cap_hit,
})
}

Expand Down
1 change: 1 addition & 0 deletions src/order/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ pub mod scoring;
pub mod types;

pub use build::build_order;
pub use scoring::DEFAULT_SAFETY_CAP;
pub use types::{
FilesetRenderSlot, NodeId, NodeKind, ObjectType, PriorityConfig,
PriorityOrder, ROOT_PQ_ID, RankedNode,
Expand Down
6 changes: 3 additions & 3 deletions src/order/scoring.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Hard ceiling on number of PQ nodes built to prevent degenerate inputs
// from blowing up memory/time while exploring the frontier.
pub(crate) const SAFETY_CAP: usize = 2_000_000;
/// Hard ceiling on number of PQ nodes built to prevent degenerate inputs
/// from blowing up memory/time while exploring the frontier.
pub const DEFAULT_SAFETY_CAP: usize = 2_000_000;

/// Root starts at a fixed minimal score so its children naturally follow.
pub(crate) const ROOT_BASE_SCORE: u128 = 1;
Expand Down
9 changes: 9 additions & 0 deletions src/order/types.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
use std::collections::HashMap;
use std::sync::Arc;

use super::scoring::DEFAULT_SAFETY_CAP;

#[derive(Copy, Clone, Debug)]
pub struct PriorityConfig {
pub max_string_graphemes: usize,
Expand All @@ -14,6 +16,9 @@ pub struct PriorityConfig {
// Indicates that rendering may favor structural breadth over deep string
// expansion under line-capped previews.
pub line_budget_only: bool,
/// Hard ceiling on priority queue nodes to prevent degenerate inputs
/// from exhausting memory/time. Default is 2,000,000.
pub safety_cap: usize,
}

impl PriorityConfig {
Expand All @@ -25,6 +30,7 @@ impl PriorityConfig {
array_bias: ArrayBias::HeadMidTail,
array_sampler: ArraySamplerStrategy::Default,
line_budget_only: false,
safety_cap: DEFAULT_SAFETY_CAP,
}
}

Expand All @@ -51,6 +57,7 @@ impl PriorityConfig {
array_bias: ArrayBias::HeadMidTail,
array_sampler,
line_budget_only,
safety_cap: DEFAULT_SAFETY_CAP,
}
}
}
Expand Down Expand Up @@ -180,6 +187,8 @@ pub struct PriorityOrder {
pub code_lines: HashMap<usize, Arc<Vec<String>>>,
// For filesets, preserve ingest order and suppression state for render slots.
pub fileset_render_slots: Option<Vec<FilesetRenderSlot>>,
/// True if the priority queue expansion hit the safety cap.
pub safety_cap_hit: bool,
}

#[derive(Copy, Clone, Debug)]
Expand Down
4 changes: 4 additions & 0 deletions src/serialization/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ fn array_omitted_markers_pseudo_head_and_tail() {
array_bias: crate::ArrayBias::HeadMidTail,
array_sampler: crate::ArraySamplerStrategy::Default,
line_budget_only: false,
safety_cap: crate::DEFAULT_SAFETY_CAP,
};
let arena = crate::ingest::formats::json::build_json_tree_arena(
"[1,2,3]", &cfg_prio,
Expand Down Expand Up @@ -149,6 +150,7 @@ fn array_omitted_markers_js_head_and_tail() {
array_bias: crate::ArrayBias::HeadMidTail,
array_sampler: crate::ArraySamplerStrategy::Default,
line_budget_only: false,
safety_cap: crate::DEFAULT_SAFETY_CAP,
};
let arena = crate::ingest::formats::json::build_json_tree_arena(
"[1,2,3]", &cfg_prio,
Expand Down Expand Up @@ -186,6 +188,7 @@ fn array_omitted_markers_yaml_head_and_tail() {
array_bias: crate::ArrayBias::HeadMidTail,
array_sampler: crate::ArraySamplerStrategy::Default,
line_budget_only: false,
safety_cap: crate::DEFAULT_SAFETY_CAP,
};
let arena = crate::ingest::formats::json::build_json_tree_arena(
"[1,2,3]", &cfg_prio,
Expand Down Expand Up @@ -523,6 +526,7 @@ fn force_child_hooks_removed() {
object_type: vec![ObjectType::Object; 3],
code_lines: HashMap::new(),
fileset_render_slots: None,
safety_cap_hit: false,
};
let mut flags = Vec::new();
let render_id = 1u32;
Expand Down
1 change: 1 addition & 0 deletions tests/fileset_tree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -630,6 +630,7 @@ fn tree_omitted_folders_render_in_input_order() {
array_bias: headson::ArrayBias::HeadMidTail,
array_sampler: headson::ArraySamplerStrategy::Default,
line_budget_only: true,
safety_cap: headson::DEFAULT_SAFETY_CAP,
};
let grep_cfg = headson::GrepConfig::default();
let budgets = headson::Budgets {
Expand Down
Loading