Skip to main content

sim_kernel/eval/
protocol.rs

1use std::sync::Arc;
2use std::time::Duration;
3
4use crate::{
5    capability::CapabilityName,
6    env::Cx,
7    error::{Diagnostic, Result, Severity},
8    expr::Expr,
9    hint::{HintMetadata, diagnostic_hints_value},
10    id::{CORE_EVAL_REPLY_CLASS_ID, CORE_EVAL_REQUEST_CLASS_ID, ClassId, Symbol},
11    object::{ClassRef, Object, ShapeRef},
12    value::Value,
13};
14
15/// A stage in the pipeline at which macro expansion may run.
16///
17/// Eval policies consult the phase via
18/// [`EvalPolicy::allow_macro_expansion`](crate::EvalPolicy::allow_macro_expansion),
19/// and expanders receive it in [`MacroExpander::expand_expr`].
20#[derive(Clone, Copy, Debug, PartialEq, Eq)]
21pub enum Phase {
22    /// While reading source forms into expressions.
23    Read,
24    /// During the dedicated macro-expansion pass.
25    Expand,
26    /// While compiling expanded expressions.
27    Compile,
28    /// During evaluation itself.
29    Eval,
30}
31
32/// The macro-expander contract: rewrite an [`Expr`] in a given [`Phase`].
33///
34/// The kernel defines only the expansion hook; concrete macro systems and
35/// expansion strategies are supplied by libraries.
36pub trait MacroExpander: Send + Sync {
37    /// Rewrites `expr` for the given `phase`, returning the expanded form.
38    fn expand_expr(&self, cx: &mut Cx, phase: Phase, expr: Expr) -> Result<Expr>;
39}
40
41/// A shared, reference-counted handle to a [`MacroExpander`].
42pub type MacroExpanderRef = Arc<dyn MacroExpander>;
43
44/// Where a [`realize`](crate::realize) request may be answered from.
45#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
46pub enum Consistency {
47    /// Answer only from the local node.
48    LocalOnly,
49    /// Prefer the local node, falling back to remote (the default).
50    #[default]
51    LocalFirst,
52    /// Answer only from a remote node.
53    RemoteOnly,
54}
55
56impl Consistency {
57    /// Returns the canonical symbol naming this consistency mode.
58    pub fn as_symbol(self) -> Symbol {
59        Symbol::new(match self {
60            Self::LocalOnly => "local-only",
61            Self::LocalFirst => "local-first",
62            Self::RemoteOnly => "remote-only",
63        })
64    }
65}
66
67/// Which evaluation discipline a request runs under.
68#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
69pub enum EvalMode {
70    /// Ordinary expression evaluation (the default).
71    #[default]
72    Eval,
73    /// Relational/logic evaluation that may yield multiple answers.
74    Logic,
75}
76
77impl EvalMode {
78    /// Returns the canonical symbol naming this evaluation mode.
79    pub fn as_symbol(self) -> Symbol {
80        Symbol::new(match self {
81            Self::Eval => "eval",
82            Self::Logic => "logic",
83        })
84    }
85}
86
87// sim-non-citizen(reason = "kernel eval protocol projection; public transport descriptors live in server/Frame and agent-runner/ModelRequest", kind = "marker", descriptor = "")
88/// A request submitted to an [`EvalFabric`] for location-transparent eval.
89///
90/// This is the kernel's protocol projection of an eval request; public
91/// transport descriptors live in server and agent-runner crates. See the
92/// README section "Distributed evaluation".
93#[derive(Clone)]
94pub struct EvalRequest {
95    /// The expression to evaluate.
96    pub expr: Expr,
97    /// Optional shape the result must satisfy.
98    pub result_shape: Option<ShapeRef>,
99    /// Capabilities the evaluation requires.
100    pub required_capabilities: Vec<CapabilityName>,
101    /// Optional wall-clock deadline for the evaluation.
102    pub deadline: Option<Duration>,
103    /// Where the request may be answered from.
104    pub consistency: Consistency,
105    /// Which evaluation discipline to run under.
106    pub mode: EvalMode,
107    /// Optional cap on the number of answers (logic mode).
108    pub answer_limit: Option<usize>,
109    /// Optional buffer size for streamed events.
110    pub stream_buffer: Option<usize>,
111    /// Whether to stream intermediate events rather than only the final value.
112    pub stream: bool,
113    /// Whether to collect and return an evaluation trace.
114    pub trace: bool,
115}
116
117// sim-non-citizen(reason = "kernel eval protocol projection; public transport descriptors live in server/Frame and agent-runner/ModelResponse", kind = "marker", descriptor = "")
118/// The answer returned by an [`EvalFabric`] for an [`EvalRequest`].
119#[derive(Clone)]
120pub struct EvalReply {
121    /// The evaluated result value.
122    pub value: Value,
123    /// Diagnostics produced during evaluation.
124    pub diagnostics: Vec<Diagnostic>,
125    /// Optional evaluation trace, when [`EvalRequest::trace`] was set.
126    pub trace: Option<Value>,
127}
128
129/// The location-transparent distributed eval contract.
130///
131/// An [`EvalFabric`] answers an [`EvalRequest`] with an [`EvalReply`] without
132/// the caller knowing whether evaluation is local or remote. Server and agent
133/// code target this surface (and the [`realize`](crate::realize) helpers built
134/// on it) instead of transport-specific APIs; libraries supply the concrete
135/// transports. See the README section "Distributed evaluation".
136pub trait EvalFabric: Send + Sync {
137    /// Evaluates `request` and returns its reply.
138    fn realize(&self, cx: &mut Cx, request: EvalRequest) -> Result<EvalReply>;
139}
140
141/// A shared, reference-counted handle to an [`EvalFabric`].
142pub type EvalFabricRef = Arc<dyn EvalFabric>;
143
144impl Object for EvalRequest {
145    fn display(&self, _cx: &mut Cx) -> Result<String> {
146        Ok("#<EvalRequest>".to_owned())
147    }
148
149    fn as_any(&self) -> &dyn std::any::Any {
150        self
151    }
152}
153
154impl crate::ObjectCompat for EvalRequest {
155    fn class(&self, cx: &mut Cx) -> Result<ClassRef> {
156        runtime_class(
157            cx,
158            CORE_EVAL_REQUEST_CLASS_ID,
159            Symbol::qualified("core", "EvalRequest"),
160        )
161    }
162    fn as_expr(&self, cx: &mut Cx) -> Result<Expr> {
163        self.as_table(cx)?.object().as_expr(cx)
164    }
165    fn as_table(&self, cx: &mut Cx) -> Result<Value> {
166        let result_shape = match &self.result_shape {
167            Some(shape) => shape.clone(),
168            None => cx.factory().nil()?,
169        };
170        let deadline = match self.deadline {
171            Some(deadline) => cx.factory().string(format_duration(deadline))?,
172            None => cx.factory().nil()?,
173        };
174        let required_capabilities = cx.factory().list(
175            self.required_capabilities
176                .iter()
177                .map(|capability| cx.factory().string(capability.as_str().to_owned()))
178                .collect::<Result<Vec<_>>>()?,
179        )?;
180        cx.factory().table(vec![
181            (Symbol::new("expr"), cx.factory().expr(self.expr.clone())?),
182            (Symbol::new("result-shape"), result_shape),
183            (Symbol::new("requires"), required_capabilities),
184            (Symbol::new("deadline"), deadline),
185            (
186                Symbol::new("consistency"),
187                cx.factory().symbol(self.consistency.as_symbol())?,
188            ),
189            (
190                Symbol::new("mode"),
191                cx.factory().symbol(self.mode.as_symbol())?,
192            ),
193            (
194                Symbol::new("answer-limit"),
195                match self.answer_limit {
196                    Some(limit) => cx.factory().string(limit.to_string())?,
197                    None => cx.factory().nil()?,
198                },
199            ),
200            (
201                Symbol::new("stream-buffer"),
202                match self.stream_buffer {
203                    Some(limit) => cx.factory().string(limit.to_string())?,
204                    None => cx.factory().nil()?,
205                },
206            ),
207            (Symbol::new("stream"), cx.factory().bool(self.stream)?),
208            (Symbol::new("trace"), cx.factory().bool(self.trace)?),
209        ])
210    }
211}
212
213impl Object for EvalReply {
214    fn display(&self, _cx: &mut Cx) -> Result<String> {
215        Ok("#<EvalReply>".to_owned())
216    }
217
218    fn as_any(&self) -> &dyn std::any::Any {
219        self
220    }
221}
222
223impl crate::ObjectCompat for EvalReply {
224    fn class(&self, cx: &mut Cx) -> Result<ClassRef> {
225        runtime_class(
226            cx,
227            CORE_EVAL_REPLY_CLASS_ID,
228            Symbol::qualified("core", "EvalReply"),
229        )
230    }
231    fn as_expr(&self, cx: &mut Cx) -> Result<Expr> {
232        self.as_table(cx)?.object().as_expr(cx)
233    }
234    fn as_table(&self, cx: &mut Cx) -> Result<Value> {
235        let trace = match &self.trace {
236            Some(trace) => trace.clone(),
237            None => cx.factory().nil()?,
238        };
239        let diagnostic_values = self
240            .diagnostics
241            .iter()
242            .map(|diagnostic| diagnostic_value(cx, diagnostic))
243            .collect::<Result<Vec<_>>>()?;
244        let diagnostics = cx.factory().list(diagnostic_values)?;
245        cx.factory().table(vec![
246            (Symbol::new("value"), self.value.clone()),
247            (Symbol::new("diagnostics"), diagnostics),
248            (Symbol::new("trace"), trace),
249        ])
250    }
251}
252
253fn runtime_class(cx: &mut Cx, id: ClassId, symbol: Symbol) -> Result<ClassRef> {
254    if let Some(value) = cx.registry().class_by_symbol(&symbol) {
255        return Ok(value.clone());
256    }
257    cx.factory().class_stub(id, symbol)
258}
259
260fn format_duration(duration: Duration) -> String {
261    if duration.subsec_nanos() == 0 && duration.as_secs() > 0 {
262        format!("{}s", duration.as_secs())
263    } else {
264        format!("{}ms", duration.as_millis())
265    }
266}
267
268fn diagnostic_value(cx: &mut Cx, diagnostic: &Diagnostic) -> Result<Value> {
269    let hints = diagnostic_hints_value(cx, diagnostic)?;
270    let severity = cx.factory().symbol(Symbol::new(match diagnostic.severity {
271        Severity::Error => "error",
272        Severity::Warning => "warning",
273        Severity::Info => "info",
274        Severity::Note => "note",
275    }))?;
276    let source = match &diagnostic.source {
277        Some(source) => cx.factory().string(source.0.to_string())?,
278        None => cx.factory().nil()?,
279    };
280    let span = match &diagnostic.span {
281        Some(span) => cx.factory().table(vec![
282            (
283                Symbol::new("start"),
284                cx.factory().string(span.start.to_string())?,
285            ),
286            (
287                Symbol::new("end"),
288                cx.factory().string(span.end.to_string())?,
289            ),
290        ])?,
291        None => cx.factory().nil()?,
292    };
293    let code = match &diagnostic.code {
294        Some(code) => cx.factory().symbol(code.clone())?,
295        None => cx.factory().nil()?,
296    };
297    let related_values = diagnostic
298        .related
299        .iter()
300        .filter(|related| !HintMetadata::is_hint_diagnostic(related))
301        .map(|related| diagnostic_value(cx, related))
302        .collect::<Result<Vec<_>>>()?;
303    let related = cx.factory().list(related_values)?;
304    cx.factory().table(vec![
305        (Symbol::new("severity"), severity),
306        (
307            Symbol::new("message"),
308            cx.factory().string(diagnostic.message.clone())?,
309        ),
310        (Symbol::new("source"), source),
311        (Symbol::new("span"), span),
312        (Symbol::new("code"), code),
313        (Symbol::new("related"), related),
314        (Symbol::new("hints"), hints),
315    ])
316}