RFC 171: Retire @rust.allow, @rust.extern and rust.module¶
- Status: In Progress
- Created: 2026-11-05
- Author(s): Danny Meijer (@dannymeijer)
- Related:
- RFC 005 (Rust interop)
- RFC 022 (stdlib namespacing and compiler handoff)
- RFC 023 (compilable stdlib and Rust module binding)
- RFC 024 (extensible derive protocol)
- RFC 041 (first-class Rust interop authoring)
- RFC 048 (checked contract metadata)
- RFC 057 (
@rust.allow), which this RFC supersedes - RFC 125 (Loaf registry and baked asset distribution)
- RFC 127 (Incan lint catalog)
- Issue: https://github.com/encero-systems/incan/issues/2682
- RFC PR: #2688
- Written against: v0.6
- Shipped in: —
Summary¶
Remove three interop forms whose reason for existing has gone. @rust.allow(...) suppressed lints in generated Rust, and the compiler no longer generates Rust source. @rust.extern marked a function whose body lives in a Rust module, and rust:: imports now name that function directly. rust.module(...) said which Rust module a file's bodies and macros came from, and rust:: imports now say that at each name. rust:: remains the one spelling for reaching Rust from Incan.
Motivation¶
@rust.allow(...) was added so an author could silence a Rust lint on the code generated for one declaration. Native compilation lowers checked Incan directly, so no generated Rust exists to carry the attribute and no Rust lint runs over it. The decorator is validated and then does nothing. An author who writes it believes they have suppressed something, and they haven't.
@rust.extern came from a time when Rust items couldn't be imported as ordinary names. An author wrote an Incan signature with no meaningful body, bound the file to a crate with rust.module(...), and the compiler connected the two. With first-class rust:: imports, the same function can be imported by name, so a signature written by hand is a second statement of a fact that the Rust item already states.
rust.module(...) has one other job: it lets a marker trait stand for a Rust derive macro, so @derive(IntoResponse) reaches the macro in incan_web_macros. That is a third route to a Rust item, and a rust:: import can name the macro directly.
@rust.extern has also been carrying something that has nothing to do with Rust. std.testing declares what a test marker means through @rust.extern(metadata={...}), and incan test reads it from there.
Goals¶
- Remove
@rust.allow(...), superseding RFC 057. - Remove
@rust.extern, so a Rust-backed function is either arust::import or an ordinary Incan function that calls one. - Remove
rust.module(...), so a Rust derive macro is reached through arust::import. - Give test markers a declaration that doesn't depend on Rust.
- Refuse all three forms with a diagnostic that names the replacement.
Non-Goals¶
- Changing
@rust.derive,rusttypeor other interop forms. - Adding an Incan lint-suppression surface. RFC 127 owns Incan lints.
- Changing what any test marker does.
Guide-level explanation¶
Lint suppression¶
Delete the decorator. Nothing it did remains to be replaced.
@rust.allow("dead_code") # refused: the compiler generates no Rust to carry this
def helper() -> int:
return 1
Rust-backed functions¶
Import the Rust function by name, and publish it the way any other name is published:
pub from rust::incan_std_testing import fail_t
When the public function needs Incan policy or its own documentation, write an ordinary function that calls the import:
from rust::incan_std_testing import fail_t as native_fail
pub def fail_t[T](msg: str) -> T:
"""Fail the current test with `msg`."""
return native_fail(msg)
Rust derive macros¶
Import the macro by name, and @derive uses it like any other derive:
pub from rust::incan_web_macros import IntoResponse
from std.web import IntoResponse
@derive(IntoResponse)
model Greeting:
message: str
Test markers¶
A marker declares its meaning with @marker, a decorator from std.testing, and incan test reads it through checked metadata:
from rust::incan_std_testing import skip as native_skip
@marker(kind="skip", runner_only=true)
pub def skip(reason: str = "") -> None:
"""Skip a test unconditionally."""
native_skip(reason)
Reference-level explanation¶
- A declaration decorated with
@rust.allow(...)must be refused. The diagnostic must say that native compilation generates no Rust source and that the decorator can be deleted. - A declaration decorated with
@rust.externmust be refused. The diagnostic must name arust::import as the replacement. - A
rust.module(...)statement must be refused. The diagnostic must name arust::import as the replacement. - A
rust::import must keep its current checking and its canonical identity through re-exports, sopub from rust::... import nameand a wrapper both present a checked signature to consumers. - A
rust::import of a generic Rust function must be callable like a generic Incan function, including a type parameter that appears only in its return type. - A
rust::import must be able to name a Rust derive macro, and@derivemust accept that name. std.testingmust declare marker semantics with@marker(...).incan testmust read them through checked metadata, and they must not depend on any Rust interop form.- Generated references and the feature inventory must not list any of the three forms as available.
Design details¶
Interaction with existing features¶
A method body supplied by Rust, such as a comparison on a Rust-backed type, becomes an ordinary method that calls the imported function. Decorators other than @rust.allow and @rust.extern are unaffected.
Compatibility¶
This breaks source that uses any of the three forms. Incan has not reached 1.0, and the diagnostics state the mechanical change. A published Loaf that uses one declares the toolchain range it builds with (RFC 125), so a toolchain that refuses the form doesn't select it.
Alternatives considered¶
- Keep
@rust.allow(...)as a no-op. It would keep telling authors that it does something. - Keep
@rust.externbesiderust::imports. That leaves two ways to reach one Rust function, and a handwritten signature that can disagree with the Rust item. - Keep
rust.module(...)for derive macros. That leaves a second route to Rust for one kind of item, and a marker trait with no behavior of its own. - Move marker metadata to a renamed decorator in the
rustnamespace. Nothing about a test marker involves Rust.
Drawbacks¶
- A wrapper is more text than a decorated signature when a function needs its own documentation.
- Standard library modules that use these forms have to be rewritten.
Layers affected¶
- Typechecker / Symbol resolution: refuse all three forms, remove the extern declaration path, resolve
rust::imports of derive macros, and check@marker. - IR Lowering: remove extern delegation.
- Stdlib / Runtime: replace existing uses with
rust::imports or wrappers, and declare test markers with@marker. - Test runner: read marker semantics through checked metadata.
- Docs / Tooling: decorator reference, generated feature inventory, LSP completion.
Progress Checklist¶
- Retire the three Rust interop forms and diagnose their replacements. Direct Rust imports and checked test markers are covered by the implementation on
0.6.0-dev.9(#2688). - Verify the supported compiler and installed toolchain paths. The implementation PR records its contract census, Oven tests and Rust facet suite results.
- Record the release containing these changes when it ships.
Design Decisions¶
rust.module(...)is retired too. It has one use beyond@rust.extern, reaching derive macros, and arust::import can do that, which keepsrust::as the one spelling.- Test markers use
@marker(...)fromstd.testing, read through checked metadata, because nothing about them involves Rust.