nandi/rustnimpublic Fork
⑂ 911457b
Commits
⬇ Clone ▾
git clone https://git.rickub.com/nandi/rustnim.git
git clone ssh://git@rickub.com/nandi/rustnim.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Download ZIP

Lower `async fn` and `impl Future` as a thunk, since Rust's futures are lazy

`async fn f() -> T` is `fn f() -> impl Future<Output = T>` -- Rust's own
desugaring -- so async and return-position `impl Future` are one feature.

The mapping that suggests itself, `async fn` onto Nim's `{.async.}`, is wrong
and measurably so. A Rust future is inert: calling an async fn runs none of
its body until something polls it. A Nim `{.async.}` proc runs eagerly, up to
its first await, at the moment it is called. Probed side by side on the same
program, rustc prints "before call / after call / body ran" where Nim prints
"before call / body ran / after call". Every side effect ahead of the first
await would move.

So a future is a thunk: RsFuture[T] = proc (): Future[T]. Calling the
function builds one and runs nothing; `.await` calls it. That restores Rust's
order exactly. tests/cases/042-async.rs pins it against the real futures
crate -- oracle-only, as with bitflags and log -- and includes a future that
is built and never awaited, whose body must not run.

The async runtime is emitted only when the input needs it, since importing
std/asyncdispatch costs compile time and most programs have no futures. A
captured openArray gets the same copy as a returned iterator, for the same
reason: Nim will not let a closure capture a view.

Not modelled and rejected where they appear: cancellation, Pin, Send/Sync
bounds, and the choice of executor -- Nim has one global dispatcher where
Rust's runtime is the application's choice, so block_on from any executor is
the same operation here.

Async blockers in the 400-crate survey: 15 to 1. Accepted crates unchanged at
18, Nim-compilable at 12. Eleventh feature with that result.

