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, Cx, Dir, Error, Event, Expr, Result, Symbol};
10use sim_lib_binding::BindingCell;
11use sim_lib_core::{
12    ReadEvalBroker, ReadEvalDecision, ReadEvalRequest, ReadEvalSource, RequestOrigin,
13    SourceAuthority,
14};
15use sim_shape::AnyShape;
16
17use crate::{IdentitySpecifierPolicy, ModuleSpecifierPolicy, SpecifierPolicyRequest};
18
19/// Capability required before namespace source resolution begins.
20pub fn module_load_capability() -> CapabilityName {
21    CapabilityName::new("namespace.module.load")
22}
23
24/// Canonical identity of a module within one caller-named root.
25#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
26pub struct ModuleIdentity {
27    root: Symbol,
28    path: String,
29}
30
31impl ModuleIdentity {
32    /// Caller-supplied root identity.
33    pub fn root(&self) -> &Symbol {
34        &self.root
35    }
36    /// Normalized, root-relative module path.
37    pub fn path(&self) -> &str {
38        &self.path
39    }
40}
41
42/// Complete input for resolving one source module.
43pub struct ModuleRequest {
44    /// Stable identity assigned to the supplied root by its caller.
45    pub root_id: Symbol,
46    /// The only directory through which source may be resolved.
47    pub root: Arc<dyn Dir>,
48    /// Importing module identity for relative resolution, if any.
49    pub importer: Option<ModuleIdentity>,
50    /// Root-relative or `./` / `../` module specifier.
51    pub specifier: String,
52    /// Installed codec used by the read-eval broker.
53    pub codec: Symbol,
54    /// Trusted host authority governing module source and evaluation.
55    pub authority: SourceAuthority,
56}
57
58impl ModuleRequest {
59    /// Builds a module request with every source and authority input explicit.
60    pub fn new(
61        root_id: Symbol,
62        root: Arc<dyn Dir>,
63        importer: Option<ModuleIdentity>,
64        specifier: String,
65        codec: Symbol,
66        authority: SourceAuthority,
67    ) -> Self {
68        Self {
69            root_id,
70            root,
71            importer,
72            specifier,
73            codec,
74            authority,
75        }
76    }
77}
78
79/// A linked module and its stable live default-export edge.
80#[derive(Clone, Debug)]
81pub struct ModuleInstance {
82    identity: ModuleIdentity,
83    generation: u64,
84    default_export: BindingCell,
85}
86
87impl ModuleInstance {
88    /// Canonical module identity.
89    pub fn identity(&self) -> &ModuleIdentity {
90        &self.identity
91    }
92    /// Successful replacement generation, starting at one.
93    pub fn generation(&self) -> u64 {
94        self.generation
95    }
96    /// Live binding followed by importers across cache replacement.
97    pub fn default_export(&self) -> &BindingCell {
98        &self.default_export
99    }
100}
101
102/// Inspectable terminal outcome for a resolution attempt.
103#[derive(Clone, Copy, Debug, PartialEq, Eq)]
104pub enum ModuleResolutionOutcome {
105    /// Source was decoded, evaluated, and linked.
106    Linked,
107    /// An already linked cache generation was returned.
108    CacheHit,
109    /// Resolution was refused before read-eval was reached.
110    ReadRefused,
111    /// The selected codec could not decode the source.
112    DecodeFailed,
113    /// Evaluation or its result-shape check failed.
114    EvalFailed,
115    /// The initializing thread requested the same canonical module again.
116    Cycle,
117}
118
119/// Deterministic evidence published for every terminal resolution attempt.
120#[derive(Clone, Debug, PartialEq, Eq)]
121pub struct ModuleResolutionReceipt {
122    /// Canonical requested identity.
123    pub identity: ModuleIdentity,
124    /// Cache generation observed or produced.
125    pub generation: u64,
126    /// Terminal result.
127    pub outcome: ModuleResolutionOutcome,
128    /// Stable failure text when resolution did not link.
129    pub detail: Option<String>,
130    /// Exact existing read-eval ledger event, when evaluation was reached.
131    pub read_eval_event: Option<Event>,
132}
133
134enum CacheState {
135    Initializing {
136        owner: ThreadId,
137        generation: u64,
138    },
139    Linked(ModuleInstance),
140    Failed {
141        generation: u64,
142        message: String,
143        binding: BindingCell,
144        outcome: ModuleResolutionOutcome,
145    },
146}
147
148#[derive(Default)]
149struct LoaderState {
150    cache: BTreeMap<ModuleIdentity, CacheState>,
151    receipts: Vec<ModuleResolutionReceipt>,
152}
153
154/// Source-bound module cache. No loader lock is held during storage or user evaluation.
155pub struct ModuleLoader {
156    state: Mutex<LoaderState>,
157    changed: Condvar,
158    broker: ReadEvalBroker,
159    specifier_policy: Arc<dyn ModuleSpecifierPolicy>,
160}
161
162impl Default for ModuleLoader {
163    fn default() -> Self {
164        Self {
165            state: Mutex::new(LoaderState::default()),
166            changed: Condvar::new(),
167            broker: ReadEvalBroker::default(),
168            specifier_policy: Arc::new(IdentitySpecifierPolicy),
169        }
170    }
171}
172
173impl ModuleLoader {
174    /// Creates an empty loader and receipt history.
175    pub fn new() -> Self {
176        Self::default()
177    }
178
179    /// Creates a loader with an explicit bounded textual specifier policy.
180    pub fn with_specifier_policy(specifier_policy: Arc<dyn ModuleSpecifierPolicy>) -> Self {
181        Self {
182            specifier_policy,
183            ..Self::default()
184        }
185    }
186
187    /// Resolves and links a module, sharing concurrent work and cached failures.
188    pub fn load(&self, cx: &mut Cx, request: ModuleRequest) -> Result<ModuleInstance> {
189        cx.require(&module_load_capability())?;
190        let identity = canonical_identity(&request, self.specifier_policy.as_ref())?;
191        let owner = std::thread::current().id();
192        let (generation, binding) = loop {
193            let mut state = self.lock_state()?;
194            match state.cache.get(&identity) {
195                Some(CacheState::Linked(instance)) => {
196                    let instance = instance.clone();
197                    push_receipt(
198                        &mut state,
199                        &identity,
200                        instance.generation,
201                        ModuleResolutionOutcome::CacheHit,
202                        None,
203                        None,
204                    );
205                    return Ok(instance);
206                }
207                Some(CacheState::Failed {
208                    generation,
209                    message,
210                    outcome,
211                    ..
212                }) => {
213                    let generation = *generation;
214                    let message = message.clone();
215                    let outcome = *outcome;
216                    push_receipt(
217                        &mut state,
218                        &identity,
219                        generation,
220                        outcome,
221                        Some(message.clone()),
222                        None,
223                    );
224                    return Err(Error::Eval(message));
225                }
226                Some(CacheState::Initializing {
227                    owner: active,
228                    generation,
229                    ..
230                }) if *active == owner => {
231                    let generation = *generation;
232                    let message = format!("module cycle at {}:{}", identity.root, identity.path);
233                    push_receipt(
234                        &mut state,
235                        &identity,
236                        generation,
237                        ModuleResolutionOutcome::Cycle,
238                        Some(message.clone()),
239                        None,
240                    );
241                    return Err(Error::Eval(message));
242                }
243                Some(CacheState::Initializing { .. }) => {
244                    drop(
245                        self.changed
246                            .wait(state)
247                            .map_err(|_| Error::PoisonedLock("module loader"))?,
248                    );
249                    continue;
250                }
251                None => {
252                    let binding = BindingCell::uninitialized(Symbol::new(identity.path.clone()));
253                    state.cache.insert(
254                        identity.clone(),
255                        CacheState::Initializing {
256                            owner,
257                            generation: 1,
258                        },
259                    );
260                    break (1, binding);
261                }
262            }
263        };
264        self.finish_load(cx, request, identity, generation, binding)
265    }
266
267    /// Forces a replacement load while preserving existing live bindings.
268    pub fn reload(&self, cx: &mut Cx, request: ModuleRequest) -> Result<ModuleInstance> {
269        cx.require(&module_load_capability())?;
270        let identity = canonical_identity(&request, self.specifier_policy.as_ref())?;
271        let owner = std::thread::current().id();
272        let (generation, binding) = {
273            let mut state = self.lock_state()?;
274            let (generation, binding) = match state.cache.remove(&identity) {
275                Some(CacheState::Linked(instance)) => {
276                    (instance.generation + 1, instance.default_export)
277                }
278                Some(CacheState::Failed {
279                    generation,
280                    binding,
281                    ..
282                }) => (generation + 1, binding),
283                Some(initializing @ CacheState::Initializing { .. }) => {
284                    state.cache.insert(identity.clone(), initializing);
285                    return Err(Error::Eval(format!(
286                        "cannot replace initializing module {}:{}",
287                        identity.root, identity.path
288                    )));
289                }
290                None => (
291                    1,
292                    BindingCell::uninitialized(Symbol::new(identity.path.clone())),
293                ),
294            };
295            state.cache.insert(
296                identity.clone(),
297                CacheState::Initializing { owner, generation },
298            );
299            (generation, binding)
300        };
301        self.finish_load(cx, request, identity, generation, binding)
302    }
303
304    /// Snapshot of ordered resolution evidence.
305    pub fn receipts(&self) -> Result<Vec<ModuleResolutionReceipt>> {
306        Ok(self.lock_state()?.receipts.clone())
307    }
308
309    /// Snapshot of the diminished read-eval decisions linked to module loads.
310    pub fn decisions(&self, cx: &Cx) -> Result<Vec<ReadEvalDecision>> {
311        self.broker.decisions(cx)
312    }
313
314    fn finish_load(
315        &self,
316        cx: &mut Cx,
317        request: ModuleRequest,
318        identity: ModuleIdentity,
319        generation: u64,
320        binding: BindingCell,
321    ) -> Result<ModuleInstance> {
322        let admission = match read_source(cx, request.root.as_ref(), &identity.path) {
323            Ok(source) => Some(self.broker.admit_with_event(
324                cx,
325                ReadEvalRequest::new(
326                    RequestOrigin::with_detail(
327                        Symbol::qualified("namespace", "module"),
328                        Expr::String(format!("{}:{}", identity.root, identity.path)),
329                    ),
330                    request.codec,
331                    source,
332                    request.authority,
333                    Arc::new(AnyShape),
334                ),
335            )?),
336            Err(error) => {
337                return self.finish_refused(identity, generation, binding, error);
338            }
339        };
340        let admission = admission.expect("successful read creates an admission");
341        let outcome = match admission.decision.outcome {
342            sim_lib_core::ReadEvalOutcome::DecodeFailed => ModuleResolutionOutcome::DecodeFailed,
343            sim_lib_core::ReadEvalOutcome::Admitted => ModuleResolutionOutcome::Linked,
344            _ => ModuleResolutionOutcome::EvalFailed,
345        };
346        let event = admission.event;
347        let result = admission.result;
348        let mut state = self.lock_state()?;
349        match result {
350            Ok(value) => {
351                binding.set(value)?;
352                let instance = ModuleInstance {
353                    identity: identity.clone(),
354                    generation,
355                    default_export: binding,
356                };
357                state
358                    .cache
359                    .insert(identity.clone(), CacheState::Linked(instance.clone()));
360                push_receipt(
361                    &mut state,
362                    &identity,
363                    generation,
364                    ModuleResolutionOutcome::Linked,
365                    None,
366                    Some(event),
367                );
368                self.changed.notify_all();
369                Ok(instance)
370            }
371            Err(error) => {
372                let message = error.to_string();
373                state.cache.insert(
374                    identity.clone(),
375                    CacheState::Failed {
376                        generation,
377                        message: message.clone(),
378                        binding,
379                        outcome,
380                    },
381                );
382                push_receipt(
383                    &mut state,
384                    &identity,
385                    generation,
386                    outcome,
387                    Some(message.clone()),
388                    Some(event),
389                );
390                self.changed.notify_all();
391                Err(Error::Eval(message))
392            }
393        }
394    }
395
396    fn finish_refused(
397        &self,
398        identity: ModuleIdentity,
399        generation: u64,
400        binding: BindingCell,
401        error: Error,
402    ) -> Result<ModuleInstance> {
403        let message = error.to_string();
404        let mut state = self.lock_state()?;
405        state.cache.insert(
406            identity.clone(),
407            CacheState::Failed {
408                generation,
409                message: message.clone(),
410                binding,
411                outcome: ModuleResolutionOutcome::ReadRefused,
412            },
413        );
414        push_receipt(
415            &mut state,
416            &identity,
417            generation,
418            ModuleResolutionOutcome::ReadRefused,
419            Some(message.clone()),
420            None,
421        );
422        self.changed.notify_all();
423        Err(Error::Eval(message))
424    }
425
426    fn lock_state(&self) -> Result<MutexGuard<'_, LoaderState>> {
427        self.state
428            .lock()
429            .map_err(|_| Error::PoisonedLock("module loader"))
430    }
431}
432
433/// Codec-bound source-module policy shared by guest-language adapters.
434///
435/// The policy owns exactly one loader, and therefore one cache and ordered
436/// evidence stream. Roots and authority remain request inputs: this shared
437/// type deliberately supplies neither an ambient root nor guest defaults.
438pub struct SourceModulePolicy {
439    loader: ModuleLoader,
440    codec: Symbol,
441}
442
443impl SourceModulePolicy {
444    /// Creates a policy for an installed codec and explicit specifier policy.
445    pub fn new(codec: Symbol, specifier_policy: Arc<dyn ModuleSpecifierPolicy>) -> Self {
446        Self {
447            loader: ModuleLoader::with_specifier_policy(specifier_policy),
448            codec,
449        }
450    }
451
452    /// Loads a root-relative module through the caller-supplied root and authority.
453    pub fn load(
454        &self,
455        cx: &mut Cx,
456        root_id: Symbol,
457        root: Arc<dyn Dir>,
458        specifier: impl Into<String>,
459        authority: SourceAuthority,
460    ) -> Result<ModuleInstance> {
461        self.load_from(cx, root_id, root, None, specifier, authority)
462    }
463
464    /// Loads a module, optionally resolving it relative to an importer.
465    pub fn load_from(
466        &self,
467        cx: &mut Cx,
468        root_id: Symbol,
469        root: Arc<dyn Dir>,
470        importer: Option<ModuleIdentity>,
471        specifier: impl Into<String>,
472        authority: SourceAuthority,
473    ) -> Result<ModuleInstance> {
474        self.loader.load(
475            cx,
476            ModuleRequest::new(
477                root_id,
478                root,
479                importer,
480                specifier.into(),
481                self.codec.clone(),
482                authority,
483            ),
484        )
485    }
486
487    /// Dynamically imports through the same cache and authority boundary as static loads.
488    pub fn dynamic_import(
489        &self,
490        cx: &mut Cx,
491        root_id: Symbol,
492        root: Arc<dyn Dir>,
493        importer: Option<ModuleIdentity>,
494        specifier: impl Into<String>,
495        authority: SourceAuthority,
496    ) -> Result<ModuleInstance> {
497        self.load_from(cx, root_id, root, importer, specifier, authority)
498    }
499
500    /// Re-evaluates a module while preserving its existing live binding edge.
501    pub fn reload(
502        &self,
503        cx: &mut Cx,
504        root_id: Symbol,
505        root: Arc<dyn Dir>,
506        importer: Option<ModuleIdentity>,
507        specifier: impl Into<String>,
508        authority: SourceAuthority,
509    ) -> Result<ModuleInstance> {
510        self.loader.reload(
511            cx,
512            ModuleRequest::new(
513                root_id,
514                root,
515                importer,
516                specifier.into(),
517                self.codec.clone(),
518                authority,
519            ),
520        )
521    }
522
523    /// Snapshot of ordered module lifecycle receipts.
524    pub fn receipts(&self) -> Result<Vec<ModuleResolutionReceipt>> {
525        self.loader.receipts()
526    }
527
528    /// Snapshot of the linked diminished read-eval decisions.
529    pub fn decisions(&self, cx: &Cx) -> Result<Vec<ReadEvalDecision>> {
530        self.loader.decisions(cx)
531    }
532}
533
534fn canonical_identity(
535    request: &ModuleRequest,
536    policy: &dyn ModuleSpecifierPolicy,
537) -> Result<ModuleIdentity> {
538    let policy_request =
539        SpecifierPolicyRequest::new(request.importer.clone(), vec![request.specifier.clone()])
540            .map_err(|refusal| Error::Eval(refusal.to_string()))?;
541    let specifier = policy
542        .resolve(&policy_request)
543        .map_err(|refusal| Error::Eval(refusal.to_string()))?;
544    let specifier = specifier.as_str();
545    let absolute = specifier.starts_with('/');
546    if absolute {
547        return Err(Error::Eval(
548            "module specifier must be root-relative, not absolute".to_owned(),
549        ));
550    }
551    let mut parts = if specifier.starts_with('.') {
552        let importer = request
553            .importer
554            .as_ref()
555            .ok_or_else(|| Error::Eval("relative module request has no importer".to_owned()))?;
556        if importer.root != request.root_id {
557            return Err(Error::Eval(
558                "relative module request crosses supplied roots".to_owned(),
559            ));
560        }
561        let mut base: Vec<&str> = importer.path.split('/').collect();
562        base.pop();
563        base
564    } else {
565        Vec::new()
566    };
567    for part in specifier.split('/') {
568        match part {
569            "" | "." => {}
570            ".." => {
571                if parts.pop().is_none() {
572                    return Err(Error::Eval(
573                        "module request escapes supplied root".to_owned(),
574                    ));
575                }
576            }
577            component if component.contains('\\') => {
578                return Err(Error::Eval(
579                    "module path contains a non-canonical separator".to_owned(),
580                ));
581            }
582            component => parts.push(component),
583        }
584    }
585    if parts.is_empty() {
586        return Err(Error::Eval("module path is empty".to_owned()));
587    }
588    Ok(ModuleIdentity {
589        root: request.root_id.clone(),
590        path: parts.join("/"),
591    })
592}
593
594fn read_source(cx: &mut Cx, root: &dyn Dir, path: &str) -> Result<ReadEvalSource> {
595    let components = path.split('/').collect::<Vec<_>>();
596    read_source_at(cx, root, &components, path)
597}
598
599fn read_source_at(
600    cx: &mut Cx,
601    dir: &dyn Dir,
602    components: &[&str],
603    path: &str,
604) -> Result<ReadEvalSource> {
605    let (component, rest) = components
606        .split_first()
607        .ok_or_else(|| Error::Eval("module path is empty".to_owned()))?;
608    let key = Symbol::new(*component);
609    if rest.is_empty() {
610        if !dir.has(cx, key.clone())? {
611            return Err(Error::Eval(format!("module source not found: {path}")));
612        }
613        let value = dir.get(cx, key)?;
614        return match value.object().as_expr(cx)? {
615            Expr::String(text) => Ok(ReadEvalSource::Text(text)),
616            Expr::Bytes(bytes) => Ok(ReadEvalSource::Bytes(bytes)),
617            _ => Err(Error::Eval(format!(
618                "module source is not text or bytes: {path}"
619            ))),
620        };
621    }
622    let value = dir
623        .opendir(cx, key)?
624        .ok_or_else(|| Error::Eval(format!("module directory not found: {path}")))?;
625    let child = value
626        .object()
627        .as_dir()
628        .ok_or_else(|| Error::Eval(format!("module path component is not a Dir: {component}")))?;
629    read_source_at(cx, child, rest, path)
630}
631
632fn push_receipt(
633    state: &mut LoaderState,
634    identity: &ModuleIdentity,
635    generation: u64,
636    outcome: ModuleResolutionOutcome,
637    detail: Option<String>,
638    read_eval_event: Option<Event>,
639) {
640    state.receipts.push(ModuleResolutionReceipt {
641        identity: identity.clone(),
642        generation,
643        outcome,
644        detail,
645        read_eval_event,
646    });
647}
648
649#[cfg(test)]
650mod tests;