Skip to main content

nifty_interp/machine/
mod.rs

1//! Evaluates checked expressions and statements against the app's state.
2
3mod carry;
4mod place;
5mod stmt;
6mod walk;
7
8use std::cell::{Cell, RefCell};
9use std::fmt::{self, Write as _};
10use std::mem;
11use std::rc::Rc;
12
13use nifty_check::hir::{
14    BinaryOp, Dialog, DialogKind, Download, DownloadCommand, DownloadField, Expr, ExprKind, FnId,
15    LocalId, Program, StateSet,
16};
17use nifty_download::{DownloadId, Report, Written, follow_id};
18use nifty_runtime::{Failure, FileDialog, MAX_CALL_DEPTH, Picked, WindowRequest};
19use nifty_syntax::Span;
20
21use crate::value::Value;
22pub(crate) use place::Location;
23pub(crate) use stmt::Flow;
24pub(crate) use walk::rounds;
25
26/// A failure while the app runs, such as dividing by zero, pointing at the code responsible.
27#[derive(Clone, Debug, PartialEq, Eq)]
28pub struct RuntimeError {
29    pub message: String,
30    pub span: Span,
31}
32
33impl RuntimeError {
34    fn new(failure: &Failure, span: Span) -> Self {
35        Self {
36            message: failure.to_string(),
37            span,
38        }
39    }
40}
41
42impl fmt::Display for RuntimeError {
43    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
44        f.write_str(&self.message)
45    }
46}
47
48pub(crate) type Result<T> = std::result::Result<T, RuntimeError>;
49
50/// Where a download's reports go: the state each of its bindings bound.
51#[derive(Debug)]
52pub(crate) struct Targets {
53    pub id: DownloadId,
54    pub bindings: Vec<(DownloadField, Location)>,
55    /// The `download(…)` call, which errors writing the reports point at.
56    pub span: Span,
57}
58
59pub(crate) type Request = nifty_download::Request<Targets>;
60
61/// Where a file dialog's answer goes: the state its call bound.
62#[derive(Debug)]
63pub(crate) struct Answer {
64    pub place: Location,
65    pub kind: DialogKind,
66    /// The `pick_…(…)` call, which errors writing the answer point at.
67    pub span: Span,
68}
69
70/// A local variable's storage. A `for` variable over a place holds where its element lives,
71/// so writes through it reach the original list.
72#[derive(Clone, Debug)]
73pub(crate) enum Slot {
74    Empty,
75    Value(Value),
76    Alias(Location),
77}
78
79pub(crate) type Frame = Vec<Slot>;
80
81pub(crate) fn new_frame(locals: usize) -> Frame {
82    vec![Slot::Empty; locals]
83}
84
85pub(crate) struct Machine {
86    program: Rc<Program>,
87    states: RefCell<Vec<Value>>,
88    /// States changed since the UI last caught up.
89    changed: RefCell<StateSet>,
90    /// Downloads asked for since the app last started them.
91    downloads: RefCell<Vec<Request>>,
92    /// What code asked of running downloads since the app last passed it on, by their ids.
93    download_commands: RefCell<Vec<(DownloadCommand, String)>>,
94    /// File dialogs asked for since the app last passed them on.
95    dialogs: RefCell<Vec<(FileDialog, Answer)>>,
96    /// Windows asked to open or close since the app last passed them on.
97    window_requests: RefCell<Vec<WindowRequest>>,
98    call_depth: Cell<usize>,
99}
100
101impl Machine {
102    /// Sets every state to its starting value, or to its value in `previous` when it still
103    /// fits there.
104    pub(crate) fn new(program: Rc<Program>, previous: Option<&Machine>) -> Result<Self> {
105        let machine = Self {
106            program,
107            states: RefCell::new(Vec::new()),
108            changed: RefCell::new(StateSet::default()),
109            downloads: RefCell::new(Vec::new()),
110            download_commands: RefCell::new(Vec::new()),
111            dialogs: RefCell::new(Vec::new()),
112            window_requests: RefCell::new(Vec::new()),
113            call_depth: Cell::new(0),
114        };
115        let program = Rc::clone(&machine.program);
116        for state in &program.states {
117            let kept = previous.and_then(|previous| carry::kept_value(previous, state, &program));
118            let value = match kept {
119                Some(value) => value,
120                None => machine.eval(&state.init, &mut Frame::new())?,
121            };
122            machine.states.borrow_mut().push(value);
123        }
124        Ok(machine)
125    }
126
127    pub(crate) fn program(&self) -> &Rc<Program> {
128        &self.program
129    }
130
131    #[cfg(test)]
132    pub(crate) fn state(&self, state: nifty_check::hir::StateId) -> Value {
133        self.states.borrow()[state.0].clone()
134    }
135
136    /// The states changed since the last call.
137    pub(crate) fn take_changed(&self) -> StateSet {
138        mem::take(&mut self.changed.borrow_mut())
139    }
140
141    /// The downloads asked for since the last call.
142    pub(crate) fn take_downloads(&self) -> Vec<Request> {
143        mem::take(&mut self.downloads.borrow_mut())
144    }
145
146    /// What code asked of running downloads since the last call, in order.
147    pub(crate) fn take_download_commands(&self) -> Vec<(DownloadCommand, String)> {
148        mem::take(&mut self.download_commands.borrow_mut())
149    }
150
151    /// The file dialogs asked for since the last call.
152    pub(crate) fn take_dialogs(&self) -> Vec<(FileDialog, Answer)> {
153        mem::take(&mut self.dialogs.borrow_mut())
154    }
155
156    /// The windows asked to open or close since the last call, in order.
157    pub(crate) fn take_window_requests(&self) -> Vec<WindowRequest> {
158        mem::take(&mut self.window_requests.borrow_mut())
159    }
160
161    /// Writes the path or paths the user picked into the state the dialog's call bound;
162    /// cancelling changes nothing.
163    pub(crate) fn answer_dialog(&self, answer: &Answer, picked: Picked) -> Result<()> {
164        let paths = match picked {
165            Picked::Chosen(paths) => paths,
166            Picked::Cancelled => return Ok(()),
167            Picked::Failed(reason) => {
168                return Err(RuntimeError::new(&Failure::Dialog(reason), answer.span));
169            }
170        };
171        let value = if answer.kind.picks_many() {
172            Value::strings(paths)
173        } else {
174            let path = paths
175                .into_iter()
176                .next()
177                .expect("backends answer a one-path dialog with exactly one path");
178            Value::String(Rc::from(path))
179        };
180        self.write_state(&answer.place, value, answer.span)
181    }
182
183    /// Asks for a file dialog, shown once the current event finishes.
184    fn ask_dialog(&self, dialog: &Dialog, frame: &mut Frame, span: Span) -> Result<()> {
185        let title = self.eval(&dialog.title, frame)?.as_str().to_owned();
186        let types = self.eval(&dialog.types, frame)?;
187        let folder = self.eval(&dialog.folder, frame)?;
188        let name = self.eval(&dialog.name, frame)?.as_str().to_owned();
189        let place = self.locate(&dialog.answer, frame)?;
190        if !place.is_in_state() {
191            return Err(RuntimeError::new(&Failure::DialogIntoVariable, span));
192        }
193        let file_dialog =
194            FileDialog::new(dialog.kind, title, &types.as_strs(), folder.as_str(), name)
195                .map_err(|error| RuntimeError::new(&Failure::Dialog(error.to_string()), span))?;
196        self.dialogs.borrow_mut().push((
197            file_dialog,
198            Answer {
199                place,
200                kind: dialog.kind,
201                span,
202            },
203        ));
204        Ok(())
205    }
206
207    /// Writes a download's report into the state its call bound.
208    pub(crate) fn report_download(&self, targets: &Targets, report: &Report) -> Result<()> {
209        for (field, written) in report.writes() {
210            let bound = targets.bindings.iter().find(|(bound, _)| *bound == field);
211            if let Some((_, location)) = bound {
212                self.write_state(location, written_value(written), targets.span)?;
213            }
214        }
215        Ok(())
216    }
217
218    /// Moves a download's targets to where the item holding its id is now; `false` when
219    /// nothing holds it any more.
220    pub(crate) fn follow_download(&self, targets: &mut Targets) -> bool {
221        follow_id(self, &mut targets.bindings, &targets.id.to_string())
222    }
223
224    /// Queues a download for the app to start once the current event finishes, and shows
225    /// that it has begun.
226    fn request_download(&self, download: &Download, frame: &mut Frame, span: Span) -> Result<()> {
227        let url = self.eval(&download.url, frame)?.as_str().to_owned();
228        let folder = self.eval(&download.folder, frame)?.as_str().to_owned();
229        let name = self.eval(&download.name, frame)?.as_str().to_owned();
230        let connections = self.eval(&download.connections, frame)?.as_int();
231        let headers = self.eval(&download.headers, frame)?;
232        let headers = headers.as_strs().into_iter().map(str::to_owned).collect();
233        let retries = self.eval(&download.retries, frame)?.as_int();
234        let mut bindings = Vec::with_capacity(download.bindings.len());
235        for (field, place) in &download.bindings {
236            let location = self.locate(place, frame)?;
237            if !location.is_in_state() {
238                return Err(RuntimeError::new(&Failure::DownloadIntoVariable, span));
239            }
240            bindings.push((*field, location));
241        }
242        let id = DownloadId::next();
243        let targets = Targets { id, bindings, span };
244        self.report_download(&targets, &Report::Started(id))?;
245        self.downloads.borrow_mut().push(Request {
246            id,
247            url,
248            folder,
249            name,
250            connections,
251            headers,
252            retries,
253            targets,
254        });
255        Ok(())
256    }
257
258    pub(crate) fn eval(&self, expr: &Expr, frame: &mut Frame) -> Result<Value> {
259        let span = expr.span;
260        Ok(match &expr.kind {
261            ExprKind::Int(value) => Value::Int(*value),
262            ExprKind::Float(value) => Value::Float(*value),
263            ExprKind::Bool(value) => Value::Bool(*value),
264            ExprKind::String(text) => Value::String(Rc::clone(text)),
265            ExprKind::Interpolate(segments) => {
266                let mut text = String::new();
267                for segment in segments {
268                    match segment {
269                        nifty_check::hir::Segment::Text(part) => text.push_str(part),
270                        nifty_check::hir::Segment::Value(value) => {
271                            let value = self.eval(value, frame)?;
272                            write!(text, "{value}").expect("writing to a String cannot fail");
273                        }
274                    }
275                }
276                Value::String(Rc::from(text))
277            }
278            ExprKind::State(state) => self.states.borrow()[state.0].clone(),
279            ExprKind::Local(local) => self.read_local(*local, frame, span)?,
280            ExprKind::List(items) => Value::List(Rc::new(self.eval_all(items, frame)?)),
281            ExprKind::Construct(_, fields) => Value::Struct(Rc::new(self.eval_all(fields, frame)?)),
282            ExprKind::Field(base, index) => match self.eval(base, frame)? {
283                Value::Struct(fields) => fields[*index].clone(),
284                other => panic!("checked code read a field of {other:?}"),
285            },
286            ExprKind::Index(base, index) => {
287                let list = self.eval(base, frame)?;
288                let position = position(self.eval(index, frame)?.as_int(), span)?;
289                item(&list, position, span)?.clone()
290            }
291            ExprKind::Not(operand) => Value::Bool(!self.eval(operand, frame)?.as_bool()),
292            ExprKind::Negate(operand) => match self.eval(operand, frame)? {
293                Value::Int(value) => Value::Int(
294                    value
295                        .checked_neg()
296                        .ok_or_else(|| RuntimeError::new(&Failure::TooLarge, span))?,
297                ),
298                Value::Float(value) => Value::Float(-value),
299                other => panic!("checked code negated {other:?}"),
300            },
301            ExprKind::Binary(op, lhs, rhs) => self.binary_expr(*op, lhs, rhs, frame, span)?,
302            ExprKind::Call(function, args) => self.call(*function, args, frame, span)?,
303            ExprKind::Len(value) => Value::Int(length(&self.eval(value, frame)?, span)?),
304            ExprKind::IsEmpty(value) => Value::Bool(length(&self.eval(value, frame)?, span)? == 0),
305            ExprKind::Push(place, value) => {
306                let value = self.eval(value, frame)?;
307                let location = self.locate(place, frame)?;
308                self.modify(&location, frame, span, |list| {
309                    list_items(list).push(value);
310                    Ok(())
311                })?;
312                Value::Void
313            }
314            ExprKind::Remove(place, index) => {
315                let position = position(self.eval(index, frame)?.as_int(), span)?;
316                let location = self.locate(place, frame)?;
317                self.modify(&location, frame, span, |list| {
318                    let items = list_items(list);
319                    if position >= items.len() {
320                        return Err(out_of_range(position, items.len(), span));
321                    }
322                    items.remove(position);
323                    Ok(())
324                })?;
325                Value::Void
326            }
327            ExprKind::Download(download) => {
328                self.request_download(download, frame, span)?;
329                Value::Void
330            }
331            ExprKind::DownloadCommand(command, id) => {
332                let id = self.eval(id, frame)?.as_str().to_owned();
333                self.download_commands.borrow_mut().push((*command, id));
334                Value::Void
335            }
336            ExprKind::FormatBytes(bytes) => Value::String(Rc::from(nifty_download::format_bytes(
337                self.eval(bytes, frame)?.as_int(),
338            ))),
339            ExprKind::Dialog(dialog) => {
340                self.ask_dialog(dialog, frame, span)?;
341                Value::Void
342            }
343            ExprKind::Window(action, window) => {
344                self.window_requests.borrow_mut().push(WindowRequest {
345                    action: *action,
346                    window: window.0,
347                });
348                Value::Void
349            }
350            ExprKind::Error => panic!("programs with errors are never run"),
351        })
352    }
353
354    fn eval_all(&self, exprs: &[Expr], frame: &mut Frame) -> Result<Vec<Value>> {
355        exprs.iter().map(|expr| self.eval(expr, frame)).collect()
356    }
357
358    fn read_local(&self, local: LocalId, frame: &Frame, span: Span) -> Result<Value> {
359        match &frame[local.0] {
360            Slot::Value(value) => Ok(value.clone()),
361            Slot::Alias(location) => self.read(location, frame, span),
362            Slot::Empty => panic!("the checker only allows reading locals after they are set"),
363        }
364    }
365
366    fn binary_expr(
367        &self,
368        op: BinaryOp,
369        lhs: &Expr,
370        rhs: &Expr,
371        frame: &mut Frame,
372        span: Span,
373    ) -> Result<Value> {
374        match op {
375            BinaryOp::And => Ok(Value::Bool(
376                self.eval(lhs, frame)?.as_bool() && self.eval(rhs, frame)?.as_bool(),
377            )),
378            BinaryOp::Or => Ok(Value::Bool(
379                self.eval(lhs, frame)?.as_bool() || self.eval(rhs, frame)?.as_bool(),
380            )),
381            _ => {
382                let lhs = self.eval(lhs, frame)?;
383                let rhs = self.eval(rhs, frame)?;
384                binary(op, &lhs, &rhs, span)
385            }
386        }
387    }
388
389    pub(crate) fn call(
390        &self,
391        function: FnId,
392        args: &[Expr],
393        frame: &mut Frame,
394        span: Span,
395    ) -> Result<Value> {
396        let program = Rc::clone(&self.program);
397        let definition = &program.functions[function.0];
398        let mut callee = new_frame(definition.locals.len());
399        for (slot, arg) in callee.iter_mut().zip(args) {
400            *slot = Slot::Value(self.eval(arg, frame)?);
401        }
402        let depth = self.call_depth.get();
403        if depth >= MAX_CALL_DEPTH {
404            let function = definition.name.clone();
405            return Err(RuntimeError::new(&Failure::TooDeep { function }, span));
406        }
407        self.call_depth.set(depth + 1);
408        let flow = self.exec_block(&definition.body, &mut callee);
409        self.call_depth.set(depth);
410        Ok(match flow? {
411            Flow::Return(value) => value,
412            Flow::Normal => Value::Void,
413        })
414    }
415}
416
417/// Applies a binary operator other than `&&` and `||` to evaluated operands.
418pub(crate) fn binary(op: BinaryOp, lhs: &Value, rhs: &Value, span: Span) -> Result<Value> {
419    use Value::{Bool, Float, Int, String};
420    let too_large = || RuntimeError::new(&Failure::TooLarge, span);
421    Ok(match (op, lhs, rhs) {
422        (BinaryOp::Eq, _, _) => Bool(lhs == rhs),
423        (BinaryOp::NotEq, _, _) => Bool(lhs != rhs),
424        (BinaryOp::Add, Int(a), Int(b)) => Int(a.checked_add(*b).ok_or_else(too_large)?),
425        (BinaryOp::Sub, Int(a), Int(b)) => Int(a.checked_sub(*b).ok_or_else(too_large)?),
426        (BinaryOp::Mul, Int(a), Int(b)) => Int(a.checked_mul(*b).ok_or_else(too_large)?),
427        (BinaryOp::Div | BinaryOp::Rem, Int(_), Int(0)) => {
428            return Err(RuntimeError::new(&Failure::DivideByZero, span));
429        }
430        (BinaryOp::Div, Int(a), Int(b)) => Int(a.checked_div(*b).ok_or_else(too_large)?),
431        (BinaryOp::Rem, Int(a), Int(b)) => Int(a.checked_rem(*b).ok_or_else(too_large)?),
432        (BinaryOp::Add, Float(a), Float(b)) => Float(a + b),
433        (BinaryOp::Sub, Float(a), Float(b)) => Float(a - b),
434        (BinaryOp::Mul, Float(a), Float(b)) => Float(a * b),
435        (BinaryOp::Div, Float(a), Float(b)) => Float(a / b),
436        (BinaryOp::Rem, Float(a), Float(b)) => Float(a % b),
437        (BinaryOp::Add, String(a), String(b)) => String(Rc::from(format!("{a}{b}"))),
438        (BinaryOp::Lt | BinaryOp::LtEq | BinaryOp::Gt | BinaryOp::GtEq, _, _) => {
439            let ordering = match (lhs, rhs) {
440                (Int(a), Int(b)) => a.partial_cmp(b),
441                (Float(a), Float(b)) => a.partial_cmp(b),
442                (String(a), String(b)) => a.partial_cmp(b),
443                _ => panic!("checked code compared {lhs:?} and {rhs:?}"),
444            };
445            Bool(ordering.is_some_and(|ordering| match op {
446                BinaryOp::Lt => ordering.is_lt(),
447                BinaryOp::LtEq => ordering.is_le(),
448                BinaryOp::Gt => ordering.is_gt(),
449                _ => ordering.is_ge(),
450            }))
451        }
452        _ => panic!("checked code applied {op:?} to {lhs:?} and {rhs:?}"),
453    })
454}
455
456/// The value a download's report writes, as the interpreter holds it.
457fn written_value(written: Written) -> Value {
458    match written {
459        Written::Int(number) => Value::Int(number),
460        Written::Text(text) => Value::String(Rc::from(text)),
461    }
462}
463
464/// A list position from an index value, which must not be negative.
465fn position(index: i64, span: Span) -> Result<usize> {
466    usize::try_from(index).map_err(|_| RuntimeError::new(&Failure::NegativePosition(index), span))
467}
468
469fn item(list: &Value, position: usize, span: Span) -> Result<&Value> {
470    let Value::List(items) = list else {
471        panic!("checked code indexed into {list:?}");
472    };
473    items
474        .get(position)
475        .ok_or_else(|| out_of_range(position, items.len(), span))
476}
477
478fn list_items(list: &mut Value) -> &mut Vec<Value> {
479    match list {
480        Value::List(items) => Rc::make_mut(items),
481        other => panic!("checked code changed {other:?} as a list"),
482    }
483}
484
485fn length(value: &Value, span: Span) -> Result<i64> {
486    let length = match value {
487        Value::List(items) => items.len(),
488        Value::String(text) => text.chars().count(),
489        other => panic!("checked code measured {other:?}"),
490    };
491    i64::try_from(length).map_err(|_| RuntimeError::new(&Failure::TooLong, span))
492}
493
494fn out_of_range(position: usize, len: usize, span: Span) -> RuntimeError {
495    RuntimeError::new(&Failure::NoItem { position, len }, span)
496}