nandi/rustnimpublic Fork
⑂ 8587dec
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 Arc and Mutex, with the guard released where Rust releases it

Arc<T> is shared ownership, which a Nim ref gives: two bindings naming one
object. clone() is the identity, since copying the ref is sharing it, and a
method call or field access on an Arc reaches the contents as Rust's
auto-deref does.

Mutex<T>'s guard releases on Drop, and Nim's defer is the same rule including
on an early return -- probed first, together with the fact that two
sequential locked scopes do not deadlock. But where it releases depends on
something a naive lowering gets wrong: a bound guard lives to the end of its
scope, while a temporary is dropped at the end of the statement. The release
is queued when the lock is taken and the statement decides which it was.
Getting that wrong deadlocks, which is how it was found.

Two differences are real and recorded rather than papered over. Arc refcounts
atomically where a plain Nim ref does not, which matters only across threads
and std::thread is not modelled at all. And poisoning has no equivalent: Rust
poisons a mutex when a holder panics and lock() then returns Err, whereas
here lock() always succeeds -- panics map to quit(101) rather than unwinding,
so nothing is left to poison it, but a program that observes poisoning would
differ. AtomicUsize stays unmapped deliberately: it needs memory orderings
and would be indistinguishable from a plain variable in a program with no
threads, which cannot be verified here.

The test case was first written with two lock() calls in one println!, which
deadlocks in Rust too -- Mutex is not reentrant and both guards live to the
end of the statement. The transpiler was right and the case was wrong, but it
exposed a real hole in the harness: the runner blocked forever instead of
reporting. Every child process now has a 60-second timeout and a hanging case
fails with a reason. A harness that can hang is a harness that can stop
telling you things.

Accepted crates unchanged at 18 of 400.

