Lower `env::var_os`, `as_deref`, `unwrap_or_default`, and `OsString`
`anstyle-query` compiles -- the fifth of libcosmic's dependencies to go the
whole way.
`var_os` answers `None` when a variable is unset and `Some` otherwise,
including when it is set to the empty string. `getEnv` alone cannot tell those
apart, so the prelude asks `existsEnv` too.
`OsString` maps to a Nim string. On Unix it is bytes with no encoding
guarantee, which is exactly what a Nim string is. On Windows it is UTF-16 and
this would not hold -- but neither does the rest of the platform support here,
and the comment says so rather than leaving it to be discovered.
`as_deref` on an `Option` yields a reference to the payload, and a reference
is the value here. `unwrap_or_default` is the payload or its zero value, and
still refuses when rustnim has no default for the type rather than inventing
one.
Two bugs found by this crate:
* The module renamer descended into nested modules, so
`mod windows { mod windows_console { fn f } fn f }` had the inner `f`
renamed for the *outer* module first, leaving the inner pass nothing to
find. A nested module has its own scope and its own pass.
* `Some(7)` into an `Option<u32>` was lowering the literal before the
expected element type was known, making it an `i32`.
tests/cases/075.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>dc9c467 parent: 81195a6 modified
src/lower.rs +68 -0 | @@ -7797,6 +7797,16 @@ impl Lowerer { | ||
| 7797 | 7797 | // which names nothing at the call site. |
| 7798 | 7798 | let (inner, ty) = match expect { |
| 7799 | 7799 | Some(Nim::Named(n, a)) if n == "Option" && a.len() == 1 => { |
| 7800 | + // Re-lowered against the element type: `Some(7)` into | |
| 7801 | + // an `Option<u32>` is a `u32` seven, and the payload | |
| 7802 | + // was lowered before that was known. | |
| 7803 | + if let Some(arg) = c.args.first() { | |
| 7804 | + let v = self.expr_at(arg, Some(&a[0]))?; | |
| 7805 | + return Ok(Val::new( | |
| 7806 | + format!("rsSome[{}]({})", a[0].render(), v.code), | |
| 7807 | + expect.cloned(), | |
| 7808 | + )); | |
| 7809 | + } | |
| 7800 | 7810 | (a[0].render(), expect.cloned()) |
| 7801 | 7811 | } |
| 7802 | 7812 | _ => match args.first().and_then(|a| a.ty.clone()) { |
| @@ -8128,6 +8138,18 @@ impl Lowerer { | ||
| 8128 | 8138 | } |
| 8129 | 8139 | } |
| 8130 | 8140 | } |
| 8141 | + // `std::env::var_os`. `None` when unset, `Some` otherwise -- | |
| 8142 | + // including when set to the empty string, which is why the | |
| 8143 | + // prelude asks twice. | |
| 8144 | + "var_os" | "var" if codes.len() == 1 => { | |
| 8145 | + return Ok(Val::new( | |
| 8146 | + format!("rsEnvVar({})", codes[0]), | |
| 8147 | + Some(Nim::Named( | |
| 8148 | + "Option".into(), | |
| 8149 | + vec![Nim::Prim("string".into())], | |
| 8150 | + )), | |
| 8151 | + )); | |
| 8152 | + } | |
| 8131 | 8153 | // `cmp::max` / `cmp::min` are Nim's `max` / `min` on the same |
| 8132 | 8154 | // ordering. |
| 8133 | 8155 | "max" | "min" if codes.len() == 2 => { |
| @@ -8797,6 +8819,45 @@ impl Lowerer { | ||
| 8797 | 8819 | "borrow" | "borrow_mut" if m.args.is_empty() => { |
| 8798 | 8820 | (recv.code.clone(), rt.clone()) |
| 8799 | 8821 | } |
| 8822 | + // `Option<T>::as_deref` yields `Option<&T::Target>`, and a | |
| 8823 | + // reference is the value here -- as is `as_ref`/`as_mut` on an | |
| 8824 | + // `Option`, which are already identity above. | |
| 8825 | + "as_deref" | "as_deref_mut" | |
| 8826 | + if matches!(&rt, Some(Nim::Named(n, _)) if n == "Option") => | |
| 8827 | + { | |
| 8828 | + (recv.code.clone(), rt.clone()) | |
| 8829 | + } | |
| 8830 | + // `unwrap_or_default` is the payload, or its zero value. | |
| 8831 | + "unwrap_or_default" if m.args.is_empty() => { | |
| 8832 | + let inner = match &rt { | |
| 8833 | + Some(Nim::Named(n, a)) if (n == "Option" || n == "Result") && !a.is_empty() => { | |
| 8834 | + a[0].clone() | |
| 8835 | + } | |
| 8836 | + _ => { | |
| 8837 | + return Err( | |
| 8838 | + "`unwrap_or_default` needs a known `Option`/`Result` type here" | |
| 8839 | + .into(), | |
| 8840 | + ) | |
| 8841 | + } | |
| 8842 | + }; | |
| 8843 | + let d = default_of(&inner).ok_or_else(|| { | |
| 8844 | + format!( | |
| 8845 | + "`unwrap_or_default` needs a default for `{}`, and rustnim \ | |
| 8846 | + has none for it", | |
| 8847 | + inner.render() | |
| 8848 | + ) | |
| 8849 | + })?; | |
| 8850 | + let ok = match &rt { | |
| 8851 | + Some(Nim::Named(n, _)) if n == "Result" => "ok", | |
| 8852 | + _ => "has", | |
| 8853 | + }; | |
| 8854 | + let tmp = self.fresh("Def"); | |
| 8855 | + self.line(&format!("let {} = {}", tmp, recv.code)); | |
| 8856 | + ( | |
| 8857 | + format!("(if {0}.{1}: {0}.val else: {2})", tmp, ok, d), | |
| 8858 | + Some(inner), | |
| 8859 | + ) | |
| 8860 | + } | |
| 8800 | 8861 | "unwrap" | "expect" => { |
| 8801 | 8862 | // Expanded inline rather than called as a generic proc: when |
| 8802 | 8863 | // the payload is a view, Nim can only borrow from a path |
| @@ -10664,6 +10725,13 @@ fn namespace_modules(f: &mut syn::File) { | ||
| 10664 | 10725 | .collect(); |
| 10665 | 10726 | if !here.is_empty() { |
| 10666 | 10727 | for item in items.iter_mut() { |
| 10728 | + // Not into a nested module: it has its own scope and its own | |
| 10729 | + // pass below. `mod windows { mod windows_console { fn f } fn f }` | |
| 10730 | + // renamed the inner `f` twice otherwise -- once for the outer | |
| 10731 | + // module, which then left the inner pass nothing to find. | |
| 10732 | + if matches!(item, Item::Mod(_)) { | |
| 10733 | + continue; | |
| 10734 | + } | |
| 10667 | 10735 | syn::visit_mut::VisitMut::visit_item_mut(&mut Renamer { renames: &here }, item); |
| 10668 | 10736 | } |
| 10669 | 10737 | } |
| @@ -7797,6 +7797,16 @@ impl Lowerer { | |||
| 7797 | // which names nothing at the call site. | 7797 | // which names nothing at the call site. |
| 7798 | let (inner, ty) = match expect { | 7798 | let (inner, ty) = match expect { |
| 7799 | Some(Nim::Named(n, a)) if n == "Option" && a.len() == 1 => { | 7799 | Some(Nim::Named(n, a)) if n == "Option" && a.len() == 1 => { |
| 7800 | + // Re-lowered against the element type: `Some(7)` into | ||
| 7801 | + // an `Option<u32>` is a `u32` seven, and the payload | ||
| 7802 | + // was lowered before that was known. | ||
| 7803 | + if let Some(arg) = c.args.first() { | ||
| 7804 | + let v = self.expr_at(arg, Some(&a[0]))?; | ||
| 7805 | + return Ok(Val::new( | ||
| 7806 | + format!("rsSome[{}]({})", a[0].render(), v.code), | ||
| 7807 | + expect.cloned(), | ||
| 7808 | + )); | ||
| 7809 | + } | ||
| 7800 | (a[0].render(), expect.cloned()) | 7810 | (a[0].render(), expect.cloned()) |
| 7801 | } | 7811 | } |
| 7802 | _ => match args.first().and_then(|a| a.ty.clone()) { | 7812 | _ => match args.first().and_then(|a| a.ty.clone()) { |
| @@ -8128,6 +8138,18 @@ impl Lowerer { | |||
| 8128 | } | 8138 | } |
| 8129 | } | 8139 | } |
| 8130 | } | 8140 | } |
| 8141 | + // `std::env::var_os`. `None` when unset, `Some` otherwise -- | ||
| 8142 | + // including when set to the empty string, which is why the | ||
| 8143 | + // prelude asks twice. | ||
| 8144 | + "var_os" | "var" if codes.len() == 1 => { | ||
| 8145 | + return Ok(Val::new( | ||
| 8146 | + format!("rsEnvVar({})", codes[0]), | ||
| 8147 | + Some(Nim::Named( | ||
| 8148 | + "Option".into(), | ||
| 8149 | + vec![Nim::Prim("string".into())], | ||
| 8150 | + )), | ||
| 8151 | + )); | ||
| 8152 | + } | ||
| 8131 | // `cmp::max` / `cmp::min` are Nim's `max` / `min` on the same | 8153 | // `cmp::max` / `cmp::min` are Nim's `max` / `min` on the same |
| 8132 | // ordering. | 8154 | // ordering. |
| 8133 | "max" | "min" if codes.len() == 2 => { | 8155 | "max" | "min" if codes.len() == 2 => { |
| @@ -8797,6 +8819,45 @@ impl Lowerer { | |||
| 8797 | "borrow" | "borrow_mut" if m.args.is_empty() => { | 8819 | "borrow" | "borrow_mut" if m.args.is_empty() => { |
| 8798 | (recv.code.clone(), rt.clone()) | 8820 | (recv.code.clone(), rt.clone()) |
| 8799 | } | 8821 | } |
| 8822 | + // `Option<T>::as_deref` yields `Option<&T::Target>`, and a | ||
| 8823 | + // reference is the value here -- as is `as_ref`/`as_mut` on an | ||
| 8824 | + // `Option`, which are already identity above. | ||
| 8825 | + "as_deref" | "as_deref_mut" | ||
| 8826 | + if matches!(&rt, Some(Nim::Named(n, _)) if n == "Option") => | ||
| 8827 | + { | ||
| 8828 | + (recv.code.clone(), rt.clone()) | ||
| 8829 | + } | ||
| 8830 | + // `unwrap_or_default` is the payload, or its zero value. | ||
| 8831 | + "unwrap_or_default" if m.args.is_empty() => { | ||
| 8832 | + let inner = match &rt { | ||
| 8833 | + Some(Nim::Named(n, a)) if (n == "Option" || n == "Result") && !a.is_empty() => { | ||
| 8834 | + a[0].clone() | ||
| 8835 | + } | ||
| 8836 | + _ => { | ||
| 8837 | + return Err( | ||
| 8838 | + "`unwrap_or_default` needs a known `Option`/`Result` type here" | ||
| 8839 | + .into(), | ||
| 8840 | + ) | ||
| 8841 | + } | ||
| 8842 | + }; | ||
| 8843 | + let d = default_of(&inner).ok_or_else(|| { | ||
| 8844 | + format!( | ||
| 8845 | + "`unwrap_or_default` needs a default for `{}`, and rustnim \ | ||
| 8846 | + has none for it", | ||
| 8847 | + inner.render() | ||
| 8848 | + ) | ||
| 8849 | + })?; | ||
| 8850 | + let ok = match &rt { | ||
| 8851 | + Some(Nim::Named(n, _)) if n == "Result" => "ok", | ||
| 8852 | + _ => "has", | ||
| 8853 | + }; | ||
| 8854 | + let tmp = self.fresh("Def"); | ||
| 8855 | + self.line(&format!("let {} = {}", tmp, recv.code)); | ||
| 8856 | + ( | ||
| 8857 | + format!("(if {0}.{1}: {0}.val else: {2})", tmp, ok, d), | ||
| 8858 | + Some(inner), | ||
| 8859 | + ) | ||
| 8860 | + } | ||
| 8800 | "unwrap" | "expect" => { | 8861 | "unwrap" | "expect" => { |
| 8801 | // Expanded inline rather than called as a generic proc: when | 8862 | // Expanded inline rather than called as a generic proc: when |
| 8802 | // the payload is a view, Nim can only borrow from a path | 8863 | // the payload is a view, Nim can only borrow from a path |
| @@ -10664,6 +10725,13 @@ fn namespace_modules(f: &mut syn::File) { | |||
| 10664 | .collect(); | 10725 | .collect(); |
| 10665 | if !here.is_empty() { | 10726 | if !here.is_empty() { |
| 10666 | for item in items.iter_mut() { | 10727 | for item in items.iter_mut() { |
| 10728 | + // Not into a nested module: it has its own scope and its own | ||
| 10729 | + // pass below. `mod windows { mod windows_console { fn f } fn f }` | ||
| 10730 | + // renamed the inner `f` twice otherwise -- once for the outer | ||
| 10731 | + // module, which then left the inner pass nothing to find. | ||
| 10732 | + if matches!(item, Item::Mod(_)) { | ||
| 10733 | + continue; | ||
| 10734 | + } | ||
| 10667 | syn::visit_mut::VisitMut::visit_item_mut(&mut Renamer { renames: &here }, item); | 10735 | syn::visit_mut::VisitMut::visit_item_mut(&mut Renamer { renames: &here }, item); |
| 10668 | } | 10736 | } |
| 10669 | } | 10737 | } |
modified
src/prelude.nim +7 -1 | @@ -12,7 +12,7 @@ | ||
| 12 | 12 | # it, because copying instead would silently change aliasing. |
| 13 | 13 | {.experimental: "views".} |
| 14 | 14 | |
| 15 | -import std/[unicode, strutils, math] | |
| 15 | +import std/[unicode, strutils, math, os] | |
| 16 | 16 | |
| 17 | 17 | type |
| 18 | 18 | RustPanic* = object of CatchableError |
| @@ -358,3 +358,9 @@ proc rsFloatToInt*[T: SomeInteger](f: float64): T = | ||
| 358 | 358 | elif f <= float64(low(T)): low(T) |
| 359 | 359 | elif f >= float64(high(T)): high(T) |
| 360 | 360 | else: T(trunc(f)) |
| 361 | + | |
| 362 | +## `std::env::var_os`. Rust answers `None` when the variable is unset and | |
| 363 | +## `Some` otherwise, including when it is set to the empty string -- which | |
| 364 | +## `getEnv` alone cannot tell apart, so both are asked. | |
| 365 | +proc rsEnvVar*(k: string): Option[string] = | |
| 366 | + if existsEnv(k): rsSome[string](getEnv(k)) else: rsNone[string]() | |
| @@ -12,7 +12,7 @@ | |||
| 12 | # it, because copying instead would silently change aliasing. | 12 | # it, because copying instead would silently change aliasing. |
| 13 | {.experimental: "views".} | 13 | {.experimental: "views".} |
| 14 | 14 | ||
| 15 | -import std/[unicode, strutils, math] | 15 | +import std/[unicode, strutils, math, os] |
| 16 | 16 | ||
| 17 | type | 17 | type |
| 18 | RustPanic* = object of CatchableError | 18 | RustPanic* = object of CatchableError |
| @@ -358,3 +358,9 @@ proc rsFloatToInt*[T: SomeInteger](f: float64): T = | |||
| 358 | elif f <= float64(low(T)): low(T) | 358 | elif f <= float64(low(T)): low(T) |
| 359 | elif f >= float64(high(T)): high(T) | 359 | elif f >= float64(high(T)): high(T) |
| 360 | else: T(trunc(f)) | 360 | else: T(trunc(f)) |
| 361 | + | ||
| 362 | +## `std::env::var_os`. Rust answers `None` when the variable is unset and | ||
| 363 | +## `Some` otherwise, including when it is set to the empty string -- which | ||
| 364 | +## `getEnv` alone cannot tell apart, so both are asked. | ||
| 365 | +proc rsEnvVar*(k: string): Option[string] = | ||
| 366 | + if existsEnv(k): rsSome[string](getEnv(k)) else: rsNone[string]() | ||
modified
src/ty.rs +5 -0 | @@ -177,6 +177,11 @@ pub fn prim(name: &str) -> Option<Nim> { | ||
| 177 | 177 | "bool" => "bool", |
| 178 | 178 | "char" => "Rune", |
| 179 | 179 | "str" | "String" => "string", |
| 180 | + // On Unix an `OsString` is bytes with no encoding guarantee, which is | |
| 181 | + // exactly what a Nim `string` is. (On Windows it is UTF-16, and this | |
| 182 | + // mapping would not hold -- but neither does the rest of the platform | |
| 183 | + // support here.) | |
| 184 | + "OsStr" | "OsString" => "string", | |
| 180 | 185 | // The named spelling of `!`: an error type that cannot occur. |
| 181 | 186 | "Infallible" => "RsNever", |
| 182 | 187 | other => match c_type(other) { |
| @@ -177,6 +177,11 @@ pub fn prim(name: &str) -> Option<Nim> { | |||
| 177 | "bool" => "bool", | 177 | "bool" => "bool", |
| 178 | "char" => "Rune", | 178 | "char" => "Rune", |
| 179 | "str" | "String" => "string", | 179 | "str" | "String" => "string", |
| 180 | + // On Unix an `OsString` is bytes with no encoding guarantee, which is | ||
| 181 | + // exactly what a Nim `string` is. (On Windows it is UTF-16, and this | ||
| 182 | + // mapping would not hold -- but neither does the rest of the platform | ||
| 183 | + // support here.) | ||
| 184 | + "OsStr" | "OsString" => "string", | ||
| 180 | // The named spelling of `!`: an error type that cannot occur. | 185 | // The named spelling of `!`: an error type that cannot occur. |
| 181 | "Infallible" => "RsNever", | 186 | "Infallible" => "RsNever", |
| 182 | other => match c_type(other) { | 187 | other => match c_type(other) { |
added
tests/cases/075-env-and-option.rs +25 -0 | new file mode 100644 | ||
| @@ -0,0 +1,25 @@ | ||
| 1 | +// `var_os` answers `None` when a variable is unset and `Some` otherwise, | |
| 2 | +// including when it is set to the empty string — which `getEnv` alone cannot | |
| 3 | +// tell apart, so both are asked. `as_deref` yields a reference, which is the | |
| 4 | +// value here. | |
| 5 | +// | |
| 6 | +// Both binaries run in the same environment, so `PATH` is set for each and a | |
| 7 | +// name nothing sets is absent from each; the *values* are not printed, since | |
| 8 | +// they would be the environment's rather than the program's. | |
| 9 | +fn main() { | |
| 10 | + println!("{}", std::env::var_os("PATH").is_some()); | |
| 11 | + println!("{}", std::env::var_os("RUSTNIM_NO_SUCH_VARIABLE_42").is_some()); | |
| 12 | + println!("{}", std::env::var_os("PATH").as_deref().is_some()); | |
| 13 | + | |
| 14 | + match std::env::var_os("RUSTNIM_NO_SUCH_VARIABLE_42") { | |
| 15 | + Some(_) => println!("set"), | |
| 16 | + None => println!("unset"), | |
| 17 | + } | |
| 18 | + | |
| 19 | + let n: Option<u32> = None; | |
| 20 | + let s: Option<u32> = Some(7); | |
| 21 | + println!("{} {}", n.unwrap_or_default(), s.unwrap_or_default()); | |
| 22 | + | |
| 23 | + let e: Option<String> = None; | |
| 24 | + println!("[{}]", e.unwrap_or_default()); | |
| 25 | +} | |
| new file mode 100644 | |||
| @@ -0,0 +1,25 @@ | |||
| 1 | +// `var_os` answers `None` when a variable is unset and `Some` otherwise, | ||
| 2 | +// including when it is set to the empty string — which `getEnv` alone cannot | ||
| 3 | +// tell apart, so both are asked. `as_deref` yields a reference, which is the | ||
| 4 | +// value here. | ||
| 5 | +// | ||
| 6 | +// Both binaries run in the same environment, so `PATH` is set for each and a | ||
| 7 | +// name nothing sets is absent from each; the *values* are not printed, since | ||
| 8 | +// they would be the environment's rather than the program's. | ||
| 9 | +fn main() { | ||
| 10 | + println!("{}", std::env::var_os("PATH").is_some()); | ||
| 11 | + println!("{}", std::env::var_os("RUSTNIM_NO_SUCH_VARIABLE_42").is_some()); | ||
| 12 | + println!("{}", std::env::var_os("PATH").as_deref().is_some()); | ||
| 13 | + | ||
| 14 | + match std::env::var_os("RUSTNIM_NO_SUCH_VARIABLE_42") { | ||
| 15 | + Some(_) => println!("set"), | ||
| 16 | + None => println!("unset"), | ||
| 17 | + } | ||
| 18 | + | ||
| 19 | + let n: Option<u32> = None; | ||
| 20 | + let s: Option<u32> = Some(7); | ||
| 21 | + println!("{} {}", n.unwrap_or_default(), s.unwrap_or_default()); | ||
| 22 | + | ||
| 23 | + let e: Option<String> = None; | ||
| 24 | + println!("[{}]", e.unwrap_or_default()); | ||
| 25 | +} | ||