nc-gcode-interpreter 0.2.7__tar.gz → 0.2.9__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 (142) hide show
  1. nc_gcode_interpreter-0.2.9/.github/workflows/benchmark.yml +41 -0
  2. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/.github/workflows/build-and-release.yml +4 -2
  3. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/CHANGELOG.md +188 -0
  4. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/Cargo.lock +1 -1
  5. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/Cargo.toml +7 -1
  6. nc_gcode_interpreter-0.2.9/Development.md +146 -0
  7. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/PKG-INFO +1 -1
  8. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/README.md +19 -2
  9. nc_gcode_interpreter-0.2.9/benches/fixtures.rs +50 -0
  10. nc_gcode_interpreter-0.2.9/benches/workloads.rs +194 -0
  11. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/examples/flattening/flatten_demo_flattened.csv +0 -1
  12. nc_gcode_interpreter-0.2.9/examples/rot.csv +17 -0
  13. nc_gcode_interpreter-0.2.9/examples/rot.mpf +18 -0
  14. nc_gcode_interpreter-0.2.9/examples/trans2.csv +5 -0
  15. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/python/nc_gcode_interpreter/__init__.py +3 -2
  16. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/python/nc_gcode_interpreter/_internal.pyi +1 -1
  17. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/python/tests/test_batch_variables.py +1 -1
  18. nc_gcode_interpreter-0.2.9/python/tests/test_compare_benchmarks.py +156 -0
  19. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/python/tests/test_diagnostics.py +64 -0
  20. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/python/tests/test_flatten.py +36 -0
  21. nc_gcode_interpreter-0.2.9/python/tests/test_frames.py +419 -0
  22. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/python/tests/test_stage1.py +32 -15
  23. nc_gcode_interpreter-0.2.9/python/tests/test_string_operations.py +169 -0
  24. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/python/tests/test_structured_errors.py +19 -0
  25. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/python/tests/test_unsupported.py +3 -2
  26. nc_gcode_interpreter-0.2.9/scripts/benchmark.py +339 -0
  27. nc_gcode_interpreter-0.2.9/scripts/compare_benchmarks.py +134 -0
  28. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/src/errors.rs +6 -3
  29. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/src/flatten.rs +235 -73
  30. nc_gcode_interpreter-0.2.9/src/frame.rs +187 -0
  31. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/src/grammar.pest +46 -32
  32. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/src/interpret_rules.rs +194 -39
  33. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/src/interpreter.rs +333 -23
  34. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/src/lib.rs +36 -25
  35. nc_gcode_interpreter-0.2.9/src/line_driver/experiments.rs +243 -0
  36. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/src/line_driver.rs +739 -160
  37. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/src/main.rs +2 -1
  38. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/src/output.rs +109 -22
  39. nc_gcode_interpreter-0.2.9/src/state.rs +704 -0
  40. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/src/structure_scan.rs +204 -24
  41. nc_gcode_interpreter-0.2.9/src/types.rs +57 -0
  42. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/src/vm.rs +12 -4
  43. nc_gcode_interpreter-0.2.9/tests/performance_contract.rs +202 -0
  44. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/uv.lock +53 -32
  45. nc_gcode_interpreter-0.2.7/Development.md +0 -88
  46. nc_gcode_interpreter-0.2.7/examples/trans2.csv +0 -5
  47. nc_gcode_interpreter-0.2.7/python/tests/test_string_operations.py +0 -79
  48. nc_gcode_interpreter-0.2.7/src/state.rs +0 -351
  49. nc_gcode_interpreter-0.2.7/src/types.rs +0 -14
  50. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/.gitignore +0 -0
  51. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/CONTRIBUTING.md +0 -0
  52. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/LICENSE +0 -0
  53. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/TODO.md +0 -0
  54. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/build.rs +0 -0
  55. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/build_backend/nc_build_backend.py +0 -0
  56. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/docs/resumable-interpretation-design.md +0 -0
  57. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/docs/sinumerik-execution-model.md +0 -0
  58. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/examples/actual_position.csv +0 -0
  59. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/examples/actual_position.mpf +0 -0
  60. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/examples/arc.csv +0 -0
  61. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/examples/arc.mpf +0 -0
  62. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/examples/arrays.csv +0 -0
  63. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/examples/arrays.mpf +0 -0
  64. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/examples/axis_index_assignment.csv +0 -0
  65. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/examples/axis_index_assignment.mpf +0 -0
  66. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/examples/basic_math.csv +0 -0
  67. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/examples/basic_math.mpf +0 -0
  68. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/examples/case.csv +0 -0
  69. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/examples/case.mpf +0 -0
  70. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/examples/case_insensitive_variables.csv +0 -0
  71. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/examples/case_insensitive_variables.mpf +0 -0
  72. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/examples/custom_vars.csv +0 -0
  73. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/examples/custom_vars.mpf +0 -0
  74. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/examples/def_string.csv +0 -0
  75. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/examples/def_string.mpf +0 -0
  76. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/examples/defaults.csv +0 -0
  77. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/examples/defaults.mpf +0 -0
  78. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/examples/edge_cases.csv +0 -0
  79. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/examples/edge_cases.mpf +0 -0
  80. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/examples/flattening/flatten_demo.mpf +0 -0
  81. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/examples/flattening/flatten_demo_raw.csv +0 -0
  82. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/examples/for_loop.csv +0 -0
  83. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/examples/for_loop.mpf +0 -0
  84. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/examples/function_calls.csv +0 -0
  85. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/examples/function_calls.mpf +0 -0
  86. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/examples/goto.csv +0 -0
  87. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/examples/goto.mpf +0 -0
  88. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/examples/if_statement.csv +0 -0
  89. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/examples/if_statement.mpf +0 -0
  90. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/examples/increment.csv +0 -0
  91. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/examples/increment.mpf +0 -0
  92. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/examples/logic_operators.csv +0 -0
  93. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/examples/logic_operators.mpf +0 -0
  94. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/examples/loop.csv +0 -0
  95. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/examples/loop.mpf +0 -0
  96. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/examples/multiple_m_codes.csv +0 -0
  97. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/examples/multiple_m_codes.mpf +0 -0
  98. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/examples/precedence.csv +0 -0
  99. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/examples/precedence.mpf +0 -0
  100. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/examples/r_param.csv +0 -0
  101. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/examples/r_param.mpf +0 -0
  102. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/examples/repeat.csv +0 -0
  103. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/examples/repeat.mpf +0 -0
  104. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/examples/simple.csv +0 -0
  105. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/examples/simple.mpf +0 -0
  106. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/examples/spline.csv +0 -0
  107. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/examples/spline.mpf +0 -0
  108. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/examples/tool.csv +0 -0
  109. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/examples/tool.mpf +0 -0
  110. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/examples/trans.csv +0 -0
  111. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/examples/trans.mpf +0 -0
  112. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/examples/trans2.mpf +0 -0
  113. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/examples/trans_ic.csv +0 -0
  114. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/examples/trans_ic.mpf +0 -0
  115. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/examples/trans_reset.csv +0 -0
  116. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/examples/trans_reset.mpf +0 -0
  117. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/examples/trig.csv +0 -0
  118. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/examples/trig.mpf +0 -0
  119. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/examples/variables.csv +0 -0
  120. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/examples/variables.mpf +0 -0
  121. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/ggroups/generate_g_commands.py +0 -0
  122. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/pyproject.toml +0 -0
  123. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/python/example/minimal.py +0 -0
  124. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/python/example/streaming.py +0 -0
  125. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/python/nc_gcode_interpreter/cli.py +0 -0
  126. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/python/nc_gcode_interpreter/ggroups.json +0 -0
  127. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/python/nc_gcode_interpreter/py.typed +0 -0
  128. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/python/nc_gcode_interpreter/viz.py +0 -0
  129. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/python/tests/nc_source.py +0 -0
  130. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/python/tests/test_arithmetic_functions.py +0 -0
  131. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/python/tests/test_batches.py +0 -0
  132. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/python/tests/test_expected_output.py +0 -0
  133. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/python/tests/test_field_defects.py +0 -0
  134. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/python/tests/test_flags.py +0 -0
  135. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/python/tests/test_g_groups.py +0 -0
  136. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/python/tests/test_g_vocabulary.py +0 -0
  137. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/python/tests/test_jumps.py +0 -0
  138. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/python/tests/test_real_comparisons.py +0 -0
  139. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/python/tests/test_streaming.py +0 -0
  140. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/rustfmt.toml +0 -0
  141. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/src/modal_groups.rs +0 -0
  142. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.9}/tests/cli.rs +0 -0
