Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Package language editions

Status: Not implemented. Exploratory candidate under B17, not an implementation commitment. Plenty currently has one language mode; its manifest rejects edition. The package reference describes implemented behavior. All edition names, settings and examples below are hypothetical.

Assessment

Plenty could plausibly support packages written in different language editions in one compilation. The existing package-aware module loader is a useful foundation. A narrow mechanism for syntax and name-resolution changes looks like moderate compiler/tooling work, with little reason for extra runtime cost. It is not just a manifest field, and it is not a cheap way to preserve arbitrary historical language behavior.

Recommendation: keep this option open, but wait for a concrete incompatible change before implementing it. Prefer additive features and contextual keywords where they solve the problem. If an edition becomes worthwhile, require the first change to demonstrate mixed-edition interoperability using one type, ownership and runtime model. Do not promise a release cadence or permanent support policy before understanding that maintenance cost.

What editions buy

An application should be able to adopt a new spelling or resolution rule without waiting for every dependency to migrate. Conversely, an older application should be able to use a newer-edition library, provided its compiler supports both. The application selects its own edition, not the edition of the whole graph.

Rust uses opt-in editions for otherwise incompatible language changes. Crates from different editions interoperate and lower to a common internal representation. That constrains which changes can be edition-specific; it does not require a separate runtime per edition. See the Rust Edition Guide.

For Plenty, distinguish four independent settings:

SettingMeaning
Package versionVersion of the library/application and its API
plenty-versionMinimum compiler version needed to build the package
manifest-versionFormat of the package metadata
Proposed editionRules used to interpret this package’s source

A new compiler can support old and new editions. An old edition may still use new additive features, and an old compiler need not understand a new edition. Editions do not pin compiler bugs, standard-library releases, dependency versions or native ABI compatibility. They would not make a new compiler link arbitrary objects emitted by old compilers: the initial contract is compilation from source with one supported toolchain and runtime.

A package author’s experience

Use illustrative names e1 and e2, not proposed release dates:

# app/plenty.toml
manifest-version = 1

[package]
name = "app"
version = "0.1.0"
edition = "e2"

[dependencies]
legacy = { path = "../legacy", version = "^0.1.0" }

The legacy package independently declares edition = "e1". All its modules, library targets, executable targets and tests use that edition; importing it does not reinterpret its source as e2. Workspace members choose independently. Start without per-file or per-target edition overrides.

For a deliberately hypothetical change, suppose e2 reserves the word memo while e1 permits it as a local name:

# legacy/src/lib.plenty — e1
pub def answer() -> int:
    memo = 42
    return memo

# app/src/main.plenty — e2
import legacy

def main() -> Result[(), IoError]:
    print(legacy.answer())?
    Ok(())

The application’s call works; only writing a local named memo in the e2 application is rejected. Migrating legacy could rename its local variable, then change its manifest. No caller needs to change its edition.

Public names make even this example harder: an e1 library might export a function named memo. Before reserving it, Plenty would need a way for e2 callers to name that existing API, such as contextual member-name parsing or escaped identifiers. Neither spelling is selected here. Renaming a public API is not a harmless automatic migration. A new keyword that makes old exports inaccessible fails the interoperability requirement.

Boundaries that keep this manageable

Candidate changeEdition suitability
New reserved spelling, old syntax translated to existing operationsPromising, with public-name access and migration rules
Different import lookup or implicit namesPossible, if resolved in the defining package before combination
New opt-in feature that accepts all existing codeUsually no edition needed
Changed type identity, value layout, calling convention or allocator contractOutside the narrow design; needs a shared representation or a separate interoperability design
Weaker historical borrow/safety checks, different destruction or concurrency guaranteesDo not preserve through this mechanism; cross-package safety obligations must still agree
Removing a standard-library API or changing its result/error typeOrdinary API compatibility problem; an edition label alone does not solve it

Every package must expose types and callable contracts that the other editions understand. Protocol satisfaction, borrows, generic arguments and errors must continue to compose. A generic function’s body must retain its defining package’s interpretation when specialized for a caller from another edition; the caller must not change how that body resolves names. Prefer resolving and normalizing edition differences before shared checking and specialization.

