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>911457b parent: fe27b4b modified
Cargo.lock +107 -0 | @@ -8,12 +8,112 @@ version = "2.13.2" | ||
| 8 | 8 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 9 | 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 | 99 | [[package]] |
| 12 | 100 | name = "log" |
| 13 | 101 | version = "0.4.34" |
| 14 | 102 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 15 | 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 | 117 | [[package]] |
| 18 | 118 | name = "proc-macro2" |
| 19 | 119 | version = "1.0.107" |
| @@ -37,11 +137,18 @@ name = "rustnim" | ||
| 37 | 137 | version = "0.1.0" |
| 38 | 138 | dependencies = [ |
| 39 | 139 | "bitflags", |
| 140 | + "futures", | |
| 40 | 141 | "log", |
| 41 | 142 | "proc-macro2", |
| 42 | 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 | 152 | [[package]] |
| 46 | 153 | name = "syn" |
| 47 | 154 | 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"] } | ||
| 13 | 13 | [dev-dependencies] |
| 14 | 14 | bitflags = "2" |
| 15 | 15 | 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: | ||
| 777 | 777 | fresh name and each parameter aliases it, since two `let x` would collide in |
| 778 | 778 | one Nim scope. |
| 779 | 779 | |
| 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. | |
| 782 | 812 | |
| 783 | 813 | ### Trait methods are named by trait, not by type |
| 784 | 814 | |
| @@ -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 in | 777 | 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`, and | 780 | +### `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 type | 813 | ### 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 | ||
| 128 | 128 | "collect into a sequence". `.map` and `.filter` work in that form, where they |
| 129 | 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 | 139 | ## Does it generalise? |
| 132 | 140 | |
| 133 | 141 | `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 they | 128 | "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 { | ||
| 277 | 277 | fmt_param: Option<String>, |
| 278 | 278 | /// `type X<T> = ...`, expanded before any type is mapped. |
| 279 | 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 | 283 | /// Module names supplied as separate input files. A `mod x;` naming one |
| 281 | 284 | /// of these is satisfied by that file having been passed in. |
| 282 | 285 | pub modules: Vec<String>, |
| @@ -336,6 +339,7 @@ impl Lowerer { | ||
| 336 | 339 | vec_expect: None, |
| 337 | 340 | forwards: Vec::new(), |
| 338 | 341 | aliases: HashMap::new(), |
| 342 | + uses_async: false, | |
| 339 | 343 | modules: Vec::new(), |
| 340 | 344 | emitted: 0, |
| 341 | 345 | features: Vec::new(), |
| @@ -419,6 +423,13 @@ impl Lowerer { | ||
| 419 | 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 | 433 | // Pass 2: type definitions, which every signature may mention. |
| 423 | 434 | for (m, f) in files { |
| 424 | 435 | self.cur_mod = m.clone(); |
| @@ -554,6 +565,9 @@ impl Lowerer { | ||
| 554 | 565 | match item { |
| 555 | 566 | Item::Fn(f) => { |
| 556 | 567 | let (params, ret) = self.signature(&f.sig)?; |
| 568 | + if matches!(ret, Nim::Future(_)) { | |
| 569 | + self.uses_async = true; | |
| 570 | + } | |
| 557 | 571 | let mut gen_names = Self::generics_of(&f.sig.generics); |
| 558 | 572 | gen_names.extend(self.impl_trait_params(&f.sig)); |
| 559 | 573 | let name = f.sig.ident.to_string(); |
| @@ -767,6 +781,9 @@ impl Lowerer { | ||
| 767 | 781 | }; |
| 768 | 782 | let mname = m.sig.ident.to_string(); |
| 769 | 783 | let (mut params, ret) = self.signature(&m.sig)?; |
| 784 | + if matches!(ret, Nim::Future(_)) { | |
| 785 | + self.uses_async = true; | |
| 786 | + } | |
| 770 | 787 | let mut gen_names = self.impl_generics.clone(); |
| 771 | 788 | gen_names.extend(Self::generics_of(&m.sig.generics)); |
| 772 | 789 | let recv = if takes_self(&m.sig) { |
| @@ -786,6 +803,9 @@ impl Lowerer { | ||
| 786 | 803 | for it in &im.items { |
| 787 | 804 | if let syn::ImplItem::Fn(m) = it { |
| 788 | 805 | let (mut params, ret) = self.signature(&m.sig)?; |
| 806 | + if matches!(ret, Nim::Future(_)) { | |
| 807 | + self.uses_async = true; | |
| 808 | + } | |
| 789 | 809 | let mut gen_names = self.impl_generics.clone(); |
| 790 | 810 | gen_names.extend(Self::generics_of(&m.sig.generics)); |
| 791 | 811 | if takes_self(&m.sig) { |
| @@ -1251,9 +1271,6 @@ impl Lowerer { | ||
| 1251 | 1271 | fn signature(&self, sig: &syn::Signature) -> Result<(Vec<Nim>, Nim), String> { |
| 1252 | 1272 | // `unsafe fn` marks a contract for callers; it does not change what |
| 1253 | 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 | 1274 | // Lifetime parameters carry no runtime meaning and Nim is GC'd, so |
| 1258 | 1275 | // they disappear. Type parameters become Nim generic parameters. |
| 1259 | 1276 | // Const parameters have no Nim equivalent and are still rejected. |
| @@ -1291,6 +1308,13 @@ impl Lowerer { | ||
| 1291 | 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 | 1318 | Ok((params, ret)) |
| 1295 | 1319 | } |
| 1296 | 1320 | |
| @@ -2186,6 +2210,57 @@ impl Lowerer { | ||
| 2186 | 2210 | // `-> impl Iterator<Item = T>` becomes a Nim closure iterator, which |
| 2187 | 2211 | // is lazy and first-class as Rust's is. The body's trailing expression |
| 2188 | 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 | 2264 | if let Nim::Iter(elem) = &ret { |
| 2190 | 2265 | let elem = (**elem).clone(); |
| 2191 | 2266 | // The *declared* parameter types, not the scope's, which record |
| @@ -3738,6 +3813,22 @@ impl Lowerer { | ||
| 3738 | 3813 | .into()), |
| 3739 | 3814 | }, |
| 3740 | 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 | 3832 | Expr::Try(t) => self.try_op(t), |
| 3742 | 3833 | Expr::Call(c) => self.call(c, expect), |
| 3743 | 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 | 4729 | // `log::set_max_level` / `log::max_level`. |
| 4629 | 4730 | if name == "set_max_level" && codes.len() == 1 { |
| 4630 | 4731 | return Ok(Val::new( |
| @@ -5978,7 +6079,6 @@ fn item_kind(i: &Item) -> &'static str { | ||
| 5978 | 6079 | fn expr_kind(e: &Expr) -> &'static str { |
| 5979 | 6080 | match e { |
| 5980 | 6081 | Expr::Async(_) => "`async` block", |
| 5981 | - Expr::Await(_) => "`.await`", | |
| 5982 | 6082 | Expr::Try(_) => "`?`", |
| 5983 | 6083 | Expr::Range(_) => "range", |
| 5984 | 6084 | 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 one | 283 | /// 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 what | 1272 | // `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, so | 1274 | // 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, which | 2210 | // `-> impl Iterator<Item = T>` becomes a Nim closure iterator, which |
| 2187 | // is lazy and first-class as Rust's is. The body's trailing expression | 2211 | // 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 record | 2266 | // 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 { | ||
| 21 | 21 | /// `-> impl Iterator<Item = T>`. Nim's closure iterator is the same |
| 22 | 22 | /// thing: lazy, first-class, and resumable. |
| 23 | 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 | 28 | /// `*const T`. The const matters at the C level even though it does not at |
| 25 | 29 | /// the Rust one: Nim emits a real prototype where Rust emits none, and a |
| 26 | 30 | /// `char *` declaration against C's `const char *` is a compile error. |
| @@ -54,6 +58,7 @@ impl Nim { | ||
| 54 | 58 | inner => format!("ptr {}", inner.render()), |
| 55 | 59 | }, |
| 56 | 60 | Nim::Iter(t) => format!("iterator (): {} {{.closure.}}", t.render()), |
| 61 | + Nim::Future(t) => format!("RsFuture[{}]", t.render()), | |
| 57 | 62 | Nim::ConstPtr(t) => match &**t { |
| 58 | 63 | Nim::Prim(p) if p == "void" => "pointer".into(), |
| 59 | 64 | inner => const_ptr_alias(inner), |
| @@ -380,6 +385,18 @@ pub fn map(t: &Type) -> Result<Nim, String> { | ||
| 380 | 385 | for b in &i.bounds { |
| 381 | 386 | if let TypeParamBound::Trait(tb) = b { |
| 382 | 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 | 400 | // `impl Iterator<Item = T>`: Nim's closure iterator is |
| 384 | 401 | // lazy and first-class as Rust's is, so this is a real |
| 385 | 402 | // equivalent rather than a materialised `seq`. |
| @@ -21,6 +21,10 @@ pub enum Nim { | |||
| 21 | /// `-> impl Iterator<Item = T>`. Nim's closure iterator is the same | 21 | /// `-> 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 at | 28 | /// `*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 a | 29 | /// 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 is | 400 | // `impl Iterator<Item = T>`: Nim's closure iterator is |
| 384 | // lazy and first-class as Rust's is, so this is a real | 401 | // 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 | +} | ||