@@ -0,0 +1,41 @@
1
+ name: Benchmark
2
+ on:
3
+ workflow_dispatch:
4
+ inputs:
5
+ lines:
6
+ description: Space-separated synthetic line counts
7
+ default: '100000 400000'
8
+ required: true
9
+ samples:
10
+ description: Warm samples per case
11
+ default: '5'
12
+ required: true
13
+ permissions:
14
+ contents: read
15
+ jobs:
16
+ benchmark:
17
+ runs-on: ubuntu-latest
18
+ timeout-minutes: 30
19
+ steps:
20
+ - uses: actions/checkout@v5
21
+ - uses: dtolnay/rust-toolchain@stable
22
+ - uses: Swatinem/rust-cache@v2
23
+ - uses: actions/setup-python@v6
24
+ with:
25
+ python-version: '3.13'
26
+ - name: Run correctness gates
27
+ run: cargo test --features bench-experiments
28
+ - name: Differential benchmark smoke test
29
+ run: python scripts/benchmark.py --lines 1000 --shapes flat --edits coordinate parameter insert delete --threads 1 --blocks 64 --samples 1 --verify-digest --output benchmark-smoke.json
30
+ - name: Record baseline and experiments
31
+ env:
32
+ BENCH_LINES: ${{ inputs.lines }}
33
+ BENCH_SAMPLES: ${{ inputs.samples }}
34
+ run: python scripts/benchmark.py --lines $BENCH_LINES --samples "$BENCH_SAMPLES" --output benchmark-results.json
35
+ - uses: actions/upload-artifact@v4
36
+ if: always()
37
+ with:
38
+ name: benchmark-results
39
+ path: |
40
+ benchmark-results.json
41
+ benchmark-smoke.json
@@ -54,7 +54,7 @@ jobs:
54
54
  - name: Check Rust formatting
