nc-gcode-interpreter 0.2.7__tar.gz → 0.2.8__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.8/.github/workflows/benchmark.yml +41 -0
  2. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/.github/workflows/build-and-release.yml +1 -1
  3. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/CHANGELOG.md +163 -0
  4. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/Cargo.lock +1 -1
  5. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/Cargo.toml +7 -1
  6. nc_gcode_interpreter-0.2.8/Development.md +146 -0
  7. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/PKG-INFO +1 -1
  8. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/README.md +18 -2
  9. nc_gcode_interpreter-0.2.8/benches/fixtures.rs +50 -0
  10. nc_gcode_interpreter-0.2.8/benches/workloads.rs +194 -0
  11. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/examples/flattening/flatten_demo_flattened.csv +0 -1
  12. nc_gcode_interpreter-0.2.8/examples/rot.csv +17 -0
  13. nc_gcode_interpreter-0.2.8/examples/rot.mpf +18 -0
  14. nc_gcode_interpreter-0.2.8/examples/trans2.csv +5 -0
  15. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/python/nc_gcode_interpreter/__init__.py +3 -2
  16. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/python/nc_gcode_interpreter/_internal.pyi +1 -1
  17. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/python/tests/test_batch_variables.py +1 -1
  18. nc_gcode_interpreter-0.2.8/python/tests/test_compare_benchmarks.py +156 -0
  19. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/python/tests/test_diagnostics.py +64 -0
  20. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/python/tests/test_flatten.py +36 -0
  21. nc_gcode_interpreter-0.2.8/python/tests/test_frames.py +419 -0
  22. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/python/tests/test_stage1.py +32 -15
  23. nc_gcode_interpreter-0.2.8/python/tests/test_string_operations.py +169 -0
  24. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/python/tests/test_structured_errors.py +19 -0
  25. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/python/tests/test_unsupported.py +3 -2
  26. nc_gcode_interpreter-0.2.8/scripts/benchmark.py +339 -0
  27. nc_gcode_interpreter-0.2.8/scripts/compare_benchmarks.py +134 -0
  28. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/src/errors.rs +6 -3
  29. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/src/flatten.rs +227 -66
  30. nc_gcode_interpreter-0.2.8/src/frame.rs +187 -0
  31. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/src/grammar.pest +46 -32
  32. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/src/interpret_rules.rs +194 -39
  33. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/src/interpreter.rs +245 -14
  34. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/src/lib.rs +11 -3
  35. nc_gcode_interpreter-0.2.8/src/line_driver/experiments.rs +243 -0
  36. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/src/line_driver.rs +636 -160
  37. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/src/main.rs +1 -0
  38. nc_gcode_interpreter-0.2.8/src/state.rs +695 -0
  39. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/src/structure_scan.rs +204 -24
  40. nc_gcode_interpreter-0.2.8/src/types.rs +57 -0
  41. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/src/vm.rs +12 -4
  42. nc_gcode_interpreter-0.2.8/tests/performance_contract.rs +202 -0
  43. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/uv.lock +53 -32
  44. nc_gcode_interpreter-0.2.7/Development.md +0 -88
  45. nc_gcode_interpreter-0.2.7/examples/trans2.csv +0 -5
  46. nc_gcode_interpreter-0.2.7/python/tests/test_string_operations.py +0 -79
  47. nc_gcode_interpreter-0.2.7/src/state.rs +0 -351
  48. nc_gcode_interpreter-0.2.7/src/types.rs +0 -14
  49. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/.gitignore +0 -0
  50. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/CONTRIBUTING.md +0 -0
  51. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/LICENSE +0 -0
  52. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/TODO.md +0 -0
  53. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/build.rs +0 -0
  54. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/build_backend/nc_build_backend.py +0 -0
  55. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/docs/resumable-interpretation-design.md +0 -0
  56. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/docs/sinumerik-execution-model.md +0 -0
  57. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/examples/actual_position.csv +0 -0
  58. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/examples/actual_position.mpf +0 -0
  59. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/examples/arc.csv +0 -0
  60. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/examples/arc.mpf +0 -0
  61. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/examples/arrays.csv +0 -0
  62. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/examples/arrays.mpf +0 -0
  63. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/examples/axis_index_assignment.csv +0 -0
  64. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/examples/axis_index_assignment.mpf +0 -0
  65. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/examples/basic_math.csv +0 -0
  66. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/examples/basic_math.mpf +0 -0
  67. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/examples/case.csv +0 -0
  68. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/examples/case.mpf +0 -0
  69. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/examples/case_insensitive_variables.csv +0 -0
  70. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/examples/case_insensitive_variables.mpf +0 -0
  71. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/examples/custom_vars.csv +0 -0
  72. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/examples/custom_vars.mpf +0 -0
  73. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/examples/def_string.csv +0 -0
  74. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/examples/def_string.mpf +0 -0
  75. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/examples/defaults.csv +0 -0
  76. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/examples/defaults.mpf +0 -0
  77. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/examples/edge_cases.csv +0 -0
  78. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/examples/edge_cases.mpf +0 -0
  79. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/examples/flattening/flatten_demo.mpf +0 -0
  80. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/examples/flattening/flatten_demo_raw.csv +0 -0
  81. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/examples/for_loop.csv +0 -0
  82. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/examples/for_loop.mpf +0 -0
  83. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/examples/function_calls.csv +0 -0
  84. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/examples/function_calls.mpf +0 -0
  85. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/examples/goto.csv +0 -0
  86. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/examples/goto.mpf +0 -0
  87. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/examples/if_statement.csv +0 -0
  88. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/examples/if_statement.mpf +0 -0
  89. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/examples/increment.csv +0 -0
  90. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/examples/increment.mpf +0 -0
  91. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/examples/logic_operators.csv +0 -0
  92. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/examples/logic_operators.mpf +0 -0
  93. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/examples/loop.csv +0 -0
  94. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/examples/loop.mpf +0 -0
  95. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/examples/multiple_m_codes.csv +0 -0
  96. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/examples/multiple_m_codes.mpf +0 -0
  97. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/examples/precedence.csv +0 -0
  98. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/examples/precedence.mpf +0 -0
  99. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/examples/r_param.csv +0 -0
  100. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/examples/r_param.mpf +0 -0
  101. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/examples/repeat.csv +0 -0
  102. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/examples/repeat.mpf +0 -0
  103. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/examples/simple.csv +0 -0
  104. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/examples/simple.mpf +0 -0
  105. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/examples/spline.csv +0 -0
  106. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/examples/spline.mpf +0 -0
  107. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/examples/tool.csv +0 -0
  108. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/examples/tool.mpf +0 -0
  109. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/examples/trans.csv +0 -0
  110. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/examples/trans.mpf +0 -0
  111. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/examples/trans2.mpf +0 -0
  112. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/examples/trans_ic.csv +0 -0
  113. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/examples/trans_ic.mpf +0 -0
  114. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/examples/trans_reset.csv +0 -0
  115. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/examples/trans_reset.mpf +0 -0
  116. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/examples/trig.csv +0 -0
  117. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/examples/trig.mpf +0 -0
  118. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/examples/variables.csv +0 -0
  119. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/examples/variables.mpf +0 -0
  120. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/ggroups/generate_g_commands.py +0 -0
  121. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/pyproject.toml +0 -0
  122. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/python/example/minimal.py +0 -0
  123. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/python/example/streaming.py +0 -0
  124. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/python/nc_gcode_interpreter/cli.py +0 -0
  125. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/python/nc_gcode_interpreter/ggroups.json +0 -0
  126. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/python/nc_gcode_interpreter/py.typed +0 -0
  127. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/python/nc_gcode_interpreter/viz.py +0 -0
  128. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/python/tests/nc_source.py +0 -0
  129. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/python/tests/test_arithmetic_functions.py +0 -0
  130. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/python/tests/test_batches.py +0 -0
  131. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/python/tests/test_expected_output.py +0 -0
  132. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/python/tests/test_field_defects.py +0 -0
  133. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/python/tests/test_flags.py +0 -0
  134. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/python/tests/test_g_groups.py +0 -0
  135. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/python/tests/test_g_vocabulary.py +0 -0
  136. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/python/tests/test_jumps.py +0 -0
  137. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/python/tests/test_real_comparisons.py +0 -0
  138. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/python/tests/test_streaming.py +0 -0
  139. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/rustfmt.toml +0 -0
  140. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/src/modal_groups.rs +0 -0
  141. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/src/output.rs +0 -0
  142. {nc_gcode_interpreter-0.2.7 → nc_gcode_interpreter-0.2.8}/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]
