├── .gitignore ├── .vscode └── settings.json ├── Cargo.lock ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── phenotype-internal ├── Cargo.toml └── src │ └── lib.rs ├── phenotype-macro ├── Cargo.toml └── src │ ├── generic.rs │ └── lib.rs └── src ├── array.rs ├── lib.rs ├── main.rs └── peapod_vec.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | .DS_Store 3 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": ["bitvec", "peapod"] 3 | } 4 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "bitvec" 7 | version = "1.0.1" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" 10 | dependencies = [ 11 | "funty", 12 | "radium", 13 | "tap", 14 | "wyz", 15 | ] 16 | 17 | [[package]] 18 | name = "funty" 19 | version = "2.0.0" 20 | source = "registry+https://github.com/rust-lang/crates.io-index" 21 | checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" 22 | 23 | [[package]] 24 | name = "peapod" 25 | version = "0.1.9" 26 | dependencies = [ 27 | "bitvec", 28 | "phenotype-internal", 29 | "phenotype-macro", 30 | ] 31 | 32 | [[package]] 33 | name = "phenotype-internal" 34 | version = "0.2.0" 35 | 36 | [[package]] 37 | name = "phenotype-macro" 38 | version = "0.1.4" 39 | dependencies = [ 40 | "phenotype-internal", 41 | "proc-macro-error", 42 | "proc-macro2", 43 | "quote", 44 | "syn", 45 | ] 46 | 47 | [[package]] 48 | name = "proc-macro-error" 49 | version = "1.0.4" 50 | source = "registry+https://github.com/rust-lang/crates.io-index" 51 | checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" 52 | dependencies = [ 53 | "proc-macro-error-attr", 54 | "proc-macro2", 55 | "quote", 56 | "syn", 57 | "version_check", 58 | ] 59 | 60 | [[package]] 61 | name = "proc-macro-error-attr" 62 | version = "1.0.4" 63 | source = "registry+https://github.com/rust-lang/crates.io-index" 64 | checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" 65 | dependencies = [ 66 | "proc-macro2", 67 | "quote", 68 | "version_check", 69 | ] 70 | 71 | [[package]] 72 | name = "proc-macro2" 73 | version = "1.0.40" 74 | source = "registry+https://github.com/rust-lang/crates.io-index" 75 | checksum = "dd96a1e8ed2596c337f8eae5f24924ec83f5ad5ab21ea8e455d3566c69fbcaf7" 76 | dependencies = [ 77 | "unicode-ident", 78 | ] 79 | 80 | [[package]] 81 | name = "quote" 82 | version = "1.0.20" 83 | source = "registry+https://github.com/rust-lang/crates.io-index" 84 | checksum = "3bcdf212e9776fbcb2d23ab029360416bb1706b1aea2d1a5ba002727cbcab804" 85 | dependencies = [ 86 | "proc-macro2", 87 | ] 88 | 89 | [[package]] 90 | name = "radium" 91 | version = "0.7.0" 92 | source = "registry+https://github.com/rust-lang/crates.io-index" 93 | checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" 94 | 95 | [[package]] 96 | name = "syn" 97 | version = "1.0.98" 98 | source = "registry+https://github.com/rust-lang/crates.io-index" 99 | checksum = "c50aef8a904de4c23c788f104b7dddc7d6f79c647c7c8ce4cc8f73eb0ca773dd" 100 | dependencies = [ 101 | "proc-macro2", 102 | "quote", 103 | "unicode-ident", 104 | ] 105 | 106 | [[package]] 107 | name = "tap" 108 | version = "1.0.1" 109 | source = "registry+https://github.com/rust-lang/crates.io-index" 110 | checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" 111 | 112 | [[package]] 113 | name = "unicode-ident" 114 | version = "1.0.1" 115 | source = "registry+https://github.com/rust-lang/crates.io-index" 116 | checksum = "5bd2fe26506023ed7b5e1e315add59d6f584c621d037f9368fea9cfb988f368c" 117 | 118 | [[package]] 119 | name = "version_check" 120 | version = "0.9.4" 121 | source = "registry+https://github.com/rust-lang/crates.io-index" 122 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 123 | 124 | [[package]] 125 | name = "wyz" 126 | version = "0.5.0" 127 | source = "registry+https://github.com/rust-lang/crates.io-index" 128 | checksum = "30b31594f29d27036c383b53b59ed3476874d518f0efb151b27a4c275141390e" 129 | dependencies = [ 130 | "tap", 131 | ] 132 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "peapod" 3 | version = "0.1.9" 4 | authors = ["Felix Prasanna"] 5 | edition = "2021" 6 | keywords = ["data-structures", "compact", "enums", "space-efficient", "vector"] 7 | categories = ["data-structures", "compression"] 8 | description = "Ultra-compact storage for enums" 9 | documentation = "https://docs.rs/peapod" 10 | license = "MIT OR Apache-2.0" 11 | readme = "README.md" 12 | repository = "https://github.com/fprasx/peapod" 13 | homepage = "https://github.com/fprasx/peapod" 14 | 15 | [dependencies] 16 | phenotype-internal = { version = "0.2.0", path = "phenotype-internal" } 17 | phenotype-macro = { version = "0.1.4", path = "phenotype-macro" } 18 | 19 | [dependencies.bitvec] 20 | version = "1" 21 | default-features = false 22 | features = ["alloc"] 23 | 24 | [workspace] 25 | members = ["phenotype-macro", "phenotype-internal"] 26 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 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 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Felix Prasanna 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Peapod 2 | 3 | `Peapod` is a `Vec`-like data structure for storing collections of enums 4 | super-compactly, like peas in a pod `:)` It works with any enum that implements 5 | the `Phenotype` trait, which captures the behaviour of each variant. 6 | 7 | ## Contents 8 | 9 | 1. [Usage](#Usage) 10 | 1. [Motivation](#motivation) 11 | 1. [Technical](#Tecnical) 12 | 1. [How `Peapod` works](#how-does-it-do-it) 13 | 1. [When not to use `Peapod`](#when-not-to-use-peapod) 14 | 15 | ## Usage 16 | 17 | First, add `peapod == 0.1.8` to your `Cargo.toml`. 18 | 19 | You can almost use `Peapod` like a normal `Vec`. Not all functionality 20 | is possible, notably, treating `Peapod` as a slice. This is due to the internal 21 | data representation. 22 | 23 | To make an enum suitable for `Peapod` storage, stick a `#[derive(Phenotype)]` on 24 | it. 25 | 26 | ```rust 27 | use peapod::{Phenotype, Peapod}; 28 | 29 | fn main() { 30 | // The Peapod representation is a lot smaller! 31 | // These numbers are in bytes 32 | assert_eq!(ILovePeas::PEAPOD_SIZE.unwrap(), 9); 33 | assert_eq!(std::mem::size_of::(), 16); 34 | 35 | let mut pp = Peapod::new(); 36 | pp.push(ILovePeas::SnowPea); 37 | pp.push(ILovePeas::Edamame(0x9EA90D)); 38 | pp.push(ILovePeas::GeneticPea { 39 | wrinkled: true, 40 | yellow: true, 41 | }); 42 | 43 | for pea in pp { 44 | // do something with pea! 45 | } 46 | } 47 | 48 | #[derive(Phenotype)] // <- this is where the magic happens 49 | enum ILovePeas { 50 | Edamame(usize), 51 | SnowPea, 52 | GeneticPea { wrinkled: bool, yellow: bool }, 53 | } 54 | ``` 55 | 56 | ## Motivation 57 | 58 | We only have so much memory to work with. Especially in space-constrained 59 | systems, we want to be particularly efficient. `Peapod` provides a way of 60 | storing `enums` that can dramatically reduce space usage. You can read more 61 | in-depth about the motivation in [technical](#technical) section. 62 | 63 | tl;dr: `Peapod` provides ultra-compact storage for enums! 64 | 65 | ## Technical 66 | 67 | enums (also known as tagged unions) are represented in memory by a tag (integer) 68 | and a union. The tag specifies how the bits of the union are interpreted. For 69 | example, a tag of 0 might mean "read the union as `Result::Ok(_)`", while a tag 70 | of 1 would mean "read the union as `Result::Err(_)`". 71 | 72 | Because of alignment reasons, the compiler has to lay out enums so that the tag 73 | takes up a more space than need be. If there are only two variants, we only need 74 | one _bit_ to keep track of which variant something is. Take this pretty drastic 75 | example: 76 | 77 | ```rust 78 | enum Two { 79 | First(usize), 80 | Second(usize) 81 | } 82 | // mem::size_of:: == 16 83 | ``` 84 | 85 | Since the size of each variant is 8 bytes, and the size of the enum is 16 bytes, 86 | **8 bytes** are being used for the tag! 63 bits are being wasted! We can do 87 | better. 88 | 89 | `Peapod` works by "cleaving" an enum into tag and union. Tags are stored 90 | together in a `bitvec` type so that no space is wasted due to alignment. All the 91 | data from the enums (in union form) is also stored together. 92 | 93 | This drawing illustrates the previous example: 94 | 95 | ``` 96 | Scale: 1 - == 1 byte 97 | 98 | Standard: 99 | +--------+--------+ 100 | | tag | data | 101 | +--------+--------+ 102 | ^ Only this byte is actually needed to store the tag 103 | 104 | Standard array: 105 | +--------+--------+--------+--------+--------+--------+ 106 | | tag | data | tag | data | tag | data | . . . 107 | +--------+--------+--------+--------+--------+--------+ 108 | 109 | Peapod: 110 | +-+--------+ 111 | | | data | 112 | +-+--------+ 113 | ^ tag 114 | 115 | Peapod array: 116 | +-+ +--------+--------+--------+ 117 | | | + | data | data | data | . . . 118 | +-+ +--------+--------+--------+ 119 | ^ many tags can be packed into one byte, we could hold 5 more tags in this byte 120 | ``` 121 | 122 | ## How does it do it? 123 | 124 | _Preface_: compiler people I beg your forgiveness. 125 | 126 | The magic is in the `Phenotype` trait, which has two very important methods: 127 | `cleave` and `reknit`. 128 | 129 | ```rust 130 | type Value; 131 | fn cleave(self) -> (usize, Self::Value) 132 | fn reknit(tag: usize, value: Self::Value) -> Self 133 | ``` 134 | 135 | The type `Value` is some type that can hold all the data from each enum variant. 136 | It should be a union. 137 | 138 | `cleave` takes a concrete instance of an enum and splits it into a tag (this tag 139 | is internal to `Phenotype`, unrelated to the compiler's) and a `Self::Value`. 140 | `reknit` does the opposite and takes a tag and a `Self::Value`, and 141 | reconstitutes it into an enum variant. 142 | 143 | The implementation all happens with the wizardry that is proc-macros. 144 | `#[derive(Phenotype)]` is the workhorse of this project. 145 | 146 | The `#[derive(Phenotype)]` takes a look at your enum and first generates some 147 | "auxiliary" types like so: 148 | 149 | ```rust 150 | enum ThreeTypes { 151 | NamedFields { 152 | one: T, 153 | two: usize 154 | }, 155 | Tuple(usize, usize), 156 | Empty 157 | } 158 | 159 | // Represents the `NamedFields` variant 160 | #[repr(packed)] 161 | struct __PhenotypeInternalThreeTypesNamedFieldsData { 162 | one: T, 163 | two: usize, 164 | } 165 | 166 | // Represents the `Tuple` variant 167 | #[repr(packed)] 168 | struct __PhenotypeInternalThreeTypesTupleData(usize, usize); 169 | 170 | #[allow(non_snake_case)] 171 | union __PhenotypeInternalThreeTypesData { 172 | NamedFields: ManuallyDrop<__PhenotypeInternalThreeTypesNamedFieldsData>, 173 | Tuple: ManuallyDrop<__PhenotypeInternalThreeTypesTupleData>, 174 | Empty: (), 175 | } 176 | ``` 177 | 178 | Then, it generates the `cleave` method. The generated code for this example 179 | looks like: 180 | 181 | ```rust 182 | fn cleave(self) -> (usize, Self::Value) { 183 | match &*ManuallyDrop::new(self) { 184 | ThreeTypes::Empty => (2usize, __PhenotypeInternalThreeTypesData { Empty: () }), 185 | ThreeTypes::Tuple(_0, _1) => ( 186 | 1usize, 187 | __PhenotypeInternalThreeTypesData { 188 | Tuple: ManuallyDrop::new(__PhenotypeInternalThreeTypesTupleData( 189 | unsafe { ::core::ptr::read(_0) }, 190 | unsafe { ::core::ptr::read(_1) }, 191 | )), 192 | }, 193 | ), 194 | ThreeTypes::NamedFields { one, two } => ( 195 | 0usize, 196 | __PhenotypeInternalThreeTypesData { 197 | NamedFields: ManuallyDrop::new(__PhenotypeInternalThreeTypesNamedFieldsData::< 198 | T, 199 | > { 200 | one: unsafe { ::core::ptr::read(one) }, 201 | two: unsafe { ::core::ptr::read(two) }, 202 | }), 203 | }, 204 | ), 205 | } 206 | } 207 | ``` 208 | 209 | All we're doing is `match`ing on the enum variant and reading out each field 210 | into the correct auxiliary struct. 211 | 212 | `cleave` does the opposite. Based on the tag, it reads the union and generates 213 | and enum variant from the data contained in the auxiliary `struct`. 214 | 215 | ```rust 216 | fn reknit(tag: usize, value: Self::Value) -> ThreeTypes { 217 | match tag { 218 | 2usize => ThreeTypes::Empty, 219 | 1usize => { 220 | let data = 221 | ManuallyDrop::<__PhenotypeInternalThreeTypesTupleData>::into_inner(unsafe { 222 | value.Tuple 223 | }); 224 | ThreeTypes::Tuple(data.0, data.1) 225 | } 226 | 0usize => { 227 | let data = 228 | ManuallyDrop::<__PhenotypeInternalThreeTypesNamedFieldsData>::into_inner( 229 | unsafe { value.NamedFields }, 230 | ); 231 | ThreeTypes::NamedFields { 232 | one: data.one, 233 | two: data.two, 234 | } 235 | } 236 | _ => unreachable!(), 237 | } 238 | } 239 | ``` 240 | 241 | ## When not to use `Peapod` 242 | 243 | - Sometimes `enums` are niche optimized, meaning the compiler has found a 244 | clever way to elide the tag. The canonical example is `Option>`: 245 | since the `NonNull` cannot be null, the compiler can use the null pointer 246 | to represent the `None` variant. This is fine as the `None` variant doesn't 247 | actually contain a `NonNull`. In summary, an valid pointer bit pattern 248 | represents a `Some` variant, and the null pointer represents the `None` 249 | variant, so there is no need to store a tag. 250 | - Sometimes `Peapod` won't produce a smaller representation. You can check 251 | this using the provided `IS_MORE_COMPACT` constant. 252 | - You don't have an allocator. I'm working on a fixed-size `Peapod` but it 253 | seems like it's going to be difficult as long as `const` generics are 254 | incomplete. 255 | 256 | ## License 257 | 258 | Licensed under either of 259 | 260 | - Apache License, Version 2.0 261 | ([LICENSE-APACHE](https://github.com/fprasx/peapod/blob/main/LICENSE-APACHE) 262 | or http://www.apache.org/licenses/LICENSE-2.0) 263 | - MIT license 264 | ([LICENSE-MIT](https://github.com/fprasx/peapod/blob/main/LICENSE-MIT) or 265 | http://opensource.org/licenses/MIT) 266 | 267 | at your option. 268 | 269 | ## Contribution 270 | 271 | Unless you explicitly state otherwise, any contribution intentionally submitted 272 | for inclusion in the work by you, as defined in the Apache-2.0 license, shall be 273 | dual licensed as above, without any additional terms or conditions. 274 | -------------------------------------------------------------------------------- /phenotype-internal/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "phenotype-internal" 3 | version = "0.2.0" 4 | edition = "2021" 5 | license = "MIT OR Apache-2.0" 6 | description = "Defines `Phenotype` trait for `Peapod` crate" 7 | repository = "https://github.com/fprasx/peapod" 8 | documentation = "https://docs.rs/peapod" 9 | homepage = "https://github.com/fprasx/peapod" 10 | 11 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 12 | 13 | [dependencies] 14 | -------------------------------------------------------------------------------- /phenotype-internal/src/lib.rs: -------------------------------------------------------------------------------- 1 | // TODO: add examples 2 | 3 | /// This trait represents the behavior of an `enum`/tagged union. 4 | /// **Note**: it should only be implemented with `#[derive(Phenotype)]` 5 | /// # Safety 6 | /// This trait is marked unsafe because _extreme_ care must be taken to implement 7 | /// it correctly. In particular, the `reknit` method can cause undefined behavior 8 | /// if called with invalid inputs. Manual implementation of the trait is heavily 9 | /// discouraged, but there may be cases (e.g. `const` generics) where it is necessary. 10 | pub unsafe trait Phenotype { 11 | /// The number of variants of the enum. 12 | const NUM_VARIANTS: usize; 13 | 14 | /// The number of bits needed to represent every variant of the enum. 15 | /// For example, if the enum has 4 variants, then two bits are needed. 16 | const BITS: usize; 17 | 18 | /// The number of bits `Phenotype` uses to represent and instance of a type. 19 | /// If the type `Phenotype` is being implemented for is generic, 20 | /// this will be `None`, as sizes may vary accross different 21 | /// generic parameters. For example, `Type` could be differently 22 | /// sized than `Type<[usize; 4]>` 23 | const PEAPOD_SIZE: Option; 24 | 25 | /// Whether using `Phenotype` produces a more compact representation. 26 | /// Will be `Some(true)` if implementations are the same size. 27 | /// If the type `Phenotype` is being implemented for is generic, 28 | /// this will be `None`, as sizes may vary accross different 29 | /// generic parameters. For example, `Type` could be differently 30 | /// sized than `Type<[usize; 4]>` 31 | const IS_MORE_COMPACT: Option; 32 | 33 | /// A type that represents all the data an enum can contain. 34 | /// This should be a union whose fields each represent a particular 35 | /// enum variant. 36 | type Value; 37 | 38 | /// Takes an enum variant and `cleave`s it into a two parts: 39 | /// a tag, and an union representing the data the enum can hold. 40 | /// If the enum variant doesn't hold data, `None` is returned as 41 | /// the second tuple element. 42 | /// **Note**: if the results of a call to `cleave` are not eventually 43 | /// `reknit`ed, the destructor for the `cleave`ed enum will not run. 44 | /// This can cause memory leaks. Types that manage heap memory often 45 | /// implement cleanup and deallocation in their `Drop` implementations. 46 | fn cleave(self) -> (usize, Self::Value); 47 | 48 | /// Takes a tag and a value and recombines them into a proper 49 | /// instance of an enum variant. 50 | /// # Safety 51 | /// Calling this function with incorrect 52 | /// inputs can result in undefined behavior. The tag must always match 53 | /// the state that the union is in. 54 | /// 55 | /// For example, consider the following example 56 | /// ``` 57 | /// #[derive(Phenotype)] 58 | /// enum UB { 59 | /// U(usize), // -> tag = 0 60 | /// B(bool) // -> tag = 1 61 | /// } 62 | /// 63 | /// // This is the type ::Value 64 | /// union Value { 65 | /// U: usize, 66 | /// B: bool 67 | /// } 68 | /// 69 | /// use peapod::Phenotype; 70 | /// fn main { 71 | /// let ub = UB::U(3); 72 | /// let (_, data) = ub.cleave(); 73 | /// // ** DANGER ** 74 | /// // We are interpreting 3 as a bool! That's undefined behavior. 75 | /// let BAD = ::reknit(1, data); 76 | /// } 77 | /// ``` 78 | unsafe fn reknit(tag: usize, value: Self::Value) -> Self; 79 | } 80 | 81 | /// Some helpful methods for using `Phenotype` 82 | pub trait PhenotypeDebug: Phenotype { 83 | /// Returns the tag that Phenotype uses internally 84 | /// to identify the enum variant. 85 | /// **Note**: this is different from the tag the compiler 86 | /// uses or a discriminant that was manually specified. It 87 | /// only has meaning in the context of `Phenotype`. 88 | fn discriminant(&self) -> usize; 89 | 90 | /// Takes a tag and returns a string that represents 91 | /// the variant. For example, it might return something 92 | /// like `Result::Ok` for 0 and `Result::Err` for 1 if `Phenotype` 93 | /// was derived on the `Result` type. 94 | fn debug_tag(tag: usize) -> &'static str; 95 | } 96 | -------------------------------------------------------------------------------- /phenotype-macro/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "phenotype-macro" 3 | version = "0.1.4" 4 | edition = "2021" 5 | license = "MIT OR Apache-2.0" 6 | description = "Exports `#[derive(Phenotype)]` macro for `Peapod` crate" 7 | repository = "https://github.com/fprasx/peapod" 8 | homepage = "https://github.com/fprasx/peapod" 9 | 10 | [lib] 11 | proc-macro = true 12 | 13 | [dependencies] 14 | phenotype-internal = { path = "../phenotype-internal", version = "0.2.0" } 15 | proc-macro2 = "1.0" 16 | quote = { version = "1.0" } 17 | syn = { version = "1.0", features = ["derive", "printing", "visit"] } 18 | proc-macro-error = "1.0" 19 | -------------------------------------------------------------------------------- /phenotype-macro/src/generic.rs: -------------------------------------------------------------------------------- 1 | extern crate proc_macro; 2 | use core::ops::Not; 3 | use quote::quote; 4 | use std::collections::BTreeSet; 5 | use syn::{ 6 | visit::{self as subrecurse, Visit}, 7 | *, 8 | }; 9 | 10 | struct GenericsVisitor<'a> { 11 | unseen_types: Vec<&'a Ident>, 12 | unseen_lifetimes: Vec<&'a Lifetime>, 13 | } 14 | 15 | impl<'i> Visit<'i> for GenericsVisitor<'_> { 16 | fn visit_type_path(&mut self, ty_path: &'i TypePath) { 17 | subrecurse::visit_type_path(self, ty_path); 18 | if ty_path.qself.is_some() { 19 | return; 20 | } 21 | // if the type path is made of a single ident: 22 | if let Some(ident) = ty_path.path.get_ident() { 23 | // Keep the types that aren't this generic we've found 24 | self.unseen_types.retain(|&generic| ident != generic) 25 | } 26 | } 27 | 28 | fn visit_type_reference(&mut self, ty_ref: &'i TypeReference) { 29 | subrecurse::visit_type_reference(self, ty_ref); 30 | if let Some(lt) = &ty_ref.lifetime { 31 | self.unseen_lifetimes.retain(|&lifetime| *lt != *lifetime) 32 | } 33 | } 34 | } 35 | 36 | pub fn extract_generics<'generics>( 37 | generics: &'generics Generics, 38 | ty: &'_ Type, 39 | ) -> (Vec<&'generics Ident>, Vec<&'generics Lifetime>) { 40 | // The generics from the input type 41 | let generic_tys = || generics.type_params().map(|it| &it.ident); 42 | let lts = || generics.lifetimes().map(|lt| <.lifetime); 43 | 44 | let mut visitor = GenericsVisitor { 45 | unseen_types: generic_tys().collect(), 46 | unseen_lifetimes: lts().collect(), 47 | }; 48 | 49 | visitor.visit_type(ty); 50 | 51 | if let Type::Reference(ty_ref) = ty { 52 | visitor.visit_type_reference(ty_ref) 53 | } 54 | 55 | ( 56 | generic_tys() 57 | // Keep the types we've not-not seen 58 | .filter(|ty| visitor.unseen_types.contains(ty).not()) 59 | .collect(), 60 | lts() 61 | // Keep the types we've not-not seen 62 | .filter(|lt| visitor.unseen_lifetimes.contains(lt).not()) 63 | .collect(), 64 | ) 65 | } 66 | 67 | pub fn variant_generics(all_generics: &Generics, variant: &Variant) -> proc_macro2::TokenStream { 68 | let mut generics = BTreeSet::new(); 69 | let mut lifetimes = BTreeSet::new(); 70 | match &variant.fields { 71 | syn::Fields::Named(fields) => { 72 | for f in &fields.named { 73 | let (tys, lts) = extract_generics(all_generics, &f.ty); 74 | generics.extend(tys); 75 | lifetimes.extend(lts); 76 | } 77 | quote! { 78 | <#(#lifetimes,)* #(#generics),*> 79 | } 80 | } 81 | syn::Fields::Unnamed(fields) => { 82 | for f in &fields.unnamed { 83 | let (tys, lts) = extract_generics(all_generics, &f.ty); 84 | generics.extend(tys); 85 | lifetimes.extend(lts); 86 | } 87 | quote! { 88 | <#(#lifetimes,)* #(#generics),*> 89 | } 90 | } 91 | syn::Fields::Unit => quote!(<>), 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /phenotype-macro/src/lib.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream; 2 | use proc_macro_error::{abort, proc_macro_error}; 3 | use quote::{format_ident, quote}; 4 | use std::collections::HashMap; 5 | use syn::{parse_macro_input, DeriveInput, FieldsNamed, FieldsUnnamed, Generics, Ident, Variant}; 6 | 7 | const NOTE: &str = "can only derive phenotype on enums"; 8 | 9 | type Tag = usize; 10 | 11 | /// Holds the logic for parsing generics 12 | mod generic; 13 | 14 | /// Condensed derive input; just the stuff we need 15 | struct Condensed<'a> { 16 | name: Ident, 17 | variants: HashMap, 18 | generics: &'a Generics, 19 | } 20 | // For calculating log without using the unstable feature 21 | const fn num_bits() -> usize { 22 | std::mem::size_of::() * 8 23 | } 24 | 25 | fn log2(x: usize) -> u32 { 26 | assert!(x > 0); 27 | num_bits::() as u32 - x.leading_zeros() - 1 28 | } 29 | 30 | #[proc_macro_derive(Phenotype)] 31 | #[proc_macro_error] 32 | pub fn phenotype(input: proc_macro::TokenStream) -> proc_macro::TokenStream { 33 | let ast = parse_macro_input!(input as DeriveInput); 34 | 35 | let (impl_generics, ty_generics, where_clause) = ast.generics.split_for_impl(); 36 | let ident = ast.ident.clone(); 37 | 38 | // Verify we have an enum 39 | let enumb = match ast.data { 40 | syn::Data::Enum(e) => e, 41 | syn::Data::Struct(data) => { 42 | abort!(data.struct_token, "struct `{}` is not an enum", ast.ident; note=NOTE) 43 | } 44 | syn::Data::Union(data) => { 45 | abort!(data.union_token, "union `{}` is not an enum", ast.ident; note=NOTE) 46 | } 47 | }; 48 | 49 | let data = Condensed { 50 | variants: enumb 51 | .variants 52 | .into_iter() 53 | .enumerate() 54 | .collect::>(), 55 | name: ident.clone(), 56 | generics: &ast.generics, 57 | }; 58 | 59 | // Make sure there are variants! 60 | if data.variants.is_empty() { 61 | abort!(data.name, "enum `{}` has no variants", data.name) 62 | } 63 | 64 | // Abort if there are const generics - works funky with the way we deal with generics 65 | if ast.generics.const_params().next().is_some() { 66 | abort!( 67 | ty_generics, 68 | "const generics are not supported for `#[derive(Phenotype)]`"; 69 | note = "it may be possible to implement `Phenotype` by hand" 70 | ) 71 | } 72 | 73 | let auxiliaries = make_auxiliaries(&data); 74 | 75 | let cleave_impl = cleave_impl(&data); 76 | 77 | let reknit_impl = reknit_impl(&data); 78 | 79 | let bits = { 80 | if data.variants.is_empty() { 81 | 0 82 | } else if data.variants.len() == 1 { 83 | // This avoids having to check everywhere if T::BITS == 1, 84 | // which is easy to forget and can easily cause panics, 85 | // for the cheap cost of one bit 86 | 1 87 | } else { 88 | let log = log2(data.variants.len()); 89 | let pow = 2usize.pow(log); 90 | 91 | // if 2 ** log is less than the number of variants, that means 92 | // the log rounded down (i.e. the float version was something like 93 | // 1.4, which became 1) 94 | // 95 | // We round up because we always carry the extra bits, i.e. 96 | // 7 variants needs 2.8 bits but we carry 3 97 | (if pow < data.variants.len() { 98 | log + 1 99 | } else { 100 | log 101 | }) as usize 102 | } 103 | }; 104 | 105 | let num_variants = data.variants.len(); 106 | 107 | let union_ident = format_ident!("__PhenotypeInternal{}Data", data.name); 108 | 109 | let peapod_size = match data.generics.type_params().next() { 110 | Some(_) => quote!(None), 111 | // No generics 112 | None => { 113 | let bytes = bits / 8 114 | + if bits % 8 == 0 { 115 | 0 116 | } else { 117 | // Add an extra byte if there are remaining bits (a partial byte) 118 | 1 119 | }; 120 | quote!(Some({ #bytes + ::core::mem::size_of::<#union_ident>() })) 121 | } 122 | }; 123 | 124 | let is_more_compact = match data.generics.type_params().next() { 125 | Some(_) => quote!(None), 126 | // No generics 127 | None => { 128 | quote!( 129 | Some( 130 | // unwrap isn't const 131 | match ::PEAPOD_SIZE { 132 | Some(size) => size <= ::core::mem::size_of::<#ident>(), 133 | // Unreachable as if there are not generics, PEAPOD_SIZE 134 | // is `Some` 135 | None => unreachable!() 136 | } 137 | 138 | ) 139 | ) 140 | } 141 | }; 142 | 143 | quote! { 144 | #auxiliaries 145 | unsafe impl #impl_generics Phenotype for #ident #ty_generics 146 | #where_clause 147 | { 148 | const NUM_VARIANTS: usize = #num_variants; 149 | const BITS: usize = #bits; 150 | const PEAPOD_SIZE: Option = #peapod_size; 151 | const IS_MORE_COMPACT: Option = #is_more_compact; 152 | #cleave_impl 153 | #reknit_impl 154 | } 155 | } 156 | .into() 157 | } 158 | 159 | fn reknit_impl(data: &Condensed) -> TokenStream { 160 | let mut arms = Vec::with_capacity(data.variants.len()); 161 | 162 | let ident = &data.name; 163 | 164 | // We're going to turn each variant into a match that handles that variant's case 165 | for (tag, var) in &data.variants { 166 | let struct_name = format_ident!("__PhenotypeInternal{}{}Data", data.name, var.ident); 167 | let var_ident = &var.ident; 168 | let var_generics = generic::variant_generics(data.generics, var); 169 | arms.push(match &var.fields { 170 | syn::Fields::Named(FieldsNamed { named, .. }) => { 171 | let struct_fields = named 172 | .iter() 173 | .map(|f| f.ident.clone().unwrap()) 174 | .collect::>(); 175 | quote! { 176 | #tag => { 177 | // SAFETY: Safe because the tag guarantees that we are reading the correct field 178 | let data = ::core::mem::ManuallyDrop::<#struct_name :: #var_generics>::into_inner( 179 | unsafe { value.#var_ident } 180 | ); 181 | #ident::#var_ident { #(#struct_fields: data.#struct_fields),* } 182 | } 183 | } 184 | } 185 | syn::Fields::Unnamed(FieldsUnnamed { unnamed, .. }) => { 186 | // This produces the indexes we use to extract the data from the struct 187 | let struct_field_placeholders = (0..unnamed.len()).map(syn::Index::from); 188 | quote! { 189 | #tag => { 190 | // SAFETY: Safe because the tag guarantees that we are reading the correct field 191 | let data = ::core::mem::ManuallyDrop::<#struct_name :: #var_generics>::into_inner( 192 | unsafe { value.#var_ident } 193 | ); 194 | #ident::#var_ident ( #(data.#struct_field_placeholders),* ) 195 | } 196 | } 197 | } 198 | syn::Fields::Unit => { 199 | quote! { 200 | #tag => { 201 | #ident::#var_ident 202 | } 203 | } 204 | } 205 | }) 206 | } 207 | 208 | let generics = data.generics.split_for_impl().1; 209 | quote! { 210 | unsafe fn reknit(tag: usize, value: ::Value) -> #ident #generics { 211 | match tag { 212 | #(#arms),* 213 | // There should be no other cases, as there are no other variants 214 | _ => ::core::unreachable!() 215 | } 216 | } 217 | } 218 | } 219 | 220 | /// Implement the `value` trait method 221 | fn cleave_impl(data: &Condensed) -> proc_macro2::TokenStream { 222 | let ident = &data.name; 223 | let union_ident = format_ident!("__PhenotypeInternal{ident}Data"); 224 | 225 | // Snippet to extract data out of each field 226 | let mut arms: Vec = Vec::with_capacity(data.variants.len()); 227 | 228 | let generics = data.generics.split_for_impl().1; 229 | 230 | // Like `reknit_impl`, we produce a match arm for each variant 231 | for (tag, var) in &data.variants { 232 | let var_ident = &var.ident; 233 | let struct_name = format_ident!("__PhenotypeInternal{ident}{var_ident}Data"); 234 | 235 | let var_generics = generic::variant_generics(data.generics, var); 236 | arms.push(match &var.fields { 237 | syn::Fields::Named(FieldsNamed { named, .. }) => { 238 | // Capture each enum field (named), use it's ident to capture it's value 239 | let fields = named.iter().map(|f| f.ident.clone()).collect::>(); 240 | quote! { 241 | #ident::#var_ident {#(#fields),*} => (#tag, 242 | #union_ident { 243 | #var_ident: ::core::mem::ManuallyDrop::new(#struct_name :: #var_generics { 244 | // We've wrapped the enum that was passed in in a ManuallyDrop, 245 | // and now we read each field with ptr::read. 246 | 247 | // We wrap the enum that was passed in a ManuallyDrop to prevent 248 | // double drops. 249 | 250 | // We have to ptr::read because you can't move out of a 251 | // type that implements `Drop` 252 | // SAFETY: we are reading from a reference 253 | #(#fields: unsafe { ::core::ptr::read(#fields) }),* 254 | }) 255 | } 256 | ) 257 | } 258 | } 259 | syn::Fields::Unnamed(FieldsUnnamed { unnamed, .. }) => { 260 | // For each field (unnamed), produce an ident like _0, _1, ... so we can capture the value 261 | let fields = (0..unnamed.iter().len()) 262 | .map(|i| format_ident!("_{i}")) 263 | .collect::>(); 264 | quote! { 265 | #ident::#var_ident(#(#fields),*) => (#tag, 266 | #union_ident { 267 | #var_ident: ::core::mem::ManuallyDrop::new( 268 | #struct_name :: #var_generics ( 269 | // We've wrapped the enum that was passed in in a ManuallyDrop, 270 | // and now we read each field with ptr::read. 271 | 272 | // We wrap the enum that was passed in a ManuallyDrop to prevent 273 | // double drops. 274 | 275 | // We have to ptr::read because you can't move out of a 276 | // type that implements `Drop` 277 | // SAFETY: we are reading from a reference 278 | #( unsafe { ::core::ptr::read(#fields) }),* 279 | ) 280 | ) 281 | } 282 | ) 283 | } 284 | } 285 | syn::Fields::Unit => quote! { 286 | #ident::#var_ident => (#tag, #union_ident { #var_ident: () }) // Doesn't contain data 287 | }, 288 | }) 289 | } 290 | quote! { 291 | type Value = #union_ident #generics; 292 | fn cleave(self) -> (usize, ::Value) { 293 | match &*::core::mem::ManuallyDrop::new(self) { 294 | #(#arms),* 295 | } 296 | } 297 | } 298 | } 299 | 300 | /// A struct that represents the data found in an enum 301 | struct Auxiliary { 302 | ident: Ident, 303 | // Tokens for the actual code of the struct 304 | tokens: proc_macro2::TokenStream, 305 | } 306 | 307 | /// Return an auxiliary struct that can hold the data from an enum variant. 308 | /// Returns `None` if the variant doesn't contain any data 309 | fn def_auxiliary_struct( 310 | variant: &Variant, 311 | enum_name: &Ident, 312 | all_generics: &Generics, 313 | ) -> Option { 314 | let field = &variant.ident; 315 | 316 | let struct_name = format_ident!("__PhenotypeInternal{}{}Data", enum_name, field); 317 | 318 | let generics = generic::variant_generics(all_generics, variant); 319 | 320 | match &variant.fields { 321 | // Create a dummy struct that contains the named fields 322 | // We need the field idents and types so we can make pairs like: 323 | // ident1: type1 324 | // ident2: type2 325 | // ... 326 | syn::Fields::Named(FieldsNamed { named, .. }) => { 327 | // Get the names of the fields 328 | let idents = named.iter().map(|field| field.ident.as_ref().unwrap()); 329 | let types = named.iter().map(|field| &field.ty); 330 | Some(Auxiliary { 331 | ident: struct_name.clone(), 332 | tokens: quote! { 333 | #[repr(packed)] 334 | struct #struct_name #generics { 335 | #(#idents: #types,)* 336 | } 337 | }, 338 | }) 339 | } 340 | 341 | // Create a dummy tuple struct that contains the fields 342 | // We only need the types so we can produce output like 343 | // type1, type2, ... 344 | syn::Fields::Unnamed(FieldsUnnamed { unnamed, .. }) => { 345 | let types = unnamed.iter().map(|field| &field.ty); 346 | Some(Auxiliary { 347 | ident: struct_name.clone(), 348 | tokens: quote! { #[repr(packed)] struct #struct_name #generics (#(#types,)*); }, 349 | }) 350 | } 351 | 352 | // No fields so we don't need to do anything 353 | syn::Fields::Unit => None, 354 | } 355 | } 356 | 357 | /// Define all auxiliary structs and the data enum 358 | fn make_auxiliaries(data: &Condensed) -> proc_macro2::TokenStream { 359 | // Define the union that holds the data 360 | let union_ident = format_ident!("__PhenotypeInternal{}Data", data.name); 361 | 362 | // Assorted data that goes into defining all the machinery 363 | let ( 364 | mut struct_idents, 365 | mut struct_defs, 366 | mut field_idents, 367 | mut empty_field_idents, 368 | mut struct_generics, 369 | ) = (vec![], vec![], vec![], vec![], vec![]); 370 | 371 | for var in data.variants.values() { 372 | if let Some(aux) = def_auxiliary_struct(var, &data.name, data.generics) { 373 | struct_idents.push(aux.ident); 374 | struct_defs.push(aux.tokens); 375 | field_idents.push(var.ident.clone()); 376 | struct_generics.push(generic::variant_generics(data.generics, var)); 377 | } else { 378 | empty_field_idents.push(var.ident.clone()) 379 | } 380 | } 381 | 382 | let union_generics = data.generics.split_for_impl().1; 383 | 384 | quote! { 385 | #(#struct_defs)* 386 | #[allow(non_snake_case)] 387 | union #union_ident #union_generics { 388 | #(#field_idents: ::core::mem::ManuallyDrop<#struct_idents #struct_generics>,)* 389 | #(#empty_field_idents: (),)* 390 | } 391 | } 392 | } 393 | 394 | #[proc_macro_derive(PhenotypeDebug)] 395 | #[proc_macro_error] 396 | pub fn phenotype_debug(input: proc_macro::TokenStream) -> proc_macro::TokenStream { 397 | let ast = parse_macro_input!(input as DeriveInput); 398 | 399 | let (impl_generics, ty_generics, where_clause) = ast.generics.split_for_impl(); 400 | let ident = ast.ident.clone(); 401 | 402 | // Verify we have an enum 403 | let enumb = match ast.data { 404 | syn::Data::Enum(e) => e, 405 | syn::Data::Struct(data) => { 406 | abort!(data.struct_token, "struct `{}` is not an enum", ast.ident; note=NOTE) 407 | } 408 | syn::Data::Union(data) => { 409 | abort!(data.union_token, "union `{}` is not an enum", ast.ident; note=NOTE) 410 | } 411 | }; 412 | 413 | let data = Condensed { 414 | variants: enumb 415 | .variants 416 | .into_iter() 417 | .enumerate() 418 | .collect::>(), 419 | name: ident.clone(), 420 | generics: &ast.generics, 421 | }; 422 | 423 | // Make sure there are variants! 424 | if data.variants.is_empty() { 425 | abort!(data.name, "enum `{}` has no variants", data.name) 426 | } 427 | 428 | // Abort if there are const generics - works funky with the way we deal with generics 429 | if ast.generics.const_params().next().is_some() { 430 | abort!( 431 | ty_generics, 432 | "const generics are not supported for `#[derive(Phenotype)]`"; 433 | note = "it may be possible to implement `Phenotype` by hand" 434 | ) 435 | } 436 | 437 | let discriminant_impl = discriminant_impl(&data); 438 | let debug_tag_impl = debug_tag_impl(&data); 439 | quote! { 440 | impl #impl_generics PhenotypeDebug for #ident #ty_generics 441 | #where_clause 442 | { 443 | #discriminant_impl 444 | #debug_tag_impl 445 | } 446 | } 447 | .into() 448 | } 449 | 450 | /// Code for the discriminant trait method 451 | fn discriminant_impl(data: &Condensed) -> proc_macro2::TokenStream { 452 | let enum_name = &data.name; 453 | 454 | // Zip variants together with discriminants 455 | // Each quote! looks something like `ident::variant => number,` 456 | let arms = data.variants.iter().map(|(tag, variant)| { 457 | let var_ident = &variant.ident; 458 | // Make sure we have the proper destructuring syntax 459 | match variant.fields { 460 | syn::Fields::Named(_) => quote! { #enum_name::#var_ident {..} => #tag,}, 461 | syn::Fields::Unnamed(_) => quote! { #enum_name::#var_ident (..) => #tag,}, 462 | syn::Fields::Unit => quote! { #enum_name::#var_ident => #tag,}, 463 | } 464 | }); 465 | 466 | quote! { 467 | fn discriminant(&self) -> usize { 468 | match &self { 469 | #(#arms)* 470 | } 471 | } 472 | } 473 | } 474 | 475 | /// Code for the debug_tag trait method 476 | fn debug_tag_impl(data: &Condensed) -> proc_macro2::TokenStream { 477 | let enum_name = &data.name; 478 | 479 | // Zip variants together with discriminants 480 | // Each quote! looks something like `ident::variant => number,` 481 | let arms = data.variants.iter().map(|(tag, variant)| { 482 | let var_ident = &variant.ident; 483 | let stringified = format!("{}::{}", enum_name, var_ident); 484 | quote! { 485 | #tag => #stringified, 486 | } 487 | }); 488 | 489 | quote! { 490 | fn debug_tag(tag: usize) -> &'static str { 491 | match tag { 492 | #(#arms)* 493 | _ => ::core::panic!("invalid tag") 494 | } 495 | } 496 | } 497 | } 498 | -------------------------------------------------------------------------------- /src/array.rs: -------------------------------------------------------------------------------- 1 | use bitvec::prelude::*; 2 | use bitvec::BitArr; 3 | use core::mem::MaybeUninit; 4 | use phenotype_internal::Phenotype; 5 | 6 | pub struct FixedPod { 7 | // TODO: calculate const in macro 8 | // 32 * (1 + T::BITS), supposing T has 3 variants 9 | // assuming N = 32 10 | tags_init: BitArr!(for 69), 11 | data: [MaybeUninit; N], 12 | len: usize, 13 | } 14 | 15 | impl FixedPod 16 | where 17 | T: Phenotype, 18 | { 19 | const UNINIT: MaybeUninit = MaybeUninit::uninit(); 20 | 21 | pub fn new() -> Self { 22 | Self { 23 | tags_init: BitArray::ZERO, 24 | data: [Self::UNINIT; N], 25 | len: 0, 26 | } 27 | } 28 | 29 | fn len(&self) -> usize { 30 | if let Some(index) = self.tags_init[..32].first_one() { 31 | index + 1 32 | } else { 33 | 0 34 | } 35 | } 36 | 37 | // return value indicates success or failure 38 | pub fn push(&mut self, elem: T) -> bool { 39 | let len = self.len(); 40 | if len == N { 41 | return false; 42 | } 43 | let (tag, data) = elem.cleave(); 44 | if self.check_init(len) { 45 | self.data[len].write(data); 46 | // We initialized the element at len'th spot, so mark it wth true 47 | self.set_tag(len, tag); 48 | // We initialized the element at len'th spot, so mark it wth true 49 | self.set_init(len); 50 | todo!() 51 | } else { 52 | todo!() 53 | } 54 | } 55 | 56 | // Will overwrite last element 57 | pub fn force_push(&mut self, elem: T) { 58 | todo!() 59 | } 60 | 61 | // **Note**: index must be in range 62 | fn check_init(&self, index: usize) -> bool { 63 | self.tags_init[index] 64 | } 65 | 66 | // **Note**: index must be in range 67 | fn set_init(&mut self, index: usize) { 68 | self.tags_init.set(index, true); 69 | } 70 | 71 | // **Note**: index must be in range 72 | fn get_tag(&self, index: usize) -> usize { 73 | // offset by 32 as the first 32 bits indicate init-ness 74 | self.tags_init[N + index * T::BITS..N + (index + 1) * T::BITS].load() 75 | } 76 | 77 | // **Note**: index must be in range 78 | fn set_tag(&mut self, index: usize, tag: usize) { 79 | // offset by 32 as the first 32 bits indicate init-ness 80 | self.tags_init[N + index * T::BITS..N + (index + 1) * T::BITS].store(tag); 81 | } 82 | } 83 | 84 | impl Default for FixedPod 85 | where 86 | T: Phenotype, 87 | { 88 | fn default() -> Self { 89 | Self::new() 90 | } 91 | } 92 | 93 | // struct FixedPod 94 | // where 95 | // [(); N / 64]:, 96 | // { 97 | // tags_init: [usize; N / 64], 98 | // ts: [usize; N], 99 | // _boo: PhantomData, 100 | // } 101 | 102 | // impl FixedPod { 103 | // fn new() -> Self { 104 | // Self { 105 | // tags_init: 0, 106 | // ts: [1; N], 107 | // _boo: PhantomData, 108 | // } 109 | // } 110 | // } 111 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc = include_str!("../README.md")] 2 | // TODO: tests! 3 | #![no_std] 4 | 5 | // Exports :) 6 | pub use crate::peapod_vec::Peapod; 7 | pub use phenotype_internal::{Phenotype, PhenotypeDebug}; 8 | pub use phenotype_macro::{Phenotype, PhenotypeDebug}; 9 | 10 | mod peapod_vec; 11 | 12 | // in the works 13 | mod array; 14 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use peapod::{Peapod, Phenotype}; 2 | 3 | #[derive(Phenotype, PartialEq, Eq, Debug)] 4 | enum Enum { 5 | Variant0, 6 | Variant1, 7 | Variant2, 8 | } 9 | 10 | fn main() { 11 | let mut pp = Peapod::new(); 12 | pp.push(Enum::Variant0); 13 | pp.truncate(usize::MAX / 2 + 1); 14 | assert_eq!(pp.pop(), Some(Enum::Variant0)); 15 | } -------------------------------------------------------------------------------- /src/peapod_vec.rs: -------------------------------------------------------------------------------- 1 | extern crate alloc; 2 | use alloc::{format, vec::Vec}; 3 | use bitvec::{field::BitField, prelude::*}; 4 | use core::{ 5 | cmp, 6 | fmt::{self, Debug, Display}, 7 | mem::ManuallyDrop, 8 | ptr, 9 | }; 10 | use phenotype_internal::Phenotype; 11 | 12 | // credit: https://veykril.github.io/tlborm/decl-macros/building-blocks/counting.html#bit-twiddling 13 | #[doc(hidden)] 14 | #[macro_export] 15 | macro_rules! count_tts { 16 | () => { 0 }; 17 | ($odd:tt $($a:tt $b:tt)*) => { ($crate::count_tts!($($a)*) << 1) | 1 }; 18 | ($($a:tt $even:tt)*) => { $crate::count_tts!($($a)*) << 1 }; 19 | } 20 | 21 | #[macro_export] 22 | /// A nice way to generate a `Peapod` from a list of elements. If you're familiar 23 | /// with the `vec![]` macro, this is `Peapod`'s equivalent. 24 | /// ```rust 25 | /// # use peapod::{Peapod, Phenotype, peapod}; 26 | /// #[derive(Phenotype)] 27 | /// enum Test { 28 | /// A, 29 | /// B 30 | /// } 31 | /// 32 | /// let mut fast = peapod![Test::A, Test::B]; 33 | /// 34 | /// // is the same as 35 | /// 36 | /// let mut slow = Peapod::with_capacity(2); 37 | /// slow.push(Test::A); 38 | /// slow.push(Test::B); 39 | /// ``` 40 | macro_rules! peapod { 41 | () => { 42 | $crate::Peapod::new(); 43 | }; 44 | ($($elem:expr),+ $(,)?) => { 45 | { 46 | let count = $crate::count_tts!($($elem:expr),*); 47 | let mut pp = $crate::Peapod::with_capacity(count); 48 | $(pp.push($elem);)* 49 | pp 50 | } 51 | 52 | }; 53 | } 54 | 55 | /// A `vec`-like data structure for compactly storing `enum`s that implement [`Phenotype`]. 56 | #[derive(Eq)] 57 | pub struct Peapod { 58 | tags: BitVec, 59 | data: Vec, 60 | } 61 | 62 | impl Peapod 63 | where 64 | T: Phenotype, 65 | { 66 | /// Create a new `Peapod` with 0 capacity and 0 length. This does not allocate. 67 | pub fn new() -> Self { 68 | Peapod { 69 | tags: BitVec::new(), 70 | data: Vec::new(), 71 | } 72 | } 73 | 74 | // **Note**: index must be in range 75 | fn get_tag(&self, index: usize) -> usize { 76 | self.tags[index * T::BITS..(index + 1) * T::BITS].load() 77 | } 78 | 79 | // **Note**: index must be in range 80 | fn set_tag(&mut self, index: usize, tag: usize) { 81 | self.tags[index * T::BITS..(index + 1) * T::BITS].store::(tag); 82 | } 83 | 84 | /// Append a new element to the end of the collection. 85 | /// 86 | /// ## Panics 87 | /// Panics if the underlying `bitvec` or `Vec` panics. 88 | /// The underlying [`Vec`](https://doc.rust-lang.org/stable/std/vec/struct.Vec.html#panics-7) 89 | /// will panic if its allocation exceeds 90 | /// `isize::MAX` bytes. The underlying `bitvec` will panic 91 | /// if the maximum tag capacity is exceeded. 92 | /// On 32-bit systems, maximum tag capacity is `0x1fff_ffff/T::BITS` tags. 93 | /// On 64-bit systems, maximum tag capacity is `0x1fff_ffff_ffff_ffff/T::BITS` tags. 94 | pub fn push(&mut self, t: T) { 95 | let pos = self.data.len(); 96 | 97 | let (tag, data) = t.cleave(); 98 | 99 | // Naively pushing seems to be faster than something like 100 | // self.tags 101 | // .extend_from_bitslice(&BitView::view_bits::(&[tag])[0..T::BITS]); 102 | for _ in 0..T::BITS { 103 | self.tags.push(false) 104 | } 105 | 106 | // https://github.com/fprasx/peapod/issues/2 107 | // We have to push the data second because pushing to 108 | // self.tags will panic if capacity is exceeded. 109 | // If this panic is caught and we already pushed a 110 | // value to self.data, but not self.tags, there 111 | // will be an untagged value on the end of self.data. 112 | // 113 | // This is still not good as we will have some cruft 114 | // on the end of self.tags, but because we always use 115 | // self.data to get self's length, and we always use 116 | // get/set instead of push/pop to modify self.tags, 117 | // the correct tag and data should still always match 118 | // up. 119 | self.data.push(data); 120 | 121 | self.set_tag(pos, tag); 122 | } 123 | 124 | /// Remove an element from the end of the collection. 125 | /// Returns `None` if the collection is empty. 126 | pub fn pop(&mut self) -> Option { 127 | let len = self.data.len(); 128 | 129 | if len == 0 { 130 | return None; 131 | } 132 | 133 | // This is safe as we checked that the length is not 0 134 | let data = self.data.pop().unwrap(); 135 | 136 | // Subtract one as len is the length of the vector before removing an element 137 | // The subtraction will not underflow as pos is guaranteed != 0 138 | let tag: usize = self.get_tag(len - 1); 139 | 140 | // Remove the last tag 141 | self.tags.truncate((len - 1) * T::BITS); 142 | 143 | // # Safety 144 | // The tag matches the data 145 | unsafe { Some(Phenotype::reknit(tag, data)) } 146 | } 147 | 148 | /// Returns the number of elements in the collection. 149 | pub fn len(&self) -> usize { 150 | self.data.len() 151 | } 152 | 153 | /// Returns the number of elements in the collection. 154 | pub fn is_empty(&self) -> bool { 155 | self.data.len() == 0 156 | } 157 | 158 | /// Returns whether the collection is empty (it contains no elements). 159 | pub fn reserve(&mut self, elements: usize) { 160 | self.data.reserve(elements); 161 | self.tags.reserve(elements * T::BITS); 162 | } 163 | 164 | /// Creates a new peapod with enough space to add `capacity` elements 165 | /// without reallocating. 166 | pub fn with_capacity(capacity: usize) -> Self { 167 | Self { 168 | tags: BitVec::with_capacity(capacity * T::BITS), 169 | data: Vec::with_capacity(capacity), 170 | } 171 | } 172 | 173 | /// Removes all elements from the collection. 174 | /// **Note**: this does not affect its allocated capacity. 175 | pub fn clear(&mut self) { 176 | self.data.clear(); 177 | self.tags.clear(); 178 | } 179 | 180 | /// Shortens the collection so it only contains the first `len` elements. 181 | /// **Note**: this does not affect its allocated capacity. 182 | pub fn truncate(&mut self, len: usize) { 183 | // https://github.com/fprasx/peapod/issues/2 184 | // len * T::BITS can overflow so we saturate at the top, 185 | // If it overflows this means len > max-capacity of the bitvec, 186 | // so it would be impossible to reach a state with that many elements. 187 | // Therefore saturating at the top won't remove anything - which is correct 188 | self.tags.truncate(usize::saturating_mul(len, T::BITS)); 189 | self.data.truncate(len); 190 | } 191 | 192 | /// Returns the number of elements the collection can hold 193 | /// without reallocating. 194 | pub fn capacity(&self) -> usize { 195 | let tag_cap = self.tags.capacity() / T::BITS; 196 | let data_cap = self.data.capacity(); 197 | cmp::min(tag_cap, data_cap) 198 | } 199 | 200 | /// Adds `other` to the end of `self`, so the new collection 201 | /// now contains all the elements of `self` followed by the elements 202 | /// of `other`. 203 | pub fn append(&mut self, other: Peapod) { 204 | self.extend(other.into_iter()); 205 | } 206 | 207 | fn cleave(self) -> (BitVec, Vec) { 208 | let levitating = ManuallyDrop::new(self); 209 | unsafe { 210 | ( 211 | // # Safety 212 | // We are reading from a reference, 213 | // we have wrapped self in ManuallyDrop to prevent a double-free 214 | ptr::read(&levitating.tags), 215 | ptr::read(&levitating.data), 216 | ) 217 | } 218 | } 219 | } 220 | 221 | impl Drop for Peapod 222 | where 223 | T: Phenotype, 224 | { 225 | fn drop(&mut self) { 226 | while self.pop().is_some() {} 227 | } 228 | } 229 | 230 | impl From> for Vec 231 | where 232 | T: Phenotype, 233 | { 234 | fn from(pp: Peapod) -> Self { 235 | pp.into_iter().collect() 236 | } 237 | } 238 | 239 | impl From> for Peapod 240 | where 241 | T: Phenotype, 242 | { 243 | fn from(v: Vec) -> Self { 244 | let mut pp = Peapod::with_capacity(v.len()); 245 | pp.extend(v.into_iter()); 246 | pp 247 | } 248 | } 249 | 250 | impl Default for Peapod 251 | where 252 | T: Phenotype, 253 | { 254 | fn default() -> Self { 255 | Self::new() 256 | } 257 | } 258 | 259 | impl IntoIterator for Peapod 260 | where 261 | T: Phenotype, 262 | { 263 | type Item = T; 264 | 265 | type IntoIter = IntoIter; 266 | 267 | fn into_iter(self) -> Self::IntoIter { 268 | let (tags, data) = self.cleave(); 269 | IntoIter { 270 | tags, 271 | data, 272 | index: 0, 273 | } 274 | } 275 | } 276 | 277 | pub struct IntoIter 278 | where 279 | T: Phenotype, 280 | { 281 | tags: BitVec, 282 | data: Vec, 283 | index: usize, 284 | } 285 | 286 | impl Iterator for IntoIter 287 | where 288 | T: Phenotype, 289 | { 290 | type Item = T; 291 | 292 | fn next(&mut self) -> Option { 293 | // Are we done iterating? 294 | if self.index == self.data.len() { 295 | None 296 | } else { 297 | // # Safety 298 | // We are reading the tag that matches the data 299 | let elem = unsafe { 300 | Some(::reknit( 301 | self.tags[self.index * T::BITS..(self.index + 1) * T::BITS].load(), 302 | // Read a value out of the vector 303 | // # Safety 304 | // We are reading from a valid ptr (as_ptr), and the offset is 305 | // in bounds as we stop iterating once index == len 306 | ptr::read(self.data.as_ptr().add(self.index)), 307 | )) 308 | }; 309 | self.index += 1; 310 | elem 311 | } 312 | } 313 | 314 | fn size_hint(&self) -> (usize, Option) { 315 | ( 316 | self.data.len() - self.index, 317 | Some(self.data.len() - self.index), 318 | ) 319 | } 320 | } 321 | 322 | impl DoubleEndedIterator for IntoIter 323 | where 324 | T: Phenotype, 325 | { 326 | fn next_back(&mut self) -> Option { 327 | let len = self.data.len(); 328 | 329 | // Are we done iterating? 330 | if self.index == len { 331 | None 332 | } else { 333 | // Reduce self.data's length by one so the last element won't be accessible, 334 | // preventing a double-free it were to be read again 335 | unsafe { 336 | self.data.set_len(len - 1); 337 | } 338 | 339 | // # Safety 340 | // The tag matches the data 341 | unsafe { 342 | Some(::reknit( 343 | self.tags[(len - 1) * T::BITS..len * T::BITS].load(), 344 | // Read a value out of the vector 345 | // # Safety 346 | // We are reading from a valid ptr (as_ptr), and the offset is 347 | // in bounds as we stop iterating once index == len 348 | ptr::read(self.data.as_ptr().add(len - 1)), 349 | )) 350 | } 351 | } 352 | } 353 | } 354 | 355 | impl ExactSizeIterator for IntoIter 356 | where 357 | T: Phenotype, 358 | { 359 | fn len(&self) -> usize { 360 | let (lower, upper) = self.size_hint(); 361 | // Note: This assertion is overly defensive, but it checks the invariant 362 | // guaranteed by the trait. If this trait were rust-internal, 363 | // we could use debug_assert!; assert_eq! will check all Rust user 364 | // implementations too. 365 | assert_eq!(upper, Some(lower)); 366 | lower 367 | } 368 | } 369 | 370 | impl Drop for IntoIter 371 | where 372 | T: Phenotype, 373 | { 374 | fn drop(&mut self) { 375 | for _ in self {} 376 | // When the drop glue for self drops self.data, 377 | // nothing get's dropped as the union fields are wrapped 378 | // in ManuallyDrop, so all the dropping happens when we 379 | // iterate over self, and we avoid a double-free 380 | } 381 | } 382 | 383 | impl Debug for Peapod 384 | where 385 | T: Phenotype, 386 | { 387 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 388 | f.debug_struct("Peapod") 389 | .field( 390 | "tags", 391 | &self 392 | .tags 393 | .chunks(T::BITS) 394 | .map(BitField::load::) 395 | .collect::>(), 396 | ) 397 | .field("data", &[..]) 398 | .finish() 399 | } 400 | } 401 | 402 | impl Display for Peapod 403 | where 404 | T: Phenotype, 405 | { 406 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 407 | f.write_str("[")?; 408 | for i in 0..self.len() { 409 | f.write_str(" ")?; 410 | f.write_str(&format!("{{ tag: {}, data: .. }}", self.get_tag(i)))?; 411 | f.write_str(",")?; 412 | } 413 | f.write_str(" ")?; 414 | f.write_str("]")?; 415 | Ok(()) 416 | } 417 | } 418 | 419 | impl Extend for Peapod 420 | where 421 | T: Phenotype, 422 | { 423 | fn extend>(&mut self, iter: A) { 424 | // If we can, reserve space ahead of time 425 | let iter = iter.into_iter(); 426 | if let (_, Some(len)) = iter.size_hint() { 427 | self.reserve(len); 428 | } else if let (len, None) = iter.size_hint() { 429 | self.reserve(len); 430 | } 431 | for elem in iter { 432 | self.push(elem); 433 | } 434 | } 435 | } 436 | 437 | impl FromIterator for Peapod 438 | where 439 | T: Phenotype, 440 | { 441 | fn from_iter>(iter: A) -> Self { 442 | let mut pp = Peapod::::new(); 443 | pp.extend(iter); 444 | pp 445 | } 446 | } 447 | 448 | impl PartialEq for Peapod 449 | where 450 | T: Phenotype, 451 | T::Value: PartialEq, 452 | { 453 | fn eq(&self, other: &Self) -> bool { 454 | self.tags == other.tags && self.data == other.data 455 | } 456 | } 457 | 458 | impl Clone for Peapod 459 | where 460 | T: Phenotype, 461 | T::Value: Clone, 462 | { 463 | fn clone(&self) -> Self { 464 | Self { 465 | tags: self.tags.clone(), 466 | data: self.data.clone(), 467 | } 468 | } 469 | } 470 | 471 | #[cfg(test)] 472 | mod tests { 473 | use super::*; 474 | use crate::peapod; 475 | use core::iter::{DoubleEndedIterator, Iterator}; 476 | use phenotype_macro::Phenotype; 477 | 478 | #[derive(Phenotype, PartialEq, Debug)] 479 | enum TestData { 480 | A { u: usize, f: f64 }, 481 | B(usize, f64), 482 | C, 483 | } 484 | 485 | #[test] 486 | fn exact_size_iterator() { 487 | let pp = peapod![ 488 | TestData::A { u: 1, f: 1.0 }, 489 | TestData::B(1, 1.0), 490 | TestData::A { u: 1, f: 1.0 }, 491 | TestData::B(1, 1.0), 492 | TestData::A { u: 1, f: 1.0 }, 493 | TestData::B(1, 1.0), 494 | TestData::A { u: 1, f: 1.0 } 495 | ]; 496 | let mut pp = pp.into_iter(); 497 | assert_eq!(pp.len(), 7); 498 | assert_eq!(pp.size_hint(), (7, Some(7))); 499 | pp.next(); 500 | assert_eq!(pp.len(), 6); 501 | assert_eq!(pp.size_hint(), (6, Some(6))); 502 | for _ in 0..10 { 503 | pp.next(); 504 | } 505 | assert_eq!(pp.len(), 0); 506 | assert_eq!(pp.size_hint(), (0, Some(0))); 507 | } 508 | 509 | #[test] 510 | fn double_ended_iterator() { 511 | let pp = peapod![ 512 | TestData::A { u: 1, f: 1.0 }, 513 | TestData::B(1, 1.0), 514 | TestData::A { u: 1, f: 1.0 }, 515 | TestData::B(1, 1.0), 516 | TestData::A { u: 1, f: 1.0 }, 517 | TestData::B(1, 1.0), 518 | TestData::A { u: 1, f: 1.0 } 519 | ]; 520 | let mut pp = pp.into_iter(); 521 | assert_eq!(pp.next_back(), Some(TestData::A { u: 1, f: 1.0 })); 522 | assert_eq!(pp.next(), Some(TestData::A { u: 1, f: 1.0 })); 523 | assert_eq!(pp.next_back(), Some(TestData::B(1, 1.0))); 524 | assert_eq!(pp.next(), Some(TestData::B(1, 1.0))); 525 | assert_eq!(pp.next_back(), Some(TestData::A { u: 1, f: 1.0 })); 526 | assert_eq!(pp.next(), Some(TestData::A { u: 1, f: 1.0 })); 527 | assert_eq!(pp.next_back(), Some(TestData::B(1, 1.0))); 528 | assert_eq!(pp.next(), None); 529 | assert_eq!(pp.next_back(), None); 530 | } 531 | 532 | #[test] 533 | fn new_is_empty() { 534 | let pp = Peapod::::new(); 535 | assert_eq!(pp.len(), 0); 536 | assert_eq!(pp.capacity(), 0); 537 | } 538 | 539 | #[test] 540 | fn push_increases_len() { 541 | let mut pp = Peapod::::new(); 542 | pp.push(TestData::A { u: 1, f: 1.0 }); 543 | pp.push(TestData::A { u: 1, f: 1.0 }); 544 | pp.push(TestData::A { u: 1, f: 1.0 }); 545 | pp.push(TestData::A { u: 1, f: 1.0 }); 546 | assert_eq!(pp.len(), 4); 547 | } 548 | 549 | #[test] 550 | fn pop_works() { 551 | let mut pp = Peapod::::new(); 552 | assert_eq!(pp.pop(), None); 553 | pp.push(TestData::A { u: 1, f: 1.0 }); 554 | assert_eq!(pp.pop(), Some(TestData::A { u: 1, f: 1.0 })); 555 | } 556 | 557 | #[test] 558 | fn clear_clears_empty() { 559 | let mut pp = Peapod::::new(); 560 | pp.clear(); 561 | assert_eq!(pp.len(), 0); 562 | assert_eq!(pp.capacity(), 0); 563 | } 564 | 565 | #[test] 566 | fn clear_clears_nonempty() { 567 | let mut pp = Peapod::::new(); 568 | pp.push(TestData::A { u: 1, f: 1.0 }); 569 | pp.push(TestData::A { u: 1, f: 1.0 }); 570 | pp.push(TestData::A { u: 1, f: 1.0 }); 571 | pp.push(TestData::A { u: 1, f: 1.0 }); 572 | pp.clear(); 573 | assert_eq!(pp.len(), 0); 574 | } 575 | } 576 | --------------------------------------------------------------------------------