nc-gcode-interpreter 0.2.9__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.9 → nc_gcode_interpreter-0.2.10}/.github/workflows/build-and-release.yml +6 -6
  2. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/CHANGELOG.md +35 -0
  3. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/Cargo.lock +1 -1
  4. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/Cargo.toml +1 -1
  5. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/PKG-INFO +1 -1
  6. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/README.md +1 -1
  7. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/docs/resumable-interpretation-design.md +1 -1
  8. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/python/tests/test_batches.py +17 -0
  9. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/python/tests/test_stage1.py +17 -0
  10. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/src/flatten.rs +57 -35
  11. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/src/interpret_rules.rs +3 -0
  12. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/src/interpreter.rs +1 -0
  13. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/src/lib.rs +13 -0
  14. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/src/line_driver.rs +35 -7
  15. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/src/output.rs +270 -43
  16. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/src/state.rs +15 -0
  17. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/uv.lock +6 -27
  18. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/.github/workflows/benchmark.yml +0 -0
  19. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/.gitignore +0 -0
  20. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/CONTRIBUTING.md +0 -0
  21. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/Development.md +0 -0
  22. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/LICENSE +0 -0
  23. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/TODO.md +0 -0
  24. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/benches/fixtures.rs +0 -0
  25. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/benches/workloads.rs +0 -0
  26. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/build.rs +0 -0
  27. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/build_backend/nc_build_backend.py +0 -0
  28. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/docs/sinumerik-execution-model.md +0 -0
  29. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/examples/actual_position.csv +0 -0
  30. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/examples/actual_position.mpf +0 -0
  31. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/examples/arc.csv +0 -0
  32. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/examples/arc.mpf +0 -0
  33. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/examples/arrays.csv +0 -0
  34. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/examples/arrays.mpf +0 -0
  35. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/examples/axis_index_assignment.csv +0 -0
  36. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/examples/axis_index_assignment.mpf +0 -0
  37. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/examples/basic_math.csv +0 -0
  38. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/examples/basic_math.mpf +0 -0
  39. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/examples/case.csv +0 -0
  40. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/examples/case.mpf +0 -0
  41. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/examples/case_insensitive_variables.csv +0 -0
  42. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/examples/case_insensitive_variables.mpf +0 -0
  43. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/examples/custom_vars.csv +0 -0
  44. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/examples/custom_vars.mpf +0 -0
  45. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/examples/def_string.csv +0 -0
  46. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/examples/def_string.mpf +0 -0
  47. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/examples/defaults.csv +0 -0
  48. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/examples/defaults.mpf +0 -0
  49. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/examples/edge_cases.csv +0 -0
  50. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/examples/edge_cases.mpf +0 -0
  51. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/examples/flattening/flatten_demo.mpf +0 -0
  52. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/examples/flattening/flatten_demo_flattened.csv +0 -0
  53. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/examples/flattening/flatten_demo_raw.csv +0 -0
  54. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/examples/for_loop.csv +0 -0
  55. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/examples/for_loop.mpf +0 -0
  56. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/examples/function_calls.csv +0 -0
  57. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/examples/function_calls.mpf +0 -0
  58. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/examples/goto.csv +0 -0
  59. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/examples/goto.mpf +0 -0
  60. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/examples/if_statement.csv +0 -0
  61. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/examples/if_statement.mpf +0 -0
  62. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/examples/increment.csv +0 -0
  63. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/examples/increment.mpf +0 -0
  64. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/examples/logic_operators.csv +0 -0
  65. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/examples/logic_operators.mpf +0 -0
  66. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/examples/loop.csv +0 -0
  67. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/examples/loop.mpf +0 -0
  68. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/examples/multiple_m_codes.csv +0 -0
  69. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/examples/multiple_m_codes.mpf +0 -0
  70. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/examples/precedence.csv +0 -0
  71. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/examples/precedence.mpf +0 -0
  72. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/examples/r_param.csv +0 -0
  73. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/examples/r_param.mpf +0 -0
  74. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/examples/repeat.csv +0 -0
  75. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/examples/repeat.mpf +0 -0
  76. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/examples/rot.csv +0 -0
  77. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/examples/rot.mpf +0 -0
  78. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/examples/simple.csv +0 -0
  79. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/examples/simple.mpf +0 -0
  80. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/examples/spline.csv +0 -0
  81. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/examples/spline.mpf +0 -0
  82. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/examples/tool.csv +0 -0
  83. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/examples/tool.mpf +0 -0
  84. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/examples/trans.csv +0 -0
  85. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/examples/trans.mpf +0 -0
  86. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/examples/trans2.csv +0 -0
  87. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/examples/trans2.mpf +0 -0
  88. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/examples/trans_ic.csv +0 -0
  89. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/examples/trans_ic.mpf +0 -0
  90. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/examples/trans_reset.csv +0 -0
  91. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/examples/trans_reset.mpf +0 -0
  92. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/examples/trig.csv +0 -0
  93. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/examples/trig.mpf +0 -0
  94. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/examples/variables.csv +0 -0
  95. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/examples/variables.mpf +0 -0
  96. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/ggroups/generate_g_commands.py +0 -0
  97. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/pyproject.toml +0 -0
  98. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/python/example/minimal.py +0 -0
  99. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/python/example/streaming.py +0 -0
  100. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/python/nc_gcode_interpreter/__init__.py +0 -0
  101. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/python/nc_gcode_interpreter/_internal.pyi +0 -0
  102. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/python/nc_gcode_interpreter/cli.py +0 -0
  103. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/python/nc_gcode_interpreter/ggroups.json +0 -0
  104. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/python/nc_gcode_interpreter/py.typed +0 -0
  105. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/python/nc_gcode_interpreter/viz.py +0 -0
  106. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/python/tests/nc_source.py +0 -0
  107. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/python/tests/test_arithmetic_functions.py +0 -0
  108. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/python/tests/test_batch_variables.py +0 -0
  109. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/python/tests/test_compare_benchmarks.py +0 -0
  110. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/python/tests/test_diagnostics.py +0 -0
  111. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/python/tests/test_expected_output.py +0 -0
  112. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/python/tests/test_field_defects.py +0 -0
  113. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/python/tests/test_flags.py +0 -0
  114. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/python/tests/test_flatten.py +0 -0
  115. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/python/tests/test_frames.py +0 -0
  116. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/python/tests/test_g_groups.py +0 -0
  117. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/python/tests/test_g_vocabulary.py +0 -0
  118. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/python/tests/test_jumps.py +0 -0
  119. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/python/tests/test_real_comparisons.py +0 -0
  120. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/python/tests/test_streaming.py +0 -0
  121. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/python/tests/test_string_operations.py +0 -0
  122. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/python/tests/test_structured_errors.py +0 -0
  123. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/python/tests/test_unsupported.py +0 -0
  124. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/rustfmt.toml +0 -0
  125. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/scripts/benchmark.py +0 -0
  126. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/scripts/compare_benchmarks.py +0 -0
  127. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/src/errors.rs +0 -0
  128. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/src/frame.rs +0 -0
  129. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/src/grammar.pest +0 -0
  130. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/src/line_driver/experiments.rs +0 -0
  131. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/src/main.rs +0 -0
  132. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/src/modal_groups.rs +0 -0
  133. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/src/structure_scan.rs +0 -0
  134. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/src/types.rs +0 -0
  135. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/src/vm.rs +0 -0
  136. {nc_gcode_interpreter-0.2.9 → nc_gcode_interpreter-0.2.10}/tests/cli.rs +0 -0
  137. {nc_gcode_interpreter-0.2.9 → 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,8 +156,8 @@ jobs:
156
156
  uses: PyO3/maturin-action@v1
157
157
  with:
158
158
  target: ${{ matrix.platform.target }}
159
- # Named interpreters: --find-interpreter also picks up the image's PyPy,
160
- # which pyo3-ffi 0.29.0 fails to compile for.
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
161
  args: --release --out dist -i python3.12 -i python3.13 -i python3.14 -i python3.14t
162
162
  sccache: 'true'
163
163
  manylinux: auto
@@ -216,7 +216,7 @@ jobs:
216
216
  uses: PyO3/maturin-action@v1
217
217
  with:
218
218
  target: ${{ matrix.platform.target }}
219
- args: --release --out dist --find-interpreter
219
+ args: --release --out dist -i python3.12 -i python3.13 -i python3.14
220
220
  sccache: 'true'
221
221
  - name: Upload wheels
222
222
  uses: actions/upload-artifact@v4
@@ -256,7 +256,7 @@ jobs:
256
256
  uses: PyO3/maturin-action@v1
257
257
  with:
258
258
  target: ${{ matrix.platform.target }}
259
- args: --release --out dist --find-interpreter
259
+ args: --release --out dist -i python3.12 -i python3.13 -i python3.14 -i python3.15
260
260
  sccache: 'true'
261
261
  - name: Upload wheels
262
262
  uses: actions/upload-artifact@v4
@@ -305,7 +305,7 @@ jobs:
305
305
  contents: write
306
306
  attestations: write
307
307
  steps:
308
- # Do NOT use `merge-multiple: true`. With `--find-interpreter`, every
308
+ # Do NOT use `merge-multiple: true`. With a shared interpreter list, every
309
309
  # build matrix leg (py3.12 AND py3.13) produces cp312/cp313/cp314 wheels
310
310
  # with identical filenames but non-identical bytes (sccache makes builds
311
311
  # nondeterministic). Merging them into one directory makes
@@ -4,6 +4,41 @@ 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
+
7
42
  ## [v0.2.9] - 2026-11-01
8
43
 
9
44
  ### Changed
@@ -506,7 +506,7 @@ checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4"
506
506
 
507
507
  [[package]]
508
508
  name = "nc-gcode-interpreter"
509
- version = "0.2.9"
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.9"
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.9
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
@@ -178,7 +178,7 @@ Measure a local program with
178
178
  --threads 4 --samples 1 --batch-size 4096 --allow-undefined --axis-index-e 4
179
179
  --axis-index-elx 5 --output tmp/memory.json`.
180
180
 
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 (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.
182
182
 
183
183
  Additionally, conversion from a Polars DataFrame back to an MPF (NC) program is also supported:
184
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,9 +67,8 @@
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
- use crate::state::FxMap;
73
72
  use crate::types::Value;
74
73
  use std::collections::HashMap;
75
74
 
@@ -136,8 +135,9 @@ pub struct Flattener {
136
135
  /// Geometric axis columns (configured axis identifiers minus the
137
136
  /// non-geometric value columns), interned.
138
137
  geometric_axes: Vec<&'static str>,
139
- /// Last known machine coordinate per geometric axis column.
140
- positions: FxMap<&'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>>,
141
141
  motion: Motion,
142
142
  plane: Plane,
143
143
  /// Buffered blocks of the active spline, in program order. Non-empty
@@ -145,7 +145,7 @@ pub struct Flattener {
145
145
  spline_buffer: Vec<SplineItem>,
146
146
  /// Snapshot of `positions` taken when the spline was selected: the
147
147
  /// spline's start point.
148
- spline_start: FxMap<&'static str, f64>,
148
+ spline_start: Vec<Option<f64>>,
149
149
  /// Current B-spline degree (`SD=`, modal within the spline).
150
150
  spline_degree: usize,
151
151
  /// Curve motion words already warned about (CIP, CT, POLY, ...): one
@@ -160,8 +160,8 @@ impl Flattener {
160
160
  /// point even though the interpreter does.
161
161
  pub fn seed_position(&mut self, axis: &str, machine_value: f64) {
162
162
  let key = intern_column(&axis.to_uppercase());
163
- if self.geometric_axes.contains(&key) {
164
- self.positions.insert(key, machine_value);
163
+ if let Some(slot) = self.axis_slot(key) {
164
+ self.positions[slot] = Some(machine_value);
165
165
  }
166
166
  }
167
167
 
@@ -171,7 +171,7 @@ impl Flattener {
171
171
  message: format!("flatten tolerance must be a positive number, got {}", tolerance),
172
172
  });
173
173
  }
174
- let geometric_axes = axis_identifiers
174
+ let geometric_axes: Vec<&'static str> = axis_identifiers
175
175
  .iter()
176
176
  .map(|a| a.to_uppercase())
177
177
  .filter(|a| !NON_GEOMETRIC_AXES.contains(&a.as_str()))
@@ -179,12 +179,12 @@ impl Flattener {
179
179
  .collect();
180
180
  Ok(Flattener {
181
181
  tolerance,
182
+ positions: vec![None; geometric_axes.len()],
182
183
  geometric_axes,
183
- positions: FxMap::default(),
184
184
  motion: Motion::Linear,
185
185
  plane: G17,
186
186
  spline_buffer: Vec::new(),
187
- spline_start: FxMap::default(),
187
+ spline_start: Vec::new(),
188
188
  spline_degree: 3,
189
189
  warned_motions: Vec::new(),
190
190
  })
@@ -267,10 +267,23 @@ impl Flattener {
267
267
  !self.spline_buffer.is_empty()
268
268
  }
269
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
+
270
280
  fn track_positions(&mut self, row: &Row) {
271
- for &axis in &self.geometric_axes {
272
- if let Some(Value::Float(v)) = row.cells.get(axis) {
273
- 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
+ }
274
287
  }
275
288
  }
276
289
  }
@@ -310,7 +323,7 @@ impl Flattener {
310
323
  return;
311
324
  }
312
325
 
313
- 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 {
314
327
  self.pass_through_with_warning(row, "arc start position is unknown", out);
315
328
  return;
316
329
  };
@@ -412,8 +425,8 @@ impl Flattener {
412
425
  continue;
413
426
  }
414
427
  if let Some(end) = cell_float(&row, axis) {
415
- let start = match self.positions.get(axis) {
416
- Some(&start) => start,
428
+ let start = match self.position(axis) {
429
+ Some(start) => start,
417
430
  None => {
418
431
  // First-ever position of this axis: there is nothing
419
432
  // to ramp from, so the channel holds its end value
@@ -502,17 +515,19 @@ impl Flattener {
502
515
  let mut current: Vec<f64> = channels
503
516
  .iter()
504
517
  .map(|&c| {
505
- start.get(c).copied().unwrap_or_else(|| {
506
- // Channel never positioned before the spline: start it at
507
- // its first programmed value (a zero-length lead-in).
508
- items
509
- .iter()
510
- .find_map(|item| match item {
511
- SplineItem::Point(row) => cell_float(row, c),
512
- _ => None,
513
- })
514
- .unwrap_or(0.0)
515
- })
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
+ })
516
531
  })
517
532
  .collect();
518
533
  points.push(current.clone());
@@ -957,6 +972,9 @@ struct BSpline {
957
972
  knots: Vec<f64>,
958
973
  ctrl: Vec<Vec<f64>>,
959
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>>,
960
978
  }
961
979
 
962
980
  impl BSpline {
@@ -985,6 +1003,7 @@ impl BSpline {
985
1003
  knots,
986
1004
  ctrl,
987
1005
  channels: points[0].len(),
1006
+ scratch: std::cell::RefCell::new(Vec::new()),
988
1007
  }
989
1008
  }
990
1009
 
@@ -999,7 +1018,13 @@ impl BSpline {
999
1018
  } else {
1000
1019
  p + t as usize
1001
1020
  };
1002
- 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
+ }
1003
1028
  for r in 1..=p {
1004
1029
  for j in (r..=p).rev() {
1005
1030
  let i = j + k - p;
@@ -1009,17 +1034,14 @@ impl BSpline {
1009
1034
  } else {
1010
1035
  (t - knots[i]) / denom
1011
1036
  };
1012
- // Indexing two rows of `d` at once: a plain indexed loop is
1013
- // clearer than a split_at_mut dance.
1014
- #[allow(clippy::needless_range_loop)]
1015
1037
  for c in 0..=channels {
1016
- let previous = d[j - 1][c];
1017
- 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];
1018
1040
  }
1019
1041
  }
1020
1042
  }
1021
- let w = d[p][channels];
1022
- 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()
1023
1045
  }
1024
1046
 
1025
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
  }
@@ -1478,6 +1478,7 @@ mod tests {
1478
1478
  let strs = |name: &str| -> Vec<Option<String>> {
1479
1479
  match &table.columns.iter().find(|(n, _)| n == name).unwrap().1 {
1480
1480
  Column::Str(v) => v.clone(),
1481
+ Column::Dict(v) => v.iter().map(|cell| cell.map(str::to_string)).collect(),
1481
1482
  other => panic!("column {name} is not a str column: {other:?}"),
1482
1483
  }
1483
1484
  };
@@ -162,6 +162,19 @@ mod python_bindings {
162
162
  // build of the offset + value buffers.
163
163
  Arc::new(v.into_iter().collect::<StringArray>()),
164
164
  ),
165
+ Column::Dict(column) => {
166
+ // Same Utf8 array as a `Column::Str`, written from the codes.
167
+ let lengths: Vec<usize> = column.values.iter().map(String::len).collect();
168
+ let bytes: usize = column.codes.iter().filter_map(|&code| lengths.get(code as usize)).sum();
169
+ let mut builder = StringBuilder::with_capacity(column.codes.len(), bytes);
170
+ for &code in &column.codes {
171
+ match column.values.get(code as usize) {
172
+ Some(value) => builder.append_value(value),
173
+ None => builder.append_null(),
174
+ }
175
+ }
176
+ (DataType::Utf8, Arc::new(builder.finish()))
177
+ }
165
178
  Column::StrList(v) => {
166
179
  let values_cap: usize = v.iter().filter_map(|c| c.as_ref()).map(|l| l.len()).sum();
167
180
  let mut builder = ListBuilder::with_capacity(StringBuilder::new(), v.len())
@@ -123,6 +123,14 @@ enum LineExec<'a, 'i> {
123
123
  Blank,
124
124
  }
125
125
 
126
+ /// A numeric tool selection (`T2`, `T=3`) goes to the row's `T` column, like `T="name"`.
127
+ fn record_tool_number(key: &str, value: f64, output: &mut Output) {
128
+ if key == "T" {
129
+ let last = output.last_mut().expect("row was just pushed");
130
+ last.insert("T", Value::Str(crate::output::tool_number_text(value)));
131
+ }
132
+ }
133
+
126
134
  /// One type per name (mirrors `interpret_assignment`): assigning a number to
127
135
  /// an existing STRING variable is a hard error, also on the fast path.
128
136
  fn reject_string_variable(key: &str, state: &State, line_no: usize) -> Result<(), ParsingError> {
@@ -406,7 +414,10 @@ fn interpret_streaming(input: &str, output: &mut Output, state: &mut State) -> R
406
414
  // for the decode the indexed driver runs in parallel.
407
415
  std::thread::scope(|scope| {
408
416
  let (sender, receiver) = std::sync::mpsc::sync_channel::<DecodedChunk>(2);
409
- scope.spawn(move || decode_chunks(input, sender));
417
+ // Executed chunks go back to the decode thread, which refills their
418
+ // buffers instead of allocating and freeing three per chunk.
419
+ let (spent_sender, spent) = std::sync::mpsc::channel::<DecodedChunk>();
420
+ scope.spawn(move || decode_chunks(input, sender, spent));
410
421
  // Returning early drops `receiver`, which stops the decode thread.
411
422
  for chunk in receiver {
412
423
  for (offset, (decoded, line)) in chunk.results.iter().zip(&chunk.lines).enumerate() {
@@ -415,6 +426,7 @@ fn interpret_streaming(input: &str, output: &mut Output, state: &mut State) -> R
415
426
  return Ok(flow);
416
427
  }
417
428
  }
429
+ let _ = spent_sender.send(chunk);
418
430
  }
419
431
  Ok(BlockFlow::Continue)
420
432
  })
@@ -432,16 +444,30 @@ struct DecodedChunk<'a> {
432
444
  arena: Vec<Word<'a>>,
433
445
  }
434
446
 
435
- fn decode_chunks<'a>(input: &'a str, sender: std::sync::mpsc::SyncSender<DecodedChunk<'a>>) {
447
+ fn decode_chunks<'a>(
448
+ input: &'a str,
449
+ sender: std::sync::mpsc::SyncSender<DecodedChunk<'a>>,
450
+ spent: std::sync::mpsc::Receiver<DecodedChunk<'a>>,
451
+ ) {
436
452
  let mut lines = input.lines().peekable();
437
453
  let mut first_line = 0;
438
454
  while lines.peek().is_some() {
439
- let mut chunk = DecodedChunk {
440
- first_line,
441
- lines: lines.by_ref().take(STREAM_CHUNK_LINES).collect(),
442
- results: Vec::with_capacity(STREAM_CHUNK_LINES),
443
- arena: Vec::with_capacity(STREAM_CHUNK_LINES * 4),
455
+ let mut chunk = match spent.try_recv() {
456
+ Ok(mut chunk) => {
457
+ chunk.lines.clear();
458
+ chunk.results.clear();
459
+ chunk.arena.clear();
460
+ chunk
461
+ }
462
+ Err(_) => DecodedChunk {
463
+ first_line: 0,
464
+ lines: Vec::with_capacity(STREAM_CHUNK_LINES),
465
+ results: Vec::with_capacity(STREAM_CHUNK_LINES),
466
+ arena: Vec::with_capacity(STREAM_CHUNK_LINES * 4),
467
+ },
444
468
  };
469
+ chunk.first_line = first_line;
470
+ chunk.lines.extend(lines.by_ref().take(STREAM_CHUNK_LINES));
445
471
  for (offset, line) in chunk.lines.iter().enumerate() {
446
472
  chunk
447
473
  .results
@@ -614,6 +640,7 @@ fn execute_decoded(
614
640
  let key = key.to_uppercase();
615
641
  reject_string_variable(&key, state, line.line_no)?;
616
642
  output.record_variable_change(&key, *value);
643
+ record_tool_number(&key, *value, output);
617
644
  state.symbol_table.insert(key, *value);
618
645
  }
619
646
  }
@@ -701,6 +728,7 @@ fn execute_decoded(
701
728
  reject_string_variable(&key, state, line.line_no)?;
702
729
  let local_value = increment_local(state, &key, value);
703
730
  output.record_variable_change(&key, local_value);
731
+ record_tool_number(&key, local_value, output);
704
732
  state.symbol_table.insert(key, local_value);
705
733
  }
706
734
  }