nandi/rustnimpublic Fork
⑂ 7303426
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 `io::Write` to a Nim `Stream`, and `Result<(), E>` at all

A `dyn io::Write` is a sink with dynamic dispatch: the callee writes bytes and
does not know where they go. Nim's `Stream` is the same thing -- `FileStream`
for stdout or a file, `StringStream` for a buffer -- so a `&mut dyn Write`
parameter is a `Stream` and the caller decides. `write_all`, `write`,
`write_str` and `flush` go through the prelude, which turns the exception a
`Stream` raises back into the `Err` Rust returns.

A user type implementing `Write` is refused, and says why: Nim's `Stream` is a
record of closures rather than an interface to implement, so accepting it
would give a sink whose writes went nowhere.

`Result<(), E>` did not work at all before this. It became `Result[void, E]`,
and Nim cannot have a `void` field -- so every fallible operation with no
payload, which is most of them, produced output that would not compile.
`RsUnit` is the empty object that `()` is.

And `Err(1)` in a `Result<_, u32>` was taking the literal as `i32`. That is
the fourth site of the same bug -- a payload lowered before the type it is
about to be asked for -- after `vec![0; 256]`, `Some(7)` and
`Shape::Circle(3)`. Each has been fixed where it stood; if there is a fifth I
will thread the expectation through rather than keep finding them.

