Feature: Implicit Lifetimes
August 31, 2026 · View on GitHub
NORMATIVE — this is what Palladium is defined to be. It is not a description of what
pdcimplements today. What is implemented, partial, or unimplemented is recorded per specification section in the implementation status annex. Palladium blocks below are fencedno-compile: the syntax is normative, the compiler does not accept it yet, andscripts/check-docs.shcounts each fence rather than hiding it.
Feature: Implicit Lifetimes
Normative specification section: language-spec.md §N9 References and lifetimes.
Overview
Palladium infers lifetimes, so the great majority of code carries no lifetime annotation at all, with the memory-safety guarantee unchanged.
Normative syntax
| Form | Meaning |
|---|---|
ref T | A shared borrow of T. Replaces Rust's &T. |
ref mut T | A mutable borrow of T. Replaces Rust's &mut T. |
ref<'a> T | An explicitly named region, for the cases inference cannot resolve. |
There is no 'a parameter list on functions, structs or impls. A region name appears only inside
a ref<...>, and only when the compiler has asked for one.
The Palladium examples below are ILLUSTRATIVE, not normative. They use
ref strandusize, andlanguage-spec.md§N4 lists neitherstrnorusizeamong the primitives — so under the current definitionref strnames no type. A specification may leave a policy question open; it may not call an ill-typed example normative. Until N4 is resolved these blocks are demoted: read them for the shape of the feature —refin place of&, no'aparameter lists, inference failure as an error — and not as well-formed Palladium. The normative content of this document is the syntax table above and the inference rules, which mention no referent type at all.
Code Comparison
Rust (Explicit Lifetimes Required)
// Simple function - still needs lifetime
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
// Struct with references
struct Parser<'a> {
input: &'a str,
position: usize,
}
impl<'a> Parser<'a> {
fn new(input: &'a str) -> Parser<'a> {
Parser { input, position: 0 }
}
fn parse_word(&mut self) -> Option<&'a str> {
// Complex lifetime reasoning
let start = self.position;
while self.position < self.input.len() {
self.position += 1;
}
Some(&self.input[start..self.position])
}
}
// Multiple lifetimes
fn compare_and_get<'a, 'b>(x: &'a str, y: &'b str) -> &'a str
where 'b: 'a {
if x.len() > y.len() { x } else { x }
}
Go (No Lifetimes - Uses GC)
// Go doesn't track lifetimes - garbage collected
func longest(x, y string) string {
if len(x) > len(y) {
return x
}
return y
}
type Parser struct {
input string
position int
}
func NewParser(input string) *Parser {
return &Parser{input: input, position: 0}
}
func (p *Parser) ParseWord() string {
start := p.position
for p.position < len(p.input) {
p.position++
}
return p.input[start:p.position]
}
// No lifetime complexity but:
// - Runtime GC overhead
// - No compile-time guarantees
// - Potential memory leaks
Palladium (Implicit Lifetimes)
Illustrative, not normative — uses str/usize, which §N4 does not define. See the note above.
// Lifetimes inferred automatically
fn longest(x: ref str, y: ref str) -> ref str {
if x.len() > y.len() { x } else { y }
}
// Struct lifetimes inferred
struct Parser {
input: ref str,
position: usize,
}
impl Parser {
fn new(input: ref str) -> Parser {
Parser { input, position: 0 }
}
fn parse_word(ref mut self) -> Option<ref str> {
// Compiler understands lifetime flow
let start = self.position;
while self.position < self.input.len() {
self.position += 1;
}
Some(self.input[start..self.position])
}
}
// Only explicit when truly ambiguous
fn compare_and_get(x: ref str, y: ref str) -> ref str {
// Compiler infers this returns x's lifetime
if x.len() > y.len() { x } else { x }
}
Why This Feature Exists
1. Cognitive Load Reduction
Explicit lifetimes are a well-known barrier to learning Rust, and most annotations are mechanical: they restate a data-flow relationship the compiler already has in front of it. Writing them down spends the programmer's attention on bookkeeping rather than on the program.
2. Maintaining Safety
- Same memory safety guarantees as Rust
- Compiler errors when inference is ambiguous
- Can always fall back to explicit annotations
How It Works
The block below is compiler-internal pseudocode rather than a Palladium program.
Inference Algorithm
Normative syntax. pdc does not accept this today.
// Compiler's internal lifetime inference
fn infer_lifetimes(ast: AST) -> Result<LifetimeMap> {
let constraints = collect_constraints(ast);
let regions = build_region_graph(constraints);
match solve_regions(regions) {
Ok(solution) => Ok(solution),
Err(Ambiguous) => Err("Lifetime annotation required"),
}
}
Inference Rules
- Single Input: Output lifetime = input lifetime
- Multiple Inputs: Look for obvious data flow
- Self Methods: Track self's lifetime through calls
- Struct Fields: Infer from usage patterns
When Explicit Annotation Is Needed
Illustrative, not normative — uses str, which §N4 does not define.
// Ambiguous case - needs annotation
fn unclear(x: ref<'a> str, y: ref str) -> ref<'a> str {
if rand() { x } else { y } // Error: can't infer
}
Inference failing is a compile error, never a silently chosen answer. The definition is that annotations become unnecessary, not that ambiguity becomes tolerable.
Where the implementation currently diverges
Measured at commit abeb665.
1. ref is not a keyword, so the normative reference syntax does not parse.
grep -n '"ref"' src/lexer/token.rs returns nothing, and grammar.ebnf:92-93 lists ref among
the words that "lex as ordinary identifiers". Compiling
fn longest(x: ref String, y: ref String) -> ref String { return x; } gives:
error: Unexpected token: expected ')' (Expected ')'), found identifier 'String'
— ref was consumed as the parameter's type and String was then unexpected.
2. The implementation uses Rust's syntax, including the 'a parameters this design removes.
reference = '&' [ "'" identifier ] [ "mut" ] type (grammar.ebnf:208), and generic parameter
lists accept lifetimes (generic_param at grammar.ebnf:192, parsed at
src/parser/mod.rs:516). Measured: fn f<'a>(x: &'a String) -> &'a String { return x; }
compiles and links. So the annotation burden the design deletes is currently the only supported
spelling.
3. Nothing consumes the lifetimes it parses. Function.lifetime_params is populated
(src/parser/mod.rs:1295) and, outside the parser, appears only as vec![] in test and LSP
fixtures — grep -rn lifetime_params src/ --include='*.rs' | grep -v '^src/parser' returns
nothing else. There is no region inference of any kind: grep -rn 'region\|Region' src/ --include='*.rs' returns nothing.
4. References are not a type. The type checker maps Type::Reference { inner, .. } to the
inner type — "For now, treat references as the inner type / TODO: Proper reference type handling"
(src/typeck/mod.rs:744-748). &i64 and i64 are indistinguishable to it, so no lifetime
relation could be inferred even if the machinery existed.
5. What exists instead is a move/initialization discipline.
src/ownership/borrow_checker.rs tracks moves and conflicting borrows over a scope
counter (Lifetime enum at src/ownership/mod.rs:27, the counter itself at
src/ownership/mod.rs:95, and the scope a borrow was taken in at
src/ownership/mod.rs:71). Whatever its defects, that is a different mechanism, not partial
delivery of this one: a scope counter is not region inference, and no amount of fixing it produces
inferred lifetimes.
The counter is a plain u32 depth, and it is deliberately not routed through Lifetime at all:
Lifetime::Scope is a declared variant that nothing ever constructs, so while exit_scope was
written to release borrows by comparing against it, it released none and borrows accumulated for
the whole compilation. Ending a borrow at its scope is now decided by the recorded depth
(src/ownership/mod.rs:148) rather than by a lifetime value — which is the point of this
document restated from the other side: the pass has scopes, and it has never had regions.
A previous version of this paragraph asserted a live defect here — that a call argument is
borrowed as Lifetime::Named("fn") and never released, so a value cannot be passed twice. That is
false and is retracted: the defect was real, it is D6, and it was fixed in commit 191f8c1, before
this branch existed. Calls take a per-call lifetime (src/ownership/borrow_checker.rs:982) and end
its borrows when the call finishes (src/ownership/borrow_checker.rs:988). Five probes are in
language-spec.md A9.4. This
paragraph then named &mut of an immutable local as the defect that WAS live; that one is fixed
too, and re-measured it is refused for struct, array and scalar referents alike
(A9.3).
The scalar spelling &mut i64 remains broken for a different reason — it reaches gcc, which
rejects the generated C.
Design intent, not measurements
The earlier version of this document asserted that "Studies show that lifetime annotations account for 30% of Rust learning curve difficulty, 15% of compilation errors for beginners, 5-10% of code verbosity", and a "Performance Impact" of "+5-10% compilation time, zero runtime overhead, identical binary size". No study is cited, none is in this repository, and there is no implementation to have measured the compile-time figure against. All of those numbers are deleted.
The intent that survives:
- Inference is a compile-time analysis with no runtime representation, so a program's generated code is unaffected by whether a lifetime was written or inferred.
- Safety is not traded for ergonomics: the checker rejects what Rust's borrow checker rejects, and reports ambiguity rather than guessing.
Translating Rust lifetimes into Palladium
A syntax correspondence, not a usage guide: the right-hand side is target syntax that pdc
does not accept today.
Rust source
// Rust source
fn process<'a, 'b>(data: &'a mut Data, config: &'b Config) -> &'a str {
data.process_with_config(config)
}
Illustrative, not normative — uses str, which §N4 does not define.
// Palladium equivalent (target syntax)
fn process(data: ref mut Data, config: ref Config) -> ref str {
data.process_with_config(config)
}
Future Improvements
- Better Error Messages: Show why inference failed
- Inference Hints: Guide compiler with attributes
- Cross-Function Inference: Infer across module boundaries
- IDE Support: Show inferred lifetimes on hover
Related
- Palladium v1.0 feature definition — where this sits among the rest
- Async as effect
- Totality checking
- Feature index
- Language specification