nc-gcode-interpreter 0.2.0__tar.gz → 0.2.2__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 (120) hide show
  1. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/.github/workflows/build-and-release.yml +7 -0
  2. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/CHANGELOG.md +60 -12
  3. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/Cargo.lock +1 -1
  4. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/Cargo.toml +1 -1
  5. nc_gcode_interpreter-0.2.2/Development.md +61 -0
  6. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/PKG-INFO +1 -1
  7. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/README.md +2 -0
  8. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/python/nc_gcode_interpreter/__init__.py +71 -2
  9. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/python/nc_gcode_interpreter/_internal.pyi +18 -1
  10. nc_gcode_interpreter-0.2.2/python/tests/test_batch_variables.py +205 -0
  11. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/python/tests/test_batches.py +53 -0
  12. nc_gcode_interpreter-0.2.2/python/tests/test_structured_errors.py +50 -0
  13. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/src/errors.rs +156 -32
  14. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/src/flatten.rs +318 -69
  15. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/src/interpret_rules.rs +208 -103
  16. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/src/interpreter.rs +220 -59
  17. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/src/lib.rs +166 -55
  18. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/src/line_driver.rs +98 -71
  19. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/src/main.rs +16 -23
  20. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/src/modal_groups.rs +41 -10
  21. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/src/output.rs +160 -12
  22. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/src/state.rs +40 -27
  23. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/src/structure_scan.rs +3 -8
  24. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/tests/cli.rs +4 -1
  25. nc_gcode_interpreter-0.2.0/Development.md +0 -46
  26. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/.gitignore +0 -0
  27. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/CONTRIBUTING.md +0 -0
  28. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/LICENSE +0 -0
  29. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/TODO.md +0 -0
  30. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/docs/sinumerik-execution-model.md +0 -0
  31. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/actual_position.csv +0 -0
  32. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/actual_position.mpf +0 -0
  33. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/arc.csv +0 -0
  34. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/arc.mpf +0 -0
  35. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/arrays.csv +0 -0
  36. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/arrays.mpf +0 -0
  37. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/axis_index_assignment.csv +0 -0
  38. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/axis_index_assignment.mpf +0 -0
  39. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/basic_math.csv +0 -0
  40. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/basic_math.mpf +0 -0
  41. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/case.csv +0 -0
  42. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/case.mpf +0 -0
  43. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/case_insensitive_variables.csv +0 -0
  44. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/case_insensitive_variables.mpf +0 -0
  45. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/custom_vars.csv +0 -0
  46. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/custom_vars.mpf +0 -0
  47. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/def_string.csv +0 -0
  48. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/def_string.mpf +0 -0
  49. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/defaults.csv +0 -0
  50. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/defaults.mpf +0 -0
  51. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/edge_cases.csv +0 -0
  52. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/edge_cases.mpf +0 -0
  53. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/flattening/flatten_demo.mpf +0 -0
  54. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/flattening/flatten_demo_flattened.csv +0 -0
  55. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/flattening/flatten_demo_raw.csv +0 -0
  56. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/for_loop.csv +0 -0
  57. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/for_loop.mpf +0 -0
  58. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/function_calls.csv +0 -0
  59. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/function_calls.mpf +0 -0
  60. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/goto.csv +0 -0
  61. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/goto.mpf +0 -0
  62. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/if_statement.csv +0 -0
  63. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/if_statement.mpf +0 -0
  64. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/increment.csv +0 -0
  65. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/increment.mpf +0 -0
  66. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/logic_operators.csv +0 -0
  67. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/logic_operators.mpf +0 -0
  68. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/loop.csv +0 -0
  69. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/loop.mpf +0 -0
  70. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/multiple_m_codes.csv +0 -0
  71. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/multiple_m_codes.mpf +0 -0
  72. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/precedence.csv +0 -0
  73. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/precedence.mpf +0 -0
  74. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/r_param.csv +0 -0
  75. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/r_param.mpf +0 -0
  76. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/repeat.csv +0 -0
  77. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/repeat.mpf +0 -0
  78. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/simple.csv +0 -0
  79. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/simple.mpf +0 -0
  80. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/spline.csv +0 -0
  81. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/spline.mpf +0 -0
  82. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/tool.csv +0 -0
  83. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/tool.mpf +0 -0
  84. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/trans.csv +0 -0
  85. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/trans.mpf +0 -0
  86. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/trans2.csv +0 -0
  87. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/trans2.mpf +0 -0
  88. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/trans_ic.csv +0 -0
  89. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/trans_ic.mpf +0 -0
  90. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/trans_reset.csv +0 -0
  91. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/trans_reset.mpf +0 -0
  92. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/trig.csv +0 -0
  93. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/trig.mpf +0 -0
  94. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/variables.csv +0 -0
  95. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/variables.mpf +0 -0
  96. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/ggroups/generate_g_commands.py +0 -0
  97. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/pyproject.toml +0 -0
  98. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/python/example/minimal.py +0 -0
  99. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/python/example/streaming.py +0 -0
  100. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/python/nc_gcode_interpreter/cli.py +0 -0
  101. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/python/nc_gcode_interpreter/ggroups.json +0 -0
  102. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/python/nc_gcode_interpreter/py.typed +0 -0
  103. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/python/nc_gcode_interpreter/viz.py +0 -0
  104. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/python/tests/test_arithmetic_functions.py +0 -0
  105. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/python/tests/test_diagnostics.py +0 -0
  106. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/python/tests/test_expected_output.py +0 -0
  107. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/python/tests/test_field_defects.py +0 -0
  108. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/python/tests/test_flags.py +0 -0
  109. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/python/tests/test_flatten.py +0 -0
  110. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/python/tests/test_g_groups.py +0 -0
  111. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/python/tests/test_g_vocabulary.py +0 -0
  112. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/python/tests/test_jumps.py +0 -0
  113. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/python/tests/test_real_comparisons.py +0 -0
  114. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/python/tests/test_stage1.py +0 -0
  115. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/python/tests/test_streaming.py +0 -0
  116. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/python/tests/test_unsupported.py +0 -0
  117. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/rustfmt.toml +0 -0
  118. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/src/grammar.pest +0 -0
  119. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/src/types.rs +0 -0
  120. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/uv.lock +0 -0