tests/cases/079.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
nandithebull committed 2026-09-20T14:21:31-07:00 Browse files
7303426 parent: 8094353
modified src/lower.rs +113 -1
@@ -1334,6 +1334,19 @@ impl Lowerer {
13341334 self.note_const_generics(&im.generics)?;
13351335 self.note_bounds(&im.generics);
13361336 self.note_fn_blanket(im);
1337+ if let Some((path, _)) = &im.trait_ {
1338+ if path.segments.last().is_some_and(|s| s.ident == "Write")
1339+ && path.segments.iter().any(|s| s.ident == "io")
1340+ {
1341+ return Err(
1342+ "`impl io::Write` for a type of your own is not implemented: a \
1343+ `dyn Write` is lowered to a Nim `Stream`, which is a record of \
1344+ closures rather than an interface to implement, so this would \
1345+ produce a sink whose writes went nowhere"
1346+ .into(),
1347+ );
1348+ }
1349+ }
13371350 if let (Some((path, _)), Ok(t)) = (&im.trait_, self.map_ty(&im.self_ty)) {
13381351 if let Some(seg) = path.segments.last() {
13391352 let targs: Vec<Nim> = match &seg.arguments {
@@ -1684,6 +1697,44 @@ impl Lowerer {
16841697 }
16851698
16861699 fn map_ty_inner(&self, t: &syn::Type) -> Result<Nim, String> {
1700+ let n = self.map_ty_raw(t)?;
1701+ // `Result<(), E>` carries nothing on success, and Nim's `void` cannot
1702+ // be a field. `RsUnit` is the empty object that `()` is.
1703+ // `io::Result<T>` is `Result<T, io::Error>`; the alias hides the error
1704+ // half and Nim needs both.
1705+ if let syn::Type::Path(p) = t {
1706+ let segs: Vec<String> =
1707+ p.path.segments.iter().map(|x| x.ident.to_string()).collect();
1708+ if segs.last().is_some_and(|x| x == "Result")
1709+ && segs.iter().any(|x| x == "io")
1710+ {
1711+ if let Nim::Named(_, a) = &n {
1712+ if a.len() == 1 {
1713+ let ok = if a[0] == Nim::Unit {
1714+ Nim::Prim("RsUnit".into())
1715+ } else {
1716+ a[0].clone()
1717+ };
1718+ return Ok(Nim::Named(
1719+ "Result".into(),
1720+ vec![ok, Nim::Prim("RsIoError".into())],
1721+ ));
1722+ }
1723+ }
1724+ }
1725+ }
1726+ Ok(match n {
1727+ Nim::Named(name, args) if name == "Result" || name == "Option" => Nim::Named(
1728+ name,
1729+ args.into_iter()
1730+ .map(|a| if a == Nim::Unit { Nim::Prim("RsUnit".into()) } else { a })
1731+ .collect(),
1732+ ),
1733+ other => other,
1734+ })
1735+ }
1736+
1737+ fn map_ty_raw(&self, t: &syn::Type) -> Result<Nim, String> {
16871738 // `cmp::Ordering` and `atomic::Ordering` are different types that end
16881739 // in the same segment, so this one needs the qualifier. A crate's own
16891740 // `Ordering` wins over both.
@@ -7857,6 +7908,17 @@ impl Lowerer {
78577908 return Ok(Val::new(String::new(), Some(Nim::Unit)));
78587909 }
78597910 "Ok" | "Err" => {
7911+ // `Ok(())` carries the empty object, not nothing at all.
7912+ if name == "Ok" && codes.len() == 1 && codes[0].is_empty() {
7913+ if let Some(Nim::Named(n, a)) = expect {
7914+ if n == "Result" && a.len() == 2 {
7915+ return Ok(Val::new(
7916+ format!("rsOk[{}, {}]({}())", a[0].render(), a[1].render(), a[0].render()),
7917+ expect.cloned(),
7918+ ));
7919+ }
7920+ }
7921+ }
78607922 let (t, e) = match expect {
78617923 Some(Nim::Named(n, a)) if n == "Result" && a.len() == 2 => {
78627924 (a[0].render(), a[1].render())
@@ -7869,7 +7931,21 @@ impl Lowerer {
78697931 }
78707932 };
78717933 let ctor = if name == "Ok" { "rsOk" } else { "rsErr" };
7872- let arg = if codes.is_empty() { String::new() } else { codes.join(", ") };
7934+ // Re-lowered against the half being constructed. A payload
7935+ // lowered before its type is known takes Rust's default for an
7936+ // unconstrained literal, which is how `Err(1)` became an
7937+ // `i32` in a `Result<_, u32>`.
7938+ let want = match expect {
7939+ Some(Nim::Named(n, a)) if n == "Result" && a.len() == 2 => {
7940+ Some(if name == "Ok" { a[0].clone() } else { a[1].clone() })
7941+ }
7942+ _ => None,
7943+ };
7944+ let arg = match (c.args.first(), &want) {
7945+ (Some(a), Some(w)) => self.expr_at(a, Some(w))?.code,
7946+ _ if codes.is_empty() => String::new(),
7947+ _ => codes.join(", "),
7948+ };
78737949 return Ok(Val::new(
78747950 format!("{}[{}, {}]({})", ctor, t, e, arg),
78757951 expect.cloned(),
@@ -8162,6 +8238,16 @@ impl Lowerer {
81628238 }
81638239 }
81648240 }
8241+ // `io::stdout()` / `stderr()`: the process's own sinks, as
8242+ // streams. `newFileStream` wraps the same file descriptor Rust
8243+ // writes to.
8244+ "stdout" | "stderr" if codes.is_empty() => {
8245+ let f = if name == "stdout" { "stdout" } else { "stderr" };
8246+ return Ok(Val::new(
8247+ format!("newFileStream({})", f),
8248+ Some(Nim::Prim("Stream".into())),
8249+ ));
8250+ }
81658251 // `std::env::var_os`. `None` when unset, `Some` otherwise --
81668252 // including when set to the empty string, which is why the
81678253 // prelude asks twice.
@@ -8665,6 +8751,32 @@ impl Lowerer {
86658751
86668752 // `x.wrapping_add(1)` and `x.min(3)` take an argument of the receiver's
86678753 // own type; `v.push(e)` takes the element type.
8754+ // `io::Write` on a real sink. A `Stream` raises where Rust returns an
8755+ // `Err`, so the prelude turns the one back into the other.
8756+ if matches!(&recv.ty, Some(Nim::Prim(p)) if p == "Stream") {
8757+ let nm = m.method.to_string();
8758+ if matches!(nm.as_str(), "write_all" | "write" | "write_str" | "flush")
8759+ && m.args.len() <= 1
8760+ {
8761+ let res = Nim::Named(
8762+ "Result".into(),
8763+ vec![Nim::Prim("RsUnit".into()), Nim::Prim("RsIoError".into())],
8764+ );
8765+ if nm == "flush" {
8766+ return Ok(Val::new(format!("rsFlush({})", recv.code), Some(res)));
8767+ }
8768+ let a = self.expr(&m.args[0])?;
8769+ let (f, code) = match &a.ty {
8770+ Some(Nim::Prim(p)) if p == "string" => ("rsWriteStr", a.code.clone()),
8771+ Some(Nim::OpenArray(e)) if matches!(&**e, Nim::Prim(p) if p == "char") => {
8772+ ("rsWriteStr", format!("rsStringOf(rsBytes({}))", a.code))
8773+ }
8774+ _ => ("rsWriteAll", a.code.clone()),
8775+ };
8776+ return Ok(Val::new(format!("{}({}, {})", f, recv.code, code), Some(res)));
8777+ }
8778+ }
8779+
86688780 let arg_want = match (name.as_str(), &recv.ty) {
86698781 ("push", Some(Nim::Seq(t))) | ("push", Some(Nim::OpenArray(t))) => Some((**t).clone()),
86708782 (_, t) => t.clone(),
@@ -1334,6 +1334,19 @@ impl Lowerer {
1334 self.note_const_generics(&im.generics)?;1334 self.note_const_generics(&im.generics)?;
1335 self.note_bounds(&im.generics);1335 self.note_bounds(&im.generics);
1336 self.note_fn_blanket(im);1336 self.note_fn_blanket(im);
1337+ if let Some((path, _)) = &im.trait_ {
1338+ if path.segments.last().is_some_and(|s| s.ident == "Write")
1339+ && path.segments.iter().any(|s| s.ident == "io")
1340+ {
1341+ return Err(
1342+ "`impl io::Write` for a type of your own is not implemented: a \
1343+ `dyn Write` is lowered to a Nim `Stream`, which is a record of \
1344+ closures rather than an interface to implement, so this would \
1345+ produce a sink whose writes went nowhere"
1346+ .into(),
1347+ );
1348+ }
1349+ }
1337 if let (Some((path, _)), Ok(t)) = (&im.trait_, self.map_ty(&im.self_ty)) {1350 if let (Some((path, _)), Ok(t)) = (&im.trait_, self.map_ty(&im.self_ty)) {
1338 if let Some(seg) = path.segments.last() {1351 if let Some(seg) = path.segments.last() {
1339 let targs: Vec<Nim> = match &seg.arguments {1352 let targs: Vec<Nim> = match &seg.arguments {
@@ -1684,6 +1697,44 @@ impl Lowerer {
1684 }1697 }
1685 1698
1686 fn map_ty_inner(&self, t: &syn::Type) -> Result<Nim, String> {1699 fn map_ty_inner(&self, t: &syn::Type) -> Result<Nim, String> {
1700+ let n = self.map_ty_raw(t)?;
1701+ // `Result<(), E>` carries nothing on success, and Nim's `void` cannot
1702+ // be a field. `RsUnit` is the empty object that `()` is.
1703+ // `io::Result<T>` is `Result<T, io::Error>`; the alias hides the error
1704+ // half and Nim needs both.
1705+ if let syn::Type::Path(p) = t {
1706+ let segs: Vec<String> =
1707+ p.path.segments.iter().map(|x| x.ident.to_string()).collect();
1708+ if segs.last().is_some_and(|x| x == "Result")
1709+ && segs.iter().any(|x| x == "io")
1710+ {
1711+ if let Nim::Named(_, a) = &n {
1712+ if a.len() == 1 {
1713+ let ok = if a[0] == Nim::Unit {
1714+ Nim::Prim("RsUnit".into())
1715+ } else {
1716+ a[0].clone()
1717+ };
1718+ return Ok(Nim::Named(
1719+ "Result".into(),
1720+ vec![ok, Nim::Prim("RsIoError".into())],
1721+ ));
1722+ }
1723+ }
1724+ }
1725+ }
1726+ Ok(match n {
1727+ Nim::Named(name, args) if name == "Result" || name == "Option" => Nim::Named(
1728+ name,
1729+ args.into_iter()
1730+ .map(|a| if a == Nim::Unit { Nim::Prim("RsUnit".into()) } else { a })
1731+ .collect(),
1732+ ),
1733+ other => other,
1734+ })
1735+ }
1736+
1737+ fn map_ty_raw(&self, t: &syn::Type) -> Result<Nim, String> {
1687 // `cmp::Ordering` and `atomic::Ordering` are different types that end1738 // `cmp::Ordering` and `atomic::Ordering` are different types that end
1688 // in the same segment, so this one needs the qualifier. A crate's own1739 // in the same segment, so this one needs the qualifier. A crate's own
1689 // `Ordering` wins over both.1740 // `Ordering` wins over both.
@@ -7857,6 +7908,17 @@ impl Lowerer {
7857 return Ok(Val::new(String::new(), Some(Nim::Unit)));7908 return Ok(Val::new(String::new(), Some(Nim::Unit)));
7858 }7909 }
7859 "Ok" | "Err" => {7910 "Ok" | "Err" => {
7911+ // `Ok(())` carries the empty object, not nothing at all.
7912+ if name == "Ok" && codes.len() == 1 && codes[0].is_empty() {
7913+ if let Some(Nim::Named(n, a)) = expect {
7914+ if n == "Result" && a.len() == 2 {
7915+ return Ok(Val::new(
7916+ format!("rsOk[{}, {}]({}())", a[0].render(), a[1].render(), a[0].render()),
7917+ expect.cloned(),
7918+ ));
7919+ }
7920+ }
7921+ }
7860 let (t, e) = match expect {7922 let (t, e) = match expect {
7861 Some(Nim::Named(n, a)) if n == "Result" && a.len() == 2 => {7923 Some(Nim::Named(n, a)) if n == "Result" && a.len() == 2 => {
7862 (a[0].render(), a[1].render())7924 (a[0].render(), a[1].render())
@@ -7869,7 +7931,21 @@ impl Lowerer {
7869 }7931 }
7870 };7932 };
7871 let ctor = if name == "Ok" { "rsOk" } else { "rsErr" };7933 let ctor = if name == "Ok" { "rsOk" } else { "rsErr" };
7872- let arg = if codes.is_empty() { String::new() } else { codes.join(", ") };7934+ // Re-lowered against the half being constructed. A payload
7935+ // lowered before its type is known takes Rust's default for an
7936+ // unconstrained literal, which is how `Err(1)` became an
7937+ // `i32` in a `Result<_, u32>`.
7938+ let want = match expect {
7939+ Some(Nim::Named(n, a)) if n == "Result" && a.len() == 2 => {
7940+ Some(if name == "Ok" { a[0].clone() } else { a[1].clone() })
7941+ }
7942+ _ => None,
7943+ };
7944+ let arg = match (c.args.first(), &want) {
7945+ (Some(a), Some(w)) => self.expr_at(a, Some(w))?.code,
7946+ _ if codes.is_empty() => String::new(),
7947+ _ => codes.join(", "),
7948+ };
7873 return Ok(Val::new(7949 return Ok(Val::new(
7874 format!("{}[{}, {}]({})", ctor, t, e, arg),7950 format!("{}[{}, {}]({})", ctor, t, e, arg),
7875 expect.cloned(),7951 expect.cloned(),
@@ -8162,6 +8238,16 @@ impl Lowerer {
8162 }8238 }
8163 }8239 }
8164 }8240 }
8241+ // `io::stdout()` / `stderr()`: the process's own sinks, as
8242+ // streams. `newFileStream` wraps the same file descriptor Rust
8243+ // writes to.
8244+ "stdout" | "stderr" if codes.is_empty() => {
8245+ let f = if name == "stdout" { "stdout" } else { "stderr" };
8246+ return Ok(Val::new(
8247+ format!("newFileStream({})", f),
8248+ Some(Nim::Prim("Stream".into())),
8249+ ));
8250+ }
8165 // `std::env::var_os`. `None` when unset, `Some` otherwise --8251 // `std::env::var_os`. `None` when unset, `Some` otherwise --
8166 // including when set to the empty string, which is why the8252 // including when set to the empty string, which is why the
8167 // prelude asks twice.8253 // prelude asks twice.
@@ -8665,6 +8751,32 @@ impl Lowerer {
8665 8751
8666 // `x.wrapping_add(1)` and `x.min(3)` take an argument of the receiver's8752 // `x.wrapping_add(1)` and `x.min(3)` take an argument of the receiver's
8667 // own type; `v.push(e)` takes the element type.8753 // own type; `v.push(e)` takes the element type.
8754+ // `io::Write` on a real sink. A `Stream` raises where Rust returns an
8755+ // `Err`, so the prelude turns the one back into the other.
8756+ if matches!(&recv.ty, Some(Nim::Prim(p)) if p == "Stream") {
8757+ let nm = m.method.to_string();
8758+ if matches!(nm.as_str(), "write_all" | "write" | "write_str" | "flush")
8759+ && m.args.len() <= 1
8760+ {
8761+ let res = Nim::Named(
8762+ "Result".into(),
8763+ vec![Nim::Prim("RsUnit".into()), Nim::Prim("RsIoError".into())],
8764+ );
8765+ if nm == "flush" {
8766+ return Ok(Val::new(format!("rsFlush({})", recv.code), Some(res)));
8767+ }
8768+ let a = self.expr(&m.args[0])?;
8769+ let (f, code) = match &a.ty {
8770+ Some(Nim::Prim(p)) if p == "string" => ("rsWriteStr", a.code.clone()),
8771+ Some(Nim::OpenArray(e)) if matches!(&**e, Nim::Prim(p) if p == "char") => {
8772+ ("rsWriteStr", format!("rsStringOf(rsBytes({}))", a.code))
8773+ }
8774+ _ => ("rsWriteAll", a.code.clone()),
8775+ };
8776+ return Ok(Val::new(format!("{}({}, {})", f, recv.code, code), Some(res)));
8777+ }
8778+ }
8779+
8668 let arg_want = match (name.as_str(), &recv.ty) {8780 let arg_want = match (name.as_str(), &recv.ty) {
8669 ("push", Some(Nim::Seq(t))) | ("push", Some(Nim::OpenArray(t))) => Some((**t).clone()),8781 ("push", Some(Nim::Seq(t))) | ("push", Some(Nim::OpenArray(t))) => Some((**t).clone()),
8670 (_, t) => t.clone(),8782 (_, t) => t.clone(),
modified src/prelude.nim +49 -0
@@ -370,3 +370,52 @@ proc rsFloatToInt*[T: SomeInteger](f: float64): T =
370370 ## `getEnv` alone cannot tell apart, so both are asked.
371371 proc rsEnvVar*(k: string): Option[string] =
372372 if existsEnv(k): rsSome[string](getEnv(k)) else: rsNone[string]()
373+
374+## Rust's `()` where a type argument is wanted: `Result<(), E>` is an ordinary
375+## `Result` whose success carries nothing. Nim's `void` cannot be a field, so
376+## the nothing is an empty object -- which is what `()` is.
377+type RsUnit* = object
378+
379+# ---------------------------------------------------------------------------
380+# `std::io::Write`.
381+#
382+# A Rust `dyn io::Write` is a sink with dynamic dispatch: the callee writes
383+# bytes and does not know where they go. Nim's `Stream` is the same thing --
384+# `FileStream` for a file or stdout, `StringStream` for an in-memory buffer --
385+# so a `&mut dyn Write` parameter is a `Stream`, and the caller decides.
386+#
387+# What this does *not* cover is a user type implementing `Write`. Nim's
388+# `Stream` is a closure record rather than an interface to inherit, and
389+# pretending otherwise would give a sink whose writes went nowhere. Such an
390+# impl is refused where it appears.
391+import std/streams
392+
393+type RsIoError* = object
394+ msg*: string
395+
396+proc rsDisplay*(e: RsIoError): string = e.msg
397+proc rsDebug*(e: RsIoError): string = "Error { msg: \"" & e.msg & "\" }"
398+
399+proc rsWriteAll*(w: Stream, b: openArray[uint8]): Result[RsUnit, RsIoError] =
400+ ## `write_all` either writes everything or fails; a `Stream` raises on
401+ ## failure, so the error is turned back into the `Result` Rust returns.
402+ try:
403+ if b.len > 0:
404+ w.writeData(unsafeAddr b[0], b.len)
405+ rsOk[RsUnit, RsIoError](RsUnit())
406+ except CatchableError as e:
407+ rsErr[RsUnit, RsIoError](RsIoError(msg: e.msg))
408+
409+proc rsWriteStr*(w: Stream, s: string): Result[RsUnit, RsIoError] =
410+ try:
411+ w.write(s)
412+ rsOk[RsUnit, RsIoError](RsUnit())
413+ except CatchableError as e:
414+ rsErr[RsUnit, RsIoError](RsIoError(msg: e.msg))
415+
416+proc rsFlush*(w: Stream): Result[RsUnit, RsIoError] =
417+ try:
418+ w.flush()
419+ rsOk[RsUnit, RsIoError](RsUnit())
420+ except CatchableError as e:
421+ rsErr[RsUnit, RsIoError](RsIoError(msg: e.msg))
@@ -370,3 +370,52 @@ proc rsFloatToInt*[T: SomeInteger](f: float64): T =
370 ## `getEnv` alone cannot tell apart, so both are asked.370 ## `getEnv` alone cannot tell apart, so both are asked.
371 proc rsEnvVar*(k: string): Option[string] =371 proc rsEnvVar*(k: string): Option[string] =
372 if existsEnv(k): rsSome[string](getEnv(k)) else: rsNone[string]()372 if existsEnv(k): rsSome[string](getEnv(k)) else: rsNone[string]()
373+
374+## Rust's `()` where a type argument is wanted: `Result<(), E>` is an ordinary
375+## `Result` whose success carries nothing. Nim's `void` cannot be a field, so
376+## the nothing is an empty object -- which is what `()` is.
377+type RsUnit* = object
378+
379+# ---------------------------------------------------------------------------
380+# `std::io::Write`.
381+#
382+# A Rust `dyn io::Write` is a sink with dynamic dispatch: the callee writes
383+# bytes and does not know where they go. Nim's `Stream` is the same thing --
384+# `FileStream` for a file or stdout, `StringStream` for an in-memory buffer --
385+# so a `&mut dyn Write` parameter is a `Stream`, and the caller decides.
386+#
387+# What this does *not* cover is a user type implementing `Write`. Nim's
388+# `Stream` is a closure record rather than an interface to inherit, and
389+# pretending otherwise would give a sink whose writes went nowhere. Such an
390+# impl is refused where it appears.
391+import std/streams
392+
393+type RsIoError* = object
394+ msg*: string
395+
396+proc rsDisplay*(e: RsIoError): string = e.msg
397+proc rsDebug*(e: RsIoError): string = "Error { msg: \"" & e.msg & "\" }"
398+
399+proc rsWriteAll*(w: Stream, b: openArray[uint8]): Result[RsUnit, RsIoError] =
400+ ## `write_all` either writes everything or fails; a `Stream` raises on
401+ ## failure, so the error is turned back into the `Result` Rust returns.
402+ try:
403+ if b.len > 0:
404+ w.writeData(unsafeAddr b[0], b.len)
405+ rsOk[RsUnit, RsIoError](RsUnit())
406+ except CatchableError as e:
407+ rsErr[RsUnit, RsIoError](RsIoError(msg: e.msg))
408+
409+proc rsWriteStr*(w: Stream, s: string): Result[RsUnit, RsIoError] =
410+ try:
411+ w.write(s)
412+ rsOk[RsUnit, RsIoError](RsUnit())
413+ except CatchableError as e:
414+ rsErr[RsUnit, RsIoError](RsIoError(msg: e.msg))
415+
416+proc rsFlush*(w: Stream): Result[RsUnit, RsIoError] =
417+ try:
418+ w.flush()
419+ rsOk[RsUnit, RsIoError](RsUnit())
420+ except CatchableError as e:
421+ rsErr[RsUnit, RsIoError](RsIoError(msg: e.msg))
modified src/ty.rs +8 -0
@@ -182,6 +182,9 @@ pub fn prim(name: &str) -> Option<Nim> {
182182 // mapping would not hold -- but neither does the rest of the platform
183183 // support here.)
184184 "OsStr" | "OsString" => "string",
185+ // A `dyn io::Write` is a sink with dynamic dispatch, which is what a
186+ // Nim `Stream` is. See `src/prelude.nim`.
187+ "Write" => "Stream",
185188 // The named spelling of `!`: an error type that cannot occur.
186189 "Infallible" => "RsNever",
187190 other => match c_type(other) {
@@ -390,6 +393,11 @@ pub fn map(t: &Type) -> Result<Nim, String> {
390393 if let TypeParamBound::Trait(tb) = b {
391394 if let Some(seg) = tb.path.segments.last() {
392395 let n = seg.ident.to_string();
396+ // `dyn io::Write`: a sink with dynamic dispatch, which
397+ // is what a Nim `Stream` is. See `src/prelude.nim`.
398+ if n == "Write" {
399+ return Ok(Nim::Prim("Stream".into()));
400+ }
393401 // `impl Iterator<Item = T>` in return position: Nim's
394402 // closure iterator is lazy and first-class, so this is
395403 // a real equivalent rather than a materialised `seq`.
@@ -182,6 +182,9 @@ pub fn prim(name: &str) -> Option<Nim> {
182 // mapping would not hold -- but neither does the rest of the platform182 // mapping would not hold -- but neither does the rest of the platform
183 // support here.)183 // support here.)
184 "OsStr" | "OsString" => "string",184 "OsStr" | "OsString" => "string",
185+ // A `dyn io::Write` is a sink with dynamic dispatch, which is what a
186+ // Nim `Stream` is. See `src/prelude.nim`.
187+ "Write" => "Stream",
185 // The named spelling of `!`: an error type that cannot occur.188 // The named spelling of `!`: an error type that cannot occur.
186 "Infallible" => "RsNever",189 "Infallible" => "RsNever",
187 other => match c_type(other) {190 other => match c_type(other) {
@@ -390,6 +393,11 @@ pub fn map(t: &Type) -> Result<Nim, String> {
390 if let TypeParamBound::Trait(tb) = b {393 if let TypeParamBound::Trait(tb) = b {
391 if let Some(seg) = tb.path.segments.last() {394 if let Some(seg) = tb.path.segments.last() {
392 let n = seg.ident.to_string();395 let n = seg.ident.to_string();
396+ // `dyn io::Write`: a sink with dynamic dispatch, which
397+ // is what a Nim `Stream` is. See `src/prelude.nim`.
398+ if n == "Write" {
399+ return Ok(Nim::Prim("Stream".into()));
400+ }
393 // `impl Iterator<Item = T>` in return position: Nim's401 // `impl Iterator<Item = T>` in return position: Nim's
394 // closure iterator is lazy and first-class, so this is402 // closure iterator is lazy and first-class, so this is
395 // a real equivalent rather than a materialised `seq`.403 // a real equivalent rather than a materialised `seq`.
added tests/cases/079-io-write.rs +33 -0
new file mode 100644
@@ -0,0 +1,33 @@
1+// A `dyn io::Write` is a sink with dynamic dispatch: the callee writes bytes
2+// and does not know where they go. Nim's `Stream` is the same thing, so the
3+// caller decides — here, stdout, which is what makes the output comparable.
4+use std::io::Write;
5+
6+const NAMES: [&str; 3] = ["bold", "dim", "italic"];
7+
8+fn render(w: &mut dyn Write, bits: u8) -> std::io::Result<()> {
9+ w.write_all(b"[")?;
10+ let mut first = true;
11+ for i in 0..3 {
12+ if bits & (1 << i) != 0 {
13+ if !first {
14+ w.write_all(b"|")?;
15+ }
16+ w.write_all(NAMES[i].as_bytes())?;
17+ first = false;
18+ }
19+ }
20+ w.write_all(b"]")?;
21+ w.flush()?;
22+ Ok(())
23+}
24+
25+fn main() {
26+ let mut out = std::io::stdout();
27+ render(&mut out, 0).unwrap();
28+ println!();
29+ render(&mut out, 5).unwrap();
30+ println!();
31+ render(&mut out, 7).unwrap();
32+ println!();
33+}
new file mode 100644
@@ -0,0 +1,33 @@
1+// A `dyn io::Write` is a sink with dynamic dispatch: the callee writes bytes
2+// and does not know where they go. Nim's `Stream` is the same thing, so the
3+// caller decides — here, stdout, which is what makes the output comparable.
4+use std::io::Write;
5+
6+const NAMES: [&str; 3] = ["bold", "dim", "italic"];
7+
8+fn render(w: &mut dyn Write, bits: u8) -> std::io::Result<()> {
9+ w.write_all(b"[")?;
10+ let mut first = true;
11+ for i in 0..3 {
12+ if bits & (1 << i) != 0 {
13+ if !first {
14+ w.write_all(b"|")?;
15+ }
16+ w.write_all(NAMES[i].as_bytes())?;
17+ first = false;
18+ }
19+ }
20+ w.write_all(b"]")?;
21+ w.flush()?;
22+ Ok(())
23+}
24+
25+fn main() {
26+ let mut out = std::io::stdout();
27+ render(&mut out, 0).unwrap();
28+ println!();
29+ render(&mut out, 5).unwrap();
30+ println!();
31+ render(&mut out, 7).unwrap();
32+ println!();
33+}