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}