Skip to main content

sim_lib_server/frame/
eval.rs

1use std::time::Duration;
2
3use sim_citizen::value_from_expr;
4use sim_kernel::{
5    CapabilityName, Consistency, Cx, Diagnostic, Error, EvalMode, EvalReply, EvalRequest, Expr,
6    ObjectCompat, ReadPolicy, Result, Severity, Symbol, Value,
7};
8use sim_value::capability_names_from_expr;
9
10use crate::helpers::parse_optional_duration;
11use crate::{FrameKind, ServerFrame};
12
13/// Encodes an [`EvalRequest`] into a request [`ServerFrame`] under `codec`.
14///
15/// Carries the request's consistency, required capabilities, trace flag, and
16/// deadline onto the frame envelope.
17pub fn server_frame_from_request(
18    cx: &mut Cx,
19    codec: &Symbol,
20    request: EvalRequest,
21) -> Result<ServerFrame> {
22    let expr = request.as_expr(cx)?;
23    let mut frame = ServerFrame::from_expr(
24        cx,
25        codec.clone(),
26        FrameKind::Request,
27        &expr,
28        request.consistency,
29        request.required_capabilities.clone(),
30        request.trace,
31    )?;
32    frame.envelope.deadline = request.deadline;
33    Ok(frame)
34}
35
36/// Encodes an [`EvalReply`] into a response [`ServerFrame`] under `codec`.
37///
38/// Applies the given `consistency` and sets the envelope trace flag from the
39/// reply's trace value.
40pub fn server_frame_from_reply(
41    cx: &mut Cx,
42    codec: &Symbol,
43    reply: EvalReply,
44    consistency: Consistency,
45) -> Result<ServerFrame> {
46    let expr = reply.as_expr(cx)?;
47    let mut frame = ServerFrame::from_expr(
48        cx,
49        codec.clone(),
50        FrameKind::Response,
51        &expr,
52        consistency,
53        Vec::new(),
54        reply.trace.is_some(),
55    )?;
56    if let Some(trace) = reply.trace {
57        frame.envelope.trace = !matches!(trace.object().as_expr(cx)?, Expr::Nil);
58    }
59    Ok(frame)
60}
61
62/// Decodes a request [`ServerFrame`] back into an [`EvalRequest`].
63///
64/// Returns an error when the frame is not a request frame.
65pub fn eval_request_from_frame(cx: &mut Cx, frame: &ServerFrame) -> Result<EvalRequest> {
66    if frame.kind != FrameKind::Request {
67        return Err(Error::Eval(format!(
68            "expected request frame, found {}",
69            frame.kind.as_symbol()
70        )));
71    }
72    let expr = frame.decode_expr(cx, ReadPolicy::default())?;
73    eval_request_from_expr(cx, expr)
74}
75
76/// Decodes a response [`ServerFrame`] back into an [`EvalReply`].
77///
78/// Returns an error when the frame is not a response frame.
79pub fn eval_reply_from_frame(cx: &mut Cx, frame: &ServerFrame) -> Result<EvalReply> {
80    if frame.kind == FrameKind::Error {
81        let detail = match frame.decode_expr(cx, ReadPolicy::default())? {
82            Expr::String(detail) => detail,
83            detail => format!("{detail:?}"),
84        };
85        return Err(Error::Eval(format!("remote evaluation failed: {detail}")));
86    }
87    if frame.kind != FrameKind::Response {
88        return Err(Error::Eval(format!(
89            "expected response frame, found {}",
90            frame.kind.as_symbol()
91        )));
92    }
93    let expr = frame.decode_expr(cx, ReadPolicy::default())?;
94    eval_reply_from_expr(cx, expr)
95}
96
97fn eval_request_from_expr(cx: &mut Cx, expr: Expr) -> Result<EvalRequest> {
98    let request_expr = required_table_field(&expr, "expr")?.clone();
99    let result_shape = parse_result_shape_expr(cx, required_table_field(&expr, "result-shape")?)?;
100    let required_capabilities = parse_capability_expr(required_table_field(&expr, "requires")?)?;
101    let deadline = parse_deadline_expr(required_table_field(&expr, "deadline")?)?;
102    let consistency = parse_consistency_expr(required_table_field(&expr, "consistency")?)?;
103    let mode = optional_table_value(&expr, "mode")
104        .map(parse_mode_expr)
105        .transpose()?
106        .unwrap_or(EvalMode::Eval);
107    let answer_limit = optional_table_value(&expr, "answer-limit")
108        .map(parse_optional_usize_expr)
109        .transpose()?
110        .flatten();
111    let stream_buffer = optional_table_value(&expr, "stream-buffer")
112        .map(parse_optional_usize_expr)
113        .transpose()?
114        .flatten();
115    let stream = optional_table_value(&expr, "stream")
116        .map(parse_bool_expr)
117        .transpose()?
118        .unwrap_or(false);
119    let trace = parse_bool_expr(required_table_field(&expr, "trace")?)?;
120    Ok(EvalRequest {
121        expr: request_expr,
122        result_shape,
123        required_capabilities,
124        deadline,
125        consistency,
126        mode,
127        answer_limit,
128        stream_buffer,
129        stream,
130        trace,
131    })
132}
133
134fn eval_reply_from_expr(cx: &mut Cx, expr: Expr) -> Result<EvalReply> {
135    let value = value_from_expr(cx, required_table_field(&expr, "value")?)?;
136    let diagnostics = parse_diagnostics_expr(required_table_field(&expr, "diagnostics")?)?;
137    let trace = parse_optional_value_expr(cx, required_table_field(&expr, "trace")?)?;
138    Ok(EvalReply {
139        value,
140        diagnostics,
141        trace,
142    })
143}
144
145fn required_table_field<'a>(expr: &'a Expr, key: &str) -> Result<&'a Expr> {
146    let Expr::Map(entries) = expr else {
147        return Err(Error::TypeMismatch {
148            expected: "table expression",
149            found: "non-table",
150        });
151    };
152    entries
153        .iter()
154        .find_map(|(entry_key, entry_value)| match entry_key {
155            Expr::Symbol(symbol) if symbol.name.as_ref() == key => Some(entry_value),
156            _ => None,
157        })
158        .ok_or_else(|| Error::Eval(format!("missing frame field {key}")))
159}
160
161fn optional_table_value<'a>(expr: &'a Expr, key: &str) -> Option<&'a Expr> {
162    let Expr::Map(entries) = expr else {
163        return None;
164    };
165    entries
166        .iter()
167        .find_map(|(entry_key, entry_value)| match entry_key {
168            Expr::Symbol(symbol) if symbol.name.as_ref() == key => Some(entry_value),
169            _ => None,
170        })
171}
172
173fn parse_result_shape_expr(cx: &mut Cx, expr: &Expr) -> Result<Option<sim_kernel::ShapeRef>> {
174    if matches!(expr, Expr::Nil) {
175        return Ok(None);
176    }
177    if let Expr::Symbol(symbol) = expr {
178        if let Ok(shape) = cx.resolve_shape(symbol) {
179            return Ok(Some(shape));
180        }
181        if symbol.name.as_ref() == "instance-shape"
182            && let Some(namespace) = &symbol.namespace
183        {
184            let class_symbol = parse_qualified_symbol(namespace);
185            if let Ok(class_value) = cx.resolve_class(&class_symbol)
186                && let Some(class) = class_value.object().as_class()
187            {
188                return Ok(Some(class.instance_shape(cx)?));
189            }
190        }
191    }
192    let value = cx.eval_expr(expr.clone())?;
193    if let Some(class) = value.object().as_class() {
194        return Ok(Some(class.instance_shape(cx)?));
195    }
196    Err(Error::TypeMismatch {
197        expected: "shape or class",
198        found: "non-shape",
199    })
200}
201
202fn parse_qualified_symbol(text: &str) -> Symbol {
203    match text.rsplit_once('/') {
204        Some((namespace, name)) => Symbol::qualified(namespace.to_owned(), name.to_owned()),
205        None => Symbol::new(text.to_owned()),
206    }
207}
208
209fn parse_capability_expr(expr: &Expr) -> Result<Vec<CapabilityName>> {
210    capability_names_from_expr(expr)
211}
212
213fn parse_deadline_expr(expr: &Expr) -> Result<Option<Duration>> {
214    parse_optional_duration(expr)
215}
216
217fn parse_consistency_expr(expr: &Expr) -> Result<Consistency> {
218    let name = match expr {
219        Expr::Symbol(symbol) => symbol.to_string(),
220        Expr::String(text) => text.clone(),
221        _ => {
222            return Err(Error::TypeMismatch {
223                expected: "consistency symbol or string",
224                found: "non-consistency",
225            });
226        }
227    };
228    match name.as_str() {
229        "local-only" => Ok(Consistency::LocalOnly),
230        "local-first" => Ok(Consistency::LocalFirst),
231        "remote-only" => Ok(Consistency::RemoteOnly),
232        _ => Err(Error::Eval(format!(
233            "unsupported realize consistency {name}"
234        ))),
235    }
236}
237
238fn parse_mode_expr(expr: &Expr) -> Result<EvalMode> {
239    let name = match expr {
240        Expr::Symbol(symbol) => symbol.to_string(),
241        Expr::String(text) => text.clone(),
242        _ => {
243            return Err(Error::TypeMismatch {
244                expected: "mode symbol or string",
245                found: "non-mode",
246            });
247        }
248    };
249    match name.as_str() {
250        "eval" => Ok(EvalMode::Eval),
251        "logic" => Ok(EvalMode::Logic),
252        _ => Err(Error::Eval(format!("unsupported realize mode {name}"))),
253    }
254}
255
256fn parse_optional_usize_expr(expr: &Expr) -> Result<Option<usize>> {
257    match expr {
258        Expr::Nil => Ok(None),
259        Expr::Number(number) => number
260            .canonical
261            .parse::<usize>()
262            .map(Some)
263            .map_err(|_| Error::Eval(format!("expected usize, found {}", number.canonical))),
264        Expr::String(text) => text
265            .parse::<usize>()
266            .map(Some)
267            .map_err(|_| Error::Eval(format!("expected usize, found {text}"))),
268        _ => Err(Error::TypeMismatch {
269            expected: "usize or nil",
270            found: "non-usize",
271        }),
272    }
273}
274
275fn parse_bool_expr(expr: &Expr) -> Result<bool> {
276    match expr {
277        Expr::Bool(value) => Ok(*value),
278        _ => Err(Error::TypeMismatch {
279            expected: "bool",
280            found: "non-bool",
281        }),
282    }
283}
284
285fn parse_diagnostics_expr(expr: &Expr) -> Result<Vec<Diagnostic>> {
286    match expr {
287        Expr::Nil => Ok(Vec::new()),
288        Expr::List(items) | Expr::Vector(items) => {
289            items.iter().map(parse_diagnostic_expr).collect()
290        }
291        _ => Err(Error::TypeMismatch {
292            expected: "diagnostic list",
293            found: "non-list",
294        }),
295    }
296}
297
298fn parse_diagnostic_expr(expr: &Expr) -> Result<Diagnostic> {
299    let severity = match required_table_field(expr, "severity")? {
300        Expr::Symbol(symbol) if symbol.name.as_ref() == "error" => Severity::Error,
301        Expr::Symbol(symbol) if symbol.name.as_ref() == "warning" => Severity::Warning,
302        Expr::Symbol(symbol) if symbol.name.as_ref() == "info" => Severity::Info,
303        Expr::Symbol(symbol) if symbol.name.as_ref() == "note" => Severity::Note,
304        _ => {
305            return Err(Error::TypeMismatch {
306                expected: "diagnostic severity symbol",
307                found: "non-severity",
308            });
309        }
310    };
311    let message = match required_table_field(expr, "message")? {
312        Expr::String(text) => text.clone(),
313        _ => {
314            return Err(Error::TypeMismatch {
315                expected: "diagnostic message string",
316                found: "non-string",
317            });
318        }
319    };
320    let code = match required_table_field(expr, "code")? {
321        Expr::Nil => None,
322        Expr::Symbol(symbol) => Some(symbol.clone()),
323        _ => {
324            return Err(Error::TypeMismatch {
325                expected: "diagnostic code symbol",
326                found: "non-symbol",
327            });
328        }
329    };
330    let related = parse_diagnostics_expr(required_table_field(expr, "related")?)?;
331    Ok(Diagnostic {
332        severity,
333        message,
334        source: None,
335        span: None,
336        code,
337        related,
338    })
339}
340
341fn parse_optional_value_expr(cx: &mut Cx, expr: &Expr) -> Result<Option<Value>> {
342    if matches!(expr, Expr::Nil) {
343        return Ok(None);
344    }
345    value_from_expr(cx, expr).map(Some)
346}
347
348#[cfg(test)]
349mod tests {
350    use super::*;
351
352    #[test]
353    fn remote_error_frames_preserve_the_server_diagnostic() {
354        let mut cx = crate::tests::cx();
355        let frame = ServerFrame::from_expr(
356            &mut cx,
357            Symbol::qualified("codec", "binary"),
358            FrameKind::Error,
359            &Expr::String("unknown symbol shared".to_owned()),
360            Consistency::RemoteOnly,
361            Vec::new(),
362            false,
363        )
364        .unwrap();
365
366        assert!(matches!(
367            eval_reply_from_frame(&mut cx, &frame),
368            Err(Error::Eval(message))
369                if message == "remote evaluation failed: unknown symbol shared"
370        ));
371    }
372}