@@ -4,6 +4,169 @@ 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.8] - 2026-10-15
8
+
9
+ ### Added
10
+
11
+ - `MATCH` and `MINDEX` string search functions (manual 4.1.4.6), next to
12
+ `INDEX`/`RINDEX`: `MATCH` returns the 0-based position of a substring,
13
+ `MINDEX` that of the first character found in a character list; both return
14
+ `-1` when nothing matches (#64).
15
+
16
+ - The `<<` operator is accepted in function-argument position, including the
17
+ prefix form with no left operand, which is the implicit conversion to STRING
18
+ (manual 4.1.4.1). Real programs write `MATCH(<machine data>, <<NAME)` in an
19
+ IF condition; that line used to be a parse error (#64).
20
+
21
+ - Programmable frame rotations `ROT` and `AROT`, with `X.. Y.. Z..` angles or
22
+ the planar form `RPL=`, on top of the existing `TRANS`/`ATRANS` offsets
23
+ (#68). The geometry axes X, Y, Z are output as machine coordinates under a
24
+ rotation matrix plus translation (machine = R * local + t); every other axis
25
+ keeps its plain offset, so programmed A/B/C orientations are never rotated.
26
+ The semantics follow the Sinumerik frame chapters (NC programming manual
27
+ 3.12.2.1 and 3.12.2.4, Basic Functions manual 4.5.2.3 and 4.5.2.4): `ROT`
28
+ and `TRANS` are both absolute and each deletes the whole programmable frame,
29
+ so a `TRANS` after `AROT` drops the rotation and a `ROT` after `TRANS` drops
30
+ the offset; `AROT` chains onto the current frame; several angles in one
31
+ block are RPY angles executed Z, then Y', then X'' (MD10600
32
+ FRAME_ANGLE_INPUT_MODE = 1, the default; the Euler ZX'Z'' mode is out of
33
+ scope); `RPL` turns about the normal of the active G17/G18/G19 plane;
34
+ `ATRANS` after a rotation offsets in the rotated system. `IC()` increments
35
+ stay in local coordinates. G2/G3 arcs (I/J/K and CR forms, TURN) are
36
+ supported while the rotation keeps the arc plane equal to the machine plane
37
+ (RPL, or a rotation about the plane normal): the centre offsets are rotated
38
+ with the frame and the flattener works unchanged. An arc under a rotation
39
+ that tilts the plane, `RPL=` mixed with axis angles, angles on non-geometry
40
+ axes, and the still unmodeled `ROTS`/`AROTS`/`CROTS`, `SCALE`/`ASCALE` and
41
+ `MIRROR`/`AMIRROR` all raise with line context. The final state dict gains a
42
+ `rotation` entry (row-major 3x3 list); `translation` keeps its meaning. The
43
+ fast path and the streaming/batch APIs mirror the whole-file interpreter.
44
+
45
+ ### Changed
46
+
47
+ - Jump-free programs of 64 MiB or more on the fast path no longer keep every
48
+ decoded line in memory. A parallel preflight scan validates the whole
49
+ program first (a parse error anywhere, also after `M30`, is still reported
50
+ before any row is emitted), then execution decodes and parses one line at a
51
+ time, so parse storage is bounded by the longest line instead of the program
52
+ length. Peak RSS of `nc_to_batches` on a 1.28M-line (112 MB) field program
53
+ drops from about 1.3 GiB to 0.4 GiB, and on a 22M-line (1.1 GB) one from
54
+ 5.4 GiB to 2.4 GiB; the second decode costs up to about 20 percent of
55
+ interpretation time. Below 64 MiB the indexed path stays: its retained state
56
+ is at most about 0.7 GiB there, and the second decode would cost the most
57
+ relative time. `NC_STREAM_MIN_BYTES` sets the threshold in bytes (0 streams
58
+ every jump-free program). Programs with a jump keep the indexed path at any
59
+ size (#73).
60
+
61
+ - The fast path builds its jump-target index (one `N:` key per block number,
62
+ one `LABEL:` key per label) only when a grammar-bound line mentions GOTO.
63
+ Trivial lines can never carry a jump, so a program without one skips the
64
+ allocation entirely; a GOTO inside a comment merely builds an unused index.
65
+ About 40 % less time on a 400k-line straight-line program (#73).
66
+
67
+ - A frame instruction no longer moves the interpreted position. `TRANS`,
68
+ `ATRANS`, `ROT` and `AROT` hold the machine position of every positioned
69
+ axis and re-derive its local (workpiece) coordinate under the new frame, as
70
+ a control does when a frame becomes active (Basic Functions manual 4.5.11:
71
+ `$AA_IM` is the machine setpoint, `$AA_IW` the workpiece one). So a block
72
+ after the frame change that programs only some axes moves the others only
73
+ through the rotation, `$AA_IM` agrees with the output rows, `$AA_IW` reports
74
+ the re-derived value, and an `IC()` increment after a `TRANS` moves from the
75
+ current position (NC programming manual 3.1.1.5) instead of jumping by the
76
+ offset; `examples/trans2.csv` changes accordingly. A geometry axis that a
77
+ rotation couples to a positioned axis before any block positioned it is
78
+ taken at machine 0 (with a warning) from then on, so it can be read with
79
+ `$AA_IM` and appears in the rows.
80
+
81
+ - `scripts/compare_benchmarks.py` refuses to compare reports recorded on
82
+ different inputs or, when both were recorded with `--verify-digest`, with
83
+ differing row counts or output/state digests in any matched case (#76).
84
+
85
+ - A control-structure keyword must stand alone in its block (#84). The
86
+ grammar accepted statements on the same line and ran them as part of the
87
+ body: `WHILE R1<3 X1` (likewise after `LOOP`, `REPEAT`, `FOR ... TO ...` and
88
+ `ELSE`) ran `X1` as the first body block of every pass, and a mid-line
89
+ `X1 ELSE X2` ended the true branch. The NC programming manual (4.1.7) shows
90
+ every structure keyword in a block of its own and does not say a control
91
+ accepts more, so these forms are now parse errors that name the text to
92
+ move. A comment after the keyword and a block number before it are still
93
+ allowed. A closer after other content (`X1 ENDIF`) is reported at the closer
94
+ instead of as "IF is never closed". A jump label on a structure line
95
+ (`LB: WHILE R1<3`, `LB: ENDIF`) is rejected too, as a real control does with
96
+ alarm 12630; a conditional jump (`LB: IF R1<3 GOTOB LB`) is no structure and
97
+ keeps its label.
98
+
99
+ ### Fixed
100
+
101
+ - Flattened `BSPLINE` samples now carry the line number of the control-point
102
+ block they belong to, instead of all carrying the last control point's line.
103
+ `ASPLINE`/`CSPLINE` and G2/G3 already attributed their samples per segment,
104
+ so a simulator that highlights the executing line from `line_no` stuck on
105
+ one line for a B-spline and walked through the control-point lines for
106
+ every other curve. The curve is now split at the Greville abscissae of the
107
+ clamped knot vector: a sample belongs to the first control point whose
108
+ Greville abscissa is at or beyond the sample's parameter, the last control
109
+ point taking the remainder. Between its Greville abscissae a control point
110
+ is the one with the most influence on the curve, and for degree 1 the rule
111
+ reduces to the polyline segment ending at each programmed point. The
112
+ sampled geometry is unchanged (same coordinates, order and count), only the
113
+ `line_no` distribution changes: the auxiliary cells of a control-point
114
+ block (feed, M codes, comments) now ride on its first sample, and a block
115
+ that owns samples no longer emits an aux-only placeholder row. Only the
116
+ curve end stays a programmed position; every other sample keeps
117
+ `flattened = 1`. The checked-in `examples/flattening/flatten_demo_flattened.csv`
118
+ is regenerated accordingly (#62).
119
+
120
+ - Two statements glued onto one line with no separator (`... = SET(...)DEF INT
121
+ ...`, a copy-paste defect seen in a real program) are still rejected, but
122
+ the error now names the trailing text and points at the column where the
123
+ second statement starts, instead of "expected the end of the program". The
124
+ INDEX-based single-character write from the same report has worked since
125
+ v0.2.6 and is now pinned by a test (#58).
126
+
127
+ - Reading an array element or indexed system variable that was never assigned
128
+ reported "Invalid array index ... used as an array index"; the message now
129
+ says the element is undefined and how to supply it. It still names the
130
+ variable and the line, so an unknown string-typed machine data in a string
131
+ argument is a loud error rather than a silent empty string (#64).
132
+
133
+ - A quoted string passed to a numeric function (`SIN("abc")`) was silently
134
+ dropped from the argument list and surfaced as an arity error; it is now a
135
+ type error naming the string.
136
+
137
+ - Nested function calls parse in linear time: each function argument, and
138
+ each assignment right-hand side, is parsed once instead of once per grammar
139
+ alternative, so a deeply nested `SIN(SIN(...))` is instant (#78).
140
+
141
+ - The line fast path no longer declines whole programs because a few hundred
142
+ grammar-bound lines (procedure calls such as `SUPPORT_PIN_TRIGGER(2.5)`,
143
+ `MATERIAL_UPDATE`, `R1=R1+1`, `STOPRE`) are spread through a large file
144
+ (#69). Grammar-bound lines used to be parsed one at a time, each padded with
145
+ one newline per preceding line so that pest reported whole-file positions;
146
+ the padding cost O(line index) bytes per line, and a budget guard sent any
147
+ program over it to the whole-file parse. The grammar-bound lines are now
148
+ parsed in one pest run over a sparse copy of the program (only those lines
149
+ keep their text, every other line is empty), which gives the same whole-file
150
+ positions at O(input) bytes. The budget guard, the padded buffers and the
151
+ grammar-line ratio guard are gone: a program that is entirely grammar-bound
152
+ now costs the same as the whole-file parse (measured equal at 100k and 400k
153
+ lines), so every structure-free program takes the line path and the
154
+ whole-file parse is only used for programs with IF/WHILE/FOR/LOOP/REPEAT
155
+ blocks. On a 112 MB, 1.28M-line CAM
156
+ program with about 12,000 such lines the Python batch API went from 24.3 s
157
+ and 3.0 GiB peak RSS (whole-file parse) to 2.2 s and 1.3 GiB.
158
+
159
+ - The line fast path no longer gives three line shapes a different meaning
160
+ than the grammar does (#83). `N10X5` (a block number glued to the next word)
161
+ was read as block 10 plus `X5`, where the grammar reads one call named
162
+ `N10X5` and warns about the glued word; `TRANS=5` (a frame keyword before
163
+ `=`) was stored as a variable, where the grammar rejects the line; and
164
+ `X=5 AND Y1` (likewise `OR`, `XOR`, `B_AND`, `B_OR`, `B_XOR` after a
165
+ constant) became `X=5`, a call `AND` and `Y1`, where the grammar evaluates
166
+ `5 AND Y1`. The decoder now hands these lines to the grammar, so the result
167
+ no longer depends on which path a program takes. A seeded random-line test
168
+ compares every line the decoder claims against the grammar.
169
+
7
170
  ## [v0.2.7] - 2026-09-08
8
171
 
9
172
  ### 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.8"
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.8"
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.8
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,22 @@ 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
176
+ `python scripts/benchmark.py --input <file> --modes fresh --edits none
177
+ --threads 4 --samples 1 --batch-size 4096 --allow-undefined --axis-index-e 4
178
+ --axis-index-elx 5 --output tmp/memory.json`.
179
+
164
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.
165
181
 
166
182
  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
+ }