nc-gcode-interpreter 0.2.4__tar.gz → 0.2.5__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/CHANGELOG.md +14 -0
  2. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/Cargo.lock +1 -1
  3. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/Cargo.toml +1 -1
  4. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/PKG-INFO +1 -1
  5. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/src/output.rs +83 -13
  6. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/.github/workflows/build-and-release.yml +0 -0
  7. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/.gitignore +0 -0
  8. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/CONTRIBUTING.md +0 -0
  9. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/Development.md +0 -0
  10. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/LICENSE +0 -0
  11. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/README.md +0 -0
  12. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/TODO.md +0 -0
  13. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/docs/resumable-interpretation-design.md +0 -0
  14. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/docs/sinumerik-execution-model.md +0 -0
  15. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/examples/actual_position.csv +0 -0
  16. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/examples/actual_position.mpf +0 -0
  17. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/examples/arc.csv +0 -0
  18. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/examples/arc.mpf +0 -0
  19. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/examples/arrays.csv +0 -0
  20. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/examples/arrays.mpf +0 -0
  21. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/examples/axis_index_assignment.csv +0 -0
  22. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/examples/axis_index_assignment.mpf +0 -0
  23. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/examples/basic_math.csv +0 -0
  24. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/examples/basic_math.mpf +0 -0
  25. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/examples/case.csv +0 -0
  26. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/examples/case.mpf +0 -0
  27. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/examples/case_insensitive_variables.csv +0 -0
  28. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/examples/case_insensitive_variables.mpf +0 -0
  29. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/examples/custom_vars.csv +0 -0
  30. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/examples/custom_vars.mpf +0 -0
  31. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/examples/def_string.csv +0 -0
  32. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/examples/def_string.mpf +0 -0
  33. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/examples/defaults.csv +0 -0
  34. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/examples/defaults.mpf +0 -0
  35. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/examples/edge_cases.csv +0 -0
  36. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/examples/edge_cases.mpf +0 -0
  37. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/examples/flattening/flatten_demo.mpf +0 -0
  38. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/examples/flattening/flatten_demo_flattened.csv +0 -0
  39. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/examples/flattening/flatten_demo_raw.csv +0 -0
  40. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/examples/for_loop.csv +0 -0
  41. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/examples/for_loop.mpf +0 -0
  42. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/examples/function_calls.csv +0 -0
  43. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/examples/function_calls.mpf +0 -0
  44. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/examples/goto.csv +0 -0
  45. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/examples/goto.mpf +0 -0
  46. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/examples/if_statement.csv +0 -0
  47. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/examples/if_statement.mpf +0 -0
  48. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/examples/increment.csv +0 -0
  49. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/examples/increment.mpf +0 -0
  50. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/examples/logic_operators.csv +0 -0
  51. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/examples/logic_operators.mpf +0 -0
  52. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/examples/loop.csv +0 -0
  53. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/examples/loop.mpf +0 -0
  54. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/examples/multiple_m_codes.csv +0 -0
  55. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/examples/multiple_m_codes.mpf +0 -0
  56. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/examples/precedence.csv +0 -0
  57. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/examples/precedence.mpf +0 -0
  58. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/examples/r_param.csv +0 -0
  59. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/examples/r_param.mpf +0 -0
  60. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/examples/repeat.csv +0 -0
  61. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/examples/repeat.mpf +0 -0
  62. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/examples/simple.csv +0 -0
  63. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/examples/simple.mpf +0 -0
  64. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/examples/spline.csv +0 -0
  65. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/examples/spline.mpf +0 -0
  66. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/examples/tool.csv +0 -0
  67. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/examples/tool.mpf +0 -0
  68. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/examples/trans.csv +0 -0
  69. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/examples/trans.mpf +0 -0
  70. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/examples/trans2.csv +0 -0
  71. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/examples/trans2.mpf +0 -0
  72. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/examples/trans_ic.csv +0 -0
  73. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/examples/trans_ic.mpf +0 -0
  74. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/examples/trans_reset.csv +0 -0
  75. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/examples/trans_reset.mpf +0 -0
  76. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/examples/trig.csv +0 -0
  77. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/examples/trig.mpf +0 -0
  78. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/examples/variables.csv +0 -0
  79. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/examples/variables.mpf +0 -0
  80. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/ggroups/generate_g_commands.py +0 -0
  81. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/pyproject.toml +0 -0
  82. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/python/example/minimal.py +0 -0
  83. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/python/example/streaming.py +0 -0
  84. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/python/nc_gcode_interpreter/__init__.py +0 -0
  85. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/python/nc_gcode_interpreter/_internal.pyi +0 -0
  86. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/python/nc_gcode_interpreter/cli.py +0 -0
  87. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/python/nc_gcode_interpreter/ggroups.json +0 -0
  88. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/python/nc_gcode_interpreter/py.typed +0 -0
  89. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/python/nc_gcode_interpreter/viz.py +0 -0
  90. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/python/tests/test_arithmetic_functions.py +0 -0
  91. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/python/tests/test_batch_variables.py +0 -0
  92. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/python/tests/test_batches.py +0 -0
  93. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/python/tests/test_diagnostics.py +0 -0
  94. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/python/tests/test_expected_output.py +0 -0
  95. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/python/tests/test_field_defects.py +0 -0
  96. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/python/tests/test_flags.py +0 -0
  97. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/python/tests/test_flatten.py +0 -0
  98. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/python/tests/test_g_groups.py +0 -0
  99. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/python/tests/test_g_vocabulary.py +0 -0
  100. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/python/tests/test_jumps.py +0 -0
  101. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/python/tests/test_real_comparisons.py +0 -0
  102. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/python/tests/test_stage1.py +0 -0
  103. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/python/tests/test_streaming.py +0 -0
  104. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/python/tests/test_structured_errors.py +0 -0
  105. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/python/tests/test_unsupported.py +0 -0
  106. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/rustfmt.toml +0 -0
  107. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/src/errors.rs +0 -0
  108. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/src/flatten.rs +0 -0
  109. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/src/grammar.pest +0 -0
  110. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/src/interpret_rules.rs +0 -0
  111. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/src/interpreter.rs +0 -0
  112. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/src/lib.rs +0 -0
  113. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/src/line_driver.rs +0 -0
  114. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/src/main.rs +0 -0
  115. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/src/modal_groups.rs +0 -0
  116. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/src/state.rs +0 -0
  117. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/src/structure_scan.rs +0 -0
  118. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/src/types.rs +0 -0
  119. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/src/vm.rs +0 -0
  120. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/tests/cli.rs +0 -0
  121. {nc_gcode_interpreter-0.2.4 → nc_gcode_interpreter-0.2.5}/uv.lock +0 -0
