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::Response {
81        return Err(Error::Eval(format!(
82            "expected response frame, found {}",
83            frame.kind.as_symbol()
84        )));
85    }
86    let expr = frame.decode_expr(cx, ReadPolicy::default())?;
87    eval_reply_from_expr(cx, expr)
88}
89
90fn eval_request_from_expr(cx: &mut Cx, expr: Expr) -> Result<EvalRequest> {
91    let request_expr = required_table_field(&expr, "expr")?.clone();
92    let result_shape = parse_result_shape_expr(cx, required_table_field(&expr, "result-shape")?)?;
93    let required_capabilities = parse_capability_expr(required_table_field(&expr, "requires")?)?;
94    let deadline = parse_deadline_expr(required_table_field(&expr, "deadline")?)?;
95    let consistency = parse_consistency_expr(required_table_field(&expr, "consistency")?)?;
96    let mode = optional_table_field(&expr, "mode")
97        .map(parse_mode_expr)
98        .transpose()?
99        .unwrap_or(EvalMode::Eval);
100    let answer_limit = optional_table_field(&expr, "answer-limit")
101        .map(parse_optional_usize_expr)
102        .transpose()?
103        .flatten();
104    let stream_buffer = optional_table_field(&expr, "stream-buffer")
105        .map(parse_optional_usize_expr)
106        .transpose()?
107        .flatten();
108    let stream = optional_table_field(&expr, "stream")
109        .map(parse_bool_expr)
110        .transpose()?
111        .unwrap_or(false);
112    let trace = parse_bool_expr(required_table_field(&expr, "trace")?)?;
113    Ok(EvalRequest {
114        expr: request_expr,
115        result_shape,
116        required_capabilities,
117        deadline,
118        consistency,
119        mode,
120        answer_limit,
121        stream_buffer,
122        stream,
123        trace,
124    })
125}
126
127fn eval_reply_from_expr(cx: &mut Cx, expr: Expr) -> Result<EvalReply> {
128    let value = value_from_expr(cx, required_table_field(&expr, "value")?)?;
129    let diagnostics = parse_diagnostics_expr(required_table_field(&expr, "diagnostics")?)?;
130    let trace = parse_optional_value_expr(cx, required_table_field(&expr, "trace")?)?;
131    Ok(EvalReply {
132        value,
133        diagnostics,
134        trace,
135    })
136}
137
138fn required_table_field<'a>(expr: &'a Expr, key: &str) -> Result<&'a Expr> {
139    let Expr::Map(entries) = expr else {
140        return Err(Error::TypeMismatch {
141            expected: "table expression",
142            found: "non-table",
143        });
144    };
145    entries
146        .iter()
147        .find_map(|(entry_key, entry_value)| match entry_key {
148            Expr::Symbol(symbol) if symbol.name.as_ref() == key => Some(entry_value),
149            _ => None,
150        })
151        .ok_or_else(|| Error::Eval(format!("missing frame field {key}")))
152}
153
154fn optional_table_field<'a>(expr: &'a Expr, key: &str) -> Option<&'a Expr> {
155    let Expr::Map(entries) = expr else {
156        return None;
157    };
158    entries
159        .iter()
160        .find_map(|(entry_key, entry_value)| match entry_key {
161            Expr::Symbol(symbol) if symbol.name.as_ref() == key => Some(entry_value),
162            _ => None,
163        })
164}
165
166fn parse_result_shape_expr(cx: &mut Cx, expr: &Expr) -> Result<Option<sim_kernel::ShapeRef>> {
167    if matches!(expr, Expr::Nil) {
168        return Ok(None);
169    }
170    if let Expr::Symbol(symbol) = expr {
171        if let Ok(shape) = cx.resolve_shape(symbol) {
172            return Ok(Some(shape));
173        }
174        if symbol.name.as_ref() == "instance-shape"
175            && let Some(namespace) = &symbol.namespace
176        {
177            let class_symbol = parse_qualified_symbol(namespace);
178            if let Ok(class_value) = cx.resolve_class(&class_symbol)
179                && let Some(class) = class_value.object().as_class()
180            {
181                return Ok(Some(class.instance_shape(cx)?));
182            }
183        }
184    }
185    let value = cx.eval_expr(expr.clone())?;
186    if let Some(class) = value.object().as_class() {
187        return Ok(Some(class.instance_shape(cx)?));
188    }
189    Err(Error::TypeMismatch {
190        expected: "shape or class",
191        found: "non-shape",
192    })
193}
194
195fn parse_qualified_symbol(text: &str) -> Symbol {
196    match text.rsplit_once('/') {
197        Some((namespace, name)) => Symbol::qualified(namespace.to_owned(), name.to_owned()),
198        None => Symbol::new(text.to_owned()),
199    }
200}
201
202fn parse_capability_expr(expr: &Expr) -> Result<Vec<CapabilityName>> {
203    capability_names_from_expr(expr)
204}
205
206fn parse_deadline_expr(expr: &Expr) -> Result<Option<Duration>> {
207    parse_optional_duration(expr)
208}
209
210fn parse_consistency_expr(expr: &Expr) -> Result<Consistency> {
211    let name = match expr {
212        Expr::Symbol(symbol) => symbol.to_string(),
213        Expr::String(text) => text.clone(),
214        _ => {
215            return Err(Error::TypeMismatch {
216                expected: "consistency symbol or string",
217                found: "non-consistency",
218            });
219        }
220    };
221    match name.as_str() {
222        "local-only" => Ok(Consistency::LocalOnly),
223        "local-first" => Ok(Consistency::LocalFirst),
224        "remote-only" => Ok(Consistency::RemoteOnly),
225        _ => Err(Error::Eval(format!(
226            "unsupported realize consistency {name}"
227        ))),
228    }
229}
230
231fn parse_mode_expr(expr: &Expr) -> Result<EvalMode> {
232    let name = match expr {
233        Expr::Symbol(symbol) => symbol.to_string(),
234        Expr::String(text) => text.clone(),
235        _ => {
236            return Err(Error::TypeMismatch {
237                expected: "mode symbol or string",
238                found: "non-mode",
239            });
240        }
241    };
242    match name.as_str() {
243        "eval" => Ok(EvalMode::Eval),
244        "logic" => Ok(EvalMode::Logic),
245        _ => Err(Error::Eval(format!("unsupported realize mode {name}"))),
246    }
247}
248
249fn parse_optional_usize_expr(expr: &Expr) -> Result<Option<usize>> {
250    match expr {
251        Expr::Nil => Ok(None),
252        Expr::Number(number) => number
253            .canonical
254            .parse::<usize>()
255            .map(Some)
256            .map_err(|_| Error::Eval(format!("expected usize, found {}", number.canonical))),
257        Expr::String(text) => text
258            .parse::<usize>()
259            .map(Some)
260            .map_err(|_| Error::Eval(format!("expected usize, found {text}"))),
261        _ => Err(Error::TypeMismatch {
262            expected: "usize or nil",
263            found: "non-usize",
264        }),
265    }
266}
267
268fn parse_bool_expr(expr: &Expr) -> Result<bool> {
269    match expr {
270        Expr::Bool(value) => Ok(*value),
271        _ => Err(Error::TypeMismatch {
272            expected: "bool",
273            found: "non-bool",
274        }),
275    }
276}
277
278fn parse_diagnostics_expr(expr: &Expr) -> Result<Vec<Diagnostic>> {
279    match expr {
280        Expr::Nil => Ok(Vec::new()),
281        Expr::List(items) | Expr::Vector(items) => {
282            items.iter().map(parse_diagnostic_expr).collect()
283        }
284        _ => Err(Error::TypeMismatch {
285            expected: "diagnostic list",
286            found: "non-list",
287        }),
288    }
289}
290
291fn parse_diagnostic_expr(expr: &Expr) -> Result<Diagnostic> {
292    let severity = match required_table_field(expr, "severity")? {
293        Expr::Symbol(symbol) if symbol.name.as_ref() == "error" => Severity::Error,
294        Expr::Symbol(symbol) if symbol.name.as_ref() == "warning" => Severity::Warning,
295        Expr::Symbol(symbol) if symbol.name.as_ref() == "info" => Severity::Info,
296        Expr::Symbol(symbol) if symbol.name.as_ref() == "note" => Severity::Note,
297        _ => {
298            return Err(Error::TypeMismatch {
299                expected: "diagnostic severity symbol",
300                found: "non-severity",
301            });
302        }
303    };
304    let message = match required_table_field(expr, "message")? {
305        Expr::String(text) => text.clone(),
306        _ => {
307            return Err(Error::TypeMismatch {
308                expected: "diagnostic message string",
309                found: "non-string",
310            });
311        }
312    };
313    let code = match required_table_field(expr, "code")? {
314        Expr::Nil => None,
315        Expr::Symbol(symbol) => Some(symbol.clone()),
316        _ => {
317            return Err(Error::TypeMismatch {
318                expected: "diagnostic code symbol",
319                found: "non-symbol",
320            });
321        }
322    };
323    let related = parse_diagnostics_expr(required_table_field(expr, "related")?)?;
324    Ok(Diagnostic {
325        severity,
326        message,
327        source: None,
328        span: None,
329        code,
330        related,
331    })
332}
333
334fn parse_optional_value_expr(cx: &mut Cx, expr: &Expr) -> Result<Option<Value>> {
335    if matches!(expr, Expr::Nil) {
336        return Ok(None);
337    }
338    value_from_expr(cx, expr).map(Some)
339}