48 differential cases, all green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
nandithebull committed 2026-09-19T06:14:04-07:00 Browse files
911457b parent: fe27b4b
modified Cargo.lock +107 -0
@@ -8,12 +8,112 @@ version = "2.13.2"
88 source = "registry+https://github.com/rust-lang/crates.io-index"
99 checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06"
1010
11+[[package]]
12+name = "futures"
13+version = "0.3.34"
14+source = "registry+https://github.com/rust-lang/crates.io-index"
15+checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3"
16+dependencies = [
17+ "futures-channel",
18+ "futures-core",
19+ "futures-executor",
20+ "futures-io",
21+ "futures-sink",
22+ "futures-task",
23+ "futures-util",
24+]
25+
26+[[package]]
27+name = "futures-channel"
28+version = "0.3.34"
29+source = "registry+https://github.com/rust-lang/crates.io-index"
30+checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4"
31+dependencies = [
32+ "futures-core",
33+ "futures-sink",
34+]
35+
36+[[package]]
37+name = "futures-core"
38+version = "0.3.34"
39+source = "registry+https://github.com/rust-lang/crates.io-index"
40+checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e"
41+
42+[[package]]
43+name = "futures-executor"
44+version = "0.3.34"
45+source = "registry+https://github.com/rust-lang/crates.io-index"
46+checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432"
47+dependencies = [
48+ "futures-core",
49+ "futures-task",
50+ "futures-util",
51+]
52+
53+[[package]]
54+name = "futures-io"
55+version = "0.3.34"
56+source = "registry+https://github.com/rust-lang/crates.io-index"
57+checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed"
58+
59+[[package]]
60+name = "futures-macro"
61+version = "0.3.34"
62+source = "registry+https://github.com/rust-lang/crates.io-index"
63+checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44"
64+dependencies = [
65+ "proc-macro2",
66+ "quote",
67+ "syn",
68+]
69+
70+[[package]]
71+name = "futures-sink"
72+version = "0.3.34"
73+source = "registry+https://github.com/rust-lang/crates.io-index"
74+checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d"
75+
76+[[package]]
77+name = "futures-task"
78+version = "0.3.34"
79+source = "registry+https://github.com/rust-lang/crates.io-index"
80+checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd"
81+
82+[[package]]
83+name = "futures-util"
84+version = "0.3.34"
85+source = "registry+https://github.com/rust-lang/crates.io-index"
86+checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc"
87+dependencies = [
88+ "futures-channel",
89+ "futures-core",
90+ "futures-io",
91+ "futures-macro",
92+ "futures-sink",
93+ "futures-task",
94+ "memchr",
95+ "pin-project-lite",
96+ "slab",
97+]
98+
1199 [[package]]
12100 name = "log"
13101 version = "0.4.34"
14102 source = "registry+https://github.com/rust-lang/crates.io-index"
15103 checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
16104
105+[[package]]
106+name = "memchr"
107+version = "2.8.3"
108+source = "registry+https://github.com/rust-lang/crates.io-index"
109+checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
110+
111+[[package]]
112+name = "pin-project-lite"
113+version = "0.2.17"
114+source = "registry+https://github.com/rust-lang/crates.io-index"
115+checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
116+
17117 [[package]]
18118 name = "proc-macro2"
19119 version = "1.0.107"
@@ -37,11 +137,18 @@ name = "rustnim"
37137 version = "0.1.0"
38138 dependencies = [
39139 "bitflags",
140+ "futures",
40141 "log",
41142 "proc-macro2",
42143 "syn",
43144 ]
44145
146+[[package]]
147+name = "slab"
148+version = "0.4.12"
149+source = "registry+https://github.com/rust-lang/crates.io-index"
150+checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
151+
45152 [[package]]
46153 name = "syn"
47154 version = "3.0.5"
@@ -8,12 +8,112 @@ version = "2.13.2"
8 source = "registry+https://github.com/rust-lang/crates.io-index"8 source = "registry+https://github.com/rust-lang/crates.io-index"
9 checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06"9 checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06"
10 10
11+[[package]]
12+name = "futures"
13+version = "0.3.34"
14+source = "registry+https://github.com/rust-lang/crates.io-index"
15+checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3"
16+dependencies = [
17+ "futures-channel",
18+ "futures-core",
19+ "futures-executor",
20+ "futures-io",
21+ "futures-sink",
22+ "futures-task",
23+ "futures-util",
24+]
25+
26+[[package]]
27+name = "futures-channel"
28+version = "0.3.34"
29+source = "registry+https://github.com/rust-lang/crates.io-index"
30+checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4"
31+dependencies = [
32+ "futures-core",
33+ "futures-sink",
34+]
35+
36+[[package]]
37+name = "futures-core"
38+version = "0.3.34"
39+source = "registry+https://github.com/rust-lang/crates.io-index"
40+checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e"
41+
42+[[package]]
43+name = "futures-executor"
44+version = "0.3.34"
45+source = "registry+https://github.com/rust-lang/crates.io-index"
46+checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432"
47+dependencies = [
48+ "futures-core",
49+ "futures-task",
50+ "futures-util",
51+]
52+
53+[[package]]
54+name = "futures-io"
55+version = "0.3.34"
56+source = "registry+https://github.com/rust-lang/crates.io-index"
57+checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed"
58+
59+[[package]]
60+name = "futures-macro"
61+version = "0.3.34"
62+source = "registry+https://github.com/rust-lang/crates.io-index"
63+checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44"
64+dependencies = [
65+ "proc-macro2",
66+ "quote",
67+ "syn",
68+]
69+
70+[[package]]
71+name = "futures-sink"
72+version = "0.3.34"
73+source = "registry+https://github.com/rust-lang/crates.io-index"
74+checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d"
75+
76+[[package]]
77+name = "futures-task"
78+version = "0.3.34"
79+source = "registry+https://github.com/rust-lang/crates.io-index"
80+checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd"
81+
82+[[package]]
83+name = "futures-util"
84+version = "0.3.34"
85+source = "registry+https://github.com/rust-lang/crates.io-index"
86+checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc"
87+dependencies = [
88+ "futures-channel",
89+ "futures-core",
90+ "futures-io",
91+ "futures-macro",
92+ "futures-sink",
93+ "futures-task",
94+ "memchr",
95+ "pin-project-lite",
96+ "slab",
97+]
98+
11 [[package]]99 [[package]]
12 name = "log"100 name = "log"
13 version = "0.4.34"101 version = "0.4.34"
14 source = "registry+https://github.com/rust-lang/crates.io-index"102 source = "registry+https://github.com/rust-lang/crates.io-index"
15 checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"103 checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
16 104
105+[[package]]
106+name = "memchr"
107+version = "2.8.3"
108+source = "registry+https://github.com/rust-lang/crates.io-index"
109+checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
110+
111+[[package]]
112+name = "pin-project-lite"
113+version = "0.2.17"
114+source = "registry+https://github.com/rust-lang/crates.io-index"
115+checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
116+
17 [[package]]117 [[package]]
18 name = "proc-macro2"118 name = "proc-macro2"
19 version = "1.0.107"119 version = "1.0.107"
@@ -37,11 +137,18 @@ name = "rustnim"
37 version = "0.1.0"137 version = "0.1.0"
38 dependencies = [138 dependencies = [
39 "bitflags",139 "bitflags",
140+ "futures",
40 "log",141 "log",
41 "proc-macro2",142 "proc-macro2",
42 "syn",143 "syn",
43 ]144 ]
44 145
146+[[package]]
147+name = "slab"
148+version = "0.4.12"
149+source = "registry+https://github.com/rust-lang/crates.io-index"
150+checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
151+
45 [[package]]152 [[package]]
46 name = "syn"153 name = "syn"
47 version = "3.0.5"154 version = "3.0.5"
modified Cargo.toml +1 -0
@@ -13,3 +13,4 @@ syn = { version = "3.0.5", features = ["full", "extra-traits", "visit"] }
1313 [dev-dependencies]
1414 bitflags = "2"
1515 log = "0.4"
16+futures = "0.3"
@@ -13,3 +13,4 @@ syn = { version = "3.0.5", features = ["full", "extra-traits", "visit"] }
13 [dev-dependencies]13 [dev-dependencies]
14 bitflags = "2"14 bitflags = "2"
15 log = "0.4"15 log = "0.4"
16+futures = "0.3"
modified DESIGN.md +32 -2
@@ -777,8 +777,38 @@ Three things this needed that are worth recording:
777777 fresh name and each parameter aliases it, since two `let x` would collide in
778778 one Nim scope.
779779
780-What remains in return position is `impl Future` (async), `impl Display`, and
781-traits from other crates — those are still rejected, with the trait named.
780+### `impl Future`: a thunk, because Rust's futures are lazy and Nim's are not
781+
782+`async fn f() -> T` *is* `fn f() -> impl Future<Output = T>` — Rust's own
783+desugaring — so the two arrived together.
784+
785+The mapping that suggests itself, `async fn` onto Nim's `{.async.}`, is
786+wrong, and measurably so. A Rust future is inert: calling an `async fn` runs
787+none of its body until something polls it. A Nim `{.async.}` proc runs
788+eagerly, up to its first `await`, at the moment it is called. Probed side by
789+side on the same program:
790+
791+```
792+Rust: before call │ after call │ body ran │ awaited: 42
793+Nim: before call │ body ran │ after call │ awaited: 42
794+```
795+
796+Every side effect ahead of the first `await` would move. So a future is a
797+**thunk** — `RsFuture[T] = proc (): Future[T]`. Calling the function builds
798+one and runs nothing; `.await` calls it. That restores Rust's order exactly,
799+and `tests/cases/042-async.rs` pins it, including a future that is built and
800+never awaited whose body must not run.
801+
802+The async runtime is emitted only when something needs it: `import
803+std/asyncdispatch` costs compile time and most programs have no futures.
804+
805+Not modelled, and rejected where they appear: cancellation, `Pin`,
806+`Send`/`Sync` bounds, and the choice of executor. Nim has one global
807+dispatcher where Rust's runtime is chosen by the application, so `block_on`
808+from any executor maps to the same operation.
809+
810+What remains in return position is `impl Display` and traits from other
811+crates — still rejected, with the trait named.
782812
783813 ### Trait methods are named by trait, not by type
784814
@@ -777,8 +777,38 @@ Three things this needed that are worth recording:
777 fresh name and each parameter aliases it, since two `let x` would collide in777 fresh name and each parameter aliases it, since two `let x` would collide in
778 one Nim scope.778 one Nim scope.
779 779
780-What remains in return position is `impl Future` (async), `impl Display`, and780+### `impl Future`: a thunk, because Rust's futures are lazy and Nim's are not
781-traits from other crates — those are still rejected, with the trait named.781+
782+`async fn f() -> T` *is* `fn f() -> impl Future<Output = T>` — Rust's own
783+desugaring — so the two arrived together.
784+
785+The mapping that suggests itself, `async fn` onto Nim's `{.async.}`, is
786+wrong, and measurably so. A Rust future is inert: calling an `async fn` runs
787+none of its body until something polls it. A Nim `{.async.}` proc runs
788+eagerly, up to its first `await`, at the moment it is called. Probed side by
789+side on the same program:
790+
791+```
792+Rust: before call │ after call │ body ran │ awaited: 42
793+Nim: before call │ body ran │ after call │ awaited: 42
794+```
795+
796+Every side effect ahead of the first `await` would move. So a future is a
797+**thunk** — `RsFuture[T] = proc (): Future[T]`. Calling the function builds
798+one and runs nothing; `.await` calls it. That restores Rust's order exactly,
799+and `tests/cases/042-async.rs` pins it, including a future that is built and
800+never awaited whose body must not run.
801+
802+The async runtime is emitted only when something needs it: `import
803+std/asyncdispatch` costs compile time and most programs have no futures.
804+
805+Not modelled, and rejected where they appear: cancellation, `Pin`,
806+`Send`/`Sync` bounds, and the choice of executor. Nim has one global
807+dispatcher where Rust's runtime is chosen by the application, so `block_on`
808+from any executor maps to the same operation.
809+
810+What remains in return position is `impl Display` and traits from other
811+crates — still rejected, with the trait named.
782 812
783 ### Trait methods are named by trait, not by type813 ### Trait methods are named by trait, not by type
784 814
modified README.md +8 -0
@@ -128,6 +128,14 @@ million-element iterator after three elements returns in 3ms, so this is not
128128 "collect into a sequence". `.map` and `.filter` work in that form, where they
129129 are exactly a transform and a skip.
130130
131+## Async
132+
133+`async fn` becomes a thunk, not a Nim `{.async.}` proc. Rust's futures are
134+inert until polled; Nim's run eagerly to the first `await` when called — so
135+the obvious mapping reorders every side effect ahead of the first await.
136+Measured side by side, then fixed by making a future
137+`proc (): Future[T]`, which `.await` calls.
138+
131139 ## Does it generalise?
132140
133141 `base16ct` is the crate this was built toward, so a second one was tried.
@@ -128,6 +128,14 @@ million-element iterator after three elements returns in 3ms, so this is not
128 "collect into a sequence". `.map` and `.filter` work in that form, where they128 "collect into a sequence". `.map` and `.filter` work in that form, where they
129 are exactly a transform and a skip.129 are exactly a transform and a skip.
130 130
131+## Async
132+
133+`async fn` becomes a thunk, not a Nim `{.async.}` proc. Rust's futures are
134+inert until polled; Nim's run eagerly to the first `await` when called — so
135+the obvious mapping reorders every side effect ahead of the first await.
136+Measured side by side, then fixed by making a future
137+`proc (): Future[T]`, which `.await` calls.
138+
131 ## Does it generalise?139 ## Does it generalise?
132 140
133 `base16ct` is the crate this was built toward, so a second one was tried.141 `base16ct` is the crate this was built toward, so a second one was tried.
modified src/lower.rs +104 -4
@@ -277,6 +277,9 @@ pub struct Lowerer {
277277 fmt_param: Option<String>,
278278 /// `type X<T> = ...`, expanded before any type is mapped.
279279 aliases: HashMap<String, (Vec<String>, syn::Type)>,
280+ /// Whether anything in the input is a future, which decides if the async
281+ /// runtime is emitted.
282+ uses_async: bool,
280283 /// Module names supplied as separate input files. A `mod x;` naming one
281284 /// of these is satisfied by that file having been passed in.
282285 pub modules: Vec<String>,
@@ -336,6 +339,7 @@ impl Lowerer {
336339 vec_expect: None,
337340 forwards: Vec::new(),
338341 aliases: HashMap::new(),
342+ uses_async: false,
339343 modules: Vec::new(),
340344 emitted: 0,
341345 features: Vec::new(),
@@ -419,6 +423,13 @@ impl Lowerer {
419423 self.collect(item)?;
420424 }
421425 }
426+ // The async runtime is only pulled in when something needs it: it
427+ // costs compile time, and most programs have no futures at all.
428+ if self.uses_async {
429+ self.out.push_str(include_str!("prelude_async.nim"));
430+ self.blank();
431+ }
432+
422433 // Pass 2: type definitions, which every signature may mention.
423434 for (m, f) in files {
424435 self.cur_mod = m.clone();
@@ -554,6 +565,9 @@ impl Lowerer {
554565 match item {
555566 Item::Fn(f) => {
556567 let (params, ret) = self.signature(&f.sig)?;
568+ if matches!(ret, Nim::Future(_)) {
569+ self.uses_async = true;
570+ }
557571 let mut gen_names = Self::generics_of(&f.sig.generics);
558572 gen_names.extend(self.impl_trait_params(&f.sig));
559573 let name = f.sig.ident.to_string();
@@ -767,6 +781,9 @@ impl Lowerer {
767781 };
768782 let mname = m.sig.ident.to_string();
769783 let (mut params, ret) = self.signature(&m.sig)?;
784+ if matches!(ret, Nim::Future(_)) {
785+ self.uses_async = true;
786+ }
770787 let mut gen_names = self.impl_generics.clone();
771788 gen_names.extend(Self::generics_of(&m.sig.generics));
772789 let recv = if takes_self(&m.sig) {
@@ -786,6 +803,9 @@ impl Lowerer {
786803 for it in &im.items {
787804 if let syn::ImplItem::Fn(m) = it {
788805 let (mut params, ret) = self.signature(&m.sig)?;
806+ if matches!(ret, Nim::Future(_)) {
807+ self.uses_async = true;
808+ }
789809 let mut gen_names = self.impl_generics.clone();
790810 gen_names.extend(Self::generics_of(&m.sig.generics));
791811 if takes_self(&m.sig) {
@@ -1251,9 +1271,6 @@ impl Lowerer {
12511271 fn signature(&self, sig: &syn::Signature) -> Result<(Vec<Nim>, Nim), String> {
12521272 // `unsafe fn` marks a contract for callers; it does not change what
12531273 // the body means, so it lowers like any other proc.
1254- if sig.asyncness.is_some() {
1255- return Err(format!("`async fn {}`: Nim has no equivalent", sig.ident));
1256- }
12571274 // Lifetime parameters carry no runtime meaning and Nim is GC'd, so
12581275 // they disappear. Type parameters become Nim generic parameters.
12591276 // Const parameters have no Nim equivalent and are still rejected.
@@ -1291,6 +1308,13 @@ impl Lowerer {
12911308 if returns_borrow(t) { n } else { n.owned() }
12921309 }
12931310 };
1311+ // `async fn f() -> T` *is* `fn f() -> impl Future<Output = T>`; that
1312+ // is Rust's own desugaring, so the signature carries it.
1313+ let ret = if sig.asyncness.is_some() {
1314+ Nim::Future(Box::new(ret))
1315+ } else {
1316+ ret
1317+ };
12941318 Ok((params, ret))
12951319 }
12961320
@@ -2186,6 +2210,57 @@ impl Lowerer {
21862210 // `-> impl Iterator<Item = T>` becomes a Nim closure iterator, which
21872211 // is lazy and first-class as Rust's is. The body's trailing expression
21882212 // is the chain it yields from.
2213+ // A future's body must not run until it is awaited, so it goes inside
2214+ // a thunk. See `src/prelude_async.nim` for the measurement that makes
2215+ // this necessary rather than merely tidy.
2216+ if let Nim::Future(out) = &ret {
2217+ let out = (**out).clone();
2218+ let views: Vec<(String, Nim)> = typed
2219+ .iter()
2220+ .zip(ptys.iter())
2221+ .filter(|(_, t)| matches!(t, Nim::OpenArray(_)))
2222+ .filter_map(|(p, t)| match &*p.pat {
2223+ Pat::Ident(i) => Some((i.ident.to_string(), t.clone())),
2224+ _ => None,
2225+ })
2226+ .collect();
2227+ // Same restriction as a returned iterator: Nim will not let a
2228+ // closure capture an `openArray`.
2229+ for (n, t) in &views {
2230+ let copy = self.fresh("Cap");
2231+ let elem_t = match t {
2232+ Nim::OpenArray(e) => (**e).clone(),
2233+ other => other.clone(),
2234+ };
2235+ self.line(&format!("let {}: seq[{}] = @{}", copy, elem_t.render(), ident(n)));
2236+ self.bind_alias(
2237+ n,
2238+ Alias::Value { code: copy, ty: Some(Nim::Seq(Box::new(elem_t))) },
2239+ );
2240+ }
2241+ self.line(&format!(
2242+ "result = proc (): Future[{}] {{.async, closure.}} =",
2243+ out.render()
2244+ ));
2245+ self.indent += 1;
2246+ let outer_r = self.ret.replace(out.clone());
2247+ let outer_t = self
2248+ .target
2249+ .replace(("result".to_string(), Some(out.clone())));
2250+ let before = self.out.len();
2251+ let tail = self.block_body_at(body, Some(&out))?;
2252+ self.emit_tail(tail);
2253+ if self.out.len() == before {
2254+ self.line("discard");
2255+ }
2256+ self.ret = outer_r;
2257+ self.target = outer_t;
2258+ self.indent -= 2;
2259+ self.fn_generics = outer_fg;
2260+ self.pop_scope();
2261+ self.blank();
2262+ return Ok(());
2263+ }
21892264 if let Nim::Iter(elem) = &ret {
21902265 let elem = (**elem).clone();
21912266 // The *declared* parameter types, not the scope's, which record
@@ -3738,6 +3813,22 @@ impl Lowerer {
37383813 .into()),
37393814 },
37403815 Expr::Closure(c) => self.closure(c, expect),
3816+ // `.await` calls the thunk and waits. Calling it is what starts
3817+ // the body, which is how Rust's laziness is preserved.
3818+ Expr::Await(a) => {
3819+ let v = self.expr(&a.base)?;
3820+ let out = match &v.ty {
3821+ Some(Nim::Future(t)) => Some((**t).clone()),
3822+ _ => {
3823+ return Err(
3824+ "`.await` on a value that is not a future; its type \
3825+ must be known here"
3826+ .into(),
3827+ )
3828+ }
3829+ };
3830+ Ok(Val::new(format!("(await {}())", v.code), out))
3831+ }
37413832 Expr::Try(t) => self.try_op(t),
37423833 Expr::Call(c) => self.call(c, expect),
37433834 Expr::MethodCall(m) => self.method(m, expect),
@@ -4625,6 +4716,16 @@ impl Lowerer {
46254716 }
46264717 }
46274718
4719+ // `block_on(f)` — whichever executor it came from, the operation is
4720+ // the same: run the future to completion and give back its output.
4721+ if name == "block_on" && codes.len() == 1 {
4722+ let out = match args[0].ty.clone() {
4723+ Some(Nim::Future(t)) => Some(*t),
4724+ _ => None,
4725+ };
4726+ return Ok(Val::new(format!("rsBlockOn({})", codes[0]), out));
4727+ }
4728+
46284729 // `log::set_max_level` / `log::max_level`.
46294730 if name == "set_max_level" && codes.len() == 1 {
46304731 return Ok(Val::new(
@@ -5978,7 +6079,6 @@ fn item_kind(i: &Item) -> &'static str {
59786079 fn expr_kind(e: &Expr) -> &'static str {
59796080 match e {
59806081 Expr::Async(_) => "`async` block",
5981- Expr::Await(_) => "`.await`",
59826082 Expr::Try(_) => "`?`",
59836083 Expr::Range(_) => "range",
59846084 Expr::Match(_) => "`match` (only statement position is implemented)",
@@ -277,6 +277,9 @@ pub struct Lowerer {
277 fmt_param: Option<String>,277 fmt_param: Option<String>,
278 /// `type X<T> = ...`, expanded before any type is mapped.278 /// `type X<T> = ...`, expanded before any type is mapped.
279 aliases: HashMap<String, (Vec<String>, syn::Type)>,279 aliases: HashMap<String, (Vec<String>, syn::Type)>,
280+ /// Whether anything in the input is a future, which decides if the async
281+ /// runtime is emitted.
282+ uses_async: bool,
280 /// Module names supplied as separate input files. A `mod x;` naming one283 /// Module names supplied as separate input files. A `mod x;` naming one
281 /// of these is satisfied by that file having been passed in.284 /// of these is satisfied by that file having been passed in.
282 pub modules: Vec<String>,285 pub modules: Vec<String>,
@@ -336,6 +339,7 @@ impl Lowerer {
336 vec_expect: None,339 vec_expect: None,
337 forwards: Vec::new(),340 forwards: Vec::new(),
338 aliases: HashMap::new(),341 aliases: HashMap::new(),
342+ uses_async: false,
339 modules: Vec::new(),343 modules: Vec::new(),
340 emitted: 0,344 emitted: 0,
341 features: Vec::new(),345 features: Vec::new(),
@@ -419,6 +423,13 @@ impl Lowerer {
419 self.collect(item)?;423 self.collect(item)?;
420 }424 }
421 }425 }
426+ // The async runtime is only pulled in when something needs it: it
427+ // costs compile time, and most programs have no futures at all.
428+ if self.uses_async {
429+ self.out.push_str(include_str!("prelude_async.nim"));
430+ self.blank();
431+ }
432+
422 // Pass 2: type definitions, which every signature may mention.433 // Pass 2: type definitions, which every signature may mention.
423 for (m, f) in files {434 for (m, f) in files {
424 self.cur_mod = m.clone();435 self.cur_mod = m.clone();
@@ -554,6 +565,9 @@ impl Lowerer {
554 match item {565 match item {
555 Item::Fn(f) => {566 Item::Fn(f) => {
556 let (params, ret) = self.signature(&f.sig)?;567 let (params, ret) = self.signature(&f.sig)?;
568+ if matches!(ret, Nim::Future(_)) {
569+ self.uses_async = true;
570+ }
557 let mut gen_names = Self::generics_of(&f.sig.generics);571 let mut gen_names = Self::generics_of(&f.sig.generics);
558 gen_names.extend(self.impl_trait_params(&f.sig));572 gen_names.extend(self.impl_trait_params(&f.sig));
559 let name = f.sig.ident.to_string();573 let name = f.sig.ident.to_string();
@@ -767,6 +781,9 @@ impl Lowerer {
767 };781 };
768 let mname = m.sig.ident.to_string();782 let mname = m.sig.ident.to_string();
769 let (mut params, ret) = self.signature(&m.sig)?;783 let (mut params, ret) = self.signature(&m.sig)?;
784+ if matches!(ret, Nim::Future(_)) {
785+ self.uses_async = true;
786+ }
770 let mut gen_names = self.impl_generics.clone();787 let mut gen_names = self.impl_generics.clone();
771 gen_names.extend(Self::generics_of(&m.sig.generics));788 gen_names.extend(Self::generics_of(&m.sig.generics));
772 let recv = if takes_self(&m.sig) {789 let recv = if takes_self(&m.sig) {
@@ -786,6 +803,9 @@ impl Lowerer {
786 for it in &im.items {803 for it in &im.items {
787 if let syn::ImplItem::Fn(m) = it {804 if let syn::ImplItem::Fn(m) = it {
788 let (mut params, ret) = self.signature(&m.sig)?;805 let (mut params, ret) = self.signature(&m.sig)?;
806+ if matches!(ret, Nim::Future(_)) {
807+ self.uses_async = true;
808+ }
789 let mut gen_names = self.impl_generics.clone();809 let mut gen_names = self.impl_generics.clone();
790 gen_names.extend(Self::generics_of(&m.sig.generics));810 gen_names.extend(Self::generics_of(&m.sig.generics));
791 if takes_self(&m.sig) {811 if takes_self(&m.sig) {
@@ -1251,9 +1271,6 @@ impl Lowerer {
1251 fn signature(&self, sig: &syn::Signature) -> Result<(Vec<Nim>, Nim), String> {1271 fn signature(&self, sig: &syn::Signature) -> Result<(Vec<Nim>, Nim), String> {
1252 // `unsafe fn` marks a contract for callers; it does not change what1272 // `unsafe fn` marks a contract for callers; it does not change what
1253 // the body means, so it lowers like any other proc.1273 // the body means, so it lowers like any other proc.
1254- if sig.asyncness.is_some() {
1255- return Err(format!("`async fn {}`: Nim has no equivalent", sig.ident));
1256- }
1257 // Lifetime parameters carry no runtime meaning and Nim is GC'd, so1274 // Lifetime parameters carry no runtime meaning and Nim is GC'd, so
1258 // they disappear. Type parameters become Nim generic parameters.1275 // they disappear. Type parameters become Nim generic parameters.
1259 // Const parameters have no Nim equivalent and are still rejected.1276 // Const parameters have no Nim equivalent and are still rejected.
@@ -1291,6 +1308,13 @@ impl Lowerer {
1291 if returns_borrow(t) { n } else { n.owned() }1308 if returns_borrow(t) { n } else { n.owned() }
1292 }1309 }
1293 };1310 };
1311+ // `async fn f() -> T` *is* `fn f() -> impl Future<Output = T>`; that
1312+ // is Rust's own desugaring, so the signature carries it.
1313+ let ret = if sig.asyncness.is_some() {
1314+ Nim::Future(Box::new(ret))
1315+ } else {
1316+ ret
1317+ };
1294 Ok((params, ret))1318 Ok((params, ret))
1295 }1319 }
1296 1320
@@ -2186,6 +2210,57 @@ impl Lowerer {
2186 // `-> impl Iterator<Item = T>` becomes a Nim closure iterator, which2210 // `-> impl Iterator<Item = T>` becomes a Nim closure iterator, which
2187 // is lazy and first-class as Rust's is. The body's trailing expression2211 // is lazy and first-class as Rust's is. The body's trailing expression
2188 // is the chain it yields from.2212 // is the chain it yields from.
2213+ // A future's body must not run until it is awaited, so it goes inside
2214+ // a thunk. See `src/prelude_async.nim` for the measurement that makes
2215+ // this necessary rather than merely tidy.
2216+ if let Nim::Future(out) = &ret {
2217+ let out = (**out).clone();
2218+ let views: Vec<(String, Nim)> = typed
2219+ .iter()
2220+ .zip(ptys.iter())
2221+ .filter(|(_, t)| matches!(t, Nim::OpenArray(_)))
2222+ .filter_map(|(p, t)| match &*p.pat {
2223+ Pat::Ident(i) => Some((i.ident.to_string(), t.clone())),
2224+ _ => None,
2225+ })
2226+ .collect();
2227+ // Same restriction as a returned iterator: Nim will not let a
2228+ // closure capture an `openArray`.
2229+ for (n, t) in &views {
2230+ let copy = self.fresh("Cap");
2231+ let elem_t = match t {
2232+ Nim::OpenArray(e) => (**e).clone(),
2233+ other => other.clone(),
2234+ };
2235+ self.line(&format!("let {}: seq[{}] = @{}", copy, elem_t.render(), ident(n)));
2236+ self.bind_alias(
2237+ n,
2238+ Alias::Value { code: copy, ty: Some(Nim::Seq(Box::new(elem_t))) },
2239+ );
2240+ }
2241+ self.line(&format!(
2242+ "result = proc (): Future[{}] {{.async, closure.}} =",
2243+ out.render()
2244+ ));
2245+ self.indent += 1;
2246+ let outer_r = self.ret.replace(out.clone());
2247+ let outer_t = self
2248+ .target
2249+ .replace(("result".to_string(), Some(out.clone())));
2250+ let before = self.out.len();
2251+ let tail = self.block_body_at(body, Some(&out))?;
2252+ self.emit_tail(tail);
2253+ if self.out.len() == before {
2254+ self.line("discard");
2255+ }
2256+ self.ret = outer_r;
2257+ self.target = outer_t;
2258+ self.indent -= 2;
2259+ self.fn_generics = outer_fg;
2260+ self.pop_scope();
2261+ self.blank();
2262+ return Ok(());
2263+ }
2189 if let Nim::Iter(elem) = &ret {2264 if let Nim::Iter(elem) = &ret {
2190 let elem = (**elem).clone();2265 let elem = (**elem).clone();
2191 // The *declared* parameter types, not the scope's, which record2266 // The *declared* parameter types, not the scope's, which record
@@ -3738,6 +3813,22 @@ impl Lowerer {
3738 .into()),3813 .into()),
3739 },3814 },
3740 Expr::Closure(c) => self.closure(c, expect),3815 Expr::Closure(c) => self.closure(c, expect),
3816+ // `.await` calls the thunk and waits. Calling it is what starts
3817+ // the body, which is how Rust's laziness is preserved.
3818+ Expr::Await(a) => {
3819+ let v = self.expr(&a.base)?;
3820+ let out = match &v.ty {
3821+ Some(Nim::Future(t)) => Some((**t).clone()),
3822+ _ => {
3823+ return Err(
3824+ "`.await` on a value that is not a future; its type \
3825+ must be known here"
3826+ .into(),
3827+ )
3828+ }
3829+ };
3830+ Ok(Val::new(format!("(await {}())", v.code), out))
3831+ }
3741 Expr::Try(t) => self.try_op(t),3832 Expr::Try(t) => self.try_op(t),
3742 Expr::Call(c) => self.call(c, expect),3833 Expr::Call(c) => self.call(c, expect),
3743 Expr::MethodCall(m) => self.method(m, expect),3834 Expr::MethodCall(m) => self.method(m, expect),
@@ -4625,6 +4716,16 @@ impl Lowerer {
4625 }4716 }
4626 }4717 }
4627 4718
4719+ // `block_on(f)` — whichever executor it came from, the operation is
4720+ // the same: run the future to completion and give back its output.
4721+ if name == "block_on" && codes.len() == 1 {
4722+ let out = match args[0].ty.clone() {
4723+ Some(Nim::Future(t)) => Some(*t),
4724+ _ => None,
4725+ };
4726+ return Ok(Val::new(format!("rsBlockOn({})", codes[0]), out));
4727+ }
4728+
4628 // `log::set_max_level` / `log::max_level`.4729 // `log::set_max_level` / `log::max_level`.
4629 if name == "set_max_level" && codes.len() == 1 {4730 if name == "set_max_level" && codes.len() == 1 {
4630 return Ok(Val::new(4731 return Ok(Val::new(
@@ -5978,7 +6079,6 @@ fn item_kind(i: &Item) -> &'static str {
5978 fn expr_kind(e: &Expr) -> &'static str {6079 fn expr_kind(e: &Expr) -> &'static str {
5979 match e {6080 match e {
5980 Expr::Async(_) => "`async` block",6081 Expr::Async(_) => "`async` block",
5981- Expr::Await(_) => "`.await`",
5982 Expr::Try(_) => "`?`",6082 Expr::Try(_) => "`?`",
5983 Expr::Range(_) => "range",6083 Expr::Range(_) => "range",
5984 Expr::Match(_) => "`match` (only statement position is implemented)",6084 Expr::Match(_) => "`match` (only statement position is implemented)",
added src/prelude_async.nim +26 -0
new file mode 100644
@@ -0,0 +1,26 @@
1+import std/asyncdispatch
2+
3+# ---------------------------------------------------------------------------
4+# Futures.
5+#
6+# A Rust future is inert: calling an `async fn` runs none of its body until
7+# something polls it. A Nim `{.async.}` proc runs eagerly, up to its first
8+# `await`, at the moment it is called. Probed side by side against rustc:
9+#
10+# Rust: before call / after call / body ran Nim: before call /
11+# body ran / after call
12+#
13+# Mapping `async fn` straight onto `{.async.}` would therefore reorder every
14+# side effect before the first await. So a future is a *thunk*: calling the
15+# function builds one and runs nothing, and `.await` calls it. That restores
16+# Rust's order exactly.
17+#
18+# What is not modelled: cancellation, `Pin`, `Send`/`Sync` bounds, and the
19+# choice of executor. Nim has one global dispatcher; Rust's runtime is chosen
20+# by the application.
21+# ---------------------------------------------------------------------------
22+
23+type RsFuture*[T] = proc (): Future[T] {.closure.}
24+
25+proc rsBlockOn*[T](f: RsFuture[T]): T = waitFor f()
26+
new file mode 100644
@@ -0,0 +1,26 @@
1+import std/asyncdispatch
2+
3+# ---------------------------------------------------------------------------
4+# Futures.
5+#
6+# A Rust future is inert: calling an `async fn` runs none of its body until
7+# something polls it. A Nim `{.async.}` proc runs eagerly, up to its first
8+# `await`, at the moment it is called. Probed side by side against rustc:
9+#
10+# Rust: before call / after call / body ran Nim: before call /
11+# body ran / after call
12+#
13+# Mapping `async fn` straight onto `{.async.}` would therefore reorder every
14+# side effect before the first await. So a future is a *thunk*: calling the
15+# function builds one and runs nothing, and `.await` calls it. That restores
16+# Rust's order exactly.
17+#
18+# What is not modelled: cancellation, `Pin`, `Send`/`Sync` bounds, and the
19+# choice of executor. Nim has one global dispatcher; Rust's runtime is chosen
20+# by the application.
21+# ---------------------------------------------------------------------------
22+
23+type RsFuture*[T] = proc (): Future[T] {.closure.}
24+
25+proc rsBlockOn*[T](f: RsFuture[T]): T = waitFor f()
26+
modified src/ty.rs +17 -0
@@ -21,6 +21,10 @@ pub enum Nim {
2121 /// `-> impl Iterator<Item = T>`. Nim's closure iterator is the same
2222 /// thing: lazy, first-class, and resumable.
2323 Iter(Box<Nim>),
24+ /// A Rust future: inert until awaited. Nim's async procs run eagerly, so
25+ /// this is a *thunk* returning a Nim `Future`, which restores the order in
26+ /// which Rust runs things. See `RsFuture` in the prelude.
27+ Future(Box<Nim>),
2428 /// `*const T`. The const matters at the C level even though it does not at
2529 /// the Rust one: Nim emits a real prototype where Rust emits none, and a
2630 /// `char *` declaration against C's `const char *` is a compile error.
@@ -54,6 +58,7 @@ impl Nim {
5458 inner => format!("ptr {}", inner.render()),
5559 },
5660 Nim::Iter(t) => format!("iterator (): {} {{.closure.}}", t.render()),
61+ Nim::Future(t) => format!("RsFuture[{}]", t.render()),
5762 Nim::ConstPtr(t) => match &**t {
5863 Nim::Prim(p) if p == "void" => "pointer".into(),
5964 inner => const_ptr_alias(inner),
@@ -380,6 +385,18 @@ pub fn map(t: &Type) -> Result<Nim, String> {
380385 for b in &i.bounds {
381386 if let TypeParamBound::Trait(tb) = b {
382387 if let Some(seg) = tb.path.segments.last() {
388+ // `impl Future<Output = T>`.
389+ if seg.ident == "Future" {
390+ if let PathArguments::AngleBracketed(a) = &seg.arguments {
391+ for g in &a.args {
392+ if let GenericArgument::AssocType(at) = g {
393+ if at.ident == "Output" {
394+ return Ok(Nim::Future(Box::new(map(&at.ty)?)));
395+ }
396+ }
397+ }
398+ }
399+ }
383400 // `impl Iterator<Item = T>`: Nim's closure iterator is
384401 // lazy and first-class as Rust's is, so this is a real
385402 // equivalent rather than a materialised `seq`.
@@ -21,6 +21,10 @@ pub enum Nim {
21 /// `-> impl Iterator<Item = T>`. Nim's closure iterator is the same21 /// `-> impl Iterator<Item = T>`. Nim's closure iterator is the same
22 /// thing: lazy, first-class, and resumable.22 /// thing: lazy, first-class, and resumable.
23 Iter(Box<Nim>),23 Iter(Box<Nim>),
24+ /// A Rust future: inert until awaited. Nim's async procs run eagerly, so
25+ /// this is a *thunk* returning a Nim `Future`, which restores the order in
26+ /// which Rust runs things. See `RsFuture` in the prelude.
27+ Future(Box<Nim>),
24 /// `*const T`. The const matters at the C level even though it does not at28 /// `*const T`. The const matters at the C level even though it does not at
25 /// the Rust one: Nim emits a real prototype where Rust emits none, and a29 /// the Rust one: Nim emits a real prototype where Rust emits none, and a
26 /// `char *` declaration against C's `const char *` is a compile error.30 /// `char *` declaration against C's `const char *` is a compile error.
@@ -54,6 +58,7 @@ impl Nim {
54 inner => format!("ptr {}", inner.render()),58 inner => format!("ptr {}", inner.render()),
55 },59 },
56 Nim::Iter(t) => format!("iterator (): {} {{.closure.}}", t.render()),60 Nim::Iter(t) => format!("iterator (): {} {{.closure.}}", t.render()),
61+ Nim::Future(t) => format!("RsFuture[{}]", t.render()),
57 Nim::ConstPtr(t) => match &**t {62 Nim::ConstPtr(t) => match &**t {
58 Nim::Prim(p) if p == "void" => "pointer".into(),63 Nim::Prim(p) if p == "void" => "pointer".into(),
59 inner => const_ptr_alias(inner),64 inner => const_ptr_alias(inner),
@@ -380,6 +385,18 @@ pub fn map(t: &Type) -> Result<Nim, String> {
380 for b in &i.bounds {385 for b in &i.bounds {
381 if let TypeParamBound::Trait(tb) = b {386 if let TypeParamBound::Trait(tb) = b {
382 if let Some(seg) = tb.path.segments.last() {387 if let Some(seg) = tb.path.segments.last() {
388+ // `impl Future<Output = T>`.
389+ if seg.ident == "Future" {
390+ if let PathArguments::AngleBracketed(a) = &seg.arguments {
391+ for g in &a.args {
392+ if let GenericArgument::AssocType(at) = g {
393+ if at.ident == "Output" {
394+ return Ok(Nim::Future(Box::new(map(&at.ty)?)));
395+ }
396+ }
397+ }
398+ }
399+ }
383 // `impl Iterator<Item = T>`: Nim's closure iterator is400 // `impl Iterator<Item = T>`: Nim's closure iterator is
384 // lazy and first-class as Rust's is, so this is a real401 // lazy and first-class as Rust's is, so this is a real
385 // equivalent rather than a materialised `seq`.402 // equivalent rather than a materialised `seq`.
added tests/cases/042-async.rs +44 -0
new file mode 100644
@@ -0,0 +1,44 @@
1+//@ extern: futures
2+// A Rust future is inert: calling an `async fn` runs none of its body until
3+// something polls it. A Nim `{.async.}` proc runs eagerly, up to its first
4+// `await`, at the moment it is called -- probed side by side, and the orders
5+// genuinely differ. So a future is lowered as a *thunk*: calling the function
6+// builds one and runs nothing, and `.await` calls it.
7+//
8+// This case exists to pin that ordering. If futures were mapped straight onto
9+// `{.async.}`, "body ran" would move before "after call" and every side
10+// effect ahead of the first await would be reordered.
11+
12+async fn work(n: i32) -> i32 {
13+ println!(" work({}) ran", n);
14+ n * 2
15+}
16+
17+async fn chained(n: i32) -> i32 {
18+ let a = work(n).await;
19+ let b = work(a).await;
20+ a + b
21+}
22+
23+async fn conditional(n: i32) -> i32 {
24+ if n > 10 {
25+ return n;
26+ }
27+ let v = work(n).await;
28+ v + 1
29+}
30+
31+fn main() {
32+ println!("before call");
33+ let f = work(21);
34+ println!("after call");
35+ println!("awaited: {}", futures::executor::block_on(f));
36+
37+ println!("chained: {}", futures::executor::block_on(chained(3)));
38+ println!("cond big: {}", futures::executor::block_on(conditional(50)));
39+ println!("cond small: {}", futures::executor::block_on(conditional(4)));
40+
41+ // Built but never awaited: the body must not run.
42+ let _unused = work(999);
43+ println!("built an unawaited future; nothing above this line said 999");
44+}
new file mode 100644
@@ -0,0 +1,44 @@
1+//@ extern: futures
2+// A Rust future is inert: calling an `async fn` runs none of its body until
3+// something polls it. A Nim `{.async.}` proc runs eagerly, up to its first
4+// `await`, at the moment it is called -- probed side by side, and the orders
5+// genuinely differ. So a future is lowered as a *thunk*: calling the function
6+// builds one and runs nothing, and `.await` calls it.
7+//
8+// This case exists to pin that ordering. If futures were mapped straight onto
9+// `{.async.}`, "body ran" would move before "after call" and every side
10+// effect ahead of the first await would be reordered.
11+
12+async fn work(n: i32) -> i32 {
13+ println!(" work({}) ran", n);
14+ n * 2
15+}
16+
17+async fn chained(n: i32) -> i32 {
18+ let a = work(n).await;
19+ let b = work(a).await;
20+ a + b
21+}
22+
23+async fn conditional(n: i32) -> i32 {
24+ if n > 10 {
25+ return n;
26+ }
27+ let v = work(n).await;
28+ v + 1
29+}
30+
31+fn main() {
32+ println!("before call");
33+ let f = work(21);
34+ println!("after call");
35+ println!("awaited: {}", futures::executor::block_on(f));
36+
37+ println!("chained: {}", futures::executor::block_on(chained(3)));
38+ println!("cond big: {}", futures::executor::block_on(conditional(50)));
39+ println!("cond small: {}", futures::executor::block_on(conditional(4)));
40+
41+ // Built but never awaited: the body must not run.
42+ let _unused = work(999);
43+ println!("built an unawaited future; nothing above this line said 999");
44+}