50 differential cases, all green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
nandithebull committed 2026-09-19T06:50:09-07:00 Browse files
8587dec parent: edb7777
modified DESIGN.md +46 -8
@@ -847,14 +847,52 @@ That took the category from 62 to 54, and what is left divides cleanly:
847847 | **dependency types** | `Aes256Gcm::new`, `GHash::new`, `Bytes::new` | the closure problem |
848848 | **platform** | `env::var_os`, `getauxval` | `std::env`, `libc` |
849849
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.
850+### `Arc` and `Mutex`
851+
852+`Arc<T>` is shared ownership, which a Nim `ref` gives: two bindings naming one
853+object. `.clone()` is therefore the identity — copying the ref *is* sharing
854+it — and a method call or field access on an `Arc` reaches the contents, as
855+Rust's auto-deref does.
856+
857+`Mutex<T>`'s guard releases on `Drop`. Nim's `defer` is the same rule,
858+including on an early return, which is what makes this exact rather than
859+approximate. But **where** it releases depends on something Rust decides and a
860+naive lowering does not:
861+
862+- a **bound** guard (`let g = m.lock().unwrap()`) lives to the end of its
863+ scope — `defer`;
864+- a **temporary** (`m.lock().unwrap().field`) is dropped at the end of the
865+ *statement*.
866+
867+Getting that wrong deadlocks: two `lock()` calls in one statement with the
868+first still held. The release is queued when the lock is taken and the
869+statement decides which it was.
870+
871+Two differences are real and are recorded rather than papered over:
872+
873+- `Arc` refcounts **atomically**; a plain Nim `ref` does not. That matters
874+ only across threads, and `std::thread` is not modelled at all, so a program
875+ using it is rejected before reaching here.
876+- **Poisoning has no equivalent.** Rust poisons a mutex when a holder panics
877+ and `lock()` then returns `Err`. Here `lock()` always succeeds. Panics map
878+ to `quit(101)` rather than unwinding, so there is no holder left to poison
879+ it — but a program that depends on *observing* poisoning would differ.
880+
881+`AtomicUsize` is still unmapped, and deliberately: it needs memory orderings,
882+and would be indistinguishable from a plain variable in a program with no
883+threads — which cannot be verified here.
884+
885+### The harness had a hole
886+
887+`tests/cases/044` was first written with two `lock()` calls in one `println!`,
888+which **deadlocks in Rust too** — `Mutex` is not reentrant and both guards
889+live to the end of the statement. The transpiler was right; the case was
890+wrong.
891+
892+It exposed a real gap: the runner blocked forever instead of reporting. Every
893+child process now has a 60-second timeout, and a case that hangs fails with
894+"timed out — a deadlock, or an unbounded loop". A harness that can hang is a
895+harness that can stop telling you things.
858896
859897 So the honest statement about this category is neither "bounded and cheap" nor
860898 "unbounded tail": part of it was cheap std surface and is done, and the rest
@@ -847,14 +847,52 @@ That took the category from 62 to 54, and what is left divides cleanly:
847 | **dependency types** | `Aes256Gcm::new`, `GHash::new`, `Bytes::new` | the closure problem |847 | **dependency types** | `Aes256Gcm::new`, `GHash::new`, `Bytes::new` | the closure problem |
848 | **platform** | `env::var_os`, `getauxval` | `std::env`, `libc` |848 | **platform** | `env::var_os`, `getauxval` | `std::env`, `libc` |
849 849
850-The concurrency group is the interesting one, and it is *not* a mapping table.850+### `Arc` and `Mutex`
851-`Arc<T>` is shared ownership, which Nim's GC'd `ref` gives — but `Arc` is851+
852-atomically refcounted where a plain Nim `ref` is not. `Mutex<T>` needs the852+`Arc<T>` is shared ownership, which a Nim `ref` gives: two bindings naming one
853-guard's `Drop` to release the lock at scope exit. `AtomicUsize` needs memory853+object. `.clone()` is therefore the identity — copying the ref *is* sharing
854-orderings, and would be indistinguishable from a plain variable in a program854+it — and a method call or field access on an `Arc` reaches the contents, as
855-with no threads — which we cannot verify, since `std::thread` is not modelled855+Rust's auto-deref does.
856-at all. Each of those is a design question about semantics, not a syntactic856+
857-gap, and none should be mapped casually.857+`Mutex<T>`'s guard releases on `Drop`. Nim's `defer` is the same rule,
858+including on an early return, which is what makes this exact rather than
859+approximate. But **where** it releases depends on something Rust decides and a
860+naive lowering does not:
861+
862+- a **bound** guard (`let g = m.lock().unwrap()`) lives to the end of its
863+ scope — `defer`;
864+- a **temporary** (`m.lock().unwrap().field`) is dropped at the end of the
865+ *statement*.
866+
867+Getting that wrong deadlocks: two `lock()` calls in one statement with the
868+first still held. The release is queued when the lock is taken and the
869+statement decides which it was.
870+
871+Two differences are real and are recorded rather than papered over:
872+
873+- `Arc` refcounts **atomically**; a plain Nim `ref` does not. That matters
874+ only across threads, and `std::thread` is not modelled at all, so a program
875+ using it is rejected before reaching here.
876+- **Poisoning has no equivalent.** Rust poisons a mutex when a holder panics
877+ and `lock()` then returns `Err`. Here `lock()` always succeeds. Panics map
878+ to `quit(101)` rather than unwinding, so there is no holder left to poison
879+ it — but a program that depends on *observing* poisoning would differ.
880+
881+`AtomicUsize` is still unmapped, and deliberately: it needs memory orderings,
882+and would be indistinguishable from a plain variable in a program with no
883+threads — which cannot be verified here.
884+
885+### The harness had a hole
886+
887+`tests/cases/044` was first written with two `lock()` calls in one `println!`,
888+which **deadlocks in Rust too** — `Mutex` is not reentrant and both guards
889+live to the end of the statement. The transpiler was right; the case was
890+wrong.
891+
892+It exposed a real gap: the runner blocked forever instead of reporting. Every
893+child process now has a 60-second timeout, and a case that hangs fails with
894+"timed out — a deadlock, or an unbounded loop". A harness that can hang is a
895+harness that can stop telling you things.
858 896
859 So the honest statement about this category is neither "bounded and cheap" nor897 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 rest898 "unbounded tail": part of it was cheap std surface and is done, and the rest
modified src/lower.rs +144 -2
@@ -279,7 +279,15 @@ pub struct Lowerer {
279279 aliases: HashMap<String, (Vec<String>, syn::Type)>,
280280 /// Whether anything in the input is a future, which decides if the async
281281 /// runtime is emitted.
282+ /// Locks acquired while lowering the current statement, awaiting a
283+ /// decision about where they are released.
284+ pending_locks: Vec<String>,
282285 uses_async: bool,
286+ /// Whether anything here is an `Arc`/`Rc`/`Mutex`, which decides if the
287+ /// locking runtime is emitted. A `Cell` because it is set while *mapping*
288+ /// a type, which happens behind `&self`, and the answer has to be known
289+ /// before the prelude is written.
290+ uses_sync: std::cell::Cell<bool>,
283291 /// Module names supplied as separate input files. A `mod x;` naming one
284292 /// of these is satisfied by that file having been passed in.
285293 pub modules: Vec<String>,
@@ -339,7 +347,9 @@ impl Lowerer {
339347 vec_expect: None,
340348 forwards: Vec::new(),
341349 aliases: HashMap::new(),
350+ pending_locks: Vec::new(),
342351 uses_async: false,
352+ uses_sync: std::cell::Cell::new(false),
343353 modules: Vec::new(),
344354 emitted: 0,
345355 features: Vec::new(),
@@ -429,6 +439,10 @@ impl Lowerer {
429439 self.out.push_str(include_str!("prelude_async.nim"));
430440 self.blank();
431441 }
442+ if self.uses_sync.get() {
443+ self.out.push_str(include_str!("prelude_sync.nim"));
444+ self.blank();
445+ }
432446
433447 // Pass 2: type definitions, which every signature may mention.
434448 for (m, f) in files {
@@ -943,6 +957,14 @@ impl Lowerer {
943957 /// lowering goes through here rather than calling `ty::map` directly, so
944958 /// an alias cannot be missed in one position and honoured in another.
945959 fn map_ty(&self, t: &syn::Type) -> Result<Nim, String> {
960+ let n = self.map_ty_inner(t)?;
961+ if mentions_sync(&n) {
962+ self.uses_sync.set(true);
963+ }
964+ Ok(n)
965+ }
966+
967+ fn map_ty_inner(&self, t: &syn::Type) -> Result<Nim, String> {
946968 // `Self::Item` names an associated type of the enclosing `impl`.
947969 if let syn::Type::Path(p) = t {
948970 let segs: Vec<String> =
@@ -2375,6 +2397,16 @@ impl Lowerer {
23752397 }
23762398
23772399 fn stmt(&mut self, s: &Stmt) -> Result<(), String> {
2400+ let r = self.stmt_inner(s);
2401+ // Any lock still queued was taken by a temporary guard, which Rust
2402+ // drops at the end of the statement.
2403+ for lk in std::mem::take(&mut self.pending_locks) {
2404+ self.line(&format!("release({})", lk));
2405+ }
2406+ r
2407+ }
2408+
2409+ fn stmt_inner(&mut self, s: &Stmt) -> Result<(), String> {
23782410 match s {
23792411 Stmt::Local(l) => self.local(l),
23802412 Stmt::Expr(e, _) => {
@@ -2477,6 +2509,13 @@ impl Lowerer {
24772509 return Ok(());
24782510 }
24792511
2512+ if v.window.is_some() && !self.pending_locks.is_empty() {
2513+ // The binding *is* the guard, so it lives as long as the scope --
2514+ // which is what `defer` gives.
2515+ for lk in std::mem::take(&mut self.pending_locks) {
2516+ self.line(&format!("defer: release({})", lk));
2517+ }
2518+ }
24802519 if let Some(w) = v.window.clone() {
24812520 // `let dst = dst.get_mut(..n).ok_or(..)?;` -- the binding names a
24822521 // view into the caller's buffer. Copying it into a `seq` would
@@ -3791,6 +3830,14 @@ impl Lowerer {
37913830 syn::Member::Named(n) => n.to_string(),
37923831 syn::Member::Unnamed(i) => format!("f{}", i.index),
37933832 };
3833+ // An `Arc` derefs to its contents, as Rust's does.
3834+ let base = match &base.ty {
3835+ Some(Nim::Named(n, a)) if n == "RsArc" => Val::new(
3836+ format!("{}.v", base.code),
3837+ a.first().cloned(),
3838+ ),
3839+ _ => base,
3840+ };
37943841 let t = match &base.ty {
37953842 Some(bt @ Nim::Named(s, _)) => self
37963843 .structs
@@ -4156,7 +4203,14 @@ impl Lowerer {
41564203 }
41574204 Ok(Val::new(format!("(not {})", v.code), v.ty))
41584205 }
4159- UnOp::Deref(_) => Ok(v),
4206+ UnOp::Deref(_) => Ok(match &v.ty {
4207+ // `*arc` is its contents; `*guard` is already the value the
4208+ // guard aliases.
4209+ Some(Nim::Named(n, a)) if n == "RsArc" => {
4210+ Val::new(format!("{}.v", v.code), a.first().cloned())
4211+ }
4212+ _ => v,
4213+ }),
41604214 _ => Err("unsupported unary operator".into()),
41614215 }
41624216 }
@@ -4716,6 +4770,31 @@ impl Lowerer {
47164770 }
47174771 }
47184772
4773+ // `Arc::new` / `Rc::new` / `Mutex::new`.
4774+ match (
4775+ p.path.segments.iter().rev().nth(1).map(|s| s.ident.to_string()).as_deref(),
4776+ name.as_str(),
4777+ codes.len(),
4778+ ) {
4779+ (Some("Arc" | "Rc"), "new", 1) => {
4780+ self.uses_sync.set(true);
4781+ let inner = args[0].ty.clone();
4782+ return Ok(Val::new(
4783+ format!("rsArcNew({})", codes[0]),
4784+ inner.map(|t| Nim::Named("RsArc".into(), vec![t])),
4785+ ));
4786+ }
4787+ (Some("Mutex"), "new", 1) => {
4788+ self.uses_sync.set(true);
4789+ let inner = args[0].ty.clone();
4790+ return Ok(Val::new(
4791+ format!("rsMutexNew({})", codes[0]),
4792+ inner.map(|t| Nim::Named("RsMutex".into(), vec![t])),
4793+ ));
4794+ }
4795+ _ => {}
4796+ }
4797+
47194798 // Standard-library constructors. These are not calls into a
47204799 // dependency -- they are `std`'s own surface, and each has an exact
47214800 // Nim counterpart rather than an approximate one.
@@ -5012,7 +5091,15 @@ impl Lowerer {
50125091 }
50135092 }
50145093 }
5015- let recv = self.expr(&m.receiver)?;
5094+ let mut recv = self.expr(&m.receiver)?;
5095+ // A method call on an `Arc` reaches the contents, as Rust's auto-deref
5096+ // does -- `arc_of_mutex.lock()` is the mutex's method.
5097+ if let Some(Nim::Named(n, a)) = recv.ty.clone() {
5098+ if n == "RsArc" && !matches!(m.method.to_string().as_str(), "clone") {
5099+ recv = Val::new(format!("{}.v", recv.code), a.first().cloned());
5100+ }
5101+ }
5102+ let recv = recv;
50165103 let rt0 = recv.ty.clone();
50175104
50185105 // `s.get(a..b)` / `s.get_mut(a..b)`: an `Option<&[T]>`. Nim has no
@@ -5093,6 +5180,21 @@ impl Lowerer {
50935180 let a0 = args.first().map(|a| a.code.clone());
50945181 let rt = recv.ty.clone();
50955182
5183+ // The guard is what `lock()` already produced; `unwrap` on it is the
5184+ // identity. A poisoned mutex has no equivalent here -- see
5185+ // `src/prelude_sync.nim`. This is checked before the general `unwrap`,
5186+ // which would look for a `Result`.
5187+ if matches!(name.as_str(), "unwrap" | "expect") {
5188+ if let Some(Nim::Named(n, a)) = &rt {
5189+ if n == "RsGuard" {
5190+ let inner = a.first().cloned();
5191+ let mut v = Val::new(recv.code.clone(), inner.clone());
5192+ v.window = Some(Alias::Value { code: recv.code.clone(), ty: inner });
5193+ return Ok(v);
5194+ }
5195+ }
5196+ }
5197+
50965198 // A method the input defines wins over our model of the standard
50975199 // library: `is_empty` on a `bitflags!` type is that type's, not the
50985200 // sequence one. Rust resolves inherent methods the same way.
@@ -5315,6 +5417,32 @@ impl Lowerer {
53155417 Some(Nim::Ptr(Box::new(elem))),
53165418 )
53175419 }
5420+ // `m.lock()` acquires, and the guard releases on Drop -- at scope
5421+ // exit and on an early return, which is exactly Nim's `defer`.
5422+ // The guard is an *alias* to the protected value, not a copy, so
5423+ // writing through it writes to the mutex.
5424+ "lock" | "try_lock" if matches!(&rt, Some(Nim::Named(n, _)) if n == "RsMutex") => {
5425+ let Some(Nim::Named(_, a)) = &rt else { unreachable!() };
5426+ let inner = a
5427+ .first()
5428+ .cloned()
5429+ .ok_or("`lock()` on a mutex with no known value type")?;
5430+ // Rust drops a *temporary* guard at the end of the statement
5431+ // and a *bound* one at the end of its scope. Which of those
5432+ // applies is not known until the statement is assembled, so
5433+ // the release is queued and the statement decides.
5434+ self.line(&format!("acquire({}.lk)", recv.code));
5435+ self.pending_locks.push(format!("{}.lk", recv.code));
5436+ let mut v = Val::new(
5437+ format!("{}.val", recv.code),
5438+ Some(Nim::Named("RsGuard".into(), vec![inner.clone()])),
5439+ );
5440+ v.window = Some(Alias::Value {
5441+ code: format!("{}.val", recv.code),
5442+ ty: Some(inner),
5443+ });
5444+ return Ok(v);
5445+ }
53185446 "abs" => (format!("abs({})", recv.code), rt.clone()),
53195447 "min" => (format!("min({}, {})", recv.code, a0.unwrap_or_default()), rt.clone()),
53205448 "max" => (format!("max({}, {})", recv.code, a0.unwrap_or_default()), rt.clone()),
@@ -5873,6 +6001,20 @@ fn module_of(prefix: &[String]) -> String {
58736001 }
58746002 }
58756003
6004+/// Whether a type involves the locking runtime.
6005+fn mentions_sync(t: &Nim) -> bool {
6006+ match t {
6007+ Nim::Named(n, a) => {
6008+ n == "RsArc" || n == "RsMutex" || a.iter().any(mentions_sync)
6009+ }
6010+ Nim::Seq(e) | Nim::OpenArray(e) | Nim::Var(e) | Nim::Array(_, e) | Nim::Iter(e)
6011+ | Nim::Future(e) | Nim::Ptr(e) | Nim::ConstPtr(e) => mentions_sync(e),
6012+ Nim::Tuple(ts) => ts.iter().any(mentions_sync),
6013+ Nim::Proc(a, r) => a.iter().any(mentions_sync) || mentions_sync(r),
6014+ _ => false,
6015+ }
6016+}
6017+
58766018 /// Rust's `Default` for a primitive is its zero value, which is Nim's too.
58776019 fn default_of(t: &Nim) -> Option<String> {
58786020 Some(match t {
@@ -279,7 +279,15 @@ pub struct Lowerer {
279 aliases: HashMap<String, (Vec<String>, syn::Type)>,279 aliases: HashMap<String, (Vec<String>, syn::Type)>,
280 /// Whether anything in the input is a future, which decides if the async280 /// Whether anything in the input is a future, which decides if the async
281 /// runtime is emitted.281 /// runtime is emitted.
282+ /// Locks acquired while lowering the current statement, awaiting a
283+ /// decision about where they are released.
284+ pending_locks: Vec<String>,
282 uses_async: bool,285 uses_async: bool,
286+ /// Whether anything here is an `Arc`/`Rc`/`Mutex`, which decides if the
287+ /// locking runtime is emitted. A `Cell` because it is set while *mapping*
288+ /// a type, which happens behind `&self`, and the answer has to be known
289+ /// before the prelude is written.
290+ uses_sync: std::cell::Cell<bool>,
283 /// Module names supplied as separate input files. A `mod x;` naming one291 /// Module names supplied as separate input files. A `mod x;` naming one
284 /// of these is satisfied by that file having been passed in.292 /// of these is satisfied by that file having been passed in.
285 pub modules: Vec<String>,293 pub modules: Vec<String>,
@@ -339,7 +347,9 @@ impl Lowerer {
339 vec_expect: None,347 vec_expect: None,
340 forwards: Vec::new(),348 forwards: Vec::new(),
341 aliases: HashMap::new(),349 aliases: HashMap::new(),
350+ pending_locks: Vec::new(),
342 uses_async: false,351 uses_async: false,
352+ uses_sync: std::cell::Cell::new(false),
343 modules: Vec::new(),353 modules: Vec::new(),
344 emitted: 0,354 emitted: 0,
345 features: Vec::new(),355 features: Vec::new(),
@@ -429,6 +439,10 @@ impl Lowerer {
429 self.out.push_str(include_str!("prelude_async.nim"));439 self.out.push_str(include_str!("prelude_async.nim"));
430 self.blank();440 self.blank();
431 }441 }
442+ if self.uses_sync.get() {
443+ self.out.push_str(include_str!("prelude_sync.nim"));
444+ self.blank();
445+ }
432 446
433 // Pass 2: type definitions, which every signature may mention.447 // Pass 2: type definitions, which every signature may mention.
434 for (m, f) in files {448 for (m, f) in files {
@@ -943,6 +957,14 @@ impl Lowerer {
943 /// lowering goes through here rather than calling `ty::map` directly, so957 /// lowering goes through here rather than calling `ty::map` directly, so
944 /// an alias cannot be missed in one position and honoured in another.958 /// an alias cannot be missed in one position and honoured in another.
945 fn map_ty(&self, t: &syn::Type) -> Result<Nim, String> {959 fn map_ty(&self, t: &syn::Type) -> Result<Nim, String> {
960+ let n = self.map_ty_inner(t)?;
961+ if mentions_sync(&n) {
962+ self.uses_sync.set(true);
963+ }
964+ Ok(n)
965+ }
966+
967+ fn map_ty_inner(&self, t: &syn::Type) -> Result<Nim, String> {
946 // `Self::Item` names an associated type of the enclosing `impl`.968 // `Self::Item` names an associated type of the enclosing `impl`.
947 if let syn::Type::Path(p) = t {969 if let syn::Type::Path(p) = t {
948 let segs: Vec<String> =970 let segs: Vec<String> =
@@ -2375,6 +2397,16 @@ impl Lowerer {
2375 }2397 }
2376 2398
2377 fn stmt(&mut self, s: &Stmt) -> Result<(), String> {2399 fn stmt(&mut self, s: &Stmt) -> Result<(), String> {
2400+ let r = self.stmt_inner(s);
2401+ // Any lock still queued was taken by a temporary guard, which Rust
2402+ // drops at the end of the statement.
2403+ for lk in std::mem::take(&mut self.pending_locks) {
2404+ self.line(&format!("release({})", lk));
2405+ }
2406+ r
2407+ }
2408+
2409+ fn stmt_inner(&mut self, s: &Stmt) -> Result<(), String> {
2378 match s {2410 match s {
2379 Stmt::Local(l) => self.local(l),2411 Stmt::Local(l) => self.local(l),
2380 Stmt::Expr(e, _) => {2412 Stmt::Expr(e, _) => {
@@ -2477,6 +2509,13 @@ impl Lowerer {
2477 return Ok(());2509 return Ok(());
2478 }2510 }
2479 2511
2512+ if v.window.is_some() && !self.pending_locks.is_empty() {
2513+ // The binding *is* the guard, so it lives as long as the scope --
2514+ // which is what `defer` gives.
2515+ for lk in std::mem::take(&mut self.pending_locks) {
2516+ self.line(&format!("defer: release({})", lk));
2517+ }
2518+ }
2480 if let Some(w) = v.window.clone() {2519 if let Some(w) = v.window.clone() {
2481 // `let dst = dst.get_mut(..n).ok_or(..)?;` -- the binding names a2520 // `let dst = dst.get_mut(..n).ok_or(..)?;` -- the binding names a
2482 // view into the caller's buffer. Copying it into a `seq` would2521 // view into the caller's buffer. Copying it into a `seq` would
@@ -3791,6 +3830,14 @@ impl Lowerer {
3791 syn::Member::Named(n) => n.to_string(),3830 syn::Member::Named(n) => n.to_string(),
3792 syn::Member::Unnamed(i) => format!("f{}", i.index),3831 syn::Member::Unnamed(i) => format!("f{}", i.index),
3793 };3832 };
3833+ // An `Arc` derefs to its contents, as Rust's does.
3834+ let base = match &base.ty {
3835+ Some(Nim::Named(n, a)) if n == "RsArc" => Val::new(
3836+ format!("{}.v", base.code),
3837+ a.first().cloned(),
3838+ ),
3839+ _ => base,
3840+ };
3794 let t = match &base.ty {3841 let t = match &base.ty {
3795 Some(bt @ Nim::Named(s, _)) => self3842 Some(bt @ Nim::Named(s, _)) => self
3796 .structs3843 .structs
@@ -4156,7 +4203,14 @@ impl Lowerer {
4156 }4203 }
4157 Ok(Val::new(format!("(not {})", v.code), v.ty))4204 Ok(Val::new(format!("(not {})", v.code), v.ty))
4158 }4205 }
4159- UnOp::Deref(_) => Ok(v),4206+ UnOp::Deref(_) => Ok(match &v.ty {
4207+ // `*arc` is its contents; `*guard` is already the value the
4208+ // guard aliases.
4209+ Some(Nim::Named(n, a)) if n == "RsArc" => {
4210+ Val::new(format!("{}.v", v.code), a.first().cloned())
4211+ }
4212+ _ => v,
4213+ }),
4160 _ => Err("unsupported unary operator".into()),4214 _ => Err("unsupported unary operator".into()),
4161 }4215 }
4162 }4216 }
@@ -4716,6 +4770,31 @@ impl Lowerer {
4716 }4770 }
4717 }4771 }
4718 4772
4773+ // `Arc::new` / `Rc::new` / `Mutex::new`.
4774+ match (
4775+ p.path.segments.iter().rev().nth(1).map(|s| s.ident.to_string()).as_deref(),
4776+ name.as_str(),
4777+ codes.len(),
4778+ ) {
4779+ (Some("Arc" | "Rc"), "new", 1) => {
4780+ self.uses_sync.set(true);
4781+ let inner = args[0].ty.clone();
4782+ return Ok(Val::new(
4783+ format!("rsArcNew({})", codes[0]),
4784+ inner.map(|t| Nim::Named("RsArc".into(), vec![t])),
4785+ ));
4786+ }
4787+ (Some("Mutex"), "new", 1) => {
4788+ self.uses_sync.set(true);
4789+ let inner = args[0].ty.clone();
4790+ return Ok(Val::new(
4791+ format!("rsMutexNew({})", codes[0]),
4792+ inner.map(|t| Nim::Named("RsMutex".into(), vec![t])),
4793+ ));
4794+ }
4795+ _ => {}
4796+ }
4797+
4719 // Standard-library constructors. These are not calls into a4798 // Standard-library constructors. These are not calls into a
4720 // dependency -- they are `std`'s own surface, and each has an exact4799 // dependency -- they are `std`'s own surface, and each has an exact
4721 // Nim counterpart rather than an approximate one.4800 // Nim counterpart rather than an approximate one.
@@ -5012,7 +5091,15 @@ impl Lowerer {
5012 }5091 }
5013 }5092 }
5014 }5093 }
5015- let recv = self.expr(&m.receiver)?;5094+ let mut recv = self.expr(&m.receiver)?;
5095+ // A method call on an `Arc` reaches the contents, as Rust's auto-deref
5096+ // does -- `arc_of_mutex.lock()` is the mutex's method.
5097+ if let Some(Nim::Named(n, a)) = recv.ty.clone() {
5098+ if n == "RsArc" && !matches!(m.method.to_string().as_str(), "clone") {
5099+ recv = Val::new(format!("{}.v", recv.code), a.first().cloned());
5100+ }
5101+ }
5102+ let recv = recv;
5016 let rt0 = recv.ty.clone();5103 let rt0 = recv.ty.clone();
5017 5104
5018 // `s.get(a..b)` / `s.get_mut(a..b)`: an `Option<&[T]>`. Nim has no5105 // `s.get(a..b)` / `s.get_mut(a..b)`: an `Option<&[T]>`. Nim has no
@@ -5093,6 +5180,21 @@ impl Lowerer {
5093 let a0 = args.first().map(|a| a.code.clone());5180 let a0 = args.first().map(|a| a.code.clone());
5094 let rt = recv.ty.clone();5181 let rt = recv.ty.clone();
5095 5182
5183+ // The guard is what `lock()` already produced; `unwrap` on it is the
5184+ // identity. A poisoned mutex has no equivalent here -- see
5185+ // `src/prelude_sync.nim`. This is checked before the general `unwrap`,
5186+ // which would look for a `Result`.
5187+ if matches!(name.as_str(), "unwrap" | "expect") {
5188+ if let Some(Nim::Named(n, a)) = &rt {
5189+ if n == "RsGuard" {
5190+ let inner = a.first().cloned();
5191+ let mut v = Val::new(recv.code.clone(), inner.clone());
5192+ v.window = Some(Alias::Value { code: recv.code.clone(), ty: inner });
5193+ return Ok(v);
5194+ }
5195+ }
5196+ }
5197+
5096 // A method the input defines wins over our model of the standard5198 // A method the input defines wins over our model of the standard
5097 // library: `is_empty` on a `bitflags!` type is that type's, not the5199 // library: `is_empty` on a `bitflags!` type is that type's, not the
5098 // sequence one. Rust resolves inherent methods the same way.5200 // sequence one. Rust resolves inherent methods the same way.
@@ -5315,6 +5417,32 @@ impl Lowerer {
5315 Some(Nim::Ptr(Box::new(elem))),5417 Some(Nim::Ptr(Box::new(elem))),
5316 )5418 )
5317 }5419 }
5420+ // `m.lock()` acquires, and the guard releases on Drop -- at scope
5421+ // exit and on an early return, which is exactly Nim's `defer`.
5422+ // The guard is an *alias* to the protected value, not a copy, so
5423+ // writing through it writes to the mutex.
5424+ "lock" | "try_lock" if matches!(&rt, Some(Nim::Named(n, _)) if n == "RsMutex") => {
5425+ let Some(Nim::Named(_, a)) = &rt else { unreachable!() };
5426+ let inner = a
5427+ .first()
5428+ .cloned()
5429+ .ok_or("`lock()` on a mutex with no known value type")?;
5430+ // Rust drops a *temporary* guard at the end of the statement
5431+ // and a *bound* one at the end of its scope. Which of those
5432+ // applies is not known until the statement is assembled, so
5433+ // the release is queued and the statement decides.
5434+ self.line(&format!("acquire({}.lk)", recv.code));
5435+ self.pending_locks.push(format!("{}.lk", recv.code));
5436+ let mut v = Val::new(
5437+ format!("{}.val", recv.code),
5438+ Some(Nim::Named("RsGuard".into(), vec![inner.clone()])),
5439+ );
5440+ v.window = Some(Alias::Value {
5441+ code: format!("{}.val", recv.code),
5442+ ty: Some(inner),
5443+ });
5444+ return Ok(v);
5445+ }
5318 "abs" => (format!("abs({})", recv.code), rt.clone()),5446 "abs" => (format!("abs({})", recv.code), rt.clone()),
5319 "min" => (format!("min({}, {})", recv.code, a0.unwrap_or_default()), rt.clone()),5447 "min" => (format!("min({}, {})", recv.code, a0.unwrap_or_default()), rt.clone()),
5320 "max" => (format!("max({}, {})", recv.code, a0.unwrap_or_default()), rt.clone()),5448 "max" => (format!("max({}, {})", recv.code, a0.unwrap_or_default()), rt.clone()),
@@ -5873,6 +6001,20 @@ fn module_of(prefix: &[String]) -> String {
5873 }6001 }
5874 }6002 }
5875 6003
6004+/// Whether a type involves the locking runtime.
6005+fn mentions_sync(t: &Nim) -> bool {
6006+ match t {
6007+ Nim::Named(n, a) => {
6008+ n == "RsArc" || n == "RsMutex" || a.iter().any(mentions_sync)
6009+ }
6010+ Nim::Seq(e) | Nim::OpenArray(e) | Nim::Var(e) | Nim::Array(_, e) | Nim::Iter(e)
6011+ | Nim::Future(e) | Nim::Ptr(e) | Nim::ConstPtr(e) => mentions_sync(e),
6012+ Nim::Tuple(ts) => ts.iter().any(mentions_sync),
6013+ Nim::Proc(a, r) => a.iter().any(mentions_sync) || mentions_sync(r),
6014+ _ => false,
6015+ }
6016+}
6017+
5876 /// Rust's `Default` for a primitive is its zero value, which is Nim's too.6018 /// Rust's `Default` for a primitive is its zero value, which is Nim's too.
5877 fn default_of(t: &Nim) -> Option<String> {6019 fn default_of(t: &Nim) -> Option<String> {
5878 Some(match t {6020 Some(match t {
added src/prelude_sync.nim +41 -0
new file mode 100644
@@ -0,0 +1,41 @@
1+import std/locks
2+
3+# ---------------------------------------------------------------------------
4+# `Arc`/`Rc` and `Mutex`.
5+#
6+# `Arc<T>` is shared ownership, which a Nim `ref` gives: two bindings naming
7+# one object, collected when the last goes away. `.clone()` is therefore the
8+# identity here -- copying the ref *is* sharing it.
9+#
10+# `Mutex<T>`'s guard releases the lock when it is dropped, at scope exit and
11+# on an early return. Nim's `defer` is that same rule, which is what makes the
12+# mapping exact rather than approximate; probed against Nim 2.2.4, including
13+# that two sequential locked scopes do not deadlock.
14+#
15+# Two differences are real and are not papered over:
16+#
17+# * `Arc` refcounts atomically; a plain Nim `ref` does not. That matters only
18+# across threads, and `std::thread` is not modelled at all -- a program
19+# using it is rejected before reaching here.
20+# * A poisoned `Mutex` has no equivalent. Rust poisons a mutex when a holder
21+# panics; `lock()` then returns `Err`. Here `lock()` always succeeds, so a
22+# program that depends on observing poisoning would differ. Panics are
23+# mapped to `quit(101)` rather than unwinding, so there is no holder left
24+# to poison it.
25+# ---------------------------------------------------------------------------
26+
27+type
28+ RsArc*[T] = ref object
29+ v*: T
30+ RsMutex*[T] = ref object
31+ lk*: Lock
32+ val*: T
33+
34+proc rsArcNew*[T](v: T): RsArc[T] =
35+ new(result)
36+ result.v = v
37+
38+proc rsMutexNew*[T](v: T): RsMutex[T] =
39+ new(result)
40+ initLock(result.lk)
41+ result.val = v
new file mode 100644
@@ -0,0 +1,41 @@
1+import std/locks
2+
3+# ---------------------------------------------------------------------------
4+# `Arc`/`Rc` and `Mutex`.
5+#
6+# `Arc<T>` is shared ownership, which a Nim `ref` gives: two bindings naming
7+# one object, collected when the last goes away. `.clone()` is therefore the
8+# identity here -- copying the ref *is* sharing it.
9+#
10+# `Mutex<T>`'s guard releases the lock when it is dropped, at scope exit and
11+# on an early return. Nim's `defer` is that same rule, which is what makes the
12+# mapping exact rather than approximate; probed against Nim 2.2.4, including
13+# that two sequential locked scopes do not deadlock.
14+#
15+# Two differences are real and are not papered over:
16+#
17+# * `Arc` refcounts atomically; a plain Nim `ref` does not. That matters only
18+# across threads, and `std::thread` is not modelled at all -- a program
19+# using it is rejected before reaching here.
20+# * A poisoned `Mutex` has no equivalent. Rust poisons a mutex when a holder
21+# panics; `lock()` then returns `Err`. Here `lock()` always succeeds, so a
22+# program that depends on observing poisoning would differ. Panics are
23+# mapped to `quit(101)` rather than unwinding, so there is no holder left
24+# to poison it.
25+# ---------------------------------------------------------------------------
26+
27+type
28+ RsArc*[T] = ref object
29+ v*: T
30+ RsMutex*[T] = ref object
31+ lk*: Lock
32+ val*: T
33+
34+proc rsArcNew*[T](v: T): RsArc[T] =
35+ new(result)
36+ result.v = v
37+
38+proc rsMutexNew*[T](v: T): RsMutex[T] =
39+ new(result)
40+ initLock(result.lk)
41+ result.val = v
modified src/ty.rs +3 -0
@@ -271,6 +271,9 @@ pub fn map(t: &Type) -> Result<Nim, String> {
271271 // The `log` facade's types, under shim names.
272272 ("Level", 0) => Ok(Nim::Prim("RsLogLevel".into())),
273273 ("LevelFilter", 0) => Ok(Nim::Prim("RsLogFilter".into())),
274+ // Shared ownership and locking: see `src/prelude_sync.nim`.
275+ ("Arc", 1) | ("Rc", 1) => Ok(Nim::Named("RsArc".into(), args)),
276+ ("Mutex", 1) => Ok(Nim::Named("RsMutex".into(), args)),
274277 ("Vec", 1) => Ok(Nim::Seq(Box::new(args[0].clone().owned()))),
275278 ("Option", 1) => Ok(Nim::Named("Option".into(), args)),
276279 ("Result", 2) => Ok(Nim::Named("Result".into(), args)),
@@ -271,6 +271,9 @@ pub fn map(t: &Type) -> Result<Nim, String> {
271 // The `log` facade's types, under shim names.271 // The `log` facade's types, under shim names.
272 ("Level", 0) => Ok(Nim::Prim("RsLogLevel".into())),272 ("Level", 0) => Ok(Nim::Prim("RsLogLevel".into())),
273 ("LevelFilter", 0) => Ok(Nim::Prim("RsLogFilter".into())),273 ("LevelFilter", 0) => Ok(Nim::Prim("RsLogFilter".into())),
274+ // Shared ownership and locking: see `src/prelude_sync.nim`.
275+ ("Arc", 1) | ("Rc", 1) => Ok(Nim::Named("RsArc".into(), args)),
276+ ("Mutex", 1) => Ok(Nim::Named("RsMutex".into(), args)),
274 ("Vec", 1) => Ok(Nim::Seq(Box::new(args[0].clone().owned()))),277 ("Vec", 1) => Ok(Nim::Seq(Box::new(args[0].clone().owned()))),
275 ("Option", 1) => Ok(Nim::Named("Option".into(), args)),278 ("Option", 1) => Ok(Nim::Named("Option".into(), args)),
276 ("Result", 2) => Ok(Nim::Named("Result".into(), args)),279 ("Result", 2) => Ok(Nim::Named("Result".into(), args)),
added tests/cases/044-arc-mutex.rs +68 -0
new file mode 100644
@@ -0,0 +1,68 @@
1+// `Arc<T>` is shared ownership, which a Nim `ref` gives: two bindings naming
2+// one object. `.clone()` is the identity -- copying the ref *is* sharing it.
3+//
4+// A `MutexGuard` releases on Drop, at scope exit and on an early return, which
5+// is exactly Nim's `defer`. That is what this case checks: the value is shared
6+// through the Arc, mutated through the guard, and a later lock in the same
7+// function must not deadlock -- which it would if the first guard never
8+// released.
9+//
10+// Single-threaded on purpose: `std::thread` is not modelled, and a program
11+// using it is rejected. What is verified here is sharing, mutation through the
12+// guard, and release-on-scope-exit.
13+
14+use std::sync::{Arc, Mutex};
15+
16+struct Counter {
17+ hits: i32,
18+ name: String,
19+}
20+
21+fn bump(m: &Arc<Mutex<i32>>) -> i32 {
22+ let mut g = m.lock().unwrap();
23+ *g += 1;
24+ *g
25+}
26+
27+fn early(m: &Arc<Mutex<i32>>, stop: bool) -> i32 {
28+ let mut g = m.lock().unwrap();
29+ if stop {
30+ // Returning here must still release the lock.
31+ return -1;
32+ }
33+ *g += 100;
34+ *g
35+}
36+
37+fn main() {
38+ let shared: Arc<Mutex<i32>> = Arc::new(Mutex::new(0));
39+
40+ println!("{}", bump(&shared));
41+ println!("{}", bump(&shared));
42+
43+ // A second handle is the same object.
44+ let other = shared.clone();
45+ println!("{}", bump(&other));
46+ println!("{}", *shared.lock().unwrap());
47+
48+ println!("{}", early(&shared, true));
49+ println!("{}", early(&shared, false));
50+ println!("{}", *shared.lock().unwrap());
51+
52+ // Arc over a struct: field access derefs through it.
53+ let c: Arc<Counter> = Arc::new(Counter { hits: 7, name: "abc".to_string() });
54+ let c2 = c.clone();
55+ println!("{} {} {}", c.hits, c2.hits, c.name);
56+
57+ // Arc over a Mutex holding a struct.
58+ let s: Arc<Mutex<Counter>> = Arc::new(Mutex::new(Counter { hits: 0, name: "m".to_string() }));
59+ {
60+ let mut g = s.lock().unwrap();
61+ g.hits += 5;
62+ }
63+ // One lock per statement: two guards alive at once would deadlock, in
64+ // Rust as much as here, since `Mutex` is not reentrant.
65+ let hits = s.lock().unwrap().hits;
66+ let nm = s.lock().unwrap().name.clone();
67+ println!("{} {}", hits, nm);
68+}
new file mode 100644
@@ -0,0 +1,68 @@
1+// `Arc<T>` is shared ownership, which a Nim `ref` gives: two bindings naming
2+// one object. `.clone()` is the identity -- copying the ref *is* sharing it.
3+//
4+// A `MutexGuard` releases on Drop, at scope exit and on an early return, which
5+// is exactly Nim's `defer`. That is what this case checks: the value is shared
6+// through the Arc, mutated through the guard, and a later lock in the same
7+// function must not deadlock -- which it would if the first guard never
8+// released.
9+//
10+// Single-threaded on purpose: `std::thread` is not modelled, and a program
11+// using it is rejected. What is verified here is sharing, mutation through the
12+// guard, and release-on-scope-exit.
13+
14+use std::sync::{Arc, Mutex};
15+
16+struct Counter {
17+ hits: i32,
18+ name: String,
19+}
20+
21+fn bump(m: &Arc<Mutex<i32>>) -> i32 {
22+ let mut g = m.lock().unwrap();
23+ *g += 1;
24+ *g
25+}
26+
27+fn early(m: &Arc<Mutex<i32>>, stop: bool) -> i32 {
28+ let mut g = m.lock().unwrap();
29+ if stop {
30+ // Returning here must still release the lock.
31+ return -1;
32+ }
33+ *g += 100;
34+ *g
35+}
36+
37+fn main() {
38+ let shared: Arc<Mutex<i32>> = Arc::new(Mutex::new(0));
39+
40+ println!("{}", bump(&shared));
41+ println!("{}", bump(&shared));
42+
43+ // A second handle is the same object.
44+ let other = shared.clone();
45+ println!("{}", bump(&other));
46+ println!("{}", *shared.lock().unwrap());
47+
48+ println!("{}", early(&shared, true));
49+ println!("{}", early(&shared, false));
50+ println!("{}", *shared.lock().unwrap());
51+
52+ // Arc over a struct: field access derefs through it.
53+ let c: Arc<Counter> = Arc::new(Counter { hits: 7, name: "abc".to_string() });
54+ let c2 = c.clone();
55+ println!("{} {} {}", c.hits, c2.hits, c.name);
56+
57+ // Arc over a Mutex holding a struct.
58+ let s: Arc<Mutex<Counter>> = Arc::new(Mutex::new(Counter { hits: 0, name: "m".to_string() }));
59+ {
60+ let mut g = s.lock().unwrap();
61+ g.hits += 5;
62+ }
63+ // One lock per statement: two guards alive at once would deadlock, in
64+ // Rust as much as here, since `Mutex` is not reentrant.
65+ let hits = s.lock().unwrap().hits;
66+ let nm = s.lock().unwrap().name.clone();
67+ println!("{} {}", hits, nm);
68+}
modified tests/differential.rs +21 -0
@@ -66,6 +66,11 @@ impl Run {
6666
6767 // ------------------------------------------------------------------ helpers
6868
69+/// A case that hangs must be a reported failure, not a hang: the suite
70+/// otherwise blocks forever on one deadlock. Found the hard way, by writing a
71+/// case that deadlocks in *Rust* -- two `MutexGuard`s alive in one statement.
72+const RUN_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(60);
73+
6974 fn run(cmd: &mut Command, stdin: Option<&str>) -> Result<Run, String> {
7075 cmd.stdin(if stdin.is_some() { Stdio::piped() } else { Stdio::null() })
7176 .stdout(Stdio::piped())
@@ -74,6 +79,22 @@ fn run(cmd: &mut Command, stdin: Option<&str>) -> Result<Run, String> {
7479 if let Some(s) = stdin {
7580 child.stdin.as_mut().unwrap().write_all(s.as_bytes()).map_err(|e| e.to_string())?;
7681 }
82+ // Poll rather than block, so a deadlocked child is killed and reported.
83+ let deadline = std::time::Instant::now() + RUN_TIMEOUT;
84+ loop {
85+ match child.try_wait().map_err(|e| e.to_string())? {
86+ Some(_) => break,
87+ None if std::time::Instant::now() >= deadline => {
88+ let _ = child.kill();
89+ let _ = child.wait();
90+ return Err(format!(
91+ "timed out after {}s -- a deadlock, or an unbounded loop",
92+ RUN_TIMEOUT.as_secs()
93+ ));
94+ }
95+ None => std::thread::sleep(std::time::Duration::from_millis(20)),
96+ }
97+ }
7798 let out = child.wait_with_output().map_err(|e| e.to_string())?;
7899 Ok(Run {
79100 status: out.status.code(),
@@ -66,6 +66,11 @@ impl Run {
66 66
67 // ------------------------------------------------------------------ helpers67 // ------------------------------------------------------------------ helpers
68 68
69+/// A case that hangs must be a reported failure, not a hang: the suite
70+/// otherwise blocks forever on one deadlock. Found the hard way, by writing a
71+/// case that deadlocks in *Rust* -- two `MutexGuard`s alive in one statement.
72+const RUN_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(60);
73+
69 fn run(cmd: &mut Command, stdin: Option<&str>) -> Result<Run, String> {74 fn run(cmd: &mut Command, stdin: Option<&str>) -> Result<Run, String> {
70 cmd.stdin(if stdin.is_some() { Stdio::piped() } else { Stdio::null() })75 cmd.stdin(if stdin.is_some() { Stdio::piped() } else { Stdio::null() })
71 .stdout(Stdio::piped())76 .stdout(Stdio::piped())
@@ -74,6 +79,22 @@ fn run(cmd: &mut Command, stdin: Option<&str>) -> Result<Run, String> {
74 if let Some(s) = stdin {79 if let Some(s) = stdin {
75 child.stdin.as_mut().unwrap().write_all(s.as_bytes()).map_err(|e| e.to_string())?;80 child.stdin.as_mut().unwrap().write_all(s.as_bytes()).map_err(|e| e.to_string())?;
76 }81 }
82+ // Poll rather than block, so a deadlocked child is killed and reported.
83+ let deadline = std::time::Instant::now() + RUN_TIMEOUT;
84+ loop {
85+ match child.try_wait().map_err(|e| e.to_string())? {
86+ Some(_) => break,
87+ None if std::time::Instant::now() >= deadline => {
88+ let _ = child.kill();
89+ let _ = child.wait();
90+ return Err(format!(
91+ "timed out after {}s -- a deadlock, or an unbounded loop",
92+ RUN_TIMEOUT.as_secs()
93+ ));
94+ }
95+ None => std::thread::sleep(std::time::Duration::from_millis(20)),
96+ }
97+ }
77 let out = child.wait_with_output().map_err(|e| e.to_string())?;98 let out = child.wait_with_output().map_err(|e| e.to_string())?;
78 Ok(Run {99 Ok(Run {
79 status: out.status.code(),100 status: out.status.code(),