nc-gcode-interpreter 0.2.3__tar.gz → 0.2.4__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.3 → nc_gcode_interpreter-0.2.4}/CHANGELOG.md +20 -0
- {nc_gcode_interpreter-0.2.3 → nc_gcode_interpreter-0.2.4}/Cargo.lock +8 -1
- {nc_gcode_interpreter-0.2.3 → nc_gcode_interpreter-0.2.4}/Cargo.toml +2 -1
- {nc_gcode_interpreter-0.2.3 → nc_gcode_interpreter-0.2.4}/PKG-INFO +1 -1
- nc_gcode_interpreter-0.2.4/docs/resumable-interpretation-design.md +233 -0
- {nc_gcode_interpreter-0.2.3 → nc_gcode_interpreter-0.2.4}/src/interpret_rules.rs +10 -2
- {nc_gcode_interpreter-0.2.3 → nc_gcode_interpreter-0.2.4}/src/interpreter.rs +8 -1
- {nc_gcode_interpreter-0.2.3 → nc_gcode_interpreter-0.2.4}/src/output.rs +45 -4
- {nc_gcode_interpreter-0.2.3 → nc_gcode_interpreter-0.2.4}/src/state.rs +36 -11
- nc_gcode_interpreter-0.2.4/src/vm.rs +661 -0
- {nc_gcode_interpreter-0.2.3 → nc_gcode_interpreter-0.2.4}/.github/workflows/build-and-release.yml +0 -0
- {nc_gcode_interpreter-0.2.3 → nc_gcode_interpreter-0.2.4}/.gitignore +0 -0
- {nc_gcode_interpreter-0.2.3 → nc_gcode_interpreter-0.2.4}/CONTRIBUTING.md +0 -0
- {nc_gcode_interpreter-0.2.3 → nc_gcode_interpreter-0.2.4}/Development.md +0 -0
- {nc_gcode_interpreter-0.2.3 → nc_gcode_interpreter-0.2.4}/LICENSE +0 -0
- {nc_gcode_interpreter-0.2.3 → nc_gcode_interpreter-0.2.4}/README.md +0 -0
- {nc_gcode_interpreter-0.2.3 → nc_gcode_interpreter-0.2.4}/TODO.md +0 -0
- {nc_gcode_interpreter-0.2.3 → nc_gcode_interpreter-0.2.4}/docs/sinumerik-execution-model.md +0 -0
- {nc_gcode_interpreter-0.2.3 → nc_gcode_interpreter-0.2.4}/examples/actual_position.csv +0 -0
- {nc_gcode_interpreter-0.2.3 → nc_gcode_interpreter-0.2.4}/examples/actual_position.mpf +0 -0
- {nc_gcode_interpreter-0.2.3 → nc_gcode_interpreter-0.2.4}/examples/arc.csv +0 -0
- {nc_gcode_interpreter-0.2.3 → nc_gcode_interpreter-0.2.4}/examples/arc.mpf +0 -0
- {nc_gcode_interpreter-0.2.3 → nc_gcode_interpreter-0.2.4}/examples/arrays.csv +0 -0
- {nc_gcode_interpreter-0.2.3 → nc_gcode_interpreter-0.2.4}/examples/arrays.mpf +0 -0
- {nc_gcode_interpreter-0.2.3 → nc_gcode_interpreter-0.2.4}/examples/axis_index_assignment.csv +0 -0
- {nc_gcode_interpreter-0.2.3 → nc_gcode_interpreter-0.2.4}/examples/axis_index_assignment.mpf +0 -0
- {nc_gcode_interpreter-0.2.3 → nc_gcode_interpreter-0.2.4}/examples/basic_math.csv +0 -0
- {nc_gcode_interpreter-0.2.3 → nc_gcode_interpreter-0.2.4}/examples/basic_math.mpf +0 -0
- {nc_gcode_interpreter-0.2.3 → nc_gcode_interpreter-0.2.4}/examples/case.csv +0 -0
- {nc_gcode_interpreter-0.2.3 → nc_gcode_interpreter-0.2.4}/examples/case.mpf +0 -0
- {nc_gcode_interpreter-0.2.3 → nc_gcode_interpreter-0.2.4}/examples/case_insensitive_variables.csv +0 -0
- {nc_gcode_interpreter-0.2.3 → nc_gcode_interpreter-0.2.4}/examples/case_insensitive_variables.mpf +0 -0
- {nc_gcode_interpreter-0.2.3 → nc_gcode_interpreter-0.2.4}/examples/custom_vars.csv +0 -0
- {nc_gcode_interpreter-0.2.3 → nc_gcode_interpreter-0.2.4}/examples/custom_vars.mpf +0 -0
- {nc_gcode_interpreter-0.2.3 → nc_gcode_interpreter-0.2.4}/examples/def_string.csv +0 -0
- {nc_gcode_interpreter-0.2.3 → nc_gcode_interpreter-0.2.4}/examples/def_string.mpf +0 -0
- {nc_gcode_interpreter-0.2.3 → nc_gcode_interpreter-0.2.4}/examples/defaults.csv +0 -0
- {nc_gcode_interpreter-0.2.3 → nc_gcode_interpreter-0.2.4}/examples/defaults.mpf +0 -0
- {nc_gcode_interpreter-0.2.3 → nc_gcode_interpreter-0.2.4}/examples/edge_cases.csv +0 -0
- {nc_gcode_interpreter-0.2.3 → nc_gcode_interpreter-0.2.4}/examples/edge_cases.mpf +0 -0
- {nc_gcode_interpreter-0.2.3 → nc_gcode_interpreter-0.2.4}/examples/flattening/flatten_demo.mpf +0 -0
- {nc_gcode_interpreter-0.2.3 → nc_gcode_interpreter-0.2.4}/examples/flattening/flatten_demo_flattened.csv +0 -0
- {nc_gcode_interpreter-0.2.3 → nc_gcode_interpreter-0.2.4}/examples/flattening/flatten_demo_raw.csv +0 -0
- {nc_gcode_interpreter-0.2.3 → nc_gcode_interpreter-0.2.4}/examples/for_loop.csv +0 -0
- {nc_gcode_interpreter-0.2.3 → nc_gcode_interpreter-0.2.4}/examples/for_loop.mpf +0 -0
- {nc_gcode_interpreter-0.2.3 → nc_gcode_interpreter-0.2.4}/examples/function_calls.csv +0 -0
- {nc_gcode_interpreter-0.2.3 → nc_gcode_interpreter-0.2.4}/examples/function_calls.mpf +0 -0
- {nc_gcode_interpreter-0.2.3 → nc_gcode_interpreter-0.2.4}/examples/goto.csv +0 -0
- {nc_gcode_interpreter-0.2.3 → nc_gcode_interpreter-0.2.4}/examples/goto.mpf +0 -0
- {nc_gcode_interpreter-0.2.3 → nc_gcode_interpreter-0.2.4}/examples/if_statement.csv +0 -0
- {nc_gcode_interpreter-0.2.3 → nc_gcode_interpreter-0.2.4}/examples/if_statement.mpf +0 -0
- {nc_gcode_interpreter-0.2.3 → nc_gcode_interpreter-0.2.4}/examples/increment.csv +0 -0
- {nc_gcode_interpreter-0.2.3 → nc_gcode_interpreter-0.2.4}/examples/increment.mpf +0 -0
- {nc_gcode_interpreter-0.2.3 → nc_gcode_interpreter-0.2.4}/examples/logic_operators.csv +0 -0
- {nc_gcode_interpreter-0.2.3 → nc_gcode_interpreter-0.2.4}/examples/logic_operators.mpf +0 -0
- {nc_gcode_interpreter-0.2.3 → nc_gcode_interpreter-0.2.4}/examples/loop.csv +0 -0
- {nc_gcode_interpreter-0.2.3 → nc_gcode_interpreter-0.2.4}/examples/loop.mpf +0 -0
- {nc_gcode_interpreter-0.2.3 → nc_gcode_interpreter-0.2.4}/examples/multiple_m_codes.csv +0 -0
- {nc_gcode_interpreter-0.2.3 → nc_gcode_interpreter-0.2.4}/examples/multiple_m_codes.mpf +0 -0
- {nc_gcode_interpreter-0.2.3 → nc_gcode_interpreter-0.2.4}/examples/precedence.csv +0 -0
- {nc_gcode_interpreter-0.2.3 → nc_gcode_interpreter-0.2.4}/examples/precedence.mpf +0 -0
- {nc_gcode_interpreter-0.2.3 → nc_gcode_interpreter-0.2.4}/examples/r_param.csv +0 -0
- {nc_gcode_interpreter-0.2.3 → nc_gcode_interpreter-0.2.4}/examples/r_param.mpf +0 -0
- {nc_gcode_interpreter-0.2.3 → nc_gcode_interpreter-0.2.4}/examples/repeat.csv +0 -0
- {nc_gcode_interpreter-0.2.3 → nc_gcode_interpreter-0.2.4}/examples/repeat.mpf +0 -0
- {nc_gcode_interpreter-0.2.3 → nc_gcode_interpreter-0.2.4}/examples/simple.csv +0 -0
- {nc_gcode_interpreter-0.2.3 → nc_gcode_interpreter-0.2.4}/examples/simple.mpf +0 -0
- {nc_gcode_interpreter-0.2.3 → nc_gcode_interpreter-0.2.4}/examples/spline.csv +0 -0
- {nc_gcode_interpreter-0.2.3 → nc_gcode_interpreter-0.2.4}/examples/spline.mpf +0 -0
- {nc_gcode_interpreter-0.2.3 → nc_gcode_interpreter-0.2.4}/examples/tool.csv +0 -0
- {nc_gcode_interpreter-0.2.3 → nc_gcode_interpreter-0.2.4}/examples/tool.mpf +0 -0
- {nc_gcode_interpreter-0.2.3 → nc_gcode_interpreter-0.2.4}/examples/trans.csv +0 -0
- {nc_gcode_interpreter-0.2.3 → nc_gcode_interpreter-0.2.4}/examples/trans.mpf +0 -0
- {nc_gcode_interpreter-0.2.3 → nc_gcode_interpreter-0.2.4}/examples/trans2.csv +0 -0
- {nc_gcode_interpreter-0.2.3 → nc_gcode_interpreter-0.2.4}/examples/trans2.mpf +0 -0
- {nc_gcode_interpreter-0.2.3 → nc_gcode_interpreter-0.2.4}/examples/trans_ic.csv +0 -0
- {nc_gcode_interpreter-0.2.3 → nc_gcode_interpreter-0.2.4}/examples/trans_ic.mpf +0 -0
- {nc_gcode_interpreter-0.2.3 → nc_gcode_interpreter-0.2.4}/examples/trans_reset.csv +0 -0
- {nc_gcode_interpreter-0.2.3 → nc_gcode_interpreter-0.2.4}/examples/trans_reset.mpf +0 -0
- {nc_gcode_interpreter-0.2.3 → nc_gcode_interpreter-0.2.4}/examples/trig.csv +0 -0
- {nc_gcode_interpreter-0.2.3 → nc_gcode_interpreter-0.2.4}/examples/trig.mpf +0 -0
- {nc_gcode_interpreter-0.2.3 → nc_gcode_interpreter-0.2.4}/examples/variables.csv +0 -0
- {nc_gcode_interpreter-0.2.3 → nc_gcode_interpreter-0.2.4}/examples/variables.mpf +0 -0
- {nc_gcode_interpreter-0.2.3 → nc_gcode_interpreter-0.2.4}/ggroups/generate_g_commands.py +0 -0
- {nc_gcode_interpreter-0.2.3 → nc_gcode_interpreter-0.2.4}/pyproject.toml +0 -0
- {nc_gcode_interpreter-0.2.3 → nc_gcode_interpreter-0.2.4}/python/example/minimal.py +0 -0
- {nc_gcode_interpreter-0.2.3 → nc_gcode_interpreter-0.2.4}/python/example/streaming.py +0 -0
- {nc_gcode_interpreter-0.2.3 → nc_gcode_interpreter-0.2.4}/python/nc_gcode_interpreter/__init__.py +0 -0
- {nc_gcode_interpreter-0.2.3 → nc_gcode_interpreter-0.2.4}/python/nc_gcode_interpreter/_internal.pyi +0 -0
- {nc_gcode_interpreter-0.2.3 → nc_gcode_interpreter-0.2.4}/python/nc_gcode_interpreter/cli.py +0 -0
- {nc_gcode_interpreter-0.2.3 → nc_gcode_interpreter-0.2.4}/python/nc_gcode_interpreter/ggroups.json +0 -0
- {nc_gcode_interpreter-0.2.3 → nc_gcode_interpreter-0.2.4}/python/nc_gcode_interpreter/py.typed +0 -0
- {nc_gcode_interpreter-0.2.3 → nc_gcode_interpreter-0.2.4}/python/nc_gcode_interpreter/viz.py +0 -0
- {nc_gcode_interpreter-0.2.3 → nc_gcode_interpreter-0.2.4}/python/tests/test_arithmetic_functions.py +0 -0
- {nc_gcode_interpreter-0.2.3 → nc_gcode_interpreter-0.2.4}/python/tests/test_batch_variables.py +0 -0
- {nc_gcode_interpreter-0.2.3 → nc_gcode_interpreter-0.2.4}/python/tests/test_batches.py +0 -0
- {nc_gcode_interpreter-0.2.3 → nc_gcode_interpreter-0.2.4}/python/tests/test_diagnostics.py +0 -0
- {nc_gcode_interpreter-0.2.3 → nc_gcode_interpreter-0.2.4}/python/tests/test_expected_output.py +0 -0
- {nc_gcode_interpreter-0.2.3 → nc_gcode_interpreter-0.2.4}/python/tests/test_field_defects.py +0 -0
- {nc_gcode_interpreter-0.2.3 → nc_gcode_interpreter-0.2.4}/python/tests/test_flags.py +0 -0
- {nc_gcode_interpreter-0.2.3 → nc_gcode_interpreter-0.2.4}/python/tests/test_flatten.py +0 -0
- {nc_gcode_interpreter-0.2.3 → nc_gcode_interpreter-0.2.4}/python/tests/test_g_groups.py +0 -0
- {nc_gcode_interpreter-0.2.3 → nc_gcode_interpreter-0.2.4}/python/tests/test_g_vocabulary.py +0 -0
- {nc_gcode_interpreter-0.2.3 → nc_gcode_interpreter-0.2.4}/python/tests/test_jumps.py +0 -0
- {nc_gcode_interpreter-0.2.3 → nc_gcode_interpreter-0.2.4}/python/tests/test_real_comparisons.py +0 -0
- {nc_gcode_interpreter-0.2.3 → nc_gcode_interpreter-0.2.4}/python/tests/test_stage1.py +0 -0
- {nc_gcode_interpreter-0.2.3 → nc_gcode_interpreter-0.2.4}/python/tests/test_streaming.py +0 -0
- {nc_gcode_interpreter-0.2.3 → nc_gcode_interpreter-0.2.4}/python/tests/test_structured_errors.py +0 -0
- {nc_gcode_interpreter-0.2.3 → nc_gcode_interpreter-0.2.4}/python/tests/test_unsupported.py +0 -0
- {nc_gcode_interpreter-0.2.3 → nc_gcode_interpreter-0.2.4}/rustfmt.toml +0 -0
- {nc_gcode_interpreter-0.2.3 → nc_gcode_interpreter-0.2.4}/src/errors.rs +0 -0
- {nc_gcode_interpreter-0.2.3 → nc_gcode_interpreter-0.2.4}/src/flatten.rs +0 -0
- {nc_gcode_interpreter-0.2.3 → nc_gcode_interpreter-0.2.4}/src/grammar.pest +0 -0
- {nc_gcode_interpreter-0.2.3 → nc_gcode_interpreter-0.2.4}/src/lib.rs +0 -0
- {nc_gcode_interpreter-0.2.3 → nc_gcode_interpreter-0.2.4}/src/line_driver.rs +0 -0
- {nc_gcode_interpreter-0.2.3 → nc_gcode_interpreter-0.2.4}/src/main.rs +0 -0
- {nc_gcode_interpreter-0.2.3 → nc_gcode_interpreter-0.2.4}/src/modal_groups.rs +0 -0
- {nc_gcode_interpreter-0.2.3 → nc_gcode_interpreter-0.2.4}/src/structure_scan.rs +0 -0
- {nc_gcode_interpreter-0.2.3 → nc_gcode_interpreter-0.2.4}/src/types.rs +0 -0
- {nc_gcode_interpreter-0.2.3 → nc_gcode_interpreter-0.2.4}/tests/cli.rs +0 -0
- {nc_gcode_interpreter-0.2.3 → nc_gcode_interpreter-0.2.4}/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.4] - 2026-07-08
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- Interpreter throughput: the hot, closed-vocabulary lookup maps (`axes`,
|
|
12
|
+
`translation`, `output_keys`, and the table builder's per-cell/forward-fill
|
|
13
|
+
maps) now use the non-cryptographic FxHash hasher, and `State::update_axis`
|
|
14
|
+
overwrites in place instead of re-allocating the key on repeat writes. ~17%
|
|
15
|
+
off the collect (dataframe) path on the large real-world benchmark, with no
|
|
16
|
+
behavior change. `symbol_table` deliberately stays on the default SipHash
|
|
17
|
+
hasher: its keys are user-controlled variable names, so it keeps its
|
|
18
|
+
hash-flooding resistance (#60).
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
- Experimental execution-cursor interpreter (opt-in via `NC_VM=1`): the
|
|
23
|
+
recursive control-flow walk is reified into an explicit frame stack, enabling
|
|
24
|
+
in-memory checkpoint/resume. Off by default; the standard path is unchanged.
|
|
25
|
+
Groundwork for resumable/streaming interpretation (#47, #59).
|
|
26
|
+
|
|
7
27
|
## [v0.2.3] - 2026-07-07
|
|
8
28
|
|
|
9
29
|
### Added
|
|
@@ -506,7 +506,7 @@ checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4"
|
|
|
506
506
|
|
|
507
507
|
[[package]]
|
|
508
508
|
name = "nc-gcode-interpreter"
|
|
509
|
-
version = "0.2.
|
|
509
|
+
version = "0.2.4"
|
|
510
510
|
dependencies = [
|
|
511
511
|
"arrow-array",
|
|
512
512
|
"arrow-data",
|
|
@@ -517,6 +517,7 @@ dependencies = [
|
|
|
517
517
|
"pest_derive",
|
|
518
518
|
"pyo3",
|
|
519
519
|
"rayon",
|
|
520
|
+
"rustc-hash",
|
|
520
521
|
"thiserror",
|
|
521
522
|
]
|
|
522
523
|
|
|
@@ -726,6 +727,12 @@ dependencies = [
|
|
|
726
727
|
"crossbeam-utils",
|
|
727
728
|
]
|
|
728
729
|
|
|
730
|
+
[[package]]
|
|
731
|
+
name = "rustc-hash"
|
|
732
|
+
version = "2.1.3"
|
|
733
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
734
|
+
checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d"
|
|
735
|
+
|
|
729
736
|
[[package]]
|
|
730
737
|
name = "rustversion"
|
|
731
738
|
version = "1.0.22"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "nc-gcode-interpreter"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.4"
|
|
4
4
|
edition = "2021"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
|
|
@@ -19,6 +19,7 @@ pest_derive = "2.8.6"
|
|
|
19
19
|
thiserror = "2.0.18"
|
|
20
20
|
clap = { version = "4.6", features = ["derive"] }
|
|
21
21
|
rayon = "1.10"
|
|
22
|
+
rustc-hash = "2.1"
|
|
22
23
|
|
|
23
24
|
# NOTE: keep every dependency version inline (`crate = { version = "..." }`),
|
|
24
25
|
# NOT as a `[dependencies.crate]` block with a standalone `version = "..."`
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
# Resumable interpretation — checkpoint/resume design (#47, targeting 0.3.0)
|
|
2
|
+
|
|
3
|
+
Status: **design + proof-of-concept**. This document scopes the resumable-`NcSession`
|
|
4
|
+
feature requested in issue #47 and the interpreter change it requires. A working
|
|
5
|
+
Phase-1 proof of concept — the execution-cursor VM (`src/vm.rs`, opt-in via
|
|
6
|
+
`NC_VM=1`) plus in-memory checkpoint/resume — now exists alongside this design;
|
|
7
|
+
the public API and Phases 2–3 remain future work.
|
|
8
|
+
|
|
9
|
+
## 1. Goal and consumer requirements
|
|
10
|
+
|
|
11
|
+
Expose a resumable interpretation handle:
|
|
12
|
+
|
|
13
|
+
- `checkpoint() -> token` — capture the complete execution position roughly
|
|
14
|
+
every *N* output rows.
|
|
15
|
+
- `resume(token, patched_text)` — restart interpretation from a checkpoint,
|
|
16
|
+
against an edited program.
|
|
17
|
+
|
|
18
|
+
Consumer: ribweaver `/sim` phase-E *incremental re-interpret* for multi-million-row
|
|
19
|
+
loop expansions. When the user edits source line *L*, it resumes from the last
|
|
20
|
+
checkpoint whose block cursor precedes *L* instead of re-running the whole program.
|
|
21
|
+
Downstream is evidence-gated: debounced-full re-interpret is fine below ~10⁵ rows,
|
|
22
|
+
so the resume path only has to pay off for the *giant* case — which is exactly the
|
|
23
|
+
hard one, because a million-row expansion is typically **one** top-level `WHILE`/`LOOP`.
|
|
24
|
+
Top-level-only checkpoints would yield a single checkpoint for the whole expansion
|
|
25
|
+
and be useless; checkpoints must be capturable **mid-loop**.
|
|
26
|
+
|
|
27
|
+
Cancellation already exists (drop the iterator → `StreamClosed`); resume composes
|
|
28
|
+
with it as the latest-wins primitive.
|
|
29
|
+
|
|
30
|
+
## 2. Why this is hard — the crux
|
|
31
|
+
|
|
32
|
+
The issue's premise ("no call stack exists, so block-granularity checkpoints are
|
|
33
|
+
tractable") is half right. There is no *subprogram* call stack (#48), but the
|
|
34
|
+
**control structures form an implicit stack**, and the instruction pointer lives
|
|
35
|
+
on the **Rust call stack**, not in any serializable structure.
|
|
36
|
+
|
|
37
|
+
Precise findings (`src/interpret_rules.rs`):
|
|
38
|
+
|
|
39
|
+
- The instruction pointer is `index: usize`, a **stack-local** in `run_blocks`
|
|
40
|
+
(`:2071-2103`) — one per active scope. There is no IP counter in `State`.
|
|
41
|
+
- Every control construct drives its body by **recursively calling**
|
|
42
|
+
`interpret_blocks`, holding borrowed `Pair<Rule>` iterators and a `loop_count`
|
|
43
|
+
as stack locals: `WHILE` (`:1386`), `LOOP` (`:1413`), `FOR` (`:1434`),
|
|
44
|
+
`REPEAT/UNTIL` (`:1493`), `IF/ELSE` (`:1305`), `CASE` (`:1623`).
|
|
45
|
+
- `Pair<Rule>` is `pest::iterators::Pair<'i, Rule>` — a **borrowed cursor into the
|
|
46
|
+
parse tree**, tied to the lifetime of the input `&str`. Execution positions are
|
|
47
|
+
expressed as *paths within the pest tree*: inherently non-serializable.
|
|
48
|
+
- `loop_count` (WHILE/LOOP/REPEAT) is **purely stack-local** — not reflected in
|
|
49
|
+
`State`. `FOR`'s counter *is* in `symbol_table`, but its `end_value` bound and
|
|
50
|
+
`variable_name` are stack-local. Loop *conditions* re-evaluate from `State` each
|
|
51
|
+
iteration, so they are re-derivable — but the position within the tree is not.
|
|
52
|
+
- `IF` branch choice is implicit in *which recursive call is currently live*.
|
|
53
|
+
- `BlockFlow::Jump` bubbles up through scopes via `resolve_jump` (`:184`); an
|
|
54
|
+
unresolved jump propagates to the enclosing `run_blocks` (`:2098`) — that is how
|
|
55
|
+
a jump leaves a loop or IF body. `EndProgram` (M2/M17/M30) terminates.
|
|
56
|
+
|
|
57
|
+
**Paused three loops deep inside an IF**, the resume position that is *not* in
|
|
58
|
+
`State` is: each nested `index`, each `jumps_taken`, each `loop_count`, each loop's
|
|
59
|
+
cached `end_value`/condition/blocks pest pairs, the live IF branch, and every
|
|
60
|
+
`block_pairs` slice cursor — all borrowed views into the pest tree.
|
|
61
|
+
|
|
62
|
+
So mid-loop resume requires **reifying the control stack** into an explicit,
|
|
63
|
+
serializable structure: replacing recursion with an execution cursor.
|
|
64
|
+
|
|
65
|
+
## 3. Convergence — this is already on the roadmap
|
|
66
|
+
|
|
67
|
+
`docs/sinumerik-execution-model.md` §"Scaling limits and the road to streaming",
|
|
68
|
+
point 4, already prescribes this exact change for gigabyte-scale programs:
|
|
69
|
+
|
|
70
|
+
> *Execution cursor* instead of recursion, yielding `(line_no, row)` — … makes
|
|
71
|
+
> seek-by-checkpoint (snapshot of `pc` + `State`) cheap.
|
|
72
|
+
|
|
73
|
+
So #47 is **not a throwaway rewrite**. The execution cursor it needs is the same
|
|
74
|
+
one already planned for streaming/scaling. Reifying the control stack:
|
|
75
|
+
|
|
76
|
+
1. unblocks checkpoint/resume (#47),
|
|
77
|
+
2. unblocks O(1)-start lazy streaming for >10⁷-line programs, and
|
|
78
|
+
3. removes the deep-recursion stack-depth risk on pathological nesting.
|
|
79
|
+
|
|
80
|
+
One change, three payoffs — that is the argument for spending a 0.3.0 on it.
|
|
81
|
+
|
|
82
|
+
## 4. What a checkpoint must capture (full inventory)
|
|
83
|
+
|
|
84
|
+
Assembled from a line-anchored audit of the three state-bearing subsystems.
|
|
85
|
+
|
|
86
|
+
### 4.1 `State` (`src/state.rs:52-86`) — already `#[derive(Clone)]`, cheap
|
|
87
|
+
`axes`, `symbol_table`, `string_table`, `translation`, `jump_scopes` (label sets,
|
|
88
|
+
mirrors scope depth but not the index within each), `seen_jump_targets`,
|
|
89
|
+
`warned_addresses`; config (`axis_identifiers`, `iteration_limit`,
|
|
90
|
+
`axis_index_map`, `allow_undefined_variables`, `output_keys`); `input`/`line_offsets`
|
|
91
|
+
shared via `Arc` so clone is O(live symbols). ✅ Already snapshot-ready in memory.
|
|
92
|
+
|
|
93
|
+
### 4.2 Control-flow position (`src/interpret_rules.rs`) — THE reified stack
|
|
94
|
+
Not in `State` today; must become an explicit `Vec<Frame>`:
|
|
95
|
+
- per scope: `index` (IP), `jumps_taken`;
|
|
96
|
+
- per loop frame: kind (WHILE/LOOP/FOR/REPEAT), `loop_count`, and cached bounds
|
|
97
|
+
(`FOR` `end_value` + `variable_name`);
|
|
98
|
+
- the scope/branch identity (which blocks list, which IF branch) as a **stable
|
|
99
|
+
address**, not a borrowed pest `Pair`.
|
|
100
|
+
|
|
101
|
+
### 4.3 Flattener (`src/flatten.rs:130-150`) — NOT currently `Clone`
|
|
102
|
+
- Reconstructable from `new(tolerance, axis_identifiers)`: `tolerance`,
|
|
103
|
+
`geometric_axes`.
|
|
104
|
+
- Must snapshot: `positions`, `motion` (modal), `plane` (modal), `spline_buffer`
|
|
105
|
+
(raw buffered `Row`s of an in-progress multi-block spline — no compressed form,
|
|
106
|
+
capture verbatim), `spline_start`, `spline_degree`, `warned_motions` (warn-once).
|
|
107
|
+
- Blockers: `SplineItem` needs a `Clone` derive (mechanical; wraps `Row: Clone`);
|
|
108
|
+
interned `&'static str` keys re-intern via `intern_column` on *disk* restore only.
|
|
109
|
+
|
|
110
|
+
### 4.4 Output pipeline (`src/output.rs`)
|
|
111
|
+
- `OutputRows::current` — the **in-flight row** for the block being interpreted
|
|
112
|
+
(not yet flushed to the sink); `warned_g91` latch. A checkpoint between blocks
|
|
113
|
+
must capture `current` together with the sink, and reproduce the
|
|
114
|
+
flush-on-next-`start_row` ordering so no row is dropped or double-emitted.
|
|
115
|
+
- Batch path forward-fill carry (`BatchBuilder`): `columns` (growing canonical
|
|
116
|
+
set, monotonic — resuming with an empty set changes later-batch schema),
|
|
117
|
+
`fill` (last-seen value per forward-filled column — the core carry).
|
|
118
|
+
- `BatchStreamSink`: `buffer` (rows since last emitted batch), `events`,
|
|
119
|
+
`name_ids`, `output_row_count` (drives `row_idx` attribution and batch cadence).
|
|
120
|
+
- Plumbing that a checkpoint *cannot* recreate: the channel handles (`Stream`
|
|
121
|
+
sender, batch `sender`, `events_sender`). Resume attaches fresh channels.
|
|
122
|
+
- Process-global (outside any checkpoint): the `intern_column` `OnceLock` pool —
|
|
123
|
+
serialize key *content*, rehydrate through `intern_column`.
|
|
124
|
+
|
|
125
|
+
## 5. Design: the reified execution VM
|
|
126
|
+
|
|
127
|
+
### 5.1 Addressing a position without a borrowed pest `Pair`
|
|
128
|
+
The single change that makes everything else fall out: express an execution
|
|
129
|
+
position as a **stable, owned address** instead of a `pest::Pair` cursor.
|
|
130
|
+
|
|
131
|
+
Two candidate representations:
|
|
132
|
+
|
|
133
|
+
- **(a) Path into the retained tree.** Keep the parse tree alive (already `Arc`-able
|
|
134
|
+
via the input) and address a block by a `Vec<u32>` path of child indices from the
|
|
135
|
+
root. Serializable (just integers); resolved to a `Pair` by walking `into_inner`.
|
|
136
|
+
Smallest change; still tied to pest at runtime.
|
|
137
|
+
- **(b) Pre-flattened owned IR (the execution cursor).** One whole-file pass lowers
|
|
138
|
+
the block/control tree into an owned, indexable program: a `Vec` of block
|
|
139
|
+
instructions with explicit scope boundaries and pre-resolved jump tables (a
|
|
140
|
+
whole-program `scan_jump_targets`). Execution is a `pc` over the IR with an
|
|
141
|
+
explicit frame stack; pest is gone at runtime. This is the roadmap's "execution
|
|
142
|
+
cursor" — larger, but unlocks lazy parse (Phase 3) and full serialization.
|
|
143
|
+
|
|
144
|
+
**Recommendation: target (b).** It is the roadmap direction; doing (a) then (b) is
|
|
145
|
+
double work. (a) is a viable *interim* only if we need #47 shipped before the IR
|
|
146
|
+
lands.
|
|
147
|
+
|
|
148
|
+
### 5.2 The frame stack and the token
|
|
149
|
+
```
|
|
150
|
+
Frame = { scope: ScopeId, ip: u32, jumps_taken: u32, loop: Option<LoopState> }
|
|
151
|
+
LoopState = While { count } | Loop { count } | Repeat { count }
|
|
152
|
+
| For { count, var: SymbolId, end: f64 }
|
|
153
|
+
Checkpoint = {
|
|
154
|
+
program_fingerprint, // structural hash of the program prefix ≤ cursor
|
|
155
|
+
stack: Vec<Frame>, // the reified control stack (innermost last)
|
|
156
|
+
state: State, // cloned
|
|
157
|
+
flattener: FlattenerSnapshot, // §4.3 evolving fields
|
|
158
|
+
output_carry: OutputCarry, // §4.4 forward-fill + counts + in-flight row
|
|
159
|
+
cursor_line_no: u32, // for the consumer's "checkpoint precedes L" gate
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
Loop conditions are **not** stored — they re-evaluate from `state` on resume,
|
|
163
|
+
exactly as the interpreter does each iteration today. That is what keeps the token
|
|
164
|
+
small and correct.
|
|
165
|
+
|
|
166
|
+
### 5.3 `checkpoint()` / `resume()` semantics
|
|
167
|
+
- `checkpoint()` is valid only at a **between-rows boundary** (`output.start_row`,
|
|
168
|
+
`interpret_rules.rs:2005`) where the in-flight `current` row is well-defined.
|
|
169
|
+
Emit checkpoints every *N* output rows.
|
|
170
|
+
- `resume(token, patched_text)`:
|
|
171
|
+
1. Re-lower `patched_text` to the IR.
|
|
172
|
+
2. Verify `program_fingerprint` against the patched program's prefix up to
|
|
173
|
+
`cursor_line_no`. If the edit changed block structure *before* the cursor, the
|
|
174
|
+
stored `ip`/`ScopeId`s are stale → **fail loudly** (`ParsingError`, a new
|
|
175
|
+
`kind = "checkpoint_invalidated"`), never resume into a shifted tree. This
|
|
176
|
+
matches the consumer's own gate ("last checkpoint whose block cursor precedes
|
|
177
|
+
L") and the project's loud-failure-over-silent-wrongness rule.
|
|
178
|
+
3. Restore `state` + frame stack + flattener + output carry; attach fresh output
|
|
179
|
+
channels; continue the VM loop from `stack`.
|
|
180
|
+
|
|
181
|
+
### 5.4 Resume + edited text — the validity contract
|
|
182
|
+
A checkpoint is resumable against a patched program **iff the patch lies at or
|
|
183
|
+
after the checkpoint's block cursor**. Enforced by the prefix fingerprint. The
|
|
184
|
+
consumer already honours this; the API makes it a checked invariant rather than a
|
|
185
|
+
convention.
|
|
186
|
+
|
|
187
|
+
## 6. Phasing
|
|
188
|
+
|
|
189
|
+
- **Phase 1 — reify the stack (delivers #47).** Replace the recursive tree-walk in
|
|
190
|
+
`run_blocks`/`interpret_*` with the explicit VM + frame stack over IR (b), keeping
|
|
191
|
+
eager whole-file parse. Add `NcSession` with in-memory, **same-process**
|
|
192
|
+
`checkpoint()`/`resume()` (token holds owned `State`/frames/snapshots; no disk
|
|
193
|
+
serialization yet). This is everything the consumer needs.
|
|
194
|
+
- **Phase 2 — serializable token.** `Serialize`/`Deserialize` for the snapshots;
|
|
195
|
+
re-intern `&'static str` keys on load; address symbols/scopes by stable ids.
|
|
196
|
+
Only if cross-process/persisted checkpoints are wanted.
|
|
197
|
+
- **Phase 3 — lazy parse / O(1) start.** The streaming payoff: lazy per-line parse
|
|
198
|
+
feeding the same VM, demoting pest to the rare structural lines (the two-pass
|
|
199
|
+
design already prototyped in `src/line_driver.rs`). Independent follow-on.
|
|
200
|
+
|
|
201
|
+
Phases 2 and 3 are optional and can slip past 0.3.0; Phase 1 is the release.
|
|
202
|
+
|
|
203
|
+
## 7. Correctness strategy
|
|
204
|
+
|
|
205
|
+
The VM must be a **behaviour-preserving** refactor of the tree-walker before
|
|
206
|
+
`checkpoint` is even exposed. Guardrails:
|
|
207
|
+
|
|
208
|
+
- **Differential parity**: run the whole existing golden-file suite + the mill-sim
|
|
209
|
+
corpus through both the old recursive path and the new VM, asserting byte-identical
|
|
210
|
+
output — the same discipline `test_stage1.py` already uses for the line-driver.
|
|
211
|
+
- **Checkpoint/resume equivalence**: for a program run straight through vs.
|
|
212
|
+
checkpointed-and-resumed at every row boundary, assert identical `(line_no, row)`
|
|
213
|
+
streams and identical final `State` — including the hard cases: mid-`WHILE`,
|
|
214
|
+
mid-`FOR` (counter in `State`, bound in the frame), mid-spline (buffered
|
|
215
|
+
`spline_buffer`), a jump out of a loop body, and forward-fill continuity across a
|
|
216
|
+
resumed batch boundary.
|
|
217
|
+
- **Invalidation**: edits before the cursor must fail loudly, never silently resume.
|
|
218
|
+
|
|
219
|
+
## 8. Rough scope
|
|
220
|
+
|
|
221
|
+
Phase 1 is real interpreter surgery — the IR lowering, the VM loop, the frame
|
|
222
|
+
stack, `FlattenerSnapshot`/`OutputCarry` capture-restore, and the parity harness —
|
|
223
|
+
but it is bounded and well-understood after this audit, and it is load-bearing for
|
|
224
|
+
the streaming roadmap regardless of #47. Estimate: the dominant 0.3.0 work item.
|
|
225
|
+
Phases 2–3 are additive and independently schedulable.
|
|
226
|
+
|
|
227
|
+
## 9. Open questions for review
|
|
228
|
+
- (b) IR now vs (a) tree-path interim — how urgently does the consumer need #47
|
|
229
|
+
relative to the streaming work? If both are wanted this quarter, go straight to (b).
|
|
230
|
+
- Token type: opaque handle only (Phase 1) vs. a documented serializable schema —
|
|
231
|
+
affects how much of §5.2 is public API from day one.
|
|
232
|
+
- Checkpoint cadence policy: fixed *N* rows vs. adaptive (e.g. denser near loop
|
|
233
|
+
headers so an edit resumes closer to *L*).
|
|
@@ -7,6 +7,11 @@ use std::collections::HashMap;
|
|
|
7
7
|
|
|
8
8
|
type Output = crate::output::OutputRows;
|
|
9
9
|
|
|
10
|
+
// Experimental execution-cursor VM (#47), a child module so it can reuse this
|
|
11
|
+
// module's private leaf fns via `super::`. Gated behind `NC_VM=1`.
|
|
12
|
+
#[path = "vm.rs"]
|
|
13
|
+
pub(crate) mod vm;
|
|
14
|
+
|
|
10
15
|
/// Control-flow signal returned by block interpretation: either fall through
|
|
11
16
|
/// to the next block, or a pending GOTO that must be resolved against the
|
|
12
17
|
/// block list of the current scope or, failing that, an enclosing scope
|
|
@@ -1850,8 +1855,11 @@ fn interpret_statement(element: Pair<Rule>, output: &mut Output, state: &mut Sta
|
|
|
1850
1855
|
match state.resolve_output_key(&key) {
|
|
1851
1856
|
Some((ColKind::Axis, skey)) => {
|
|
1852
1857
|
// State keeps local coordinates; the output row gets the machine
|
|
1853
|
-
// coordinate under the translation active
|
|
1854
|
-
|
|
1858
|
+
// coordinate under the translation active here. `update_axis` just
|
|
1859
|
+
// stored this axis' local == `local_value`, so machine =
|
|
1860
|
+
// local_value + translation — one lookup instead of
|
|
1861
|
+
// `get_axis_machine`'s two (it would re-read the axis we just set).
|
|
1862
|
+
let machine_value = local_value + state.get_translation(skey);
|
|
1855
1863
|
last.insert(skey, Value::Float(machine_value));
|
|
1856
1864
|
}
|
|
1857
1865
|
Some((ColKind::Block, skey)) => {
|
|
@@ -270,7 +270,14 @@ fn interpret_file(input: &str, state: &mut State, output: &mut OutputRows) -> Re
|
|
|
270
270
|
message: "No inner blocks found".to_string(),
|
|
271
271
|
})?;
|
|
272
272
|
|
|
273
|
-
|
|
273
|
+
// Experimental: the explicit-stack VM (#47) runs the same structured path
|
|
274
|
+
// when NC_VM=1, for differential parity against the recursive walker.
|
|
275
|
+
let flow = if crate::interpret_rules::vm::vm_enabled() {
|
|
276
|
+
crate::interpret_rules::vm::run(blocks, output, state)?
|
|
277
|
+
} else {
|
|
278
|
+
interpret_blocks(blocks, output, state)?
|
|
279
|
+
};
|
|
280
|
+
match flow {
|
|
274
281
|
BlockFlow::Continue | BlockFlow::EndProgram => Ok(()),
|
|
275
282
|
// A jump that no scope could resolve: the destination does not exist
|
|
276
283
|
// in the programmed search direction (alarm 14080 on a real control).
|
|
@@ -14,6 +14,11 @@ use crate::types::Value;
|
|
|
14
14
|
use std::collections::{HashMap, HashSet};
|
|
15
15
|
use std::sync::{Mutex, OnceLock};
|
|
16
16
|
|
|
17
|
+
/// FxHash map for the hot table-build lookups (`index_of`, per cell per row;
|
|
18
|
+
/// `fill`, per column per batch) — trusted `&'static str` keys, no need for
|
|
19
|
+
/// SipHash's DoS resistance. See [`crate::state::FxMap`].
|
|
20
|
+
type FxMap<K, V> = HashMap<K, V, rustc_hash::FxBuildHasher>;
|
|
21
|
+
|
|
17
22
|
/// Intern a column name to a process-stable `&'static str`.
|
|
18
23
|
///
|
|
19
24
|
/// The set of distinct output-column names is a small closed vocabulary
|
|
@@ -378,6 +383,39 @@ impl OutputRows {
|
|
|
378
383
|
self.flattener = Some(flattener);
|
|
379
384
|
}
|
|
380
385
|
|
|
386
|
+
/// Number of committed rows in a `Collect` sink (0 for streaming sinks).
|
|
387
|
+
/// Lets the experimental resumable VM (#47) pause at a row boundary.
|
|
388
|
+
#[allow(dead_code)]
|
|
389
|
+
pub(crate) fn collected_len(&self) -> usize {
|
|
390
|
+
match &self.sink {
|
|
391
|
+
RowSink::Collect(rows) => rows.len(),
|
|
392
|
+
_ => 0,
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/// Deep-clone a checkpointable `Collect`-sink output: the committed rows,
|
|
397
|
+
/// the in-flight row, and the warn latches. `None` when the sink is a
|
|
398
|
+
/// streaming channel or a flattener is installed (their state isn't
|
|
399
|
+
/// snapshottable this simply). For the experimental resumable VM (#47);
|
|
400
|
+
/// the whole-file forward-fill happens later in `Table::from_rows`, so a
|
|
401
|
+
/// Collect snapshot needs no columnar carry.
|
|
402
|
+
#[allow(dead_code)]
|
|
403
|
+
pub(crate) fn snapshot_collect(&self) -> Option<OutputRows> {
|
|
404
|
+
if self.flattener.is_some() {
|
|
405
|
+
return None;
|
|
406
|
+
}
|
|
407
|
+
match &self.sink {
|
|
408
|
+
RowSink::Collect(rows) => Some(OutputRows {
|
|
409
|
+
current: self.current.clone(),
|
|
410
|
+
sink: RowSink::Collect(rows.clone()),
|
|
411
|
+
record_variables: self.record_variables,
|
|
412
|
+
flattener: None,
|
|
413
|
+
warned_g91: self.warned_g91,
|
|
414
|
+
}),
|
|
415
|
+
_ => None,
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
381
419
|
/// Route a finished row to the sink, passing it through the flattener
|
|
382
420
|
/// first when one is installed. Shared by `flush`.
|
|
383
421
|
fn deliver(&mut self, row: Row) -> Result<(), ParsingError> {
|
|
@@ -710,7 +748,7 @@ pub struct BatchBuilder {
|
|
|
710
748
|
/// table.
|
|
711
749
|
columns: Vec<&'static str>,
|
|
712
750
|
/// Last carried non-null value per forward-filled column.
|
|
713
|
-
fill:
|
|
751
|
+
fill: FxMap<&'static str, Carry>,
|
|
714
752
|
}
|
|
715
753
|
|
|
716
754
|
impl BatchBuilder {
|
|
@@ -719,7 +757,7 @@ impl BatchBuilder {
|
|
|
719
757
|
disable_forward_fill,
|
|
720
758
|
emit_line_no: false,
|
|
721
759
|
columns: Vec::new(),
|
|
722
|
-
fill:
|
|
760
|
+
fill: FxMap::default(),
|
|
723
761
|
}
|
|
724
762
|
}
|
|
725
763
|
|
|
@@ -756,7 +794,8 @@ impl BatchBuilder {
|
|
|
756
794
|
// One typed builder per column, in canonical order, each pre-filled
|
|
757
795
|
// with `height` nulls. A name->position index lets each cell find its
|
|
758
796
|
// builder in O(1).
|
|
759
|
-
let mut index_of:
|
|
797
|
+
let mut index_of: FxMap<&'static str, usize> =
|
|
798
|
+
FxMap::with_capacity_and_hasher(self.columns.len(), Default::default());
|
|
760
799
|
let mut builders: Vec<ColumnBuilder> = Vec::with_capacity(self.columns.len());
|
|
761
800
|
for (position, &name) in self.columns.iter().enumerate() {
|
|
762
801
|
index_of.insert(name, position);
|
|
@@ -765,7 +804,9 @@ impl BatchBuilder {
|
|
|
765
804
|
|
|
766
805
|
// Single pass: dispatch each present cell to its column builder. Every
|
|
767
806
|
// cell key is in `present` (hence in `index_of`), so the lookup always
|
|
768
|
-
// hits; the `if let` is defensive only.
|
|
807
|
+
// hits; the `if let` is defensive only. FxHash beat a linear scan of
|
|
808
|
+
// `self.columns` here (~22 keys need content compare — interning does
|
|
809
|
+
// NOT guarantee pointer identity across all column sources).
|
|
769
810
|
for (row_index, cell) in cells.iter().enumerate() {
|
|
770
811
|
for (&key, value) in cell.iter() {
|
|
771
812
|
if let Some(&position) = index_of.get(key) {
|
|
@@ -2,6 +2,20 @@ use crate::errors::ParsingError;
|
|
|
2
2
|
use std::collections::{HashMap, HashSet};
|
|
3
3
|
use std::sync::Arc;
|
|
4
4
|
|
|
5
|
+
/// A `HashMap` using the non-cryptographic FxHash instead of the default
|
|
6
|
+
/// SipHash. Reserved for hot maps with a *closed, trusted* key vocabulary:
|
|
7
|
+
/// `axes`, `translation`, `output_keys` — axis/block names fixed at
|
|
8
|
+
/// construction, hit millions of times a run. Profiling the 1.1 GB file showed
|
|
9
|
+
/// SipHash of these keys (esp. in `update_axis`) as a top cost, and with a
|
|
10
|
+
/// closed key set there is nothing to hash-flood.
|
|
11
|
+
///
|
|
12
|
+
/// NOTE: deliberately *not* used for `symbol_table`, whose keys are
|
|
13
|
+
/// user-controlled, unbounded-length variable names parsed from the input.
|
|
14
|
+
/// That map stays on SipHash to preserve hash-flooding (DoS) resistance on
|
|
15
|
+
/// crafted programs; it is also cold on the CAM-flood workloads that motivated
|
|
16
|
+
/// this change, so the default hasher costs nothing there.
|
|
17
|
+
pub(crate) type FxMap<K, V> = HashMap<K, V, rustc_hash::FxBuildHasher>;
|
|
18
|
+
|
|
5
19
|
/// Emit an interpreter warning to stderr. Callers pass `format_args!(...)` so
|
|
6
20
|
/// the message is only formatted at the point of emission.
|
|
7
21
|
pub fn emit_warning(args: std::fmt::Arguments) {
|
|
@@ -51,12 +65,15 @@ pub enum ColKind {
|
|
|
51
65
|
|
|
52
66
|
#[derive(Debug, Clone)]
|
|
53
67
|
pub struct State {
|
|
54
|
-
pub axes:
|
|
68
|
+
pub axes: FxMap<String, f64>,
|
|
69
|
+
/// Numeric variables. Keys are user-controlled variable names parsed from
|
|
70
|
+
/// the input, so this map stays on the default SipHash hasher for
|
|
71
|
+
/// hash-flooding resistance (see [`FxMap`]).
|
|
55
72
|
pub symbol_table: HashMap<String, f64>,
|
|
56
73
|
/// String variables (DEF STRING[n]); kept apart from the numeric
|
|
57
74
|
/// symbol_table - using one in a numeric expression is a loud error.
|
|
58
75
|
pub string_table: HashMap<String, String>,
|
|
59
|
-
pub translation:
|
|
76
|
+
pub translation: FxMap<String, f64>,
|
|
60
77
|
pub axis_identifiers: Vec<String>,
|
|
61
78
|
pub iteration_limit: usize,
|
|
62
79
|
pub axis_index_map: Option<HashMap<String, usize>>,
|
|
@@ -82,7 +99,7 @@ pub struct State {
|
|
|
82
99
|
/// lookup falls back to uppercasing only when a direct (already-uppercase)
|
|
83
100
|
/// hit misses. Built once at construction from the axis identifiers and the
|
|
84
101
|
/// fixed block addresses.
|
|
85
|
-
output_keys:
|
|
102
|
+
output_keys: FxMap<String, (ColKind, &'static str)>,
|
|
86
103
|
}
|
|
87
104
|
|
|
88
105
|
impl State {
|
|
@@ -99,11 +116,11 @@ impl State {
|
|
|
99
116
|
axis_index_map: Option<HashMap<String, usize>>,
|
|
100
117
|
allow_undefined_variables: bool,
|
|
101
118
|
) -> Self {
|
|
102
|
-
let mut symbols = HashMap::new();
|
|
119
|
+
let mut symbols: HashMap<String, f64> = HashMap::new();
|
|
103
120
|
symbols.insert("TRUE".to_string(), 1.0);
|
|
104
121
|
symbols.insert("FALSE".to_string(), 0.0);
|
|
105
122
|
|
|
106
|
-
let mut translation =
|
|
123
|
+
let mut translation = FxMap::default();
|
|
107
124
|
for axis in &axis_identifiers {
|
|
108
125
|
translation.insert(axis.clone(), 0.0);
|
|
109
126
|
}
|
|
@@ -120,7 +137,7 @@ impl State {
|
|
|
120
137
|
// Pre-resolve every output-column key to its interned &'static str
|
|
121
138
|
// once, up front. Axis identifiers are case-insensitive on lookup, so
|
|
122
139
|
// the registry is keyed by the uppercased name.
|
|
123
|
-
let mut output_keys:
|
|
140
|
+
let mut output_keys: FxMap<String, (ColKind, &'static str)> = FxMap::default();
|
|
124
141
|
for axis in &axis_identifiers {
|
|
125
142
|
let upper = axis.to_uppercase();
|
|
126
143
|
let interned = crate::output::intern_column(&upper);
|
|
@@ -135,7 +152,7 @@ impl State {
|
|
|
135
152
|
}
|
|
136
153
|
|
|
137
154
|
State {
|
|
138
|
-
axes:
|
|
155
|
+
axes: FxMap::default(),
|
|
139
156
|
symbol_table: symbols,
|
|
140
157
|
string_table: HashMap::new(),
|
|
141
158
|
translation,
|
|
@@ -262,8 +279,16 @@ impl State {
|
|
|
262
279
|
/// Updates an axis value in local coordinates (without translation).
|
|
263
280
|
/// Returns the machine coordinate (local + translation) for output purposes.
|
|
264
281
|
pub fn update_axis(&mut self, key: &str, local_value: f64) -> Result<f64, ParsingError> {
|
|
265
|
-
// Store the local coordinate
|
|
266
|
-
|
|
282
|
+
// Store the local coordinate. Get-mut first: after the first block that
|
|
283
|
+
// moves an axis, the key already exists, so the common path overwrites
|
|
284
|
+
// in place and allocates no String (HashMap::insert would take the key
|
|
285
|
+
// by value and allocate `key.to_string()` on every row).
|
|
286
|
+
match self.axes.get_mut(key) {
|
|
287
|
+
Some(slot) => *slot = local_value,
|
|
288
|
+
None => {
|
|
289
|
+
self.axes.insert(key.to_string(), local_value);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
267
292
|
// Return the machine coordinate for output
|
|
268
293
|
let translation_value = self.get_translation(key);
|
|
269
294
|
Ok(local_value + translation_value)
|
|
@@ -319,8 +344,8 @@ impl State {
|
|
|
319
344
|
#[derive(Debug, Clone, Default)]
|
|
320
345
|
#[allow(dead_code)] // fields read only by the python-feature bindings, not the bin
|
|
321
346
|
pub struct FinalState {
|
|
322
|
-
pub axes:
|
|
347
|
+
pub axes: FxMap<String, f64>,
|
|
323
348
|
pub symbol_table: HashMap<String, f64>,
|
|
324
|
-
pub translation:
|
|
349
|
+
pub translation: FxMap<String, f64>,
|
|
325
350
|
pub string_table: HashMap<String, String>,
|
|
326
351
|
}
|