nc-gcode-interpreter 0.2.0__tar.gz → 0.2.1__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 (119) hide show
  1. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.1}/.github/workflows/build-and-release.yml +7 -0
  2. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.1}/CHANGELOG.md +44 -12
  3. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.1}/Cargo.lock +1 -1
  4. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.1}/Cargo.toml +1 -1
  5. nc_gcode_interpreter-0.2.1/Development.md +61 -0
  6. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.1}/PKG-INFO +1 -1
  7. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.1}/README.md +2 -0
  8. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.1}/python/nc_gcode_interpreter/__init__.py +40 -2
  9. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.1}/python/nc_gcode_interpreter/_internal.pyi +10 -0
  10. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.1}/python/tests/test_batches.py +53 -0
  11. nc_gcode_interpreter-0.2.1/python/tests/test_structured_errors.py +50 -0
  12. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.1}/src/errors.rs +156 -32
  13. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.1}/src/flatten.rs +318 -69
  14. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.1}/src/interpret_rules.rs +208 -103
  15. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.1}/src/interpreter.rs +206 -59
  16. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.1}/src/lib.rs +82 -40
  17. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.1}/src/line_driver.rs +98 -71
  18. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.1}/src/main.rs +16 -23
  19. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.1}/src/modal_groups.rs +41 -10
  20. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.1}/src/output.rs +48 -10
  21. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.1}/src/state.rs +15 -19
  22. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.1}/src/structure_scan.rs +3 -8
  23. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.1}/tests/cli.rs +4 -1
  24. nc_gcode_interpreter-0.2.0/Development.md +0 -46
  25. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.1}/.gitignore +0 -0
  26. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.1}/CONTRIBUTING.md +0 -0
  27. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.1}/LICENSE +0 -0
  28. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.1}/TODO.md +0 -0
  29. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.1}/docs/sinumerik-execution-model.md +0 -0
  30. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.1}/examples/actual_position.csv +0 -0
  31. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.1}/examples/actual_position.mpf +0 -0
  32. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.1}/examples/arc.csv +0 -0
  33. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.1}/examples/arc.mpf +0 -0
  34. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.1}/examples/arrays.csv +0 -0
  35. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.1}/examples/arrays.mpf +0 -0
  36. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.1}/examples/axis_index_assignment.csv +0 -0
  37. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.1}/examples/axis_index_assignment.mpf +0 -0
  38. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.1}/examples/basic_math.csv +0 -0
  39. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.1}/examples/basic_math.mpf +0 -0
  40. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.1}/examples/case.csv +0 -0
  41. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.1}/examples/case.mpf +0 -0
  42. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.1}/examples/case_insensitive_variables.csv +0 -0
  43. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.1}/examples/case_insensitive_variables.mpf +0 -0
  44. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.1}/examples/custom_vars.csv +0 -0
  45. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.1}/examples/custom_vars.mpf +0 -0
  46. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.1}/examples/def_string.csv +0 -0
  47. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.1}/examples/def_string.mpf +0 -0
  48. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.1}/examples/defaults.csv +0 -0
  49. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.1}/examples/defaults.mpf +0 -0
  50. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.1}/examples/edge_cases.csv +0 -0
  51. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.1}/examples/edge_cases.mpf +0 -0
  52. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.1}/examples/flattening/flatten_demo.mpf +0 -0
  53. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.1}/examples/flattening/flatten_demo_flattened.csv +0 -0
  54. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.1}/examples/flattening/flatten_demo_raw.csv +0 -0
  55. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.1}/examples/for_loop.csv +0 -0
  56. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.1}/examples/for_loop.mpf +0 -0
  57. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.1}/examples/function_calls.csv +0 -0
  58. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.1}/examples/function_calls.mpf +0 -0
  59. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.1}/examples/goto.csv +0 -0
  60. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.1}/examples/goto.mpf +0 -0
  61. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.1}/examples/if_statement.csv +0 -0
  62. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.1}/examples/if_statement.mpf +0 -0
  63. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.1}/examples/increment.csv +0 -0
  64. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.1}/examples/increment.mpf +0 -0
  65. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.1}/examples/logic_operators.csv +0 -0
  66. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.1}/examples/logic_operators.mpf +0 -0
  67. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.1}/examples/loop.csv +0 -0
  68. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.1}/examples/loop.mpf +0 -0
  69. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.1}/examples/multiple_m_codes.csv +0 -0
  70. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.1}/examples/multiple_m_codes.mpf +0 -0
  71. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.1}/examples/precedence.csv +0 -0
  72. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.1}/examples/precedence.mpf +0 -0
  73. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.1}/examples/r_param.csv +0 -0
  74. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.1}/examples/r_param.mpf +0 -0
  75. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.1}/examples/repeat.csv +0 -0
  76. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.1}/examples/repeat.mpf +0 -0
  77. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.1}/examples/simple.csv +0 -0
  78. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.1}/examples/simple.mpf +0 -0
  79. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.1}/examples/spline.csv +0 -0
  80. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.1}/examples/spline.mpf +0 -0
  81. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.1}/examples/tool.csv +0 -0
  82. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.1}/examples/tool.mpf +0 -0
  83. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.1}/examples/trans.csv +0 -0
  84. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.1}/examples/trans.mpf +0 -0
  85. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.1}/examples/trans2.csv +0 -0
  86. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.1}/examples/trans2.mpf +0 -0
  87. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.1}/examples/trans_ic.csv +0 -0
  88. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.1}/examples/trans_ic.mpf +0 -0
  89. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.1}/examples/trans_reset.csv +0 -0
  90. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.1}/examples/trans_reset.mpf +0 -0
  91. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.1}/examples/trig.csv +0 -0
  92. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.1}/examples/trig.mpf +0 -0
  93. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.1}/examples/variables.csv +0 -0
  94. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.1}/examples/variables.mpf +0 -0
  95. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.1}/ggroups/generate_g_commands.py +0 -0
  96. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.1}/pyproject.toml +0 -0
  97. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.1}/python/example/minimal.py +0 -0
  98. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.1}/python/example/streaming.py +0 -0
  99. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.1}/python/nc_gcode_interpreter/cli.py +0 -0
  100. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.1}/python/nc_gcode_interpreter/ggroups.json +0 -0
  101. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.1}/python/nc_gcode_interpreter/py.typed +0 -0
  102. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.1}/python/nc_gcode_interpreter/viz.py +0 -0
  103. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.1}/python/tests/test_arithmetic_functions.py +0 -0
  104. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.1}/python/tests/test_diagnostics.py +0 -0
  105. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.1}/python/tests/test_expected_output.py +0 -0
  106. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.1}/python/tests/test_field_defects.py +0 -0
  107. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.1}/python/tests/test_flags.py +0 -0
  108. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.1}/python/tests/test_flatten.py +0 -0
  109. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.1}/python/tests/test_g_groups.py +0 -0
  110. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.1}/python/tests/test_g_vocabulary.py +0 -0
  111. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.1}/python/tests/test_jumps.py +0 -0
  112. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.1}/python/tests/test_real_comparisons.py +0 -0
  113. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.1}/python/tests/test_stage1.py +0 -0
  114. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.1}/python/tests/test_streaming.py +0 -0
  115. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.1}/python/tests/test_unsupported.py +0 -0
  116. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.1}/rustfmt.toml +0 -0
  117. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.1}/src/grammar.pest +0 -0
  118. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.1}/src/types.rs +0 -0
  119. {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.1}/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,42 @@ 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.1] - 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
