Skip to main content

logicaffeine_compile/vm/
session.rs

1//! A program the VM has run, held open so a host can call back into it.
2//!
3//! A browser instantiates a page's module, runs `main` once, and from then on calls INTO it —
4//! an event handler, a fetch completion, a timer tick, a failure handler. Each is the host
5//! invoking an export by name or a closure the module handed it earlier, against the globals
6//! `main` built. A trap unwinds to the host and leaves the instance callable.
7//!
8//! [`Session`] is that shape for the in-process bytecode VM: [`Session::run_main`] once, then
9//! [`Session::invoke`] and [`Session::call_value`] for as long as the host likes. It is the
10//! seam `page_host` drives a lowered page through, and what lets a host-delivered completion run
11//! in-process at all.
12
13use super::instruction::FuncIdx;
14use super::machine::Vm;
15use crate::intern::Interner;
16use crate::interpreter::RuntimeValue;
17use std::collections::HashMap;
18
19/// A VM whose program has run (or is about to), open to calls from the host.
20pub struct Session<'p> {
21    vm: Vm<'p>,
22    /// Every named function of the program, by its source name.
23    functions: HashMap<String, FuncIdx>,
24    main_ran: bool,
25}
26
27impl<'p> Session<'p> {
28    /// Hold `vm` open for host calls. Nothing runs until [`Session::run_main`].
29    pub fn new(vm: Vm<'p>, interner: &Interner) -> Self {
30        let functions = vm
31            .program()
32            .fn_index
33            .iter()
34            .map(|(sym, fi)| (interner.resolve(*sym).to_string(), *fi))
35            .collect();
36        Session {
37            vm,
38            functions,
39            main_ran: false,
40        }
41    }
42
43    /// Run the program's main code, once. A failure is reported and the session stays callable,
44    /// exactly as a browser can still call into a module whose `main` trapped.
45    pub fn run_main(&mut self) -> Result<(), String> {
46        if self.main_ran {
47            return Err(
48                "vm session: main has already run, and running it again would \
49                        re-initialize every global the program is using"
50                    .to_string(),
51            );
52        }
53        self.main_ran = true;
54        let outcome = self.vm.run_keeping_entries();
55        if outcome.is_err() {
56            self.vm.quiesce();
57        }
58        outcome
59    }
60
61    /// The failure a page entry the program took itself contained and no handler absorbed, with
62    /// its failure site — the host carrying the page on takes it as the page's failure
63    /// ([`Vm::take_unhandled`]).
64    pub fn take_unhandled(&mut self) -> Option<(String, Option<u32>)> {
65        self.vm.take_unhandled()
66    }
67
68    /// Call the function the program names `name`, with `args` in parameter order.
69    pub fn invoke(&mut self, name: &str, args: Vec<RuntimeValue>) -> Result<RuntimeValue, String> {
70        let fi = *self
71            .functions
72            .get(name)
73            .ok_or_else(|| format!("vm session: this program has no function `{name}`"))?;
74        self.require_main(name)?;
75        let expected = self.vm.program().functions[fi as usize].param_count as usize;
76        if args.len() != expected {
77            return Err(format!(
78                "`{name}` expects {expected} arguments, got {}",
79                args.len()
80            ));
81        }
82        self.vm.invoke_function(fi, args)
83    }
84
85    /// Call a closure the program handed out — a completion, a listener, a route handler.
86    pub fn call_value(
87        &mut self,
88        callee: &RuntimeValue,
89        args: Vec<RuntimeValue>,
90    ) -> Result<RuntimeValue, String> {
91        self.require_main("a closure")?;
92        self.vm.call_closure(callee, args)
93    }
94
95    /// A page reload: the program re-instantiated with the same configuration and its main run
96    /// again. Every global starts over; what lives outside the program (the document recorder,
97    /// browser storage) is the host's to keep or clear.
98    pub fn reload(&mut self) -> Result<(), String> {
99        self.vm = self.vm.reinstantiate();
100        self.main_ran = false;
101        self.run_main()
102    }
103
104    /// Whether the program defines a function named `name`.
105    pub fn has(&self, name: &str) -> bool {
106        self.functions.contains_key(name)
107    }
108
109    /// The current value of the named global, shown exactly as `Show` would render it — `None`
110    /// when the program declares no such global, or it has not been assigned one yet. The read a
111    /// host needs for state a lowering writes into a global specifically so the host can see it.
112    pub fn global_text(&self, name: &str) -> Option<String> {
113        self.vm
114            .debug_view()
115            .globals
116            .into_iter()
117            .find(|(n, _)| n == name)
118            .map(|(_, v)| v)
119    }
120
121    /// The number of top-level elements the named global holds, when it is a `List`/`Set`
122    /// (`[a, b, c]`) or a `Map` (`{k: v, k: v}`) — `None` when the global does not exist, has no
123    /// value yet, or does not display as one of those two bracket shapes. Reads
124    /// [`Session::global_text`]'s display string rather than the VM's own value representation
125    /// (nothing in this crate exposes that to a host), so it walks the string BRACKET-DEPTH-AWARE
126    /// — counting a comma only at depth 1, never inside a nested collection's own brackets — the
127    /// one thing that would otherwise make this indistinguishable from a naive split. It remains
128    /// unsound only for a leaf value whose OWN display contains an unbracketed `,`/`[`/`{`/`}`/`]`
129    /// (a `Text`, which displays raw and unquoted) — exactly why this is a length probe for a
130    /// table of plain scalars (an `Int`-keyed listener/site map, the shape a boundedness proof
131    /// measures), not a general-purpose value parser.
132    pub fn global_len(&self, name: &str) -> Option<usize> {
133        let text = self.global_text(name)?;
134        let inner = text
135            .strip_prefix('[')
136            .and_then(|s| s.strip_suffix(']'))
137            .or_else(|| text.strip_prefix('{').and_then(|s| s.strip_suffix('}')))?;
138        if inner.is_empty() {
139            return Some(0);
140        }
141        let mut depth = 0i32;
142        let mut count = 1usize;
143        for c in inner.chars() {
144            match c {
145                '[' | '{' | '(' => depth += 1,
146                ']' | '}' | ')' => depth -= 1,
147                ',' if depth == 0 => count += 1,
148                _ => {}
149            }
150        }
151        Some(count)
152    }
153
154    /// Every line the program has shown, main and every call since, in order.
155    pub fn lines(&self) -> &[String] {
156        self.vm.lines()
157    }
158
159    /// The machine itself, for the structural tests beside it.
160    #[cfg(test)]
161    pub(super) fn machine(&self) -> &Vm<'p> {
162        &self.vm
163    }
164
165    fn require_main(&self, what: &str) -> Result<(), String> {
166        if self.main_ran {
167            Ok(())
168        } else {
169            Err(format!(
170                "vm session: cannot call {what} before main has run — main initializes the \
171                 globals every function reads"
172            ))
173        }
174    }
175}