nandi/rustnimpublic Fork
⑂ edb7777
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 std constructors; "unknown function" was mostly not dependencies

Twice I described this category as "calls into dependencies" without opening
it, and twice that was wrong. The most common name by far was `new`, and the
calls were Vec::new(), String::new(), Vec::with_capacity() and
size_of::<T>() -- std's own surface, each with an exact Nim counterpart.

Those are lowered now, together with mem::swap and Default for primitives.
One detail is easy to get wrong and is pinned by tests/cases/043:
with_capacity reserves without adding elements, which is newSeqOfCap.
newSeq would create that many zeroed elements and len() would not match --
right shape, wrong answer.

That took the category from 62 to 54. What remains divides cleanly into three
groups, and DESIGN.md records them because the distinction matters more than
the count: std concurrency (Arc, Mutex, AtomicUsize, Pin), dependency types
(Aes256Gcm::new, Bytes::new), and platform calls (env::var_os, getauxval).

The concurrency group is not a mapping table. Arc is shared ownership, which
Nim's GC'd ref gives, but Arc is atomically refcounted where a plain ref is
not. Mutex needs the guard's Drop to release at scope exit. AtomicUsize needs
memory orderings and would be indistinguishable from a plain variable in a
program with no threads -- which cannot be verified here, since std::thread
is not modelled at all. Each is a design question about semantics rather than
a syntactic gap, so none is mapped casually.

Accepted crates unchanged at 18 of 400, Nim-compilable at 12.

