RFC 172: Async syntax without an import¶
- Status: In Progress
- Created: 2026-11-05
- Author(s): Danny Meijer (@dannymeijer)
- Related:
- RFC 022 (stdlib namespacing and compiler handoff)
- RFC 039 (
race for), whose import activation this RFC supersedes - RFC 142 (async consumers and structured concurrency)
- RFC 168 (
std.primitives)
- Issue: https://github.com/encero-systems/incan/issues/2683
- RFC PR: #2685
- Written against: v0.6
- Shipped in: —
Summary¶
async and await become reserved words of the language, and race for becomes language syntax. None of them needs an import. Importing remains how a program reaches the async library: spawn, sleep, channels and the rest. A module named async stays importable and is used like any other module, so std.async keeps its name.
Motivation¶
Async syntax activates when a module imports std.async or std.primitives.async. That rule came from a Cargo feature: importing the module was how a generated project got its async runtime. Native compilation has no generated project, and a program's runtime dependencies follow from what it uses, so the reason for the rule has gone.
What's left is a surprise for authors. This module defines a coroutine and awaits nothing from any library, yet it fails until an unrelated import appears:
async def answer() -> int:
return 42
Python separates these concerns. async def and await are reserved words since Python 3.7 and need no import. asyncio is imported only for the library that runs and coordinates coroutines.
Goals¶
asyncandawaitare always reserved.race forparses without an import.- A program that uses async syntax gets the runtime it needs because it uses it, not because of an import.
std.asynckeeps its name and works like any other module.
Non-Goals¶
- Changing what
async,awaitorrace formean. - Making
std.asyncnames ambient.spawn,sleepand channels are still imported. - Changing how library vocabulary activates. A package's own keywords still activate on import.
Guide-level explanation¶
from std.async import sleep
async def answer() -> int:
await sleep(0.1)
return 42
The import is there for sleep. Without the sleep call, the module needs no import at all.
A program can't declare either word: async = 1, def async() and a parameter named await are all refused. A module named async is different. Importing it binds it like any other module, so this works too:
from std import async
async def answer() -> int:
await async.sleep(0.1)
return 42
race stays an ordinary name, because only race for <name>: is syntax. [race for race in races] is still a comprehension.
Reference-level explanation¶
asyncandawaitmust be reserved in every module, whatever it imports. A variable, parameter, field, function or other declaration namedasyncorawaitmust be refused with a diagnostic that says the word is reserved.asyncfollowed bydefmust parse as the async modifier in every module, andawaitmust parse as the await expression.- A reserved word may be a segment of a module path. A module named
asyncmay be bound by an import, andasyncfollowed by.must resolve through that binding as any other module binding does. race for <name>:must parse as the race expression in every module.racemust remain an ordinary identifier, andrace forfollowed by anything else must keep its ordinary meaning.- Compiling a program that uses async syntax must select the async runtime from the checked use of that syntax, with no import required.
- A program that uses no async syntax and no async library, directly or through its dependencies, must not reach the async runtime.
Design details¶
Compatibility¶
Source that declares something named async or await no longer compiles. Python made the same change in 3.7. Incan has not reached 1.0, and renaming is mechanical. Existing async imports stay valid: an import that was only there to activate syntax becomes an unused import.
Interaction with std.primitives¶
Whether the syntax needs a library is now separate from where the library lives, so neither std.async nor std.primitives.async is special to the parser.
Alternatives considered¶
- Contextual keywords, like
assert.asynccould stay usable as a declared name outside the positions where it has meaning. That keeps two meanings for one word in every program, for little benefit. - Keep import activation. It preserves a rule whose reason has gone.
- Refuse
asyncas a module binding. Nothing about parsing requires it, sinceasyncas the modifier is only ever followed bydef. It would refuseimport std.asyncand makestd.asyncthe one module that can't be called through its name. - Rename the module, as Python's
asyncio. That changes every existing import to avoid a conflict that doesn't exist.
Drawbacks¶
- Two more words are unavailable as declared names.
- A reserved word that can still be a module binding needs one sentence to explain.
Layers affected¶
- Parser / AST:
asyncandawaitno longer check for an import,asyncis the modifier beforedefand a name before., andrace for <name>:no longer checks for an import. - Typechecker / Symbol resolution: refuse the reserved words as declared names, and resolve a module named
asynclike any other module binding. - Stdlib / Runtime: runtime selection follows checked async use.
- Formatter: no new syntax.
- LSP / Tooling: completion offers the keywords in every module.
Progress Checklist¶
- Reserve
asyncandawaitindependently of imports and supportrace forwithout an activation import. The implementation is on0.6.0-dev.9(#2685). - Verify reserved names, module access and existing programs. The implementation PR records its contract census, reserved-name refusal fixtures and example runs.
- Record the release containing these changes when it ships.
Design Decisions¶
asyncandawaitcan't be declared, matching Python.- A module named
asyncis imported and called like any other module, sostd.asynckeeps its name andasync.sleep(0.1)works. racestays an identifier, and onlyrace for <name>:is syntax.