+ - Optional `line_no` output column on the batch/dataframe path, enabled with
12
+ `include_line_numbers=True` on `nc_to_dataframe` / `nc_to_batches` (default
13
+ `False`, so the output schema is unchanged unless you ask for it). When
14
+ enabled it prepends a leading `Int64` column giving the 1-based source line
15
+ each output row came from - previously only the streaming `nc_to_rows`
16
+ exposed it. Loops repeat the value and jumps make it non-monotonic, matching
17
+ `nc_to_rows` row-for-row; flatten-generated samples keep the originating
18
+ block's line number; `dataframe_to_nc` ignores it (source provenance, not an
19
+ emittable word). Concatenating batches reconstructs the same per-row
20
+ `line_no` as the whole-file dataframe (#45)
21
+ - Structured error locations: parse/interpret failures now raise
22
+ `nc_gcode_interpreter.NcError` (a `ValueError` subclass, so existing
23
+ `except ValueError` keeps working) carrying the position as data - `.line`,
24
+ `.column` (syntax errors), `.context`, and `.line_text` attributes (each an
25
+ int / str or `None`) - so a caller (e.g. an editor) can locate the offending
26
+ token without regex-parsing the message. `str(err)` is unchanged
27
+
28
+ ### Fixed
29
+
30
+ - Arithmetic-function arity: added a regression test pinning that every
31
+ `SIN`/`COS`/`ATAN2`/`BOUND`/... call already validates its argument count
32
+ (`check_args`) before indexing `args[..]`, returning
33
+ `ParsingError::InvalidFunctionArity` instead of panicking on a wrong-arity
34
+ call (#16).
35
+
36
+ ### Documented
37
+
38
+ - Pinned iterator-drop -> `ParsingError::StreamClosed` as the supported
39
+ cancel contract for `nc_to_rows`/`nc_to_batches` (README, docstrings) and
40
+ noted the release-profile `strip`/`lto` tradeoff in `Development.md` (#49).
41
+
42
+ ## [v0.2.0] - 2026-07-07
21
43
 
22
44
  ### Added
23
45
 
@@ -95,6 +117,16 @@ released to PyPI.
95
117
 
96
118
  ### Changed
97
119
 
120
+ - Rust-side polars is gone. The Table -> Python DataFrame handoff no longer
121
+ builds a polars DataFrame in Rust (via `pyo3-polars`); it builds an Arrow
122
+ record batch with the minimal `arrow-array`/`arrow-schema`/`arrow-data`
123
+ crates and hands it to Python zero-copy through the Arrow PyCapsule
124
+ interface (`__arrow_c_array__`), where `pl.DataFrame(...)` wraps it. The
125
+ Python API is unchanged (still returns `polars.DataFrame`), needs no
126
+ `pyarrow`, and performance is unchanged. This drops ~60 crates from the
127
+ `python`-feature build (127 -> 64), cutting a clean release build ~4x
128
+ (83s -> 21s), and bumps PyO3 0.28 -> 0.29 (resolving the RUSTSEC pyo3
129
+ advisories).
98
130
  - **Breaking:** `I`/`J`/`K`/`CR` are now treated as arc interpolation-parameter
99
131
  block addresses (output columns), so they can no longer be used as user
100
132
  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.1"
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.1"
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.1
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`.
@@ -485,6 +510,7 @@ def nc_to_batches(
485
510
  axis_index_map: dict[str, int] | None = None,
486
511
  allow_undefined_variables: bool = False,
487
512
  flatten_tolerance: float | None = None,
513
+ include_line_numbers: bool = False,
488
514
  ) -> _BatchIterator:
489
515
  """Interpret an NC program into a stream of columnar polars DataFrames.
490
516
 
@@ -504,6 +530,17 @@ def nc_to_batches(
504
530
  The returned iterator exposes a ``state`` attribute holding the final
505
531
  interpreter state once it is exhausted, like :func:`nc_to_rows`.
506
532
 
533
+ Like :func:`nc_to_rows`, this runs on a background thread behind a
534
+ bounded channel, and dropping the iterator is the supported way to
535
+ cancel: the next attempted send hits the closed channel and returns
536
+ internally via ``ParsingError::StreamClosed``, unwinding the thread
537
+ promptly instead of finishing the rest of the program.
538
+
539
+ With ``include_line_numbers=True`` every batch gains a leading ``line_no``
540
+ (Int64) column: the 1-based source line each row came from (repeated under
541
+ a loop, non-monotonic across a jump), never forward-filled. Default False
542
+ leaves the schema unchanged.
543
+
507
544
  Example:
508
545
  --------
509
546
  >>> batches = nc_to_batches("G1 X10\\nX20 Y5", batch_size=1)
@@ -526,5 +563,6 @@ def nc_to_batches(
526
563
  allow_undefined_variables,
527
564
  input_is_path,
528
565
  flatten_tolerance,
566
+ include_line_numbers,
529
567
  )
530
568
  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,6 +42,7 @@ 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,
36
46
  ) -> Any:
37
47
  """Interpret an NC program into an iterator of columnar polars DataFrames."""
38
48
  ...
@@ -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
@@ -0,0 +1,50 @@
1
+ """NcError exposes an error's source location as data (line / column / context
2
+ / line_text) instead of only as a formatted string, while remaining a
3
+ ValueError subclass so existing `except ValueError` handlers keep working."""
4
+
5
+ import pytest
6
+ from nc_gcode_interpreter import NcError, nc_to_dataframe
7
+
8
+
9
+ def test_nc_error_is_a_value_error_subclass():
10
+ assert issubclass(NcError, ValueError)
11
+ with pytest.raises(ValueError):
12
+ nc_to_dataframe("X=R99\n") # undefined variable
13
+
14
+
15
+ def test_syntax_error_carries_line_and_column():
16
+ # The malformed token is on line 2; the parser reports its column.
17
+ with pytest.raises(NcError) as exc:
18
+ nc_to_dataframe("G1 X10\nX20 Y((\nX30\n")
19
+ e = exc.value
20
+ assert e.line == 2
21
+ assert isinstance(e.column, int) and e.column > 0
22
+ assert e.line_text == "X20 Y(("
23
+ assert e.context == "line parsing"
24
+
25
+
26
+ def test_semantic_error_carries_line_but_no_column():
27
+ # An undefined-variable error is anchored to a line, not a column.
28
+ with pytest.raises(NcError) as exc:
29
+ nc_to_dataframe("X=R99\n")
30
+ e = exc.value
31
+ assert e.line == 1
32
+ assert e.column is None
33
+ assert e.line_text == "X=R99"
34
+
35
+
36
+ def test_location_attributes_always_present():
37
+ # Every NcError has the four attributes, even when a value is None, so
38
+ # callers can read them unconditionally.
39
+ with pytest.raises(NcError) as exc:
40
+ nc_to_dataframe("G999 X1\n") # unknown G code
41
+ e = exc.value
42
+ assert e.line == 1
43
+ for attr in ("line", "column", "context", "line_text"):
44
+ assert hasattr(e, attr)
45
+
46
+
47
+ def test_str_is_still_the_full_message():
48
+ with pytest.raises(NcError) as exc:
49
+ nc_to_dataframe("X=R99\n")
50
+ assert "Undefined variable" in str(exc.value)