49 differential cases, all green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
nandithebull committed 2026-09-19T06:35:05-07:00 Browse files
edb7777 parent: 911457b
modified DESIGN.md +35 -0
@@ -826,6 +826,41 @@ resolve — `overloaded 'rsDefault_default' leads to ambiguous calls`. Those
826826 keep the type in the name. The `bitflags!` shim's operators were renamed to
827827 match, since they are trait methods with a receiver.
828828
829+## What "call to unknown function" was hiding
830+
831+Twice I characterised this category as "calls into dependencies" without
832+opening it, and twice that was wrong. Opening it: the most common name by far
833+was `new` — and the calls were `Vec::new()`, `String::new()`,
834+`Vec::with_capacity()`, `size_of::<T>()`. Not dependencies at all, but
835+**`std`'s own surface**, each with an exact Nim counterpart.
836+
837+Those are now lowered, with one detail that is easy to get wrong:
838+`with_capacity` reserves without adding elements, which is `newSeqOfCap`.
839+`newSeq` would create that many zeroed elements and `len()` would not match.
840+`tests/cases/043` checks exactly that.
841+
842+That took the category from 62 to 54, and what is left divides cleanly:
843+
844+| | | |
845+|---|---|---|
846+| **std concurrency** | `Arc`, `Mutex`, `AtomicUsize`, `AtomicBool`, `Pin` | semantically loaded |
847+| **dependency types** | `Aes256Gcm::new`, `GHash::new`, `Bytes::new` | the closure problem |
848+| **platform** | `env::var_os`, `getauxval` | `std::env`, `libc` |
849+
850+The concurrency group is the interesting one, and it is *not* a mapping table.
851+`Arc<T>` is shared ownership, which Nim's GC'd `ref` gives — but `Arc` is
852+atomically refcounted where a plain Nim `ref` is not. `Mutex<T>` needs the
853+guard's `Drop` to release the lock at scope exit. `AtomicUsize` needs memory
854+orderings, and would be indistinguishable from a plain variable in a program
855+with no threads — which we cannot verify, since `std::thread` is not modelled
856+at all. Each of those is a design question about semantics, not a syntactic
857+gap, and none should be mapped casually.
858+
859+So the honest statement about this category is neither "bounded and cheap" nor
860+"unbounded tail": part of it was cheap std surface and is done, and the rest
861+is a small number of semantically heavy primitives plus the dependency
862+closure.
863+
829864 ## Proof of byte-identity for `base16ct`
830865
831866 [`PROOF.md`](PROOF.md) sets out what is actually established: exhaustive
@@ -826,6 +826,41 @@ resolve — `overloaded 'rsDefault_default' leads to ambiguous calls`. Those
826 keep the type in the name. The `bitflags!` shim's operators were renamed to826 keep the type in the name. The `bitflags!` shim's operators were renamed to
827 match, since they are trait methods with a receiver.827 match, since they are trait methods with a receiver.
828 828
829+## What "call to unknown function" was hiding
830+
831+Twice I characterised this category as "calls into dependencies" without
832+opening it, and twice that was wrong. Opening it: the most common name by far
833+was `new` — and the calls were `Vec::new()`, `String::new()`,
834+`Vec::with_capacity()`, `size_of::<T>()`. Not dependencies at all, but
835+**`std`'s own surface**, each with an exact Nim counterpart.
836+
837+Those are now lowered, with one detail that is easy to get wrong:
838+`with_capacity` reserves without adding elements, which is `newSeqOfCap`.
839+`newSeq` would create that many zeroed elements and `len()` would not match.
840+`tests/cases/043` checks exactly that.
841+
842+That took the category from 62 to 54, and what is left divides cleanly:
843+
844+| | | |
845+|---|---|---|
846+| **std concurrency** | `Arc`, `Mutex`, `AtomicUsize`, `AtomicBool`, `Pin` | semantically loaded |
847+| **dependency types** | `Aes256Gcm::new`, `GHash::new`, `Bytes::new` | the closure problem |
848+| **platform** | `env::var_os`, `getauxval` | `std::env`, `libc` |
849+
850+The concurrency group is the interesting one, and it is *not* a mapping table.
851+`Arc<T>` is shared ownership, which Nim's GC'd `ref` gives — but `Arc` is
852+atomically refcounted where a plain Nim `ref` is not. `Mutex<T>` needs the
853+guard's `Drop` to release the lock at scope exit. `AtomicUsize` needs memory
854+orderings, and would be indistinguishable from a plain variable in a program
855+with no threads — which we cannot verify, since `std::thread` is not modelled
856+at all. Each of those is a design question about semantics, not a syntactic
857+gap, and none should be mapped casually.
858+
859+So the honest statement about this category is neither "bounded and cheap" nor
860+"unbounded tail": part of it was cheap std surface and is done, and the rest
861+is a small number of semantically heavy primitives plus the dependency
862+closure.
863+
829 ## Proof of byte-identity for `base16ct`864 ## Proof of byte-identity for `base16ct`
830 865
831 [`PROOF.md`](PROOF.md) sets out what is actually established: exhaustive866 [`PROOF.md`](PROOF.md) sets out what is actually established: exhaustive
modified src/lower.rs +103 -0
@@ -4716,6 +4716,97 @@ impl Lowerer {
47164716 }
47174717 }
47184718
4719+ // Standard-library constructors. These are not calls into a
4720+ // dependency -- they are `std`'s own surface, and each has an exact
4721+ // Nim counterpart rather than an approximate one.
4722+ let qual = p.path.segments.iter().rev().nth(1).map(|s| s.ident.to_string());
4723+ match (qual.as_deref(), name.as_str(), codes.len()) {
4724+ (Some("Vec"), "new", 0) | (Some("Vec"), "with_capacity", 1) => {
4725+ let elem = match expect {
4726+ Some(Nim::Seq(e)) | Some(Nim::OpenArray(e)) => (**e).clone(),
4727+ _ => {
4728+ return Err(
4729+ "`Vec::new()` needs a known element type; annotate the \
4730+ binding"
4731+ .into(),
4732+ )
4733+ }
4734+ };
4735+ let t = Nim::Seq(Box::new(elem.clone()));
4736+ return Ok(Val::new(
4737+ if codes.is_empty() {
4738+ format!("newSeq[{}]()", elem.render())
4739+ } else {
4740+ // `with_capacity` reserves without adding elements,
4741+ // which is what `newSeqOfCap` does; `newSeq` would
4742+ // create that many zeroed ones.
4743+ format!("newSeqOfCap[{}](int({}))", elem.render(), codes[0])
4744+ },
4745+ Some(t),
4746+ ));
4747+ }
4748+ (Some("String"), "new", 0) => {
4749+ return Ok(Val::new("\"\"", Some(Nim::Prim("string".into()))))
4750+ }
4751+ (Some("String"), "with_capacity", 1) => {
4752+ return Ok(Val::new(
4753+ format!("newStringOfCap(int({}))", codes[0]),
4754+ Some(Nim::Prim("string".into())),
4755+ ))
4756+ }
4757+ (_, "size_of" | "size_of_val", _) => {
4758+ // `size_of::<T>()` -- the width is the platform's, and both
4759+ // languages ask the same C compiler for it.
4760+ let t = p
4761+ .path
4762+ .segments
4763+ .last()
4764+ .and_then(|seg| match &seg.arguments {
4765+ syn::PathArguments::AngleBracketed(a) => a.args.iter().find_map(|g| {
4766+ match g {
4767+ GenericArgument::Type(t) => Some(t.clone()),
4768+ _ => None,
4769+ }
4770+ }),
4771+ _ => None,
4772+ });
4773+ let inner = match (t, args.first()) {
4774+ (Some(t), _) => self.map_ty(&t)?.render(),
4775+ (None, Some(a)) => a
4776+ .ty
4777+ .clone()
4778+ .ok_or("`size_of_val` needs a known argument type")?
4779+ .render(),
4780+ _ => return Err("`size_of` needs a type argument".into()),
4781+ };
4782+ return Ok(Val::new(
4783+ format!("uint(sizeof({}))", inner),
4784+ Some(Nim::Prim("uint".into())),
4785+ ));
4786+ }
4787+ (Some("mem"), "swap", 2) => {
4788+ return Ok(Val::new(
4789+ format!("swap({}, {})", codes[0], codes[1]),
4790+ Some(Nim::Unit),
4791+ ))
4792+ }
4793+ _ => {}
4794+ }
4795+
4796+ // `T::default()` for a primitive: Rust's `Default` for these is the
4797+ // zero value, which is also Nim's.
4798+ if name == "default" && codes.is_empty() {
4799+ let t = qual
4800+ .as_deref()
4801+ .and_then(ty::prim)
4802+ .or_else(|| expect.cloned());
4803+ if let Some(t) = t {
4804+ if let Some(code) = default_of(&t) {
4805+ return Ok(Val::new(code, Some(t)));
4806+ }
4807+ }
4808+ }
4809+
47194810 // `block_on(f)` — whichever executor it came from, the operation is
47204811 // the same: run the future to completion and give back its output.
47214812 if name == "block_on" && codes.len() == 1 {
@@ -5782,6 +5873,18 @@ fn module_of(prefix: &[String]) -> String {
57825873 }
57835874 }
57845875
5876+/// Rust's `Default` for a primitive is its zero value, which is Nim's too.
5877+fn default_of(t: &Nim) -> Option<String> {
5878+ Some(match t {
5879+ Nim::Prim(p) if p == "string" => "\"\"".into(),
5880+ Nim::Prim(p) if p == "bool" => "false".into(),
5881+ Nim::Prim(p) if p.starts_with("float") => format!("{}(0.0)", p),
5882+ Nim::Prim(_) if t.is_integer() => format!("{}(0)", t.render()),
5883+ Nim::Seq(e) => format!("newSeq[{}]()", e.render()),
5884+ _ => return None,
5885+ })
5886+}
5887+
57855888 /// The first type argument of an `Option[T]` / `Result[T, E]`.
57865889 fn elem_arg(t: &Nim) -> Nim {
57875890 match t {
@@ -4716,6 +4716,97 @@ impl Lowerer {
4716 }4716 }
4717 }4717 }
4718 4718
4719+ // Standard-library constructors. These are not calls into a
4720+ // dependency -- they are `std`'s own surface, and each has an exact
4721+ // Nim counterpart rather than an approximate one.
4722+ let qual = p.path.segments.iter().rev().nth(1).map(|s| s.ident.to_string());
4723+ match (qual.as_deref(), name.as_str(), codes.len()) {
4724+ (Some("Vec"), "new", 0) | (Some("Vec"), "with_capacity", 1) => {
4725+ let elem = match expect {
4726+ Some(Nim::Seq(e)) | Some(Nim::OpenArray(e)) => (**e).clone(),
4727+ _ => {
4728+ return Err(
4729+ "`Vec::new()` needs a known element type; annotate the \
4730+ binding"
4731+ .into(),
4732+ )
4733+ }
4734+ };
4735+ let t = Nim::Seq(Box::new(elem.clone()));
4736+ return Ok(Val::new(
4737+ if codes.is_empty() {
4738+ format!("newSeq[{}]()", elem.render())
4739+ } else {
4740+ // `with_capacity` reserves without adding elements,
4741+ // which is what `newSeqOfCap` does; `newSeq` would
4742+ // create that many zeroed ones.
4743+ format!("newSeqOfCap[{}](int({}))", elem.render(), codes[0])
4744+ },
4745+ Some(t),
4746+ ));
4747+ }
4748+ (Some("String"), "new", 0) => {
4749+ return Ok(Val::new("\"\"", Some(Nim::Prim("string".into()))))
4750+ }
4751+ (Some("String"), "with_capacity", 1) => {
4752+ return Ok(Val::new(
4753+ format!("newStringOfCap(int({}))", codes[0]),
4754+ Some(Nim::Prim("string".into())),
4755+ ))
4756+ }
4757+ (_, "size_of" | "size_of_val", _) => {
4758+ // `size_of::<T>()` -- the width is the platform's, and both
4759+ // languages ask the same C compiler for it.
4760+ let t = p
4761+ .path
4762+ .segments
4763+ .last()
4764+ .and_then(|seg| match &seg.arguments {
4765+ syn::PathArguments::AngleBracketed(a) => a.args.iter().find_map(|g| {
4766+ match g {
4767+ GenericArgument::Type(t) => Some(t.clone()),
4768+ _ => None,
4769+ }
4770+ }),
4771+ _ => None,
4772+ });
4773+ let inner = match (t, args.first()) {
4774+ (Some(t), _) => self.map_ty(&t)?.render(),
4775+ (None, Some(a)) => a
4776+ .ty
4777+ .clone()
4778+ .ok_or("`size_of_val` needs a known argument type")?
4779+ .render(),
4780+ _ => return Err("`size_of` needs a type argument".into()),
4781+ };
4782+ return Ok(Val::new(
4783+ format!("uint(sizeof({}))", inner),
4784+ Some(Nim::Prim("uint".into())),
4785+ ));
4786+ }
4787+ (Some("mem"), "swap", 2) => {
4788+ return Ok(Val::new(
4789+ format!("swap({}, {})", codes[0], codes[1]),
4790+ Some(Nim::Unit),
4791+ ))
4792+ }
4793+ _ => {}
4794+ }
4795+
4796+ // `T::default()` for a primitive: Rust's `Default` for these is the
4797+ // zero value, which is also Nim's.
4798+ if name == "default" && codes.is_empty() {
4799+ let t = qual
4800+ .as_deref()
4801+ .and_then(ty::prim)
4802+ .or_else(|| expect.cloned());
4803+ if let Some(t) = t {
4804+ if let Some(code) = default_of(&t) {
4805+ return Ok(Val::new(code, Some(t)));
4806+ }
4807+ }
4808+ }
4809+
4719 // `block_on(f)` — whichever executor it came from, the operation is4810 // `block_on(f)` — whichever executor it came from, the operation is
4720 // the same: run the future to completion and give back its output.4811 // the same: run the future to completion and give back its output.
4721 if name == "block_on" && codes.len() == 1 {4812 if name == "block_on" && codes.len() == 1 {
@@ -5782,6 +5873,18 @@ fn module_of(prefix: &[String]) -> String {
5782 }5873 }
5783 }5874 }
5784 5875
5876+/// Rust's `Default` for a primitive is its zero value, which is Nim's too.
5877+fn default_of(t: &Nim) -> Option<String> {
5878+ Some(match t {
5879+ Nim::Prim(p) if p == "string" => "\"\"".into(),
5880+ Nim::Prim(p) if p == "bool" => "false".into(),
5881+ Nim::Prim(p) if p.starts_with("float") => format!("{}(0.0)", p),
5882+ Nim::Prim(_) if t.is_integer() => format!("{}(0)", t.render()),
5883+ Nim::Seq(e) => format!("newSeq[{}]()", e.render()),
5884+ _ => return None,
5885+ })
5886+}
5887+
5785 /// The first type argument of an `Option[T]` / `Result[T, E]`.5888 /// The first type argument of an `Option[T]` / `Result[T, E]`.
5786 fn elem_arg(t: &Nim) -> Nim {5889 fn elem_arg(t: &Nim) -> Nim {
5787 match t {5890 match t {
added tests/cases/043-std-constructors.rs +35 -0
new file mode 100644
@@ -0,0 +1,35 @@
1+// These are `std`'s own surface rather than calls into a dependency, and each
2+// has an exact Nim counterpart. `with_capacity` reserves without adding
3+// elements, which is `newSeqOfCap`; `newSeq` would create that many zeroed
4+// ones and `len()` would not match.
5+
6+use core::mem::size_of;
7+
8+fn main() {
9+ let mut v: Vec<i32> = Vec::new();
10+ println!("{} {}", v.len(), v.is_empty());
11+ v.push(3);
12+ v.push(4);
13+ println!("{:?} {}", v, v.len());
14+
15+ let w: Vec<u8> = Vec::with_capacity(16);
16+ println!("{} {}", w.len(), w.is_empty());
17+
18+ let s = String::new();
19+ println!("[{}] {}", s, s.len());
20+ let t = String::with_capacity(32);
21+ println!("[{}] {}", t, t.len());
22+
23+ println!("{} {} {} {}", size_of::<u8>(), size_of::<u32>(), size_of::<i64>(), size_of::<f64>());
24+
25+ let a: i32 = i32::default();
26+ let b: bool = bool::default();
27+ let c: f64 = f64::default();
28+ let d: String = String::default();
29+ println!("{} {} {} [{}]", a, b, c, d);
30+
31+ let mut x: i32 = 1;
32+ let mut y: i32 = 2;
33+ core::mem::swap(&mut x, &mut y);
34+ println!("{} {}", x, y);
35+}
new file mode 100644
@@ -0,0 +1,35 @@
1+// These are `std`'s own surface rather than calls into a dependency, and each
2+// has an exact Nim counterpart. `with_capacity` reserves without adding
3+// elements, which is `newSeqOfCap`; `newSeq` would create that many zeroed
4+// ones and `len()` would not match.
5+
6+use core::mem::size_of;
7+
8+fn main() {
9+ let mut v: Vec<i32> = Vec::new();
10+ println!("{} {}", v.len(), v.is_empty());
11+ v.push(3);
12+ v.push(4);
13+ println!("{:?} {}", v, v.len());
14+
15+ let w: Vec<u8> = Vec::with_capacity(16);
16+ println!("{} {}", w.len(), w.is_empty());
17+
18+ let s = String::new();
19+ println!("[{}] {}", s, s.len());
20+ let t = String::with_capacity(32);
21+ println!("[{}] {}", t, t.len());
22+
23+ println!("{} {} {} {}", size_of::<u8>(), size_of::<u32>(), size_of::<i64>(), size_of::<f64>());
24+
25+ let a: i32 = i32::default();
26+ let b: bool = bool::default();
27+ let c: f64 = f64::default();
28+ let d: String = String::default();
29+ println!("{} {} {} [{}]", a, b, c, d);
30+
31+ let mut x: i32 = 1;
32+ let mut y: i32 = 2;
33+ core::mem::swap(&mut x, &mut y);
34+ println!("{} {}", x, y);
35+}