Skip to main content

sim_lib_namespace/
module.rs

1//! Capability-aware source module resolution and lifecycle.
2
3use std::{
4    collections::BTreeMap,
5    sync::{Arc, Condvar, Mutex, MutexGuard},
6    thread::ThreadId,
7};
8
9use sim_kernel::{CapabilityName, CapabilitySet, Cx, Dir, Error, Expr, ReadPolicy, Result, Symbol};
10use sim_lib_binding::BindingCell;
11use sim_lib_core::{ReadEvalBroker, ReadEvalRequest, ReadEvalSource, RequestOrigin};
12use sim_shape::AnyShape;
13
14/// Capability required before namespace source resolution begins.
15pub fn module_load_capability() -> CapabilityName {
16    CapabilityName::new("namespace.module.load")
17}
18
19/// Canonical identity of a module within one caller-named root.
20#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
21pub struct ModuleIdentity {
22    root: Symbol,
23    path: String,
24}
25
26impl ModuleIdentity {
27    /// Caller-supplied root identity.
28    pub fn root(&self) -> &Symbol {
29        &self.root
30    }
31    /// Normalized, root-relative module path.
32    pub fn path(&self) -> &str {
33        &self.path
34    }
35}
36
37/// Complete input for resolving one source module.
38pub struct ModuleRequest {
39    /// Stable identity assigned to the supplied root by its caller.
40    pub root_id: Symbol,
41    /// The only directory through which source may be resolved.
42    pub root: Arc<dyn Dir>,
43    /// Importing module identity for relative resolution, if any.
44    pub importer: Option<ModuleIdentity>,
45    /// Root-relative or `./` / `../` module specifier.
46    pub specifier: String,
47    /// Installed codec used by the read-eval broker.
48    pub codec: Symbol,
49    /// Trusted host-built read policy.
50    pub read_policy: ReadPolicy,
51    /// Additional caller powers required by this module.
52    pub requires: Vec<CapabilityName>,
53    /// Diminished powers under which module code evaluates.
54    pub allow: CapabilitySet,
55}
56
57/// A linked module and its stable live default-export edge.
58#[derive(Clone, Debug)]
59pub struct ModuleInstance {
60    identity: ModuleIdentity,
61    generation: u64,
62    default_export: BindingCell,
63}
64
65impl ModuleInstance {
66    /// Canonical module identity.
67    pub fn identity(&self) -> &ModuleIdentity {
68        &self.identity
69    }
70    /// Successful replacement generation, starting at one.
71    pub fn generation(&self) -> u64 {
72        self.generation
73    }
74    /// Live binding followed by importers across cache replacement.
75    pub fn default_export(&self) -> &BindingCell {
76        &self.default_export
77    }
78}
79
80/// Inspectable terminal outcome for a resolution attempt.
81#[derive(Clone, Copy, Debug, PartialEq, Eq)]
82pub enum ModuleResolutionOutcome {
83    /// Source was decoded, evaluated, and linked.
84    Linked,
85    /// An already linked cache generation was returned.
86    CacheHit,
87    /// Resolution, decoding, or evaluation failed.
88    Failed,
89    /// The initializing thread requested the same canonical module again.
90    Cycle,
91}
92
93/// Deterministic evidence published for every terminal resolution attempt.
94#[derive(Clone, Debug, PartialEq, Eq)]
95pub struct ModuleResolutionReceipt {
96    /// Canonical requested identity.
97    pub identity: ModuleIdentity,
98    /// Cache generation observed or produced.
99    pub generation: u64,
100    /// Terminal result.
101    pub outcome: ModuleResolutionOutcome,
102    /// Stable failure text when resolution did not link.
103    pub detail: Option<String>,
104}
105
106enum CacheState {
107    Initializing {
108        owner: ThreadId,
109        generation: u64,
110    },
111    Linked(ModuleInstance),
112    Failed {
113        generation: u64,
114        message: String,
115        binding: BindingCell,
116    },
117}
118
119#[derive(Default)]
120struct LoaderState {
121    cache: BTreeMap<ModuleIdentity, CacheState>,
122    receipts: Vec<ModuleResolutionReceipt>,
123}
124
125/// Source-bound module cache. No loader lock is held during storage or user evaluation.
126#[derive(Default)]
127pub struct ModuleLoader {
128    state: Mutex<LoaderState>,
129    changed: Condvar,
130    broker: ReadEvalBroker,
131}
132
133impl ModuleLoader {
134    /// Creates an empty loader and receipt history.
135    pub fn new() -> Self {
136        Self::default()
137    }
138
139    /// Resolves and links a module, sharing concurrent work and cached failures.
140    pub fn load(&self, cx: &mut Cx, request: ModuleRequest) -> Result<ModuleInstance> {
141        cx.require(&module_load_capability())?;
142        let identity = canonical_identity(&request)?;
143        let owner = std::thread::current().id();
144        let (generation, binding) = loop {
145            let mut state = self.lock_state()?;
146            match state.cache.get(&identity) {
147                Some(CacheState::Linked(instance)) => {
148                    let instance = instance.clone();
149                    push_receipt(
150                        &mut state,
151                        &identity,
152                        instance.generation,
153                        ModuleResolutionOutcome::CacheHit,
154                        None,
155                    );
156                    return Ok(instance);
157                }
158                Some(CacheState::Failed {
159                    generation,
160                    message,
161                    ..
162                }) => {
163                    let generation = *generation;
164                    let message = message.clone();
165                    push_receipt(
166                        &mut state,
167                        &identity,
168                        generation,
169                        ModuleResolutionOutcome::Failed,
170                        Some(message.clone()),
171                    );
172                    return Err(Error::Eval(message));
173                }
174                Some(CacheState::Initializing {
175                    owner: active,
176                    generation,
177                    ..
178                }) if *active == owner => {
179                    let generation = *generation;
180                    let message = format!("module cycle at {}:{}", identity.root, identity.path);
181                    push_receipt(
182                        &mut state,
183                        &identity,
184                        generation,
185                        ModuleResolutionOutcome::Cycle,
186                        Some(message.clone()),
187                    );
188                    return Err(Error::Eval(message));
189                }
190                Some(CacheState::Initializing { .. }) => {
191                    drop(
192                        self.changed
193                            .wait(state)
194                            .map_err(|_| Error::PoisonedLock("module loader"))?,
195                    );
196                    continue;
197                }
198                None => {
199                    let binding = BindingCell::uninitialized(Symbol::new(identity.path.clone()));
200                    state.cache.insert(
201                        identity.clone(),
202                        CacheState::Initializing {
203                            owner,
204                            generation: 1,
205                        },
206                    );
207                    break (1, binding);
208                }
209            }
210        };
211        self.finish_load(cx, request, identity, generation, binding)
212    }
213
214    /// Forces a replacement load while preserving existing live bindings.
215    pub fn reload(&self, cx: &mut Cx, request: ModuleRequest) -> Result<ModuleInstance> {
216        cx.require(&module_load_capability())?;
217        let identity = canonical_identity(&request)?;
218        let owner = std::thread::current().id();
219        let (generation, binding) = {
220            let mut state = self.lock_state()?;
221            let (generation, binding) = match state.cache.remove(&identity) {
222                Some(CacheState::Linked(instance)) => {
223                    (instance.generation + 1, instance.default_export)
224                }
225                Some(CacheState::Failed {
226                    generation,
227                    binding,
228                    ..
229                }) => (generation + 1, binding),
230                Some(initializing @ CacheState::Initializing { .. }) => {
231                    state.cache.insert(identity.clone(), initializing);
232                    return Err(Error::Eval(format!(
233                        "cannot replace initializing module {}:{}",
234                        identity.root, identity.path
235                    )));
236                }
237                None => (
238                    1,
239                    BindingCell::uninitialized(Symbol::new(identity.path.clone())),
240                ),
241            };
242            state.cache.insert(
243                identity.clone(),
244                CacheState::Initializing { owner, generation },
245            );
246            (generation, binding)
247        };
248        self.finish_load(cx, request, identity, generation, binding)
249    }
250
251    /// Snapshot of ordered resolution evidence.
252    pub fn receipts(&self) -> Result<Vec<ModuleResolutionReceipt>> {
253        Ok(self.lock_state()?.receipts.clone())
254    }
255
256    fn finish_load(
257        &self,
258        cx: &mut Cx,
259        request: ModuleRequest,
260        identity: ModuleIdentity,
261        generation: u64,
262        binding: BindingCell,
263    ) -> Result<ModuleInstance> {
264        let result = (|| {
265            let source = read_source(cx, request.root.as_ref(), &identity.path)?;
266            self.broker.admit(
267                cx,
268                ReadEvalRequest {
269                    origin: RequestOrigin::with_detail(
270                        Symbol::qualified("namespace", "module"),
271                        Expr::String(format!("{}:{}", identity.root, identity.path)),
272                    ),
273                    codec: request.codec,
274                    source,
275                    read_policy: request.read_policy,
276                    requires: request.requires,
277                    allow: request.allow,
278                    expected_shape: Arc::new(AnyShape),
279                },
280            )
281        })();
282        let mut state = self.lock_state()?;
283        match result {
284            Ok(value) => {
285                binding.set(value)?;
286                let instance = ModuleInstance {
287                    identity: identity.clone(),
288                    generation,
289                    default_export: binding,
290                };
291                state
292                    .cache
293                    .insert(identity.clone(), CacheState::Linked(instance.clone()));
294                push_receipt(
295                    &mut state,
296                    &identity,
297                    generation,
298                    ModuleResolutionOutcome::Linked,
299                    None,
300                );
301                self.changed.notify_all();
302                Ok(instance)
303            }
304            Err(error) => {
305                let message = error.to_string();
306                state.cache.insert(
307                    identity.clone(),
308                    CacheState::Failed {
309                        generation,
310                        message: message.clone(),
311                        binding,
312                    },
313                );
314                push_receipt(
315                    &mut state,
316                    &identity,
317                    generation,
318                    ModuleResolutionOutcome::Failed,
319                    Some(message.clone()),
320                );
321                self.changed.notify_all();
322                Err(Error::Eval(message))
323            }
324        }
325    }
326
327    fn lock_state(&self) -> Result<MutexGuard<'_, LoaderState>> {
328        self.state
329            .lock()
330            .map_err(|_| Error::PoisonedLock("module loader"))
331    }
332}
333
334fn canonical_identity(request: &ModuleRequest) -> Result<ModuleIdentity> {
335    let absolute = request.specifier.starts_with('/');
336    if absolute {
337        return Err(Error::Eval(
338            "module specifier must be root-relative, not absolute".to_owned(),
339        ));
340    }
341    let mut parts = if request.specifier.starts_with('.') {
342        let importer = request
343            .importer
344            .as_ref()
345            .ok_or_else(|| Error::Eval("relative module request has no importer".to_owned()))?;
346        if importer.root != request.root_id {
347            return Err(Error::Eval(
348                "relative module request crosses supplied roots".to_owned(),
349            ));
350        }
351        let mut base: Vec<&str> = importer.path.split('/').collect();
352        base.pop();
353        base
354    } else {
355        Vec::new()
356    };
357    for part in request.specifier.split('/') {
358        match part {
359            "" | "." => {}
360            ".." => {
361                if parts.pop().is_none() {
362                    return Err(Error::Eval(
363                        "module request escapes supplied root".to_owned(),
364                    ));
365                }
366            }
367            component if component.contains('\\') => {
368                return Err(Error::Eval(
369                    "module path contains a non-canonical separator".to_owned(),
370                ));
371            }
372            component => parts.push(component),
373        }
374    }
375    if parts.is_empty() {
376        return Err(Error::Eval("module path is empty".to_owned()));
377    }
378    Ok(ModuleIdentity {
379        root: request.root_id.clone(),
380        path: parts.join("/"),
381    })
382}
383
384fn read_source(cx: &mut Cx, root: &dyn Dir, path: &str) -> Result<ReadEvalSource> {
385    let components = path.split('/').collect::<Vec<_>>();
386    read_source_at(cx, root, &components, path)
387}
388
389fn read_source_at(
390    cx: &mut Cx,
391    dir: &dyn Dir,
392    components: &[&str],
393    path: &str,
394) -> Result<ReadEvalSource> {
395    let (component, rest) = components
396        .split_first()
397        .ok_or_else(|| Error::Eval("module path is empty".to_owned()))?;
398    let key = Symbol::new(*component);
399    if rest.is_empty() {
400        if !dir.has(cx, key.clone())? {
401            return Err(Error::Eval(format!("module source not found: {path}")));
402        }
403        let value = dir.get(cx, key)?;
404        return match value.object().as_expr(cx)? {
405            Expr::String(text) => Ok(ReadEvalSource::Text(text)),
406            Expr::Bytes(bytes) => Ok(ReadEvalSource::Bytes(bytes)),
407            _ => Err(Error::Eval(format!(
408                "module source is not text or bytes: {path}"
409            ))),
410        };
411    }
412    let value = dir
413        .opendir(cx, key)?
414        .ok_or_else(|| Error::Eval(format!("module directory not found: {path}")))?;
415    let child = value
416        .object()
417        .as_dir()
418        .ok_or_else(|| Error::Eval(format!("module path component is not a Dir: {component}")))?;
419    read_source_at(cx, child, rest, path)
420}
421
422fn push_receipt(
423    state: &mut LoaderState,
424    identity: &ModuleIdentity,
425    generation: u64,
426    outcome: ModuleResolutionOutcome,
427    detail: Option<String>,
428) {
429    state.receipts.push(ModuleResolutionReceipt {
430        identity: identity.clone(),
431        generation,
432        outcome,
433        detail,
434    });
435}
436
437#[cfg(test)]
438mod tests;