├── .appveyor.yml
├── .gitignore
├── .travis.yml
├── LICENSE_APACHE.md
├── LICENSE_MIT.md
├── README.md
├── gcode
├── Cargo.toml
├── LICENSE_APACHE.md
├── LICENSE_MIT.md
├── README.md
├── benches
│ └── example_files.rs
├── src
│ ├── buffers.rs
│ ├── callbacks.rs
│ ├── comment.rs
│ ├── gcode.rs
│ ├── lexer.rs
│ ├── lib.rs
│ ├── line.rs
│ ├── macros.rs
│ ├── parser.rs
│ ├── span.rs
│ └── words.rs
└── tests
│ ├── data
│ ├── Insulpro.Piping.-.115mm.OD.-.40mm.WT.txt
│ ├── PI_octcat.gcode
│ ├── PI_rustlogo.gcode
│ ├── program_1.gcode
│ ├── program_2.gcode
│ └── program_3.gcode
│ └── smoke_test.rs
├── rustfmt.toml
└── wasm
├── .gitignore
├── Cargo.toml
├── LICENSE_APACHE.md
├── LICENSE_MIT.md
├── README.md
├── jest.config.js
├── package.json
├── rust
├── callbacks.rs
├── lib.rs
├── parser.rs
└── simple_wrappers.rs
├── ts
├── index.test.ts
└── index.ts
├── tsconfig.json
└── yarn.lock
/.appveyor.yml:
--------------------------------------------------------------------------------
1 | os: Visual Studio 2015
2 |
3 | environment:
4 | matrix:
5 | - channel: stable
6 | target: x86_64-pc-windows-msvc
7 | cargoflags: --all-features
8 |
9 | install:
10 | - appveyor DownloadFile https://win.rustup.rs/ -FileName rustup-init.exe
11 | - rustup-init -yv --default-toolchain %channel% --default-host %target%
12 | - set PATH=%PATH%;%USERPROFILE%\.cargo\bin
13 | - rustc -vV
14 | - cargo -vV
15 |
16 | build: false
17 |
18 | test_script:
19 | - dir
20 | - pwd
21 | - cargo test --verbose %cargoflags% --manifest-path %APPVEYOR_BUILD_FOLDER%\gcode\Cargo.toml
22 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | target/
2 | **/*.rs.bk
3 | Cargo.lock
4 | .vscode
5 | gcode.h
6 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: rust
2 |
3 | env:
4 | global:
5 | - secure: KYlhj8S0cH4AT1fkrecOIMmJrvnC3nUDQZ+fTtstVvm+rrV6KqwRqy6WfekRNq3Le6S7SsH5SyyJFfdFFkHVtpUSnYvd2pxy7mrYpgFba/9vJxndJYSqI6TuxJ5tx+HxouCEFVNZg8f1De+RtGayD2f4xOixRbSHQO3kzH2Cz0pzTBMas5cfveImjHwc+abwp/tp0VKgCz/91V698/cDC0RWVX6aUSe3z1lBN/mmnqXLiDLqcT1xlQqq5JJZ2SjCnLoSnwqMPZzWGBIxz9mvqsE9531pgj1YWYUkQGGNLotwdrpkRhTALVTZJBoKcDqN9rAvdIrw32ufJ2RwO6AjBonurCcZwCYzGDxSulyjrXchKN+hPQ/qpz5kwAh8o9vLfExVYP/gKaDTKVAiva8Zdi3tT/8WYhXm2P+UH2U4qFuhgYxr/bi/HIsVqPRgurKtgK0keCHh+h6VqGOpy49Td9r3uCzWdCiC2vawThQEPNl3NF0BhA5mpQkEEtAcO7tKagvptBaZTLP1gxtT1eTcYvWypff4Eo3m1TXFf4dkKk23gblK9UAzdbqrH0WqFWLLyHv+vwCNA6hdV4BfiiS/dseuB/66tQ6so3vwoVcTyPEy9U1cFzCxu64J1OcmTY1h72fToAiBNjoDevzofbZPduQruVYLqZj74e1tvl4xPOM=
6 |
7 | matrix:
8 | include:
9 | # MSRV from arrayvec
10 | - rust: 1.36.0
11 | # and check each feature individually
12 | - env: FEATURES="--no-default-features --features serde-1"
13 | - env: FEATURES="--no-default-features --features std"
14 | # Make sure it compiles without std by targeting an embedded platform
15 | - env:
16 | - TARGET=thumbv7em-none-eabihf
17 | - FEATURES=--no-default-features
18 | script:
19 | - cd gcode
20 | - cargo build --verbose $FEATURES $TARGET
21 |
22 | # Use nightly for better docs
23 | - env:
24 | - FEATURES=--all-features
25 | - RUSTDOCFLAGS="--cfg docsrs"
26 | rust: nightly
27 |
28 | # the webassembly bindings
29 | - script:
30 | - cd wasm && yarn install
31 | - yarn test
32 |
33 | before_script:
34 | - |
35 | if [ ! -z "$TARGET" ]; then
36 | rustup target add $TARGET
37 | export TARGET="--target $TARGET"
38 | fi
39 | - set -e
40 |
41 | script:
42 | - cd gcode
43 | - cargo build --no-default-features
44 | - cargo build --verbose $FEATURES $TARGET
45 | - cargo test --verbose $FEATURES $TARGET
46 | - cargo doc --verbose $FEATURES $TARGET
47 |
48 | after_script: set +e
49 |
50 | before_deploy:
51 | - echo ' ' > $TRAVIS_BUILD_DIR/gcode/target/doc/index.html
52 |
53 | deploy:
54 | - provider: pages
55 | skip-cleanup: true
56 | github-token: $GH_TOKEN
57 | keep-history: true
58 | local-dir: $TRAVIS_BUILD_DIR/gcode/target/doc
59 | on:
60 | branch: master
61 | rust: nightly
62 |
63 | before_cache:
64 | - chmod -R a+r $HOME/.cargo
65 |
66 | branches:
67 | only:
68 | # release tags
69 | - /^v\d+\.\d+\.\d+.*$/
70 | - master
71 |
72 | notifications:
73 | email:
74 | on_success: never
75 |
--------------------------------------------------------------------------------
/LICENSE_APACHE.md:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
--------------------------------------------------------------------------------
/LICENSE_MIT.md:
--------------------------------------------------------------------------------
1 | Copyright (c) 2020 Michael Bryan
2 |
3 | Permission is hereby granted, free of charge, to any
4 | person obtaining a copy of this software and associated
5 | documentation files (the "Software"), to deal in the
6 | Software without restriction, including without
7 | limitation the rights to use, copy, modify, merge,
8 | publish, distribute, sublicense, and/or sell copies of
9 | the Software, and to permit persons to whom the Software
10 | is furnished to do so, subject to the following
11 | conditions:
12 |
13 | The above copyright notice and this permission notice
14 | shall be included in all copies or substantial portions
15 | of the Software.
16 |
17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
25 | DEALINGS IN THE SOFTWARE.
26 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # gcode-rs
2 |
3 | [](https://crates.io/crates/gcode)
4 | [](https://docs.rs/gcode/)
5 | [](https://travis-ci.org/Michael-F-Bryan/gcode-rs)
6 |
7 | A gcode parser designed for use in `#[no_std]` environments.
8 |
9 | For an example of the `gcode` crate in use, see
10 | [@etrombly][etrombly]'s [`gcode-yew`][gc-y].
11 |
12 | ## Useful Links
13 |
14 | - [The thread that kicked this idea off][thread]
15 | - [Rendered Documentation][docs]
16 | - [NIST GCode Interpreter Spec][nist]
17 |
18 | ## License
19 |
20 | This project is licensed under either of
21 |
22 | * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE_APACHE.md) or
23 | http://www.apache.org/licenses/LICENSE-2.0)
24 | * MIT license ([LICENSE-MIT](LICENSE_MIT.md) or
25 | http://opensource.org/licenses/MIT)
26 |
27 | at your option.
28 |
29 | It is recommended to always use [cargo-crev][crev] to verify the
30 | trustworthiness of each of your dependencies, including this one.
31 |
32 | ### Contribution
33 |
34 | The intent of this crate is to be free of soundness bugs. The developers will
35 | do their best to avoid them, and welcome help in analyzing and fixing them.
36 |
37 | Unless you explicitly state otherwise, any contribution intentionally
38 | submitted for inclusion in the work by you, as defined in the Apache-2.0
39 | license, shall be dual licensed as above, without any additional terms or
40 | conditions.
41 |
42 | [thread]:https://users.rust-lang.org/t/g-code-interpreter/10930
43 | [docs]: https://michael-f-bryan.github.io/gcode-rs/
44 | [p3]: https://github.com/Michael-F-Bryan/gcode-rs/blob/master/tests/data/program_3.gcode
45 | [nist]: http://ws680.nist.gov/publication/get_pdf.cfm?pub_id=823374
46 | [cargo-c]: https://github.com/lu-zero/cargo-c
47 | [etrombly]: https://github.com/etrombly
48 | [gc-y]: https://github.com/etrombly/gcode-yew
49 | [crev]: https://github.com/crev-dev/cargo-crev
50 |
--------------------------------------------------------------------------------
/gcode/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "gcode"
3 | version = "0.6.2-alpha.0"
4 | authors = ["Michael Bryan "]
5 | description = "A gcode parser for no-std applications."
6 | repository = "https://github.com/Michael-F-Bryan/gcode-rs"
7 | readme = "../README.md"
8 | license = "MIT OR Apache-2.0"
9 | keywords = ["gcode", "parser"]
10 | categories = ["no-std", "parser-implementations", "embedded"]
11 | edition = "2018"
12 |
13 | [package.metadata.docs.rs]
14 | all-features = true
15 |
16 | [badges]
17 | appveyor = { repository = "Michael-F-Bryan/gcode-rs" }
18 | travis-ci = { repository = "Michael-F-Bryan/gcode-rs" }
19 | maintenance = { status = "actively-developed" }
20 |
21 | [features]
22 | default = ["std"]
23 | std = ["arrayvec/std"]
24 | serde-1 = ["serde", "serde_derive", "arrayvec/serde"]
25 |
26 | [dependencies]
27 | cfg-if = "0.1.9"
28 | arrayvec = { version ="0.5", default-features = false }
29 | serde = { version = "1.0", optional = true }
30 | serde_derive = { version = "1.0", optional = true }
31 | libm = "0.2"
32 |
33 | [dev-dependencies]
34 | pretty_assertions = "0.6.1"
35 |
--------------------------------------------------------------------------------
/gcode/LICENSE_APACHE.md:
--------------------------------------------------------------------------------
1 | ../LICENSE_APACHE.md
--------------------------------------------------------------------------------
/gcode/LICENSE_MIT.md:
--------------------------------------------------------------------------------
1 | ../LICENSE_MIT.md
--------------------------------------------------------------------------------
/gcode/README.md:
--------------------------------------------------------------------------------
1 | ../README.md
--------------------------------------------------------------------------------
/gcode/benches/example_files.rs:
--------------------------------------------------------------------------------
1 | #![feature(test)]
2 |
3 | extern crate test;
4 |
5 | use test::Bencher;
6 |
7 | macro_rules! bench {
8 | ($name:ident) => {
9 | #[bench]
10 | #[allow(non_snake_case)]
11 | fn $name(b: &mut Bencher) {
12 | let src = include_str!(concat!(
13 | "../tests/data/",
14 | stringify!($name),
15 | ".gcode"
16 | ));
17 | b.bytes = src.len() as u64;
18 |
19 | b.iter(|| gcode::parse(src).count());
20 | }
21 | };
22 | }
23 |
24 | bench!(program_1);
25 | bench!(program_2);
26 | bench!(program_3);
27 | bench!(PI_octcat);
28 |
--------------------------------------------------------------------------------
/gcode/src/buffers.rs:
--------------------------------------------------------------------------------
1 | //! Buffer Management.
2 | //!
3 | //! This module is mainly intended for use cases when the amount of space that
4 | //! can be consumed by buffers needs to be defined at compile time. For most
5 | //! users, the [`DefaultBuffers`] alias should be suitable.
6 | //!
7 | //! For most end users it is probably simpler to determine a "good enough"
8 | //! buffer size and create type aliases of [`GCode`] and [`Line`] for that size.
9 |
10 | use crate::{Comment, GCode, Word};
11 | use arrayvec::{Array, ArrayVec};
12 | use core::{
13 | fmt::{self, Debug, Display, Formatter},
14 | marker::PhantomData,
15 | };
16 |
17 | #[allow(unused_imports)] // for rustdoc links
18 | use crate::Line;
19 |
20 | cfg_if::cfg_if! {
21 | if #[cfg(feature = "std")] {
22 | /// The default buffer type for this platform.
23 | ///
24 | /// This is a type alias for [`VecBuffers`] because the crate is compiled
25 | /// with the *"std"* feature.
26 | pub type DefaultBuffers = VecBuffers;
27 |
28 | /// The default [`Buffer`] to use for a [`GCode`]'s arguments.
29 | ///
30 | /// This is a type alias for [`Vec`] because the crate is compiled
31 | /// with the *"std"* feature.
32 | pub type DefaultArguments = Vec;
33 | } else {
34 | /// The default buffer type for this platform.
35 | ///
36 | /// This is a type alias for [`SmallFixedBuffers`] because the crate is compiled
37 | /// without the *"std"* feature.
38 | pub type DefaultBuffers = SmallFixedBuffers;
39 |
40 | /// The default [`Buffer`] to use for a [`GCode`]'s arguments.
41 | ///
42 | /// This is a type alias for [`ArrayVec`] because the crate is compiled
43 | /// without the *"std"* feature.
44 | pub type DefaultArguments = ArrayVec<[Word; 5]>;
45 | }
46 | }
47 |
48 | /// A set of type aliases defining the types to use when storing data.
49 | pub trait Buffers<'input> {
50 | /// The [`Buffer`] used to store [`GCode`] arguments.
51 | type Arguments: Buffer + Default;
52 | /// The [`Buffer`] used to store [`GCode`]s.
53 | type Commands: Buffer> + Default;
54 | /// The [`Buffer`] used to store [`Comment`]s.
55 | type Comments: Buffer> + Default;
56 | }
57 |
58 | /// Something which can store items sequentially in memory. This doesn't
59 | /// necessarily require dynamic memory allocation.
60 | pub trait Buffer {
61 | /// Try to add another item to this [`Buffer`], returning the item if there
62 | /// is no more room.
63 | fn try_push(&mut self, item: T) -> Result<(), CapacityError>;
64 |
65 | /// The items currently stored in the [`Buffer`].
66 | fn as_slice(&self) -> &[T];
67 | }
68 |
69 | impl> Buffer for ArrayVec {
70 | fn try_push(&mut self, item: T) -> Result<(), CapacityError> {
71 | ArrayVec::try_push(self, item).map_err(|e| CapacityError(e.element()))
72 | }
73 |
74 | fn as_slice(&self) -> &[T] { &self }
75 | }
76 |
77 | /// The smallest usable set of [`Buffers`].
78 | ///
79 | /// ```rust
80 | /// # use gcode::{Line, GCode, buffers::{Buffers, SmallFixedBuffers}};
81 | /// let line_size = std::mem::size_of::>();
82 | /// assert!(line_size <= 350, "Got {}", line_size);
83 | ///
84 | /// // the explicit type for a `GCode` backed by `SmallFixedBuffers`
85 | /// type SmallBufferGCode<'a> = GCode<>::Arguments>;
86 | ///
87 | /// let gcode_size = std::mem::size_of::>();
88 | /// assert!(gcode_size <= 250, "Got {}", gcode_size);
89 | /// ```
90 | #[derive(Debug, Copy, Clone, PartialEq)]
91 | pub enum SmallFixedBuffers {}
92 |
93 | impl<'input> Buffers<'input> for SmallFixedBuffers {
94 | type Arguments = DefaultArguments;
95 | type Commands = ArrayVec<[GCode; 1]>;
96 | type Comments = ArrayVec<[Comment<'input>; 1]>;
97 | }
98 |
99 | with_std! {
100 | /// A [`Buffers`] implementation which uses [`std::vec::Vec`] for storing items.
101 | ///
102 | /// In terms of memory usage, this has the potential to use a lot less overall
103 | /// than something like [`SmallFixedBuffers`] because we've traded deterministic
104 | /// memory usage for only allocating memory when it is required.
105 | #[derive(Debug, Copy, Clone, PartialEq)]
106 | pub enum VecBuffers {}
107 |
108 | impl<'input> Buffers<'input> for VecBuffers {
109 | type Arguments = DefaultArguments;
110 | type Commands = Vec>;
111 | type Comments = Vec>;
112 | }
113 |
114 | impl Buffer for Vec {
115 | fn try_push(&mut self, item: T) -> Result<(), CapacityError> {
116 | self.push(item);
117 | Ok(())
118 | }
119 |
120 | fn as_slice(&self) -> &[T] { &self }
121 | }
122 | }
123 |
124 | /// An error returned when [`Buffer::try_push()`] fails.
125 | ///
126 | /// When a [`Buffer`] can't add an item, it will use [`CapacityError`] to pass
127 | /// the original item back to the caller.
128 | #[derive(Debug, Copy, Clone, PartialEq)]
129 | pub struct CapacityError(pub T);
130 |
131 | impl Display for CapacityError {
132 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
133 | write!(f, "insufficient capacity")
134 | }
135 | }
136 |
137 | with_std! {
138 | impl std::error::Error for CapacityError {}
139 | }
140 |
141 | /// Debug *any* [`Buffer`] when the item is [`Debug`].
142 | pub(crate) fn debug<'a, T, B>(buffer: &'a B) -> impl Debug + 'a
143 | where
144 | B: Buffer + 'a,
145 | T: Debug + 'a,
146 | {
147 | DebugBuffer::new(buffer)
148 | }
149 |
150 | struct DebugBuffer<'a, B, T> {
151 | buffer: &'a B,
152 | _item: PhantomData<&'a T>,
153 | }
154 |
155 | impl<'a, T, B: Buffer> DebugBuffer<'a, B, T> {
156 | fn new(buffer: &'a B) -> Self {
157 | DebugBuffer {
158 | buffer,
159 | _item: PhantomData,
160 | }
161 | }
162 | }
163 |
164 | impl<'a, B, T> Debug for DebugBuffer<'a, B, T>
165 | where
166 | B: Buffer,
167 | T: Debug,
168 | {
169 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
170 | let entries =
171 | self.buffer.as_slice().iter().map(|item| item as &dyn Debug);
172 |
173 | f.debug_list().entries(entries).finish()
174 | }
175 | }
176 |
--------------------------------------------------------------------------------
/gcode/src/callbacks.rs:
--------------------------------------------------------------------------------
1 | use crate::{Comment, Mnemonic, Span, Word};
2 |
3 | #[allow(unused_imports)] // rustdoc links
4 | use crate::{buffers::Buffers, GCode};
5 |
6 | /// Callbacks used during the parsing process to indicate possible errors.
7 | pub trait Callbacks {
8 | /// The parser encountered some text it wasn't able to make sense of.
9 | fn unknown_content(&mut self, _text: &str, _span: Span) {}
10 |
11 | /// The [`Buffers::Commands`] buffer had insufficient capacity when trying
12 | /// to add a [`GCode`].
13 | fn gcode_buffer_overflowed(
14 | &mut self,
15 | _mnemonic: Mnemonic,
16 | _major_number: u32,
17 | _minor_number: u32,
18 | _arguments: &[Word],
19 | _span: Span,
20 | ) {
21 | }
22 |
23 | /// The [`Buffers::Arguments`] buffer had insufficient capacity when trying
24 | /// to add a [`Word`].
25 | ///
26 | /// To aid in diagnostics, the caller is also given the [`GCode`]'s
27 | /// mnemonic and major/minor numbers.
28 | fn gcode_argument_buffer_overflowed(
29 | &mut self,
30 | _mnemonic: Mnemonic,
31 | _major_number: u32,
32 | _minor_number: u32,
33 | _argument: Word,
34 | ) {
35 | }
36 |
37 | /// A [`Comment`] was encountered, but there wasn't enough room in
38 | /// [`Buffers::Comments`].
39 | fn comment_buffer_overflow(&mut self, _comment: Comment<'_>) {}
40 |
41 | /// A line number was encountered when it wasn't expected.
42 | fn unexpected_line_number(&mut self, _line_number: f32, _span: Span) {}
43 |
44 | /// An argument was found, but the parser couldn't figure out which
45 | /// [`GCode`] it corresponds to.
46 | fn argument_without_a_command(
47 | &mut self,
48 | _letter: char,
49 | _value: f32,
50 | _span: Span,
51 | ) {
52 | }
53 |
54 | /// A [`Word`]'s number was encountered without an accompanying letter.
55 | fn number_without_a_letter(&mut self, _value: &str, _span: Span) {}
56 |
57 | /// A [`Word`]'s letter was encountered without an accompanying number.
58 | fn letter_without_a_number(&mut self, _value: &str, _span: Span) {}
59 | }
60 |
61 | impl<'a, C: Callbacks + ?Sized> Callbacks for &'a mut C {
62 | fn unknown_content(&mut self, text: &str, span: Span) {
63 | (*self).unknown_content(text, span);
64 | }
65 |
66 | fn gcode_buffer_overflowed(
67 | &mut self,
68 | mnemonic: Mnemonic,
69 | major_number: u32,
70 | minor_number: u32,
71 | arguments: &[Word],
72 | span: Span,
73 | ) {
74 | (*self).gcode_buffer_overflowed(
75 | mnemonic,
76 | major_number,
77 | minor_number,
78 | arguments,
79 | span,
80 | );
81 | }
82 |
83 | fn gcode_argument_buffer_overflowed(
84 | &mut self,
85 | mnemonic: Mnemonic,
86 | major_number: u32,
87 | minor_number: u32,
88 | argument: Word,
89 | ) {
90 | (*self).gcode_argument_buffer_overflowed(
91 | mnemonic,
92 | major_number,
93 | minor_number,
94 | argument,
95 | );
96 | }
97 |
98 | fn comment_buffer_overflow(&mut self, comment: Comment<'_>) {
99 | (*self).comment_buffer_overflow(comment);
100 | }
101 |
102 | fn unexpected_line_number(&mut self, line_number: f32, span: Span) {
103 | (*self).unexpected_line_number(line_number, span);
104 | }
105 |
106 | fn argument_without_a_command(
107 | &mut self,
108 | letter: char,
109 | value: f32,
110 | span: Span,
111 | ) {
112 | (*self).argument_without_a_command(letter, value, span);
113 | }
114 |
115 | fn number_without_a_letter(&mut self, value: &str, span: Span) {
116 | (*self).number_without_a_letter(value, span);
117 | }
118 |
119 | fn letter_without_a_number(&mut self, value: &str, span: Span) {
120 | (*self).letter_without_a_number(value, span);
121 | }
122 | }
123 |
124 | /// A set of callbacks that ignore any errors that occur.
125 | #[derive(Debug, Copy, Clone, PartialEq, Default)]
126 | pub struct Nop;
127 |
128 | impl Callbacks for Nop {}
129 |
--------------------------------------------------------------------------------
/gcode/src/comment.rs:
--------------------------------------------------------------------------------
1 | use crate::Span;
2 |
3 | /// A comment.
4 | #[derive(Debug, Copy, Clone, PartialEq, Eq)]
5 | #[cfg_attr(
6 | feature = "serde-1",
7 | derive(serde_derive::Serialize, serde_derive::Deserialize)
8 | )]
9 | pub struct Comment<'input> {
10 | /// The comment itself.
11 | pub value: &'input str,
12 | /// Where the comment is located in the original string.
13 | pub span: Span,
14 | }
15 |
--------------------------------------------------------------------------------
/gcode/src/gcode.rs:
--------------------------------------------------------------------------------
1 | use crate::{
2 | buffers::{Buffer, CapacityError, DefaultArguments},
3 | Span, Word,
4 | };
5 | use core::fmt::{self, Debug, Display, Formatter};
6 |
7 | /// The general category for a [`GCode`].
8 | #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
9 | #[cfg_attr(
10 | feature = "serde-1",
11 | derive(serde_derive::Serialize, serde_derive::Deserialize)
12 | )]
13 | #[repr(C)]
14 | pub enum Mnemonic {
15 | /// Preparatory commands, often telling the controller what kind of motion
16 | /// or offset is desired.
17 | General,
18 | /// Auxilliary commands.
19 | Miscellaneous,
20 | /// Used to give the current program a unique "name".
21 | ProgramNumber,
22 | /// Tool selection.
23 | ToolChange,
24 | }
25 |
26 | impl Mnemonic {
27 | /// Try to convert a letter to its [`Mnemonic`] equivalent.
28 | ///
29 | /// # Examples
30 | ///
31 | /// ```rust
32 | /// # use gcode::Mnemonic;
33 | /// assert_eq!(Mnemonic::for_letter('M'), Some(Mnemonic::Miscellaneous));
34 | /// assert_eq!(Mnemonic::for_letter('g'), Some(Mnemonic::General));
35 | /// ```
36 | pub fn for_letter(letter: char) -> Option {
37 | match letter.to_ascii_lowercase() {
38 | 'g' => Some(Mnemonic::General),
39 | 'm' => Some(Mnemonic::Miscellaneous),
40 | 'o' => Some(Mnemonic::ProgramNumber),
41 | 't' => Some(Mnemonic::ToolChange),
42 | _ => None,
43 | }
44 | }
45 | }
46 |
47 | impl Display for Mnemonic {
48 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
49 | match self {
50 | Mnemonic::General => write!(f, "G"),
51 | Mnemonic::Miscellaneous => write!(f, "M"),
52 | Mnemonic::ProgramNumber => write!(f, "O"),
53 | Mnemonic::ToolChange => write!(f, "T"),
54 | }
55 | }
56 | }
57 |
58 | /// The in-memory representation of a single command in the G-code language
59 | /// (e.g. `"G01 X50.0 Y-20.0"`).
60 | #[derive(Clone)]
61 | #[cfg_attr(
62 | feature = "serde-1",
63 | derive(serde_derive::Serialize, serde_derive::Deserialize)
64 | )]
65 | pub struct GCode {
66 | mnemonic: Mnemonic,
67 | number: f32,
68 | arguments: A,
69 | span: Span,
70 | }
71 |
72 | impl GCode {
73 | /// Create a new [`GCode`] which uses the [`DefaultArguments`] buffer.
74 | pub fn new(mnemonic: Mnemonic, number: f32, span: Span) -> Self {
75 | GCode {
76 | mnemonic,
77 | number,
78 | span,
79 | arguments: DefaultArguments::default(),
80 | }
81 | }
82 | }
83 |
84 | impl> GCode {
85 | /// Create a new [`GCode`] which uses a custom [`Buffer`].
86 | pub fn new_with_argument_buffer(
87 | mnemonic: Mnemonic,
88 | number: f32,
89 | span: Span,
90 | arguments: A,
91 | ) -> Self {
92 | GCode {
93 | mnemonic,
94 | number,
95 | span,
96 | arguments,
97 | }
98 | }
99 |
100 | /// The overall category this [`GCode`] belongs to.
101 | pub fn mnemonic(&self) -> Mnemonic { self.mnemonic }
102 |
103 | /// The integral part of a command number (i.e. the `12` in `G12.3`).
104 | pub fn major_number(&self) -> u32 {
105 | debug_assert!(self.number >= 0.0);
106 |
107 | libm::floorf(self.number) as u32
108 | }
109 |
110 | /// The fractional part of a command number (i.e. the `3` in `G12.3`).
111 | pub fn minor_number(&self) -> u32 {
112 | let fract = self.number - libm::floorf(self.number);
113 | let digit = libm::roundf(fract * 10.0);
114 | digit as u32
115 | }
116 |
117 | /// The arguments attached to this [`GCode`].
118 | pub fn arguments(&self) -> &[Word] { self.arguments.as_slice() }
119 |
120 | /// Where the [`GCode`] was found in its source text.
121 | pub fn span(&self) -> Span { self.span }
122 |
123 | /// Add an argument to the list of arguments attached to this [`GCode`].
124 | pub fn push_argument(
125 | &mut self,
126 | arg: Word,
127 | ) -> Result<(), CapacityError> {
128 | self.span = self.span.merge(arg.span);
129 | self.arguments.try_push(arg)
130 | }
131 |
132 | /// The builder equivalent of [`GCode::push_argument()`].
133 | ///
134 | /// # Panics
135 | ///
136 | /// This will panic if the underlying [`Buffer`] returns a
137 | /// [`CapacityError`].
138 | pub fn with_argument(mut self, arg: Word) -> Self {
139 | if let Err(e) = self.push_argument(arg) {
140 | panic!("Unable to add the argument {:?}: {}", arg, e);
141 | }
142 | self
143 | }
144 |
145 | /// Get the value for a particular argument.
146 | ///
147 | /// # Examples
148 | ///
149 | /// ```rust
150 | /// # use gcode::{GCode, Mnemonic, Span, Word};
151 | /// let gcode = GCode::new(Mnemonic::General, 1.0, Span::PLACEHOLDER)
152 | /// .with_argument(Word::new('X', 30.0, Span::PLACEHOLDER))
153 | /// .with_argument(Word::new('Y', -3.14, Span::PLACEHOLDER));
154 | ///
155 | /// assert_eq!(gcode.value_for('Y'), Some(-3.14));
156 | /// ```
157 | pub fn value_for(&self, letter: char) -> Option {
158 | let letter = letter.to_ascii_lowercase();
159 |
160 | for arg in self.arguments() {
161 | if arg.letter.to_ascii_lowercase() == letter {
162 | return Some(arg.value);
163 | }
164 | }
165 |
166 | None
167 | }
168 | }
169 |
170 | impl> Extend for GCode {
171 | fn extend>(&mut self, words: I) {
172 | for word in words {
173 | if self.push_argument(word).is_err() {
174 | // we can't add any more arguments
175 | return;
176 | }
177 | }
178 | }
179 | }
180 |
181 | impl> Debug for GCode {
182 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
183 | // we manually implement Debug because the the derive will constrain
184 | // the buffer type to be Debug, which isn't necessary and actually makes
185 | // it impossible to print something like ArrayVec<[T; 128]>
186 | let GCode {
187 | mnemonic,
188 | number,
189 | arguments,
190 | span,
191 | } = self;
192 |
193 | f.debug_struct("GCode")
194 | .field("mnemonic", mnemonic)
195 | .field("number", number)
196 | .field("arguments", &crate::buffers::debug(arguments))
197 | .field("span", span)
198 | .finish()
199 | }
200 | }
201 |
202 | impl> Display for GCode {
203 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
204 | write!(f, "{}{}", self.mnemonic(), self.major_number())?;
205 |
206 | if self.minor_number() != 0 {
207 | write!(f, ".{}", self.minor_number())?;
208 | }
209 |
210 | for arg in self.arguments() {
211 | write!(f, " {}", arg)?;
212 | }
213 |
214 | Ok(())
215 | }
216 | }
217 |
218 | impl PartialEq> for GCode
219 | where
220 | A: Buffer,
221 | B: Buffer,
222 | {
223 | fn eq(&self, other: &GCode) -> bool {
224 | let GCode {
225 | mnemonic,
226 | number,
227 | arguments,
228 | span,
229 | } = self;
230 |
231 | *span == other.span()
232 | && *mnemonic == other.mnemonic
233 | && *number == other.number
234 | && arguments.as_slice() == other.arguments.as_slice()
235 | }
236 | }
237 |
238 | #[cfg(test)]
239 | mod tests {
240 | use super::*;
241 | use arrayvec::ArrayVec;
242 | use std::prelude::v1::*;
243 |
244 | type BigBuffer = ArrayVec<[Word; 32]>;
245 |
246 | #[test]
247 | fn correct_major_number() {
248 | let code = GCode {
249 | mnemonic: Mnemonic::General,
250 | number: 90.5,
251 | arguments: BigBuffer::default(),
252 | span: Span::default(),
253 | };
254 |
255 | assert_eq!(code.major_number(), 90);
256 | }
257 |
258 | #[test]
259 | fn correct_minor_number() {
260 | for i in 0..=9 {
261 | let code = GCode {
262 | mnemonic: Mnemonic::General,
263 | number: 10.0 + (i as f32) / 10.0,
264 | arguments: BigBuffer::default(),
265 | span: Span::default(),
266 | };
267 |
268 | assert_eq!(code.minor_number(), i);
269 | }
270 | }
271 |
272 | #[test]
273 | fn get_argument_values() {
274 | let mut code = GCode::new_with_argument_buffer(
275 | Mnemonic::General,
276 | 90.0,
277 | Span::default(),
278 | BigBuffer::default(),
279 | );
280 | code.push_argument(Word {
281 | letter: 'X',
282 | value: 10.0,
283 | span: Span::default(),
284 | })
285 | .unwrap();
286 | code.push_argument(Word {
287 | letter: 'y',
288 | value: -3.5,
289 | span: Span::default(),
290 | })
291 | .unwrap();
292 |
293 | assert_eq!(code.value_for('X'), Some(10.0));
294 | assert_eq!(code.value_for('x'), Some(10.0));
295 | assert_eq!(code.value_for('Y'), Some(-3.5));
296 | assert_eq!(code.value_for('Z'), None);
297 | }
298 | }
299 |
--------------------------------------------------------------------------------
/gcode/src/lexer.rs:
--------------------------------------------------------------------------------
1 | use crate::Span;
2 |
3 | #[derive(Debug, Copy, Clone, PartialEq)]
4 | pub(crate) enum TokenType {
5 | Letter,
6 | Number,
7 | Comment,
8 | Unknown,
9 | }
10 |
11 | impl From for TokenType {
12 | fn from(c: char) -> TokenType {
13 | if c.is_ascii_alphabetic() {
14 | TokenType::Letter
15 | } else if c.is_ascii_digit() || c == '.' || c == '-' || c == '+' {
16 | TokenType::Number
17 | } else if c == '(' || c == ';' || c == ')' {
18 | TokenType::Comment
19 | } else {
20 | TokenType::Unknown
21 | }
22 | }
23 | }
24 |
25 | #[derive(Debug, Copy, Clone, PartialEq)]
26 | pub(crate) struct Token<'input> {
27 | pub(crate) kind: TokenType,
28 | pub(crate) value: &'input str,
29 | pub(crate) span: Span,
30 | }
31 |
32 | #[derive(Debug, Clone, PartialEq)]
33 | pub(crate) struct Lexer<'input> {
34 | current_position: usize,
35 | current_line: usize,
36 | src: &'input str,
37 | }
38 |
39 | impl<'input> Lexer<'input> {
40 | pub(crate) fn new(src: &'input str) -> Self {
41 | Lexer {
42 | current_position: 0,
43 | current_line: 0,
44 | src,
45 | }
46 | }
47 |
48 | /// Keep advancing the [`Lexer`] as long as a `predicate` returns `true`,
49 | /// returning the chomped string, if any.
50 | fn chomp(&mut self, mut predicate: F) -> Option<&'input str>
51 | where
52 | F: FnMut(char) -> bool,
53 | {
54 | let start = self.current_position;
55 | let mut end = start;
56 | let mut line_endings = 0;
57 |
58 | for letter in self.rest().chars() {
59 | if !predicate(letter) {
60 | break;
61 | }
62 | if letter == '\n' {
63 | line_endings += 1;
64 | }
65 | end += letter.len_utf8();
66 | }
67 |
68 | if start == end {
69 | None
70 | } else {
71 | self.current_position = end;
72 | self.current_line += line_endings;
73 | Some(&self.src[start..end])
74 | }
75 | }
76 |
77 | fn rest(&self) -> &'input str {
78 | if self.finished() {
79 | ""
80 | } else {
81 | &self.src[self.current_position..]
82 | }
83 | }
84 |
85 | fn skip_whitespace(&mut self) { let _ = self.chomp(char::is_whitespace); }
86 |
87 | fn tokenize_comment(&mut self) -> Option> {
88 | let start = self.current_position;
89 | let line = self.current_line;
90 |
91 | if self.rest().starts_with(';') {
92 | // the comment is every character from ';' to '\n' or EOF
93 | let comment = self.chomp(|c| c != '\n').unwrap_or("");
94 | let end = self.current_position;
95 |
96 | Some(Token {
97 | kind: TokenType::Comment,
98 | value: comment,
99 | span: Span { start, end, line },
100 | })
101 | } else if self.rest().starts_with('(') {
102 | // skip past the comment body
103 | let _ = self.chomp(|c| c != '\n' && c != ')');
104 |
105 | // at this point, it's guaranteed that the next character is '\n',
106 | // ')' or EOF
107 | let kind = self.peek().unwrap_or(TokenType::Unknown);
108 |
109 | if kind == TokenType::Comment {
110 | // we need to consume the closing paren
111 | self.current_position += 1;
112 | }
113 |
114 | let end = self.current_position;
115 | let value = &self.src[start..end];
116 |
117 | Some(Token {
118 | kind,
119 | value,
120 | span: Span { start, end, line },
121 | })
122 | } else {
123 | None
124 | }
125 | }
126 |
127 | fn tokenize_letter(&mut self) -> Option> {
128 | let c = self.rest().chars().next()?;
129 | let start = self.current_position;
130 |
131 | if c.is_ascii_alphabetic() {
132 | self.current_position += 1;
133 | Some(Token {
134 | kind: TokenType::Letter,
135 | value: &self.src[start..=start],
136 | span: Span {
137 | start,
138 | end: start + 1,
139 | line: self.current_line,
140 | },
141 | })
142 | } else {
143 | None
144 | }
145 | }
146 |
147 | fn tokenize_number(&mut self) -> Option> {
148 | let start = self.current_position;
149 | let line = self.current_line;
150 |
151 | let mut decimal_seen = false;
152 | let mut letters_seen = 0;
153 |
154 | let value = self.chomp(|c| {
155 | letters_seen += 1;
156 | let is_sign = c == '-' || c == '+';
157 |
158 | if (is_sign && letters_seen == 1) || c.is_ascii_digit() {
159 | true
160 | } else if c == '.' && !decimal_seen {
161 | decimal_seen = true;
162 | true
163 | } else {
164 | false
165 | }
166 | })?;
167 |
168 | Some(Token {
169 | kind: TokenType::Number,
170 | value,
171 | span: Span {
172 | start,
173 | line,
174 | end: self.current_position,
175 | },
176 | })
177 | }
178 |
179 | fn finished(&self) -> bool { self.current_position >= self.src.len() }
180 |
181 | fn peek(&self) -> Option {
182 | self.rest().chars().next().map(TokenType::from)
183 | }
184 | }
185 |
186 | impl<'input> From<&'input str> for Lexer<'input> {
187 | fn from(other: &'input str) -> Lexer<'input> { Lexer::new(other) }
188 | }
189 |
190 | impl<'input> Iterator for Lexer<'input> {
191 | type Item = Token<'input>;
192 |
193 | fn next(&mut self) -> Option {
194 | const MSG: &str =
195 | "This should be unreachable, we've already done a bounds check";
196 | self.skip_whitespace();
197 |
198 | let start = self.current_position;
199 | let line = self.current_line;
200 |
201 | while let Some(kind) = self.peek() {
202 | if kind != TokenType::Unknown && self.current_position != start {
203 | // we've finished processing some garbage
204 | let end = self.current_position;
205 | return Some(Token {
206 | kind: TokenType::Unknown,
207 | value: &self.src[start..end],
208 | span: Span::new(start, end, line),
209 | });
210 | }
211 |
212 | match kind {
213 | TokenType::Comment => {
214 | return Some(self.tokenize_comment().expect(MSG))
215 | },
216 | TokenType::Letter => {
217 | return Some(self.tokenize_letter().expect(MSG))
218 | },
219 | TokenType::Number => {
220 | return Some(self.tokenize_number().expect(MSG))
221 | },
222 | TokenType::Unknown => self.current_position += 1,
223 | }
224 | }
225 |
226 | if self.current_position != start {
227 | // make sure we deal with trailing garbage
228 | Some(Token {
229 | kind: TokenType::Unknown,
230 | value: &self.src[start..],
231 | span: Span::new(start, self.current_position, line),
232 | })
233 | } else {
234 | None
235 | }
236 | }
237 | }
238 |
239 | #[cfg(test)]
240 | mod tests {
241 | use super::*;
242 |
243 | #[test]
244 | fn take_while_works_as_expected() {
245 | let mut lexer = Lexer::new("12345abcd");
246 |
247 | let got = lexer.chomp(|c| c.is_digit(10));
248 |
249 | assert_eq!(got, Some("12345"));
250 | assert_eq!(lexer.current_position, 5);
251 | assert_eq!(lexer.rest(), "abcd");
252 | }
253 |
254 | #[test]
255 | fn skip_whitespace() {
256 | let mut lexer = Lexer::new(" \n\r\t ");
257 |
258 | lexer.skip_whitespace();
259 |
260 | assert_eq!(lexer.current_position, lexer.src.len());
261 | assert_eq!(lexer.current_line, 1);
262 | }
263 |
264 | #[test]
265 | fn tokenize_a_semicolon_comment() {
266 | let mut lexer = Lexer::new("; this is a comment\nbut this is not");
267 | let newline = lexer.src.find('\n').unwrap();
268 |
269 | let got = lexer.next().unwrap();
270 |
271 | assert_eq!(got.value, "; this is a comment");
272 | assert_eq!(got.kind, TokenType::Comment);
273 | assert_eq!(
274 | got.span,
275 | Span {
276 | start: 0,
277 | end: newline,
278 | line: 0
279 | }
280 | );
281 | assert_eq!(lexer.current_position, newline);
282 | }
283 |
284 | #[test]
285 | fn tokenize_a_parens_comment() {
286 | let mut lexer = Lexer::new("( this is a comment) but this is not");
287 | let comment = "( this is a comment)";
288 |
289 | let got = lexer.next().unwrap();
290 |
291 | assert_eq!(got.value, comment);
292 | assert_eq!(got.kind, TokenType::Comment);
293 | assert_eq!(
294 | got.span,
295 | Span {
296 | start: 0,
297 | end: comment.len(),
298 | line: 0
299 | }
300 | );
301 | assert_eq!(lexer.current_position, comment.len());
302 | }
303 |
304 | #[test]
305 | fn unclosed_parens_are_garbage() {
306 | let mut lexer = Lexer::new("( missing a closing paren");
307 |
308 | let got = lexer.next().unwrap();
309 |
310 | assert_eq!(got.value, lexer.src);
311 | assert_eq!(got.kind, TokenType::Unknown);
312 | assert_eq!(got.span.end, lexer.src.len());
313 | assert_eq!(lexer.current_position, lexer.src.len());
314 | }
315 |
316 | #[test]
317 | fn invalid_characters_are_all_garbage_until_next_valid_character() {
318 | let mut lexer = Lexer::new("$# ! @ x52");
319 | let expected = Token {
320 | value: "$# ! @ ",
321 | kind: TokenType::Unknown,
322 | span: Span::new(0, 7, 0),
323 | };
324 |
325 | let got = lexer.next().unwrap();
326 |
327 | assert_eq!(got, expected);
328 | assert_eq!(lexer.current_position, 7);
329 | let next = lexer.next().unwrap();
330 | assert_eq!(next.value, "x");
331 | }
332 |
333 | #[test]
334 | fn tokenize_a_letter() {
335 | let mut lexer = Lexer::new("asd\nf");
336 |
337 | let got = lexer.next().unwrap();
338 |
339 | assert_eq!(got.value, "a");
340 | assert_eq!(got.kind, TokenType::Letter);
341 | assert_eq!(got.span.end, 1);
342 | assert_eq!(lexer.current_position, 1);
343 | }
344 |
345 | #[test]
346 | fn normal_number() {
347 | let mut lexer = Lexer::new("3.14.56\nf");
348 |
349 | let got = lexer.next().unwrap();
350 |
351 | assert_eq!(got.value, "3.14");
352 | assert_eq!(got.kind, TokenType::Number);
353 | assert_eq!(got.span.end, 4);
354 | assert_eq!(lexer.current_position, 4);
355 | }
356 |
357 | #[test]
358 | fn negative_number() {
359 | let mut lexer = Lexer::new("-3.14\nf");
360 |
361 | let got = lexer.next().unwrap();
362 |
363 | assert_eq!(got.value, "-3.14");
364 | }
365 |
366 | #[test]
367 | fn positive_number() {
368 | let mut lexer = Lexer::new("+3.14\nf");
369 |
370 | let got = lexer.next().unwrap();
371 |
372 | assert_eq!(got.value, "+3.14");
373 | }
374 | }
375 |
--------------------------------------------------------------------------------
/gcode/src/lib.rs:
--------------------------------------------------------------------------------
1 | //! A crate for parsing g-code programs, designed with embedded environments in
2 | //! mind.
3 | //!
4 | //! Some explicit design goals of this crate are:
5 | //!
6 | //! - **embedded-friendly:** users should be able to use this crate without
7 | //! requiring access to an operating system (e.g. `#[no_std]` environments or
8 | //! WebAssembly)
9 | //! - **deterministic memory usage:** the library can be tweaked to use no
10 | //! dynamic allocation (see [`buffers::Buffers`])
11 | //! - **error-resistant:** erroneous input won't abort parsing, instead
12 | //! notifying the caller and continuing on (see [`Callbacks`])
13 | //! - **performance:** parsing should be reasonably fast, guaranteeing `O(n)`
14 | //! time complexity with no backtracking
15 | //!
16 | //! # Getting Started
17 | //!
18 | //! The typical entry point to this crate is via the [`parse()`] function. This
19 | //! gives you an iterator over the [`GCode`]s in a string of text, ignoring any
20 | //! errors or comments that may appear along the way.
21 | //!
22 | //! ```rust
23 | //! use gcode::Mnemonic;
24 | //!
25 | //! let src = r#"
26 | //! G90 (absolute coordinates)
27 | //! G00 X50.0 Y-10 (move somewhere)
28 | //! "#;
29 | //!
30 | //! let got: Vec<_> = gcode::parse(src).collect();
31 | //!
32 | //! assert_eq!(got.len(), 2);
33 | //!
34 | //! let g90 = &got[0];
35 | //! assert_eq!(g90.mnemonic(), Mnemonic::General);
36 | //! assert_eq!(g90.major_number(), 90);
37 | //! assert_eq!(g90.minor_number(), 0);
38 | //!
39 | //! let rapid_move = &got[1];
40 | //! assert_eq!(rapid_move.mnemonic(), Mnemonic::General);
41 | //! assert_eq!(rapid_move.major_number(), 0);
42 | //! assert_eq!(rapid_move.value_for('X'), Some(50.0));
43 | //! assert_eq!(rapid_move.value_for('y'), Some(-10.0));
44 | //! ```
45 | //!
46 | //! The [`full_parse_with_callbacks()`] function can be used if you want access
47 | //! to [`Line`] information and to be notified on any parse errors.
48 | //!
49 | //! ```rust
50 | //! use gcode::{Callbacks, Span};
51 | //!
52 | //! /// A custom set of [`Callbacks`] we'll use to keep track of errors.
53 | //! #[derive(Debug, Default)]
54 | //! struct Errors {
55 | //! unexpected_line_number : usize,
56 | //! letter_without_number: usize,
57 | //! garbage: Vec,
58 | //! }
59 | //!
60 | //! impl Callbacks for Errors {
61 | //! fn unknown_content(&mut self, text: &str, _span: Span) {
62 | //! self.garbage.push(text.to_string());
63 | //! }
64 | //!
65 | //! fn unexpected_line_number(&mut self, _line_number: f32, _span: Span) {
66 | //! self.unexpected_line_number += 1;
67 | //! }
68 | //!
69 | //! fn letter_without_a_number(&mut self, _value: &str, _span: Span) {
70 | //! self.letter_without_number += 1;
71 | //! }
72 | //! }
73 | //!
74 | //! let src = r"
75 | //! G90 N1 ; Line numbers (N) should be at the start of a line
76 | //! G ; there was a G, but no number
77 | //! G01 X50 $$%# Y20 ; invalid characters are ignored
78 | //! ";
79 | //!
80 | //! let mut errors = Errors::default();
81 | //!
82 | //! {
83 | //! let lines: Vec<_> = gcode::full_parse_with_callbacks(src, &mut errors)
84 | //! .collect();
85 | //!
86 | //! assert_eq!(lines.len(), 3);
87 | //! let total_gcodes: usize = lines.iter()
88 | //! .map(|line| line.gcodes().len())
89 | //! .sum();
90 | //! assert_eq!(total_gcodes, 2);
91 | //! }
92 | //!
93 | //! assert_eq!(errors.unexpected_line_number, 1);
94 | //! assert_eq!(errors.letter_without_number, 1);
95 | //! assert_eq!(errors.garbage.len(), 1);
96 | //! assert_eq!(errors.garbage[0], "$$%# ");
97 | //! ```
98 | //!
99 | //! # Customising Memory Usage
100 | //!
101 | //! You'll need to manually create a [`Parser`] if you want control over buffer
102 | //! sizes instead of relying on [`buffers::DefaultBuffers`].
103 | //!
104 | //! You shouldn't normally need to do this unless you are on an embedded device
105 | //! and know your expected input will be bigger than
106 | //! [`buffers::SmallFixedBuffers`] will allow.
107 | //!
108 | //! ```rust
109 | //! use gcode::{Word, Comment, GCode, Nop, Parser, buffers::Buffers};
110 | //! use arrayvec::ArrayVec;
111 | //!
112 | //! /// A type-level variable which contains definitions for each of our buffer
113 | //! /// types.
114 | //! enum MyBuffers {}
115 | //!
116 | //! impl<'input> Buffers<'input> for MyBuffers {
117 | //! type Arguments = ArrayVec<[Word; 10]>;
118 | //! type Commands = ArrayVec<[GCode; 2]>;
119 | //! type Comments = ArrayVec<[Comment<'input>; 1]>;
120 | //! }
121 | //!
122 | //! let src = "G90 G01 X5.1";
123 | //!
124 | //! let parser: Parser = Parser::new(src, Nop);
125 | //!
126 | //! let lines = parser.count();
127 | //! assert_eq!(lines, 1);
128 | //! ```
129 | //!
130 | //! # Spans
131 | //!
132 | //! Something that distinguishes this crate from a lot of other g-code parsers
133 | //! is that each element's original location, its [`Span`], is retained and
134 | //! passed to the caller.
135 | //!
136 | //! This is important for applications such as:
137 | //!
138 | //! - Indicating where in the source text a parsing error or semantic error has
139 | //! occurred
140 | //! - Visually highlighting the command currently being executed when stepping
141 | //! through a program in a simulator
142 | //! - Reporting what point a CNC machine is up to when executing a job
143 | //!
144 | //! It's pretty easy to check whether something contains its [`Span`], just look
145 | //! for a `span()` method (e.g. [`GCode::span()`]) or a `span` field (e.g.
146 | //! [`Comment::span`]).
147 | //!
148 | //! # Cargo Features
149 | //!
150 | //! Additional functionality can be enabled by adding feature flags to your
151 | //! `Cargo.toml` file:
152 | //!
153 | //! - **std:** adds `std::error::Error` impls to any errors and switches to
154 | //! `Vec` for the default backing buffers
155 | //! - **serde-1:** allows serializing and deserializing most types with `serde`
156 | #![deny(
157 | bare_trait_objects,
158 | elided_lifetimes_in_paths,
159 | missing_copy_implementations,
160 | missing_debug_implementations,
161 | rust_2018_idioms,
162 | unreachable_pub,
163 | unsafe_code,
164 | unused_qualifications,
165 | unused_results,
166 | variant_size_differences,
167 | intra_doc_link_resolution_failure,
168 | missing_docs
169 | )]
170 | #![cfg_attr(not(feature = "std"), no_std)]
171 | #![cfg_attr(docsrs, feature(doc_cfg))]
172 |
173 | #[cfg(all(test, not(feature = "std")))]
174 | #[macro_use]
175 | extern crate std;
176 |
177 | #[cfg(test)]
178 | #[macro_use]
179 | extern crate pretty_assertions;
180 |
181 | #[macro_use]
182 | mod macros;
183 |
184 | pub mod buffers;
185 | mod callbacks;
186 | mod comment;
187 | mod gcode;
188 | mod lexer;
189 | mod line;
190 | mod parser;
191 | mod span;
192 | mod words;
193 |
194 | pub use crate::{
195 | callbacks::{Callbacks, Nop},
196 | comment::Comment,
197 | gcode::{GCode, Mnemonic},
198 | line::Line,
199 | parser::{full_parse_with_callbacks, parse, Parser},
200 | span::Span,
201 | words::Word,
202 | };
203 |
--------------------------------------------------------------------------------
/gcode/src/line.rs:
--------------------------------------------------------------------------------
1 | use crate::{
2 | buffers::{self, Buffer, Buffers, CapacityError, DefaultBuffers},
3 | Comment, GCode, Span, Word,
4 | };
5 | use core::fmt::{self, Debug, Formatter};
6 |
7 | /// A single line, possibly containing some [`Comment`]s or [`GCode`]s.
8 | #[derive(Clone, PartialEq)]
9 | #[cfg_attr(
10 | feature = "serde-1",
11 | derive(serde_derive::Serialize, serde_derive::Deserialize)
12 | )]
13 | pub struct Line<'input, B: Buffers<'input> = DefaultBuffers> {
14 | gcodes: B::Commands,
15 | comments: B::Comments,
16 | line_number: Option,
17 | span: Span,
18 | }
19 |
20 | impl<'input, B> Debug for Line<'input, B>
21 | where
22 | B: Buffers<'input>,
23 | {
24 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
25 | // explicitly implement Debug because the normal derive is too strict
26 | let Line {
27 | gcodes,
28 | comments,
29 | line_number,
30 | span,
31 | } = self;
32 |
33 | f.debug_struct("Line")
34 | .field("gcodes", &buffers::debug(gcodes))
35 | .field("comments", &buffers::debug(comments))
36 | .field("line_number", line_number)
37 | .field("span", span)
38 | .finish()
39 | }
40 | }
41 |
42 | impl<'input, B> Default for Line<'input, B>
43 | where
44 | B: Buffers<'input>,
45 | B::Commands: Default,
46 | B::Comments: Default,
47 | {
48 | fn default() -> Line<'input, B> {
49 | Line {
50 | gcodes: B::Commands::default(),
51 | comments: B::Comments::default(),
52 | line_number: None,
53 | span: Span::default(),
54 | }
55 | }
56 | }
57 |
58 | impl<'input, B: Buffers<'input>> Line<'input, B> {
59 | /// All [`GCode`]s in this line.
60 | pub fn gcodes(&self) -> &[GCode] { self.gcodes.as_slice() }
61 |
62 | /// All [`Comment`]s in this line.
63 | pub fn comments(&self) -> &[Comment<'input>] { self.comments.as_slice() }
64 |
65 | /// Try to add another [`GCode`] to the line.
66 | pub fn push_gcode(
67 | &mut self,
68 | gcode: GCode,
69 | ) -> Result<(), CapacityError>> {
70 | // Note: We need to make sure a failed push doesn't change our span
71 | let span = self.span.merge(gcode.span());
72 | self.gcodes.try_push(gcode)?;
73 | self.span = span;
74 |
75 | Ok(())
76 | }
77 |
78 | /// Try to add a [`Comment`] to the line.
79 | pub fn push_comment(
80 | &mut self,
81 | comment: Comment<'input>,
82 | ) -> Result<(), CapacityError>> {
83 | let span = self.span.merge(comment.span);
84 | self.comments.try_push(comment)?;
85 | self.span = span;
86 | Ok(())
87 | }
88 |
89 | /// Does the [`Line`] contain anything at all?
90 | pub fn is_empty(&self) -> bool {
91 | self.gcodes.as_slice().is_empty()
92 | && self.comments.as_slice().is_empty()
93 | && self.line_number().is_none()
94 | }
95 |
96 | /// Try to get the line number, if there was one.
97 | pub fn line_number(&self) -> Option { self.line_number }
98 |
99 | /// Set the [`Line::line_number()`].
100 | pub fn set_line_number>>(&mut self, line_number: W) {
101 | match line_number.into() {
102 | Some(n) => {
103 | self.span = self.span.merge(n.span);
104 | self.line_number = Some(n);
105 | },
106 | None => self.line_number = None,
107 | }
108 | }
109 |
110 | /// Get the [`Line`]'s position in its source text.
111 | pub fn span(&self) -> Span { self.span }
112 |
113 | pub(crate) fn into_gcodes(self) -> B::Commands { self.gcodes }
114 | }
115 |
--------------------------------------------------------------------------------
/gcode/src/macros.rs:
--------------------------------------------------------------------------------
1 | /// Declare some items as requiring the "std" feature flag.
2 | macro_rules! with_std {
3 | ($($item:item)*) => {
4 | $(
5 | #[cfg(feature = "std")]
6 | #[cfg_attr(docsrs, doc(cfg(feature = "std")))]
7 | $item
8 | )*
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/gcode/src/parser.rs:
--------------------------------------------------------------------------------
1 | use crate::{
2 | buffers::{Buffers, DefaultBuffers},
3 | lexer::{Lexer, Token, TokenType},
4 | words::{Atom, Word, WordsOrComments},
5 | Callbacks, Comment, GCode, Line, Mnemonic, Nop,
6 | };
7 | use core::{iter::Peekable, marker::PhantomData};
8 |
9 | /// Parse each [`GCode`] in some text, ignoring any errors that may occur or
10 | /// [`Comment`]s that are found.
11 | ///
12 | /// This function is probably what you are looking for if you just want to read
13 | /// the [`GCode`] commands in a program. If more detailed information is needed,
14 | /// have a look at [`full_parse_with_callbacks()`].
15 | pub fn parse<'input>(src: &'input str) -> impl Iterator- + 'input {
16 | full_parse_with_callbacks(src, Nop).flat_map(|line| line.into_gcodes())
17 | }
18 |
19 | /// Parse each [`Line`] in some text, using the provided [`Callbacks`] when a
20 | /// parse error occurs that we can recover from.
21 | ///
22 | /// Unlike [`parse()`], this function will also give you access to any comments
23 | /// and line numbers that are found, plus the location of the entire [`Line`]
24 | /// in its source text.
25 | pub fn full_parse_with_callbacks<'input, C: Callbacks + 'input>(
26 | src: &'input str,
27 | callbacks: C,
28 | ) -> impl Iterator
- > + 'input {
29 | let tokens = Lexer::new(src);
30 | let atoms = WordsOrComments::new(tokens);
31 | Lines::new(atoms, callbacks)
32 | }
33 |
34 | /// A parser for parsing g-code programs.
35 | #[derive(Debug)]
36 | pub struct Parser<'input, C, B = DefaultBuffers> {
37 | // Explicitly instantiate Lines so Parser's type parameters don't expose
38 | // internal details
39 | lines: Lines<'input, WordsOrComments<'input, Lexer<'input>>, C, B>,
40 | }
41 |
42 | impl<'input, C, B> Parser<'input, C, B> {
43 | /// Create a new [`Parser`] from some source text and a set of
44 | /// [`Callbacks`].
45 | pub fn new(src: &'input str, callbacks: C) -> Self {
46 | let tokens = Lexer::new(src);
47 | let atoms = WordsOrComments::new(tokens);
48 | let lines = Lines::new(atoms, callbacks);
49 | Parser { lines }
50 | }
51 | }
52 |
53 | impl<'input, B> From<&'input str> for Parser<'input, Nop, B> {
54 | fn from(src: &'input str) -> Self { Parser::new(src, Nop) }
55 | }
56 |
57 | impl<'input, C: Callbacks, B: Buffers<'input>> Iterator
58 | for Parser<'input, C, B>
59 | {
60 | type Item = Line<'input, B>;
61 |
62 | fn next(&mut self) -> Option { self.lines.next() }
63 | }
64 |
65 | #[derive(Debug)]
66 | struct Lines<'input, I, C, B>
67 | where
68 | I: Iterator
- >,
69 | {
70 | atoms: Peekable,
71 | callbacks: C,
72 | last_gcode_type: Option,
73 | _buffers: PhantomData,
74 | }
75 |
76 | impl<'input, I, C, B> Lines<'input, I, C, B>
77 | where
78 | I: Iterator
- >,
79 | {
80 | fn new(atoms: I, callbacks: C) -> Self {
81 | Lines {
82 | atoms: atoms.peekable(),
83 | callbacks,
84 | last_gcode_type: None,
85 | _buffers: PhantomData,
86 | }
87 | }
88 | }
89 |
90 | impl<'input, I, C, B> Lines<'input, I, C, B>
91 | where
92 | I: Iterator
- >,
93 | C: Callbacks,
94 | B: Buffers<'input>,
95 | {
96 | fn handle_line_number(
97 | &mut self,
98 | word: Word,
99 | line: &mut Line<'input, B>,
100 | has_temp_gcode: bool,
101 | ) {
102 | if line.gcodes().is_empty()
103 | && line.line_number().is_none()
104 | && !has_temp_gcode
105 | {
106 | line.set_line_number(word);
107 | } else {
108 | self.callbacks.unexpected_line_number(word.value, word.span);
109 | }
110 | }
111 |
112 | fn handle_arg(
113 | &mut self,
114 | word: Word,
115 | line: &mut Line<'input, B>,
116 | temp_gcode: &mut Option>,
117 | ) {
118 | if let Some(mnemonic) = Mnemonic::for_letter(word.letter) {
119 | // we need to start another gcode. push the one we were building
120 | // onto the line so we can start working on the next one
121 | self.last_gcode_type = Some(word);
122 | if let Some(completed) = temp_gcode.take() {
123 | if let Err(e) = line.push_gcode(completed) {
124 | self.on_gcode_push_error(e.0);
125 | }
126 | }
127 | *temp_gcode = Some(GCode::new_with_argument_buffer(
128 | mnemonic,
129 | word.value,
130 | word.span,
131 | B::Arguments::default(),
132 | ));
133 | return;
134 | }
135 |
136 | // we've got an argument, try adding it to the gcode we're building
137 | if let Some(temp) = temp_gcode {
138 | if let Err(e) = temp.push_argument(word) {
139 | self.on_arg_push_error(&temp, e.0);
140 | }
141 | return;
142 | }
143 |
144 | // we haven't already started building a gcode, maybe the author elided
145 | // the command ("G90") and wants to use the one from the last line?
146 | match self.last_gcode_type {
147 | Some(ty) => {
148 | let mut new_gcode = GCode::new_with_argument_buffer(
149 | Mnemonic::for_letter(ty.letter).unwrap(),
150 | ty.value,
151 | ty.span,
152 | B::Arguments::default(),
153 | );
154 | if let Err(e) = new_gcode.push_argument(word) {
155 | self.on_arg_push_error(&new_gcode, e.0);
156 | }
157 | *temp_gcode = Some(new_gcode);
158 | },
159 | // oh well, you can't say we didn't try...
160 | None => {
161 | self.callbacks.argument_without_a_command(
162 | word.letter,
163 | word.value,
164 | word.span,
165 | );
166 | },
167 | }
168 | }
169 |
170 | fn handle_broken_word(&mut self, token: Token<'_>) {
171 | if token.kind == TokenType::Letter {
172 | self.callbacks
173 | .letter_without_a_number(token.value, token.span);
174 | } else {
175 | self.callbacks
176 | .number_without_a_letter(token.value, token.span);
177 | }
178 | }
179 |
180 | fn on_arg_push_error(&mut self, gcode: &GCode, arg: Word) {
181 | self.callbacks.gcode_argument_buffer_overflowed(
182 | gcode.mnemonic(),
183 | gcode.major_number(),
184 | gcode.minor_number(),
185 | arg,
186 | );
187 | }
188 |
189 | fn on_comment_push_error(&mut self, comment: Comment<'_>) {
190 | self.callbacks.comment_buffer_overflow(comment);
191 | }
192 |
193 | fn on_gcode_push_error(&mut self, gcode: GCode) {
194 | self.callbacks.gcode_buffer_overflowed(
195 | gcode.mnemonic(),
196 | gcode.major_number(),
197 | gcode.minor_number(),
198 | gcode.arguments(),
199 | gcode.span(),
200 | );
201 | }
202 |
203 | fn next_line_number(&mut self) -> Option {
204 | self.atoms.peek().map(|a| a.span().line)
205 | }
206 | }
207 |
208 | impl<'input, I, C, B> Iterator for Lines<'input, I, C, B>
209 | where
210 | I: Iterator
- > + 'input,
211 | C: Callbacks,
212 | B: Buffers<'input>,
213 | {
214 | type Item = Line<'input, B>;
215 |
216 | fn next(&mut self) -> Option {
217 | let mut line = Line::default();
218 | // we need a scratch space for the gcode we're in the middle of
219 | // constructing
220 | let mut temp_gcode = None;
221 |
222 | while let Some(next_line) = self.next_line_number() {
223 | if !line.is_empty() && next_line != line.span().line {
224 | // we've started the next line
225 | break;
226 | }
227 |
228 | match self.atoms.next().expect("unreachable") {
229 | Atom::Unknown(token) => {
230 | self.callbacks.unknown_content(token.value, token.span)
231 | },
232 | Atom::Comment(comment) => {
233 | if let Err(e) = line.push_comment(comment) {
234 | self.on_comment_push_error(e.0);
235 | }
236 | },
237 | // line numbers are annoying, so handle them separately
238 | Atom::Word(word) if word.letter.to_ascii_lowercase() == 'n' => {
239 | self.handle_line_number(
240 | word,
241 | &mut line,
242 | temp_gcode.is_some(),
243 | );
244 | },
245 | Atom::Word(word) => {
246 | self.handle_arg(word, &mut line, &mut temp_gcode)
247 | },
248 | Atom::BrokenWord(token) => self.handle_broken_word(token),
249 | }
250 | }
251 |
252 | if let Some(gcode) = temp_gcode.take() {
253 | if let Err(e) = line.push_gcode(gcode) {
254 | self.on_gcode_push_error(e.0);
255 | }
256 | }
257 |
258 | if line.is_empty() {
259 | None
260 | } else {
261 | Some(line)
262 | }
263 | }
264 | }
265 |
266 | #[cfg(test)]
267 | mod tests {
268 | use super::*;
269 | use crate::Span;
270 | use arrayvec::ArrayVec;
271 | use std::{prelude::v1::*, sync::Mutex};
272 |
273 | #[derive(Debug)]
274 | struct MockCallbacks<'a> {
275 | unexpected_line_number: &'a Mutex>,
276 | }
277 |
278 | impl<'a> Callbacks for MockCallbacks<'a> {
279 | fn unexpected_line_number(&mut self, line_number: f32, span: Span) {
280 | self.unexpected_line_number
281 | .lock()
282 | .unwrap()
283 | .push((line_number, span));
284 | }
285 | }
286 |
287 | #[derive(Debug, Copy, Clone, PartialEq)]
288 | enum BigBuffers {}
289 |
290 | impl<'input> Buffers<'input> for BigBuffers {
291 | type Arguments = ArrayVec<[Word; 16]>;
292 | type Commands = ArrayVec<[GCode; 16]>;
293 | type Comments = ArrayVec<[Comment<'input>; 16]>;
294 | }
295 |
296 | fn parse(
297 | src: &str,
298 | ) -> Lines<'_, impl Iterator
- >, Nop, BigBuffers> {
299 | let tokens = Lexer::new(src);
300 | let atoms = WordsOrComments::new(tokens);
301 | Lines::new(atoms, Nop)
302 | }
303 |
304 | #[test]
305 | fn we_can_parse_a_comment() {
306 | let src = "(this is a comment)";
307 | let got: Vec<_> = parse(src).collect();
308 |
309 | assert_eq!(got.len(), 1);
310 | let line = &got[0];
311 | assert_eq!(line.comments().len(), 1);
312 | assert_eq!(line.gcodes().len(), 0);
313 | assert_eq!(line.span(), Span::new(0, src.len(), 0));
314 | }
315 |
316 | #[test]
317 | fn line_numbers() {
318 | let src = "N42";
319 | let got: Vec<_> = parse(src).collect();
320 |
321 | assert_eq!(got.len(), 1);
322 | let line = &got[0];
323 | assert_eq!(line.comments().len(), 0);
324 | assert_eq!(line.gcodes().len(), 0);
325 | let span = Span::new(0, src.len(), 0);
326 | assert_eq!(
327 | line.line_number(),
328 | Some(Word {
329 | letter: 'N',
330 | value: 42.0,
331 | span
332 | })
333 | );
334 | assert_eq!(line.span(), span);
335 | }
336 |
337 | #[test]
338 | fn line_numbers_after_the_start_are_an_error() {
339 | let src = "G90 N42";
340 | let unexpected_line_number = Default::default();
341 | let got: Vec<_> = full_parse_with_callbacks(
342 | src,
343 | MockCallbacks {
344 | unexpected_line_number: &unexpected_line_number,
345 | },
346 | )
347 | .collect();
348 |
349 | assert_eq!(got.len(), 1);
350 | assert!(got[0].line_number().is_none());
351 | let unexpected_line_number = unexpected_line_number.lock().unwrap();
352 | assert_eq!(unexpected_line_number.len(), 1);
353 | assert_eq!(unexpected_line_number[0].0, 42.0);
354 | }
355 |
356 | #[test]
357 | fn parse_g90() {
358 | let src = "G90";
359 | let got: Vec<_> = parse(src).collect();
360 |
361 | assert_eq!(got.len(), 1);
362 | let line = &got[0];
363 | assert_eq!(line.gcodes().len(), 1);
364 | let g90 = &line.gcodes()[0];
365 | assert_eq!(g90.major_number(), 90);
366 | assert_eq!(g90.minor_number(), 0);
367 | assert_eq!(g90.arguments().len(), 0);
368 | }
369 |
370 | #[test]
371 | fn parse_command_with_arguments() {
372 | let src = "G01X5 Y-20";
373 | let should_be =
374 | GCode::new(Mnemonic::General, 1.0, Span::new(0, src.len(), 0))
375 | .with_argument(Word {
376 | letter: 'X',
377 | value: 5.0,
378 | span: Span::new(3, 5, 0),
379 | })
380 | .with_argument(Word {
381 | letter: 'Y',
382 | value: -20.0,
383 | span: Span::new(6, 10, 0),
384 | });
385 |
386 | let got: Vec<_> = parse(src).collect();
387 |
388 | assert_eq!(got.len(), 1);
389 | let line = &got[0];
390 | assert_eq!(line.gcodes().len(), 1);
391 | let g01 = &line.gcodes()[0];
392 | assert_eq!(g01, &should_be);
393 | }
394 |
395 | #[test]
396 | fn multiple_commands_on_the_same_line() {
397 | let src = "G01 X5 G90 (comment) G91 M10\nG01";
398 |
399 | let got: Vec<_> = parse(src).collect();
400 |
401 | assert_eq!(got.len(), 2);
402 | assert_eq!(got[0].gcodes().len(), 4);
403 | assert_eq!(got[0].comments().len(), 1);
404 | assert_eq!(got[1].gcodes().len(), 1);
405 | }
406 |
407 | /// I wasn't sure if the `#[derive(Serialize)]` would work given we use
408 | /// `B::Comments`, which would borrow from the original source.
409 | #[test]
410 | #[cfg(feature = "serde-1")]
411 | fn you_can_actually_serialize_lines() {
412 | let src = "G01 X5 G90 (comment) G91 M10\nG01\n";
413 | let line = parse(src).next().unwrap();
414 |
415 | fn assert_serializable(_: &S) {}
416 | fn assert_deserializable<'de, D: serde::Deserialize<'de>>() {}
417 |
418 | assert_serializable(&line);
419 | assert_deserializable::>();
420 | }
421 |
422 | /// For some reason we were parsing the G90, then an empty G01 and the
423 | /// actual G01.
424 | #[test]
425 | #[ignore]
426 | fn funny_bug_in_crate_example() {
427 | let src = "G90 \n G01 X50.0 Y-10";
428 | let expected = vec![
429 | GCode::new(Mnemonic::General, 90.0, Span::PLACEHOLDER),
430 | GCode::new(Mnemonic::General, 1.0, Span::PLACEHOLDER)
431 | .with_argument(Word::new('X', 50.0, Span::PLACEHOLDER))
432 | .with_argument(Word::new('Y', -10.0, Span::PLACEHOLDER)),
433 | ];
434 |
435 | let got: Vec<_> = crate::parse(src).collect();
436 |
437 | assert_eq!(got, expected);
438 | }
439 | }
440 |
--------------------------------------------------------------------------------
/gcode/src/span.rs:
--------------------------------------------------------------------------------
1 | use core::{
2 | cmp,
3 | fmt::{self, Debug, Formatter},
4 | ops::Range,
5 | };
6 |
7 | /// A half-open range which indicates the location of something in a body of
8 | /// text.
9 | #[derive(Copy, Clone, Eq)]
10 | #[cfg_attr(
11 | feature = "serde-1",
12 | derive(serde_derive::Serialize, serde_derive::Deserialize)
13 | )]
14 | #[repr(C)]
15 | pub struct Span {
16 | /// The byte index corresponding to the item's start.
17 | pub start: usize,
18 | /// The index one byte past the item's end.
19 | pub end: usize,
20 | /// The (zero-based) line number.
21 | pub line: usize,
22 | }
23 |
24 | impl Span {
25 | /// A placeholder [`Span`] which will be ignored by [`Span::merge()`] and
26 | /// equality checks.
27 | pub const PLACEHOLDER: Span =
28 | Span::new(usize::max_value(), usize::max_value(), usize::max_value());
29 |
30 | /// Create a new [`Span`].
31 | pub const fn new(start: usize, end: usize, line: usize) -> Self {
32 | Span { start, end, line }
33 | }
34 |
35 | /// Get the string this [`Span`] corresponds to.
36 | ///
37 | /// Passing in a different string will probably lead to... strange...
38 | /// results.
39 | pub fn get_text<'input>(&self, src: &'input str) -> Option<&'input str> {
40 | src.get(self.start..self.end)
41 | }
42 |
43 | /// Merge two [`Span`]s, making sure [`Span::PLACEHOLDER`] spans go away.
44 | pub fn merge(self, other: Span) -> Span {
45 | if self.is_placeholder() {
46 | other
47 | } else if other.is_placeholder() {
48 | self
49 | } else {
50 | Span {
51 | start: cmp::min(self.start, other.start),
52 | end: cmp::max(self.end, other.end),
53 | line: cmp::min(self.line, other.line),
54 | }
55 | }
56 | }
57 |
58 | /// Is this a [`Span::PLACEHOLDER`]?
59 | pub fn is_placeholder(self) -> bool {
60 | let Span { start, end, line } = Span::PLACEHOLDER;
61 |
62 | self.start == start && self.end == end && self.line == line
63 | }
64 | }
65 |
66 | impl PartialEq for Span {
67 | fn eq(&self, other: &Span) -> bool {
68 | let Span { start, end, line } = *other;
69 |
70 | self.is_placeholder()
71 | || other.is_placeholder()
72 | || (self.start == start && self.end == end && self.line == line)
73 | }
74 | }
75 |
76 | impl From for Range {
77 | fn from(other: Span) -> Range { other.start..other.end }
78 | }
79 |
80 | impl Debug for Span {
81 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
82 | if self.is_placeholder() {
83 | write!(f, "")
84 | } else {
85 | let Span { start, end, line } = self;
86 |
87 | f.debug_struct("Span")
88 | .field("start", start)
89 | .field("end", end)
90 | .field("line", line)
91 | .finish()
92 | }
93 | }
94 | }
95 |
96 | impl Default for Span {
97 | fn default() -> Span { Span::PLACEHOLDER }
98 | }
99 |
100 | #[cfg(test)]
101 | mod tests {
102 | use super::*;
103 |
104 | #[test]
105 | fn a_span_is_equal_to_itself() {
106 | let span = Span::new(1, 2, 3);
107 |
108 | assert_eq!(span, span);
109 | }
110 |
111 | #[test]
112 | fn all_spans_are_equal_to_the_placeholder() {
113 | let inputs = vec![
114 | Span::default(),
115 | Span::PLACEHOLDER,
116 | Span::new(42, 0, 0),
117 | Span::new(0, 42, 0),
118 | Span::new(0, 0, 42),
119 | ];
120 |
121 | for input in inputs {
122 | assert_eq!(input, Span::PLACEHOLDER);
123 | }
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/gcode/src/words.rs:
--------------------------------------------------------------------------------
1 | use crate::{
2 | lexer::{Lexer, Token, TokenType},
3 | Comment, Span,
4 | };
5 | use core::fmt::{self, Display, Formatter};
6 |
7 | /// A [`char`]-[`f32`] pair, used for things like arguments (`X3.14`), command
8 | /// numbers (`G90`) and line numbers (`N10`).
9 | #[derive(Debug, Copy, Clone, PartialEq)]
10 | #[cfg_attr(
11 | feature = "serde-1",
12 | derive(serde_derive::Serialize, serde_derive::Deserialize)
13 | )]
14 | #[repr(C)]
15 | pub struct Word {
16 | /// The letter part of this [`Word`].
17 | pub letter: char,
18 | /// The value part.
19 | pub value: f32,
20 | /// Where the [`Word`] lies in the original string.
21 | pub span: Span,
22 | }
23 |
24 | impl Word {
25 | /// Create a new [`Word`].
26 | pub fn new(letter: char, value: f32, span: Span) -> Self {
27 | Word {
28 | letter,
29 | value,
30 | span,
31 | }
32 | }
33 | }
34 |
35 | impl Display for Word {
36 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
37 | write!(f, "{}{}", self.letter, self.value)
38 | }
39 | }
40 |
41 | #[derive(Debug, Copy, Clone, PartialEq)]
42 | pub(crate) enum Atom<'input> {
43 | Word(Word),
44 | Comment(Comment<'input>),
45 | /// Incomplete parts of a [`Word`].
46 | BrokenWord(Token<'input>),
47 | /// Garbage from the tokenizer (see [`TokenType::Unknown`]).
48 | Unknown(Token<'input>),
49 | }
50 |
51 | impl<'input> Atom<'input> {
52 | pub(crate) fn span(&self) -> Span {
53 | match self {
54 | Atom::Word(word) => word.span,
55 | Atom::Comment(comment) => comment.span,
56 | Atom::Unknown(token) | Atom::BrokenWord(token) => token.span,
57 | }
58 | }
59 | }
60 |
61 | #[derive(Debug, Clone, PartialEq)]
62 | pub(crate) struct WordsOrComments<'input, I> {
63 | tokens: I,
64 | /// keep track of the last letter so we can deal with a trailing letter
65 | /// that has no number
66 | last_letter: Option>,
67 | }
68 |
69 | impl<'input, I> WordsOrComments<'input, I>
70 | where
71 | I: Iterator
- >,
72 | {
73 | pub(crate) fn new(tokens: I) -> Self {
74 | WordsOrComments {
75 | tokens,
76 | last_letter: None,
77 | }
78 | }
79 | }
80 |
81 | impl<'input, I> Iterator for WordsOrComments<'input, I>
82 | where
83 | I: Iterator
- >,
84 | {
85 | type Item = Atom<'input>;
86 |
87 | fn next(&mut self) -> Option {
88 | while let Some(token) = self.tokens.next() {
89 | let Token { kind, value, span } = token;
90 |
91 | match kind {
92 | TokenType::Unknown => return Some(Atom::Unknown(token)),
93 | TokenType::Comment => {
94 | return Some(Atom::Comment(Comment { value, span }))
95 | },
96 | TokenType::Letter if self.last_letter.is_none() => {
97 | self.last_letter = Some(token);
98 | },
99 | TokenType::Number if self.last_letter.is_some() => {
100 | let letter_token = self.last_letter.take().unwrap();
101 | let span = letter_token.span.merge(span);
102 |
103 | debug_assert_eq!(letter_token.value.len(), 1);
104 | let letter = letter_token.value.chars().next().unwrap();
105 | let value = value.parse().expect("");
106 |
107 | return Some(Atom::Word(Word {
108 | letter,
109 | value,
110 | span,
111 | }));
112 | },
113 | _ => return Some(Atom::BrokenWord(token)),
114 | }
115 | }
116 |
117 | self.last_letter.take().map(Atom::BrokenWord)
118 | }
119 | }
120 |
121 | impl<'input> From<&'input str> for WordsOrComments<'input, Lexer<'input>> {
122 | fn from(other: &'input str) -> WordsOrComments<'input, Lexer<'input>> {
123 | WordsOrComments::new(Lexer::new(other))
124 | }
125 | }
126 |
127 | #[cfg(test)]
128 | mod tests {
129 | use super::*;
130 | use crate::lexer::Lexer;
131 |
132 | #[test]
133 | fn pass_comments_through() {
134 | let mut words =
135 | WordsOrComments::new(Lexer::new("(this is a comment) 3.14"));
136 |
137 | let got = words.next().unwrap();
138 |
139 | let comment = "(this is a comment)";
140 | let expected = Atom::Comment(Comment {
141 | value: comment,
142 | span: Span {
143 | start: 0,
144 | end: comment.len(),
145 | line: 0,
146 | },
147 | });
148 | assert_eq!(got, expected);
149 | }
150 |
151 | #[test]
152 | fn pass_garbage_through() {
153 | let text = "!@#$ *";
154 | let mut words = WordsOrComments::new(Lexer::new(text));
155 |
156 | let got = words.next().unwrap();
157 |
158 | let expected = Atom::Unknown(Token {
159 | value: text,
160 | kind: TokenType::Unknown,
161 | span: Span {
162 | start: 0,
163 | end: text.len(),
164 | line: 0,
165 | },
166 | });
167 | assert_eq!(got, expected);
168 | }
169 |
170 | #[test]
171 | fn numbers_are_garbage_if_they_dont_have_a_letter_in_front() {
172 | let text = "3.14 ()";
173 | let mut words = WordsOrComments::new(Lexer::new(text));
174 |
175 | let got = words.next().unwrap();
176 |
177 | let expected = Atom::BrokenWord(Token {
178 | value: "3.14",
179 | kind: TokenType::Number,
180 | span: Span {
181 | start: 0,
182 | end: 4,
183 | line: 0,
184 | },
185 | });
186 | assert_eq!(got, expected);
187 | }
188 |
189 | #[test]
190 | fn recognise_a_valid_word() {
191 | let text = "G90";
192 | let mut words = WordsOrComments::new(Lexer::new(text));
193 |
194 | let got = words.next().unwrap();
195 |
196 | let expected = Atom::Word(Word {
197 | letter: 'G',
198 | value: 90.0,
199 | span: Span {
200 | start: 0,
201 | end: text.len(),
202 | line: 0,
203 | },
204 | });
205 | assert_eq!(got, expected);
206 | }
207 | }
208 |
--------------------------------------------------------------------------------
/gcode/tests/data/Insulpro.Piping.-.115mm.OD.-.40mm.WT.txt:
--------------------------------------------------------------------------------
1 | %
2 | (Layout "Model")
3 | G00 G90 G17 G21
4 | M3 S3000
5 |
6 | (Contour 0)
7 | T1 M6
8 | X0. Y-89.314
9 | G01 Z-2. F150
10 | Y-0.001 F450
11 | X5.952 Y-0.757
12 | G03 X199.4 Y-25.314 I96.724 J-12.278
13 | G01 X179.559 Y-22.795
14 | Y-3.275
15 | X159.718 Y-5.793
16 | G02 X45.634 Y-20.277 I-57.042 J-7.242
17 | G01 X25.793 Y-22.795
18 | Y-3.275
19 | X5.952 Y-0.756
20 | X0. Y0.
21 | G00 Z5.
22 | M5
23 | M30
24 | %
--------------------------------------------------------------------------------
/gcode/tests/data/program_1.gcode:
--------------------------------------------------------------------------------
1 | ; Simple G Code Example Mill
2 | ; From: http://www.helmancnc.com/simple-g-code-example-mill-g-code-programming-for-beginners/
3 | O1000
4 | T1 M6
5 | (Linear / Feed - Absolute)
6 | G0 G90 G40 G21 G17 G94 G80
7 | G54 X-75 Y-75 S500 M3 (Position 6)
8 | G43 Z100 H1
9 | G01 Z5
10 | G01 Z-20 F100
11 | G01 X-40 (Position 1)
12 | G01 Y40 M8 (Position 2)
13 | G01 X40 (Position 3)
14 | G01 Y-40 (Position 4)
15 | G01 X-75 (Position 5)
16 | G01 Y-75 (Position 6)
17 | G0 Z100
18 | M30
19 |
--------------------------------------------------------------------------------
/gcode/tests/data/program_2.gcode:
--------------------------------------------------------------------------------
1 | ; CNC Milling programming example code with drawing, which shows how G41
2 | ; Cutter Radius Compensation Left is used in a cnc mill program
3 | ;
4 | ; From: http://www.helmancnc.com/cnc-mill-program-g41-cutter-radius-compensation-left/
5 | N10 T2 M3 S447 F80
6 | N20 G0 X112 Y-2
7 | ;N30 Z-5
8 | N40 G41
9 | N50 G1 X95 Y8 M8
10 | ;N60 X32
11 | ;N70 X5 Y15
12 | ;N80 Y52
13 | N90 G2 X15 Y62 I10 J0
14 | N100 G1 X83
15 | N110 G3 X95 Y50 I12 J0
16 | N120 G1 Y-12
17 | N130 G40
18 | N140 G0 Z100 M9
19 | ;N150 X150 Y150
20 | N160 M30
21 |
--------------------------------------------------------------------------------
/gcode/tests/data/program_3.gcode:
--------------------------------------------------------------------------------
1 | (Tweakie.CNC)
2 | G00G21G17G90G40G49G80
3 | G71G91.1
4 | T1M06
5 | S12000M03
6 | G94 M63P1 F1016.0
7 | X0.000Y0.000
8 | G00X0.033Y15.914M63P1
9 | G1X0.033Y15.914M62P1
10 | G1X13.855Y13.140M62P1
11 | G1X8.012Y1.013
12 | G1X18.895Y8.132
13 | G1X28.215Y0.000
14 | G00X28.215Y0.000M63P1
15 | G00X28.244Y0.000M63P1
16 | G1X28.244Y0.000M62P1
17 | G3X27.385Y6.606I-1605.937J-205.318
18 | G2X26.567Y13.218I408.701J53.962
19 | G1X42.895Y23.862M62P1
20 | G2X42.640Y26.315I527.406J55.934
21 | G3X42.374Y28.766I-175.888J-17.827
22 | G2X42.407Y29.013I0.506J0.058
23 | G2X42.519Y29.213I0.806J-0.320
24 | G3X42.412Y29.252I-0.205J-0.396
25 | G2X42.317Y29.318I0.033J0.148
26 | G2X42.301Y29.378I0.086J0.056
27 | G3X42.298Y29.628I-2.420J0.102
28 | G2X38.881Y28.566I-171.156J544.552
29 | G2X21.945Y23.371I-4108.938J13365.432
30 | G1X13.257Y33.082M62P1
31 | G3X13.319Y26.726I1947.056J15.903
32 | G2X13.384Y20.371I-2736.022J-31.279
33 | G1X0.672Y16.140M62P1
34 | G1X0.165Y15.991
35 | G2X0.033Y15.973I-0.124J0.433
36 | G00X0.033Y15.973M63P1
37 | G00X17.110Y11.385M63P1
38 | G1X17.110Y11.385M62P1
39 | G1X15.476Y16.162M62P1
40 | G1X19.386Y12.709
41 | G1X18.317Y17.815
42 | G1X21.662Y14.034
43 | G00X21.662Y14.034M63P1
44 | G00X21.597Y16.717M63P1
45 | G1X21.597Y16.717M62P1
46 | G2X21.729Y17.275I1.794J-0.130
47 | G2X22.387Y18.023I1.420J-0.588
48 | G2X24.667Y16.134I0.784J-1.374
49 | G2X21.709Y16.053I-1.494J0.515
50 | G2X21.614Y16.381I1.498J0.610
51 | G2X21.592Y16.715I1.527J0.270
52 | G1X21.656Y18.260M62P1
53 | G1X21.721Y19.804
54 | G00X21.721Y19.804M63P1
55 | G00X24.434Y19.901M63P1
56 | G1X24.434Y19.901M62P1
57 | G2X24.288Y20.373I1.738J0.796
58 | G2X24.403Y21.161I1.268J0.218
59 | G2X25.914Y21.870I1.200J-0.592
60 | G2X26.938Y20.552I-0.312J-1.299
61 | G1X26.934Y20.479M62P1
62 | G1X26.913Y20.313
63 | G2X26.692Y19.043I-23.731J3.479
64 | G3X26.234Y16.694I168.388J-34.014
65 | G1X28.588Y18.064M62P1
66 | G00X28.588Y18.064M63P1
67 | G00X45.529Y30.532M63P1
68 | G1X45.529Y30.532M62P1
69 | G2X52.306Y32.524I1246.421J-4228.934
70 | G2X57.375Y34.003I692.553J-2363.694
71 | G2X56.947Y34.874I13.755J7.298
72 | G2X56.880Y35.094I0.801J0.363
73 | G3X54.513Y35.505I-79.750J-452.575
74 | G2X53.927Y35.637I0.980J5.738
75 | G2X53.776Y35.735I0.081J0.290
76 | G2X53.584Y35.989I2.608J2.160
77 | G3X53.771Y36.194I-2.433J2.401
78 | G2X53.974Y36.384I0.875J-0.734
79 | G1X64.244Y41.566M62P1
80 | G2X66.059Y42.433I31.629J-63.907
81 | G2X66.911Y42.588I0.727J-1.579
82 | G2X67.379Y42.509I-0.184J-2.534
83 | G2X68.012Y42.295I-1.289J-4.857
84 | G2X68.081Y42.256I-0.123J-0.297
85 | G3X68.158Y42.232I0.072J0.096
86 | G1X97.113Y57.360M62P1
87 | G2X97.301Y57.343I0.078J-0.192
88 | G3X97.482Y57.289I0.187J0.297
89 | G3X97.655Y57.328I0.007J0.372
90 | G2X98.328Y57.556I1.159J-2.314
91 | G2X98.965Y57.542I0.287J-1.502
92 | G2X100.539Y56.809I-0.978J-4.155
93 | G2X101.682Y54.510I-1.699J-2.278
94 | G2X100.506Y52.362I-2.585J0.019
95 | G3X100.158Y51.962I0.526J-0.809
96 | G2X99.790Y51.580I-0.745J0.349
97 | G1X99.044Y51.199M62P1
98 | G3X98.997Y51.036I0.170J-0.137
99 | G2X98.834Y50.767I-0.252J-0.031
100 | G1X98.825Y50.764M62P1
101 | G1X98.815Y50.761
102 | G1X91.578Y47.249
103 | G1X91.757Y46.945
104 | G3X91.941Y46.645I5.339J3.076
105 | G1X99.580Y35.901M62P1
106 | G1X100.113Y39.541
107 | G1X100.369Y41.266
108 | G1X97.290Y39.873
109 | G1X96.261Y41.438
110 | G1X95.234Y43.003
111 | G2X94.721Y43.810I29.361J19.221
112 | G2X94.622Y43.995I1.430J0.882
113 | G3X105.371Y50.313I-5698.440J9707.666
114 | G3X108.316Y52.054I-429.841J730.409
115 | G3X109.355Y50.736I19.078J13.966
116 | G3X110.902Y49.568I3.367J2.853
117 | G1X109.130Y45.300M62P1
118 | G3X108.571Y45.288I0.002J-13.233
119 | G3X108.310Y45.218I0.026J-0.618
120 | G1X103.815Y42.911M62P1
121 | G1X103.064Y37.475
122 | G1X107.447Y39.910
123 | G1X109.876Y45.801
124 | G2X111.100Y48.684I199.626J-83.077
125 | G2X111.700Y50.012I42.912J-18.592
126 | G2X110.669Y53.002I1.683J2.253
127 | G2X111.648Y55.119I6.751J-1.835
128 | G2X113.893Y57.578I9.877J-6.763
129 | G2X118.293Y60.619I19.696J-23.799
130 | G2X123.515Y63.110I19.679J-34.530
131 | G2X135.336Y66.322I20.864J-53.427
132 | G2X141.990Y66.854I6.702J-41.982
133 | G2X146.129Y66.335I0.019J-16.611
134 | G2X148.819Y65.131I-2.158J-8.426
135 | G2X149.586Y64.390I-1.689J-2.516
136 | G3X149.631Y64.337I0.343J0.248
137 | G2X149.664Y64.275I-0.082J-0.083
138 | G2X149.657Y64.167I-0.216J-0.040
139 | G3X149.528Y63.757I18.047J-5.881
140 | G2X147.830Y60.647I-8.233J2.478
141 | G2X146.277Y59.121I-9.569J8.180
142 | G2X142.979Y56.791I-17.061J20.661
143 | G2X134.226Y52.571I-27.721J46.307
144 | G2X128.342Y50.681I-22.521J59.994
145 | G2X121.007Y49.017I-24.640J91.649
146 | G1X118.735Y48.598M62P1
147 | G1X117.386Y46.059
148 | G2X116.925Y45.260I-14.515J7.832
149 | G2X116.752Y45.026I-1.402J0.861
150 | G3X116.173Y43.825I1.808J-1.611
151 | G3X116.203Y41.832I5.328J-0.915
152 | G3X116.761Y40.032I9.280J1.888
153 | G3X117.863Y37.908I14.062J5.947
154 | G3X118.862Y36.588I7.724J4.810
155 | G3X120.195Y35.590I3.284J2.993
156 | G2X120.214Y35.518I-0.022J-0.045
157 | G2X120.085Y35.423I-0.191J0.126
158 | G1X104.337Y29.005M62P1
159 | G2X101.797Y28.032I-32.777J81.742
160 | G2X100.412Y27.712I-1.982J5.422
161 | G2X99.216Y27.701I-0.653J6.280
162 | G2X98.475Y27.879I0.220J2.552
163 | G3X98.322Y27.906I-0.139J-0.343
164 | G3X97.361Y27.833I0.580J-14.031
165 | G3X95.234Y27.527I3.603J-32.651
166 | G3X93.363Y27.124I4.542J-25.640
167 | G2X92.430Y26.957I-1.646J6.479
168 | G2X91.665Y26.942I-0.464J4.324
169 | G2X91.460Y26.981I0.062J0.891
170 | G2X91.401Y27.025I0.033J0.107
171 | G1X91.272Y27.350M62P1
172 | G1X91.446Y28.021
173 | G3X91.404Y28.258I-0.664J0.006
174 | G2X91.299Y29.216I1.844J0.687
175 | G2X91.360Y29.473I1.361J-0.189
176 | G2X91.475Y29.729I1.455J-0.500
177 | G2X91.050Y30.397I8.846J6.099
178 | G2X90.951Y30.802I0.629J0.368
179 | G1X90.950Y30.826M62P1
180 | G2X90.955Y30.848I0.032J0.005
181 | G1X93.752Y32.314M62P1
182 | G3X95.272Y33.140I-66.215J123.808
183 | G1X96.788Y33.971M62P1
184 | G2X95.029Y36.287I50.562J40.220
185 | G1X88.201Y45.708M62P1
186 | G3X66.691Y35.913I8550.998J-18808.443
187 | G2X61.307Y33.480I-357.189J783.279
188 | G1X60.292Y33.033M62P1
189 | G2X60.254Y33.036I-0.010J0.117
190 | G2X60.133Y33.072I0.305J1.260
191 | G1X59.415Y33.403M62P1
192 | G3X59.301Y33.386I-0.017J-0.278
193 | G2X58.847Y33.297I-0.502J1.347
194 | G2X58.321Y33.349I-0.067J1.989
195 | G2X57.786Y33.548I0.536J2.265
196 | G1X48.757Y27.500M62P1
197 | G1X45.529Y30.532
198 | G00X45.529Y30.532M63P1
199 | G00X54.098Y20.976M63P1
200 | G1X54.098Y20.976M62P1
201 | G2X54.320Y22.090I1.310J0.318
202 | G2X56.163Y22.185I0.960J-0.702
203 | G2X56.493Y21.287I-0.952J-0.859
204 | G2X56.111Y20.461I-1.160J0.035
205 | G2X56.017Y20.388I-0.507J0.561
206 | G1X55.920Y20.321M62P1
207 | G2X56.379Y20.476I0.839J-1.733
208 | G2X57.154Y20.390I0.254J-1.261
209 | G2X57.938Y19.016I-0.540J-1.218
210 | G2X56.262Y17.848I-1.357J0.160
211 | G2X55.963Y17.973I0.235J0.979
212 | G2X55.598Y18.263I1.105J1.766
213 | G00X55.598Y18.263M63P1
214 | G00X51.467Y17.269M63P1
215 | G1X51.467Y17.269M62P1
216 | G2X51.585Y17.835I1.815J-0.082
217 | G2X52.230Y18.608I1.450J-0.555
218 | G2X54.583Y16.763I0.829J-1.366
219 | G2X51.598Y16.601I-1.523J0.479
220 | G2X51.493Y16.931I1.507J0.661
221 | G2X51.462Y17.266I1.524J0.313
222 | G1X51.485Y18.827M62P1
223 | G1X51.509Y20.389
224 | G00X51.509Y20.389M63P1
225 | G00X47.082Y11.763M63P1
226 | G1X47.082Y11.763M62P1
227 | G1X45.302Y16.541M62P1
228 | G1X49.344Y13.162
229 | G1X48.126Y18.288
230 | G1X51.606Y14.561
231 | G00X51.606Y14.561M63P1
232 | G00X108.816Y52.254M63P1
233 | G1X108.816Y52.254M62P1
234 | G3X109.429Y51.353I3.696J1.858
235 | G3X110.486Y50.388I5.309J4.752
236 | G2X110.154Y51.128I16.941J8.047
237 | G2X109.973Y51.975I2.108J0.892
238 | G2X110.773Y54.890I5.335J0.104
239 | G2X112.853Y57.506I12.181J-7.550
240 | G2X116.779Y60.578I14.897J-14.993
241 | G2X123.044Y63.694I21.600J-35.571
242 | G2X137.179Y67.296I21.074J-53.161
243 | G2X143.440Y67.420I3.733J-30.541
244 | G2X147.075Y66.695I-1.332J-16.160
245 | G2X149.274Y65.542I-2.230J-6.929
246 | G2X149.556Y65.313I-3.776J-4.924
247 | G1X149.833Y65.079M62P1
248 | G3X149.918Y66.248I-25.520J2.441
249 | G3X149.670Y67.421I-2.350J0.117
250 | G3X148.684Y68.458I-2.197J-1.103
251 | G3X147.078Y69.164I-4.310J-7.617
252 | G3X145.448Y69.576I-3.897J-11.995
253 | G3X143.054Y69.900I-4.686J-25.641
254 | G3X142.009Y69.986I-8.860J-100.393
255 | G3X141.764Y70.000I-0.428J-5.497
256 | G3X140.151Y69.989I-0.668J-20.137
257 | G3X135.996Y69.614I2.276J-48.437
258 | G3X128.793Y68.151I7.236J-54.082
259 | G3X119.281Y64.609I15.171J-55.285
260 | G3X113.568Y61.352I19.420J-40.709
261 | G3X110.418Y58.662I9.976J-14.866
262 | G3X108.499Y55.118I5.939J-5.509
263 | G3X108.816Y52.254I4.017J-1.005
264 | G00X108.816Y52.254M63P1
265 | G00X119.882Y53.603M63P1
266 | G1X119.882Y53.603M62P1
267 | G3X120.315Y53.118I0.967J0.427
268 | G3X121.513Y52.649I2.087J3.568
269 | G3X123.377Y52.339I3.935J17.877
270 | G3X128.276Y52.052I5.048J44.327
271 | G3X130.398Y52.153I0.070J20.929
272 | G3X131.853Y52.586I-0.393J3.983
273 | G3X132.461Y52.954I-2.509J4.823
274 | G3X134.803Y54.677I-35.226J50.343
275 | G3X139.791Y58.694I-97.462J126.131
276 | G3X140.505Y59.461I-2.741J3.267
277 | G3X140.801Y60.014I-1.895J1.372
278 | G3X140.630Y60.798I-0.693J0.259
279 | G3X139.949Y61.191I-1.086J-1.094
280 | G3X137.573Y61.523I-2.484J-9.107
281 | G3X136.905Y61.516I-0.167J-14.581
282 | G3X136.159Y61.475I0.627J-18.239
283 | G3X130.781Y60.515I2.035J-26.962
284 | G3X125.306Y58.381I8.580J-30.097
285 | G3X122.155Y56.516I12.001J-23.876
286 | G3X120.481Y55.059I5.571J-8.094
287 | G3X119.914Y54.276I3.098J-2.838
288 | G3X119.882Y53.603I0.657J-0.368
289 | G00X119.882Y53.603M63P1
290 | G00X116.901Y44.536M63P1
291 | G1X116.901Y44.536M62P1
292 | G3X116.643Y43.254I3.115J-1.294
293 | G3X116.856Y41.676I5.814J-0.020
294 | G3X118.487Y38.100I12.132J3.374
295 | G3X120.255Y36.129I7.574J5.014
296 | G3X120.810Y35.888I0.690J0.830
297 | G3X121.190Y35.925I0.108J0.860
298 | G3X121.954Y36.426I-0.554J1.678
299 | G3X122.148Y36.698I-1.135J1.013
300 | G3X122.273Y36.998I-1.257J0.701
301 | G2X122.381Y37.145I0.255J-0.074
302 | G3X122.963Y37.576I-3.842J5.799
303 | G3X123.696Y38.208I-13.350J16.201
304 | G3X123.895Y38.590I-0.415J0.459
305 | G3X123.901Y38.791I-1.053J0.132
306 | G3X123.853Y39.231I-5.131J-0.336
307 | G1X123.755Y39.822M62P1
308 | G2X123.808Y39.864I0.075J-0.039
309 | G3X124.057Y39.965I-0.291J1.073
310 | G3X124.546Y40.399I-0.728J1.313
311 | G3X124.721Y40.886I-0.761J0.549
312 | G1X124.691Y41.709M62P1
313 | G3X124.559Y42.241I-2.488J-0.337
314 | G3X123.868Y43.656I-7.373J-2.721
315 | G3X122.802Y45.037I-7.163J-4.433
316 | G3X121.772Y45.274I-0.695J-0.663
317 | G1X121.215Y45.003M62P1
318 | G2X121.108Y45.051I0.006J0.156
319 | G3X120.697Y45.277I-0.561J-0.534
320 | G3X120.594Y45.277I-0.052J-0.261
321 | G3X120.169Y45.173I0.926J-4.694
322 | G1X118.758Y44.702M62P1
323 | G2X118.667Y44.744I0.024J0.172
324 | G3X118.336Y44.966I-0.982J-1.111
325 | G3X117.884Y45.074I-0.471J-0.963
326 | G3X117.361Y44.983I-0.024J-1.402
327 | G3X116.901Y44.536I0.288J-0.757
328 | G00X116.901Y44.536M63P1
329 | G00X93.246Y38.028M63P1
330 | G1X93.246Y38.028M62P1
331 | G1X90.509Y41.679M62P1
332 | G1X83.618Y38.141
333 | G3X83.399Y38.323I-2.466J-2.727
334 | G3X83.259Y38.374I-0.147J-0.191
335 | G3X81.697Y38.093I-0.115J-3.853
336 | G3X80.447Y37.537I7.572J-18.691
337 | G3X77.924Y36.269I47.356J-97.340
338 | G3X76.545Y35.511I15.499J-29.853
339 | G1X75.942Y35.150M62P1
340 | G3X75.907Y35.071I0.145J-0.112
341 | G2X75.825Y34.740I-2.787J0.515
342 | G2X75.559Y34.112I-3.341J1.045
343 | G2X74.858Y33.473I-1.274J0.693
344 | G3X73.796Y32.977I7.530J-17.515
345 | G2X71.845Y32.012I-61.539J121.888
346 | G1X71.848Y31.865M62P1
347 | G1X73.124Y31.463
348 | G2X73.271Y31.332I-0.286J-0.471
349 | G3X73.402Y31.185I1.292J1.017
350 | G1X52.220Y23.471M62P1
351 | G2X50.074Y25.448I366.897J400.251
352 | G2X42.668Y32.321I4424.031J4774.434
353 | G3X43.305Y25.989I4481.152J447.293
354 | G1X43.943Y19.657M62P1
355 | G3X35.680Y15.788I2153.789J-4610.324
356 | G3X32.245Y14.167I374.766J-798.474
357 | G2X34.889Y13.988I-27.376J-425.352
358 | G2X44.334Y13.313I-383.394J-5429.970
359 | G2X41.502Y5.767I-2582.688J965.061
360 | G3X40.811Y3.875I91.554J-34.516
361 | G2X40.328Y2.569I-46.055J16.288
362 | G3X39.876Y1.253I17.311J-6.683
363 | G2X40.168Y1.515I3.609J-3.735
364 | G2X41.541Y2.635I67.991J-81.904
365 | G2X45.804Y6.032I493.654J-615.236
366 | G3X50.074Y9.418I-1468.170J1855.893
367 | G1X60.084Y2.168M62P1
368 | G1X57.220Y15.214
369 | G1X60.307Y18.057
370 | G3X67.062Y24.336I-1583.770J1710.541
371 | G2X73.809Y30.625I4771.573J-5112.781
372 | G3X74.289Y30.177I1.309J0.922
373 | G3X74.856Y29.990I0.686J1.133
374 | G2X75.362Y29.837I-0.117J-1.293
375 | G3X76.338Y29.748I0.590J1.079
376 | G3X76.794Y29.946I-0.867J2.626
377 | G2X77.236Y30.175I5.774J-10.593
378 | G2X85.239Y34.105I1438.599J-2919.446
379 | G3X93.246Y38.028I-4291.786J8770.125
380 | G00X93.246Y38.028M63P1
381 | G00X120.394Y53.771M63P1
382 | G1X120.394Y53.771M62P1
383 | G2X120.365Y53.965I0.134J0.119
384 | G2X121.013Y54.786I2.041J-0.945
385 | G2X122.540Y55.908I17.166J-21.757
386 | G2X127.169Y58.440I15.061J-22.033
387 | G2X132.671Y60.210I12.518J-29.477
388 | G2X135.862Y60.707I5.346J-23.809
389 | G2X138.954Y60.767I1.976J-22.289
390 | G2X140.089Y60.548I-0.205J-4.113
391 | G2X140.280Y60.310I-0.093J-0.271
392 | G2X140.207Y59.997I-0.472J-0.055
393 | G2X139.616Y59.282I-3.321J2.145
394 | G2X138.902Y58.649I-8.984J9.412
395 | G2X134.721Y55.306I-118.849J144.352
396 | G2X133.275Y54.207I-53.089J68.381
397 | G2X132.537Y53.682I-12.854J17.294
398 | G2X131.801Y53.202I-12.653J18.608
399 | G2X130.918Y52.826I-1.649J2.641
400 | G2X129.744Y52.649I-1.473J5.799
401 | G2X127.062Y52.633I-1.517J30.439
402 | G1X124.008Y52.833M62P1
403 | G2X121.545Y53.207I2.159J22.538
404 | G2X120.819Y53.451I0.662J3.175
405 | G2X120.394Y53.771I0.566J1.194
406 | G00X120.394Y53.771M63P1
407 | M63P1
408 | G00X0.000Y0.000
409 | M09
410 | M30
411 | %
412 |
--------------------------------------------------------------------------------
/gcode/tests/smoke_test.rs:
--------------------------------------------------------------------------------
1 | use gcode::{GCode, Mnemonic, Span, Word};
2 |
3 | macro_rules! smoke_test {
4 | ($name:ident, $filename:expr) => {
5 | #[test]
6 | #[cfg(feature = "std")]
7 | fn $name() {
8 | let src = include_str!(concat!(
9 | env!("CARGO_MANIFEST_DIR"),
10 | "/tests/data/",
11 | $filename
12 | ));
13 | let src = sanitise_input(src);
14 |
15 | let _got: Vec<_> =
16 | gcode::full_parse_with_callbacks(&src, PanicOnError).collect();
17 | }
18 | };
19 | }
20 |
21 | smoke_test!(program_1, "program_1.gcode");
22 | smoke_test!(program_2, "program_2.gcode");
23 | smoke_test!(program_3, "program_3.gcode");
24 | smoke_test!(pi_octcat, "PI_octcat.gcode");
25 | smoke_test!(pi_rustlogo, "PI_rustlogo.gcode");
26 | smoke_test!(insulpro_piping, "Insulpro.Piping.-.115mm.OD.-.40mm.WT.txt");
27 |
28 | #[test]
29 | #[ignore]
30 | fn expected_program_2_output() {
31 | // N10 T2 M3 S447 F80
32 | // N20 G0 X112 Y-2
33 | // ;N30 Z-5
34 | // N40 G41
35 | // N50 G1 X95 Y8 M8
36 | // ;N60 X32
37 | // ;N70 X5 Y15
38 | // ;N80 Y52
39 | // N90 G2 X15 Y62 I10 J0
40 | // N100 G1 X83
41 | // N110 G3 X95 Y50 I12 J0
42 | // N120 G1 Y-12
43 | // N130 G40
44 | // N140 G0 Z100 M9
45 | // ;N150 X150 Y150
46 | // N160 M30
47 |
48 | let src = include_str!("data/program_2.gcode");
49 |
50 | let got: Vec<_> =
51 | gcode::full_parse_with_callbacks(src, PanicOnError).collect();
52 |
53 | // total lines
54 | assert_eq!(got.len(), 20);
55 | // check lines without any comments
56 | assert_eq!(got.iter().filter(|l| l.comments().is_empty()).count(), 11);
57 |
58 | let gcodes: Vec<_> = got.iter().flat_map(|l| l.gcodes()).cloned().collect();
59 | let expected = vec![
60 | GCode::new(Mnemonic::ToolChange, 2.0, Span::PLACEHOLDER),
61 | GCode::new(Mnemonic::Miscellaneous, 3.0, Span::PLACEHOLDER)
62 | .with_argument(Word::new('S', 447.0, Span::PLACEHOLDER))
63 | .with_argument(Word::new('F', 80.0, Span::PLACEHOLDER)),
64 | ];
65 | pretty_assertions::assert_eq!(gcodes, expected);
66 | }
67 |
68 | struct PanicOnError;
69 |
70 | impl gcode::Callbacks for PanicOnError {
71 | fn unknown_content(&mut self, text: &str, span: Span) {
72 | panic!("Unknown content at {:?}: {}", span, text);
73 | }
74 |
75 | fn gcode_buffer_overflowed(
76 | &mut self,
77 | _mnemonic: Mnemonic,
78 | _major_number: u32,
79 | _minor_number: u32,
80 | _arguments: &[Word],
81 | _span: Span,
82 | ) {
83 | panic!("Buffer overflow");
84 | }
85 |
86 | fn unexpected_line_number(&mut self, line_number: f32, span: Span) {
87 | panic!("Unexpected line number at {:?}: {}", span, line_number);
88 | }
89 |
90 | fn argument_without_a_command(
91 | &mut self,
92 | letter: char,
93 | value: f32,
94 | span: Span,
95 | ) {
96 | panic!(
97 | "Argument without a command at {:?}: {}{}",
98 | span, letter, value
99 | );
100 | }
101 |
102 | fn number_without_a_letter(&mut self, value: &str, span: Span) {
103 | panic!("Number without a letter at {:?}: {}", span, value);
104 | }
105 |
106 | fn letter_without_a_number(&mut self, value: &str, span: Span) {
107 | panic!("Letter without a number at {:?}: {}", span, value);
108 | }
109 | }
110 |
111 | #[allow(dead_code)]
112 | fn sanitise_input(src: &str) -> String {
113 | let mut src = src.to_string();
114 | let callbacks = [handle_percent, ignore_message_lines];
115 |
116 | for cb in &callbacks {
117 | src = cb(&src);
118 | }
119 |
120 | src
121 | }
122 |
123 | #[allow(dead_code)]
124 | fn handle_percent(src: &str) -> String {
125 | let pieces: Vec<&str> = src.split('%').collect();
126 |
127 | match pieces.len() {
128 | 0 => unreachable!(),
129 | 1 => src.to_string(),
130 | 2 => pieces[0].to_string(),
131 | 3 => pieces[1].to_string(),
132 | _ => panic!(),
133 | }
134 | }
135 |
136 | #[allow(dead_code)]
137 | fn ignore_message_lines(src: &str) -> String {
138 | // "M117 Printing..." uses string arguments, not the normal char-float word
139 | let blacklist = ["M117"];
140 |
141 | src.lines()
142 | .filter(|line| blacklist.iter().all(|word| !line.contains(word)))
143 | .collect::>()
144 | .join("\n")
145 | }
146 |
--------------------------------------------------------------------------------
/rustfmt.toml:
--------------------------------------------------------------------------------
1 | max_width = 80
2 | tab_spaces = 4
3 | fn_single_line = true
4 | match_block_trailing_comma = true
5 | normalize_comments = true
6 | wrap_comments = true
7 | merge_imports = true
8 | reorder_impl_items = true
9 | use_field_init_shorthand = true
10 | use_try_shorthand = true
11 | normalize_doc_attributes = true
12 | report_todo = "Always"
13 | report_fixme = "Always"
14 | edition = "2018"
15 |
--------------------------------------------------------------------------------
/wasm/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | dist/
3 | pkg/
4 | yarn-error.log
--------------------------------------------------------------------------------
/wasm/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "gcode-wasm"
3 | version = "0.6.1"
4 | authors = ["Michael-F-Bryan "]
5 | edition = "2018"
6 | publish = false
7 | description = "WebAssembly bindings for use in the @michael-f-bryan/gcode package. Not intended for public use."
8 | repository = "https://github.com/Michael-F-Bryan/gcode-rs"
9 | homepage = "https://github.com/Michael-F-Bryan/gcode-rs"
10 | license = "MIT OR Apache-2.0"
11 | keywords = ["gcode", "wasm", "rust"]
12 |
13 | [dependencies]
14 | wasm-bindgen = "0.2.59"
15 | gcode = "0.6.1"
16 |
17 | # we're using "rust/" instead of "src/" to prevent any mix-ups between the Rust
18 | # world and the JS/TS world
19 | [lib]
20 | path = "rust/lib.rs"
21 | crate-type = ["cdylib", "rlib"]
22 |
--------------------------------------------------------------------------------
/wasm/LICENSE_APACHE.md:
--------------------------------------------------------------------------------
1 | ../LICENSE_APACHE.md
--------------------------------------------------------------------------------
/wasm/LICENSE_MIT.md:
--------------------------------------------------------------------------------
1 | ../LICENSE_MIT.md
--------------------------------------------------------------------------------
/wasm/README.md:
--------------------------------------------------------------------------------
1 | ../README.md
--------------------------------------------------------------------------------
/wasm/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | transform: { '^.+\\.ts?$': 'ts-jest' },
3 | testEnvironment: 'node',
4 | testRegex: '.*\\.(test|spec)?\\.(ts|tsx)$',
5 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node']
6 | };
--------------------------------------------------------------------------------
/wasm/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@michael-f-bryan/gcode",
3 | "version": "0.6.1",
4 | "description": "An interface to the Rust gcode parser library",
5 | "main": "main/index.js",
6 | "types": "dist/index.d.ts",
7 | "repository": "https://github.com/Michael-F-Bryan/gcode-rs",
8 | "author": "Michael-F-Bryan ",
9 | "license": "MIT OR Apache-2.0",
10 | "scripts": {
11 | "test": "jest",
12 | "coverage": "jest --coverage",
13 | "prepublish": "tsc"
14 | },
15 | "dependencies": {
16 | "@michael-f-bryan/gcode-wasm": "0.6.1"
17 | },
18 | "devDependencies": {
19 | "@babel/parser": "^7.8.7",
20 | "@types/jest": "^25.1.4",
21 | "@wasm-tool/wasm-pack-plugin": "^1.1.0",
22 | "jest": "^25.1.0",
23 | "ts-jest": "^25.2.1",
24 | "ts-loader": "^6.2.1",
25 | "typescript": "^3.8.3",
26 | "webpack": "^4.42.0",
27 | "webpack-cli": "^3.3.11"
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/wasm/rust/callbacks.rs:
--------------------------------------------------------------------------------
1 | use crate::{Span, Word};
2 | use gcode::{Callbacks, Mnemonic};
3 | use wasm_bindgen::prelude::wasm_bindgen;
4 |
5 | #[wasm_bindgen]
6 | extern "C" {
7 | pub type JavaScriptCallbacks;
8 |
9 | #[wasm_bindgen(method)]
10 | fn unknown_content(this: &JavaScriptCallbacks, text: &str, span: Span);
11 |
12 | #[wasm_bindgen(method)]
13 | fn gcode_buffer_overflowed(
14 | this: &JavaScriptCallbacks,
15 | mnemonic: char,
16 | number: f32,
17 | span: Span,
18 | );
19 |
20 | #[wasm_bindgen(method)]
21 | fn gcode_argument_buffer_overflowed(
22 | this: &JavaScriptCallbacks,
23 | mnemonic: char,
24 | number: f32,
25 | argument: Word,
26 | );
27 |
28 | #[wasm_bindgen(method)]
29 | fn comment_buffer_overflow(
30 | this: &JavaScriptCallbacks,
31 | comment: &str,
32 | span: Span,
33 | );
34 |
35 | #[wasm_bindgen(method)]
36 | fn unexpected_line_number(
37 | this: &JavaScriptCallbacks,
38 | line_number: f32,
39 | span: Span,
40 | );
41 |
42 | #[wasm_bindgen(method)]
43 | fn argument_without_a_command(
44 | this: &JavaScriptCallbacks,
45 | letter: char,
46 | value: f32,
47 | span: Span,
48 | );
49 |
50 | #[wasm_bindgen(method)]
51 | fn number_without_a_letter(
52 | this: &JavaScriptCallbacks,
53 | value: &str,
54 | span: Span,
55 | );
56 |
57 | #[wasm_bindgen(method)]
58 | fn letter_without_a_number(
59 | this: &JavaScriptCallbacks,
60 | value: &str,
61 | span: Span,
62 | );
63 | }
64 |
65 | impl Callbacks for JavaScriptCallbacks {
66 | fn unknown_content(&mut self, text: &str, span: gcode::Span) {
67 | JavaScriptCallbacks::unknown_content(self, text, span.into());
68 | }
69 |
70 | fn gcode_buffer_overflowed(
71 | &mut self,
72 | mnemonic: Mnemonic,
73 | major_number: u32,
74 | minor_number: u32,
75 | _arguments: &[gcode::Word],
76 | span: gcode::Span,
77 | ) {
78 | JavaScriptCallbacks::gcode_buffer_overflowed(
79 | self,
80 | crate::mnemonic_letter(mnemonic),
81 | (major_number as f32) + (minor_number as f32)/10.0,
82 | span.into(),
83 | );
84 | }
85 |
86 | fn gcode_argument_buffer_overflowed(
87 | &mut self,
88 | mnemonic: Mnemonic,
89 | major_number: u32,
90 | minor_number: u32,
91 | argument: gcode::Word,
92 | ) {
93 | JavaScriptCallbacks::gcode_argument_buffer_overflowed(
94 | self,
95 | crate::mnemonic_letter(mnemonic),
96 | (major_number as f32) + (minor_number as f32)/10.0,
97 | argument.into(),
98 | );
99 | }
100 |
101 | fn comment_buffer_overflow(&mut self, comment: gcode::Comment) {
102 | JavaScriptCallbacks::comment_buffer_overflow(
103 | self,
104 | comment.value,
105 | comment.span.into(),
106 | );
107 | }
108 |
109 | fn unexpected_line_number(&mut self, line_number: f32, span: gcode::Span) {
110 | JavaScriptCallbacks::unexpected_line_number(
111 | self,
112 | line_number,
113 | span.into(),
114 | );
115 | }
116 |
117 | fn argument_without_a_command(
118 | &mut self,
119 | letter: char,
120 | value: f32,
121 | span: gcode::Span,
122 | ) {
123 | JavaScriptCallbacks::argument_without_a_command(
124 | self,
125 | letter,
126 | value,
127 | span.into(),
128 | );
129 | }
130 |
131 | fn number_without_a_letter(&mut self, value: &str, span: gcode::Span) {
132 | JavaScriptCallbacks::number_without_a_letter(self, value, span.into());
133 | }
134 |
135 | fn letter_without_a_number(&mut self, value: &str, span: gcode::Span) {
136 | JavaScriptCallbacks::letter_without_a_number(self, value, span.into());
137 | }
138 | }
139 |
--------------------------------------------------------------------------------
/wasm/rust/lib.rs:
--------------------------------------------------------------------------------
1 | //! Internals for the `@michael-f-bryan/gcode` package. Not intended for public
2 | //! use.
3 |
4 | mod callbacks;
5 | mod parser;
6 | mod simple_wrappers;
7 |
8 | pub use callbacks::JavaScriptCallbacks;
9 | pub use parser::Parser;
10 | pub use simple_wrappers::{Comment, GCode, Line, Span, Word};
11 |
12 | use gcode::Mnemonic;
13 |
14 | pub(crate) fn mnemonic_letter(m: Mnemonic) -> char {
15 | match m {
16 | Mnemonic::General => 'G',
17 | Mnemonic::Miscellaneous => 'M',
18 | Mnemonic::ProgramNumber => 'O',
19 | Mnemonic::ToolChange => 'T',
20 | }
21 | }
--------------------------------------------------------------------------------
/wasm/rust/parser.rs:
--------------------------------------------------------------------------------
1 | use crate::{JavaScriptCallbacks, Line};
2 | use std::{mem::ManuallyDrop, pin::Pin};
3 | use wasm_bindgen::prelude::wasm_bindgen;
4 |
5 | #[wasm_bindgen]
6 | pub struct Parser {
7 | /// A pinned heap allocation containing the text that `inner` has
8 | /// references to.
9 | ///
10 | /// # Safety
11 | ///
12 | /// This field **must** be destroyed after `inner`. The `&str` it contains
13 | /// should also never change, otherwise we may invalidate references inside
14 | /// `inner`.
15 | _text: Pin>,
16 | /// The actual `gcode::Parser`. We've told the compiler that it has a
17 | /// `'static` lifetime because we'll be using `unsafe` code to manually
18 | /// manage memory.
19 | inner: ManuallyDrop>,
20 | }
21 |
22 | #[wasm_bindgen]
23 | impl Parser {
24 | #[wasm_bindgen(constructor)]
25 | pub fn new(text: String, callbacks: JavaScriptCallbacks) -> Parser {
26 | // make sure our text is allocated on the heap and will never move
27 | let text: Pin> = text.into_boxed_str().into();
28 |
29 | // SAFETY: Because gcode::Parser contains a reference to the text string
30 | // it needs a lifetime, however it's not sound to expose a struct with
31 | // a lifetime to JavaScript (JavaScript doesn't have a borrow checker).
32 | //
33 | // To work around this we turn the string into a `Box>` (to
34 | // statically ensure pointers to our string will never change) then
35 | // take a reference to it and "extend" the reference lifetime to
36 | // 'static.
37 | //
38 | // The order that `text` and `inner` are destroyed in isn't really
39 | // defined, so we use `ManuallyDrop` to ensure the `gcode::Parser` is
40 | // destroyed first. That way we don't get the situation where `text` is
41 | // destroyed and our `inner` parser is left with dangling references.
42 | unsafe {
43 | // get a pointer to the underlying text
44 | let text_ptr: *const str = &*text;
45 | // then convert it to a reference with a 'static lifetime
46 | let static_str: &'static str = &*text_ptr;
47 |
48 | // now make a gcode::Parser which uses the 'static text as input
49 | let inner =
50 | ManuallyDrop::new(gcode::Parser::new(static_str, callbacks));
51 |
52 | Parser { _text: text, inner }
53 | }
54 | }
55 |
56 | /// Try to parse the next [`Line`].
57 | ///
58 | /// # Safety
59 | ///
60 | /// The line must not outlive the [`Parser`] it came from.
61 | pub fn next_line(&mut self) -> Option { self.inner.next().map(From::from) }
62 | }
63 |
64 | impl Drop for Parser {
65 | fn drop(&mut self) {
66 | // SAFETY: This is the only place `inner` gets destroyed, and the field
67 | // can never be touch after `Parser::drop()` is called.
68 | unsafe {
69 | ManuallyDrop::drop(&mut self.inner);
70 | }
71 |
72 | // the text will be destroyed somewhere after here because Rust's drop
73 | // glue destroys fields after the containing type is destroyed.
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/wasm/rust/simple_wrappers.rs:
--------------------------------------------------------------------------------
1 | use wasm_bindgen::prelude::{wasm_bindgen, JsValue};
2 |
3 | #[wasm_bindgen]
4 | #[derive(Debug, Copy, Clone)]
5 | pub struct Span(gcode::Span);
6 |
7 | #[wasm_bindgen]
8 | impl Span {
9 | #[wasm_bindgen(getter)]
10 | pub fn start(&self) -> usize { self.0.start }
11 |
12 | #[wasm_bindgen(getter)]
13 | pub fn end(&self) -> usize { self.0.end }
14 |
15 | #[wasm_bindgen(getter)]
16 | pub fn line(&self) -> usize { self.0.line }
17 | }
18 |
19 | impl From for Span {
20 | fn from(other: gcode::Span) -> Span {
21 | Span(other)
22 | }
23 | }
24 |
25 | #[wasm_bindgen]
26 | #[derive(Debug, Copy, Clone)]
27 | pub struct Word(gcode::Word);
28 |
29 | #[wasm_bindgen]
30 | impl Word {
31 | #[wasm_bindgen(getter)]
32 | pub fn letter(&self) -> char { self.0.letter }
33 |
34 | #[wasm_bindgen(getter)]
35 | pub fn value(&self) -> f32 { self.0.value }
36 |
37 | #[wasm_bindgen(getter)]
38 | pub fn span(&self) -> Span { Span(self.0.span) }
39 | }
40 |
41 | impl From for Word {
42 | fn from(other: gcode::Word) -> Word {
43 | Word(other)
44 | }
45 | }
46 |
47 | #[wasm_bindgen]
48 | #[derive(Debug)]
49 | pub struct Line(gcode::Line<'static>);
50 |
51 | #[wasm_bindgen]
52 | impl Line {
53 | pub fn num_gcodes(&self) -> usize { self.0.gcodes().len() }
54 |
55 | pub fn get_gcode(&self, index: usize) -> Option {
56 | self.0.gcodes().get(index).map(|g| GCode(g.clone()))
57 | }
58 |
59 | pub fn num_comments(&self) -> usize { self.0.comments().len() }
60 |
61 | pub fn get_comment(&self, index: usize) -> Option {
62 | self.0.comments().get(index).map(|c| Comment {
63 | text: c.value.into(),
64 | span: Span(c.span),
65 | })
66 | }
67 |
68 | #[wasm_bindgen(getter)]
69 | pub fn span(&self) -> Span {
70 | self.0.span().into()
71 | }
72 | }
73 |
74 | impl From> for Line {
75 | fn from(other: gcode::Line<'static>) -> Line {
76 | Line(other)
77 | }
78 | }
79 |
80 | #[wasm_bindgen]
81 | #[derive(Debug)]
82 | pub struct GCode(gcode::GCode);
83 |
84 | #[wasm_bindgen]
85 | impl GCode {
86 | #[wasm_bindgen(getter)]
87 | pub fn mnemonic(&self) -> char { crate::mnemonic_letter(self.0.mnemonic()) }
88 |
89 | #[wasm_bindgen(getter)]
90 | pub fn number(&self) -> f32 {
91 | self.0.major_number() as f32 + (self.0.minor_number() as f32) / 10.0
92 | }
93 |
94 | #[wasm_bindgen(getter)]
95 | pub fn span(&self) -> Span {
96 | self.0.span().into()
97 | }
98 |
99 | pub fn num_arguments(&self) -> usize {
100 | self.0.arguments().len()
101 | }
102 |
103 | pub fn get_argument(&self, index: usize) -> Option {
104 | self.0.arguments().get(index).copied().map(|w| Word::from(w))
105 | }
106 | }
107 |
108 | impl From for GCode {
109 | fn from(other: gcode::GCode) -> GCode {
110 | GCode(other)
111 | }
112 | }
113 |
114 | #[wasm_bindgen]
115 | #[derive(Debug)]
116 | pub struct Comment {
117 | text: String,
118 | #[wasm_bindgen(readonly)]
119 | pub span: Span,
120 | }
121 |
122 | #[wasm_bindgen]
123 | impl Comment {
124 | #[wasm_bindgen(getter)]
125 | pub fn text(&self) -> JsValue { JsValue::from_str(&self.text) }
126 | }
127 |
128 | impl<'a> From> for Comment {
129 | fn from(other: gcode::Comment<'a>) -> Self {
130 | Comment {
131 | text: other.value.to_string(),
132 | span: Span(other.span),
133 | }
134 | }
135 | }
--------------------------------------------------------------------------------
/wasm/ts/index.test.ts:
--------------------------------------------------------------------------------
1 | import { parse, GCode } from "./index";
2 |
3 | describe("gcode parsing", () => {
4 | it("can parse G90", () => {
5 | const src = "G90";
6 | const expected: GCode[] = [
7 | {
8 | mnemonic: "G",
9 | number: 90,
10 | arguments: {},
11 | span: {
12 | start: 0,
13 | end: src.length,
14 | line: 0,
15 | }
16 | },
17 | ];
18 |
19 | const got = Array.from(parse(src));
20 |
21 | expect(got).toEqual(expected);
22 | });
23 |
24 | it("can parse more complex items", () => {
25 | const src = "G01 (the x-coordinate) X50 Y (comment between Y and number) -10.0";
26 | const expected: GCode[] = [
27 | {
28 | mnemonic: "G",
29 | number: 1,
30 | arguments: {
31 | X: 50,
32 | Y: -10
33 | },
34 | span: {
35 | start: 0,
36 | end: src.length,
37 | line: 0,
38 | }
39 | },
40 | ];
41 |
42 | const got = Array.from(parse(src));
43 |
44 | expect(got).toEqual(expected);
45 | });
46 | });
--------------------------------------------------------------------------------
/wasm/ts/index.ts:
--------------------------------------------------------------------------------
1 | import * as wasm from "@michael-f-bryan/gcode-wasm";
2 |
3 | export type Line = {
4 | gcodes: GCode[],
5 | comments: Comment[],
6 | span: Span,
7 | };
8 |
9 | export type Comment = {
10 | text: string,
11 | span: Span,
12 | };
13 |
14 | type Arguments = { [key: string]: number };
15 |
16 | export type GCode = {
17 | mnemonic: string,
18 | number: number,
19 | arguments: Arguments,
20 | span: Span,
21 | };
22 |
23 | export type Span = {
24 | start: number,
25 | end: number,
26 | line: number,
27 | }
28 |
29 | export interface Callbacks {
30 | unknown_content?(text: string, span: Span): void;
31 |
32 | gcode_buffer_overflowed?(
33 | mnemonic: string,
34 | number: number,
35 | span: Span,
36 | ): void;
37 |
38 | gcode_argument_buffer_overflowed?(
39 | mnemonic: string,
40 | number: number,
41 | argument: wasm.Word,
42 | ): void;
43 |
44 | comment_buffer_overflow?(
45 | comment: string,
46 | span: Span,
47 | ): void;
48 |
49 | unexpected_line_number?(
50 | line_number: number,
51 | span: Span,
52 | ): void;
53 |
54 | argument_without_a_command?(
55 | letter: string,
56 | value: number,
57 | span: Span,
58 | ): void;
59 |
60 | number_without_a_letter?(
61 | value: string,
62 | span: Span,
63 | ): void;
64 |
65 | letter_without_a_number?(
66 | value: string,
67 | span: Span,
68 | ): void;
69 | }
70 |
71 | export function* parseLines(text: string, callbacks?: Callbacks): Iterable {
72 | const parser = new wasm.Parser(text, callbacks);
73 |
74 | try {
75 | while (true) {
76 | const line = parser.next_line();
77 |
78 | if (line) {
79 | yield translateLine(line);
80 | } else {
81 | break;
82 | }
83 | }
84 |
85 | } finally {
86 | parser.free();
87 | }
88 | }
89 |
90 | export function* parse(text: string, callbacks?: Callbacks): Iterable {
91 | for (const line of parseLines(text, callbacks)) {
92 | for (const gcode of line.gcodes) {
93 | yield gcode;
94 | }
95 | }
96 | }
97 |
98 | function translateLine(line: wasm.Line): Line {
99 | try {
100 | return {
101 | comments: getAll(line, (l, i) => l.get_comment(i)).map(translateComment),
102 | gcodes: getAll(line, (l, i) => l.get_gcode(i)).map(translateGCode),
103 | span: line.span,
104 | };
105 | } finally {
106 | line.free();
107 | }
108 | }
109 |
110 | function translateGCode(gcode: wasm.GCode): GCode {
111 | const translated = {
112 | mnemonic: gcode.mnemonic,
113 | number: gcode.number,
114 | arguments: translateArguments(gcode),
115 | span: translateSpan(gcode.span),
116 | };
117 |
118 | gcode.free();
119 | return translated;
120 | }
121 |
122 | function translateArguments(gcode: wasm.GCode): Arguments {
123 | const map: Arguments = {};
124 |
125 | for (const word of getAll(gcode, (g, i) => g.get_argument(i))) {
126 | try {
127 | map[word.letter] = word.value;
128 | } finally {
129 | word.free();
130 | }
131 | }
132 |
133 | return map;
134 | }
135 |
136 | function translateComment(gcode: wasm.Comment): Comment {
137 | const translated = {
138 | text: gcode.text,
139 | span: translateSpan(gcode.span),
140 | };
141 |
142 | gcode.free();
143 | return translated;
144 | }
145 |
146 | function translateSpan(span: wasm.Span): Span {
147 | const translated = {
148 | start: span.start,
149 | end: span.end,
150 | line: span.line,
151 | };
152 | return translated;
153 | }
154 |
155 | function getAll(line: TContainer, getter: (line: TContainer, index: number) => TItem | undefined): TItem[] {
156 | const items = [];
157 | let i = 0;
158 |
159 | while (true) {
160 | const item = getter(line, i);
161 |
162 | if (item) {
163 | items.push(item);
164 | } else {
165 | break;
166 | }
167 |
168 | i++;
169 | }
170 |
171 | return items;
172 | }
173 |
--------------------------------------------------------------------------------
/wasm/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2015",
4 | "module": "es6",
5 | "strict": true,
6 | "outDir": "dist",
7 | "sourceMap": true,
8 | "esModuleInterop": true,
9 | "declaration": true,
10 | "lib": [
11 | "ES6",
12 | "DOM",
13 | ]
14 | }
15 | }
--------------------------------------------------------------------------------