RFC 026: User-Defined Trait Bridges¶
- Status: Draft
- Author: Danny Meijer (@dannymeijer)
- Issue: #152
- RFC PR: -
- Created: 2026-02-19
- Related:
Summary¶
During implementation of RFC 023 (stdlib web module), the stdlib work necessitated us to implement a trait bridge system
that automatically generates Rust trait implementations for newtypes wrapping external types
(e.g., Query[T] wrapping Axum's AxumQuery<T> gets automatic FromRequestParts delegation).
This system is currently hardcoded for stdlib patterns only. This RFC proposes exposing trait bridges to user code via
a new @rust.delegate decorator, enabling users to wrap arbitrary external Rust types (sqlx, reqwest, diesel, etc.)
with automatic trait delegation.
Scope / Non-Goals¶
This RFC covers:
- Trait delegation for newtypes wrapping external Rust types
- Method subsetting and renaming via decorator parameters
- Associated type specification for traits that require them
- Multi-trait delegation with collision detection
- Automatic async trait handling via rust-analyzer introspection
- Capability parity between stdlib and user code: delegations used by stdlib modules must be expressible through
the same
@rust.delegatemechanism available to users
This RFC does NOT cover:
- Manual
implblocks - Writing full trait implementations with custom logic (see Future Extensions) - Custom delegation logic - Delegation is pure forwarding; no custom code in delegated methods
- Implementing unimplemented traits -
rust.delegatecan only delegate traits the wrapped type already implements - Auto traits -
Send,Sync,Unpinare inferred by Rust automatically (see Limitations: Auto Traits) - Conditional delegation - No
#[cfg]or type-parameter-based conditional delegation - Cross-language bridges - Only Rust trait delegation (no C FFI, Python protocols, etc.)
- Runtime trait objects - Delegation is compile-time only; no
dyn Traitboxing - Permanent stdlib-only delegation paths - hardcoded bridges are treated as migration scaffolding, not long-term architecture
This RFC establishes the decorator surface and delegation semantics. Implementation details may evolve, but the user-facing contract (decorator parameters, error messages, generated behavior) is the specification boundary.
Motivation¶
What is a Trait Bridge?¶
A trait bridge solves the newtype transparency problem: when you wrap an external Rust type in a newtype, you lose
access to its traits. Without trait bridges, Query[T] would just be an opaque wrapper—you couldn't use it as an Axum
extractor because the FromRequestParts trait is implemented on AxumQuery<T>, not Query[T].
The trait bridge system generates delegation code automatically. For example, for axum::Query[T]:
// Pseudo-Rust: simplified for illustration (actual codegen would include async_trait desugaring)
impl<T, S> FromRequestParts<S> for Query<T>
where
T: DeserializeOwned,
S: Send + Sync,
AxumQuery<T>: FromRequestParts<S>,
{
type Rejection = <AxumQuery<T> as FromRequestParts<S>>::Rejection;
async fn from_request_parts(
parts: &mut Parts,
state: &S,
) -> Result<Self, Self::Rejection> {
AxumQuery::<T>::from_request_parts(parts, state)
.await
.map(Self)
}
}
This delegation is:
- Type-safe - preserves all generic constraints, associated types, and where clauses
- Transparent - unwraps the newtype (
self.0), delegates to wrapped type, re-wraps result - Automatic - triggered by pattern matching on the wrapped type's path during codegen
Why we need this: Incan newtypes provide type safety and semantics (e.g., Query[SearchParams] vs raw
AxumQuery<SearchParams>), but Rust's trait system doesn't automatically forward trait impls through newtype wrappers.
Trait bridges make newtypes truly transparent.
Current State¶
Trait bridges work beautifully for stdlib:
# stdlib/web/request.incn
from rust::axum::extract @ "0.8" import Query as AxumQuery
type Query[T] = newtype AxumQuery[T]
# Compiler auto-generates FromRequestParts impl via hardcoded bridge
Backend pattern matching in src/backend/ir/emit/decls/structures.rs:
for bridge in TRAIT_BRIDGES {
if wrapped_type_path.contains(bridge.applies_to_type_path) {
generate_delegation_impl(bridge);
}
}
The Problem¶
Users cannot define their own delegation patterns:
from rust::sqlx @ "0.7" import PgPool
type Pool = newtype PgPool
# ❌ No trait delegation - PgPool's traits are lost!
Users are forced to either:
- Write manual Rust wrapper code (defeats the purpose)
- Use raw
PgPooldirectly (no type safety) - Wait for stdlib support
The Solution¶
Let users define trait bridges inline:
from rust::sqlx @ "0.7" import PgPool
@rust.delegate(
trait=sqlx::Executor,
methods=["execute", "fetch_one", "fetch_all"],
)
type Pool = newtype PgPool
# ✅ Generates Rust impl block: makes Pool usable wherever sqlx::Executor is expected
The @rust.delegate decorator tells the compiler to generate a Rust impl sqlx::Executor for Pool that forwards all
trait methods to the wrapped PgPool. This preserves Rust's trait-based polymorphism: code expecting an Executor will
accept Pool because it implements the trait, just like the wrapped type does.
Design¶
Core Syntax: @rust.delegate¶
The decorator attaches to newtype declarations and instructs the compiler to generate trait delegation code.
Basic Usage¶
The simplest form delegates specific methods from a single trait:
import rust::sqlx
@rust.delegate(
trait=sqlx::Executor,
methods=["execute", "fetch_one"],
)
type Pool = newtype rust::sqlx::PgPool
Parameters:
trait- The Rust trait to delegate (must be an imported symbol, not a string). The compiler generatesimpl sqlx::Executor for Poolthat forwards the specified methods to the wrappedPgPool.methods- List of method names to delegate. If omitted, delegates all trait methods (see Open Questions: Default Delegation Strategy). Use explicit lists when you only need a subset of the trait's methods.
Multiple Traits¶
When your newtype needs to implement multiple traits, use the traits parameter (note: plural):
@rust.delegate(
traits=[
sqlx::Executor,
sqlx::PgExecutor,
std::fmt::Debug,
],
)
type Pool = newtype PgPool
Parameter:
traits- List of trait symbols to delegate. Generates multipleimplblocks (one per trait). All traits must be implemented on the wrapped type. If method names collide across traits, the compiler rejects with an error (see Multiple Decorator Handling).
When to use: Your newtype needs to preserve multiple unrelated trait implementations from the wrapped type (e.g., database traits + formatting traits).
Method Renaming¶
Sometimes you want to expose Rust trait methods under different names in Incan. Use a dictionary for methods:
@rust.delegate(
trait=sqlx::Connection,
methods={
"connect": "establish", # Rust: establish → Incan: connect
"close": "shutdown", # Rust: shutdown → Incan: close
},
)
type DbConnection = newtype PgConnection
Parameter:
methods(dict form) - Maps Incan method names (keys) to Rust method names (values). The compiler generates methods with the Incan names that delegate to the Rust names.
When to use: The Rust trait uses naming conventions that conflict with Incan style or when avoiding reserved keywords.
Note: Method renaming affects only the generated Incan-facing methods; the Rust trait is implemented with its original method names.
Advanced: Associated Types¶
Some Rust traits have associated types - type placeholders that must be specified when implementing the trait. These are different from generic type parameters: they're output types that the implementer chooses, not input types the caller provides.
Example: Iterator has an associated type Item
trait Iterator {
type Item; // ← Associated type: "what does this iterator yield?"
fn next(&mut self) -> Option<Self::Item>;
}
// When implementing, you must specify what Item is:
impl Iterator for MyRange {
type Item = i32; // ← "This iterator yields i32 values"
fn next(&mut self) -> Option<i32> { /* ... */ }
}
Why this matters:
Associated types answer questions like:
Iterator::Item- What does this iterator yield?Future::Output- What does this future resolve to?FromStr::Err- What error does parsing return?
They're part of the trait's contract - without specifying them, the implementation is incomplete.
Why trait bridges need this information:
When you delegate a trait with associated types, the compiler needs to know what to put in those "holes":
from my_crate import CustomIterator, MyItem # custom types defined in user code
@rust.delegate(
trait=std::iter::Iterator,
associated_types={
"Item": MyItem, # ← Symbolic type reference, validated by compiler
},
)
type MyIter = newtype CustomIterator
Generated Rust:
impl Iterator for MyIter {
type Item = MyItem; // ← From your decorator
fn next(&mut self) -> Option<MyItem> {
self.0.next() // Delegate to wrapped type
}
}
Without the associated_types parameter, the compiler can't generate the type Item = ...; line, and Rust will reject
the incomplete impl.
Parameter:
associated_types- Dict mapping associated type names (strings) to type symbols (not strings). The type values must be valid Incan or Rust types accessible in the current scope (either defined locally or imported). Each entry becomes atype Name = Type;declaration in the generated impl block. The wrapped type must implement the same trait with compatible associated types.
Common traits with associated types:
Not just Iterator! Many Rust traits use associated types:
Iterator-Item(what you iterate over)Future-Output(what the future resolves to)FromStr-Err(what error parsing returns)Add,Sub,Mul-Output(result type of arithmetic)Deref-Target(what the smart pointer points to)Index-Output(type returned by indexing)TryFrom-Error(error type of conversion)
When to use:
Only when delegating traits that define associated types. Most common traits (Display, Debug, Clone, Send, Sync)
have no associated types and don't need this parameter.
Associated type inference:
When associated_types is omitted, the compiler attempts to infer associated types from the wrapped type's existing
trait impl:
- If the wrapped type implements the trait with concrete associated types, the compiler mirrors those types in the delegation impl
- If inference succeeds (wrapped type's impl is visible and concrete), no explicit
associated_typesneeded - If inference fails (wrapped type's impl is generic, conditional, or unavailable), the compiler errors and requires explicit specification
Example: When inference works
from rust::std::iter import Iterator
from rust::std::vec import IntoIter # IntoIter<T> implements Iterator with Item = T
@rust.delegate(trait=Iterator)
type MyIter[T] = newtype IntoIter[T]
# Compiler infers: type Item = T (from IntoIter's impl)
Example: When explicit specification is required
from rust::std::iter import Iterator
from my_crate import CustomIter # Opaque type, impl not visible to Incan
@rust.delegate(
trait=Iterator,
associated_types={"Item": int}, # Required: compiler can't infer
)
type MyIter = newtype CustomIter
Rule of thumb: Try omitting associated_types first. If the compiler errors with "missing associated type", add
explicit specification.
Why symbols instead of strings:
Like trait= parameters, type values are symbolic references (they must exist in the namespace).
Built-in types like int, str, bool work without imports. The LSP autocompletes available types as you type.
For example:
# ✅ Symbols - validated, refactoring-safe
from rust::tokio import JoinHandle
import std::result as result
@rust.delegate(
trait=std::future::Future,
associated_types={"Output": result::Result[(), str]},
)
type Task = newtype JoinHandle
# ❌ Strings - unvalidated, breaks silently
@rust.delegate(
trait=std::future::Future,
associated_types={"Output": "Result<(), String>"}, # Typo won't be caught
)
type Task = newtype JoinHandle
Advanced: Async Traits¶
Rust's async traits require special handling for delegation. The decorator automatically handles this through trait introspection — the compiler reads the trait definition and extracts method signatures, generic parameters, and constraints.
Example: Delegating FromRequestParts
import rust::axum::extract as extract
@rust.delegate(trait=extract::FromRequestParts)
type CustomExtractor[T] = newtype ExternalExtractor[T]
What the compiler does automatically:
- Introspects the trait definition from Rust metadata (via rust-analyzer LSP — see Rust Trait Introspection)
- Discovers generic parameters—reads
trait FromRequestParts<S>and discovers<S>automatically - Extracts method signatures—parameter names, types, return types
- Identifies async methods—methods returning
impl Future<...> - Preserves trait bounds—carries over where clauses from the trait definition
Generated Rust:
// Pseudo-Rust: simplified for illustration (actual codegen would include async_trait desugaring)
impl<T, S> axum::extract::FromRequestParts<S> for CustomExtractor<T>
where
T: DeserializeOwned,
S: Send + Sync, // From trait definition's bounds
ExternalExtractor<T>: axum::extract::FromRequestParts<S>,
{
type Rejection = <ExternalExtractor<T> as FromRequestParts<S>>::Rejection;
async fn from_request_parts(
parts: &mut axum::http::request::Parts, // Introspected from trait
state: &S, // Introspected from trait
) -> Result<Self, Self::Rejection> {
ExternalExtractor::<T>::from_request_parts(parts, state)
.await
.map(Self)
}
}
No manual parameters needed:
The compiler discovers all trait metadata automatically—generic parameters, method signatures, async detection, bounds, and associated types. You only specify the trait symbol:
@rust.delegate(trait=extract::FromRequestParts)
type CustomExtractor[T] = newtype ExternalExtractor[T]
The compiler reads trait FromRequestParts<S> and discovers that <S> exists, just like it discovers method parameters
(parts: &mut Parts, state: &S).
Common patterns:
# Database connection
@rust.delegate(trait=sqlx::Executor)
type Pool = newtype PgPool
# Async extractors (compiler discovers <S> from trait definition)
@rust.delegate(trait=extract::FromRequestParts)
type Query[T] = newtype AxumQuery[T]
# Futures (compiler infers Output from wrapped type's impl)
@rust.delegate(trait=std::future::Future)
type Task[T] = newtype JoinHandle[T] # Infers: type Output = T
Decorator Parameters Reference¶
| Parameter | Type | Description | Example |
|---|---|---|---|
trait |
Symbol | Single trait to delegate | trait=sqlx::Executor |
traits |
List[Symbol] | Multiple traits | traits=[TraitA, TraitB] |
methods |
List[str] or Dict | Methods to delegate (default: all) | methods=["execute"] or {"connect": "establish"} |
associated_types |
Dict[str, Symbol] | Associated type mappings (when trait requires explicit specification) | {"Item": int} or {"Item": MyType} |
Comparison: Hardcoded vs User-Defined¶
Hardcoded (v0.2 Stdlib Approach)¶
Definition (in crates/incan_core/src/lang/trait_bridges.rs):
TraitBridge {
applies_to_type_path: "axum::extract::",
trait_path: "axum::extract::FromRequestParts",
// ...
}
Usage:
type Query[T] = newtype AxumQuery[T] # Pattern matches automatically
Pros: Automatic, zero boilerplate
Cons: Only stdlib, users can't extend
User-Defined (This RFC)¶
Definition:
@rust.delegate(trait=sqlx::Executor, methods=["execute"])
type Pool = newtype PgPool
Pros: User-extensible, explicit, grep-able
Cons: More boilerplate for custom cases
LSP Support¶
The Language Server Protocol integration is critical for usability. The LSP provides:
Autocomplete¶
trait=parameter - suggests available imported traitsassociated_typeskeys - when trait is set, suggests trait's required associated type namesassociated_typesvalues - suggests types in scope (imported, built-in, or locally defined)
Hover Information¶
Hovering over trait= shows the trait signature:
@rust.delegate(
trait=std::iter::Iterator, # ← Hover shows trait definition
associated_types={"Item": int},
)
type MyIter = newtype CustomIterator
Displays:
trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
}
Diagnostics¶
- Missing required associated types - "Iterator requires associated type 'Item'"
- Unknown associated type names - "Trait Iterator has no associated type 'Output'"
- Invalid trait symbols - "Trait 'Foo' not found in scope"
- Missing trait import - "Trait must be imported to use in decorator"
Signature Help¶
As you type decorator parameters, shows available parameters and their types:
@rust.delegate(| # ← Shows: trait=Symbol, traits=List[Symbol], methods=...
This makes the decorator self-documenting without referring to external Rust documentation.
Examples¶
Example 1: Database Connection Pool¶
"""Custom async database wrapper"""
import rust::sqlx as sqlx
from rust::sqlx @ "0.7" import PgPool
@rust.delegate(
traits=[
sqlx::Executor,
sqlx::PgExecutor,
],
)
type Pool = newtype PgPool
async def get_users(pool: Pool) -> List[User]:
# pool.execute() works because Executor is delegated!
rows = await pool.fetch_all("SELECT * FROM users")
return [User.from_row(r) for r in rows]
Example 2: Custom Iterator¶
from rust::my_crate @ "1.0" import RangeIter as RustRange
@rust.delegate(
trait=std::iter::Iterator,
associated_types={"Item": int}, # Symbolic type reference
)
type Range = newtype RustRange
# Now works in for loops!
for x in Range.new(0, 10):
println(x)
Example 3: Error Handling¶
from rust::anyhow @ "1.0" import Error as AnyhowError
@rust.delegate(
traits=[
std::error::Error,
std::fmt::Display,
std::fmt::Debug,
],
)
type AppError = newtype AnyhowError
def risky() -> Result[Data, AppError]:
return Err(AppError.msg("Something failed")) # ? operator works!
Example 4: Method Renaming¶
"""
Hypothetical: demonstrating method renaming with a custom HTTP trait.
(Note: reqwest::Client is a struct in Rust; this assumes a fictional HttpClient trait for illustration)
"""
from rust::my_http_lib @ "1.0" import Client as LibClient
@rust.delegate(
trait=my_http_lib::HttpClient, # Fictional trait for demonstration
methods={
"get": "http_get", # Expose http_get as get
"post": "http_post", # Expose http_post as post
"execute": "send_request", # Rename for clarity
},
)
type HttpClient = newtype LibClient
async def fetch(client: HttpClient, url: str) -> str:
response = await client.get(url).execute()
return await response.text()
Note: This example uses a hypothetical
HttpClienttrait for illustration. In practice, HTTP client libraries likereqwestprovide structs (not traits), so delegation would apply to traits those structs implement (e.g.,Clone,Debug, ortower::Service).
Example 5: Disambiguating Method Collisions¶
"""Wrapping a type that implements multiple Executor traits"""
import rust::sqlx as sqlx
import rust::custom_db as custom
@rust.delegate(
trait=sqlx::Executor,
methods={
"execute_sql": "execute", # Rename to avoid collision
"fetch_one": "fetch_one", # No collision, keep original
}
)
@rust.delegate(
trait=custom::Executor,
methods={
"execute_custom": "execute", # Rename to avoid collision
"batch_execute": "batch_execute",
}
)
type HybridPool = newtype CustomPgPool
async def run_queries(pool: HybridPool):
# Both executors available under different names
await pool.execute_sql("SELECT * FROM users")
await pool.execute_custom(custom_query)
Why @rust.delegate Syntax?¶
Namespacing¶
The @rust.* prefix clearly marks Rust interop:
@rust.delegate- Trait delegation@rust.unsafe(future) - Unsafe operations@rust.ffi(future) - C FFI bindings@rust.inline(future) - Force inline
vs Incan-native decorators:
@derive- Incan codegen@test- Test framework@fixture- Test fixtures
Symbolic Arguments¶
Use trait=sqlx::Executor (symbol) not trait="sqlx::Executor" (string):
Benefits:
- Typechecker validation - trait path must resolve
- IDE autocomplete - works on trait names
- Refactoring safe - rename tracking
- Import enforcement - compiler ensures import exists
Important: The trait symbol must be imported for the decorator to work. This is by design—it forces users to ensure proper Rust imports are in place, making the underlying dependencies explicit and verified.
Comparison:
# ✅ GOOD - Symbol imported and used
import rust::sqlx as sqlx
@rust.delegate(trait=sqlx::Executor)
type Pool = newtype PgPool
# ❌ BAD - String, unvalidated, no import verification
@rust.delegate(trait="sqlx::Executor")
type Pool = newtype PgPool
Design Decisions¶
1. Multiple Decorator Handling¶
Decision: Multiple @rust.delegate decorators on the same type ARE allowed, with collision detection.
# ✅ ALLOWED - no method name collisions
@rust.delegate(trait=sqlx::Executor)
@rust.delegate(trait=std::fmt::Debug)
type Pool = newtype PgPool
Method name collision rule:
If two decorators delegate methods with the same final Incan name (after renaming), the compiler must reject:
# ❌ ERROR - both expose 'execute'
@rust.delegate(trait=sqlx::Executor, methods=["execute"])
@rust.delegate(trait=custom::Executor, methods=["execute"])
type Pool = newtype PgPool
# Error: Method 'execute' delegated by multiple decorators (sqlx::Executor, custom::Executor)
# Hint: Use method renaming to resolve the conflict
Disambiguation via renaming:
# ✅ VALID - renamed to avoid collision
@rust.delegate(trait=sqlx::Executor, methods={"execute_sql": "execute"})
@rust.delegate(trait=custom::Executor, methods={"execute_custom": "execute"})
type Pool = newtype PgPool
Rationale:
- Flexibility: Allows incremental trait addition and per-trait method selection
- Clarity: Each decorator states its trait and method subset independently
- Simplicity: Collision detection is straightforward (check final method names across all decorators)
Alternative single-decorator form:
For convenience, traits=[...] in a single decorator is still supported when no renaming is needed:
# Equivalent to three separate decorators (when no collisions)
@rust.delegate(traits=[TraitA, TraitB, TraitC])
type T = newtype Wrapped
Method name conflicts within a single decorator:
When using traits=[...], if multiple traits define the same method name, the compiler rejects with an error:
@rust.delegate(traits=[sqlx::Executor, custom::Executor])
type Pool = newtype PgPool
# Error: Method 'execute' appears in multiple traits: sqlx::Executor, custom::Executor
# Hint: Use separate decorators with method renaming to disambiguate
2. Default Delegation Strategy¶
Decision: When methods is omitted, delegate all trait methods by default.
@rust.delegate(trait=Executor) # Delegates ALL Executor methods
Rationale:
- Most common use case is full trait delegation (newtypes as transparent wrappers)
- Explicit
methods=["execute", "fetch"]available for subsets - No need for redundant
methods="all"parameter
For subsets, use explicit list:
@rust.delegate(trait=Executor, methods=["execute", "fetch"]) # Only these two
3. Limitations: Auto Traits¶
Auto traits (Send, Sync, Unpin, UnwindSafe, RefUnwindSafe) are not supported by @rust.delegate.
These traits are automatically inferred by Rust's compiler based on type composition and cannot be implemented manually. The newtype automatically inherits these traits if the wrapped type has them—no delegation needed.
What happens automatically:
from rust::std::sync import Arc
# Arc<T> is Send + Sync when T is Send + Sync
type SharedData[T] = newtype Arc[T]
# SharedData[T] automatically gets Send + Sync - no decorator needed!
# Rust infers this based on Arc's auto trait impls
What's rejected:
# ❌ ERROR - auto traits cannot be explicitly delegated
@rust.delegate(trait=std::marker::Send)
type MyWrapper = newtype SomeType
# Error: Cannot delegate auto trait 'std::marker::Send'
# Note: Auto traits (Send, Sync, Unpin, etc.) are inferred automatically by Rust
Why this limitation exists:
Auto traits are special in Rust's type system — they're implemented automatically based on the types a struct contains.
You cannot write impl Send for MyType manually. The newtype wrapper inherits auto traits from its wrapped type
automatically, so explicit delegation is both unnecessary and impossible.
Common auto traits:
Send- Type can be transferred across thread boundariesSync- Type can be shared between threads (via&T)Unpin- Type can be moved even when pinnedUnwindSafe/RefUnwindSafe- Type is safe across panic unwinding
If you need to control these traits, you must do so at the type definition level (e.g., wrapping in !Send types), not
through delegation.
4. Rust Trait Introspection¶
How does the compiler access Rust trait definitions?
Approach: Leverage rust-analyzer via LSP
Query rust-analyzer through the Language Server Protocol for trait metadata. This approach piggybacks on Incan's
existing rust-analyzer dependency (required for rust:: imports per RFC 005).
Why rust-analyzer:
- Already required - Incan's LSP uses rust-analyzer for
rust::import resolution, type checking, and autocomplete - Solves hard problems - Expands proc macros, resolves cargo features, handles GATs and const generics
- Stable protocol - LSP is versioned and stable (unlike rustc internals or rustdoc JSON)
- No custom parser maintenance - rust-analyzer handles Rust syntax evolution; we consume stable APIs
- IDE integration - Same service powers editor features (hover, autocomplete)
What rust-analyzer provides:
// Pseudo-code: trait information from rust-analyzer LSP
TraitInfo {
name: "Executor",
generics: [TypeParam("S"), TypeParam("T"), ...],
methods: [
Method { name: "execute", is_async: true, params: [...], return_type: ... },
Method { name: "fetch_one", ... },
],
associated_types: [
AssocType { name: "Item", bounds: [...], default: None },
],
supertraits: [Executor, Send, Sync],
where_clauses: [...],
}
Key capabilities:
- Proc macro expansion - Sees traits generated by
#[async_trait],#[derive], etc. - Cargo features resolution - Knows which features are enabled in current build
- Trait impl checking - Verifies wrapped type actually implements the trait
- Cross-crate resolution - Follows trait definitions through dependencies
- GATs and const generics - Full support for advanced Rust type features
How it works:
// Pseudo-code: LSP query during compilation
let client = LspClient::new("rust-analyzer");
let trait_info = client.query_trait("sqlx::Executor")?;
// Generate delegation impl using trait metadata
for method in trait_info.methods {
generate_delegated_method(method);
}
Caching:
- Queried trait metadata cached in
target/incan/trait-cache/ - Keyed by (crate_name, version, trait_path, content_hash)
- Content hash computed from trait definition source (transitively including supertraits)
- Automatically invalidated when trait definition changes (hash mismatch), regardless of cause
- Resilient to edge cases: manual edits, git branch switches, dependency updates
- Reduces LSP queries on incremental builds
Fallback for CI/offline environments:
When rust-analyzer is unavailable (CI, offline builds, cross-compilation):
- Use cached metadata from previous builds (if available)
- Defer trait validation to rustc during final Rust compilation
- rustc errors map back to Incan source locations (e.g., "trait
Executornot implemented forPgPool")
This ensures Incan works in all environments while preferring rich metadata when available.
Introspection capabilities comparison:
We considered using syn for this parsing model, but unfortunately it lacks the features to make that a viable option.
This table shows syn compared to the rust-analyzer approach:
| Edge Case | rust-analyzer | syn-only |
|---|---|---|
| Proc macro traits | ✅ Yes | ❌ No |
| Cargo features | ✅ Yes | ❌ No |
| Trait impl checking | ✅ Yes | ❌ No |
| GATs (Generic Assoc Types) | ✅ Yes | ⚠️ Partial |
| Const generics | ✅ Yes | ⚠️ Partial |
| Target-specific impls | ✅ Yes | ❌ No |
| Supertrait resolution | ✅ Yes | ✅ Yes |
| Async trait detection | ✅ Yes | ⚠️ Partial |
Compiler responsibilities:
The compiler still handles Incan-specific delegation logic:
- Generic substitution - Maps Incan type parameters to Rust generics
- Method conflicts - Detects name collisions in multi-trait delegation
- Associated type inference - Validates
associated_types=matches trait definition - Error translation - Converts rust-analyzer/rustc errors to Incan diagnostics
Migration Path¶
Stdlib Migration (Required for RFC Completion)¶
Currently, stdlib uses hardcoded trait bridges:
# stdlib/web/request.incn - current
type Query[T] = newtype AxumQuery[T] # Auto-delegates via hardcoded pattern
Stdlib must converge to explicit decorators for trait delegation parity:
# stdlib/web/request.incn - with decorator
@rust.delegate(trait=axum::extract::FromRequestParts)
type Query[T] = newtype AxumQuery[T]
Benefits of migration:
- Makes trait delegation explicit and grep-able
- Same mechanism for stdlib and user code
- Better documentation (hover shows what traits are implemented)
Transitional allowance:
- Existing hardcoded bridges may remain temporarily while stdlib modules are migrated
- New trait delegation behavior should be added through
@rust.delegate, not new hardcoded bridge entries - Hardcoded bridges are removed (or reduced to compatibility shims) once equivalent decorator-based coverage exists
Acceptance Criteria¶
- Parity: Any trait delegation behavior used by
std.web(or other stdlib modules) is expressible with@rust.delegatein user code. - No privileged path for new features: New delegation features are implemented in the decorator pipeline, not via stdlib-only hardcoded bridges.
- Stdlib convergence: Core stdlib newtype delegations are represented via explicit decorators (or generated from an equivalent decorator model), with hardcoded behavior retained only as temporary compatibility scaffolding.
- User validation: At least one non-stdlib integration example (e.g.,
sqlx,tower, or equivalent) demonstrates that user libraries can achieve the same delegation outcomes without handwritten Rust wrappers.
Alternatives Considered¶
- Hardcoded bridges only - Keep trait bridges internal to stdlib. Rejected: forces users to wait or write Rust wrappers.
- Build stdlib in Rust - Write
std.webas Rust wrappers instead of Incan. Rejected: defeats Incan's purpose as a high-level language. - String-based trait names - Use
trait="sqlx::Executor"instead of symbols. Rejected: no validation, no IDE support, breaks refactoring.
Success Metrics¶
- Adoption: 50%+ of external Rust wrapper newtypes use
@rust.delegatein community packages - Reduction in manual Rust code: 75% less hand-written delegation glue
- Parity achieved: No net-new stdlib-only trait bridge rules are introduced after RFC adoption
- Documentation: Clear examples in Incan Book chapter on Rust interop
- Performance: Zero runtime overhead (compile-time only)
References¶
- RFC 005: Rust Interop (
rust::import syntax) - RFC 021: Field Metadata & Aliases (similar decorator pattern)
- Trait bridges implementation:
src/backend/ir/emit/decls/structures.rs - Trait bridge registry:
crates/incan_core/src/lang/trait_bridges.rs
Future Extensions¶
1. @rust.unsafe Decorator¶
@rust.unsafe
def raw_pointer_deref(ptr: *const i32) -> i32:
return *ptr # Allowed in unsafe block
2. @rust.inline Hint¶
@rust.inline(always)
def hot_path(x: int) -> int:
return x * 2
3. @rust.ffi for C Bindings¶
@rust.ffi(lib="mylib", symbol="compute")
def native_compute(x: float) -> float: ...
4. Full impl Blocks¶
impl CustomTrait for MyType:
fn custom_method(self) -> int:
# Full method body in Incan
return 42
This RFC focuses on delegation; full impl blocks are a separate (larger) feature.
Appendix: Current Trait Bridge Registry¶
For reference, stdlib's hardcoded bridges:
// crates/incan_core/src/lang/trait_bridges.rs
pub const TRAIT_BRIDGES: &[TraitBridge] = &[
// Display: println!("{}", x)
TraitBridge {
applies_to_type_path: "", // Matches all
trait_path: "std::fmt::Display",
// ...
},
// FromRequestParts: Axum extractors
TraitBridge {
applies_to_type_path: "axum::extract::",
trait_path: "axum::extract::FromRequestParts",
// ...
},
// Iterator: for x in iter
TraitBridge {
applies_to_type_path: "",
trait_path: "std::iter::Iterator",
// ...
},
// IntoResponse: Axum responses
TraitBridge {
applies_to_type_path: "axum::response::",
trait_path: "axum::response::IntoResponse",
// ...
},
];
This RFC proposes making this extensible via @rust.delegate.