1# Editions: Life of a FeatureSet 2 3**Author:** [@mkruskal-google](https://github.com/mkruskal-google) 4 5**Approved:** 2023-08-17 6 7## Background 8 9Outside of some minor spelling tweaks, our current implementation of features 10has very closely followed the original design laid out in 11[Protobuf Editions Design: Features](protobuf-editions-design-features.md). This 12approach led to the creation of four different feature sets for each descriptor 13though, and it's left under-specified who is responsible for generating these 14(protoc, plugins, runtimes), who has access to them, and where they need to be 15propagated to. 16 17*Exposing Editions Feature Sets* (not available externally) was a first attempt 18to try to define some of these concepts. It locks down feature visibility to 19protoc, generators, and runtimes. Users will only be exposed to them indirectly, 20via codegen changes or runtime helper functions, in order to avoid Hyrum's law 21cementing every decision we make about them. We (incorrectly) assumed that the 22protoc frontend would be able to calculate all the feature sets and then 23propagate all four sets to the generators, who would then forward the fully 24resolved runtime features to the runtime. This had the added benefit that we 25could treat our C++ feature resolution logic as a source-of-truth and didn't 26have to reimplement it identically in every language we support. 27 28*Editions: Runtime Feature Set Defaults* (not available externally) was a 29follow-up attempt to specifically handle the default feature sets of an edition. 30We had realized that we would need proto2/proto3 default features in each 31language to safely roll out editions, and that languages supporting descriptor 32pools would have cases that bypass protoc entirely. The solution we arrived at 33was that we should continue using the protoc frontend as the source-of-truth, 34and propagate these defaults down to the necessary runtimes. This would fix the 35proto2/proto3 issue, and at least provide some utilities to make the situation 36easier for descriptor pool users. 37 38[Protobuf Editions Design: Features](protobuf-editions-design-features.md) 39defines the feature resolution algorithm, which can be summarized by the 40following diagram: 41 42 43 44Feature resolution for a given descriptor starts by using the proto file's 45edition and the feature schemas to generate the default feature set. It then 46merges all of the parent features from top to bottom, merging the descriptor's 47features last. 48 49## Glossary 50 51We will be discussing features **a lot** in this document, but the meaning 52behind the word can vary in some subtle ways depending on context. Whenever it's 53ambiguous, we will stick to qualifying these according to the following 54definitions: 55 56* **Global features** - The features contained directly in `FeatureSet` as 57 fields. These apply to the protobuf language itself, rather than any 58 particular runtime or generator. 59 60* **Generator features** - Extensions of `FeatureSet` owned by a specific 61 runtime or generator. 62 63* **Feature resolution** - The process of applying the algorithm laid out in 64 [Protobuf Editions Design: Features](protobuf-editions-design-features.md). 65 This means that edition defaults, parent features, and overrides have all 66 been merged together. After resolution, every feature should have an 67 explicit value. 68 69 * **Unresolved features** - The features a user has explicitly set on 70 their descriptors in the `.proto` file. These have not gone through 71 feature resolution and are a minimal representation that require more 72 knowledge to be useful. 73 74 * **Resolved features** - Features that have gone through feature 75 resolution, with defaults and inheritance applied. These are the only 76 feature sets that should be used to make decisions. 77 78* **Option Retention** - We support a retention specification on all options 79 (see 80 [here](https://protobuf.dev/programming-guides/proto3#option-retention)), 81 including features 82 83 * **Source features** - The features available to protoc and generators, 84 before option retention has been applied. These can be either resolved 85 or unresolved. 86 87 * **Runtime features** - The features available to runtimes after option 88 retention has been applied. These can be either resolved or unresolved. 89 90## Problem Description 91 92The flaw that all of these design documents suffer from is that protoc **can't** 93be the universal source-of-truth for feature resolution under the original 94design. For global features, there's of course no issue (protoc has a 95bootstrapping setup for `descriptor.proto`` and always knows the global feature 96set). For generator features though, we depend on [imports to make them 97discoverable](protobuf-editions-design-features.md#specification-of-an-edition). 98 99If a user is actually overriding one of these features, there will necessarily 100be an import and therefore protoc will be able to discover generator features 101and handle resolution. However, if the user is ok with the edition defaults 102there's no need for an import. Without the import, protoc has **no way of 103knowing** that those generator features exist in general. We could hardcode the 104ones we own, but that just pushes the problem off to third-party plugins. We 105could also force proto owners to include imports for *every* (transitive) 106language they generate code to, even if they're unused, but that would be very 107disruptive and isn't practical or idiomatic. 108 109Pushing the source-of-truth to the generators makes things a little better, 110since they each know exactly what feature file needs to be included. There's no 111longer any knowledge gap, and we don't need to rely on imports to discover the 112feature extension. Additionally, many of our generators are written in C++ (even 113non-built-in plugins), so we could at least reuse our existing feature 114resolution utility for all of those and limit the amount of duplication 115necessary. However, there's still a code-size issue with this approach. As 116described in the previous documents, we would need to send four feature sets for 117**every** descriptor to the runtime (i.e. in the generator request and embedded 118as a serialized string). We wouldn't be able to use inheritance or references to 119minimize the cost, and every generator that embeds a `FileDescriptorProto` into 120its gencode would see a massive code-size increase. 121 122There's also still the issue of descriptor pools that need to be able to build 123descriptors at runtime. These are typically power users (and our own unit-tests) 124doing very atypical things and bypassing protoc entirely. In previous documents 125we've attempted to push some of the cost onto them by explicitly not giving them 126feature resolution. They would have to specify every feature on every 127descriptor, and would not be able to use edition defaults or inheritance. 128However, this cost is fairly high and it also makes the `edition` field 129meaningless. Any missing feature would be a runtime error, and there would be no 130concept of "edition". This creates an inconsistent experience for developers, 131where they think in terms of editions in one context and then throw it out in 132another. Also, it would mean that we have two distinct ways of specifying a 133`FileDescriptorProto``: with unresolved features meant to only go through 134protoc, and with fully resolved features meant to always bypass protoc. 135Round-tripping descriptors would become difficult or impossible. 136 137The following image attempts illustrates the issue: 138 139 140 141Here, a proto file is used in both A and B runtimes. The schema itself only 142overrides features for A though, and doesn't declare an import on B's features. 143This means that protoc doesn't know about B's features, and Generator B will 144need to resolve them. Additionally, dynamic messages in both A and B runtimes 145have issues because they've bypassed protoc and don't have any way to follow the 146feature resolution spec. 147 148### Requirements 149 150The following minimal feature sets are required by protoc: 151 152* **Resolved global source features** - to make proto-level decisions 153* **Unresolved global source features** - for validation 154 155For each generator: 156 157* **Resolved generator source features** - to make language-specific codegen 158 decisions 159* **Unresolved generator source features** - for validation 160* **Resolved global source features** - to make more complex decisions 161 162For each runtime: 163 164* **All resolved runtime features** - for making runtime decisions 165* **All unresolved runtime features** - for round-trip behavior and debugging 166 167With some additional requirements on an ideal solution: 168 169* **Minimal code-size costs** - code size bloat can easily block the rollout 170 of editions, and once those limits are hit we don't have great solutions 171 172* **Minimal performance costs** - we want a solution that avoids any 173 unnecessary CPU or RAM regressions 174 175* **Minimal code duplication** - obviously we want to minimize this, but where 176 we can't, we need a suitable test strategy to keep the duplication in sync 177 178* **Runtime support for dynamic messages** - while dynamic messages are a 179 less-frequently-used feature, they are a critical feature used by a lot of 180 important systems. Our solution should avoid making them harder to use in 181 any runtime that supports them. 182 183## Recommended Solution 184 185Our long-term recommendation here is to support and use feature resolution in 186every stage in the life of a FeatureSet. Every runtime, generator, and protoc 187itself will all handle feature resolution independently, only sharing unresolved 188features between each other. This will necessarily mean duplication across 189nearly every language we support, and the following sections will go into detail 190about strategies for managing this. 191 192The main justification for this duplication is the simple fact that *edition 193defaults* will be needed almost everywhere. The generators need defaults for 194*their* features to get fully resolved generator features to make decisions on, 195and can't get them from protoc in every case. The runtimes need defaults for 196both global and generator features in order to honor editions in dynamic 197messages and to keep RAM costs down (e.g. the absence of feature overrides 198should result in a reference to some shared default object). Since the 199calculation of edition defaults is by far the most complicated piece of feature 200resolution, with the remainder just being proto merges, it makes everything 201simpler to understand if we just duplicate the entire algorithm. 202 203#### Pros 204 205* Resolved feature sets will never be publicly exposed 206 207 * Our APIs will be significantly simpler, cutting the number of different 208 types of feature sets by a factor of 2 209 210 * There will be no ambiguity about what a `FeatureSet` object *means*. It 211 will always either be unresolved (outside of protobuf code) or fully 212 resolved on all accessible features (inside protobuf code). 213 214 * RAM and code-size costs will be minimal, since we'll only be storing and 215 propagating the minimal amount of information (unresolved features) 216 217 * Combats Hyrum's law by allowing us to provide wrappers around resolved 218 features everywhere, instead of letting people depend on them directly 219 220* **Minimal** duplication on top of what's already necessary (edition 221 defaults). 222 223* Dynamic messages will be treated on equal footing to proto files 224 225* The necessary feature dependencies will always be available in the 226 appropriate context 227 228* We can simplify the current implementation since protoc won't need to handle 229 resolution of imported features. 230 231#### Cons 232 233* Requires duplication of feature resolution in every runtime and every unique 234 generator language 235 236 * This means building out additional infrastructure to enforce 237 cross-language conformance 238 239### Runtimes Without Reflection 240 241There are various runtimes that do not support reflection or dynamic messages at 242all (e.g. Java lite, ObjC). They typically embed the "feature-like" information 243they need directly into custom objects in the gencode. In these cases, the 244problem becomes a lot simpler because they *don't need* the full FeatureSet 245objects. We **don't** need to duplicate feature resolution in the runtime, and 246the generator can just directly embed the fully resolved features values needed 247by the runtime (of course, the generator might still need duplicate logic to get 248those). 249 250### Staged Rollout for Dynamic Messages 251 252Long-term, we want to be able to handle feature resolution at run-time for any 253runtime that supports reflection (and therefore needs FeatureSet objects) to 254reduce code-size/RAM costs and support dynamic messages. However, in any 255language where these costs are less critical, a staged rollout could be 256appropriate. Here, the generator would embed the serialized resolved source 257features into the gencode along with the rest of the options. We would use the 258`raw_features` field (which should eventually be deleted) to also include the 259unresolved features for reflection. 260 261This would allow us to implement and test editions, and unblock the migration of 262all non-dynamic cases. A follow-up optimization at a later stage could push this 263down the runtime, and only embed unresolved features in the gencode. 264 265Under this scenario, dynamic messages could still allow editions, as long as 266fully-resolved features were provided on every descriptor. When we do implement 267feature resolution, it will just be a matter of deleting redundant/unnecessary 268features, but there should always be a valid transformation from fully-resolved 269features to unresolved ones. 270 271### C++ Generators 272 273Generators written in C++ are in a better position since they don't require any 274code duplication. They could be given visibility to our existing feature 275resolution utility to resolve the features themselves. However, a better 276alternative is to make improvements to this utility so that some helpers like 277the ones we proposed in *Exposing Editions Feature Sets* can be used to access 278the resolved features that *already exist*. 279 280Protoc works by first parsing the input protofiles and building them into a 281descriptor pool. This is the frontend pass, where only the global features are 282needed. For built-in languages, the resulting descriptors are passed directly to 283the generator for codegen. For plugins, they're serialized into descriptor 284protos, rebuilt in a new descriptor pool (in the generator process), and then 285sent to the generator code for codegen. In both of these cases, a 286`DescriptorPool` build of the protos is done from a binary that *necessarily* 287links in the relevant generator features. 288 289Today, we discover features in the pool which are imported by the protos being 290built. This has the hole we mentioned above where non-imported features can't be 291discovered. Instead, we will pivot to a more explicit strategy for discovering 292features. By default, `DescriptorPool` will only resolve the global features and 293the C++ features (since this is the C++ runtime). A new method will be added to 294`DescriptorPool` that allows new feature sets to replace the C++ features for 295feature resolution. Generators will register their features via a virtual method 296in `CodeGenerator` and the generator's pool build will take those into account 297during feature resolution. 298 299There are a few ways to actually define this registration, which we'll leave as 300implementation details. Some examples that we're considering include: 301 302* Have the generator provide its own `DescriptorPool` containing the relevant 303 feature sets 304* Have the generator provide a mapping of edition -> default `FeatureSet` 305 objects 306 307Expanding on previous designs, we will provide the following API to C++ 308generators via the `CodeGenerator` class: 309 310They will have access to all the fully-resolved feature sets of any descriptor 311for making codegen decisions, and they will have access to their own unresolved 312generator features for validation. The `FileDescriptor::CopyTo` method will 313continue to output unresolved runtime features, which will become unresolved 314source features after option retention stripping (which generators should 315already be doing), for embedding in the gencode for runtime use. 316 317#### Example 318 319As an example, let's look at some hypothetical language `lang` and how it would 320introduce its own features. First, if it needs features at runtime it would 321create a `lang_features.proto` file in its runtime directory and bootstrap the 322gencode the same as it does for `descriptor.proto`. It would then *also* 323bootstrap C++ gencode using a special C++-only build of protoc. This can be 324illustrated in the following diagram: 325 326 327 328This illustrates the bootstrapping setup for a built-in C++ generator. If 329generator features weren't needed in the runtime, that red box would disappear. 330If this were a separate plugin, the "plugin" box would simply be moved out of 331`protoc` and `protoc` could also serve as `protoc_cpp`. 332 333If `lang` didn't need runtime features, we would simply put the features proto 334in the `lang` generator and only generate C++ code (using the same bootstrapping 335technique as above). 336 337After the generator registers `lang_features.proto` with the DescriptorPool, the 338`FeatureSet` objects returned by `GetFeatures` will always have fully resolved 339`lang` features. 340 341### Non-C++ Generators 342 343As we've shown above, non-C++ generators are already in a situation where they'd 344need to duplicate *some* of the feature resolution logic. With this solution, 345they'd need to duplicate much more of it. The `GeneratorRequest` from protoc 346will provide the full set of *unresolved* features, which they will need to 347resolve and apply retention stripping to. 348 349**Note:** If we're able to implement bidirectional plugin communication, the 350[Bidirectional Plugins](#bidirectional-plugins) alternative may be a simpler 351solution for non-C++ generators that *don't* need features at runtime. Ones that 352need it at runtime will need to reimplement feature resolution anyway, so it may 353be less useful. 354 355One of the trickier pieces of the resolution logic is the calculation of edition 356defaults, which requires a lot of reflection. One of the ideas mentioned above 357in [C++ Generators](#c++-generators) could actually be repurposed to avoid 358duplication of this in non-C++ generators as well. The basic idea is that we 359start by defining a proto: 360 361``` 362message EditionFeatureDefaults { 363 message FeatureDefaults { 364 string edition = 1; 365 FeatureSet defaults = 2; 366 } 367 repeated FeatureDefaults defaults = 1; 368 string minimum_edition = 2; 369 string maximum_edition = 3; 370} 371``` 372 373This can be filled from any feature set extension to provide a much more usable 374specification of defaults. We can package a genrule that converts from feature 375protos to a serialized `EditionFeatureDefaults` string, and embed this anywhere 376we want. Both C++ and non-C++ generators/runtimes could embed this into their 377code. Once this is known, feature resolution becomes a lot simpler. The hardest 378part is creating a comparator for edition strings. After that, it's a simple 379search for the lower bound in the defaults, followed by some proto merges. 380 381### Bootstrapping 382 383One major complication we're likely to hit revolves around our bootstrapping of 384`descriptor.proto`. In languages that have dynamic messages, one codegen 385strategy is to embed the `FileDescriptorProto` of the file and then parse and 386build it at the beginning of runtime. For `descriptor.proto` in particular, 387handling options can be very challenging. For example, in Python, we 388intentionally strip all options from this file and then assume that the options 389descriptors always exist during build (in the presence of serialized options). 390Since features *are* options, this poses a challenge that's likely to vary 391language by language. 392 393We will likely need to special-case `descriptor.proto` in a number of ways. 394Notably, this file will **never** have any generator feature overrides, since it 395can't import those files. In every other case, we can safely assume that 396generator features exist in a fully resolved feature set. But for 397`descriptor.proto`, at least at the time it's first being built by the runtime, 398this extension won't be present. We also can't figure out edition defaults at 399that point since we don't have the generator features proto to reflect over. 400 401One possible solution would be to codegen extra information specifically for 402this bootstrapped proto, similar to what we suggested in *Editions: Runtime 403Feature Set Defaults* for edition defaults. That would allow the generator to 404provide enough information to build `descriptor.proto` during runtime. As long 405as these special cases are limited to `descriptor.proto` though, it can be left 406to a more isolated language-specific discussion. 407 408### Conformance Testing 409 410Code duplication means that we need a test strategy for making sure everyone 411stays conformant. We will need to implement a conformance testing framework for 412validating that all the different implementations of feature resolution agree. 413Our current conformance tests provide a good model for accomplishing this, even 414though they don't quite fit the problem (they're designed for 415parsing/serialization). There's a runner binary that can be hooked up to another 416binary built in any language. It sends a `ConformanceRequest` proto with a 417serialized payload and set of instructions, and then receives a 418`ConformanceResponse` with the result. In the runner, we just loop over a number 419of fixed test suites to validate that the supplied binary is conformant. 420 421We would want a similar setup here for language-agnostic testing. While we could 422write a highly focused framework just for feature resolution, a more general 423approach may set us up better in the future (e.g. option retention isn't 424duplicated now but could have been implemented that way). This will allow us to 425test any kind of transformation to descriptor protos, such as: proto3_optional, 426group/DELIMITED, required/LEGACY_REQUIRED. The following request/response protos 427describe the API: 428 429``` 430message DescriptorConformanceRequest { 431 // The file under test, pre-transformation. 432 FileDescriptorProto file = 1; 433 434 // The pool of dependencies and feature files required for build. 435 FileDescriptorSet dependencies = 2; 436} 437 438message DescriptorConformanceResponse { 439 // The transformed file. 440 FileDescriptorProto file = 1; 441 442 // Any additional features added during build. 443 FileDescriptorSet added_features = 2; 444} 445``` 446 447Each test point would construct a proto file, its dependencies, and any feature 448files to include in feature resolution. The conformance binary would use this to 449fully decorate the proto file with resolved features, and send the result back 450for comparison against our C++ source-of-truth. Any generator features added by 451the binary will also need to be sent back to get matching results. 452 453### Documentation 454 455Because we're now asking third-party generator owners to handle feature 456resolution on their own, we will need to document this. Specifically, we need to 457open-source documentation for: 458 459* The algorithm described in 460 [Protobuf Editions Design: Features](protobuf-editions-design-features.md) 461* The conformance test framework and how to use it (once it's implemented) 462 463On the other hand, we will have significantly less documentation to write about 464which feature sets to use where. Descriptor protos will *always* contain 465unresolved features, and C++ generators will have a simple API for getting the 466fully-resolved features. 467 468## Considered Alternatives 469 470### Use Generated Pool for C++ Generators 471 472*Note: this was part of the original proposal, but has been refactored (see 473cons)* 474 475Generators written in C++ are in a better position since they don't require any 476code duplication. They could be given visibility to our existing feature 477resolution utility to resolve the features themselves. However, a better 478alternative is to make improvements to this utility so that some helpers like 479the ones we proposed in *Exposing Editions Feature Sets* can be used to access 480the resolved features that *already exist*. 481 482Protoc works by first parsing the input protofiles and building them into a 483descriptor pool. This is the frontend pass, where only the global features are 484needed. For built-in languages, the resulting descriptors are passed directly to 485the generator for codegen. For plugins, they're serialized into descriptor 486protos, rebuilt in a new descriptor pool (in the generator process), and then 487sent to the generator code for codegen. In both of these cases, a 488`DescriptorPool` build of the protos is done from a binary that *necessarily* 489links in the relevant generator features. 490 491However, the FeatureSets we supply to generators are transformed to the 492generated pool (i.e. `FeatureSet` objects rather than `Message`) where the 493generator features will always exist. We've decided that there's no longer any 494reason to scrape the imports for features, but we *could* scrape the generated 495pool for them. This essentially means that when you call `MergeFeatures` to get 496a `FeatureSet`, the returned set is fully resolved *with respect to the current 497generated pool*. This is a much clearer contract, and has the benefit that the 498features visible to every C++ generator would automatically be populated with 499the correct generator features for them to use. 500 501Expanding on previous designs, we will provide the following API to C++ 502generators via the `CodeGenerator` class: 503 504They will have access to all the fully-resolved feature set of any descriptor 505for making codegen decisions, and they will have access to their own unresolved 506generator features for validation. The `FileDescriptor::CopyTo` method will 507continue to output unresolved runtime features, which will become unresolved 508source features after option retention stripping (which generators should 509already be doing), for embedding in the gencode for runtime use. 510 511#### Pros 512 513* Automatic inclusion of any features used in a binary 514* Features will never be partially resolved 515 516#### Cons 517 518* Implicit action at a distance could cause unexpected behaviors 519* Uses globals, making testing awkward 520* Not friendly to `DescriptorPool` cases who wouldn't necessarily want every 521 linked-in feature to go through feature resolution. 522 523### Default Placeholders 524 525Protoc continues to propagate and resolve core features and imported language 526level features. For language level features that protoc does not know about 527(that is, not imported), a core placeholder feature indicating that the default 528for a given edition should be respected can be propagated. 529 530``` 531message FeatureSet { 532 optional string unknown_feature_edition_default = N; // e.g. 2023 533} 534``` 535 536Instead of duplicating the entire feature resolution algorithm, plugins must 537only provide a utility mapping editions to their default FeatureSet using the 538generator feature files and optionally caching them. 539 540For example: 541 542``` 543if features.hasUtf8Validation(): 544 return features.getUtf8Validation() 545else: 546 default_features = getDefaultFeatures(features.getUnknownFeatureEditionDefault()) 547 return default_features.getUtf8Validation() 548``` 549 550#### Pros 551 552* Less duplicate logic for propagating features 553 554#### Cons 555 556* Descriptor proto bloat that is technically redundant with 557 `FileDescriptorProto` edition. 558* Confusing that some but not all features are fully resolved 559* Duplicated logic to resolve edition default from edition # 560* Code-size and memory costs associated with the original approach still exist 561* Still doesn't help with the descriptor pool case, which may require 562 duplicate logic. 563 564### Bidirectional Plugins 565 566Since the generators know the features they care about, we could have some kind 567of bidirectional communication between protoc and the plugins. The plugin would 568start by telling protoc the features it wants added, and then protoc would be 569able to fully resolve all feature sets before sending them off. This has the 570added benefit that it would allow us to do more interesting enhancements in the 571future. For example, the plugin could send its minimum required edition and 572other requirements *before* actually starting the build. 573 574**Note:** Bidirectional plugins could still be implemented for other purposes. 575This "alternative" is specifically for *using* that communication to pass 576missing feature specs. 577 578#### Pros 579 580* Eliminates code duplication problem 581* Provides infrastructure to enable future enhancements 582 583#### Cons 584 585* Doesn't address the confusing API we have now where it's unclear what kind 586 of features are contained in the `features` field 587* Doesn't address the code-size and memory costs during runtime 588* Doesn't address the descriptor pool case 589 590### Central Feature Registry 591 592Instead of relying on generators and imports to supply feature specs, we could 593pivot to a central registry of all known features. Instead of simply claiming an 594extension number, generator owners could be required to submit all the feature 595protos to a central repository of feature protos. This would give protoc access 596to **all** features. There would be two ways to implement this: 597 598* If it were built *into* protoc, we could avoid requiring any import 599 statements. We would probably still want an extension point to avoid adding 600 a dependency to `descriptor.proto`, but instead of `features.(pb.cpp)` they 601 would be something more like `features.(pb).cpp`. 602 603* We could keep the current extension and import scheme. Proto files would 604 still need to import the features they override, but protoc would depend on 605 all of them and populate defaults for unspecified ones. 606 607#### Pros 608 609* Makes all features easily discoverable wherever they're needed 610* Eliminates the code duplication problem 611* Gives us an option to remove the import statements, which are likely to 612 cause future headaches (in the edition zero LSC, in maintenance afterward, 613 and also for proto files that need to support a lot of third-party 614 runtimes). 615 616#### Cons 617 618* Doesn't address the code-size and memory costs 619* Creates version skew problems 620* Confusing ownership semantics 621 622### Do Nothing 623 624Doing nothing would basically mean abandoning editions. The current design 625doesn't (and can't) work for third party generators. They'd be left to duplicate 626the logic themselves with no guidance or support from us. We would also see 627code-size and RAM bloat (except in C++) that would be very difficult to resolve. 628 629#### Pros 630 631* Less work 632 633#### Cons 634 635* Worse in every other way 636