Skip to content

Parse through gcode-ast instead of the hand-rolled scanner - #487

Draft
sophiedeziel wants to merge 3 commits into
developfrom
poc/gcode-ast-parser
Draft

Parse through gcode-ast instead of the hand-rolled scanner#487
sophiedeziel wants to merge 3 commits into
developfrom
poc/gcode-ast-parser

Conversation

@sophiedeziel

@sophiedeziel sophiedeziel commented Sep 8, 2026

Copy link
Copy Markdown
Collaborator

Swaps Parser.parseCommand to delegate to gcode-ast's parseLine + GCodeCommand compat view, then rewires the interpreter to dispatch on the typed AST nodes instead of lower-cased mnemonics.

Not for merge. Opened to make the tradeoff concrete.

Commit 1 — parse through gcode-ast

parseCommand delegates to parseLine; the local GCodeCommand class, GCodeParameters interface and isAlphaCode scanner are re-exported or deleted. Thumbnails, slicer detection and layer metadata are untouched.

Geometry is identical on all 7 demo fixtures (240 layers / 97,574 points on 3DBenchy, plus every JobStats field and the bounding box).

3DBenchy, parse → interpret:

time heap
current 69 ms 47 MB
this PR 182 ms 206 MB
if spans: false were reachable 153 ms 128 MB
if raw: false were too (loses src) 133 ms 104 MB

Two tokenizing regressions — both real gaps in gcode-ast, both with existing tests here:

  • G 1 E 42 X 42 (spaces between letter and number) → gcode: 'g' instead of 'g1'
  • G1 X1.2.3 Y5 → drops x entirely; we take the leading 1.2. Arguably better, still a behaviour change.

The perf knobs are unreachable. spans: false doesn't typecheck — toGCodeCommand requires a span. raw: false drops line.raw, and GCodeCommand.src is public API asserted by two tests.

GCodeCommand.node is most of the memory. The back-reference retains the whole typed node — words[], commandWord, spans — per line.

Commit 2 — typed dispatch

The registry is keyed on the AST command type, and each handler receives its node narrowed. Registering a handler for a type the parser cannot produce is now a compile error instead of an entry that never fires.

  • probe re-derived the three G31 forms from a P word and an axis-word count — now reads command.form
  • selectTool parsed the index out of the mnemonic — now reads command.index, so T8+ works instead of falling off a t0t7 registry
  • setInchUnits/setMillimeterUnits collapse into one setUnits; the node carries which unit it selects

Geometry is still identical on all 7 fixtures, and dispatch cost is neutral — the overhead is all in tokenizing. 873 / 875 tests pass (same 2 as commit 1). Coverage stays at 100%.

Two things worth review:

  • G92 reads "bare" off words, not the typed fields. The node names only X/Y/Z/E, so G92 F3000 would look bare through them and reset a workspace it should leave alone — there is a test pinning exactly that.
  • modal is deliberately unregistered. gcode-ast now emits a node for continuation lines like F140; registering a handler would make CNC files gain geometry they don't render today. Separate decision, separate PR.

Handler tests now build commands by parsing a real line instead of hand-pairing a mnemonic with a params bag, so they can no longer assert on a shape the parser would never produce.

Nothing here is public API — Interpreter, handlers and CommandHandler were never exported.

Still unsolved

Depends on github:xyz-tools/gcode-ast#add-prepare-scriptgcode-ast#14, needed because a github: dep resolved to a package with no dist/. A real dependency means publishing to npm.

Verdict

The swap works, the typed dispatch is a clear win on its own, and the geometry proves both. Still not mergeable at ~2.6× time and ~4× memory, and the remaining fixes are all upstream: the two tokenizing gaps, an opt-out-friendly toGCodeCommand signature, a way to skip node, and npm.

Assisted by Claude Code - Opus 5

parseCommand delegates to gcode-ast parseLine plus its GCodeCommand
compat view, and the local GCodeCommand class, GCodeParameters
interface and isAlphaCode scanner are re-exported or deleted. Nothing
else moves: thumbnails, slicer detection, layer metadata and the whole
interpreter keep consuming the same GCodeCommand shape.

Geometry is unchanged on every demo fixture. Two tokenizing cases
regress and the ingestion cost roughly doubles; both are covered in
the PR body.
@sophiedeziel sophiedeziel added poc proof-of-concept. not intended te be merged into develop marinating Left to simmer — needs more thought before acting labels Sep 8, 2026
The handler registry is keyed on the AST command type instead of a
lower-cased mnemonic, and each handler receives its node already
narrowed. Registering a handler under a type the parser cannot produce
is now a compile error rather than an entry that never fires.

What the types replace: probe re-derived the three G31 forms from a P
word and an axis-word count, and now reads the node form; selectTool
parsed the tool index out of the mnemonic, and now reads index, so T8
and above work instead of being dropped by a t0-t7 registry; setUnits
is one handler because the node carries which unit it selects.

G92 reads "bare" off words rather than the typed axis fields: the node
names only X/Y/Z/E, so G92 F3000 would otherwise look bare and reset a
workspace it should leave alone.

Handler tests build their commands by parsing a real line instead of
hand-pairing a mnemonic with a params bag, so they can no longer assert
on a shape the parser would never produce.

Geometry is unchanged on all seven demo fixtures. Modal continuation
lines stay unregistered, so nothing that was ignored starts rendering.
Nothing here had a caller.

- SceneManager._inches and _wireframe were assigned at their declaration
  and never read again. _wireframe was orphaned by the commented-out
  dev-gui control that used to drive it, removed here too.
- A commented-out camera.updateProjectionMatrix() that has been inert
  since the commit that introduced it; the live call site is elsewhere
  in the same file.
- A commented-out GridHelper alternative in BuildVolume.createGrid.
- getAvailableParsers, reachable from its own test and nowhere else: it
  is not re-exported from the package entry, so no consumer can call it.
  Its test goes with it.
- demo throttle, debounce and humanFileSize, none of them imported.

home takes _command rather than carrying an eslint-disable: the
registry fixes the argument position, so the parameter has to stay.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

marinating Left to simmer — needs more thought before acting poc proof-of-concept. not intended te be merged into develop

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant