1use crate::{
4 LAST_VARIABLE_ID, Span, Value,
5 ast::{Block, Expr, Expression, Pipeline},
6};
7
8pub fn truncate_value_to_budget(value: Value, budget: usize) -> (Value, bool) {
22 if budget == 0 {
23 return (Value::nothing(value.span()), true);
24 }
25
26 if value.memory_size() <= budget {
27 return (value, false);
28 }
29
30 let span = value.span();
31 match value {
32 Value::List { vals, .. } => truncate_list(vals.into_owned(), budget, span),
33 Value::Record { val, .. } => truncate_record(val.into_owned(), budget, span),
34 Value::String { val, .. } => truncate_string(val, budget, span),
35 Value::Binary { val, .. } => truncate_binary(val.into_owned(), budget, span),
36 Value::Glob { val, .. } => {
37 truncate_string(val, budget, span)
39 }
40 other => {
41 drop(other);
43 (Value::nothing(span), true)
44 }
45 }
46}
47
48fn truncate_list(vals: Vec<Value>, budget: usize, span: Span) -> (Value, bool) {
49 let base = Value::list(vec![], span).memory_size();
51 if base > budget {
52 return (Value::nothing(span), true);
53 }
54
55 let original_len = vals.len();
56 let table_like = !vals.is_empty() && vals.iter().all(|v| matches!(v, Value::Record { .. }));
58
59 let mut kept = Vec::new();
60 let mut used = base;
61 let mut truncated = false;
62
63 for item in vals {
64 let item_size = item.memory_size();
65 if used.saturating_add(item_size) <= budget {
66 used += item_size;
67 kept.push(item);
68 continue;
69 }
70
71 truncated = true;
73
74 if table_like {
75 break;
77 }
78
79 let remaining = budget.saturating_sub(used);
82 if remaining > 0 && can_partially_truncate_list_item(&item) {
83 let (partial, _) = truncate_value_to_budget(item, remaining);
84 if !matches!(partial, Value::Nothing { .. })
85 && used.saturating_add(partial.memory_size()) <= budget
86 {
87 kept.push(partial);
88 }
89 }
90 break;
91 }
92
93 if kept.len() < original_len {
94 truncated = true;
95 }
96
97 let out = Value::list(kept, span);
98 let still_over = out.memory_size() > budget;
99 (out, truncated || still_over)
100}
101
102fn can_partially_truncate_list_item(item: &Value) -> bool {
104 matches!(
105 item,
106 Value::String { .. } | Value::Binary { .. } | Value::Glob { .. } | Value::List { .. }
107 )
108}
109
110fn truncate_record(record: crate::Record, budget: usize, span: Span) -> (Value, bool) {
111 let base = Value::record(crate::Record::new(), span).memory_size();
112 if base > budget {
113 return (Value::nothing(span), true);
114 }
115
116 let mut out = crate::Record::new();
117 let mut used = base;
118 let mut truncated = false;
119 let original_len = record.len();
120
121 for (key, val) in record {
122 let key_cost = key.capacity();
123 let remaining = budget.saturating_sub(used.saturating_add(key_cost));
124 if remaining == 0 {
125 truncated = true;
126 break;
127 }
128
129 let (stored_val, val_trunc) = if val.memory_size() <= remaining {
130 (val, false)
131 } else {
132 let (v, t) = truncate_value_to_budget(val, remaining);
133 (v, t)
134 };
135
136 let entry_size = key_cost + stored_val.memory_size();
137 if used.saturating_add(entry_size) > budget {
138 truncated = true;
139 break;
140 }
141 used += entry_size;
142 truncated |= val_trunc;
143 out.push(key, stored_val);
144 }
145
146 if out.len() < original_len {
147 truncated = true;
148 }
149
150 (Value::record(out, span), truncated)
151}
152
153fn truncate_string(val: String, budget: usize, span: Span) -> (Value, bool) {
154 let whole = Value::string(val, span);
156 if whole.memory_size() <= budget {
157 return (whole, false);
158 }
159
160 let val = match whole {
161 Value::String { val, .. } => val,
162 other => {
163 drop(other);
165 return (Value::nothing(span), true);
166 }
167 };
168
169 if std::mem::size_of::<Value>() > budget {
170 return (Value::nothing(span), true);
171 }
172
173 let mut end = budget
175 .saturating_sub(std::mem::size_of::<Value>())
176 .min(val.len());
177 while end > 0 && !val.is_char_boundary(end) {
178 end -= 1;
179 }
180
181 loop {
182 let prefix = val[..end].to_string();
183 let out = Value::string(prefix, span);
184 if out.memory_size() <= budget {
185 return (out, true);
186 }
187 if end == 0 {
188 return (Value::nothing(span), true);
189 }
190 end -= 1;
191 while end > 0 && !val.is_char_boundary(end) {
192 end -= 1;
193 }
194 }
195}
196
197fn truncate_binary(val: Vec<u8>, budget: usize, span: Span) -> (Value, bool) {
198 let whole = Value::binary(val, span);
199 if whole.memory_size() <= budget {
200 return (whole, false);
201 }
202
203 let val = match whole {
204 Value::Binary { val, .. } => val,
205 other => {
206 drop(other);
207 return (Value::nothing(span), true);
208 }
209 };
210
211 if std::mem::size_of::<Value>() > budget {
212 return (Value::nothing(span), true);
213 }
214
215 let mut end = budget
216 .saturating_sub(std::mem::size_of::<Value>())
217 .min(val.len());
218 loop {
219 let out = Value::binary(val[..end].to_vec(), span);
220 if out.memory_size() <= budget {
221 return (out, true);
222 }
223 if end == 0 {
224 return (Value::nothing(span), true);
225 }
226 end -= 1;
227 }
228}
229
230pub fn value_is_error_only(value: &Value) -> bool {
237 match value {
238 Value::Error { .. } => true,
239 Value::List { vals, .. } => !vals.is_empty() && vals.iter().all(Value::is_error),
240 _ => false,
241 }
242}
243
244pub fn block_is_bare_last_result(block: &Block) -> bool {
250 if block.pipelines.len() != 1 {
251 return false;
252 }
253 pipeline_is_bare_last_result(&block.pipelines[0])
254}
255
256fn pipeline_is_bare_last_result(pipeline: &Pipeline) -> bool {
257 if pipeline.elements.len() != 1 {
258 return false;
259 }
260 let element = &pipeline.elements[0];
261 if element.redirection.is_some() {
262 return false;
263 }
264 expr_is_bare_last_result(&element.expr)
265}
266
267fn expr_is_bare_last_result(expr: &Expression) -> bool {
268 match &expr.expr {
269 Expr::Var(var_id) => *var_id == LAST_VARIABLE_ID,
270 Expr::FullCellPath(path) => expr_is_bare_last_result(&path.head),
271 Expr::Block(block_id) | Expr::RowCondition(block_id) | Expr::Closure(block_id) => {
273 let _ = block_id;
275 false
276 }
277 Expr::Subexpression(block_id) => {
278 let _ = block_id;
282 false
283 }
284 _ => false,
285 }
286}
287
288pub fn block_is_bare_last_result_with<'a>(
292 block: &Block,
293 get_block: &mut dyn FnMut(crate::BlockId) -> &'a Block,
294) -> bool {
295 if block.pipelines.len() != 1 {
296 return false;
297 }
298 let pipeline = &block.pipelines[0];
299 if pipeline.elements.len() != 1 {
300 return false;
301 }
302 let element = &pipeline.elements[0];
303 if element.redirection.is_some() {
304 return false;
305 }
306 expr_is_bare_last_result_with(&element.expr, get_block)
307}
308
309fn expr_is_bare_last_result_with<'a>(
310 expr: &Expression,
311 get_block: &mut dyn FnMut(crate::BlockId) -> &'a Block,
312) -> bool {
313 match &expr.expr {
314 Expr::Var(var_id) => *var_id == LAST_VARIABLE_ID,
315 Expr::FullCellPath(path) => expr_is_bare_last_result_with(&path.head, get_block),
316 Expr::Subexpression(block_id) => {
317 let inner = get_block(*block_id);
318 block_is_bare_last_result_with(inner, get_block)
319 }
320 _ => false,
321 }
322}
323
324#[cfg(test)]
325mod tests {
326 use super::*;
327 use crate::record;
328
329 #[test]
330 fn under_budget_unchanged() {
331 let v = Value::test_int(42);
332 let size = v.memory_size();
333 let (out, truncated) = truncate_value_to_budget(v.clone(), size);
334 assert!(!truncated);
335 assert_eq!(out, v);
336 assert!(out.memory_size() <= size);
337 }
338
339 #[test]
340 fn list_prefix_respects_budget() {
341 let items: Vec<_> = (0..100).map(Value::test_int).collect();
342 let list = Value::test_list(items);
343 let full = list.memory_size();
344 assert!(full > 0);
345
346 let one = Value::test_int(0).memory_size();
348 let budget = Value::test_list(vec![]).memory_size() + one * 3;
349 let (out, truncated) = truncate_value_to_budget(list, budget);
350 assert!(truncated);
351 assert!(out.memory_size() <= budget);
352 match out {
353 Value::List { vals, .. } => {
354 assert!(vals.len() <= 3);
355 assert!(
356 !vals.is_empty()
357 || budget < Value::test_list(vec![Value::test_int(0)]).memory_size()
358 );
359 }
360 Value::Nothing { .. } => {
361 assert!(budget < Value::test_list(vec![Value::test_int(0)]).memory_size());
363 }
364 other => panic!("unexpected truncated type: {other:?}"),
365 }
366 }
367
368 #[test]
369 fn string_prefix_respects_budget() {
370 let s = "x".repeat(10_000);
371 let val = Value::test_string(s);
372 let budget = std::mem::size_of::<Value>() + 100;
373 let (out, truncated) = truncate_value_to_budget(val, budget);
374 assert!(truncated);
375 assert!(out.memory_size() <= budget);
376 if let Value::String { val, .. } = out {
377 assert!(val.len() <= 100);
378 }
379 }
380
381 #[test]
382 fn binary_prefix_respects_budget() {
383 let val = Value::test_binary(vec![0u8; 10_000]);
384 let budget = std::mem::size_of::<Value>() + 64;
385 let (out, truncated) = truncate_value_to_budget(val, budget);
386 assert!(truncated);
387 assert!(out.memory_size() <= budget);
388 if let Value::Binary { val, .. } = out {
389 assert!(val.len() <= 64);
390 }
391 }
392
393 #[test]
394 fn zero_budget_yields_nothing() {
395 let v = Value::test_string("hello");
396 let (out, truncated) = truncate_value_to_budget(v, 0);
397 assert!(truncated);
398 assert!(matches!(out, Value::Nothing { .. }));
399 }
400
401 #[test]
402 fn table_like_list_keeps_whole_records_only() {
403 let row = |name: &str| {
405 Value::test_record(record! {
406 "name" => Value::test_string(name),
407 "type" => Value::test_string("file"),
408 "size" => Value::test_int(1),
409 "modified" => Value::test_string("now"),
410 })
411 };
412 let rows: Vec<_> = (0..50).map(|i| row(&format!("f{i}.txt"))).collect();
413 let list = Value::test_list(rows);
414 let one = row("x").memory_size();
415 let budget = Value::test_list(vec![]).memory_size() + one * 3 + one / 2;
416
417 let (out, truncated) = truncate_value_to_budget(list, budget);
418 assert!(truncated);
419 assert!(out.memory_size() <= budget);
420
421 let Value::List { vals, .. } = out else {
422 panic!("expected list, got {out:?}");
423 };
424 assert!(vals.len() <= 3);
425 assert!(!vals.is_empty());
426 for v in &vals {
428 match v {
429 Value::Record { val, .. } => {
430 assert_eq!(val.len(), 4, "row must keep all columns for table display");
431 assert!(val.get("name").is_some());
432 assert!(val.get("type").is_some());
433 assert!(val.get("size").is_some());
434 assert!(val.get("modified").is_some());
435 }
436 other => panic!("expected full record row, got {other:?}"),
437 }
438 }
439 }
440
441 #[test]
442 fn record_fields_respect_budget() {
443 let rec = Value::test_record(record! {
444 "a" => Value::test_string("x".repeat(5000)),
445 "b" => Value::test_string("y".repeat(5000)),
446 });
447 let budget = rec.memory_size() / 2;
448 let (out, truncated) = truncate_value_to_budget(rec, budget);
449 assert!(truncated);
450 assert!(out.memory_size() <= budget);
451 }
452
453 fn error_value() -> Value {
454 Value::error(
455 crate::ShellError::Generic(crate::shell_error::generic::GenericError::new(
456 "boom",
457 "",
458 Span::test_data(),
459 )),
460 Span::test_data(),
461 )
462 }
463
464 #[test]
465 fn error_only_detects_error_value() {
466 assert!(value_is_error_only(&error_value()));
467 assert!(!value_is_error_only(&Value::test_int(1)));
468 assert!(!value_is_error_only(&Value::test_nothing()));
469 }
470
471 #[test]
472 fn error_only_detects_nonempty_error_list() {
473 let list = Value::test_list(vec![error_value(), error_value()]);
474 assert!(value_is_error_only(&list));
475 assert!(!value_is_error_only(&Value::test_list(vec![])));
476 }
477
478 #[test]
479 fn error_only_rejects_mixed_list() {
480 let list = Value::test_list(vec![error_value(), Value::test_int(2)]);
481 assert!(!value_is_error_only(&list));
482 }
483}