@@ -4,6 +4,20 @@ Notable changes to **nc-gcode-interpreter**. The format loosely follows
4
4
  [Keep a Changelog](https://keepachangelog.com/); versions are git tags,
5
5
  released to PyPI.
6
6
 
7
+ ## [v0.2.5] - 2026-07-08
8
+
9
+ ### Changed
10
+
11
+ - Interpreter throughput on the DataFrame/batch path: the per-output-row `Row`
12
+ allocations are now recycled instead of freed. Profiling the 1.1 GB → DataFrame
13
+ conversion showed ~50% of CPU in the system allocator, dominated by allocating
14
+ and freeing each of the 22M rows' cell buffers. After a batch is built its rows
15
+ are cleared (capacity retained) into a pool and handed back to the interpreter
16
+ to refill, bounding live row allocations to ~2× the batch size instead of the
17
+ whole-file row count. Output is byte-identical; the streaming (`nc_to_rows`)
18
+ and in-memory collect paths are unchanged. ~10% off end-to-end on the large
19
+ real-world program (#61).
20
+
7
21
  ## [v0.2.4] - 2026-07-08
8
22
 
9
23
  ### Changed
@@ -506,7 +506,7 @@ checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4"
506
506
 
507
507
  [[package]]
508
508
  name = "nc-gcode-interpreter"
509
- version = "0.2.4"
509
+ version = "0.2.5"
510
510
  dependencies = [
511
511
  "arrow-array",
512
512
  "arrow-data",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "nc-gcode-interpreter"
3
- version = "0.2.4"
3
+ version = "0.2.5"
4
4
  edition = "2021"
5
5
  readme = "README.md"
6
6
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nc-gcode-interpreter
3
- Version: 0.2.4
3
+ Version: 0.2.5
4
4
  Classifier: Programming Language :: Rust
5
5
  Classifier: Programming Language :: Python :: Implementation :: CPython
6
6
  Classifier: Programming Language :: Python :: 3.12
@@ -82,6 +82,13 @@ impl CellMap {
82
82
  None
83
83
  }
84
84
 
85
+ /// Empty the cells, retaining the backing allocation so a recycled row can
86
+ /// be refilled without a fresh heap allocation (see [`Row::reset_for_reuse`]).
87
+ #[inline]
88
+ pub fn clear(&mut self) {
89
+ self.entries.clear();
90
+ }
91
+
85
92
  #[inline]
86
93
  pub fn get_mut(&mut self, key: &str) -> Option<&mut Value> {
87
94
  self.entries.iter_mut().find(|(k, _)| *k == key).map(|(_, v)| v)
@@ -131,6 +138,20 @@ pub struct Row {
131
138
  pub variable_changes: Vec<(String, f64)>,
132
139
  }
133
140
 
141
+ impl Row {
142
+ /// Empty this row for reuse as the next `current`, retaining both the cells
143
+ /// and variable-change allocations. On the batch path a finished row's
144
+ /// buffers are consumed into the columnar batch and then recycled here,
145
+ /// which collapses the per-row heap alloc/free (~22M on the 1.1 GB file,
146
+ /// the dominant allocator cost) into a small pool cycled batch-to-batch.
147
+ #[inline]
148
+ pub fn reset_for_reuse(&mut self) {
149
+ self.line_no = 0;
150
+ self.cells.clear();
151
+ self.variable_changes.clear();
152
+ }
153
+ }
154
+
134
155
  /// Sparse side-table of variable-change events for the batch path, mirroring
135
156
  /// what the streaming `nc_to_rows` yields per row as `variable_changes`. Stored
136
157
  /// columnar (struct-of-arrays) so it converts to an Arrow table cheaply:
@@ -205,6 +226,10 @@ pub struct BatchStreamSink {
205
226
  sender: std::sync::mpsc::SyncSender<Table>,
206
227
  builder: BatchBuilder,
207
228
  buffer: Vec<Row>,
229
+ /// Emptied rows kept for reuse: after a batch is built the buffered rows'
230
+ /// allocations are cleared (not freed) into this pool, and `accept` hands
231
+ /// them back to the interpreter to refill instead of allocating fresh.
232
+ recycle: Vec<Row>,
208
233
  batch_size: usize,
209
234
  /// Whether to accumulate variable-change events (opt-in `include_variables`).
210
235
  record_variables: bool,
@@ -226,7 +251,7 @@ impl BatchStreamSink {
226
251
  /// rows have accumulated. Variable-only rows carry no output cells and are
227
252
  /// dropped from the batch table, but their variable changes are still
228
253
  /// recorded into the sparse event log when `record_variables` is on.
229
- fn accept(&mut self, row: Row) -> Result<(), ParsingError> {
254
+ fn accept(&mut self, mut row: Row) -> Result<Option<Row>, ParsingError> {
230
255
  if self.record_variables {
231
256
  for (name, value) in &row.variable_changes {
232
257
  let id = match self.name_ids.get(name) {
@@ -246,15 +271,20 @@ impl BatchStreamSink {
246
271
  self.events.value.push(*value);
247
272
  }
248
273
  }
274
+ // A variable-only row carries no output cell: it is not buffered, so hand
275
+ // its (cleared) allocation straight back for reuse.
249
276
  if row.cells.is_empty() {
250
- return Ok(());
277
+ row.reset_for_reuse();
278
+ return Ok(Some(row));
251
279
  }
252
280
  self.output_row_count += 1;
253
281
  self.buffer.push(row);
254
282
  if self.buffer.len() >= self.batch_size {
255
283
  self.emit()?;
256
284
  }
257
- Ok(())
285
+ // Hand back a pooled row (refilled after the last emit) so the caller
286
+ // refills a retained allocation instead of allocating a fresh one.
287
+ Ok(self.recycle.pop())
258
288
  }
259
289
 
260
290
  /// Send the accumulated variable-change events once, at finish. A no-op when
@@ -272,8 +302,20 @@ impl BatchStreamSink {
272
302
  if self.buffer.is_empty() {
273
303
  return Ok(());
274
304
  }
275
- let rows = std::mem::take(&mut self.buffer);
305
+ // Take the buffered rows out to build the batch, then recycle their
306
+ // allocations: `build_batch` only borrows the rows (it copies each cell
307
+ // value into the columnar builders), so once it returns the row buffers
308
+ // are free to be cleared and pooled rather than dropped.
309
+ let mut rows = std::mem::take(&mut self.buffer);
276
310
  let table = self.builder.build_batch(&rows);
311
+ self.recycle.reserve(rows.len());
312
+ for mut row in rows.drain(..) {
313
+ row.reset_for_reuse();
314
+ self.recycle.push(row);
315
+ }
316
+ // `rows` is now empty but keeps its `Vec<Row>` capacity - reinstate it as
317
+ // the buffer so the outer vector is not reallocated each batch either.
318
+ self.buffer = rows;
277
319
  self.sender.send(table).map_err(|_| ParsingError::StreamClosed)
278
320
  }
279
321
  }
@@ -362,6 +404,7 @@ impl OutputRows {
362
404
  sender,
363
405
  builder: BatchBuilder::new(disable_forward_fill).with_line_numbers(emit_line_no),
364
406
  buffer: Vec::new(),
407
+ recycle: Vec::new(),
365
408
  batch_size,
366
409
  record_variables,
367
410
  events_sender,
@@ -416,32 +459,53 @@ impl OutputRows {
416
459
  }
417
460
  }
418
461
 
419
- /// Route a finished row to the sink, passing it through the flattener
420
- /// first when one is installed. Shared by `flush`.
421
- fn deliver(&mut self, row: Row) -> Result<(), ParsingError> {
462
+ /// Return a recycled row to the sink's pool. Used when a single `deliver`
463
+ /// yields more recycled rows than the one the caller can reuse as `current`
464
+ /// (the flattener fans one block out to many rows): the surplus goes back to
465
+ /// the pool instead of being freed, so recycling stays effective on
466
+ /// flattened paths too. A no-op for the non-recycling sinks.
467
+ fn reclaim(&mut self, row: Row) {
468
+ if let RowSink::Batch(sink) = &mut self.sink {
469
+ sink.recycle.push(row);
470
+ }
471
+ }
472
+
473
+ /// Route a row to the sink, passing it through the flattener first when one
474
+ /// is installed, and returning an emptied row the sink no longer needs
475
+ /// (batch path) so the caller can reuse its allocation as the next
476
+ /// `current`. `None` when nothing is available to recycle.
477
+ fn deliver(&mut self, row: Row) -> Result<Option<Row>, ParsingError> {
422
478
  if let Some(mut flattener) = self.flattener.take() {
423
479
  let mut flattened = Vec::new();
424
480
  flattener.push(row, &mut flattened);
425
481
  self.flattener = Some(flattener);
482
+ // Keep one recycled row for the caller to reuse; hand any others the
483
+ // sink returns this call straight back to the pool so they are not
484
+ // freed (a flattened block delivers many rows in one `deliver`).
485
+ let mut recycled = None;
426
486
  for row in flattened {
427
- self.deliver_to_sink(row)?;
487
+ if let Some(r) = self.deliver_to_sink(row)? {
488
+ if let Some(prev) = recycled.replace(r) {
489
+ self.reclaim(prev);
490
+ }
491
+ }
428
492
  }
429
- return Ok(());
493
+ return Ok(recycled);
430
494
  }
431
495
  self.deliver_to_sink(row)
432
496
  }
433
497
 
434
498
  /// Route a finished row to the sink: collected, streamed row-at-a-time, or
435
499
  /// fed to the worker-side batch producer.
436
- fn deliver_to_sink(&mut self, row: Row) -> Result<(), ParsingError> {
500
+ fn deliver_to_sink(&mut self, row: Row) -> Result<Option<Row>, ParsingError> {
437
501
  match &mut self.sink {
438
502
  RowSink::Collect(rows) => {
439
503
  rows.push(row);
440
- Ok(())
504
+ Ok(None)
441
505
  }
442
506
  // The receiver hung up: the consumer stopped iterating. Abort
443
507
  // interpretation instead of running the rest of the program.
444
- RowSink::Stream(sender) => sender.send(row).map_err(|_| ParsingError::StreamClosed),
508
+ RowSink::Stream(sender) => sender.send(row).map(|_| None).map_err(|_| ParsingError::StreamClosed),
445
509
  RowSink::Batch(sink) => sink.accept(row),
446
510
  }
447
511
  }
@@ -464,7 +528,13 @@ impl OutputRows {
464
528
  self.warned_g91 = true;
465
529
  }
466
530
  }
467
- self.deliver(row)
531
+ // `mem::take` above left `current` as an empty (unallocated) default; if
532
+ // the sink handed back a pooled row, adopt its retained allocation for
533
+ // the next block instead of allocating fresh on the first cell insert.
534
+ if let Some(recycled) = self.deliver(row)? {
535
+ self.current = recycled;
536
+ }
537
+ Ok(())
468
538
  }
469
539
 
470
540
  /// Begin the row for the block at `line_no`, flushing the previous row.