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 block_is_bare_last_result(block: &Block) -> bool {
236 if block.pipelines.len() != 1 {
237 return false;
238 }
239 pipeline_is_bare_last_result(&block.pipelines[0])
240}
241
242fn pipeline_is_bare_last_result(pipeline: &Pipeline) -> bool {
243 if pipeline.elements.len() != 1 {
244 return false;
245 }
246 let element = &pipeline.elements[0];
247 if element.redirection.is_some() {
248 return false;
249 }
250 expr_is_bare_last_result(&element.expr)
251}
252
253fn expr_is_bare_last_result(expr: &Expression) -> bool {
254 match &expr.expr {
255 Expr::Var(var_id) => *var_id == LAST_VARIABLE_ID,
256 Expr::FullCellPath(path) => expr_is_bare_last_result(&path.head),
257 Expr::Block(block_id) | Expr::RowCondition(block_id) | Expr::Closure(block_id) => {
259 let _ = block_id;
261 false
262 }
263 Expr::Subexpression(block_id) => {
264 let _ = block_id;
268 false
269 }
270 _ => false,
271 }
272}
273
274pub fn block_is_bare_last_result_with<'a>(
278 block: &Block,
279 get_block: &mut dyn FnMut(crate::BlockId) -> &'a Block,
280) -> bool {
281 if block.pipelines.len() != 1 {
282 return false;
283 }
284 let pipeline = &block.pipelines[0];
285 if pipeline.elements.len() != 1 {
286 return false;
287 }
288 let element = &pipeline.elements[0];
289 if element.redirection.is_some() {
290 return false;
291 }
292 expr_is_bare_last_result_with(&element.expr, get_block)
293}
294
295fn expr_is_bare_last_result_with<'a>(
296 expr: &Expression,
297 get_block: &mut dyn FnMut(crate::BlockId) -> &'a Block,
298) -> bool {
299 match &expr.expr {
300 Expr::Var(var_id) => *var_id == LAST_VARIABLE_ID,
301 Expr::FullCellPath(path) => expr_is_bare_last_result_with(&path.head, get_block),
302 Expr::Subexpression(block_id) => {
303 let inner = get_block(*block_id);
304 block_is_bare_last_result_with(inner, get_block)
305 }
306 _ => false,
307 }
308}
309
310#[cfg(test)]
311mod tests {
312 use super::*;
313 use crate::record;
314
315 #[test]
316 fn under_budget_unchanged() {
317 let v = Value::test_int(42);
318 let size = v.memory_size();
319 let (out, truncated) = truncate_value_to_budget(v.clone(), size);
320 assert!(!truncated);
321 assert_eq!(out, v);
322 assert!(out.memory_size() <= size);
323 }
324
325 #[test]
326 fn list_prefix_respects_budget() {
327 let items: Vec<_> = (0..100).map(Value::test_int).collect();
328 let list = Value::test_list(items);
329 let full = list.memory_size();
330 assert!(full > 0);
331
332 let one = Value::test_int(0).memory_size();
334 let budget = Value::test_list(vec![]).memory_size() + one * 3;
335 let (out, truncated) = truncate_value_to_budget(list, budget);
336 assert!(truncated);
337 assert!(out.memory_size() <= budget);
338 match out {
339 Value::List { vals, .. } => {
340 assert!(vals.len() <= 3);
341 assert!(
342 !vals.is_empty()
343 || budget < Value::test_list(vec![Value::test_int(0)]).memory_size()
344 );
345 }
346 Value::Nothing { .. } => {
347 assert!(budget < Value::test_list(vec![Value::test_int(0)]).memory_size());
349 }
350 other => panic!("unexpected truncated type: {other:?}"),
351 }
352 }
353
354 #[test]
355 fn string_prefix_respects_budget() {
356 let s = "x".repeat(10_000);
357 let val = Value::test_string(s);
358 let budget = std::mem::size_of::<Value>() + 100;
359 let (out, truncated) = truncate_value_to_budget(val, budget);
360 assert!(truncated);
361 assert!(out.memory_size() <= budget);
362 if let Value::String { val, .. } = out {
363 assert!(val.len() <= 100);
364 }
365 }
366
367 #[test]
368 fn binary_prefix_respects_budget() {
369 let val = Value::test_binary(vec![0u8; 10_000]);
370 let budget = std::mem::size_of::<Value>() + 64;
371 let (out, truncated) = truncate_value_to_budget(val, budget);
372 assert!(truncated);
373 assert!(out.memory_size() <= budget);
374 if let Value::Binary { val, .. } = out {
375 assert!(val.len() <= 64);
376 }
377 }
378
379 #[test]
380 fn zero_budget_yields_nothing() {
381 let v = Value::test_string("hello");
382 let (out, truncated) = truncate_value_to_budget(v, 0);
383 assert!(truncated);
384 assert!(matches!(out, Value::Nothing { .. }));
385 }
386
387 #[test]
388 fn table_like_list_keeps_whole_records_only() {
389 let row = |name: &str| {
391 Value::test_record(record! {
392 "name" => Value::test_string(name),
393 "type" => Value::test_string("file"),
394 "size" => Value::test_int(1),
395 "modified" => Value::test_string("now"),
396 })
397 };
398 let rows: Vec<_> = (0..50).map(|i| row(&format!("f{i}.txt"))).collect();
399 let list = Value::test_list(rows);
400 let one = row("x").memory_size();
401 let budget = Value::test_list(vec![]).memory_size() + one * 3 + one / 2;
402
403 let (out, truncated) = truncate_value_to_budget(list, budget);
404 assert!(truncated);
405 assert!(out.memory_size() <= budget);
406
407 let Value::List { vals, .. } = out else {
408 panic!("expected list, got {out:?}");
409 };
410 assert!(vals.len() <= 3);
411 assert!(!vals.is_empty());
412 for v in &vals {
414 match v {
415 Value::Record { val, .. } => {
416 assert_eq!(val.len(), 4, "row must keep all columns for table display");
417 assert!(val.get("name").is_some());
418 assert!(val.get("type").is_some());
419 assert!(val.get("size").is_some());
420 assert!(val.get("modified").is_some());
421 }
422 other => panic!("expected full record row, got {other:?}"),
423 }
424 }
425 }
426
427 #[test]
428 fn record_fields_respect_budget() {
429 let rec = Value::test_record(record! {
430 "a" => Value::test_string("x".repeat(5000)),
431 "b" => Value::test_string("y".repeat(5000)),
432 });
433 let budget = rec.memory_size() / 2;
434 let (out, truncated) = truncate_value_to_budget(rec, budget);
435 assert!(truncated);
436 assert!(out.memory_size() <= budget);
437 }
438}