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>
7303426 parent: 8094353 modified
src/lower.rs +113 -1 | @@ -1334,6 +1334,19 @@ impl Lowerer { | ||
| 1334 | 1334 | self.note_const_generics(&im.generics)?; |
| 1335 | 1335 | self.note_bounds(&im.generics); |
| 1336 | 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 | 1350 | if let (Some((path, _)), Ok(t)) = (&im.trait_, self.map_ty(&im.self_ty)) { |
| 1338 | 1351 | if let Some(seg) = path.segments.last() { |
| 1339 | 1352 | let targs: Vec<Nim> = match &seg.arguments { |
| @@ -1684,6 +1697,44 @@ impl Lowerer { | ||
| 1684 | 1697 | } |
| 1685 | 1698 | |
| 1686 | 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 | 1738 | // `cmp::Ordering` and `atomic::Ordering` are different types that end |
| 1688 | 1739 | // in the same segment, so this one needs the qualifier. A crate's own |
| 1689 | 1740 | // `Ordering` wins over both. |
| @@ -7857,6 +7908,17 @@ impl Lowerer { | ||
| 7857 | 7908 | return Ok(Val::new(String::new(), Some(Nim::Unit))); |
| 7858 | 7909 | } |
| 7859 | 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 | 7922 | let (t, e) = match expect { |
| 7861 | 7923 | Some(Nim::Named(n, a)) if n == "Result" && a.len() == 2 => { |
| 7862 | 7924 | (a[0].render(), a[1].render()) |
| @@ -7869,7 +7931,21 @@ impl Lowerer { | ||
| 7869 | 7931 | } |
| 7870 | 7932 | }; |
| 7871 | 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 | 7949 | return Ok(Val::new( |
| 7874 | 7950 | format!("{}[{}, {}]({})", ctor, t, e, arg), |
| 7875 | 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 | 8251 | // `std::env::var_os`. `None` when unset, `Some` otherwise -- |
| 8166 | 8252 | // including when set to the empty string, which is why the |
| 8167 | 8253 | // prelude asks twice. |
| @@ -8665,6 +8751,32 @@ impl Lowerer { | ||
| 8665 | 8751 | |
| 8666 | 8752 | // `x.wrapping_add(1)` and `x.min(3)` take an argument of the receiver's |
| 8667 | 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 | 8780 | let arg_want = match (name.as_str(), &recv.ty) { |
| 8669 | 8781 | ("push", Some(Nim::Seq(t))) | ("push", Some(Nim::OpenArray(t))) => Some((**t).clone()), |
| 8670 | 8782 | (_, 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 end | 1738 | // `cmp::Ordering` and `atomic::Ordering` are different types that end |
| 1688 | // in the same segment, so this one needs the qualifier. A crate's own | 1739 | // 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 the | 8252 | // 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's | 8752 | // `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 = | ||
| 370 | 370 | ## `getEnv` alone cannot tell apart, so both are asked. |
| 371 | 371 | proc rsEnvVar*(k: string): Option[string] = |
| 372 | 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)) | |
| @@ -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> { | ||
| 182 | 182 | // mapping would not hold -- but neither does the rest of the platform |
| 183 | 183 | // support here.) |
| 184 | 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 | 188 | // The named spelling of `!`: an error type that cannot occur. |
| 186 | 189 | "Infallible" => "RsNever", |
| 187 | 190 | other => match c_type(other) { |
| @@ -390,6 +393,11 @@ pub fn map(t: &Type) -> Result<Nim, String> { | ||
| 390 | 393 | if let TypeParamBound::Trait(tb) = b { |
| 391 | 394 | if let Some(seg) = tb.path.segments.last() { |
| 392 | 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 | 401 | // `impl Iterator<Item = T>` in return position: Nim's |
| 394 | 402 | // closure iterator is lazy and first-class, so this is |
| 395 | 403 | // 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 platform | 182 | // 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's | 401 | // `impl Iterator<Item = T>` in return position: Nim's |
| 394 | // closure iterator is lazy and first-class, so this is | 402 | // 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 | +} | ||