Skip to main content

sim_lib_server/
isolation.rs

1use std::sync::Arc;
2
3use sim_kernel::{
4    CapabilityName, CapabilitySet, Cx, DefaultFactory, Env, Error, Expr, Factory, Registry, Result,
5    Symbol, Value,
6};
7
8use crate::{EvalSite, ServerAddress, ServerFrame};
9
10#[derive(Clone, Debug, PartialEq, Eq)]
11/// How one isolation axis (env, libs, factory, registry, capabilities) is
12/// derived for an evaluation.
13pub enum ShareMode {
14    /// Reuse the caller's value for this axis directly.
15    Share,
16    /// Derive a child scoped to the caller's value where applicable.
17    Child,
18    /// Start fresh with a default, sharing nothing from the caller.
19    Isolate,
20    /// Reconstruct the axis from a named registry snapshot symbol.
21    Import(Symbol),
22}
23
24#[derive(Clone, Debug, PartialEq, Eq)]
25/// Per-axis [`ShareMode`] selection controlling how an evaluation is isolated
26/// from its caller.
27pub struct IsolationPolicy {
28    /// Share mode for the lexical environment.
29    pub env: ShareMode,
30    /// Share mode for the loaded library set.
31    pub libs: ShareMode,
32    /// Share mode for the object factory.
33    pub factory: ShareMode,
34    /// Share mode for the registry.
35    pub registry: ShareMode,
36    /// Share mode for the capability set.
37    pub capabilities: ShareMode,
38}
39
40impl Default for IsolationPolicy {
41    fn default() -> Self {
42        Self {
43            env: ShareMode::Share,
44            libs: ShareMode::Share,
45            factory: ShareMode::Share,
46            registry: ShareMode::Share,
47            capabilities: ShareMode::Share,
48        }
49    }
50}
51
52impl IsolationPolicy {
53    /// Parses an isolation policy from a key/value-pair expression.
54    ///
55    /// Nil or the `all-shared` symbol yields the default (all axes shared);
56    /// otherwise a list or vector of `:axis mode` pairs sets each named axis.
57    pub fn from_expr(expr: &Expr) -> Result<Self> {
58        match expr {
59            Expr::Nil => Ok(Self::default()),
60            Expr::Symbol(symbol) if symbol.name.as_ref() == "all-shared" => Ok(Self::default()),
61            Expr::List(items) | Expr::Vector(items) => {
62                let mut policy = Self::default();
63                if !items.len().is_multiple_of(2) {
64                    return Err(Error::Eval(
65                        "isolation policy must use key/value pairs".to_owned(),
66                    ));
67                }
68                for pair in items.chunks(2) {
69                    let Expr::Symbol(symbol) = &pair[0] else {
70                        return Err(Error::TypeMismatch {
71                            expected: "keyword symbol",
72                            found: "non-symbol",
73                        });
74                    };
75                    let key = symbol
76                        .name
77                        .strip_prefix(':')
78                        .unwrap_or(symbol.name.as_ref());
79                    let mode = parse_share_mode(&pair[1])?;
80                    match key {
81                        "env" => policy.env = mode,
82                        "libs" => policy.libs = mode,
83                        "factory" => policy.factory = mode,
84                        "registry" => policy.registry = mode,
85                        "capabilities" => policy.capabilities = mode,
86                        other => {
87                            return Err(Error::Eval(format!(
88                                "unknown isolation policy axis :{other}"
89                            )));
90                        }
91                    }
92                }
93                Ok(policy)
94            }
95            _ => Err(Error::TypeMismatch {
96                expected: "isolation policy expression",
97                found: "non-policy",
98            }),
99        }
100    }
101
102    /// Reflects the policy as a table keyed by axis name.
103    pub fn as_value(&self, cx: &mut Cx) -> Result<Value> {
104        let env = share_mode_value(cx, &self.env)?;
105        let libs = share_mode_value(cx, &self.libs)?;
106        let factory = share_mode_value(cx, &self.factory)?;
107        let registry = share_mode_value(cx, &self.registry)?;
108        let capabilities = share_mode_value(cx, &self.capabilities)?;
109        cx.factory().table(vec![
110            (Symbol::new("env"), env),
111            (Symbol::new("libs"), libs),
112            (Symbol::new("factory"), factory),
113            (Symbol::new("registry"), registry),
114            (Symbol::new("capabilities"), capabilities),
115        ])
116    }
117
118    /// Runs `f` under a context derived per this policy's axes.
119    ///
120    /// Derives the env, capabilities, factory, and registry according to each
121    /// axis and installs them for the duration of the call.
122    pub fn apply<T>(&self, cx: &mut Cx, f: impl FnOnce(&mut Cx) -> Result<T>) -> Result<T> {
123        let env = derive_env(cx, &self.env)?;
124        let capabilities = derive_capabilities(cx, &self.capabilities)?;
125        let factory = derive_factory(cx, &self.factory);
126        let registry = derive_registry(cx, &self.registry, &self.libs)?;
127        cx.with_registry(registry, |cx| {
128            cx.with_factory(factory, |cx| {
129                cx.with_capabilities(capabilities, |cx| cx.with_env(env, f))
130            })
131        })
132    }
133}
134
135fn parse_share_mode(expr: &Expr) -> Result<ShareMode> {
136    match expr {
137        Expr::Symbol(symbol) => match symbol.name.as_ref() {
138            "share" => Ok(ShareMode::Share),
139            "child" => Ok(ShareMode::Child),
140            "isolate" => Ok(ShareMode::Isolate),
141            other => Err(Error::Eval(format!("unsupported share mode {other}"))),
142        },
143        Expr::List(items) | Expr::Vector(items) => match items.as_slice() {
144            [Expr::Symbol(head), value] if head.name.as_ref() == "import" => match value {
145                Expr::Symbol(symbol) => Ok(ShareMode::Import(symbol.clone())),
146                _ => Err(Error::TypeMismatch {
147                    expected: "import symbol",
148                    found: "non-symbol",
149                }),
150            },
151            _ => Err(Error::Eval("unsupported share mode form".to_owned())),
152        },
153        _ => Err(Error::TypeMismatch {
154            expected: "share mode",
155            found: "non-share-mode",
156        }),
157    }
158}
159
160fn share_mode_value(cx: &mut Cx, mode: &ShareMode) -> Result<Value> {
161    match mode {
162        ShareMode::Share => cx.factory().symbol(Symbol::new("share")),
163        ShareMode::Child => cx.factory().symbol(Symbol::new("child")),
164        ShareMode::Isolate => cx.factory().symbol(Symbol::new("isolate")),
165        ShareMode::Import(symbol) => cx.factory().table(vec![
166            (
167                Symbol::new("kind"),
168                cx.factory().symbol(Symbol::new("import"))?,
169            ),
170            (Symbol::new("value"), cx.factory().symbol(symbol.clone())?),
171        ]),
172    }
173}
174
175fn derive_env(cx: &mut Cx, mode: &ShareMode) -> Result<Env> {
176    match mode {
177        ShareMode::Share => Ok(cx.env().clone()),
178        ShareMode::Child => Ok(Env::child(Arc::new(cx.env().clone()))),
179        ShareMode::Isolate => Ok(Env::default()),
180        ShareMode::Import(symbol) => env_from_snapshot_expr(&snapshot_expr(cx, symbol)?, cx),
181    }
182}
183
184fn derive_capabilities(cx: &mut Cx, mode: &ShareMode) -> Result<CapabilitySet> {
185    match mode {
186        ShareMode::Share | ShareMode::Child => Ok(cx.capabilities().clone()),
187        ShareMode::Isolate => Ok(CapabilitySet::default()),
188        ShareMode::Import(symbol) => capabilities_from_snapshot_expr(&snapshot_expr(cx, symbol)?),
189    }
190}
191
192fn derive_factory(cx: &Cx, mode: &ShareMode) -> Arc<dyn Factory> {
193    match mode {
194        ShareMode::Share | ShareMode::Child => cx.factory_ref(),
195        ShareMode::Isolate | ShareMode::Import(_) => Arc::new(DefaultFactory),
196    }
197}
198
199fn derive_registry(
200    cx: &mut Cx,
201    registry_mode: &ShareMode,
202    libs_mode: &ShareMode,
203) -> Result<Registry> {
204    let base = match registry_mode {
205        ShareMode::Share | ShareMode::Child => cx.registry().clone(),
206        ShareMode::Isolate => Registry::default(),
207        ShareMode::Import(symbol) => registry_from_snapshot_expr(&snapshot_expr(cx, symbol)?, cx)?,
208    };
209    apply_libs_mode(cx, base, libs_mode)
210}
211
212fn apply_libs_mode(cx: &mut Cx, registry: Registry, mode: &ShareMode) -> Result<Registry> {
213    match mode {
214        ShareMode::Share | ShareMode::Child => Ok(registry),
215        ShareMode::Isolate => Ok(Registry::default()),
216        ShareMode::Import(symbol) => registry_from_snapshot_expr(&snapshot_expr(cx, symbol)?, cx),
217    }
218}
219
220fn snapshot_expr(cx: &mut Cx, symbol: &Symbol) -> Result<Expr> {
221    let value = cx
222        .registry()
223        .value_by_symbol(symbol)
224        .cloned()
225        .ok_or_else(|| Error::UnknownSymbol {
226            symbol: symbol.clone(),
227        })?;
228    value.object().as_expr(cx)
229}
230
231fn env_from_snapshot_expr(expr: &Expr, cx: &mut Cx) -> Result<Env> {
232    let Expr::Map(entries) = expr else {
233        return Err(Error::TypeMismatch {
234            expected: "env snapshot table",
235            found: "non-table",
236        });
237    };
238    let mut env = Env::default();
239    for (key, value) in entries {
240        let Expr::Symbol(symbol) = key else {
241            return Err(Error::TypeMismatch {
242                expected: "symbol table key",
243                found: "non-symbol",
244            });
245        };
246        env.define(symbol.clone(), expr_to_value(cx, value)?);
247    }
248    Ok(env)
249}
250
251fn capabilities_from_snapshot_expr(expr: &Expr) -> Result<CapabilitySet> {
252    let mut capabilities = CapabilitySet::new();
253    match expr {
254        Expr::Nil => {}
255        Expr::Symbol(symbol) => {
256            capabilities.insert(CapabilityName::new(symbol.to_string()));
257        }
258        Expr::String(text) => {
259            capabilities.insert(CapabilityName::new(text.clone()));
260        }
261        Expr::List(items) | Expr::Vector(items) => {
262            for item in items {
263                match item {
264                    Expr::Symbol(symbol) => {
265                        capabilities.insert(CapabilityName::new(symbol.to_string()));
266                    }
267                    Expr::String(text) => {
268                        capabilities.insert(CapabilityName::new(text.clone()));
269                    }
270                    _ => {
271                        return Err(Error::TypeMismatch {
272                            expected: "capability symbol or string",
273                            found: "non-capability",
274                        });
275                    }
276                }
277            }
278        }
279        _ => {
280            return Err(Error::TypeMismatch {
281                expected: "capability list",
282                found: "non-list",
283            });
284        }
285    }
286    Ok(capabilities)
287}
288
289fn registry_from_snapshot_expr(expr: &Expr, cx: &mut Cx) -> Result<Registry> {
290    let libs = match expr {
291        Expr::Nil => Vec::new(),
292        Expr::Symbol(symbol) => vec![symbol.clone()],
293        Expr::List(items) | Expr::Vector(items) => items
294            .iter()
295            .map(|item| match item {
296                Expr::Symbol(symbol) => Ok(symbol.clone()),
297                _ => Err(Error::TypeMismatch {
298                    expected: "library symbol",
299                    found: "non-symbol",
300                }),
301            })
302            .collect::<Result<Vec<_>>>()?,
303        _ => {
304            return Err(Error::TypeMismatch {
305                expected: "registry snapshot symbol list",
306                found: "non-list",
307            });
308        }
309    };
310    Ok(cx.registry().subset_for_libs(&libs))
311}
312
313fn expr_to_value(cx: &mut Cx, expr: &Expr) -> Result<Value> {
314    match expr {
315        Expr::Nil => cx.factory().nil(),
316        Expr::Bool(value) => cx.factory().bool(*value),
317        Expr::Number(number) => cx
318            .factory()
319            .number_literal(number.domain.clone(), number.canonical.clone()),
320        Expr::Symbol(symbol) => cx.factory().symbol(symbol.clone()),
321        Expr::String(text) => cx.factory().string(text.clone()),
322        Expr::Bytes(bytes) => cx.factory().bytes(bytes.clone()),
323        Expr::List(items) | Expr::Vector(items) => {
324            let values = items
325                .iter()
326                .map(|item| expr_to_value(cx, item))
327                .collect::<Result<Vec<_>>>()?;
328            cx.factory().list(values)
329        }
330        Expr::Map(entries) => {
331            let values = entries
332                .iter()
333                .map(|(key, value)| {
334                    let Expr::Symbol(symbol) = key else {
335                        return Err(Error::TypeMismatch {
336                            expected: "symbol table key",
337                            found: "non-symbol",
338                        });
339                    };
340                    Ok((symbol.clone(), expr_to_value(cx, value)?))
341                })
342                .collect::<Result<Vec<_>>>()?;
343            cx.factory().table(values)
344        }
345        _ => cx.factory().expr(expr.clone()),
346    }
347}
348
349#[derive(Clone)]
350pub(crate) struct IsolatedEvalSite {
351    inner: Arc<dyn EvalSite>,
352    policy: IsolationPolicy,
353}
354
355impl IsolatedEvalSite {
356    pub(crate) fn wrap(inner: Arc<dyn EvalSite>, policy: IsolationPolicy) -> Arc<dyn EvalSite> {
357        Arc::new(Self { inner, policy })
358    }
359
360    pub(crate) fn inner(&self) -> &Arc<dyn EvalSite> {
361        &self.inner
362    }
363}
364
365impl EvalSite for IsolatedEvalSite {
366    fn site_kind(&self) -> &'static str {
367        self.inner.site_kind()
368    }
369
370    fn address(&self) -> &ServerAddress {
371        self.inner.address()
372    }
373
374    fn codecs(&self) -> &[Symbol] {
375        self.inner.codecs()
376    }
377
378    fn answer(&self, cx: &mut Cx, frame: ServerFrame) -> Result<ServerFrame> {
379        self.policy.apply(cx, |cx| self.inner.answer(cx, frame))
380    }
381
382    fn stream(
383        &self,
384        cx: &mut Cx,
385        frame: ServerFrame,
386        sink: &mut dyn crate::StreamSink,
387    ) -> Result<()> {
388        self.policy
389            .apply(cx, |cx| self.inner.stream(cx, frame, sink))
390    }
391
392    fn as_eval_fabric(&self) -> Option<&dyn sim_kernel::EvalFabric> {
393        self.inner.as_eval_fabric()
394    }
395
396    fn as_any(&self) -> &dyn std::any::Any {
397        self
398    }
399}