Skip to main content

sim_lib_server/
isolation.rs

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