nc-gcode-interpreter 0.2.8__tar.gz → 0.2.10__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 (137) hide show
  1. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/.github/workflows/build-and-release.yml +7 -5
  2. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/CHANGELOG.md +60 -0
  3. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/Cargo.lock +1 -1
  4. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/Cargo.toml +1 -1
  5. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/PKG-INFO +1 -1
  6. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/README.md +14 -13
  7. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/docs/resumable-interpretation-design.md +1 -1
  8. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/python/tests/test_batches.py +17 -0
  9. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/python/tests/test_stage1.py +17 -0
  10. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/src/flatten.rs +60 -37
  11. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/src/interpret_rules.rs +3 -0
  12. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/src/interpreter.rs +89 -9
  13. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/src/lib.rs +38 -22
  14. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/src/line_driver.rs +147 -16
  15. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/src/main.rs +1 -1
  16. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/src/output.rs +368 -54
  17. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/src/state.rs +38 -14
  18. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/uv.lock +6 -27
  19. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/.github/workflows/benchmark.yml +0 -0
  20. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/.gitignore +0 -0
  21. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/CONTRIBUTING.md +0 -0
  22. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/Development.md +0 -0
  23. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/LICENSE +0 -0
  24. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/TODO.md +0 -0
  25. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/benches/fixtures.rs +0 -0
  26. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/benches/workloads.rs +0 -0
  27. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/build.rs +0 -0
  28. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/build_backend/nc_build_backend.py +0 -0
  29. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/docs/sinumerik-execution-model.md +0 -0
  30. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/examples/actual_position.csv +0 -0
  31. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/examples/actual_position.mpf +0 -0
  32. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/examples/arc.csv +0 -0
  33. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/examples/arc.mpf +0 -0
  34. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/examples/arrays.csv +0 -0
  35. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/examples/arrays.mpf +0 -0
  36. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/examples/axis_index_assignment.csv +0 -0
  37. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/examples/axis_index_assignment.mpf +0 -0
  38. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/examples/basic_math.csv +0 -0
  39. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/examples/basic_math.mpf +0 -0
  40. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/examples/case.csv +0 -0
  41. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/examples/case.mpf +0 -0
  42. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/examples/case_insensitive_variables.csv +0 -0
  43. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/examples/case_insensitive_variables.mpf +0 -0
  44. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/examples/custom_vars.csv +0 -0
  45. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/examples/custom_vars.mpf +0 -0
  46. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/examples/def_string.csv +0 -0
  47. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/examples/def_string.mpf +0 -0
  48. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/examples/defaults.csv +0 -0
  49. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/examples/defaults.mpf +0 -0
  50. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/examples/edge_cases.csv +0 -0
  51. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/examples/edge_cases.mpf +0 -0
  52. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/examples/flattening/flatten_demo.mpf +0 -0
  53. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/examples/flattening/flatten_demo_flattened.csv +0 -0
  54. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/examples/flattening/flatten_demo_raw.csv +0 -0
  55. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/examples/for_loop.csv +0 -0
  56. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/examples/for_loop.mpf +0 -0
  57. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/examples/function_calls.csv +0 -0
  58. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/examples/function_calls.mpf +0 -0
  59. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/examples/goto.csv +0 -0
  60. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/examples/goto.mpf +0 -0
  61. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/examples/if_statement.csv +0 -0
  62. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/examples/if_statement.mpf +0 -0
  63. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/examples/increment.csv +0 -0
  64. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/examples/increment.mpf +0 -0
  65. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/examples/logic_operators.csv +0 -0
  66. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/examples/logic_operators.mpf +0 -0
  67. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/examples/loop.csv +0 -0
  68. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/examples/loop.mpf +0 -0
  69. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/examples/multiple_m_codes.csv +0 -0
  70. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/examples/multiple_m_codes.mpf +0 -0
  71. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/examples/precedence.csv +0 -0
  72. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/examples/precedence.mpf +0 -0
  73. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/examples/r_param.csv +0 -0
  74. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/examples/r_param.mpf +0 -0
  75. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/examples/repeat.csv +0 -0
  76. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/examples/repeat.mpf +0 -0
  77. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/examples/rot.csv +0 -0
  78. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/examples/rot.mpf +0 -0
  79. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/examples/simple.csv +0 -0
  80. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/examples/simple.mpf +0 -0
  81. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/examples/spline.csv +0 -0
  82. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/examples/spline.mpf +0 -0
  83. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/examples/tool.csv +0 -0
  84. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/examples/tool.mpf +0 -0
  85. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/examples/trans.csv +0 -0
  86. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/examples/trans.mpf +0 -0
  87. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/examples/trans2.csv +0 -0
  88. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/examples/trans2.mpf +0 -0
  89. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/examples/trans_ic.csv +0 -0
  90. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/examples/trans_ic.mpf +0 -0
  91. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/examples/trans_reset.csv +0 -0
  92. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/examples/trans_reset.mpf +0 -0
  93. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/examples/trig.csv +0 -0
  94. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/examples/trig.mpf +0 -0
  95. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/examples/variables.csv +0 -0
  96. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/examples/variables.mpf +0 -0
  97. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/ggroups/generate_g_commands.py +0 -0
  98. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/pyproject.toml +0 -0
  99. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/python/example/minimal.py +0 -0
  100. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/python/example/streaming.py +0 -0
  101. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/python/nc_gcode_interpreter/__init__.py +0 -0
  102. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/python/nc_gcode_interpreter/_internal.pyi +0 -0
  103. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/python/nc_gcode_interpreter/cli.py +0 -0
  104. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/python/nc_gcode_interpreter/ggroups.json +0 -0
  105. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/python/nc_gcode_interpreter/py.typed +0 -0
  106. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/python/nc_gcode_interpreter/viz.py +0 -0
  107. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/python/tests/nc_source.py +0 -0
  108. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/python/tests/test_arithmetic_functions.py +0 -0
  109. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/python/tests/test_batch_variables.py +0 -0
  110. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/python/tests/test_compare_benchmarks.py +0 -0
  111. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/python/tests/test_diagnostics.py +0 -0
  112. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/python/tests/test_expected_output.py +0 -0
  113. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/python/tests/test_field_defects.py +0 -0
  114. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/python/tests/test_flags.py +0 -0
  115. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/python/tests/test_flatten.py +0 -0
  116. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/python/tests/test_frames.py +0 -0
  117. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/python/tests/test_g_groups.py +0 -0
  118. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/python/tests/test_g_vocabulary.py +0 -0
  119. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/python/tests/test_jumps.py +0 -0
  120. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/python/tests/test_real_comparisons.py +0 -0
  121. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/python/tests/test_streaming.py +0 -0
  122. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/python/tests/test_string_operations.py +0 -0
  123. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/python/tests/test_structured_errors.py +0 -0
  124. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/python/tests/test_unsupported.py +0 -0
  125. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/rustfmt.toml +0 -0
  126. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/scripts/benchmark.py +0 -0
  127. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/scripts/compare_benchmarks.py +0 -0
  128. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/src/errors.rs +0 -0
  129. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/src/frame.rs +0 -0
  130. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/src/grammar.pest +0 -0
  131. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/src/line_driver/experiments.rs +0 -0
  132. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/src/modal_groups.rs +0 -0
  133. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/src/structure_scan.rs +0 -0
  134. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/src/types.rs +0 -0
  135. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/src/vm.rs +0 -0
  136. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/tests/cli.rs +0 -0
  137. {nc_gcode_interpreter-0.2.8 → nc_gcode_interpreter-0.2.10}/tests/performance_contract.rs +0 -0
