RFC 023: Compilable Stdlib & Rust Module Binding¶
- Status: Implemented
- Created: 2026-02-08
- Author(s): Danny Meijer (@dannymeijer)
- Related: RFC 005 (Rust interop), RFC 013 (Rust crate dependencies), RFC 022 (stdlib namespacing & compiler→stdlib handoff)
- Target version: 0.1.0
- Implemented version: 0.2.0
Note: (re-baselined closure criteria:
.incnsource-of-truth, Incan-first stdlib, narrow runtime bridges)
Summary¶
This RFC proposes two related changes that reduce the compiler's role as a registry of stdlib implementations, and enable an ecosystem of Rust-backed Incan libraries:
- Compilable stdlib: stdlib
.incnfiles transition from documentation-only stubs to real, compilable Incan source code. The compiler compiles them through the normal pipeline. Rust-backed leaves are kept narrow and explicit: most use@rust.extern, while source-declared wrappers may still use internalrust::imports when that keeps the public Incan surface as the source of truth. rust.module()binding: a new module-level directive that declares an Incan module (or stdlib module) is backed by a specific Rust module path. This replaces hardcoded path-rewriting in the compiler with a data-driven declaration, and enables third-party Incan libraries backed by Rust crates.
Together, these changes push the Incan stdlib toward being written in mostly plain Incan, with a minimal set of Rust-backed primitives — and make the same pattern available to the ecosystem, allowing users to write their own rust-backed Incan libraries.
Implemented Closeout Notes¶
.incnsource is now the source of truth for migrated stdlib surfaces, includingstd.async,std.math,std.reflection, andstd.traits.- Build, test, and lock flows derive stdlib-driven feature/extra-dependency activation from shared namespace metadata (for example
std.asyncenabling Tokio andstd.mathpullinglibm). - Explicit generic
withbounds are enforced in the frontend against concrete argument types; backend trait-bound inference remains additive rather than the first place bound violations show up. @rust.externdeclaration-shape errors are caught in the frontend, and downstream Cargo/rustcfailures are wrapped back onto the.incndeclaration site in the CLI build surface.
Motivation¶
The stdlib is all Rust, and the compiler is the bottleneck¶
Today, Incan's stdlib is implemented entirely in Rust (crates/incan_stdlib/src/), and the .incn files in stdlib/
are documentation-only stubs that the compiler ignores. Adding a single function to a stdlib module requires touching
up to five files across four compiler stages:
- Rust implementation (
crates/incan_stdlib/src/testing.rs) — write the function - Incan stub (
stdlib/testing.incn) — add the signature for docs/IDE - Typechecker registry (
src/frontend/typechecker/collect.rs) — hardcode the function's type signature - Emission mapping (
src/backend/ir/emit/decls.rs) — hardcode thestd.testing→incan_stdlib::testingpath rewrite - Module registry (
crates/incan_core/src/lang/stdlib.rs) — register the module metadata
The function signature is duplicated (once in the .incn stub, once as handwritten Rust data structures in the
typechecker), and they can drift. The path-rewriting in emission is a growing if/else chain that must be extended for
every new stdlib module. After this RFC, the .incn source file becomes the single source of truth — the compiler
parses it for type signatures and compiles it into the generated output — eliminating both the duplication and the
manual wiring.
Most stdlib code doesn't need to be Rust¶
Many stdlib functions are algorithmically simple and can be written in Incan, provided a small set of Rust-backed primitives exists. The stdlib already demonstrates this pattern:
# already today
trait Ord:
@rust.extern
def __lt__(self, other: Self) -> bool: ... # Rust-backed primitive
def __le__(self, other: Self) -> bool: # Pure Incan
return self.__lt__(other) or self.__eq__(other)
def __gt__(self, other: Self) -> bool: # Pure Incan
return other.__lt__(self)
def __ge__(self, other: Self) -> bool: # Pure Incan
return other.__lt__(self) or self.__eq__(other)
Only __lt__ is @rust.extern; the other three are pure Incan built on that primitive. The same pattern applies
broadly. For example, std.testing has 12+ functions but only one (fail()) is irreducibly Rust — every assert_*
variant is expressible as pure Incan on top of fail().
No ecosystem path for Rust-backed Incan libraries¶
Users cannot create their own Rust-backed Incan libraries without modifying the compiler. Today, @std.builtin is
reserved for stdlib sources, and there is no mechanism for a third-party package to say "my Incan module is backed by
this Rust crate." This limits the ecosystem to either pure Incan packages or raw rust:: imports.
Goals¶
- Stdlib
.incnfiles become the single source of truth for both documentation and compilation — no more duplicated signatures in the typechecker. - Adding a new stdlib function requires touching only two files: the Rust implementation (if
@rust.extern) and the.incnsource. The compiler requires no per-function or per-module special-casing. - Third-party Incan libraries can wrap Rust crates using the same mechanism the stdlib uses, without compiler changes.
- The set of Rust-backed primitives is explicitly minimized and clearly identified.
Non-Goals¶
- Replacing the
rust::import mechanism (RFC 005).rust::remains the way to import Rust crate items directly into Incan code.rust.module()+@rust.externprovides a higher-level alternative: wrapping Rust crates with Incan-shaped APIs. Both mechanisms coexist. - Automatic generation of
.incnstubs from Rust source. This may be valuable tooling but is out of scope. - Advanced trait features (associated types, supertraits, dispatch strategy). This RFC establishes the core trait mechanics — bound syntax, inference, and the Rust mapping — which cover the vast majority of use cases. Remaining capabilities can be addressed in targeted follow-ups as real-world usage demands them.
Guide-level explanation (how users think about it)¶
Stdlib: mostly Incan, with Rust-backed leaves¶
The Incan standard library is written in Incan. Most functions have real Incan implementations that the compiler compiles
through the normal pipeline. Only the functions that need runtime/OS/framework access are marked @rust.extern and
backed by Rust.
For example, std.testing looks like this:
"""
Incan Testing Framework
"""
rust.module("incan_stdlib::testing")
# ---- Rust-backed primitive ----
@rust.extern
def fail(msg: str) -> None:
"""Explicitly fail a test. Runtime-provided."""
...
# ---- Pure Incan (compiled normally) ----
def assert(condition: bool) -> None:
"""Assert condition is true."""
if not condition:
fail("assertion failed")
def assert_eq[T](left: T, right: T) -> None:
"""Assert two values are equal."""
if left != right:
fail(f"assertion failed: left != right\n left: {left}\n right: {right}")
def assert_ne[T](left: T, right: T) -> None:
"""Assert two values are not equal."""
if left == right:
fail(f"assertion failed: left == right\n both: {left}")
def assert_true(condition: bool) -> None:
"""Assert condition is true."""
assert(condition)
def assert_false(condition: bool) -> None:
"""Assert condition is false."""
assert(not condition)
def assert_is_some[T](option: Option[T], msg: str = "") -> T:
"""Assert Option is Some and return the value."""
match option:
Some(value) => return value
None => fail(if msg != "": msg else "expected Some, got None")
def assert_is_none[T](option: Option[T], msg: str = "") -> None:
"""Assert Option is None."""
match option:
Some(_) => fail(if msg != "": msg else "expected None, got Some")
None => pass
def assert_is_ok[T, E](result: Result[T, E], msg: str = "") -> T:
"""Assert Result is Ok and return the value."""
match result:
Ok(value) => return value
Err(e) => fail(if msg != "": msg else f"expected Ok, got Err({e})")
def assert_is_err[T, E](result: Result[T, E], msg: str = "") -> E:
"""Assert Result is Err and return the error."""
match result:
Ok(_) => fail(if msg != "": msg else "expected Err, got Ok")
Err(e) => return e
One @rust.extern leaf, many pure Incan functions. Users import and use them exactly as before — the change is internal
to how the compiler processes the stdlib.
rust.module(): declaring a Rust-backed module¶
The rust.module() directive appears at the top of a .incn file and tells the compiler where @rust.extern items
are backed:
# stdlib/testing.incn
rust.module("incan_stdlib::testing")
@rust.extern
def fail(msg: str) -> None: ...
# ... pure Incan functions ...
When the compiler encounters @rust.extern def fail(...) in a module with rust.module("incan_stdlib::testing"), it
emits a reference to incan_stdlib::testing::fail in the generated Rust code.
Third-party Rust-backed libraries¶
The same mechanism works outside the stdlib. A library author can ship an Incan package backed by a Rust crate:
my_cache_lib/
├── Cargo.toml # Rust crate with the implementation
├── src/
│ └── lib.rs # pub fn get(key: &str) -> Option<String> { ... }
└── stubs/
└── cache.incn # Incan-shaped contract
rust.module("my_cache_lib")
@rust.extern
def get(key: str) -> Option[str]: ...
@rust.extern
def set(key: str, value: str, ttl: int = 0) -> None: ...
# Pure Incan convenience functions built on the primitives
def get_or_default(key: str, default: str) -> str:
"""Get a value from the cache, returning a default if not found."""
match get(key):
Some(value) => return value
None => return default
def get_or_set(key: str, default: str, ttl: int = 0) -> str:
"""Get a value from the cache, setting it to default if not found."""
match get(key):
Some(value) => return value
None:
set(key, default, ttl)
return default
Users consume it like any Incan module:
from my_cache.cache import get_or_default
def load_config(key: str) -> str:
return get_or_default(key, "default_value")
Trait bounds on generics¶
When generic Incan functions are compiled to Rust, the compiler infers trait bounds from usage. For example,
assert_eq[T] uses != and f-string interpolation on T, so the compiler emits
fn assert_eq<T: PartialEq + std::fmt::Display>(...). Authors can also annotate bounds explicitly using with:
def assert_eq[T with (Eq, Display)](left: T, right: T) -> None:
...
See Section 5 for the full inference rules and annotation syntax.
Reference-level explanation (precise rules)¶
1) Compilation rules (normative)¶
Any .incn file — whether it belongs to the standard library, a third-party library, or an application — can mix pure
Incan code with Rust-backed functions. These rules apply uniformly:
- The compiler parses and compiles
.incnfiles through the normal pipeline (parser → typechecker → lowering → emission) regardless of where they live. - Function signatures and type definitions in
.incnfiles are the authoritative type information for the typechecker. The compiler must not maintain separate, hardcoded signature registries. - Functions with
@rust.externhave...bodies; the compiler emits a call to the corresponding Rust implementation (resolved viarust.module()) rather than compiling the body. - A function with
@rust.externthat also has a non-trivial body (anything other than...orpass) is a compile error: "@rust.externfunction must have a...body — the implementation is provided by Rust." - The Incan signature of a
@rust.externfunction is a contract: its parameter types and return type must correspond to the Rust function's signature after Incan-to-Rust type mapping (e.g.str→String,int→i64,List[T]→Vec<T>). The compiler does not validate this — mismatches are caught downstream byrustc, and Incan wraps the resulting error with a diagnostic pointing to the@rust.externitem and itsrust.module()directive (see Diagnostics). - Models, classes, enums, and traits are compiled through the normal pipeline. Only individual functions/methods marked
@rust.externreceive special treatment.
RFC 022 supersession: RFC 022 introduced
@std.builtinas a stdlib-only decorator. This RFC replaces it with@rust.extern, which serves the same purpose (marking a function whose body is provided by Rust) but is not restricted to stdlib modules. Thein_stdlib_module()check in the typechecker that currently gates@std.builtinshould be removed. TheDecoratorId::StdBuiltinvariant should be renamed toDecoratorId::RustExternwith canonical spelling"rust.extern".
2) rust.module() directive (normative)¶
Syntax¶
rust_module_directive = "rust" "." "module" "(" STRING_LITERAL ")" ;
Where <rust_path> is a string literal containing a valid Rust module path using :: separators
(e.g., "incan_stdlib::testing", "my_crate::sub::module"). The compiler stores it as an opaque string and emits it
verbatim in generated use statements.
rust.module() is a module-level directive — a bare statement that appears at the top of a .incn file (before
any declarations). It is not a decorator: decorators in Incan modify the declaration that immediately follows them,
while rust.module() is a standalone statement about the module itself. (The decorator form @rust.module("...") is
reserved for future use when module is introduced as a keyword — e.g., @rust.module("...") module foo:.)
Semantics¶
rust.module("path::to::module")declares that@rust.externitems in this Incan module are backed by Rust functions atpath::to::module::<item_name>.- The Rust module path is treated as opaque — the compiler does not validate it against Rust source. Mismatches are
caught downstream by
rustc, and Incan wraps the error with a diagnostic (see Diagnostics).
Placement rules¶
- Exactly one per file: a file with
@rust.externitems must have exactly onerust.module()directive. A file without@rust.externitems may omit it. Multiple directives in the same file are a hard error. - No inheritance:
rust.module("incan_stdlib::web")instd.webdoes not apply tostd.web.response. Each module with@rust.externitems needs its own directive. - One Rust module per Incan module: if you need
@rust.externbindings to different Rust modules, split them into separate.incnfiles.
Example — non-propagation across submodules:
rust.module("incan_stdlib::web")
@rust.extern
def run_server(app: App) -> None: ... # OK — directive present
# ERROR: contains @rust.extern but has no rust.module() directive
@rust.extern
def json_response(data: str) -> Response: ...
# Fix: add rust.module("incan_stdlib::web::response") to the top of this file
Resolution & validation¶
The rust.module() path must resolve to one of:
- The standard library runtime crate (
incan_stdlib::*), added to the generatedCargo.tomlwhen anystd.*module is imported. - A crate declared in the package's manifest (
incan.toml[dependencies], per RFC 013).
If the path references an undeclared crate, the compiler emits an error explaining the crate must be declared as a dependency. This ensures paths are auditable and Cargo dependency generation is deterministic.
Path sanitization (security): the rust.module() path is emitted verbatim into generated Rust use statements. To
prevent code injection, the compiler must validate that the path is a well-formed Rust module path — only identifier
segments ([a-zA-Z_][a-zA-Z0-9_]*) separated by ::. Paths containing semicolons, quotes, whitespace, parentheses,
or any other characters are rejected with a diagnostic. This validation is low-cost and eliminates the possibility of
crafted path strings breaking out of a use statement in the generated Rust output.
@rust.extern item-kind restriction¶
@rust.extern is allowed on free functions and trait default methods. It is not allowed on instance methods
(def method(self, ...)).
The reason: rust.module("path") + @rust.extern maps to path::<item_name> — unambiguous for free functions, but
for instance methods it would require a naming convention like path::TypeName__method_name that couples Incan type
names to Rust function names. Instead, types that need Rust-backed behavior should delegate to free-function primitives:
@rust.extern
def run_server(app: App, host: str, port: int) -> None: ...
class App:
def run(self, host: str = "127.0.0.1", port: int = 8080) -> None:
run_server(self, host, port)
3) Irreducible primitives (normative direction)¶
@rust.extern should be applied to the smallest possible set of primitives — functions whose implementations
fundamentally require Rust runtime, OS, or framework access.
| Primitive | Module | Why it needs Rust |
|---|---|---|
fail(msg) |
std.testing |
panic!() — process termination |
print(msg) / println(msg) |
(builtin) | println!() — stdout I/O |
__eq__ (derived) |
std.derives.comparison |
Compiler-generated field-by-field comparison |
__lt__ (derived) |
std.derives.comparison |
Compiler-generated field-by-field ordering |
__hash__ (derived) |
std.derives.comparison |
Compiler-generated hashing via std::hash |
clone (derived) |
std.derives.copying |
Compiler-generated deep copy |
to_json / from_json |
std.serde.json |
Serde derives — proc macros |
math.* functions |
std.math |
Rust f64 methods (.sqrt(), .sin(), etc.) |
run_server(app) |
std.web |
Tokio/Axum server bootstrap |
request_header(req, name) etc. |
std.web |
Framework extractor access |
Everything else — assert_eq, assert_ne, assert_true, Ord.__le__, Eq.__ne__, conversion traits, response
builders — should be pure Incan.
4) Stdlib compilation model (normative direction)¶
When the compiler encounters from std.testing import assert_eq:
- Resolution: resolves
std.testingtostdlib/testing.incn. - Parsing: parses the file as normal Incan source (cached after first parse within a compilation unit).
- Type extraction: finds
def assert_eq[T](left: T, right: T) -> Nonewith a real body — registers it as a normal function in the typechecker's symbol table (no hardcodedFunctionInfoneeded). - Compilation: compiles the function body through the normal pipeline. The call to
fail()withinassert_eqresolves to@rust.extern→incan_stdlib::testing::fail. - Emission: emits a normal Rust function for
assert_eq. Onlyfail()results in a reference toincan_stdlib.
5) Trait bound inference and annotation (normative)¶
When an Incan generic function is compiled to Rust, the generated Rust function needs appropriate trait bounds on its type parameters. Today this is not an issue because generic stdlib functions are routed to pre-written Rust that already carries bounds. In the compilable stdlib model, the compiler must generate these bounds itself.
Inference from usage (normative; required for v0.x)¶
The compiler infers Rust trait bounds from operations used on generic type parameters within a function body.
Inference rules (minimum required set):
| Incan operation | Inferred Rust trait bound |
|---|---|
==, != |
PartialEq |
<, <=, >, >= |
PartialOrd |
f-string interpolation (f"...{x}...") |
std::fmt::Display |
+ |
std::ops::Add<Output = T> |
- |
std::ops::Sub<Output = T> |
* |
std::ops::Mul<Output = T> |
/ |
std::ops::Div<Output = T> |
% |
std::ops::Rem<Output = T> |
clone() |
Clone |
used as Dict key |
Eq + Hash |
used as Set element |
Eq + Hash |
Deferred operations (require associated-type inference; out of scope for initial implementation):
| Incan operation | Inferred Rust trait bound | Why deferred |
|---|---|---|
x[i] (read) |
std::ops::Index<I, Output = U> |
Requires inferring index type I and output type U |
x[i] = v (write) |
std::ops::IndexMut<I, Output = U> |
Same; plus mutability inference |
for elem in iter |
IntoIterator<Item = Elem> |
Requires propagating the associated Item type |
When multiple operations are used on the same type parameter, all inferred bounds are combined (unioned) into a single
where clause on the generated Rust function.
Bounds are placed on the underlying generic parameter T even when the generated Rust passes &T. For comparison,
formatting, and hashing traits this works via Rust's blanket implementations (e.g., &T: PartialEq when
T: PartialEq, &T: Display when T: Display). For arithmetic traits (Add, Sub, etc.) blanket impls do not
exist on references — the compiler's ownership inference (section 6) must ensure that arithmetic operations receive
owned or copied values rather than references, so the bound on T remains sufficient.
Example — compiling assert_eq[T]:
def assert_eq[T](left: T, right: T) -> None:
if left != right:
fail(f"assertion failed: left != right\n left: {left}\n right: {right}")
The compiler observes != → PartialEq and f"...{left}..." → Display, emitting:
fn assert_eq<T: PartialEq + std::fmt::Display>(left: T, right: T) {
if left != right {
incan_stdlib::testing::fail(format!(
"assertion failed: left != right\n left: {}\n right: {}", left, right
));
}
}
Inference must be transitive: if a generic function calls another generic function that requires bounds, the caller must infer those bounds as well.
Explicit annotation syntax (normative)¶
Incan supports explicit trait bound annotations using the with keyword — consistent with existing trait conformance
syntax on type declarations (model Money with Add[Money, Money]:).
# Single bound — bare word
def identity[T with Clone](value: T) -> T: ...
# Multiple bounds — parenthesised
def assert_eq[T with (Eq, Debug)](left: T, right: T) -> None: ...
# Multiple type parameters — with on each
def convert[T with (From[U], Clone), U with Debug](value: U) -> T: ...
Grammar extension:
type_param = IDENT [ "with" bounds ] ;
bounds = bound | "(" bound { "," bound } ")" ;
bound = IDENT [ "[" type_args "]" ] ;
Commas always separate type parameters; parentheses group multiple bounds within a single parameter's with clause.
The + operator (Rust-style) is intentionally avoided because + already means addition in Incan.
Semantics:
- Explicit bounds are additive with inferred bounds. Writing
[T with Eq]when the body also uses f-string interpolation onTresults inT: PartialEq + std::fmt::Display. - Explicit bounds enable the Incan typechecker to validate callers at the Incan level, rather than deferring all
trait-bound errors to
rustc. - Incan trait names map to Rust trait bounds deterministically:
| Incan trait | Rust trait bound |
|---|---|
Eq |
PartialEq |
Ord |
PartialOrd |
Hash |
Hash |
Clone |
Clone |
Debug |
std::fmt::Debug |
Display |
std::fmt::Display |
Serialize |
serde::Serialize |
Deserialize |
serde::de::DeserializeOwned |
6) Implicit ownership and borrowing (normative principle)¶
Incan handles ownership and borrowing implicitly. The compiler analyzes code to determine the most efficient ownership strategy for the generated Rust. Users do not annotate ownership, borrowing, or lifetimes.
This principle is already partially implemented (the compiler infers &self vs &mut self for method receivers and
auto-borrows strings to &str). The compilable stdlib extends it to all compiled Incan code.
Compiler strategy:
- Read-only parameters: borrowed references (
&T). - Mutated parameters: mutable borrows (
&mut T). - Consumed parameters (stored, returned, or moved): owned values (
T). - Primitives (
int,float,bool): always copied (Copyin Rust). - Return values: always owned.
- Ambiguous cases: fall back to cloning. Cloning is always safe; it trades potential performance for simplicity.
Two invariants constrain the inference:
- No observable semantic change: emitted decisions must not change user-visible behavior.
- Clone implies
Clonebound: if the inference clones a value of generic typeT, the generated Rust needsT: Clone. Ownership inference feeds into trait-bound inference — they are not independent systems.
Borrow safety across awaits: the inference must never emit borrows held across await points (borrows across
suspension points prevent Send, breaking Tokio). When in doubt, clone.
Performance-critical code that requires hand-tuned ownership can use rust:: imports (RFC 005) as an escape hatch.
7) Build output for compiled stdlib (normative direction)¶
- Option A (recommended for v0.x): compile from source each time. The compiler compiles stdlib
.incnon every build, emitting the generated Rust alongside user code. Simple, always fresh, cacheable via incremental compilation. - Option B (future optimization): pre-compiled stdlib. Stdlib is compiled during the Incan toolchain's own build
process and baked into
incan_stdlib. Reduces per-project compile times but requires version-locking.
Design details¶
Interaction with existing features¶
RFC 005 (Rust interop)¶
rust:: imports (RFC 005) let end users import Rust crate items directly — Rust-shaped API. rust.module() lets
a module declare that its @rust.extern items are backed by Rust — Incan-shaped API wrapping Rust. These compose
cleanly: a library's .incn source might use rust:: imports internally while also declaring rust.module().
RFC 022 (stdlib namespacing & compiler→stdlib handoff)¶
This RFC is a natural sequel to RFC 022. RFC 022's StdlibModuleInfo registry becomes a transitional fallback: the
stub_path field is still used for source discovery, but the feature field (for Cargo feature gating) can eventually
be derived from rust.module() directives + incan.toml configuration.
Traits and derives¶
Derived trait implementations (@derive(Eq), @derive(Hash), etc.) are compiler-generated and remain @rust.extern
at the individual method level. The trait definitions themselves (including non-@rust.extern methods like __ne__,
__le__, __gt__, __ge__) are compiled as pure Incan.
Async¶
Async functions follow the same rules: @rust.extern async functions have their bodies provided by Rust;
non-@rust.extern async functions are compiled normally.
Layering implications¶
The Incan codebase follows a strict dependency direction:
┌────────────────┐
│ incan_core │ (shared types & registries)
└────▲───────▲───┘
│ │
depends on │ │ depends on
│ │
┌───────────────┴──┐ ┌─┴──────────────┐
│ incan (compiler) │ │ incan_stdlib │ (Rust runtime backing)
└──────────┬───────┘ └───────▲────────┘
│ │
emits │ │ depends on (via Cargo.toml)
│ │
┌────▼──────────────────┴────┐
│ generated program │
└────────────────────────────┘
This RFC preserves and strengthens that layering:
- The compiler depends on
incan_corebut must not depend onincan_stdlib. - Generated user programs depend on
incan_stdlib(added toCargo.tomlby the compiler whenstd.*modules are used). - Compiled stdlib Rust code is emitted as part of the generated user project — not injected back into
incan_stdlib. After this RFC,incan_stdlibnarrows to irreducible runtime primitives only.
Generated Rust module naming for Incan std.*¶
Compiling std.* modules would naively produce mod std { ... }, which shadows Rust's own std crate. To avoid
this, compiled Incan std.* modules are emitted under a renamed root module:
- Incan
std.testing→ Rustcrate::__incan_std::testing - Incan
std.web.app→ Rustcrate::__incan_std::web::app
The __incan_std prefix is an implementation detail — never visible to Incan users. This mapping applies only to the
generated module tree, not to rust.module() paths (which point to Rust crates and are emitted as-is).
Rust-keyword module names (implementation prerequisite)¶
Incan module names that are Rust keywords (e.g., std.async) produce invalid Rust (mod async;). The emitter's
existing escape_keyword() helper must be applied consistently across all generated Rust output:
- Module declarations:
mod async;→mod r#async; - Use-path segments:
use crate::async::...→use crate::r#async::... - Filenames: use
#[path = "async.rs"]attributes so the filesystem name stays clean - Generated identifiers: type paths,
implblocks, etc. originating from keyword-named modules
Reserved keyword list (Rust edition 2021)¶
Strict keywords: as, async, await, break, const, continue, crate, dyn, else, enum, extern,
false, fn, for, if, impl, in, let, loop, match, mod, move, mut, pub, ref, return,
self, Self, static, struct, super, trait, true, type, unsafe, use, where, while
Reserved for future use: abstract, become, box, do, final, macro, override, priv, try, typeof,
unsized, virtual, yield
This list is tied to Rust edition 2021. If the target edition changes, this list must be updated.
Note: 2024 edition adds
genas a reserved keyword.
Diagnostics¶
Mismatch between .incn declaration and Rust implementation¶
When rustc reports a type mismatch for a @rust.extern function call, the Incan compiler must wrap it with a
diagnostic that names the rust.module() path and the @rust.extern function, points to the .incn declaration, and
suggests verifying the Rust signature under the standard type mapping (RFC 005).
Missing rust.module()¶
error: `@rust.extern` function `fail` in module `std.testing` has no Rust backing path.
--> stdlib/testing.incn:5:1
|
5 | @rust.extern
| ^^^^^^^^^^^^ this function's body is marked as runtime-provided
|
= help: add `rust.module("path::to::rust::module")` to the top of this file
Unused rust.module()¶
If a module has rust.module() but no @rust.extern items, emit a warning:
warning: `rust.module()` directive has no effect — no `@rust.extern` items found.
--> stdlib/utils.incn:1:1
|
1 | rust.module("incan_stdlib::utils")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ unused directive
|
= help: remove it if this module is pure Incan, or add `@rust.extern` to Rust-backed functions
@rust.extern on instance methods¶
error: `@rust.extern` is not allowed on instance methods.
--> stdlib/web/app.incn:12:5
|
12 | @rust.extern
| ^^^^^^^^^^^^ instance methods cannot be runtime-provided
|
= help: extract a free function (e.g. `run_server(app, ...)`) and delegate to it from the method
@rust.extern with non-trivial body¶
error: `@rust.extern` function must have a `...` body — the implementation is provided by Rust.
--> my_lib/stubs/cache.incn:5:1
|
5 | @rust.extern
| ^^^^^^^^^^^^ this function is marked as Rust-provided
6 | def get(key: str) -> Option[str]:
7 | return None
| ^^^^^^^^^^ but has an Incan body
|
= help: remove the body and use `...` instead, or remove `@rust.extern` if this is a pure Incan function
Invalid rust.module() path¶
error: `rust.module()` path contains invalid characters.
--> my_lib/stubs/cache.incn:1:1
|
1 | rust.module("my_crate; malicious_code()")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ must be a valid Rust module path
|
= help: use only identifier segments separated by `::` (e.g. `"my_crate::my_module"`)
Unresolved Rust path (feature-gated crates)¶
If the rust.module() path references a crate behind a disabled Cargo feature, the build error should be wrapped with
a diagnostic pointing to the directive and suggesting the feature may not be enabled.
Compatibility / migration¶
This RFC is a non-breaking internal change for end users — the import syntax and stdlib behavior are unchanged.
For compiler/stdlib developers, migration is:
- Add
rust.module()directives to stdlib.incnfiles. - Convert stub-only functions to real Incan implementations where possible, keeping
@rust.externonly on irreducible primitives. - Remove hardcoded
FunctionInforegistries from the typechecker. - Remove hardcoded path-rewriting logic from emission.
- Verify behavior via existing tests (codegen snapshots, integration tests).
This can be done incrementally, one stdlib module at a time.
Alternatives considered¶
Keep everything in Rust, improve the wiring¶
Continue writing all stdlib implementations in Rust and invest in making the wiring less manual (e.g., auto-generating
typechecker registries from .incn stubs). This reduces the symptom (manual wiring) but not the cause (stdlib can't be
written in Incan). It also doesn't enable third-party Rust-backed libraries.
py4j / PyO3-style runtime bridge¶
Use a runtime bridge to call between Incan and Rust at runtime. Architecturally wrong for Incan: since Incan compiles into Rust, there's no separate runtime to bridge. A bridge would add serialization overhead, runtime complexity, and GC coordination problems for no benefit.
Auto-generate .incn stubs from Rust metadata¶
Use rustdoc --output-format json or similar. Potentially valuable as a tooling layer (incan stubgen
rust::serde_json) but doesn't address the core issue: the stdlib should be Incan, not generated wrappers. Could potentially
complement this RFC as a follow-up tool.
@rust.module (decorator) instead of rust.module() (directive)¶
Decorators modify the declaration that immediately follows them — a file-level @rust.module("...") with no following
declaration breaks that contract. The bare directive form is reserved for module-level statements; the decorator form is
reserved for future use when module is introduced as a keyword.
Drawbacks¶
- Stdlib compile time: compiling stdlib
.incnon every build adds cost vs. pre-compiledincan_stdlib. Should be negligible for the stdlib's size; caching and pre-compilation (Option B) can mitigate. - Downstream error quality:
@rust.externsignature mismatches surface asrustcerrors. Incan must wrap these with good diagnostics, but underlying messages may leak Rust types. Same challenge and mitigation asrust::imports. - Validation gap: the compiler trusts
rust.module()paths and@rust.externsignatures — some errors are only caught at therustcstage. Acceptable and consistent withrust::imports. - Library authoring complexity: creating a Rust-backed Incan library requires both Incan and Rust knowledge. Inherent to the use case; comparable to writing C extensions for Python.
Acceptance checklist¶
Closure definition (re-baselined)¶
RFC 023 is considered complete only when all three closure gates are true at the same time:
- [x] Source-of-truth gate: Stdlib/public Rust-backed module contracts are authored in
.incn, and the compiler no longer depends on duplicated handwritten signature registries or ad-hoc fallback sources for those contracts. - [x] Dogfooding gate: The stdlib is aggressively Incan-first; behavior that can reasonably be expressed in current Incan semantics is implemented in
.incnand compiled through the normal pipeline. - [x] Runtime-bridge gate: Remaining Rust runtime code is explicit and narrow (
rust.module()+@rust.externleaves only) and retained only where the boundary is genuinely irreducible at the current language/runtime stage.
Re-baselining note: this closure definition intentionally avoids smuggling unrelated later-RFC scope into RFC 023. Follow-up RFCs can extend semantics, but RFC 023 closes on clean boundaries and single-source ownership.
Spec / semantics¶
- [x] Stdlib
.incnfiles are compilable Incan source, not documentation-only stubs. - [x]
rust.module("path::to::module")directive is specified with clear syntax and semantics. - [x]
@rust.externsupersedes RFC 022's@std.builtin— same semantics, no source-category restriction. - [x]
DecoratorId::StdBuiltinrenamed toDecoratorId::RustExternwith canonical spelling"rust.extern". - [x]
@rust.externitems require arust.module()directive on their containing module. - [x]
@rust.externrestricted to free functions and trait default methods; rejected on instance methods. - [x] The irreducible primitives principle is documented:
@rust.externshould be minimized. - [x] Compiled Incan
std.*modules emitted under__incan_stdroot to avoid Ruststdshadowing.
Compilation pipeline¶
- [x] Compiler parses and compiles stdlib
.incnfiles through the normal pipeline. - [x] Typechecker derives function signatures from parsed
.incnAST, not hardcoded registries. - [x] Emitter uses
rust.module()path for@rust.externitems, not hardcoded path-rewriting. - [x] Hardcoded
testing_import_function_info()(and equivalents) are removed. - [x] Hardcoded
if is_stdlib_testing/if is_stdlib_webbranches are removed from emission. - [x]
escape_keywordapplied to module declarations,use-path segments, and filenames.
Trait bound inference and annotation¶
- [x] Emitter infers Rust trait bounds from operations on generic type parameters (minimum set per table above).
- [x] Inferred bounds are combined and emitted as
whereclause / inline bounds on generated Rust functions. - [x] Inference is transitive: calling a generic function that requires bounds propagates those bounds to the caller.
- [x] Parser/AST supports explicit trait bound syntax (
[T with (Eq, Debug)]). - [x] Explicit bounds are additive with inferred bounds.
- [x] Trait names in bounds are resolved through normal import/scoping and mapped to Rust trait bounds.
rust.module() directive¶
- [x] Parser/AST supports
rust.module("...")as a module-level directive. - [x] Path validated: must reference
incan_stdlibor a declared dependency inincan.toml. - [x]
@rust.externitems without a resolvable Rust backing path produce a clear error. - [x] No propagation to nested submodules.
- [x] Duplicate
rust.module()in the same module is a hard error. - [x]
rust.module()path sanitized: only valid Rust identifier segments separated by::are accepted.
Stdlib migration¶
- [x]
std.testingconverted to compilable Incan withfail()as sole@rust.externprimitive. - [x]
std.derives.*: non-@rust.externmethods compiled from Incan source. - [x]
std.webresponse builders: pure Incan where possible,@rust.externfor framework I/O. - [x] All stdlib
.incnfiles with@rust.externcarryrust.module()directives. - [x]
std.async.*behavior is runtime-backed via narrow@rust.externleaves (no broadfail_tplaceholder surface). > Note: remaining closeout is concentrated instd.async.select; follow-up language/library work is now tracked by RFC 038 and RFC 039 rather than being hand-waved inside RFC 023. - [x]
StdlibModuleInfofallback mapping removed (or marked deprecated).
Diagnostics checklist¶
- [x] Missing
rust.module()→ error with suggestion. - [x]
@rust.externwith non-trivial body → error suggesting...body or removing the decorator. - [x]
@rust.externon instance method → error suggesting free-function extraction. - [x]
@rust.externsignature mismatches → wrappedrustcdiagnostic pointing to.incndeclaration. - [x] Unused
rust.module()(no@rust.externitems) → warning. - [x] Invalid
rust.module()path (failed sanitization) → error with valid-path hint.
Tests¶
- [x] Typechecker tests: stdlib signatures resolved from
.incnsource. - [x] Codegen snapshot tests: compiled Incan stdlib functions in generated output.
- [x] Codegen snapshot tests: generic functions emit correct Rust trait bounds (inferred and explicit).
- [ ] Integration tests: behavioral equivalence with pre-migration stdlib.
- [x] Negative tests: calling a bounded generic function with a non-conforming type → Incan-level error.
- [x] Negative tests:
@rust.externwith non-trivial body, invalidrust.module()path, unusedrust.module()warning. - [x] Transitive inference test:
foo[T]callingassert_eq[T]acquiresPartialEq + Displaybounds from callee.