Safety fixes apply to every edition even if previously accepted code stops compiling. More general compatibility exceptions need an explicit policy; “old edition” cannot mean “all past compiler behavior is frozen”.

Embedded std should use the edition selected by the compiler distribution, not the importing package’s edition. Its exported contracts remain usable from every supported edition. Generated test harnesses and interface source also need an explicit interpretation; never inherit an unrelated caller’s setting.

Selection and tooling

If adopted, missing edition metadata should mean a fixed baseline, never “whatever is newest today”. New-package commands should write the latest supported edition explicitly. Reject unknown editions with the package name and supported values; do not silently choose a nearby version. Validate the minimum compiler requirement independently.

Standalone compilation needs an equivalent explicit option, provisionally plentyc --edition e2 FILE, with the same fixed baseline when absent. A package graph supplies editions per package; reject a conflicting global override instead of applying it to dependencies. Compiler library APIs, the language server and documentation checks must use the same selection rules.

The proposed single-file script header could carry [package].edition. It should share the package schema and default rather than invent another source directive. An edition does not require a script runner.

Resolution continues to choose package sources/versions. Edition is metadata of the selected package, not a request to compile that dependency in multiple editions. A lockfile is not the authority for source interpretation. Generated package graphs must preserve the edition, and build reuse must be invalidated when it changes, including when no source bytes change.

Fit with the current implementation

This is a source inspection at local main 2d6998b4, not an implemented prototype or a measured delivery estimate:

AreaExisting foundation and required work
src/project/manifest.rsStrict keys and minimum compiler metadata exist; tests explicitly reject edition. Add validated metadata, defaults and generator support.
src/project.rsSourcePackage/SourceGraph already carry package identity, roots and dependency edges. Carry edition through resolution, graph serialization and synthetic test packages. Version the graph format so older readers cannot silently drop semantics; preserve the old format as baseline input if supported.
src/frontend/modules.rsLoader::file already receives a package index. parse/parse_buffered create a lexer/parser without an edition. Pass the owning edition through both buffered parsing paths and retain it wherever interpretation remains deferred.
Lexer, parser and checkerGate only the intended source rule and normalize it; audit generic specialization, interfaces and source-only entry points for caller-dependent interpretation.
src/project/fingerprint.rs and compiler cachesTarget fingerprints cover sources, selected graph fields and compiler identity, but currently have no edition. Include effective editions in relevant build/cache inputs and version affected formats.
CLI, IDE and test toolingAgree on package discovery, explicit standalone selection, generated code and diagnostics. Migration tooling is additional work, not supplied by storing the field.

Module loading currently deduplicates by ModuleSource. If one physical file can be assigned to different package contexts, implementation must either reject conflicting ownership/editions or include that context in module identity. Taking whichever edition happened to load first would be incorrect.

The inexpensive part is plumbing a value to the parser. The larger recurring cost is maintaining old rules and proving that their combinations still work. A syntax-only first edition should not need a second backend, runtime or dependency resolver. Rules that spread throughout checking or require adapters at package boundaries are a reason to reconsider the change, not evidence that editions in general are easy.

Evidence required before adoption

An implementation experiment should use a real proposed incompatible rule and prove old-to-new and new-to-old calls, including a diamond graph. It should cover public names that collide with new keywords, shared class/enum values, protocol satisfaction, borrows and cross-package generic specialization. Merely accepting two manifest values does not establish mixed-edition support.

Check omitted/unknown editions, independent workspace members, test harnesses, embedded std, interfaces, direct compiler/API use and IDE diagnostics. Warm builds must invalidate after changing only a root or dependency edition; graph round trips must retain it. Run existing baseline tests unchanged and measure compile-time/cache effects before claiming negligible cost.

Migration should first make source valid in both editions, then change the manifest and rerun checks/tests. Rust’s migration workflow uses compatibility diagnostics and verified edits; it also documents limits for generated code and configurations. Plenty could begin with diagnostics and a short manual migration guide for a small change. Automated public-API rewrites or claims of complete migration coverage need substantially more evidence.

Before committing, choose the first change, edition naming, baseline/default policy, supported-edition lifetime, and public-name escape policy. With no pressing incompatible change, retaining one language mode is the simpler choice. Scheduling and priority remain solely in the backlog.