@@ -80,7 +80,7 @@ jobs:
80
80
  uses: PyO3/maturin-action@v1
81
81
  with:
82
82
  target: ${{ matrix.platform.target }}
83
- args: --release --out dist --find-interpreter
83
+ args: --release --out dist -i python3.12 -i python3.13 -i python3.14 -i python3.14t -i python3.15 -i python3.15t
84
84
  sccache: 'true'
85
85
  manylinux: auto
86
86
  - name: Upload wheels
@@ -156,7 +156,9 @@ jobs:
156
156
  uses: PyO3/maturin-action@v1
157
157
  with:
158
158
  target: ${{ matrix.platform.target }}
159
- args: --release --out dist --find-interpreter
159
+ # Every wheel job names its interpreters (the set 0.2.8 shipped per platform):
160
+ # --find-interpreter picks up whatever the image has, e.g. PyPy, which fails.
161
+ args: --release --out dist -i python3.12 -i python3.13 -i python3.14 -i python3.14t
160
162
  sccache: 'true'
161
163
  manylinux: auto
162
164
  - name: Upload wheels
@@ -214,7 +216,7 @@ jobs:
214
216
  uses: PyO3/maturin-action@v1
215
217
  with:
216
218
  target: ${{ matrix.platform.target }}
217
- args: --release --out dist --find-interpreter
219
+ args: --release --out dist -i python3.12 -i python3.13 -i python3.14
218
220
  sccache: 'true'
219
221
  - name: Upload wheels
220
222
  uses: actions/upload-artifact@v4
@@ -254,7 +256,7 @@ jobs:
254
256
  uses: PyO3/maturin-action@v1
255
257
  with:
256
258
  target: ${{ matrix.platform.target }}
257
- args: --release --out dist --find-interpreter
259
+ args: --release --out dist -i python3.12 -i python3.13 -i python3.14 -i python3.15
258
260
  sccache: 'true'
259
261
  - name: Upload wheels
260
262
  uses: actions/upload-artifact@v4
@@ -303,7 +305,7 @@ jobs:
303
305
  contents: write
304
306
  attestations: write
305
307
  steps:
306
- # Do NOT use `merge-multiple: true`. With `--find-interpreter`, every
308
+ # Do NOT use `merge-multiple: true`. With a shared interpreter list, every
307
309
  # build matrix leg (py3.12 AND py3.13) produces cp312/cp313/cp314 wheels
308
310
  # with identical filenames but non-identical bytes (sccache makes builds
309
311
  # nondeterministic). Merging them into one directory makes
