Skip to main content

nifty_rt/
context.rs

1//! What compiled code runs against: the app's state, what changed since the UI last caught
2//! up, how deep calls are, and why the code last failed.
3
4use std::cell::{Cell, RefCell};
5use std::fmt;
6use std::mem;
7
8use nifty_runtime::{DownloadCommand, Failure, FileDialog, StateId, StateSet, WindowRequest};
9
10use crate::dialog::Answer;
11use crate::download::Request;
12use crate::ffi::Str;
13use crate::program::ProgramDesc;
14use crate::value::{Slot, release};
15
16/// A failure while the app runs, pointing at the code responsible when the program says
17/// where that is.
18#[derive(Clone, Debug, PartialEq, Eq)]
19pub struct AppError {
20    pub message: String,
21    pub location: Option<SourceLocation>,
22}
23
24#[derive(Clone, Debug, PartialEq, Eq)]
25pub struct SourceLocation {
26    pub file: String,
27    pub line: u32,
28    pub column: u32,
29}
30
31impl AppError {
32    pub(crate) fn malformed(why: &str) -> Self {
33        Self {
34            message: format!("this app was built wrong, so it can't run: {why}"),
35            location: None,
36        }
37    }
38}
39
40impl fmt::Display for AppError {
41    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
42        match &self.location {
43            Some(SourceLocation { file, line, column }) => {
44                write!(f, "{file}:{line}:{column}: {}", self.message)
45            }
46            None => f.write_str(&self.message),
47        }
48    }
49}
50
51/// Compiled code receives this as an opaque pointer and passes it back on every call.
52pub struct Context {
53    program: &'static ProgramDesc,
54    states: Box<[Cell<Slot>]>,
55    /// States changed since the UI last caught up.
56    changed: RefCell<StateSet>,
57    call_depth: Cell<usize>,
58    /// Why the code that just returned `false` failed, and the span id of where.
59    failed: RefCell<Option<(Failure, u32)>>,
60    /// Downloads asked for since the app last started them.
61    downloads: RefCell<Vec<Request>>,
62    /// What code asked of running downloads since the app last passed it on.
63    download_commands: RefCell<Vec<(DownloadCommand, String)>>,
64    /// File dialogs asked for since the app last passed them on.
65    dialogs: RefCell<Vec<(FileDialog, Answer)>>,
66    /// Windows asked to open or close since the app last passed them on.
67    window_requests: RefCell<Vec<WindowRequest>>,
68}
69
70impl Context {
71    /// Sets every state to its starting value, in order, so each may read the ones before.
72    pub(crate) fn new(program: &'static ProgramDesc) -> Result<Self, AppError> {
73        let states = program.states.items();
74        let context = Self {
75            program,
76            states: states.iter().map(|_| Cell::new(0)).collect(),
77            changed: RefCell::new(StateSet::default()),
78            call_depth: Cell::new(0),
79            failed: RefCell::new(None),
80            downloads: RefCell::new(Vec::new()),
81            download_commands: RefCell::new(Vec::new()),
82            dialogs: RefCell::new(Vec::new()),
83            window_requests: RefCell::new(Vec::new()),
84        };
85        for (slot, state) in context.states.iter().zip(states) {
86            let mut value = 0;
87            // SAFETY: compiled initialisers take the context and write their value to `out`.
88            if !unsafe { (state.init)(&raw const context, &raw mut value) } {
89                return Err(context.take_error());
90            }
91            slot.set(value);
92        }
93        Ok(context)
94    }
95
96    pub(crate) fn state(&self, state: usize) -> Slot {
97        self.states[state].get()
98    }
99
100    /// The slot of `state`, which is marked changed since the caller is about to write it.
101    pub(crate) fn state_mut(&self, state: usize) -> *mut Slot {
102        self.changed.borrow_mut().insert(StateId(state));
103        self.states[state].as_ptr()
104    }
105
106    pub(crate) fn take_changed(&self) -> StateSet {
107        mem::take(&mut self.changed.borrow_mut())
108    }
109
110    pub(crate) fn fail(&self, failure: Failure, span: u32) {
111        *self.failed.borrow_mut() = Some((failure, span));
112    }
113
114    /// Counts a call into `function`, or fails when calls are too deep.
115    pub(crate) fn enter(&self, function: u32, span: u32) -> bool {
116        let depth = self.call_depth.get();
117        if depth >= nifty_runtime::MAX_CALL_DEPTH {
118            let name = usize::try_from(function)
119                .ok()
120                .and_then(|function| self.program.functions.items().get(function))
121                .map_or_else(|| format!("function {function}"), Str::lossy);
122            self.fail(Failure::TooDeep { function: name }, span);
123            return false;
124        }
125        self.call_depth.set(depth + 1);
126        true
127    }
128
129    pub(crate) fn leave(&self) {
130        self.call_depth.set(self.call_depth.get().saturating_sub(1));
131    }
132
133    pub(crate) fn request_download(&self, request: Request) {
134        self.downloads.borrow_mut().push(request);
135    }
136
137    /// The downloads asked for since the last call.
138    pub(crate) fn take_downloads(&self) -> Vec<Request> {
139        mem::take(&mut self.downloads.borrow_mut())
140    }
141
142    pub(crate) fn request_download_command(&self, command: DownloadCommand, id: String) {
143        self.download_commands.borrow_mut().push((command, id));
144    }
145
146    /// What code asked of running downloads since the last call, in order, by their ids.
147    pub(crate) fn take_download_commands(&self) -> Vec<(DownloadCommand, String)> {
148        mem::take(&mut self.download_commands.borrow_mut())
149    }
150
151    pub(crate) fn ask_dialog(&self, dialog: FileDialog, answer: Answer) {
152        self.dialogs.borrow_mut().push((dialog, answer));
153    }
154
155    pub(crate) fn request_window(&self, request: WindowRequest) {
156        self.window_requests.borrow_mut().push(request);
157    }
158
159    /// The windows asked to open or close since the last call, in order.
160    pub(crate) fn take_window_requests(&self) -> Vec<WindowRequest> {
161        mem::take(&mut self.window_requests.borrow_mut())
162    }
163
164    /// The file dialogs asked for since the last call.
165    pub(crate) fn take_dialogs(&self) -> Vec<(FileDialog, Answer)> {
166        mem::take(&mut self.dialogs.borrow_mut())
167    }
168
169    /// Why compiled code just returned `false`, which it does only after a runtime call
170    /// recorded a failure.
171    ///
172    /// # Panics
173    /// When no failure was recorded, which only wrongly compiled code does.
174    pub(crate) fn take_error(&self) -> AppError {
175        let (failure, span) = self
176            .failed
177            .borrow_mut()
178            .take()
179            .expect("compiled code fails only after the runtime records why");
180        self.error(&failure, span)
181    }
182
183    pub(crate) fn error(&self, failure: &Failure, span: u32) -> AppError {
184        AppError {
185            message: failure.to_string(),
186            location: self.location(span),
187        }
188    }
189
190    fn location(&self, span: u32) -> Option<SourceLocation> {
191        let span = self
192            .program
193            .spans
194            .items()
195            .get(usize::try_from(span).ok()?)?;
196        Some(SourceLocation {
197            file: span.file.lossy(),
198            line: span.line,
199            column: span.column,
200        })
201    }
202}
203
204impl Drop for Context {
205    fn drop(&mut self) {
206        for (slot, state) in self.states.iter().zip(self.program.states.items()) {
207            release(slot.get(), state.ty);
208        }
209    }
210}
211
212impl fmt::Debug for Context {
213    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
214        f.debug_struct("Context")
215            .field("states", &self.states.len())
216            .field("call_depth", &self.call_depth.get())
217            .finish_non_exhaustive()
218    }
219}