lex_bytecode/conc_registry.rs
1//! Process-global named-actor registry (#444).
2//!
3//! Keys are user-chosen `String` names; values are `Value::Actor`
4//! handles. The registry is flat (one namespace per process) — if two
5//! libraries need to avoid name collisions they prefix the string
6//! themselves. Access is serialised through a single `Mutex` because
7//! register/lookup/unregister all touch the same `HashMap`; contention
8//! is expected to be negligible (actor wiring happens once at
9//! `main()`, lookups serialise with the actor's own mutex anyway).
10//!
11//! **Type tag.** v1 stores `Value::Actor` opaquely — `conc.lookup[S, M]`
12//! parameterises the static return type but the runtime trusts the
13//! registration site. Reified `(S, M)` SigId tagging at register/lookup
14//! is the right design (see #444 option B) but requires plumbing
15//! compile-time type info into bytecode constants — deferred to a
16//! follow-up. The TypeMismatch variant of `ConcError` is reserved for
17//! that future check.
18
19use crate::value::Value;
20use std::collections::HashMap;
21use std::sync::{Mutex, OnceLock};
22
23fn registry() -> &'static Mutex<HashMap<String, Value>> {
24 static REG: OnceLock<Mutex<HashMap<String, Value>>> = OnceLock::new();
25 REG.get_or_init(|| Mutex::new(HashMap::new()))
26}
27
28/// Register `actor` under `name`. Returns `Err` if the name is already
29/// taken — registration is intentionally exclusive so the agent code
30/// catches the "two actors fighting over one name" bug at the source
31/// level rather than overwriting silently.
32pub fn register(name: &str, actor: Value) -> Result<(), RegError> {
33 let mut g = registry().lock().expect("conc registry poisoned");
34 if g.contains_key(name) {
35 return Err(RegError::AlreadyRegistered(name.to_string()));
36 }
37 g.insert(name.to_string(), actor);
38 Ok(())
39}
40
41/// Look up an actor by name. Returns `None` if not registered.
42pub fn lookup(name: &str) -> Option<Value> {
43 let g = registry().lock().expect("conc registry poisoned");
44 g.get(name).cloned()
45}
46
47/// Unregister by name. Returns `Err` if the name isn't registered.
48/// Existing `Value::Actor` handles held by callers continue to work;
49/// the actor cell is reclaimed when the last handle drops.
50pub fn unregister(name: &str) -> Result<(), RegError> {
51 let mut g = registry().lock().expect("conc registry poisoned");
52 if g.remove(name).is_none() {
53 return Err(RegError::NotRegistered(name.to_string()));
54 }
55 Ok(())
56}
57
58/// Snapshot the current registered names (debug / introspection).
59pub fn registered() -> Vec<String> {
60 let g = registry().lock().expect("conc registry poisoned");
61 let mut names: Vec<String> = g.keys().cloned().collect();
62 names.sort();
63 names
64}
65
66#[derive(Debug, Clone)]
67pub enum RegError {
68 AlreadyRegistered(String),
69 NotRegistered(String),
70}
71
72/// Reset the registry. Used by tests that need a clean slate; not
73/// exposed to user code.
74#[doc(hidden)]
75pub fn _reset_for_tests() {
76 let mut g = registry().lock().expect("conc registry poisoned");
77 g.clear();
78}