├── .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 | [![Crates.io version](https://img.shields.io/crates/v/gcode.svg)](https://crates.io/crates/gcode) 4 | [![Docs](https://docs.rs/gcode/badge.svg)](https://docs.rs/gcode/) 5 | [![Build Status](https://travis-ci.org/Michael-F-Bryan/gcode-rs.svg?branch=master)](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 | } --------------------------------------------------------------------------------