From 4e09fe921a3e976ce8212c98c2436d9c8270820a Mon Sep 17 00:00:00 2001 From: Liam Kinne Date: Fri, 17 Jul 2026 10:37:56 +1000 Subject: [PATCH 1/4] add defmt feature and dependency --- Cargo.toml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index ca1378b8..fb6eb135 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,9 +21,11 @@ maintenance = { status = "actively-developed" } [features] default = ["alloc", "serde"] ## Enables the `alloc` crate, enabling [`parse`] and the heap-allocated AST ([`Program`], [`Diagnostics`], etc.). -alloc = [] +alloc = ["defmt?/alloc"] ## Enables serialisation and deserialisation of core and AST types via `serde`. serde = ["dep:serde"] +## Enabled defmt formatting +defmt = ["dep:defmt"] #! ### Internal Features #! The following features are not intended for public use. @@ -33,6 +35,7 @@ unstable-doc-cfg = [] [dependencies] cfg-if = "1.0.4" +defmt = { version = "1.1", optional = true } doc-cfg = "0.1.0" document-features = "0.2" libm = "0.2" From 77b151b1fe96c14c8e69d19cd11c87b06efa8916 Mon Sep 17 00:00:00 2001 From: Liam Kinne Date: Fri, 17 Jul 2026 10:49:12 +1000 Subject: [PATCH 2/4] add defmt traits --- src/core/types.rs | 31 ++++++++++++ src/diags.rs | 38 +++++++++++++++ src/types.rs | 118 ++++++++++++++++++++++++++++++++++++++++++++++ src/visitor.rs | 1 + 4 files changed, 188 insertions(+) diff --git a/src/core/types.rs b/src/core/types.rs index ccf68e1d..5e9311f6 100644 --- a/src/core/types.rs +++ b/src/core/types.rs @@ -63,6 +63,13 @@ impl Display for TokenType { } } +#[cfg(feature = "defmt")] +impl defmt::Format for TokenType { + fn format(&self, fmt: defmt::Formatter<'_>) { + defmt::write!(fmt, "{}", self.as_str()) + } +} + /// Return type for visitor methods that may either continue with a child visitor /// or pause parsing. See the [module-level docs](crate::core) for the control-flow model. pub type ControlFlow = core::ops::ControlFlow<(), T>; @@ -74,6 +81,7 @@ pub type ControlFlow = core::ops::ControlFlow<(), T>; /// refer into the same `&str` passed to [`parse`](crate::core::parse) or /// [`resume`](crate::core::resume). #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[repr(C)] pub struct Span { @@ -178,6 +186,18 @@ impl Debug for Number { } } +#[cfg(feature = "defmt")] +impl defmt::Format for Number { + fn format(&self, fmt: defmt::Formatter<'_>) { + let major = self.major(); + defmt::write!(fmt, "{}", major); + + if let Some(minor) = self.minor() { + defmt::write!(fmt, ".{}", minor); + } + } +} + #[derive(Debug, Clone, PartialEq, Eq)] pub enum ParseNumberError { ParseInt(core::num::ParseIntError), @@ -371,6 +391,16 @@ impl Display for Value<'_> { } } +#[cfg(feature = "defmt")] +impl defmt::Format for Value<'_> { + fn format(&self, fmt: defmt::Formatter<'_>) { + match self { + Value::Literal(n) => defmt::write!(fmt, "{}", n), + Value::Variable(s) => defmt::write!(fmt, "#{}", s), + } + } +} + /// A no-op visitor that ignores all callbacks. /// /// Use when you only need to drive the parser (e.g. to validate syntax or @@ -385,6 +415,7 @@ impl Display for Value<'_> { /// parse(src, &mut Noop); /// ``` #[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] pub struct Noop; impl ProgramVisitor for Noop { diff --git a/src/diags.rs b/src/diags.rs index 4562f80b..dc9418fe 100644 --- a/src/diags.rs +++ b/src/diags.rs @@ -29,6 +29,19 @@ impl Display for Diagnostic { } } +#[cfg(feature = "defmt")] +impl defmt::Format for Diagnostic { + fn format(&self, fmt: defmt::Formatter<'_>) { + let Diagnostic { + kind, + span: Span { line, .. }, + } = self; + let line = line + 1; + + defmt::write!(fmt, "{} on line {}", kind, line) + } +} + /// Category of parse diagnostic emitted during recovery. #[derive(Debug, Clone, PartialEq, Eq, Hash)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] @@ -61,10 +74,35 @@ impl Display for DiagnosticKind { } } +#[cfg(feature = "defmt")] +impl defmt::Format for DiagnosticKind { + fn format(&self, fmt: defmt::Formatter<'_>) { + match self { + DiagnosticKind::UnknownContent { text } => { + defmt::write!(fmt, "Unknown content: {}", text) + }, + DiagnosticKind::Unexpected { actual, expected } => { + let expected = expected + .iter() + .map(|s| s.as_str()) + .collect::>() + .join(", "); + defmt::write!( + fmt, + "Unexpected: {} (expected: {})", + actual, + expected + ) + }, + } + } +} + /// Collection of [`Diagnostic`]s produced by a parse. /// /// Returned by [`parse`](crate::parse) in `Err` when any diagnostic was emitted. #[derive(Debug, Clone, PartialEq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] pub struct Diagnostics(Vec); impl Diagnostics { diff --git a/src/types.rs b/src/types.rs index 3c010d94..c3a22a64 100644 --- a/src/types.rs +++ b/src/types.rs @@ -24,6 +24,15 @@ impl fmt::Display for Program { } } +#[cfg(feature = "defmt")] +impl defmt::Format for Program { + fn format(&self, fmt: defmt::Formatter<'_>) { + for block in &self.blocks { + defmt::write!(fmt, "{}", block); + } + } +} + impl core::str::FromStr for Program { type Err = crate::Diagnostics; @@ -78,6 +87,39 @@ impl fmt::Display for Block { } } +#[cfg(feature = "defmt")] +impl defmt::Format for Block { + fn format(&self, fmt: defmt::Formatter<'_>) { + let mut need_space = false; + if let Some(n) = self.line_number { + defmt::write!(fmt, "N{}", n); + need_space = true; + } + for c in &self.comments { + if need_space { + defmt::write!(fmt, " "); + } + defmt::write!(fmt, "{}", c); + need_space = true; + } + for code in &self.codes { + if need_space { + defmt::write!(fmt, " "); + } + defmt::write!(fmt, "{}", code); + need_space = true; + } + for w in &self.word_addresses { + if need_space { + defmt::write!(fmt, " "); + } + defmt::write!(fmt, "{}", w); + need_space = true; + } + defmt::write!(fmt, "\n"); + } +} + /// Modal bare address at block level (e.g. `X5.0`, `S12000`) without a G/M/T prefix. /// /// See [`Block::word_addresses`]. @@ -96,8 +138,16 @@ impl fmt::Display for WordAddress { } } +#[cfg(feature = "defmt")] +impl defmt::Format for WordAddress { + fn format(&self, fmt: defmt::Formatter<'_>) { + defmt::write!(fmt, "{}{}", self.letter, self.value); + } +} + /// How the comment appears in source: semicolon (`;...`) or parentheses (`(...)`). #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[non_exhaustive] pub enum CommentKind { @@ -124,6 +174,16 @@ impl fmt::Display for Comment { } } +#[cfg(feature = "defmt")] +impl defmt::Format for Comment { + fn format(&self, fmt: defmt::Formatter<'_>) { + match self.kind { + CommentKind::Semicolon => defmt::write!(fmt, ";{}", self.value), + CommentKind::Parentheses => defmt::write!(fmt, "({}", self.value), + } + } +} + /// One G, M, or T command (variant plus optional arguments). /// /// Appears in [`Block::codes`]. @@ -146,6 +206,17 @@ impl fmt::Display for Code { } } +#[cfg(feature = "defmt")] +impl defmt::Format for Code { + fn format(&self, fmt: defmt::Formatter<'_>) { + match self { + Code::General(g) => defmt::write!(fmt, "{}", g), + Code::Miscellaneous(m) => defmt::write!(fmt, "{}", m), + Code::ToolChange(t) => defmt::write!(fmt, "{}", t), + } + } +} + /// G-code: motion, coordinate system, plane selection, etc. #[derive(Debug, Clone, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] @@ -166,6 +237,16 @@ impl fmt::Display for GeneralCode { } } +#[cfg(feature = "defmt")] +impl defmt::Format for GeneralCode { + fn format(&self, fmt: defmt::Formatter<'_>) { + defmt::write!(fmt, "G{}", self.number); + for arg in &self.args { + defmt::write!(fmt, "{}", arg); + } + } +} + /// M-code: spindle, coolant, program control, etc. #[derive(Debug, Clone, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] @@ -186,6 +267,16 @@ impl fmt::Display for MiscellaneousCode { } } +#[cfg(feature = "defmt")] +impl defmt::Format for MiscellaneousCode { + fn format(&self, fmt: defmt::Formatter<'_>) { + defmt::write!(fmt, "M{}", self.number); + for arg in &self.args { + defmt::write!(fmt, "{}", arg); + } + } +} + /// T-code: tool selection. #[derive(Debug, Clone, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] @@ -206,6 +297,16 @@ impl fmt::Display for ToolChangeCode { } } +#[cfg(feature = "defmt")] +impl defmt::Format for ToolChangeCode { + fn format(&self, fmt: defmt::Formatter<'_>) { + defmt::write!(fmt, "T{}", self.number); + for arg in &self.args { + defmt::write!(fmt, "{}", arg); + } + } +} + /// One address letter and its value (e.g. X, Y, Z, F, S). /// /// On a G/M/T code; see e.g. [`GeneralCode::args`]. @@ -224,6 +325,13 @@ impl fmt::Display for Argument { } } +#[cfg(feature = "defmt")] +impl defmt::Format for Argument { + fn format(&self, fmt: defmt::Formatter<'_>) { + defmt::write!(fmt, " {}{}", self.letter, self.value) + } +} + /// Argument value: a literal number or a variable reference (e.g. `#1`). #[derive(Debug, Clone, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] @@ -251,6 +359,16 @@ impl fmt::Display for Value { } } +#[cfg(feature = "defmt")] +impl defmt::Format for Value { + fn format(&self, fmt: defmt::Formatter<'_>) { + match self { + Value::Literal(n) => defmt::write!(fmt, "{}", n), + Value::Variable(s) => defmt::write!(fmt, "#{}", s), + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/visitor.rs b/src/visitor.rs index 19cf4ad5..0e601faf 100644 --- a/src/visitor.rs +++ b/src/visitor.rs @@ -16,6 +16,7 @@ use crate::{ /// /// Used by [`parse`](crate::parse); typically not constructed by users. #[derive(Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] pub struct AstBuilder { blocks: Vec, diagnostics: Diagnostics, From 2eb97e60f0083264bfe2ff7253af146d480e42ec Mon Sep 17 00:00:00 2001 From: Liam Kinne Date: Fri, 17 Jul 2026 10:49:39 +1000 Subject: [PATCH 3/4] fix feature description --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index fb6eb135..04e109bd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,7 +24,7 @@ default = ["alloc", "serde"] alloc = ["defmt?/alloc"] ## Enables serialisation and deserialisation of core and AST types via `serde`. serde = ["dep:serde"] -## Enabled defmt formatting +## Enables defmt format implementations. defmt = ["dep:defmt"] #! ### Internal Features From d2c414523726586b3b0473250886100f47e99c73 Mon Sep 17 00:00:00 2001 From: Liam Kinne Date: Fri, 17 Jul 2026 10:49:43 +1000 Subject: [PATCH 4/4] update lockfile --- Cargo.lock | 64 +++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 61 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d8ed7eba..1ce9b9a2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -58,6 +58,12 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.11.0" @@ -128,6 +134,37 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "defmt" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1" +dependencies = [ + "bitflags 1.3.2", + "defmt-macros", +] + +[[package]] +name = "defmt-macros" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8" +dependencies = [ + "defmt-parser", + "proc-macro2 1.0.106", + "quote 1.0.45", + "syn", +] + +[[package]] +name = "defmt-parser" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" +dependencies = [ + "thiserror", +] + [[package]] name = "diff" version = "0.1.13" @@ -198,6 +235,7 @@ name = "gcode" version = "0.7.0" dependencies = [ "cfg-if", + "defmt", "doc-cfg", "document-features", "insta", @@ -433,7 +471,7 @@ version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags", + "bitflags 2.11.0", "errno", "libc", "linux-raw-sys", @@ -534,6 +572,26 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2 1.0.106", + "quote 1.0.45", + "syn", +] + [[package]] name = "threadpool" version = "1.8.1" @@ -613,7 +671,7 @@ version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ - "bitflags", + "bitflags 2.11.0", "hashbrown 0.15.5", "indexmap", "semver", @@ -780,7 +838,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", - "bitflags", + "bitflags 2.11.0", "indexmap", "log", "serde",