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>
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: | ||
| 847 | 847 | | **dependency types** | `Aes256Gcm::new`, `GHash::new`, `Bytes::new` | the closure problem | |
| 848 | 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. | |
| 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. | |
| 858 | 896 | |
| 859 | 897 | So the honest statement about this category is neither "bounded and cheap" nor |
| 860 | 898 | "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` is | 851 | + |
| 852 | -atomically refcounted where a plain Nim `ref` is not. `Mutex<T>` needs the | 852 | +`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 memory | 853 | +object. `.clone()` is therefore the identity — copying the ref *is* sharing |
| 854 | -orderings, and would be indistinguishable from a plain variable in a program | 854 | +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 modelled | 855 | +Rust's auto-deref does. |
| 856 | -at all. Each of those is a design question about semantics, not a syntactic | 856 | + |
| 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" nor | 897 | 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 | 898 | "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 { | ||
| 279 | 279 | aliases: HashMap<String, (Vec<String>, syn::Type)>, |
| 280 | 280 | /// Whether anything in the input is a future, which decides if the async |
| 281 | 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 | 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 | 291 | /// Module names supplied as separate input files. A `mod x;` naming one |
| 284 | 292 | /// of these is satisfied by that file having been passed in. |
| 285 | 293 | pub modules: Vec<String>, |
| @@ -339,7 +347,9 @@ impl Lowerer { | ||
| 339 | 347 | vec_expect: None, |
| 340 | 348 | forwards: Vec::new(), |
| 341 | 349 | aliases: HashMap::new(), |
| 350 | + pending_locks: Vec::new(), | |
| 342 | 351 | uses_async: false, |
| 352 | + uses_sync: std::cell::Cell::new(false), | |
| 343 | 353 | modules: Vec::new(), |
| 344 | 354 | emitted: 0, |
| 345 | 355 | features: Vec::new(), |
| @@ -429,6 +439,10 @@ impl Lowerer { | ||
| 429 | 439 | self.out.push_str(include_str!("prelude_async.nim")); |
| 430 | 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 | 447 | // Pass 2: type definitions, which every signature may mention. |
| 434 | 448 | for (m, f) in files { |
| @@ -943,6 +957,14 @@ impl Lowerer { | ||
| 943 | 957 | /// lowering goes through here rather than calling `ty::map` directly, so |
| 944 | 958 | /// an alias cannot be missed in one position and honoured in another. |
| 945 | 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 | 968 | // `Self::Item` names an associated type of the enclosing `impl`. |
| 947 | 969 | if let syn::Type::Path(p) = t { |
| 948 | 970 | let segs: Vec<String> = |
| @@ -2375,6 +2397,16 @@ impl Lowerer { | ||
| 2375 | 2397 | } |
| 2376 | 2398 | |
| 2377 | 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 | 2410 | match s { |
| 2379 | 2411 | Stmt::Local(l) => self.local(l), |
| 2380 | 2412 | Stmt::Expr(e, _) => { |
| @@ -2477,6 +2509,13 @@ impl Lowerer { | ||
| 2477 | 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 | 2519 | if let Some(w) = v.window.clone() { |
| 2481 | 2520 | // `let dst = dst.get_mut(..n).ok_or(..)?;` -- the binding names a |
| 2482 | 2521 | // view into the caller's buffer. Copying it into a `seq` would |
| @@ -3791,6 +3830,14 @@ impl Lowerer { | ||
| 3791 | 3830 | syn::Member::Named(n) => n.to_string(), |
| 3792 | 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 | 3841 | let t = match &base.ty { |
| 3795 | 3842 | Some(bt @ Nim::Named(s, _)) => self |
| 3796 | 3843 | .structs |
| @@ -4156,7 +4203,14 @@ impl Lowerer { | ||
| 4156 | 4203 | } |
| 4157 | 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 | 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 | 4798 | // Standard-library constructors. These are not calls into a |
| 4720 | 4799 | // dependency -- they are `std`'s own surface, and each has an exact |
| 4721 | 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 | 5103 | let rt0 = recv.ty.clone(); |
| 5017 | 5104 | |
| 5018 | 5105 | // `s.get(a..b)` / `s.get_mut(a..b)`: an `Option<&[T]>`. Nim has no |
| @@ -5093,6 +5180,21 @@ impl Lowerer { | ||
| 5093 | 5180 | let a0 = args.first().map(|a| a.code.clone()); |
| 5094 | 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 | 5198 | // A method the input defines wins over our model of the standard |
| 5097 | 5199 | // library: `is_empty` on a `bitflags!` type is that type's, not the |
| 5098 | 5200 | // sequence one. Rust resolves inherent methods the same way. |
| @@ -5315,6 +5417,32 @@ impl Lowerer { | ||
| 5315 | 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 | 5446 | "abs" => (format!("abs({})", recv.code), rt.clone()), |
| 5319 | 5447 | "min" => (format!("min({}, {})", recv.code, a0.unwrap_or_default()), rt.clone()), |
| 5320 | 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 | 6018 | /// Rust's `Default` for a primitive is its zero value, which is Nim's too. |
| 5877 | 6019 | fn default_of(t: &Nim) -> Option<String> { |
| 5878 | 6020 | 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 async | 280 | /// 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 one | 291 | /// 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, so | 957 | /// 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 a | 2520 | // `let dst = dst.get_mut(..n).ok_or(..)?;` -- the binding names a |
| 2482 | // view into the caller's buffer. Copying it into a `seq` would | 2521 | // 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, _)) => self | 3842 | Some(bt @ Nim::Named(s, _)) => self |
| 3796 | .structs | 3843 | .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 a | 4798 | // Standard-library constructors. These are not calls into a |
| 4720 | // dependency -- they are `std`'s own surface, and each has an exact | 4799 | // 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 no | 5105 | // `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 standard | 5198 | // 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 the | 5199 | // 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> { | ||
| 271 | 271 | // The `log` facade's types, under shim names. |
| 272 | 272 | ("Level", 0) => Ok(Nim::Prim("RsLogLevel".into())), |
| 273 | 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 | 277 | ("Vec", 1) => Ok(Nim::Seq(Box::new(args[0].clone().owned()))), |
| 275 | 278 | ("Option", 1) => Ok(Nim::Named("Option".into(), args)), |
| 276 | 279 | ("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 { | ||
| 66 | 66 | |
| 67 | 67 | // ------------------------------------------------------------------ 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 | 74 | fn run(cmd: &mut Command, stdin: Option<&str>) -> Result<Run, String> { |
| 70 | 75 | cmd.stdin(if stdin.is_some() { Stdio::piped() } else { Stdio::null() }) |
| 71 | 76 | .stdout(Stdio::piped()) |
| @@ -74,6 +79,22 @@ fn run(cmd: &mut Command, stdin: Option<&str>) -> Result<Run, String> { | ||
| 74 | 79 | if let Some(s) = stdin { |
| 75 | 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 | 98 | let out = child.wait_with_output().map_err(|e| e.to_string())?; |
| 78 | 99 | Ok(Run { |
| 79 | 100 | status: out.status.code(), |
| @@ -66,6 +66,11 @@ impl Run { | |||
| 66 | 66 | ||
| 67 | // ------------------------------------------------------------------ helpers | 67 | // ------------------------------------------------------------------ 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(), |