@@ -43,7 +43,14 @@ jobs:
43
43
  runs-on: ubuntu-latest
44
44
  steps:
45
45
  - uses: actions/checkout@v5
46
+ # Pin the toolchain and explicitly pull in rustfmt so `cargo fmt --check`
47
+ # does not depend on whatever components the runner image ships with.
48
+ - uses: dtolnay/rust-toolchain@stable
49
+ with:
50
+ components: rustfmt
46
51
  - uses: Swatinem/rust-cache@v2
52
+ - name: Check Rust formatting
53
+ run: cargo fmt --check
47
54
  - name: Run Rust unit and integration tests
48
55
  run: cargo test
49
56
 
@@ -4,20 +4,58 @@ 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.0] - 2026-07-07
7
+ ## [v0.2.2] - 2026-07-07
8
8
 
9
- ### Changed
9
+ ### Added
10
10
 
11
- - Rust-side polars is gone. The Table -> Python DataFrame handoff no longer
12
- builds a polars DataFrame in Rust (via `pyo3-polars`); it builds an Arrow
13
- record batch with the minimal `arrow-array`/`arrow-schema`/`arrow-data`
14
- crates and hands it to Python zero-copy through the Arrow PyCapsule
15
- interface (`__arrow_c_array__`), where `pl.DataFrame(...)` wraps it. The
16
- Python API is unchanged (still returns `polars.DataFrame`), needs no
17
- `pyarrow`, and performance is unchanged. This drops ~60 crates from the
18
- `python`-feature build (127 -> 64), cutting a clean release build ~4x
19
- (83s -> 21s), and bumps PyO3 0.28 -> 0.29 (resolving the RUSTSEC pyo3
20
- advisories).
11
+ - Variable-change events on the batch path: `nc_to_batches(...,
12
+ include_variables=True)` exposes, once exhausted, a sparse `variable_events`
13
+ DataFrame (`row_idx` / `name_id` / `value`) plus a `variable_names` list -
14
+ the batch-path twin of the per-row `variables` dict `nc_to_rows` already
15
+ yields. `row_idx` is the output-row index a change is seen at (a change on a
16
+ variable-only block is attributed to the next output row), so replaying the
17
+ events reconstructs the symbol table at any row. Off by default (no cost).
18
+ - `string_table` in the interpreter state dict: `DEF STRING` variables now
19
+ round-trip into `.state` / the `nc_to_dataframe` state tuple as
20
+ `state["string_table"]` (`dict[str, str]`), alongside the existing `axes` /
21
+ `symbol_table` / `translation` numeric tables (previously omitted).
22
+
23
+ ## [v0.2.1] - 2026-07-07
24
+
25
+ ### Added
26
+
27
+ - Optional `line_no` output column on the batch/dataframe path, enabled with
28
+ `include_line_numbers=True` on `nc_to_dataframe` / `nc_to_batches` (default
29
+ `False`, so the output schema is unchanged unless you ask for it). When
30
+ enabled it prepends a leading `Int64` column giving the 1-based source line
31
+ each output row came from - previously only the streaming `nc_to_rows`
32
+ exposed it. Loops repeat the value and jumps make it non-monotonic, matching
33
+ `nc_to_rows` row-for-row; flatten-generated samples keep the originating
34
+ block's line number; `dataframe_to_nc` ignores it (source provenance, not an
35
+ emittable word). Concatenating batches reconstructs the same per-row
36
+ `line_no` as the whole-file dataframe (#45)
37
+ - Structured error locations: parse/interpret failures now raise
38
+ `nc_gcode_interpreter.NcError` (a `ValueError` subclass, so existing
39
+ `except ValueError` keeps working) carrying the position as data - `.line`,
40
+ `.column` (syntax errors), `.context`, and `.line_text` attributes (each an
41
+ int / str or `None`) - so a caller (e.g. an editor) can locate the offending
42
+ token without regex-parsing the message. `str(err)` is unchanged
43
+
44
+ ### Fixed
45
+
46
+ - Arithmetic-function arity: added a regression test pinning that every
47
+ `SIN`/`COS`/`ATAN2`/`BOUND`/... call already validates its argument count
48
+ (`check_args`) before indexing `args[..]`, returning
49
+ `ParsingError::InvalidFunctionArity` instead of panicking on a wrong-arity
50
+ call (#16).
51
+
52
+ ### Documented
53
+
54
+ - Pinned iterator-drop -> `ParsingError::StreamClosed` as the supported
55
+ cancel contract for `nc_to_rows`/`nc_to_batches` (README, docstrings) and
56
+ noted the release-profile `strip`/`lto` tradeoff in `Development.md` (#49).
57
+
58
+ ## [v0.2.0] - 2026-07-07
21
59
 
22
60
  ### Added
23
61
 
@@ -95,6 +133,16 @@ released to PyPI.
95
133
 
96
134
  ### Changed
97
135
 
136
+ - Rust-side polars is gone. The Table -> Python DataFrame handoff no longer
137
+ builds a polars DataFrame in Rust (via `pyo3-polars`); it builds an Arrow
138
+ record batch with the minimal `arrow-array`/`arrow-schema`/`arrow-data`
139
+ crates and hands it to Python zero-copy through the Arrow PyCapsule
140
+ interface (`__arrow_c_array__`), where `pl.DataFrame(...)` wraps it. The
141
+ Python API is unchanged (still returns `polars.DataFrame`), needs no
142
+ `pyarrow`, and performance is unchanged. This drops ~60 crates from the
143
+ `python`-feature build (127 -> 64), cutting a clean release build ~4x
144
+ (83s -> 21s), and bumps PyO3 0.28 -> 0.29 (resolving the RUSTSEC pyo3
145
+ advisories).
98
146
  - **Breaking:** `I`/`J`/`K`/`CR` are now treated as arc interpolation-parameter
99
147
  block addresses (output columns), so they can no longer be used as user
100
148
  variable names: `I=5` followed by `X=I+1` was a variable read before and is
@@ -506,7 +506,7 @@ checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4"
506
506
 
507
507
  [[package]]
508
508
  name = "nc-gcode-interpreter"
509
- version = "0.2.0"
509
+ version = "0.2.2"
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.0"
3
+ version = "0.2.2"
4
4
  edition = "2021"
5
5
  readme = "README.md"
6
6
 
@@ -0,0 +1,61 @@
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
+ ### Release profile
34
+
35
+ `[profile.release]` currently sets no `strip`/`lto` overrides. `strip = true` was
36
+ tried and dropped (`e1ff63a`): cargo runs the *host* `strip` binary even on a
37
+ cross-compiled artifact, so stripping the cross-built macOS x86_64 dylib with
38
+ the arm64 host `strip` produced a wheel PyPI rejected as "not a zipfile" -
39
+ stripping was reverted to unblock the release. It was a secondary size win
40
+ (mostly orthogonal to the polars removal that motivated it) and can be
41
+ revisited via maturin's cross-aware `--strip` flag instead of a Cargo
42
+ profile setting.
43
+
44
+ `lto` remains an untried, plausible speed knob (doesn't invoke an external
45
+ tool, so it isn't blocked by the cross-compile issue above) - just not yet
46
+ evaluated for its extra build time across the wheel matrix.
47
+
48
+
49
+ ## Super simple test
50
+
51
+ There are a bunch of csv files in the examples directory. To test the tool on all of them (use git to check changes)
52
+
53
+ ```bash
54
+ 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 {}
55
+ ```
56
+
57
+ ## python test
58
+
59
+ ```bash
60
+ maturin develop --release --uv && pytest
61
+ ```
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nc-gcode-interpreter
3
- Version: 0.2.0
3
+ Version: 0.2.2
4
4
  Classifier: Programming Language :: Rust
5
5
  Classifier: Programming Language :: Python :: Implementation :: CPython
6
6
  Classifier: Programming Language :: Python :: 3.12
@@ -161,6 +161,8 @@ 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
+ **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
+
164
166
  Additionally, conversion from a Polars DataFrame back to an MPF (NC) program is also supported:
165
167
 
166
168
  ```bash
@@ -3,6 +3,7 @@ import os
3
3
  import polars as pl
4
4
  from ._internal import nc_to_rows as _nc_to_rows
5
5
  from ._internal import nc_to_batches as _nc_to_batches
6
+ from ._internal import NcError
6
7
  from ._internal import __doc__ # noqa: F401
7
8
  import json
8
9
  from pathlib import Path
@@ -21,6 +22,7 @@ __all__ = [
21
22
  "nc_to_batches",
22
23
  "sanitize_dataframe",
23
24
  "dataframe_to_nc",
25
+ "NcError",
24
26
  ]
25
27
 
26
28
  # Batch size nc_to_dataframe uses internally when draining the batch stream it
@@ -60,6 +62,7 @@ def nc_to_dataframe(
60
62
  axis_index_map: dict[str, int] | None = None,
61
63
  allow_undefined_variables: bool = False,
62
64
  flatten_tolerance: float | None = None,
65
+ include_line_numbers: bool = False,
63
66
  ) -> tuple[pl.DataFrame, dict]:
64
67
  """
65
68
  Parses Sinumerik-flavored NC G-code and converts it into a Polars DataFrame along with the final state.
@@ -93,6 +96,12 @@ def nc_to_dataframe(
93
96
  deviation (in path units, i.e. mm) of the true curve. The interpolation
94
97
  parameters (I/J/K/CR, PW/SD/PL) are consumed and do not appear in the
95
98
  output. Default None (curves pass through untouched).
99
+ include_line_numbers: bool, optional
100
+ If True, prepend a `line_no` (Int64) column giving the 1-based source
101
+ line each output row came from - repeated under a loop, non-monotonic
102
+ across a jump, matching the streaming `nc_to_rows`. Never
103
+ forward-filled; `dataframe_to_nc` ignores it. Default False (column
104
+ absent, output schema unchanged).
96
105
 
97
106
  Returns:
98
107
  --------
@@ -142,6 +151,7 @@ def nc_to_dataframe(
142
151
  allow_undefined_variables,
143
152
  input_is_path,
144
153
  flatten_tolerance,
154
+ include_line_numbers,
145
155
  )
146
156
  # pl.DataFrame wraps each Arrow record batch via __arrow_c_array__ (polars
147
157
  # >= 1.3), no pyarrow needed. Exhaust the iterator before reading .state.
@@ -245,6 +255,13 @@ def sanitize_dataframe(
245
255
  pl.DataFrame
246
256
  The sanitized DataFrame ready for analysis or conversion back to G-code.
247
257
  """
258
+ # `line_no` (present only when interpreted with include_line_numbers=True)
259
+ # is source-provenance metadata, not an emittable G-code word; drop it up
260
+ # front so it is never cast/forward-filled or written back out by
261
+ # dataframe_to_nc.
262
+ if "line_no" in df.columns:
263
+ df = df.drop("line_no")
264
+
248
265
  modal = [g["short_name"] for g in GGroups.g_groups if g["effectiveness"] == "modal"]
249
266
  non_modal = [g["short_name"] for g in GGroups.g_groups if g["effectiveness"] != "modal"]
250
267
  known_axes = [
@@ -415,6 +432,13 @@ def nc_to_rows(
415
432
  an anonymous iterator drops it; a stored iterator keeps the worker
416
433
  alive (parked on the bounded channel) until it is deleted or
417
434
  garbage-collected.
435
+
436
+ Dropping the iterator is the **supported cancel contract**: the
437
+ background thread's next attempted send hits the now-closed channel and
438
+ returns internally via ``ParsingError::StreamClosed``, so it unwinds
439
+ promptly instead of finishing the rest of the program. There is no
440
+ separate ``.cancel()``/``.close()`` - drop is the whole mechanism.
441
+
418
442
  Errors raise ``ValueError`` from ``next()`` when reached. After the
419
443
  iterator is exhausted, its ``state`` attribute holds the final
420
444
  interpreter state (axes, symbol_table, translation).
@@ -453,8 +477,9 @@ class _BatchIterator:
453
477
 
454
478
  Wraps the Rust batch iterator, which yields each batch as an Arrow record
455
479
  batch built column-wise in Rust and handed over the Arrow PyCapsule
456
- interface (``__arrow_c_array__``, via pyo3-arrow) - a zero-copy transfer
457
- with no Python list of primitives materialized. ``pl.DataFrame`` wraps that
480
+ interface (``__arrow_c_array__``, exported directly with the minimal
481
+ arrow-rs crates - no pyo3-arrow/pyo3-polars) - a zero-copy transfer with
482
+ no Python list of primitives materialized. ``pl.DataFrame`` wraps that
458
483
  capsule directly (polars >= 1.3), so no ``pyarrow`` is involved. After
459
484
  exhaustion its ``state`` attribute holds the final interpreter state (axes,
460
485
  symbol_table, translation), like the iterator returned by :func:`nc_to_rows`.
@@ -473,6 +498,29 @@ class _BatchIterator:
473
498
  def state(self) -> dict | None:
474
499
  return self._inner.state
475
500
 
501
+ @property
502
+ def variable_events(self) -> "pl.DataFrame | None":
503
+ """Sparse variable-change events (``include_variables=True``).
504
+
505
+ A :class:`polars.DataFrame` with columns ``row_idx`` (the output-row
506
+ index the change is seen at), ``name_id`` (index into
507
+ :attr:`variable_names`) and ``value``. ``None`` unless the iterator was
508
+ created with ``include_variables=True``. Available once exhausted.
509
+ Replaying the events in order (decoding ``name_id`` via
510
+ :attr:`variable_names`) reconstructs the symbol table at any row, the
511
+ batch-path twin of the per-row ``variables`` dict ``nc_to_rows`` yields.
512
+ """
513
+ batch = self._inner.variable_events
514
+ return None if batch is None else pl.DataFrame(batch)
515
+
516
+ @property
517
+ def variable_names(self) -> list[str] | None:
518
+ """Variable names ``variable_events.name_id`` indexes into.
519
+
520
+ ``None`` unless ``include_variables=True``; available once exhausted.
521
+ """
522
+ return self._inner.variable_names
523
+
476
524
 
477
525
  def nc_to_batches(
478
526
  input: "TextFileLike | str | os.PathLike",
@@ -485,6 +533,8 @@ def nc_to_batches(
485
533
  axis_index_map: dict[str, int] | None = None,
486
534
  allow_undefined_variables: bool = False,
487
535
  flatten_tolerance: float | None = None,
536
+ include_line_numbers: bool = False,
537
+ include_variables: bool = False,
488
538
  ) -> _BatchIterator:
489
539
  """Interpret an NC program into a stream of columnar polars DataFrames.
490
540
 
@@ -504,6 +554,23 @@ def nc_to_batches(
504
554
  The returned iterator exposes a ``state`` attribute holding the final
505
555
  interpreter state once it is exhausted, like :func:`nc_to_rows`.
506
556
 
557
+ Like :func:`nc_to_rows`, this runs on a background thread behind a
558
+ bounded channel, and dropping the iterator is the supported way to
559
+ cancel: the next attempted send hits the closed channel and returns
560
+ internally via ``ParsingError::StreamClosed``, unwinding the thread
561
+ promptly instead of finishing the rest of the program.
562
+
563
+ With ``include_line_numbers=True`` every batch gains a leading ``line_no``
564
+ (Int64) column: the 1-based source line each row came from (repeated under
565
+ a loop, non-monotonic across a jump), never forward-filled. Default False
566
+ leaves the schema unchanged.
567
+
568
+ With ``include_variables=True`` the iterator additionally exposes
569
+ ``variable_events`` (a sparse :class:`polars.DataFrame` of ``row_idx`` /
570
+ ``name_id`` / ``value``) and ``variable_names`` once exhausted — the
571
+ batch-path equivalent of the per-row ``variables`` dict :func:`nc_to_rows`
572
+ yields; replaying the events reconstructs the symbol table at any row.
573
+
507
574
  Example:
508
575
  --------
509
576
  >>> batches = nc_to_batches("G1 X10\\nX20 Y5", batch_size=1)
@@ -526,5 +593,7 @@ def nc_to_batches(
526
593
  allow_undefined_variables,
527
594
  input_is_path,
528
595
  flatten_tolerance,
596
+ include_line_numbers,
597
+ include_variables,
529
598
  )
530
599
  return _BatchIterator(inner)
@@ -5,6 +5,15 @@ from typing import Any, Iterator, Optional, List, Dict, Tuple
5
5
  # wrapper imports are declared here. Signatures mirror the `#[pyo3(signature)]`
6
6
  # defaults in src/lib.rs.
7
7
 
8
+ class NcError(ValueError):
9
+ """NC parse/interpret error carrying structured location data. Subclasses
10
+ ValueError so `except ValueError` keeps working."""
11
+
12
+ line: Optional[int]
13
+ column: Optional[int]
14
+ context: Optional[str]
15
+ line_text: Optional[str]
16
+
8
17
  def nc_to_rows(
9
18
  input: str,
10
19
  initial_state: Optional[str] = None,
@@ -33,8 +42,16 @@ def nc_to_batches(
33
42
  allow_undefined_variables: bool = False,
34
43
  input_is_path: bool = False,
35
44
  flatten_tolerance: Optional[float] = None,
45
+ include_line_numbers: bool = False,
46
+ include_variables: bool = False,
36
47
  ) -> Any:
37
- """Interpret an NC program into an iterator of columnar polars DataFrames."""
48
+ """Interpret an NC program into an iterator of columnar polars DataFrames.
49
+
50
+ The returned iterator exposes ``state`` (dict with ``axes``,
51
+ ``symbol_table``, ``translation`` and ``string_table``) once exhausted, and
52
+ - when ``include_variables`` is set - ``variable_events`` (an Arrow batch of
53
+ ``row_idx`` / ``name_id`` / ``value``) and ``variable_names`` (list[str]).
54
+ """
38
55
  ...
39
56
 
40
57
  __all__ = ["nc_to_rows", "nc_to_batches"]
@@ -0,0 +1,205 @@
1
+ """Batch-path variable-change events (#46) + `string_table` in the state dict.
2
+
3
+ The streaming `nc_to_rows(include_variables=True)` path yields a per-row
4
+ `variables` dict; the batch path (`nc_to_batches` / `nc_to_dataframe`) now
5
+ surfaces the same information as a sparse side-table (`row_idx`, `name_id`,
6
+ `value`) plus a `variable_names` list. These tests pin (a) parity of the
7
+ ordered assignment sequence between the two paths and (b) that `DEF STRING`
8
+ variables round-trip into the returned state dict.
9
+ """
10
+
11
+ import polars as pl
12
+
13
+ from nc_gcode_interpreter import nc_to_batches, nc_to_dataframe, nc_to_rows
14
+
15
+
16
+ def _stream_sequence(program: str) -> list[tuple[str, float]]:
17
+ """Flat, ordered `(name, value)` assignment sequence from the stream."""
18
+ return [
19
+ (name, value)
20
+ for _line, _row, variables in nc_to_rows(program, include_variables=True)
21
+ for name, value in variables.items()
22
+ ]
23
+
24
+
25
+ def _batch_sequence(it) -> list[tuple[str, float]]:
26
+ """Flat, ordered `(name, value)` sequence decoded from the batch events."""
27
+ events = it.variable_events
28
+ names = it.variable_names
29
+ return [
30
+ (names[name_id], value)
31
+ for name_id, value in zip(events["name_id"], events["value"])
32
+ ]
33
+
34
+
35
+ def test_batch_variable_events_match_stream_simple():
36
+ program = "\n".join(
37
+ [
38
+ "DEF REAL Q=2.5", # variable-only
39
+ "R1=0", # variable-only
40
+ "X=R1", # output row, no change
41
+ "X=Q", # output row, no change
42
+ ]
43
+ )
44
+ it = nc_to_batches(program, include_variables=True)
45
+ list(it) # exhaust to populate variable_events / state
46
+ assert _batch_sequence(it) == _stream_sequence(program)
47
+
48
+
49
+ def test_batch_variable_events_match_stream_while_loop():
50
+ # A WHILE loop where R1 changes once per iteration: the per-iteration
51
+ # counter events must appear on the batch path exactly as they do on the
52
+ # stream, in the same order.
53
+ program = "\n".join(
54
+ [
55
+ "DEF REAL Q=2.5",
56
+ "R1=0",
57
+ "WHILE R1<3",
58
+ "X=R1 Q=Q*2", # output row that ALSO assigns a variable
59
+ "R1=R1+1", # variable-only, per iteration
60
+ "ENDWHILE",
61
+ "X=Q",
62
+ ]
63
+ )
64
+ it = nc_to_batches(program, include_variables=True)
65
+ list(it)
66
+ assert _batch_sequence(it) == _stream_sequence(program)
67
+
68
+
69
+ def test_batch_variable_events_row_idx_reconstructs_symbol_table():
70
+ # Replaying every event reconstructs the final symbol table (minus the
71
+ # built-in TRUE/FALSE), mirroring the streaming accumulation invariant.
72
+ program = "\n".join(
73
+ [
74
+ "DEF REAL Q=1",
75
+ "R1=0",
76
+ "WHILE R1<3",
77
+ "X=R1 Q=Q*2",
78
+ "R1=R1+1",
79
+ "ENDWHILE",
80
+ ]
81
+ )
82
+ it = nc_to_batches(program, include_variables=True)
83
+ list(it)
84
+ accumulated: dict[str, float] = {}
85
+ for name, value in _batch_sequence(it):
86
+ accumulated[name] = value
87
+ symbol_table = dict(it.state["symbol_table"])
88
+ for builtin in ("TRUE", "FALSE"):
89
+ symbol_table.pop(builtin)
90
+ assert accumulated == symbol_table
91
+
92
+ # row_idx is monotonic non-decreasing (events arrive in program order) and
93
+ # bounded by the number of output rows.
94
+ events = it.variable_events
95
+ row_idx = events["row_idx"].to_list()
96
+ assert row_idx == sorted(row_idx)
97
+
98
+
99
+ def test_batch_variable_events_row_idx_aligns_with_output_rows():
100
+ # A change on a variable-only block is attributed to the NEXT output row;
101
+ # a change on an output row gets that row's own index.
102
+ program = "\n".join(
103
+ [
104
+ "R1=0", # variable-only, before output row 0
105
+ "X=R1", # output row 0
106
+ "R1=1", # variable-only, before output row 1
107
+ "X=R1", # output row 1
108
+ ]
109
+ )
110
+ it = nc_to_batches(program, include_variables=True)
111
+ list(it)
112
+ events = it.variable_events
113
+ names = it.variable_names
114
+ decoded = [
115
+ (row_idx, names[name_id], value)
116
+ for row_idx, name_id, value in zip(
117
+ events["row_idx"], events["name_id"], events["value"]
118
+ )
119
+ ]
120
+ assert decoded == [(0, "R1", 0.0), (1, "R1", 1.0)]
121
+
122
+
123
+ def test_batch_variable_events_absent_without_flag():
124
+ it = nc_to_batches("R1=5\nX=R1")
125
+ list(it)
126
+ assert it.variable_events is None
127
+ assert it.variable_names is None
128
+
129
+
130
+ def test_batch_variable_events_empty_when_no_assignments():
131
+ it = nc_to_batches("G1 X10\nX20 Y5", include_variables=True)
132
+ list(it)
133
+ assert isinstance(it.variable_events, pl.DataFrame)
134
+ assert it.variable_events.height == 0
135
+ assert it.variable_names == []
136
+
137
+
138
+ def test_string_table_round_trips_through_state_dict():
139
+ program = "\n".join(
140
+ [
141
+ "DEF STRING[16] MSG",
142
+ 'MSG="HELLO WORLD"',
143
+ "DEF STRING[8] TAG",
144
+ 'TAG="ABC"',
145
+ "G1 X1",
146
+ ]
147
+ )
148
+ _df, state = nc_to_dataframe(program)
149
+ assert state["string_table"] == {"MSG": "HELLO WORLD", "TAG": "ABC"}
150
+ # The existing numeric sub-tables are untouched by the new key.
151
+ assert set(state) == {"axes", "symbol_table", "translation", "string_table"}
152
+ # The string variable never leaks into the numeric symbol table.
153
+ assert "MSG" not in state["symbol_table"]
154
+
155
+
156
+ def test_string_table_on_streaming_and_batch_iterators():
157
+ program = 'DEF STRING[8] MSG\nMSG="HI"\nG1 X1'
158
+ rows = nc_to_rows(program)
159
+ list(rows)
160
+ assert rows.state["string_table"] == {"MSG": "HI"}
161
+
162
+ it = nc_to_batches(program)
163
+ list(it)
164
+ assert it.state["string_table"] == {"MSG": "HI"}
165
+
166
+
167
+ def test_line_numbers_and_variables_compose_on_the_batch_path():
168
+ # include_line_numbers (#45) and include_variables (#46) share the batch
169
+ # path and were merged independently; requesting both at once must give the
170
+ # leading `line_no` column AND the sparse variable-event side-table, with
171
+ # `row_idx` still aligned to the emitted (line_no-carrying) output rows.
172
+ program = "\n".join(
173
+ [
174
+ "R1=0", # variable-only, before output row 0
175
+ "WHILE R1<3",
176
+ "X=R1", # output row (loop body, source line 3)
177
+ "R1=R1+1", # variable-only
178
+ "ENDWHILE",
179
+ "X9", # output row, source line 6
180
+ ]
181
+ )
182
+ it = nc_to_batches(
183
+ program,
184
+ batch_size=1_000_000,
185
+ include_line_numbers=True,
186
+ include_variables=True,
187
+ )
188
+ df = pl.concat(list(it), how="diagonal")
189
+
190
+ # line_no leads and tracks source lines (loop body repeats line 3).
191
+ assert df.columns[0] == "line_no"
192
+ assert df["line_no"].to_list() == [3, 3, 3, 6]
193
+ assert df["X"].to_list() == [0.0, 1.0, 2.0, 9.0]
194
+
195
+ # Variable events still decode independently of the line_no column.
196
+ names = it.variable_names
197
+ decoded = [
198
+ (row_idx, names[name_id], value)
199
+ for row_idx, name_id, value in zip(
200
+ it.variable_events["row_idx"],
201
+ it.variable_events["name_id"],
202
+ it.variable_events["value"],
203
+ )
204
+ ]
205
+ assert decoded == [(0, "R1", 0.0), (1, "R1", 1.0), (2, "R1", 2.0), (3, "R1", 3.0)]
@@ -155,3 +155,56 @@ def test_path_input_batches(tmp_path):
155
155
  df, _ = nc_to_dataframe(program)
156
156
  frames = list(nc_to_batches(mpf, batch_size=2))
157
157
  assert_frame_equal(pl.concat(frames, how="diagonal").select(df.columns), df)
158
+
159
+
160
+ def test_include_line_numbers_is_opt_in():
161
+ """`line_no` is off by default and, when enabled, is a leading Int64 column
162
+ that repeats under a loop and is non-monotonic across a jump - matching the
163
+ streaming `nc_to_rows` row-for-row and reconstructible across batches."""
164
+ prog = "R1=0\nWHILE R1<2\nX=R1 F100\nR1=R1+1\nENDWHILE\nX9\n"
165
+
166
+ # Default off: schema unchanged, no line_no column anywhere.
167
+ df_off, _ = nc_to_dataframe(prog)
168
+ assert "line_no" not in df_off.columns
169
+ off_batches = list(nc_to_batches(prog, batch_size=1))
170
+ assert all("line_no" not in f.columns for f in off_batches)
171
+
172
+ # Opt-in: leading Int64 line_no with the loop's repeated source lines.
173
+ df_on, _ = nc_to_dataframe(prog, include_line_numbers=True)
174
+ assert df_on.columns[0] == "line_no"
175
+ assert df_on["line_no"].dtype == pl.Int64
176
+ assert df_on["line_no"].to_list() == [3, 3, 6]
177
+ # Dropping line_no reproduces the default-off frame exactly.
178
+ assert_frame_equal(df_on.drop("line_no"), df_off)
179
+
180
+ # Batches carry it too and concatenate back to the whole-file column.
181
+ on_batches = list(nc_to_batches(prog, batch_size=1, include_line_numbers=True))
182
+ assert pl.concat(on_batches, how="diagonal")["line_no"].to_list() == [3, 3, 6]
183
+
184
+
185
+ def test_include_line_numbers_matches_streaming_under_jumps():
186
+ """A GOTO reorders execution; the batch `line_no` column must match what
187
+ the streaming `nc_to_rows` yields, line for line."""
188
+ from nc_gcode_interpreter import nc_to_rows
189
+
190
+ prog = "N10 X1\nGOTOF 40\nN30 X999\nN40 X4\n"
191
+ df, _ = nc_to_dataframe(prog, include_line_numbers=True)
192
+ streamed = [line_no for line_no, _row in nc_to_rows(prog)]
193
+ assert df["line_no"].to_list() == streamed == [1, 4]
194
+
195
+
196
+ def test_include_line_numbers_survives_flattening():
197
+ """Flatten-generated samples keep the originating block's source line, so a
198
+ flattened arc/spline block yields many rows all carrying its line_no - and
199
+ still matches the streaming path row-for-row."""
200
+ from nc_gcode_interpreter import nc_to_rows
201
+
202
+ program = "G1 X0 Y0 F1000\nG2 X100 Y0 I50 J0\nG1 X100 Y50\n"
203
+ df, _ = nc_to_dataframe(program, flatten_tolerance=0.1, include_line_numbers=True)
204
+ assert df.columns[0] == "line_no"
205
+ assert df["line_no"].null_count() == 0
206
+ per_line = df["line_no"].to_list()
207
+ # The arc on line 2 expands to many G1 samples, every one tagged line 2.
208
+ assert per_line.count(2) > 5
209
+ stream_lines = [line for line, *_ in nc_to_rows(program, flatten_tolerance=0.1)]
210
+ assert per_line == stream_lines