55
55
  run: cargo fmt --check
56
56
  - name: Run Rust unit and integration tests
57
- run: cargo test
57
+ run: cargo test --features bench-experiments
58
58
 
59
59
  linux-x64:
60
60
  needs: [lint]
@@ -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
+ # Named interpreters: --find-interpreter also picks up the image's PyPy,
160
+ # which pyo3-ffi 0.29.0 fails to compile for.
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
@@ -4,6 +4,194 @@ 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.9] - 2026-11-01
8
+
9
+ ### Changed
10
+
11
+ - `nc_to_batches` and `nc_to_rows` borrow the Python str's UTF-8 buffer for
12
+ the run instead of copying it twice (once into a Rust `String`, once more
13
+ for error messages). On a 5M-line, 322 MB program the peak footprint of
14
+ `nc_to_batches` drops from 1.28 to 0.72 GiB. The Rust entry points
15
+ (`nc_to_table`, `nc_to_row_stream`, `nc_to_batch_stream*`) take
16
+ `impl Into<SourceText>`: a `&str` still works and is copied once, an owned
17
+ `String` or `Arc<str>` is shared without a copy (#89).
18
+
19
+ - Every jump-free program now runs on the streaming line driver, not only
20
+ those of 64 MiB or more, and the streaming driver decodes ahead on a second
21
+ thread instead of decoding inline. On a 1M-line, 63 MB program
22
+ `nc_to_batches` peaks at 0.21 GiB instead of 0.60 GiB and takes 1.02 s
23
+ instead of 1.24 s; at 5M lines it takes 5.1 s instead of 6.3 s. Programs
24
+ with jumps keep the indexed driver; `NC_STREAM_MIN_BYTES` still sets a size
25
+ threshold (#89).
26
+
27
+ - Less work per output row on the interpreter thread: the batch builder moves
28
+ string cells out of the rows it recycles instead of cloning them, and the
29
+ per-row column set and the flattener's position maps hash with FxHash. At
30
+ 5M lines `nc_to_batches` takes 4.4 s instead of 5.2 s (#89).
31
+
32
+ ## [v0.2.8] - 2026-10-15
33
+
34
+ ### Added
35
+
36
+ - `MATCH` and `MINDEX` string search functions (manual 4.1.4.6), next to
37
+ `INDEX`/`RINDEX`: `MATCH` returns the 0-based position of a substring,
38
+ `MINDEX` that of the first character found in a character list; both return
39
+ `-1` when nothing matches (#64).
40
+
41
+ - The `<<` operator is accepted in function-argument position, including the
42
+ prefix form with no left operand, which is the implicit conversion to STRING
43
+ (manual 4.1.4.1). Real programs write `MATCH(<machine data>, <<NAME)` in an
44
+ IF condition; that line used to be a parse error (#64).
45
+
46
+ - Programmable frame rotations `ROT` and `AROT`, with `X.. Y.. Z..` angles or
47
+ the planar form `RPL=`, on top of the existing `TRANS`/`ATRANS` offsets
48
+ (#68). The geometry axes X, Y, Z are output as machine coordinates under a
49
+ rotation matrix plus translation (machine = R * local + t); every other axis
50
+ keeps its plain offset, so programmed A/B/C orientations are never rotated.
51
+ The semantics follow the Sinumerik frame chapters (NC programming manual
52
+ 3.12.2.1 and 3.12.2.4, Basic Functions manual 4.5.2.3 and 4.5.2.4): `ROT`
53
+ and `TRANS` are both absolute and each deletes the whole programmable frame,
54
+ so a `TRANS` after `AROT` drops the rotation and a `ROT` after `TRANS` drops
55
+ the offset; `AROT` chains onto the current frame; several angles in one
56
+ block are RPY angles executed Z, then Y', then X'' (MD10600
57
+ FRAME_ANGLE_INPUT_MODE = 1, the default; the Euler ZX'Z'' mode is out of
58
+ scope); `RPL` turns about the normal of the active G17/G18/G19 plane;
59
+ `ATRANS` after a rotation offsets in the rotated system. `IC()` increments
60
+ stay in local coordinates. G2/G3 arcs (I/J/K and CR forms, TURN) are
61
+ supported while the rotation keeps the arc plane equal to the machine plane
62
+ (RPL, or a rotation about the plane normal): the centre offsets are rotated
63
+ with the frame and the flattener works unchanged. An arc under a rotation
64
+ that tilts the plane, `RPL=` mixed with axis angles, angles on non-geometry
65
+ axes, and the still unmodeled `ROTS`/`AROTS`/`CROTS`, `SCALE`/`ASCALE` and
66
+ `MIRROR`/`AMIRROR` all raise with line context. The final state dict gains a
67
+ `rotation` entry (row-major 3x3 list); `translation` keeps its meaning. The
68
+ fast path and the streaming/batch APIs mirror the whole-file interpreter.
69
+
70
+ ### Changed
71
+
72
+ - Jump-free programs of 64 MiB or more on the fast path no longer keep every
73
+ decoded line in memory. A parallel preflight scan validates the whole
74
+ program first (a parse error anywhere, also after `M30`, is still reported
75
+ before any row is emitted), then execution decodes and parses one line at a
76
+ time, so parse storage is bounded by the longest line instead of the program
77
+ length. Peak RSS of `nc_to_batches` on a 1.28M-line (112 MB) field program
78
+ drops from about 1.3 GiB to 0.4 GiB, and on a 22M-line (1.1 GB) one from
79
+ 5.4 GiB to 2.4 GiB; the second decode costs up to about 20 percent of
80
+ interpretation time. Below 64 MiB the indexed path stays: its retained state
81
+ is at most about 0.7 GiB there, and the second decode would cost the most
82
+ relative time. `NC_STREAM_MIN_BYTES` sets the threshold in bytes (0 streams
83
+ every jump-free program). Programs with a jump keep the indexed path at any
84
+ size (#73).
85
+
86
+ - The fast path builds its jump-target index (one `N:` key per block number,
87
+ one `LABEL:` key per label) only when a grammar-bound line mentions GOTO.
88
+ Trivial lines can never carry a jump, so a program without one skips the
89
+ allocation entirely; a GOTO inside a comment merely builds an unused index.
90
+ About 40 % less time on a 400k-line straight-line program (#73).
91
+
92
+ - A frame instruction no longer moves the interpreted position. `TRANS`,
93
+ `ATRANS`, `ROT` and `AROT` hold the machine position of every positioned
94
+ axis and re-derive its local (workpiece) coordinate under the new frame, as
95
+ a control does when a frame becomes active (Basic Functions manual 4.5.11:
96
+ `$AA_IM` is the machine setpoint, `$AA_IW` the workpiece one). So a block
97
+ after the frame change that programs only some axes moves the others only
98
+ through the rotation, `$AA_IM` agrees with the output rows, `$AA_IW` reports
99
+ the re-derived value, and an `IC()` increment after a `TRANS` moves from the
100
+ current position (NC programming manual 3.1.1.5) instead of jumping by the
101
+ offset; `examples/trans2.csv` changes accordingly. A geometry axis that a
102
+ rotation couples to a positioned axis before any block positioned it is
103
+ taken at machine 0 (with a warning) from then on, so it can be read with
104
+ `$AA_IM` and appears in the rows.
105
+
106
+ - `scripts/compare_benchmarks.py` refuses to compare reports recorded on
107
+ different inputs or, when both were recorded with `--verify-digest`, with
108
+ differing row counts or output/state digests in any matched case (#76).
109
+
110
+ - A control-structure keyword must stand alone in its block (#84). The
111
+ grammar accepted statements on the same line and ran them as part of the
112
+ body: `WHILE R1<3 X1` (likewise after `LOOP`, `REPEAT`, `FOR ... TO ...` and
113
+ `ELSE`) ran `X1` as the first body block of every pass, and a mid-line
114
+ `X1 ELSE X2` ended the true branch. The NC programming manual (4.1.7) shows
115
+ every structure keyword in a block of its own and does not say a control
116
+ accepts more, so these forms are now parse errors that name the text to
117
+ move. A comment after the keyword and a block number before it are still
118
+ allowed. A closer after other content (`X1 ENDIF`) is reported at the closer
119
+ instead of as "IF is never closed". A jump label on a structure line
120
+ (`LB: WHILE R1<3`, `LB: ENDIF`) is rejected too, as a real control does with
121
+ alarm 12630; a conditional jump (`LB: IF R1<3 GOTOB LB`) is no structure and
122
+ keeps its label.
123
+
124
+ ### Fixed
125
+
126
+ - Flattened `BSPLINE` samples now carry the line number of the control-point
127
+ block they belong to, instead of all carrying the last control point's line.
128
+ `ASPLINE`/`CSPLINE` and G2/G3 already attributed their samples per segment,
129
+ so a simulator that highlights the executing line from `line_no` stuck on
130
+ one line for a B-spline and walked through the control-point lines for
131
+ every other curve. The curve is now split at the Greville abscissae of the
132
+ clamped knot vector: a sample belongs to the first control point whose
133
+ Greville abscissa is at or beyond the sample's parameter, the last control
134
+ point taking the remainder. Between its Greville abscissae a control point
135
+ is the one with the most influence on the curve, and for degree 1 the rule
136
+ reduces to the polyline segment ending at each programmed point. The
137
+ sampled geometry is unchanged (same coordinates, order and count), only the
138
+ `line_no` distribution changes: the auxiliary cells of a control-point
139
+ block (feed, M codes, comments) now ride on its first sample, and a block
140
+ that owns samples no longer emits an aux-only placeholder row. Only the
141
+ curve end stays a programmed position; every other sample keeps
142
+ `flattened = 1`. The checked-in `examples/flattening/flatten_demo_flattened.csv`
143
+ is regenerated accordingly (#62).
144
+
145
+ - Two statements glued onto one line with no separator (`... = SET(...)DEF INT
146
+ ...`, a copy-paste defect seen in a real program) are still rejected, but
147
+ the error now names the trailing text and points at the column where the
148
+ second statement starts, instead of "expected the end of the program". The
149
+ INDEX-based single-character write from the same report has worked since
150
+ v0.2.6 and is now pinned by a test (#58).
151
+
152
+ - Reading an array element or indexed system variable that was never assigned
153
+ reported "Invalid array index ... used as an array index"; the message now
154
+ says the element is undefined and how to supply it. It still names the
155
+ variable and the line, so an unknown string-typed machine data in a string
156
+ argument is a loud error rather than a silent empty string (#64).
157
+
158
+ - A quoted string passed to a numeric function (`SIN("abc")`) was silently
159
+ dropped from the argument list and surfaced as an arity error; it is now a
160
+ type error naming the string.
161
+
162
+ - Nested function calls parse in linear time: each function argument, and
163
+ each assignment right-hand side, is parsed once instead of once per grammar
164
+ alternative, so a deeply nested `SIN(SIN(...))` is instant (#78).
165
+
166
+ - The line fast path no longer declines whole programs because a few hundred
167
+ grammar-bound lines (procedure calls such as `SUPPORT_PIN_TRIGGER(2.5)`,
168
+ `MATERIAL_UPDATE`, `R1=R1+1`, `STOPRE`) are spread through a large file
169
+ (#69). Grammar-bound lines used to be parsed one at a time, each padded with
170
+ one newline per preceding line so that pest reported whole-file positions;
171
+ the padding cost O(line index) bytes per line, and a budget guard sent any
172
+ program over it to the whole-file parse. The grammar-bound lines are now
173
+ parsed in one pest run over a sparse copy of the program (only those lines
174
+ keep their text, every other line is empty), which gives the same whole-file
175
+ positions at O(input) bytes. The budget guard, the padded buffers and the
176
+ grammar-line ratio guard are gone: a program that is entirely grammar-bound
177
+ now costs the same as the whole-file parse (measured equal at 100k and 400k
178
+ lines), so every structure-free program takes the line path and the
179
+ whole-file parse is only used for programs with IF/WHILE/FOR/LOOP/REPEAT
180
+ blocks. On a 112 MB, 1.28M-line CAM
181
+ program with about 12,000 such lines the Python batch API went from 24.3 s
182
+ and 3.0 GiB peak RSS (whole-file parse) to 2.2 s and 1.3 GiB.
183
+
184
+ - The line fast path no longer gives three line shapes a different meaning
185
+ than the grammar does (#83). `N10X5` (a block number glued to the next word)
186
+ was read as block 10 plus `X5`, where the grammar reads one call named
187
+ `N10X5` and warns about the glued word; `TRANS=5` (a frame keyword before
188
+ `=`) was stored as a variable, where the grammar rejects the line; and
189
+ `X=5 AND Y1` (likewise `OR`, `XOR`, `B_AND`, `B_OR`, `B_XOR` after a
190
+ constant) became `X=5`, a call `AND` and `Y1`, where the grammar evaluates
191
+ `5 AND Y1`. The decoder now hands these lines to the grammar, so the result
192
+ no longer depends on which path a program takes. A seeded random-line test
193
+ compares every line the decoder claims against the grammar.
194
+
7
195
  ## [v0.2.7] - 2026-09-08
8
196
 
9
197
  ### Changed
@@ -506,7 +506,7 @@ checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4"
506
506
 
507
507
  [[package]]
508
508
  name = "nc-gcode-interpreter"
509
- version = "0.2.7"
509
+ version = "0.2.9"
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.7"
3
+ version = "0.2.9"
4
4
  edition = "2021"
5
5
  readme = "README.md"
6
6
 
@@ -47,4 +47,10 @@ arrow-data = { version = "59", optional = true, default-features = false }
47
47
 
48
48
  [features]
49
49
  default = []
50
+ bench-experiments = []
50
51
  python = ["pyo3", "arrow-array", "arrow-schema", "arrow-data"]
52
+
53
+ [[bench]]
54
+ name = "workloads"
55
+ harness = false
56
+ required-features = ["bench-experiments"]
@@ -0,0 +1,146 @@
1
+ # Development
2
+
3
+
4
+ Use Cargo to build and run the CLi tool:
5
+
6
+ ```
7
+ cargo run -- --help
8
+ ```
9
+
10
+
11
+ To compile the python module:
12
+
13
+ ```bash
14
+ maturin develop
15
+ ```
16
+
17
+ ## Setup python environment
18
+
19
+ ```bash
20
+ uv venv -p 3.12
21
+ uv synv --all-extras
22
+ ```
23
+
24
+
25
+
26
+ ## Release
27
+
28
+ ```bash
29
+ cargo build --release
30
+ maturin develop --release --uv
31
+ ```
32
+
33
+ ### Versioning
34
+
35
+ `Cargo.toml` holds the placeholder version `0.0.0-dev`; the release workflow
36
+ rewrites it from the git tag before building the wheels, so the released
37
+ version always comes from the tag and never from a committed number.
38
+
39
+ Local builds get a version too. `build_backend/nc_build_backend.py` is an
40
+ in-tree PEP 517 backend that wraps maturin: when it sees the placeholder and
41
+ finds a git checkout with tags, it stamps
42
+ `git describe --tags --long --dirty` onto the `Cargo.toml` version as
43
+ `0.2.6-dev.3+g<sha>` (maturin normalises that to the PEP 440
44
+ `0.2.6.dev3+<sha>`), builds, then restores `Cargo.toml` and `Cargo.lock`. So an
45
+ editable `uv sync` reports the commit it was built from rather than
46
+ `0.0.0.dev0` - which is what lets a downstream consumer notice it is running a
47
+ local edit of the interpreter rather than a release.
48
+
49
+ Any non-placeholder version in `Cargo.toml` is left alone, so the CI release
50
+ path is unaffected. Without git (an unpacked sdist, say) the placeholder
51
+ stands.
52
+
53
+ A plain `cargo build` doesn't go through that shim, so `build.rs` derives the
54
+ same version independently and exposes it as `NC_GCODE_INTERPRETER_VERSION`,
55
+ which is what the CLI's `--version` reports. It applies the same rules: an
56
+ already-stamped `Cargo.toml` version wins, and without git the placeholder
57
+ stands. The CLI therefore shows cargo's semver spelling
58
+ (`0.2.6-dev.1+ge5f2306`), not maturin's PEP 440 normalisation of it.
59
+
60
+ ### Release profile
61
+
62
+ `[profile.release]` currently sets no `strip`/`lto` overrides. `strip = true` was
63
+ tried and dropped (`e1ff63a`): cargo runs the *host* `strip` binary even on a
64
+ cross-compiled artifact, so stripping the cross-built macOS x86_64 dylib with
65
+ the arm64 host `strip` produced a wheel PyPI rejected as "not a zipfile" -
66
+ stripping was reverted to unblock the release. It was a secondary size win
67
+ (mostly orthogonal to the polars removal that motivated it) and can be
68
+ revisited via maturin's cross-aware `--strip` flag instead of a Cargo
69
+ profile setting.
70
+
71
+ `lto` remains an untried, plausible speed knob (doesn't invoke an external
72
+ tool, so it isn't blocked by the cross-compile issue above) - just not yet
73
+ evaluated for its extra build time across the wheel matrix.
74
+
75
+
76
+ ## Super simple test
77
+
78
+ There are a bunch of csv files in the examples directory. To test the tool on all of them (use git to check changes)
79
+
80
+ ```bash
81
+ rm **/*.csv && cargo build --release && find examples -name "*.mpf" -type f -print0 | xargs -0 -I {} sh -c './target/release/nc-gcode-interpreter --axis-index-map E:4 --initial_state=examples/defaults.mpf "$1" || echo "Failed to process $1" >&2' sh {}
82
+ ```
83
+
84
+ ## python test
85
+
86
+ ```bash
87
+ maturin develop --release --uv && pytest
88
+ ```
89
+
90
+ ## Benchmark harness and parsing experiments
91
+
92
+ The `bench-experiments` cargo feature compiles research-only strategies
93
+ (`src/line_driver/experiments.rs`), a benchmark executable (`benches/`) and
94
+ differential tests (`tests/performance_contract.rs`). Normal library and Python
95
+ builds do not include any of it. The strategies are a laboratory for #71, #72,
96
+ #73 and #74, not a session or incremental API.
97
+
98
+ ```bash
99
+ cargo test --features bench-experiments
100
+ python3 scripts/benchmark.py --lines 10000 100000 400000 --samples 5 --output tmp/baseline.json
101
+ python3 scripts/compare_benchmarks.py tmp/baseline.json tmp/candidate.json
102
+ ```
103
+
104
+ `scripts/benchmark.py` (Python 3.11 or newer, macOS or Linux) runs each case in
105
+ its own release-build process and records cold and warm samples, time to the
106
+ first batch, per-process peak RSS, build settings, the source and binary
107
+ SHA-256 and the git revision. Sample zero is cold. `prepare_ms + run_ms` is the
108
+ interpreter turnaround. The timings exclude Python, Arrow and viewer work, so
109
+ they say nothing about edit-to-visible latency (#72). Do not run other builds
110
+ concurrently when comparing. Results are not committed; keep them under `tmp/`.
111
+
112
+ | Mode | What it measures | Boundary |
113
+ | --- | --- | --- |
114
+ | `fresh` | The public batch API, 1 or 4 Rayon threads; `--reference` adds the whole-file grammar and the VM | Full interpretation every time |
115
+ | `grammar-cache` | Keeping a pest tree for unchanged reruns | Every edit reparses; O(file) AST |
116
+ | `decoded-cache` | Immutable decoded blocks, patched by line range | Trivial label-free lines only; output is re-executed |
117
+ | `bounded` | Validate eligibility, then decode and execute one line at a time | Two decode passes; trivial label-free lines only |
118
+ | `fused` | Decode and execute immediately with a reusable word arena | Same language; a later unsupported line fails after output started |
119
+
120
+ Edit cases (`--edits coordinate parameter insert delete translation`) prepare
121
+ two revised sources before timing; `decoded-cache` undoes the previous patch
122
+ and applies the next one inside `prepare_ms`. `--input <file>` benchmarks a
123
+ local program with `--edits none`; `--verify-digest` additionally checks that
124
+ every strategy produces the same rows and final state (hashing adds time, so
125
+ do not compare those timings with plain runs). `--cancel` drops the consumer
126
+ after the first batch. The manually dispatched Benchmark workflow runs the
127
+ correctness gate and uploads the JSON; it asserts no wall-clock numbers.
128
+
129
+ Findings so far (Apple M1 Pro, release builds, median of five warm samples,
130
+ single thread, 400k-line synthetic straight-line program):
131
+
132
+ | Strategy | Time | Peak RSS |
133
+ | --- | ---: | ---: |
134
+ | Public batch API | 672 ms | 560 MiB |
135
+ | Cached decoded blocks (64 lines) | 260 ms | 272 MiB |
136
+ | Fused decode and execute | 357 ms | 87 MiB |
137
+
138
+ Parsing reuse removes about a quarter of the warm-edit time; execution and
139
+ output dominate the rest, so output chunks or checkpoints (#47, #74) are
140
+ needed to go further. Keeping the whole pest tree was slower than the fast
141
+ path on ordinary CAM input and cost 3.7 s to rebuild after a TRANS edit. Four
142
+ Rayon threads did not consistently beat one. Thin LTO changed the flat cases by
143
+ 1 to 9 %. Fused decoding handled synthetic 7M and 22M-line programs in 6.3 s
144
+ and 21 s at 1.0 and 3.1 GiB; the input copy and the State line index still
145
+ scale with the source. The sparse grammar buffer that came out of this work is
146
+ now the production fast path (#69).
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nc-gcode-interpreter
3
- Version: 0.2.7
3
+ Version: 0.2.9
4
4
  Classifier: Programming Language :: Rust
5
5
  Classifier: Programming Language :: Python :: Implementation :: CPython
6
6
  Classifier: Programming Language :: Python :: 3.12
@@ -11,7 +11,7 @@ The **NC-GCode-Interpreter** offers a streamlined and efficient solution for int
11
11
  ### Supported G-code Features
12
12
 
13
13
  - **G Group Commands**: Recognizes G-code groups and modal G-code commands.
14
- - **Global Transformations**: Supports commands like `TRANS` and `ATRANS` for adjusting coordinates globally.
14
+ - **Programmable frames**: `TRANS`/`ATRANS` work offsets and `ROT`/`AROT` rotations (`X.. Y.. Z..` RPY angles or `RPL=` in the active plane) for the geometry axes X, Y, Z; output coordinates are machine coordinates with the frame applied, and the final state reports the `translation` and `rotation`.
15
15
  - **Looping Constructs**: Handles loops using `WHILE`, `FOR`, `REPEAT ... UNTIL` and `LOOP ... ENDLOOP` statements.
16
16
  - **Variable Handling**: Supports definition and manipulation of local variables.
17
17
  - **Conditional Logic**: Implements conditional execution with `IF`, `ELSE`, and `ENDIF`.
@@ -23,7 +23,7 @@ The **NC-GCode-Interpreter** offers a streamlined and efficient solution for int
23
23
 
24
24
  ### Additional Functionality
25
25
 
26
- - **Streaming interpretation**: `nc_to_rows(program)` yields `(line_no, row)` tuples lazily while the interpreter runs on a background thread — constant memory, early exit by dropping the iterator (breaking out of a `for` loop over an anonymous iterator does this; a stored iterator keeps the run alive until it is deleted or garbage-collected), and source-line numbers for mapping trace rows back to the program. With `include_variables=True` it yields `(line_no, row, variables)` instead, exposing every variable assignment (`R1=R1+1`, `DEF`, FOR counters) as it happens — including blocks that only assign variables, which are invisible in the batch DataFrame.
26
+ - **Streaming interpretation**: `nc_to_rows(program)` yields `(line_no, row)` tuples lazily while the interpreter runs on a background thread — bounded output buffering, early exit by dropping the iterator (breaking out of a `for` loop over an anonymous iterator does this; a stored iterator keeps the run alive until it is deleted or garbage-collected), and source-line numbers for mapping trace rows back to the program. With `include_variables=True` it yields `(line_no, row, variables)` instead, exposing every variable assignment (`R1=R1+1`, `DEF`, FOR counters) as it happens — including blocks that only assign variables, which are invisible in the batch DataFrame.
27
27
 
28
28
  - **Curve flattening**: pass `flatten_tolerance` (Python) or `--flatten-tolerance` (CLI) to convert curved motions — `G2`/`G3` arcs (`I`/`J`/`K` centre offsets or `CR=` radius form, `G17`/`G18`/`G19` planes, helical third axis with `TURN=` multi-turn support, full circles) and `ASPLINE`/`BSPLINE`/`CSPLINE` splines (including `PW` weights and `SD` degree) — into runs of plain `G1` rows in the same table format. `CIP`/`CT`/`POLY`/thread/involute blocks pass through unchanged with a warning; spline start/end conditions (`BAUTO`/`BNAT`/... ) are approximated by natural (CSPLINE) / Akima (ASPLINE) boundaries — see the `flatten` module docs for the full list of checked approximations. The single tolerance is the maximum deviation between the emitted polyline and the true curve, in path units: arcs use the exact sagitta bound, splines adaptive subdivision. The interpolation addresses (`I`/`J`/`K`/`CR`/`PW`/`SD`/`PL`) are consumed and do not appear in the output; sampled rows keep the source block's line number, and its other cells (`F`, `M`, comments, ...) ride on the first sample so modal behavior is preserved. Generated samples carry a `flattened = 1` marker column (never forward-filled); programmed positions stay null there, so filtering on null recovers the original toolpath points.
29
29
  - **Toolpath visualization** (optional): with the `viz` extra (`pip install 'nc-gcode-interpreter[viz]'`), `nc_gcode_interpreter.viz.view_toolpath(df)` renders an interpreted toolpath in [threejs-viewer](https://pypi.org/project/threejs-viewer/) as an extruded bead tube with a draw-range animation replaying the program at feed-rate-proportional speed. Combined with `flatten_tolerance`, programmed points render orange and flattener-generated samples blue. The extra also installs the `nc-view` command: `nc-view part.mpf` interprets, flattens (default tolerance 0.1, `--no-flatten` to skip) and opens the animated toolpath in the browser; `--speed` sets the time-lapse factor (default 60 = one machine-minute per second).
@@ -161,6 +161,23 @@ 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 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
177
+ `python scripts/benchmark.py --input <file> --modes fresh --edits none
178
+ --threads 4 --samples 1 --batch-size 4096 --allow-undefined --axis-index-e 4
179
+ --axis-index-elx 5 --output tmp/memory.json`.
180
+
164
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.
165
182
 
166
183
  Additionally, conversion from a Polars DataFrame back to an MPF (NC) program is also supported:
@@ -0,0 +1,50 @@
1
+ use std::fmt::Write;
2
+
3
+ pub fn program(shape: &str, lines: usize) -> String {
4
+ assert!(lines >= 8);
5
+ let mut text = String::with_capacity(lines * 90);
6
+ text.push_str(if shape == "frames" {
7
+ "TRANS X10\n"
8
+ } else {
9
+ "MULTIPLIER=1\n"
10
+ });
11
+ text.push_str("G1 X0 Y0 Z0 E0 F1200\n");
12
+ for i in 2..lines - 1 {
13
+ match shape {
14
+ "calls" if i % 1000 == 0 => text.push_str("SUPPORT_PIN_TRIGGER(2.5)\n"),
15
+ "grammar" => text.push_str("X=SIN(30) Y=COS(60)\n"),
16
+ "structured" if i == 2 => text.push_str("IF 1==1\n"),
17
+ "structured" if i == lines - 2 => text.push_str("ENDIF\n"),
18
+ _ => writeln!(
19
+ text,
20
+ "N{i} G1 X{}.125 Y{}.25 Z1 E=IC({}) ELX=100 F1200",
21
+ i % 1000,
22
+ i % 300,
23
+ if shape == "frames" { "0.025" } else { "MULTIPLIER*0.025" }
24
+ )
25
+ .unwrap(),
26
+ }
27
+ }
28
+ text.push_str("M30\n");
29
+ text
30
+ }
31
+
32
+ /// All edits are against the original revision, allowing alternating edit/undo.
33
+ pub fn edit(source: &str, kind: &str, alternate: bool) -> (String, std::ops::Range<usize>, String) {
34
+ let mut lines: Vec<_> = source.lines().collect();
35
+ let mid = lines.len() / 2;
36
+ let (range, replacement) = match kind {
37
+ "none" => (0..0, String::new()),
38
+ "coordinate" => (
39
+ mid..mid + 1,
40
+ format!("G1 X{} Y2 E=IC(0.025)", if alternate { 7 } else { 8 }),
41
+ ),
42
+ "parameter" => (0..1, format!("MULTIPLIER={}", if alternate { 2 } else { 3 })),
43
+ "translation" => (0..1, format!("TRANS X{}", if alternate { 20 } else { 30 })),
44
+ "insert" => (mid..mid, "; inserted comment\nG1 X3".into()),
45
+ "delete" => (mid..mid + 1, String::new()),
46
+ _ => panic!("unknown edit {kind}"),
47
+ };
48
+ lines.splice(range.clone(), replacement.lines());
49
+ (format!("{}\n", lines.join("\n")), range, replacement)
50
+ }