nandi/rustnimpublic Fork
⑂ dc9c467
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 `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>
nandithebull committed 2026-09-20T13:39:37-07:00 Browse files
dc9c467 parent: 81195a6
modified src/lower.rs +68 -0
@@ -7797,6 +7797,16 @@ impl Lowerer {
77977797 // which names nothing at the call site.
77987798 let (inner, ty) = match expect {
77997799 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+ }
78007810 (a[0].render(), expect.cloned())
78017811 }
78027812 _ => match args.first().and_then(|a| a.ty.clone()) {
@@ -8128,6 +8138,18 @@ impl Lowerer {
81288138 }
81298139 }
81308140 }
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+ }
81318153 // `cmp::max` / `cmp::min` are Nim's `max` / `min` on the same
81328154 // ordering.
81338155 "max" | "min" if codes.len() == 2 => {
@@ -8797,6 +8819,45 @@ impl Lowerer {
87978819 "borrow" | "borrow_mut" if m.args.is_empty() => {
87988820 (recv.code.clone(), rt.clone())
87998821 }
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+ }
88008861 "unwrap" | "expect" => {
88018862 // Expanded inline rather than called as a generic proc: when
88028863 // the payload is a view, Nim can only borrow from a path
@@ -10664,6 +10725,13 @@ fn namespace_modules(f: &mut syn::File) {
1066410725 .collect();
1066510726 if !here.is_empty() {
1066610727 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+ }
1066710735 syn::visit_mut::VisitMut::visit_item_mut(&mut Renamer { renames: &here }, item);
1066810736 }
1066910737 }
@@ -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 same8153 // `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: when8862 // Expanded inline rather than called as a generic proc: when
8802 // the payload is a view, Nim can only borrow from a path8863 // 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 @@
1212 # it, because copying instead would silently change aliasing.
1313 {.experimental: "views".}
1414
15-import std/[unicode, strutils, math]
15+import std/[unicode, strutils, math, os]
1616
1717 type
1818 RustPanic* = object of CatchableError
@@ -358,3 +358,9 @@ proc rsFloatToInt*[T: SomeInteger](f: float64): T =
358358 elif f <= float64(low(T)): low(T)
359359 elif f >= float64(high(T)): high(T)
360360 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 type17 type
18 RustPanic* = object of CatchableError18 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> {
177177 "bool" => "bool",
178178 "char" => "Rune",
179179 "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",
180185 // The named spelling of `!`: an error type that cannot occur.
181186 "Infallible" => "RsNever",
182187 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+}