nc-gcode-interpreter 0.2.2__tar.gz → 0.2.3__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.
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/CHANGELOG.md +20 -0
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/Cargo.lock +1 -1
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/Cargo.toml +1 -1
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/PKG-INFO +1 -1
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/python/nc_gcode_interpreter/_internal.pyi +4 -0
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/python/tests/test_structured_errors.py +41 -0
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/src/errors.rs +240 -8
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/src/interpret_rules.rs +25 -6
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/src/lib.rs +13 -8
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/src/state.rs +9 -1
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/.github/workflows/build-and-release.yml +0 -0
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/.gitignore +0 -0
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/CONTRIBUTING.md +0 -0
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/Development.md +0 -0
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/LICENSE +0 -0
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/README.md +0 -0
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/TODO.md +0 -0
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/docs/sinumerik-execution-model.md +0 -0
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/examples/actual_position.csv +0 -0
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/examples/actual_position.mpf +0 -0
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/examples/arc.csv +0 -0
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/examples/arc.mpf +0 -0
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/examples/arrays.csv +0 -0
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/examples/arrays.mpf +0 -0
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/examples/axis_index_assignment.csv +0 -0
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/examples/axis_index_assignment.mpf +0 -0
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/examples/basic_math.csv +0 -0
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/examples/basic_math.mpf +0 -0
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/examples/case.csv +0 -0
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/examples/case.mpf +0 -0
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/examples/case_insensitive_variables.csv +0 -0
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/examples/case_insensitive_variables.mpf +0 -0
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/examples/custom_vars.csv +0 -0
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/examples/custom_vars.mpf +0 -0
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/examples/def_string.csv +0 -0
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/examples/def_string.mpf +0 -0
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/examples/defaults.csv +0 -0
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/examples/defaults.mpf +0 -0
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/examples/edge_cases.csv +0 -0
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/examples/edge_cases.mpf +0 -0
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/examples/flattening/flatten_demo.mpf +0 -0
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/examples/flattening/flatten_demo_flattened.csv +0 -0
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/examples/flattening/flatten_demo_raw.csv +0 -0
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/examples/for_loop.csv +0 -0
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/examples/for_loop.mpf +0 -0
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/examples/function_calls.csv +0 -0
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/examples/function_calls.mpf +0 -0
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/examples/goto.csv +0 -0
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/examples/goto.mpf +0 -0
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/examples/if_statement.csv +0 -0
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/examples/if_statement.mpf +0 -0
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/examples/increment.csv +0 -0
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/examples/increment.mpf +0 -0
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/examples/logic_operators.csv +0 -0
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/examples/logic_operators.mpf +0 -0
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/examples/loop.csv +0 -0
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/examples/loop.mpf +0 -0
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/examples/multiple_m_codes.csv +0 -0
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/examples/multiple_m_codes.mpf +0 -0
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/examples/precedence.csv +0 -0
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/examples/precedence.mpf +0 -0
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/examples/r_param.csv +0 -0
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/examples/r_param.mpf +0 -0
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/examples/repeat.csv +0 -0
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/examples/repeat.mpf +0 -0
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/examples/simple.csv +0 -0
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/examples/simple.mpf +0 -0
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/examples/spline.csv +0 -0
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/examples/spline.mpf +0 -0
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/examples/tool.csv +0 -0
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/examples/tool.mpf +0 -0
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/examples/trans.csv +0 -0
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/examples/trans.mpf +0 -0
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/examples/trans2.csv +0 -0
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/examples/trans2.mpf +0 -0
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/examples/trans_ic.csv +0 -0
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/examples/trans_ic.mpf +0 -0
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/examples/trans_reset.csv +0 -0
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/examples/trans_reset.mpf +0 -0
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/examples/trig.csv +0 -0
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/examples/trig.mpf +0 -0
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/examples/variables.csv +0 -0
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/examples/variables.mpf +0 -0
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/ggroups/generate_g_commands.py +0 -0
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/pyproject.toml +0 -0
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/python/example/minimal.py +0 -0
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/python/example/streaming.py +0 -0
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/python/nc_gcode_interpreter/__init__.py +0 -0
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/python/nc_gcode_interpreter/cli.py +0 -0
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/python/nc_gcode_interpreter/ggroups.json +0 -0
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/python/nc_gcode_interpreter/py.typed +0 -0
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/python/nc_gcode_interpreter/viz.py +0 -0
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/python/tests/test_arithmetic_functions.py +0 -0
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/python/tests/test_batch_variables.py +0 -0
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/python/tests/test_batches.py +0 -0
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/python/tests/test_diagnostics.py +0 -0
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/python/tests/test_expected_output.py +0 -0
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/python/tests/test_field_defects.py +0 -0
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/python/tests/test_flags.py +0 -0
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/python/tests/test_flatten.py +0 -0
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/python/tests/test_g_groups.py +0 -0
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/python/tests/test_g_vocabulary.py +0 -0
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/python/tests/test_jumps.py +0 -0
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/python/tests/test_real_comparisons.py +0 -0
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/python/tests/test_stage1.py +0 -0
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/python/tests/test_streaming.py +0 -0
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/python/tests/test_unsupported.py +0 -0
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/rustfmt.toml +0 -0
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/src/flatten.rs +0 -0
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/src/grammar.pest +0 -0
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/src/interpreter.rs +0 -0
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/src/line_driver.rs +0 -0
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/src/main.rs +0 -0
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/src/modal_groups.rs +0 -0
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/src/output.rs +0 -0
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/src/structure_scan.rs +0 -0
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/src/types.rs +0 -0
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/tests/cli.rs +0 -0
- {nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/uv.lock +0 -0
|
@@ -4,6 +4,26 @@ 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.3] - 2026-07-07
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- `NcError.kind`: a stable, machine-readable string discriminating the error
|
|
12
|
+
class (e.g. `"unexpected_axis"`, `"undefined_variable"`, `"unknown_g_command"`,
|
|
13
|
+
`"parse_context"`), so a consumer can branch on the kind of error without
|
|
14
|
+
string-matching the formatted message. Present on every `NcError` alongside
|
|
15
|
+
the existing `line` / `column` / `context` / `line_text` location attributes
|
|
16
|
+
(#56).
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- Validation/semantic errors now carry their source location. `Unexpected axis`,
|
|
21
|
+
and the "axis/reserved name used as a variable" definition errors, previously
|
|
22
|
+
raised an `NcError` with `line` / `column` / `context` / `line_text` all
|
|
23
|
+
`None`; they now anchor to the offending line (and expose its text), so an
|
|
24
|
+
editor can mark the exact spot - matching what syntactic parse errors already
|
|
25
|
+
did (#56).
|
|
26
|
+
|
|
7
27
|
## [v0.2.2] - 2026-07-07
|
|
8
28
|
|
|
9
29
|
### Added
|
{nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/python/nc_gcode_interpreter/_internal.pyi
RENAMED
|
@@ -9,6 +9,10 @@ class NcError(ValueError):
|
|
|
9
9
|
"""NC parse/interpret error carrying structured location data. Subclasses
|
|
10
10
|
ValueError so `except ValueError` keeps working."""
|
|
11
11
|
|
|
12
|
+
#: Stable, machine-readable discriminator for the error class (e.g.
|
|
13
|
+
#: ``"unexpected_axis"``, ``"undefined_variable"``), for branching without
|
|
14
|
+
#: matching the formatted message.
|
|
15
|
+
kind: str
|
|
12
16
|
line: Optional[int]
|
|
13
17
|
column: Optional[int]
|
|
14
18
|
context: Optional[str]
|
{nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/python/tests/test_structured_errors.py
RENAMED
|
@@ -48,3 +48,44 @@ def test_str_is_still_the_full_message():
|
|
|
48
48
|
with pytest.raises(NcError) as exc:
|
|
49
49
|
nc_to_dataframe("X=R99\n")
|
|
50
50
|
assert "Undefined variable" in str(exc.value)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_kind_is_a_stable_machine_readable_discriminator():
|
|
54
|
+
# #56: consumers branch on `kind` instead of string-matching the message.
|
|
55
|
+
with pytest.raises(NcError) as exc:
|
|
56
|
+
nc_to_dataframe("X=R99\n")
|
|
57
|
+
assert exc.value.kind == "undefined_variable"
|
|
58
|
+
|
|
59
|
+
with pytest.raises(NcError) as exc:
|
|
60
|
+
nc_to_dataframe("G999 X1\n")
|
|
61
|
+
assert exc.value.kind == "unknown_g_command"
|
|
62
|
+
|
|
63
|
+
# Present (and stable) on every NcError, alongside the location attrs.
|
|
64
|
+
with pytest.raises(NcError) as exc:
|
|
65
|
+
nc_to_dataframe("G1 X10\nX20 Y((\n")
|
|
66
|
+
assert exc.value.kind == "parse_context"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_validation_error_carries_kind_and_location():
|
|
70
|
+
# #56: an undeclared axis is a validation error; it now reports both a
|
|
71
|
+
# stable kind AND the source location (previously all-None).
|
|
72
|
+
with pytest.raises(NcError) as exc:
|
|
73
|
+
# Q is not a declared axis; the frame instruction is on line 2.
|
|
74
|
+
nc_to_dataframe("G1 X0 Y0 F100\nTRANS Q=10\n")
|
|
75
|
+
e = exc.value
|
|
76
|
+
assert e.kind == "unexpected_axis"
|
|
77
|
+
assert e.line == 2
|
|
78
|
+
assert e.column is None
|
|
79
|
+
assert e.line_text is not None and "Q=10" in e.line_text
|
|
80
|
+
assert "Unexpected axis" in str(e)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def test_axis_used_as_variable_carries_location():
|
|
84
|
+
# Declaring a variable with an axis name is a validation error that now
|
|
85
|
+
# anchors to the offending DEF line.
|
|
86
|
+
with pytest.raises(NcError) as exc:
|
|
87
|
+
nc_to_dataframe("DEF REAL X\n")
|
|
88
|
+
e = exc.value
|
|
89
|
+
assert e.kind == "axis_used_as_variable"
|
|
90
|
+
assert e.line == 1
|
|
91
|
+
assert e.line_text is not None and "X" in e.line_text
|
|
@@ -99,11 +99,26 @@ To fix this, ensure that each block contains at most one M command.
|
|
|
99
99
|
message: String,
|
|
100
100
|
},
|
|
101
101
|
#[error("Unexpected axis '{axis}'. Valid axes are: {axes}")]
|
|
102
|
-
UnexpectedAxis {
|
|
102
|
+
UnexpectedAxis {
|
|
103
|
+
axis: String,
|
|
104
|
+
axes: String,
|
|
105
|
+
/// Source location of the offending axis word. Not shown in the
|
|
106
|
+
/// formatted message, only exposed as data (see [`ParsingError::location`]).
|
|
107
|
+
line_no: usize,
|
|
108
|
+
preview: String,
|
|
109
|
+
},
|
|
103
110
|
#[error("Cannot define a variable named '{name}', as it conflicts with an axis name")]
|
|
104
|
-
AxisUsedAsVariable {
|
|
111
|
+
AxisUsedAsVariable {
|
|
112
|
+
name: String,
|
|
113
|
+
line_no: usize,
|
|
114
|
+
preview: String,
|
|
115
|
+
},
|
|
105
116
|
#[error("Cannot define a variable named '{name}', as it is a reserved block address (spline PW/SD/PL)")]
|
|
106
|
-
ReservedNameUsedAsVariable {
|
|
117
|
+
ReservedNameUsedAsVariable {
|
|
118
|
+
name: String,
|
|
119
|
+
line_no: usize,
|
|
120
|
+
preview: String,
|
|
121
|
+
},
|
|
107
122
|
#[error(
|
|
108
123
|
r#"
|
|
109
124
|
Missing axis mapping on line {line_no}
|
|
@@ -280,16 +295,48 @@ impl ParsingError {
|
|
|
280
295
|
| Self::JumpTargetNotFound { line_no, preview, .. }
|
|
281
296
|
| Self::UnmatchedStructure { line_no, preview, .. }
|
|
282
297
|
| Self::UnknownGCommand { line_no, preview, .. }
|
|
283
|
-
| Self::InvalidFunctionArity { line_no, preview, .. }
|
|
298
|
+
| Self::InvalidFunctionArity { line_no, preview, .. }
|
|
299
|
+
// Semantic/validation errors: the offending block is known at raise
|
|
300
|
+
// time, so they anchor to a line (no column) like the others above.
|
|
301
|
+
| Self::UnexpectedAxis { line_no, preview, .. }
|
|
302
|
+
| Self::AxisUsedAsVariable { line_no, preview, .. }
|
|
303
|
+
| Self::ReservedNameUsedAsVariable { line_no, preview, .. } => some(*line_no, None, None, Some(preview)),
|
|
284
304
|
Self::ParseError { .. }
|
|
285
305
|
| Self::InvalidElementCount { .. }
|
|
286
306
|
| Self::InvalidCondition
|
|
287
307
|
| Self::UnexpectedOperator { .. }
|
|
288
308
|
| Self::LoopLimit { .. }
|
|
289
|
-
| Self::StreamClosed
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
309
|
+
| Self::StreamClosed => None,
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/// A stable, machine-readable discriminator for the error class, so a
|
|
314
|
+
/// consumer can branch on the kind of error without string-matching the
|
|
315
|
+
/// formatted message. Exposed to Python as the `NcError.kind` attribute.
|
|
316
|
+
/// These strings are part of the public API: keep them stable.
|
|
317
|
+
pub fn kind(&self) -> &'static str {
|
|
318
|
+
match self {
|
|
319
|
+
Self::ParsingContext { .. } => "parse_context",
|
|
320
|
+
Self::UnknownVariable { .. } => "unknown_variable",
|
|
321
|
+
Self::UndefinedVariable { .. } => "undefined_variable",
|
|
322
|
+
Self::UnexpectedRule { .. } => "unexpected_rule",
|
|
323
|
+
Self::ParseError { .. } => "parse_error",
|
|
324
|
+
Self::InvalidElementCount { .. } => "invalid_element_count",
|
|
325
|
+
Self::InvalidCondition => "invalid_condition",
|
|
326
|
+
Self::UnexpectedOperator { .. } => "unexpected_operator",
|
|
327
|
+
Self::LoopLimit { .. } => "loop_limit",
|
|
328
|
+
Self::StreamClosed => "stream_closed",
|
|
329
|
+
Self::TooManyMCommands { .. } => "too_many_m_commands",
|
|
330
|
+
Self::UnexpectedAxis { .. } => "unexpected_axis",
|
|
331
|
+
Self::AxisUsedAsVariable { .. } => "axis_used_as_variable",
|
|
332
|
+
Self::ReservedNameUsedAsVariable { .. } => "reserved_name_used_as_variable",
|
|
333
|
+
Self::MissingAxisMapping { .. } => "missing_axis_mapping",
|
|
334
|
+
Self::InvalidAxisIndex { .. } => "invalid_axis_index",
|
|
335
|
+
Self::UnsupportedStatement { .. } => "unsupported_statement",
|
|
336
|
+
Self::JumpTargetNotFound { .. } => "jump_target_not_found",
|
|
337
|
+
Self::UnmatchedStructure { .. } => "unmatched_structure",
|
|
338
|
+
Self::UnknownGCommand { .. } => "unknown_g_command",
|
|
339
|
+
Self::InvalidFunctionArity { .. } => "invalid_function_arity",
|
|
293
340
|
}
|
|
294
341
|
}
|
|
295
342
|
}
|
|
@@ -333,4 +380,189 @@ mod tests {
|
|
|
333
380
|
assert!(ParsingError::StreamClosed.location().is_none());
|
|
334
381
|
assert!(ParsingError::InvalidCondition.location().is_none());
|
|
335
382
|
}
|
|
383
|
+
|
|
384
|
+
#[test]
|
|
385
|
+
fn validation_errors_now_carry_a_location() {
|
|
386
|
+
// #56: semantic/validation errors (previously location-less) anchor to
|
|
387
|
+
// the offending line so an editor can mark the spot.
|
|
388
|
+
let axis = ParsingError::UnexpectedAxis {
|
|
389
|
+
axis: "QQ".to_string(),
|
|
390
|
+
axes: "X, Y, Z".to_string(),
|
|
391
|
+
line_no: 4,
|
|
392
|
+
preview: "TRANS QQ10".to_string(),
|
|
393
|
+
};
|
|
394
|
+
let loc = axis.location().expect("UnexpectedAxis now has a location");
|
|
395
|
+
assert_eq!((loc.line, loc.column), (4, None));
|
|
396
|
+
assert_eq!(loc.line_text.as_deref(), Some("TRANS QQ10"));
|
|
397
|
+
|
|
398
|
+
let dup = ParsingError::AxisUsedAsVariable {
|
|
399
|
+
name: "X".to_string(),
|
|
400
|
+
line_no: 2,
|
|
401
|
+
preview: "DEF REAL X".to_string(),
|
|
402
|
+
};
|
|
403
|
+
assert_eq!(dup.location().expect("has location").line, 2);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
#[test]
|
|
407
|
+
fn kind_is_a_stable_per_variant_discriminator() {
|
|
408
|
+
// Exhaustive: `kind()` strings are public API, so pin every variant's
|
|
409
|
+
// value. The wildcard-free match in `kind()` forces a new variant to be
|
|
410
|
+
// handled there; this list forces its string to be chosen deliberately.
|
|
411
|
+
use crate::types::Rule;
|
|
412
|
+
let s = String::new;
|
|
413
|
+
let cases: Vec<(ParsingError, &str)> = vec![
|
|
414
|
+
(
|
|
415
|
+
ParsingError::ParsingContext {
|
|
416
|
+
line_no: 1,
|
|
417
|
+
column: None,
|
|
418
|
+
preview: s(),
|
|
419
|
+
context: s(),
|
|
420
|
+
message: s(),
|
|
421
|
+
},
|
|
422
|
+
"parse_context",
|
|
423
|
+
),
|
|
424
|
+
(
|
|
425
|
+
ParsingError::UnknownVariable {
|
|
426
|
+
line_no: 1,
|
|
427
|
+
preview: s(),
|
|
428
|
+
variable: s(),
|
|
429
|
+
},
|
|
430
|
+
"unknown_variable",
|
|
431
|
+
),
|
|
432
|
+
(
|
|
433
|
+
ParsingError::UndefinedVariable {
|
|
434
|
+
line_no: 1,
|
|
435
|
+
preview: s(),
|
|
436
|
+
name: s(),
|
|
437
|
+
},
|
|
438
|
+
"undefined_variable",
|
|
439
|
+
),
|
|
440
|
+
(
|
|
441
|
+
ParsingError::UnexpectedRule {
|
|
442
|
+
rule: Rule::EOI,
|
|
443
|
+
context: s(),
|
|
444
|
+
line_no: 1,
|
|
445
|
+
preview: s(),
|
|
446
|
+
message: s(),
|
|
447
|
+
},
|
|
448
|
+
"unexpected_rule",
|
|
449
|
+
),
|
|
450
|
+
(ParsingError::ParseError { message: s() }, "parse_error"),
|
|
451
|
+
(
|
|
452
|
+
ParsingError::InvalidElementCount { expected: 1, actual: 2 },
|
|
453
|
+
"invalid_element_count",
|
|
454
|
+
),
|
|
455
|
+
(ParsingError::InvalidCondition, "invalid_condition"),
|
|
456
|
+
(
|
|
457
|
+
ParsingError::UnexpectedOperator { operator: s() },
|
|
458
|
+
"unexpected_operator",
|
|
459
|
+
),
|
|
460
|
+
(ParsingError::LoopLimit { limit: s() }, "loop_limit"),
|
|
461
|
+
(ParsingError::StreamClosed, "stream_closed"),
|
|
462
|
+
(
|
|
463
|
+
ParsingError::TooManyMCommands {
|
|
464
|
+
line_no: 1,
|
|
465
|
+
preview: s(),
|
|
466
|
+
message: s(),
|
|
467
|
+
},
|
|
468
|
+
"too_many_m_commands",
|
|
469
|
+
),
|
|
470
|
+
(
|
|
471
|
+
ParsingError::UnexpectedAxis {
|
|
472
|
+
axis: s(),
|
|
473
|
+
axes: s(),
|
|
474
|
+
line_no: 1,
|
|
475
|
+
preview: s(),
|
|
476
|
+
},
|
|
477
|
+
"unexpected_axis",
|
|
478
|
+
),
|
|
479
|
+
(
|
|
480
|
+
ParsingError::AxisUsedAsVariable {
|
|
481
|
+
name: s(),
|
|
482
|
+
line_no: 1,
|
|
483
|
+
preview: s(),
|
|
484
|
+
},
|
|
485
|
+
"axis_used_as_variable",
|
|
486
|
+
),
|
|
487
|
+
(
|
|
488
|
+
ParsingError::ReservedNameUsedAsVariable {
|
|
489
|
+
name: s(),
|
|
490
|
+
line_no: 1,
|
|
491
|
+
preview: s(),
|
|
492
|
+
},
|
|
493
|
+
"reserved_name_used_as_variable",
|
|
494
|
+
),
|
|
495
|
+
(
|
|
496
|
+
ParsingError::MissingAxisMapping {
|
|
497
|
+
line_no: 1,
|
|
498
|
+
preview: s(),
|
|
499
|
+
axis: s(),
|
|
500
|
+
},
|
|
501
|
+
"missing_axis_mapping",
|
|
502
|
+
),
|
|
503
|
+
(
|
|
504
|
+
ParsingError::InvalidAxisIndex {
|
|
505
|
+
line_no: 1,
|
|
506
|
+
preview: s(),
|
|
507
|
+
axis: s(),
|
|
508
|
+
index: 0,
|
|
509
|
+
},
|
|
510
|
+
"invalid_axis_index",
|
|
511
|
+
),
|
|
512
|
+
(
|
|
513
|
+
ParsingError::UnsupportedStatement {
|
|
514
|
+
line_no: 1,
|
|
515
|
+
preview: s(),
|
|
516
|
+
statement: s(),
|
|
517
|
+
hint: s(),
|
|
518
|
+
},
|
|
519
|
+
"unsupported_statement",
|
|
520
|
+
),
|
|
521
|
+
(
|
|
522
|
+
ParsingError::JumpTargetNotFound {
|
|
523
|
+
line_no: 1,
|
|
524
|
+
preview: s(),
|
|
525
|
+
target: s(),
|
|
526
|
+
search_direction: s(),
|
|
527
|
+
hint: s(),
|
|
528
|
+
},
|
|
529
|
+
"jump_target_not_found",
|
|
530
|
+
),
|
|
531
|
+
(
|
|
532
|
+
ParsingError::UnmatchedStructure {
|
|
533
|
+
line_no: 1,
|
|
534
|
+
preview: s(),
|
|
535
|
+
message: s(),
|
|
536
|
+
},
|
|
537
|
+
"unmatched_structure",
|
|
538
|
+
),
|
|
539
|
+
(
|
|
540
|
+
ParsingError::UnknownGCommand {
|
|
541
|
+
line_no: 1,
|
|
542
|
+
preview: s(),
|
|
543
|
+
code: s(),
|
|
544
|
+
},
|
|
545
|
+
"unknown_g_command",
|
|
546
|
+
),
|
|
547
|
+
(
|
|
548
|
+
ParsingError::InvalidFunctionArity {
|
|
549
|
+
line_no: 1,
|
|
550
|
+
preview: s(),
|
|
551
|
+
name: s(),
|
|
552
|
+
expected: 1,
|
|
553
|
+
actual: 2,
|
|
554
|
+
},
|
|
555
|
+
"invalid_function_arity",
|
|
556
|
+
),
|
|
557
|
+
];
|
|
558
|
+
for (err, expected) in &cases {
|
|
559
|
+
assert_eq!(err.kind(), *expected, "kind mismatch for {err:?}");
|
|
560
|
+
}
|
|
561
|
+
// Every kind string is distinct (no two variants share a discriminator).
|
|
562
|
+
let mut kinds: Vec<&str> = cases.iter().map(|(_, k)| *k).collect();
|
|
563
|
+
kinds.sort_unstable();
|
|
564
|
+
let unique = kinds.len();
|
|
565
|
+
kinds.dedup();
|
|
566
|
+
assert_eq!(kinds.len(), unique, "duplicate kind discriminator");
|
|
567
|
+
}
|
|
336
568
|
}
|
|
@@ -1168,9 +1168,17 @@ fn interpret_definition(element: Pair<Rule>, output: &mut Output, state: &mut St
|
|
|
1168
1168
|
let (line_no, preview) = get_error_context(&pair, state);
|
|
1169
1169
|
let res = interpret_assignment(pair, state)?;
|
|
1170
1170
|
if state.is_axis(res.0.as_str()) {
|
|
1171
|
-
return Err(ParsingError::AxisUsedAsVariable {
|
|
1171
|
+
return Err(ParsingError::AxisUsedAsVariable {
|
|
1172
|
+
name: res.0,
|
|
1173
|
+
line_no,
|
|
1174
|
+
preview,
|
|
1175
|
+
});
|
|
1172
1176
|
} else if state.is_block_address(res.0.as_str()) {
|
|
1173
|
-
return Err(ParsingError::ReservedNameUsedAsVariable {
|
|
1177
|
+
return Err(ParsingError::ReservedNameUsedAsVariable {
|
|
1178
|
+
name: res.0,
|
|
1179
|
+
line_no,
|
|
1180
|
+
preview,
|
|
1181
|
+
});
|
|
1174
1182
|
}
|
|
1175
1183
|
match res.1 {
|
|
1176
1184
|
Some(value) => {
|
|
@@ -1207,11 +1215,20 @@ fn interpret_definition(element: Pair<Rule>, output: &mut Output, state: &mut St
|
|
|
1207
1215
|
}
|
|
1208
1216
|
}
|
|
1209
1217
|
Rule::variable => {
|
|
1218
|
+
let (line_no, preview) = get_error_context(&pair, state);
|
|
1210
1219
|
let key = interpret_variable(pair, state)?;
|
|
1211
1220
|
if state.is_axis(&key) {
|
|
1212
|
-
return Err(ParsingError::AxisUsedAsVariable {
|
|
1221
|
+
return Err(ParsingError::AxisUsedAsVariable {
|
|
1222
|
+
name: key,
|
|
1223
|
+
line_no,
|
|
1224
|
+
preview,
|
|
1225
|
+
});
|
|
1213
1226
|
} else if state.is_block_address(&key) {
|
|
1214
|
-
return Err(ParsingError::ReservedNameUsedAsVariable {
|
|
1227
|
+
return Err(ParsingError::ReservedNameUsedAsVariable {
|
|
1228
|
+
name: key,
|
|
1229
|
+
line_no,
|
|
1230
|
+
preview,
|
|
1231
|
+
});
|
|
1215
1232
|
}
|
|
1216
1233
|
if is_string {
|
|
1217
1234
|
state.string_table.insert(key.clone(), String::new());
|
|
@@ -1883,6 +1900,8 @@ fn frame_assignments(pairs: Vec<Pair<Rule>>, state: &mut State) -> Result<Vec<(S
|
|
|
1883
1900
|
return Err(ParsingError::UnexpectedAxis {
|
|
1884
1901
|
axis: key,
|
|
1885
1902
|
axes: state.axis_identifiers.join(", "),
|
|
1903
|
+
line_no: pair_line_no,
|
|
1904
|
+
preview: pair_preview,
|
|
1886
1905
|
});
|
|
1887
1906
|
}
|
|
1888
1907
|
result.push((key, value));
|
|
@@ -1908,14 +1927,14 @@ fn interpret_frame_op(element: Pair<Rule>, state: &mut State) -> Result<(), Pars
|
|
|
1908
1927
|
// frames"). Bare TRANS is therefore just the reset.
|
|
1909
1928
|
state.reset_translations();
|
|
1910
1929
|
for (key, value) in frame_assignments(assignments, state)? {
|
|
1911
|
-
state.update_translation(&key, value)?;
|
|
1930
|
+
state.update_translation(&key, value, line_no, &preview)?;
|
|
1912
1931
|
}
|
|
1913
1932
|
Ok(())
|
|
1914
1933
|
}
|
|
1915
1934
|
"ATRANS" => {
|
|
1916
1935
|
for (key, value) in frame_assignments(assignments, state)? {
|
|
1917
1936
|
let current_translation = state.get_translation(&key);
|
|
1918
|
-
state.update_translation(&key, current_translation + value)?;
|
|
1937
|
+
state.update_translation(&key, current_translation + value, line_no, &preview)?;
|
|
1919
1938
|
}
|
|
1920
1939
|
Ok(())
|
|
1921
1940
|
}
|
|
@@ -71,15 +71,18 @@ mod python_bindings {
|
|
|
71
71
|
"NC parse/interpret error. Subclasses ValueError (so `except ValueError` \
|
|
72
72
|
still catches it) and carries the error's source location as data: the \
|
|
73
73
|
`line`, `column`, `context`, and `line_text` attributes (each an int / \
|
|
74
|
-
str, or None when not applicable)
|
|
75
|
-
|
|
74
|
+
str, or None when not applicable), plus a stable `kind` string \
|
|
75
|
+
discriminating the error class (e.g. 'unexpected_axis', \
|
|
76
|
+
'undefined_variable') for branching without matching the message. \
|
|
77
|
+
`str(err)` is the full formatted message as before."
|
|
76
78
|
);
|
|
77
79
|
|
|
78
|
-
/// An error crossing the worker channel: the formatted message
|
|
79
|
-
/// structured location, so the consuming
|
|
80
|
-
/// carrying
|
|
80
|
+
/// An error crossing the worker channel: the formatted message, the stable
|
|
81
|
+
/// error-kind discriminator, and the structured location, so the consuming
|
|
82
|
+
/// thread can raise an `NcError` carrying all three.
|
|
81
83
|
struct ErrInfo {
|
|
82
84
|
message: String,
|
|
85
|
+
kind: &'static str,
|
|
83
86
|
location: Option<ErrorLocation>,
|
|
84
87
|
}
|
|
85
88
|
|
|
@@ -87,13 +90,14 @@ mod python_bindings {
|
|
|
87
90
|
fn from_error(error: &crate::errors::ParsingError) -> Self {
|
|
88
91
|
ErrInfo {
|
|
89
92
|
message: error.to_string(),
|
|
93
|
+
kind: error.kind(),
|
|
90
94
|
location: error.location(),
|
|
91
95
|
}
|
|
92
96
|
}
|
|
93
97
|
|
|
94
|
-
/// Build the Python `NcError`, always setting the
|
|
95
|
-
/// attributes (None when absent) so callers can
|
|
96
|
-
/// unconditionally.
|
|
98
|
+
/// Build the Python `NcError`, always setting the `kind` discriminator
|
|
99
|
+
/// and the four location attributes (None when absent) so callers can
|
|
100
|
+
/// read them unconditionally.
|
|
97
101
|
fn into_pyerr(self, py: Python<'_>) -> PyErr {
|
|
98
102
|
let err = NcError::new_err(self.message);
|
|
99
103
|
let value = err.value(py);
|
|
@@ -101,6 +105,7 @@ mod python_bindings {
|
|
|
101
105
|
Some(l) => (Some(l.line), l.column, l.context, l.line_text),
|
|
102
106
|
None => (None, None, None, None),
|
|
103
107
|
};
|
|
108
|
+
let _ = value.setattr("kind", self.kind);
|
|
104
109
|
let _ = value.setattr("line", line);
|
|
105
110
|
let _ = value.setattr("column", column);
|
|
106
111
|
let _ = value.setattr("context", context);
|
|
@@ -226,7 +226,13 @@ impl State {
|
|
|
226
226
|
}
|
|
227
227
|
|
|
228
228
|
/// Updates the translation value for an axis
|
|
229
|
-
pub fn update_translation(
|
|
229
|
+
pub fn update_translation(
|
|
230
|
+
&mut self,
|
|
231
|
+
axis: &str,
|
|
232
|
+
value: f64,
|
|
233
|
+
line_no: usize,
|
|
234
|
+
preview: &str,
|
|
235
|
+
) -> Result<(), ParsingError> {
|
|
230
236
|
if self.is_axis(axis) {
|
|
231
237
|
self.translation.insert(axis.to_string(), value);
|
|
232
238
|
Ok(())
|
|
@@ -234,6 +240,8 @@ impl State {
|
|
|
234
240
|
Err(ParsingError::UnexpectedAxis {
|
|
235
241
|
axis: axis.to_string(),
|
|
236
242
|
axes: self.axis_identifiers.join(", "),
|
|
243
|
+
line_no,
|
|
244
|
+
preview: preview.to_string(),
|
|
237
245
|
})
|
|
238
246
|
}
|
|
239
247
|
}
|
{nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/.github/workflows/build-and-release.yml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/examples/axis_index_assignment.csv
RENAMED
|
File without changes
|
{nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/examples/axis_index_assignment.mpf
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/examples/case_insensitive_variables.csv
RENAMED
|
File without changes
|
{nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/examples/case_insensitive_variables.mpf
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/examples/flattening/flatten_demo.mpf
RENAMED
|
File without changes
|
|
File without changes
|
{nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/examples/flattening/flatten_demo_raw.csv
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/python/nc_gcode_interpreter/__init__.py
RENAMED
|
File without changes
|
{nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/python/nc_gcode_interpreter/cli.py
RENAMED
|
File without changes
|
{nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/python/nc_gcode_interpreter/ggroups.json
RENAMED
|
File without changes
|
{nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/python/nc_gcode_interpreter/py.typed
RENAMED
|
File without changes
|
{nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/python/nc_gcode_interpreter/viz.py
RENAMED
|
File without changes
|
{nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/python/tests/test_arithmetic_functions.py
RENAMED
|
File without changes
|
{nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/python/tests/test_batch_variables.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/python/tests/test_expected_output.py
RENAMED
|
File without changes
|
{nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/python/tests/test_field_defects.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{nc_gcode_interpreter-0.2.2 → nc_gcode_interpreter-0.2.3}/python/tests/test_real_comparisons.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|