@@ -4,6 +4,66 @@ 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.10] - 2026-11-10
8
+
9
+ ### Changed
10
+
11
+ - `nc_to_batches` and `nc_to_dataframe` do less work per output row. The
12
+ G-group columns are held as per-batch dictionary codes and written straight
13
+ to the Arrow string array, instead of one `String` per row per column that
14
+ the consumer thread then freed; the batch builder finds a cell's column
15
+ without hashing its name; the streaming driver reuses its decode buffers.
16
+ Python output is unchanged (same columns, `String` dtype, values and
17
+ `variable_events`). With `flatten_tolerance`, line numbers and variables on,
18
+ a 7.2M-row, 442 MiB linear program takes 3.3 s instead of 8.3 s and a
19
+ 22M-row, 1.1 GB program 10.5 s instead of 24 s, at the same peak memory
20
+ (#89).
21
+
22
+ - `flatten_tolerance` costs almost nothing on rows that are not curves: the
23
+ flattener tracked positions with an allocation and a lookup per configured
24
+ axis for every row, about 2 s on the 7.2M-row program above, now about
25
+ 0.15 s. B-spline flattening reuses one working buffer per curve; a
26
+ 5.4M-row, 380 MB spline program takes 8 s instead of 16 s (#89).
27
+
28
+ - Rust API: `Column` has a new `Dict(DictColumn)` variant, used for the
29
+ G-group columns (`values` plus one `u32` code per row,
30
+ `DictColumn::NULL` for a null cell). Code that matches `Column`
31
+ exhaustively needs one more arm; `DictColumn::iter` yields the cells as
32
+ `Option<&str>` (#89).
33
+
34
+ ### Fixed
35
+
36
+ - A numeric tool selection (`T2`, `T=3`, `T=R1`) now reaches the output table.
37
+ It is written to the same string `T` column as `T="name"`, as the number's
38
+ text (`"2"`), on the row of the block that programs it. Before, only named
39
+ tools appeared and a numeric one was dropped without a warning. The value is
40
+ still stored as the variable `T` in the final state, as before (#101).
41
+
42
+ ## [v0.2.9] - 2026-11-01
43
+
44
+ ### Changed
45
+
46
+ - `nc_to_batches` and `nc_to_rows` borrow the Python str's UTF-8 buffer for
47
+ the run instead of copying it twice (once into a Rust `String`, once more
48
+ for error messages). On a 5M-line, 322 MB program the peak footprint of
49
+ `nc_to_batches` drops from 1.28 to 0.72 GiB. The Rust entry points
50
+ (`nc_to_table`, `nc_to_row_stream`, `nc_to_batch_stream*`) take
51
+ `impl Into<SourceText>`: a `&str` still works and is copied once, an owned
52
+ `String` or `Arc<str>` is shared without a copy (#89).
53
+
54
+ - Every jump-free program now runs on the streaming line driver, not only
55
+ those of 64 MiB or more, and the streaming driver decodes ahead on a second
56
+ thread instead of decoding inline. On a 1M-line, 63 MB program
57
+ `nc_to_batches` peaks at 0.21 GiB instead of 0.60 GiB and takes 1.02 s
58
+ instead of 1.24 s; at 5M lines it takes 5.1 s instead of 6.3 s. Programs
59
+ with jumps keep the indexed driver; `NC_STREAM_MIN_BYTES` still sets a size
60
+ threshold (#89).
61
+
62
+ - Less work per output row on the interpreter thread: the batch builder moves
63
+ string cells out of the rows it recycles instead of cloning them, and the
64
+ per-row column set and the flattener's position maps hash with FxHash. At
65
+ 5M lines `nc_to_batches` takes 4.4 s instead of 5.2 s (#89).
66
+
7
67
  ## [v0.2.8] - 2026-10-15
8
68
 
9
69
  ### Added
@@ -506,7 +506,7 @@ checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4"
506
506
 
507
507
  [[package]]
508
508
  name = "nc-gcode-interpreter"
509
- version = "0.2.8"
509
+ version = "0.2.10"
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.8"
3
+ version = "0.2.10"
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.8
3
+ Version: 0.2.10
4
4
  Classifier: Programming Language :: Rust
5
5
  Classifier: Programming Language :: Python :: Implementation :: CPython
6
6
  Classifier: Programming Language :: Python :: 3.12
@@ -161,23 +161,24 @@ for line_no, row, variables in nc_to_rows(program, include_variables=True):
161
161
 
162
162
  Rows are typed and forward-filled like the batch DataFrame (disable with `forward_fill=False`); errors raise from `next()` at the offending row; after exhaustion the iterator's `state` attribute holds the final interpreter state. See `python/example/streaming.py` for a runnable version.
163
163
 
164
- **Memory.** Programs of 64 MiB or more without multi-line control blocks or
165
- jumps are validated in a parallel preflight scan and then executed one line at a
166
- time, so parse storage is bounded by the longest line instead of the program
167
- length. The input text, its diagnostic copy and line-offset index, variable
168
- state and any output the caller retains still grow with the program, so use
169
- `nc_to_batches` with a bounded consumer for large files. Smaller programs and
170
- programs with jumps keep the indexed fast path; `NC_STREAM_MIN_BYTES` sets the
171
- threshold in bytes (0 streams every jump-free program). Control blocks and
172
- `NC_STAGE1=0` still use the whole-file parser. On a 1.28M-line (112 MB) field
173
- program the peak RSS of `nc_to_batches` drops from about 1.3 GiB to 0.4 GiB,
174
- and the second decode during execution costs up to about 20 percent of
175
- interpretation time. Measure a local program with
164
+ **Memory.** Programs without multi-line control blocks or jumps are validated
165
+ in a preflight scan and then executed one line at a time, with the next lines
166
+ decoded on a second thread, so parse storage is bounded by a chunk of lines
167
+ instead of the program length. The input text (held once: the Python str is
168
+ borrowed, not copied, and error messages read from the same buffer), its
169
+ line-offset index, variable state and any output the caller retains still grow
170
+ with the program, so use `nc_to_batches` with a bounded consumer for large
171
+ files. Programs with jumps keep the indexed fast path; `NC_STREAM_MIN_BYTES`
172
+ sets a size below which jump-free programs take it too (default 0). Control
173
+ blocks and `NC_STAGE1=0` still use the whole-file parser. On a 1M-line (63 MB)
174
+ generated program the peak footprint of `nc_to_batches` is about 0.2 GiB, and
175
+ streaming is 5 to 20 percent faster than the indexed driver from 1 MB up.
176
+ Measure a local program with
176
177
  `python scripts/benchmark.py --input <file> --modes fresh --edits none
177
178
  --threads 4 --samples 1 --batch-size 4096 --allow-undefined --axis-index-e 4
178
179
  --axis-index-elx 5 --output tmp/memory.json`.
179
180
 
180
- **Cancellation.** Dropping the iterator is the supported way to cancel a `nc_to_rows`/`nc_to_batches` run — leaving a `for` loop early or `del`-ing it drops it immediately; relying on garbage collection works too but only whenever the runtime happens to collect it, so it is not prompt on all interpreters: the background interpreter thread's next attempt to send a row hits a closed channel and returns internally via `ParsingError::StreamClosed`, so the thread unwinds promptly instead of running the rest of the program to completion. No explicit `.close()`/`.cancel()` call is needed or provided — drop is the whole contract, and downstreams (e.g. ribweaver's `/sim` live-edit reinterpret) rely on it for latest-wins cancellation.
181
+ **Cancellation.** Dropping the iterator is the supported way to cancel a `nc_to_rows`/`nc_to_batches` run — leaving a `for` loop early or `del`-ing it drops it immediately; relying on garbage collection works too but only whenever the runtime happens to collect it, so it is not prompt on all interpreters: the background interpreter thread's next attempt to send a row hits a closed channel and returns internally via `ParsingError::StreamClosed`, so the thread unwinds promptly instead of running the rest of the program to completion. No explicit `.close()`/`.cancel()` call is needed or provided — drop is the whole contract, and downstreams (for example a live-edit re-interpret in an editor) rely on it for latest-wins cancellation.
181
182
 
182
183
  Additionally, conversion from a Polars DataFrame back to an MPF (NC) program is also supported:
183
184
 
@@ -15,7 +15,7 @@ Expose a resumable interpretation handle:
15
15
  - `resume(token, patched_text)` — restart interpretation from a checkpoint,
16
16
  against an edited program.
17
17
 
18
- Consumer: ribweaver `/sim` phase-E *incremental re-interpret* for multi-million-row
18
+ Consumer: a live-edit *incremental re-interpret* in a G-code editor, for multi-million-row
19
19
  loop expansions. When the user edits source line *L*, it resumes from the last
20
20
  checkpoint whose block cursor precedes *L* instead of re-running the whole program.
21
21
  Downstream is evidence-gated: debounced-full re-interpret is fine below ~10⁵ rows,
@@ -208,3 +208,20 @@ def test_include_line_numbers_survives_flattening():
208
208
  assert per_line.count(2) > 5
209
209
  stream_lines = [line for line, *_ in nc_to_rows(program, flatten_tolerance=0.1)]
210
210
  assert per_line == stream_lines
211
+
212
+
213
+ def test_g_group_columns_stay_plain_strings():
214
+ # The interpreter holds G-group columns as dictionary codes internally;
215
+ # Python must keep seeing String columns with the same values.
216
+ program = "G1 G64 X1\nX2\nG4 F0.5\nG0 X3\nG17 X4\nM30\n"
217
+ frames = list(nc_to_batches(program, batch_size=2))
218
+ assert [f.height for f in frames] == [2, 2, 2]
219
+ for frame in frames:
220
+ for name in frame.columns:
221
+ if name.startswith("gg"):
222
+ assert frame[name].dtype == pl.String, name
223
+ df = pl.concat(frames, how="diagonal")
224
+ assert df["gg01_motion"].to_list() == ["G1", "G1", "G1", "G0", "G0", "G0"]
225
+ assert df["gg10_exact_stop_mode"].to_list() == ["G64"] * 6
226
+ assert df["gg02_wait"].to_list() == [None, None, "G4", None, None, None]
227
+ assert df["gg06_plane_select"].to_list() == [None, None, None, None, "G17", "G17"]
@@ -159,3 +159,20 @@ def test_fast_path_matches_on_real_corpus(mpf):
159
159
  extra_axes=["ELX"],
160
160
  allow_undefined_variables=True,
161
161
  )
162
+
163
+
164
+ @pytest.mark.parametrize(
165
+ "program, expected",
166
+ [
167
+ ("G1 X0\nT2\nX1", [None, "2", None]),
168
+ ("G1 X0\nT=3\nX1", [None, "3", None]),
169
+ ("G1 X0\nt4\nX1", [None, "4", None]),
170
+ ("R1=7\nG1 X0\nT=R1\nX1", [None, "7", None]),
171
+ ('G1 X0\nT="NOZ_A"\nT5 X1', [None, "NOZ_A", "5"]),
172
+ ],
173
+ ids=["T2", "T=3", "lowercase", "expression", "mixed with a named tool"],
174
+ )
175
+ def test_numeric_tool_selection_reaches_the_t_column(program, expected):
176
+ """A numeric tool selection lands in the same string T column as a named one (#101)."""
177
+ df = run_both(program)
178
+ assert df["T"].to_list() == expected
@@ -67,7 +67,7 @@
67
67
  //! than being misread).
68
68
 
69
69
  use crate::errors::ParsingError;
70
- use crate::output::{intern_column, CellMap, Row, FLATTENED_COLUMN};
70
+ use crate::output::{intern_column, key_eq, CellMap, Row, FLATTENED_COLUMN};
71
71
  use crate::state::emit_warning;
72
72
  use crate::types::Value;
73
73
  use std::collections::HashMap;
@@ -135,8 +135,9 @@ pub struct Flattener {
135
135
  /// Geometric axis columns (configured axis identifiers minus the
136
136
  /// non-geometric value columns), interned.
137
137
  geometric_axes: Vec<&'static str>,
138
- /// Last known machine coordinate per geometric axis column.
139
- positions: HashMap<&'static str, f64>,
138
+ /// Last known machine coordinate per geometric axis, indexed like
139
+ /// `geometric_axes` (a row updates a few slots, with no hashing).
140
+ positions: Vec<Option<f64>>,
140
141
  motion: Motion,
141
142
  plane: Plane,
142
143
  /// Buffered blocks of the active spline, in program order. Non-empty
@@ -144,7 +145,7 @@ pub struct Flattener {
144
145
  spline_buffer: Vec<SplineItem>,
145
146
  /// Snapshot of `positions` taken when the spline was selected: the
146
147
  /// spline's start point.
147
- spline_start: HashMap<&'static str, f64>,
148
+ spline_start: Vec<Option<f64>>,
148
149
  /// Current B-spline degree (`SD=`, modal within the spline).
149
150
  spline_degree: usize,
150
151
  /// Curve motion words already warned about (CIP, CT, POLY, ...): one
@@ -159,8 +160,8 @@ impl Flattener {
159
160
  /// point even though the interpreter does.
160
161
  pub fn seed_position(&mut self, axis: &str, machine_value: f64) {
161
162
  let key = intern_column(&axis.to_uppercase());
162
- if self.geometric_axes.contains(&key) {
163
- self.positions.insert(key, machine_value);
163
+ if let Some(slot) = self.axis_slot(key) {
164
+ self.positions[slot] = Some(machine_value);
164
165
  }
165
166
  }
166
167
 
@@ -170,7 +171,7 @@ impl Flattener {
170
171
  message: format!("flatten tolerance must be a positive number, got {}", tolerance),
171
172
  });
172
173
  }
173
- let geometric_axes = axis_identifiers
174
+ let geometric_axes: Vec<&'static str> = axis_identifiers
174
175
  .iter()
175
176
  .map(|a| a.to_uppercase())
176
177
  .filter(|a| !NON_GEOMETRIC_AXES.contains(&a.as_str()))
@@ -178,12 +179,12 @@ impl Flattener {
178
179
  .collect();
179
180
  Ok(Flattener {
180
181
  tolerance,
182
+ positions: vec![None; geometric_axes.len()],
181
183
  geometric_axes,
182
- positions: HashMap::new(),
183
184
  motion: Motion::Linear,
184
185
  plane: G17,
185
186
  spline_buffer: Vec::new(),
186
- spline_start: HashMap::new(),
187
+ spline_start: Vec::new(),
187
188
  spline_degree: 3,
188
189
  warned_motions: Vec::new(),
189
190
  })
@@ -201,7 +202,7 @@ impl Flattener {
201
202
  }
202
203
  self.motion = new_motion;
203
204
  }
204
- if let Some(Value::Str(plane)) = row.cells.get(intern_column("gg06_plane_select")) {
205
+ if let Some(Value::Str(plane)) = row.cells.get("gg06_plane_select") {
205
206
  self.plane = match plane.as_str() {
206
207
  "G18" => G18,
207
208
  "G19" => G19,
@@ -215,7 +216,7 @@ impl Flattener {
215
216
  self.spline_start = self.positions.clone();
216
217
  self.spline_degree = 3;
217
218
  }
218
- if let Some(Value::Float(sd)) = row.cells.get(intern_column("SD")) {
219
+ if let Some(Value::Float(sd)) = row.cells.get("SD") {
219
220
  self.spline_degree = (*sd as usize).max(1);
220
221
  }
221
222
  let has_point = self
@@ -266,10 +267,23 @@ impl Flattener {
266
267
  !self.spline_buffer.is_empty()
267
268
  }
268
269
 
270
+ /// Index of `name` in `geometric_axes`.
271
+ fn axis_slot(&self, name: &str) -> Option<usize> {
272
+ self.geometric_axes.iter().position(|&axis| key_eq(axis, name))
273
+ }
274
+
275
+ /// Last known machine coordinate of a geometric axis.
276
+ fn position(&self, axis: &str) -> Option<f64> {
277
+ self.axis_slot(axis).and_then(|slot| self.positions[slot])
278
+ }
279
+
269
280
  fn track_positions(&mut self, row: &Row) {
270
- for &axis in &self.geometric_axes {
271
- if let Some(Value::Float(v)) = row.cells.get(axis) {
272
- self.positions.insert(axis, *v);
281
+ // Walk the row's few cells, not every configured axis.
282
+ for (&key, value) in row.cells.iter() {
283
+ if let Value::Float(v) = value {
284
+ if let Some(slot) = self.axis_slot(key) {
285
+ self.positions[slot] = Some(*v);
286
+ }
273
287
  }
274
288
  }
275
289
  }
@@ -309,7 +323,7 @@ impl Flattener {
309
323
  return;
310
324
  }
311
325
 
312
- let (Some(su), Some(sv)) = (self.positions.get(u_axis).copied(), self.positions.get(v_axis).copied()) else {
326
+ let (Some(su), Some(sv)) = (self.position(u_axis), self.position(v_axis)) else {
313
327
  self.pass_through_with_warning(row, "arc start position is unknown", out);
314
328
  return;
315
329
  };
@@ -411,8 +425,8 @@ impl Flattener {
411
425
  continue;
412
426
  }
413
427
  if let Some(end) = cell_float(&row, axis) {
414
- let start = match self.positions.get(axis) {
415
- Some(&start) => start,
428
+ let start = match self.position(axis) {
429
+ Some(start) => start,
416
430
  None => {
417
431
  // First-ever position of this axis: there is nothing
418
432
  // to ramp from, so the channel holds its end value
@@ -501,17 +515,19 @@ impl Flattener {
501
515
  let mut current: Vec<f64> = channels
502
516
  .iter()
503
517
  .map(|&c| {
504
- start.get(c).copied().unwrap_or_else(|| {
505
- // Channel never positioned before the spline: start it at
506
- // its first programmed value (a zero-length lead-in).
507
- items
508
- .iter()
509
- .find_map(|item| match item {
510
- SplineItem::Point(row) => cell_float(row, c),
511
- _ => None,
512
- })
513
- .unwrap_or(0.0)
514
- })
518
+ self.axis_slot(c)
519
+ .and_then(|slot| start.get(slot).copied().flatten())
520
+ .unwrap_or_else(|| {
521
+ // Channel never positioned before the spline: start it at
522
+ // its first programmed value (a zero-length lead-in).
523
+ items
524
+ .iter()
525
+ .find_map(|item| match item {
526
+ SplineItem::Point(row) => cell_float(row, c),
527
+ _ => None,
528
+ })
529
+ .unwrap_or(0.0)
530
+ })
515
531
  })
516
532
  .collect();
517
533
  points.push(current.clone());
@@ -659,7 +675,7 @@ fn points_value(points: &[Vec<f64>], point_rows: &[Option<usize>], item_index: u
659
675
  }
660
676
 
661
677
  fn row_motion(row: &Row) -> Option<Motion> {
662
- match row.cells.get(intern_column("gg01_motion")) {
678
+ match row.cells.get("gg01_motion") {
663
679
  Some(Value::Str(m)) => Some(match m.as_str() {
664
680
  "G0" | "G1" => Motion::Linear,
665
681
  "G2" => Motion::Arc { cw: true },
@@ -956,6 +972,9 @@ struct BSpline {
956
972
  knots: Vec<f64>,
957
973
  ctrl: Vec<Vec<f64>>,
958
974
  channels: usize,
975
+ /// de Boor working rows, `(p + 1) * (channels + 1)` values, reused by
976
+ /// every `eval` (the flattener evaluates several times per control point).
977
+ scratch: std::cell::RefCell<Vec<f64>>,
959
978
  }
960
979
 
961
980
  impl BSpline {
@@ -984,6 +1003,7 @@ impl BSpline {
984
1003
  knots,
985
1004
  ctrl,
986
1005
  channels: points[0].len(),
1006
+ scratch: std::cell::RefCell::new(Vec::new()),
987
1007
  }
988
1008
  }
989
1009
 
@@ -998,7 +1018,13 @@ impl BSpline {
998
1018
  } else {
999
1019
  p + t as usize
1000
1020
  };
1001
- let mut d: Vec<Vec<f64>> = (0..=p).map(|j| self.ctrl[j + k - p].clone()).collect();
1021
+ // Row j of `d` is control point j + k - p.
1022
+ let width = channels + 1;
1023
+ let mut d = self.scratch.borrow_mut();
1024
+ d.clear();
1025
+ for j in 0..=p {
1026
+ d.extend_from_slice(&self.ctrl[j + k - p]);
1027
+ }
1002
1028
  for r in 1..=p {
1003
1029
  for j in (r..=p).rev() {
1004
1030
  let i = j + k - p;
@@ -1008,17 +1034,14 @@ impl BSpline {
1008
1034
  } else {
1009
1035
  (t - knots[i]) / denom
1010
1036
  };
1011
- // Indexing two rows of `d` at once: a plain indexed loop is
1012
- // clearer than a split_at_mut dance.
1013
- #[allow(clippy::needless_range_loop)]
1014
1037
  for c in 0..=channels {
1015
- let previous = d[j - 1][c];
1016
- d[j][c] = (1.0 - alpha) * previous + alpha * d[j][c];
1038
+ let previous = d[(j - 1) * width + c];
1039
+ d[j * width + c] = (1.0 - alpha) * previous + alpha * d[j * width + c];
1017
1040
  }
1018
1041
  }
1019
1042
  }
1020
- let w = d[p][channels];
1021
- d[p][..channels].iter().map(|v| v / w).collect()
1043
+ let w = d[p * width + channels];
1044
+ d[p * width..p * width + channels].iter().map(|v| v / w).collect()
1022
1045
  }
1023
1046
 
1024
1047
  /// Greville abscissa of control point `i`, the mean of the `p` knots
@@ -2514,6 +2514,9 @@ fn interpret_statement(element: Pair<Rule>, output: &mut Output, state: &mut Sta
2514
2514
  }
2515
2515
  None => {
2516
2516
  state.warn_unsupported_address(&key, line_no);
2517
+ if key.eq_ignore_ascii_case("T") {
2518
+ last.insert("T", Value::Str(crate::output::tool_number_text(local_value)));
2519
+ }
2517
2520
  output.record_variable_change(&key, local_value);
2518
2521
  }
2519
2522
  }
@@ -6,6 +6,54 @@ use crate::state::{self, State};
6
6
  use crate::types::{NCParser, Rule};
7
7
  use pest::Parser;
8
8
  use std::collections::HashMap;
9
+ use std::sync::Arc;
10
+
11
+ /// Program text shared between the driver that interprets it and the state's
12
+ /// diagnostics, so a run holds a single copy of the source. Converting from a
13
+ /// `&str` copies once; an owned `String`, an `Arc<str>` or any other
14
+ /// [`SourceText::new`] buffer is shared as is.
15
+ #[derive(Clone)]
16
+ pub struct SourceText(Arc<dyn AsRef<str> + Send + Sync>);
17
+
18
+ impl SourceText {
19
+ pub fn new<T: AsRef<str> + Send + Sync + 'static>(text: T) -> Self {
20
+ SourceText(Arc::new(text))
21
+ }
22
+
23
+ pub fn as_str(&self) -> &str {
24
+ (*self.0).as_ref()
25
+ }
26
+ }
27
+
28
+ impl std::fmt::Debug for SourceText {
29
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
30
+ f.debug_struct("SourceText").field("len", &self.as_str().len()).finish()
31
+ }
32
+ }
33
+
34
+ impl From<&str> for SourceText {
35
+ fn from(text: &str) -> Self {
36
+ SourceText(Arc::new(Box::<str>::from(text)))
37
+ }
38
+ }
39
+
40
+ impl From<&String> for SourceText {
41
+ fn from(text: &String) -> Self {
42
+ SourceText::from(text.as_str())
43
+ }
44
+ }
45
+
46
+ impl From<String> for SourceText {
47
+ fn from(text: String) -> Self {
48
+ SourceText::new(text)
49
+ }
50
+ }
51
+
52
+ impl From<Arc<str>> for SourceText {
53
+ fn from(text: Arc<str>) -> Self {
54
+ SourceText::new(text)
55
+ }
56
+ }
9
57
 
10
58
  const DEFAULT_AXIS_IDENTIFIERS: &[&str] = &[
11
59
  "N", "X", "Y", "Z", "A", "B", "C", "D", "E", "F", "S", "U", "V", "RA1", "RA2", "RA3", "RA4", "RA5", "RA6",
@@ -13,7 +61,7 @@ const DEFAULT_AXIS_IDENTIFIERS: &[&str] = &[
13
61
 
14
62
  /// Main function: interpret the input program into a sanitized output table.
15
63
  pub fn nc_to_table(
16
- input: &str,
64
+ input: impl Into<SourceText>,
17
65
  initial_state: Option<&str>,
18
66
  axis_identifiers: Option<Vec<String>>,
19
67
  extra_axes: Option<Vec<String>>,
@@ -41,7 +89,7 @@ pub fn nc_to_table(
41
89
  // Now interpret the main input using the axis_index_map from state
42
90
  let mut output = OutputRows::collect();
43
91
  install_flattener(&mut output, &state, flatten_tolerance)?;
44
- interpret_file(input, &mut state, &mut output)?;
92
+ interpret_source(input.into(), &mut state, &mut output)?;
45
93
  let rows = output.finish()?;
46
94
 
47
95
  let table = Table::from_rows(&rows, disable_forward_fill);
@@ -104,7 +152,7 @@ fn install_flattener(
104
152
  #[allow(clippy::too_many_arguments)]
105
153
  #[allow(dead_code)] // used by the python-feature bindings, not the bin
106
154
  pub fn nc_to_row_stream(
107
- input: &str,
155
+ input: impl Into<SourceText>,
108
156
  initial_state: Option<&str>,
109
157
  axis_identifiers: Option<Vec<String>>,
110
158
  extra_axes: Option<Vec<String>>,
@@ -127,7 +175,7 @@ pub fn nc_to_row_stream(
127
175
  }
128
176
  let mut output = OutputRows::stream(sender);
129
177
  install_flattener(&mut output, &state, flatten_tolerance)?;
130
- interpret_file(input, &mut state, &mut output)?;
178
+ interpret_source(input.into(), &mut state, &mut output)?;
131
179
  output.finish()?;
132
180
  Ok(state)
133
181
  }
@@ -145,7 +193,7 @@ pub fn nc_to_row_stream(
145
193
  #[allow(dead_code)] // used by the python-feature bindings, not the bin
146
194
  #[allow(clippy::too_many_arguments)]
147
195
  pub fn nc_to_batch_stream(
148
- input: &str,
196
+ input: impl Into<SourceText>,
149
197
  initial_state: Option<&str>,
150
198
  axis_identifiers: Option<Vec<String>>,
151
199
  extra_axes: Option<Vec<String>>,
@@ -183,7 +231,7 @@ pub fn nc_to_batch_stream(
183
231
  /// callers.
184
232
  #[allow(clippy::too_many_arguments)]
185
233
  pub fn nc_to_batch_stream_with_line_numbers(
186
- input: &str,
234
+ input: impl Into<SourceText>,
187
235
  initial_state: Option<&str>,
188
236
  axis_identifiers: Option<Vec<String>>,
189
237
  extra_axes: Option<Vec<String>>,
@@ -218,15 +266,20 @@ pub fn nc_to_batch_stream_with_line_numbers(
218
266
  emit_line_no,
219
267
  );
220
268
  install_flattener(&mut output, &state, flatten_tolerance)?;
221
- interpret_file(input, &mut state, &mut output)?;
269
+ interpret_source(input.into(), &mut state, &mut output)?;
222
270
  output.finish()?;
223
271
  Ok(state)
224
272
  }
225
273
 
226
274
  /// Interpret a file, pushing rows into `output`.
227
275
  fn interpret_file(input: &str, state: &mut State, output: &mut OutputRows) -> Result<(), ParsingError> {
228
- // Store input for error messages
229
- state.set_input(input);
276
+ interpret_source(SourceText::from(input), state, output)
277
+ }
278
+
279
+ /// As [`interpret_file`], sharing `source` with the state's diagnostics.
280
+ fn interpret_source(source: SourceText, state: &mut State, output: &mut OutputRows) -> Result<(), ParsingError> {
281
+ state.set_source(source.clone());
282
+ let input = source.as_str();
230
283
 
231
284
  // Validate control-structure nesting first: a PEG reports an unclosed
232
285
  // IF/WHILE at the end of the file; the line scan reports the opener.
@@ -698,6 +751,32 @@ mod tests {
698
751
  .unwrap_or_else(|| panic!("column {name} missing; have {:?}", column_names(table)))
699
752
  }
700
753
 
754
+ /// An owned program is shared with the state's diagnostics, not copied.
755
+ #[test]
756
+ fn owned_source_is_shared_with_diagnostics() {
757
+ let program = String::from("G1 X0 Y0 F100\nX10 Y=\n");
758
+ let ptr = program.as_ptr();
759
+ let source = SourceText::from(program);
760
+ assert_eq!(source.as_str().as_ptr(), ptr);
761
+ let mut state = build_state(None, None, 10000, None, false);
762
+ state.set_source(source.clone());
763
+ assert_eq!(state.get_line(2), Some("X10 Y="));
764
+ assert_eq!(state.get_line(2).unwrap().as_ptr(), source.as_str()[14..].as_ptr());
765
+ let err = nc_to_table(
766
+ String::from("G1 X0\nX10 Y=\n"),
767
+ None,
768
+ None,
769
+ None,
770
+ 10000,
771
+ false,
772
+ None,
773
+ false,
774
+ None,
775
+ )
776
+ .expect_err("a dangling assignment does not parse");
777
+ assert!(err.to_string().contains("X10 Y="), "{err}");
778
+ }
779
+
701
780
  /// `include_line_numbers` (default off) prepends a `line_no` column: one
702
781
  /// 1-based source line per output row, repeated under a loop and
703
782
  /// non-monotonic across a jump - matching what `nc_to_rows` yields.
@@ -1399,6 +1478,7 @@ mod tests {
1399
1478
  let strs = |name: &str| -> Vec<Option<String>> {
1400
1479
  match &table.columns.iter().find(|(n, _)| n == name).unwrap().1 {
1401
1480
  Column::Str(v) => v.clone(),
1481
+ Column::Dict(v) => v.iter().map(|cell| cell.map(str::to_string)).collect(),
1402
1482
  other => panic!("column {name} is not a str column: {other:?}"),
1403
1483
  }
1404
1484
  };