Skip to main content

logicaffeine_compile/page_host/
lowered.rs

1//! A lowered page, driven the way `runtime.js` drives it.
2//!
3//! A page compiled for a browser is ordinary LOGOS: `__mount_<C>`, the entries a host calls it
4//! through (`__lgx_dispatch_node` for an element's event, `__lgx_tick`, `__lgx_on_failure`, the rest
5//! of `lgx_lower::LGX_ENTRIES`), and closures it hands the host
6//! (a fetch completion, a route handler, an interval). The browser runs `main`, then calls in:
7//! each event, completion and tick through one guarded door that reports a trap, hands its
8//! message to the page's failure handler, and settles the page. [`LoweredHost`] is that host's
9//! state, and its driver works any engine that can call a lowered function by name and a closure by
10//! value ([`LoweredCalls`]) — the bytecode VM and the tree-walker alike. The host OWNS NO ENGINE: it
11//! borrows one for the statement it is carrying out, so the tree-walker, which also runs the test
12//! driving the page, can hold its page's host itself. [`LoweredPage`] is a host and the engine it
13//! drives held together, for a host process that owns both.
14
15use super::Drive;
16use crate::interpreter::RuntimeValue;
17use crate::semantics::dom;
18use crate::semantics::drive::{self as answers, Answer, Verb};
19use logicaffeine_language::parser::lgx_lower;
20use std::collections::BTreeSet;
21use std::rc::Rc;
22
23/// An engine a lowered page runs on, seen from the host: call a function the page defines, call
24/// a closure it handed out, ask whether it defines a name, and reload it.
25pub trait LoweredCalls {
26    fn call(&mut self, name: &str, args: Vec<RuntimeValue>) -> Result<RuntimeValue, String>;
27    fn call_value(
28        &mut self,
29        callee: &RuntimeValue,
30        args: Vec<RuntimeValue>,
31    ) -> Result<RuntimeValue, String>;
32    fn has(&self, name: &str) -> bool;
33    /// The page reloaded: a fresh instance of the same program, its main run again.
34    fn reload(&mut self) -> Result<(), String>;
35    /// Every line the engine has shown, main and every call since, in order.
36    fn lines(&self) -> &[String];
37    /// The failure site (`Stmt::Site`) in force when the most recent failed call trapped — where
38    /// in the page's source it failed — for an engine that tracks one.
39    fn failure_site(&self) -> Option<u32> {
40        None
41    }
42    /// A failure the page's own code contained while it ran — a turn a driving verb the program
43    /// runs on its own page took ([`crate::semantics::builtins::BuiltinId::DomDispatchEntry`]),
44    /// whose failure door already heard it and no handler absorbed — with its failure site, for
45    /// the host to hold as the page's failure. `None` for an engine whose entries report through
46    /// this host alone.
47    fn take_unhandled(&mut self) -> Option<(String, Option<u32>)> {
48        None
49    }
50}
51
52impl LoweredCalls for crate::vm::session::Session<'_> {
53    fn take_unhandled(&mut self) -> Option<(String, Option<u32>)> {
54        crate::vm::session::Session::take_unhandled(self)
55    }
56    fn call(&mut self, name: &str, args: Vec<RuntimeValue>) -> Result<RuntimeValue, String> {
57        self.invoke(name, args)
58    }
59    fn call_value(
60        &mut self,
61        callee: &RuntimeValue,
62        args: Vec<RuntimeValue>,
63    ) -> Result<RuntimeValue, String> {
64        crate::vm::session::Session::call_value(self, callee, args)
65    }
66    fn has(&self, name: &str) -> bool {
67        crate::vm::session::Session::has(self, name)
68    }
69    fn reload(&mut self) -> Result<(), String> {
70        crate::vm::session::Session::reload(self)
71    }
72    fn lines(&self) -> &[String] {
73        crate::vm::session::Session::lines(self)
74    }
75}
76
77/// Why a statement did not drive the page.
78#[derive(Clone, Debug, PartialEq, Eq)]
79pub enum DriveError {
80    /// The page answered with a failure — the one the reference tier raises for the same
81    /// statement, and the one a test author reads.
82    Failed(String),
83    /// This tier cannot carry the statement at all; the reason says what is missing.
84    Refused(String),
85}
86
87/// One interval the page armed, and when it next comes due on the page's clock.
88struct Armed {
89    period_ms: i64,
90    handler: RuntimeValue,
91    next_due_ms: i64,
92}
93
94/// The host state a browser keeps for a lowered page. It owns no engine: every statement it
95/// carries out borrows the engine the page runs on for exactly that statement
96/// ([`LoweredHost::drive`]).
97#[derive(Default)]
98pub struct LoweredHost {
99    /// The page's clock, in virtual milliseconds since it started.
100    now_ms: i64,
101    armed: Vec<Armed>,
102    /// Requests the host has already decided about. A request is answered by what answers it
103    /// when it is sent, once — the reference tier's rule — so a stub arranged later does not
104    /// reach back and answer a request that had none.
105    decided: BTreeSet<i64>,
106    serve_routes: bool,
107    /// The first action failure no handler absorbed.
108    unhandled: Option<String>,
109    /// Where that failure happened, when the engine tracks it ([`LoweredCalls::failure_site`]).
110    unhandled_site: Option<u32>,
111    /// The mounted component's own name — a page mounts itself, so this is set before `main` runs
112    /// for one, and by a [`Drive::Mount`] for an explicit `Mount`. What `Replay these actions:`
113    /// runs its actions against.
114    mounted_component: Option<String>,
115    /// The `## Page` component the program mounts by itself, if any — what loading the page
116    /// mounts, so a reload mounts it again.
117    page: Option<String>,
118}
119
120impl LoweredHost {
121    /// The host of a page that has not started.
122    pub fn new() -> Self {
123        LoweredHost::default()
124    }
125
126    /// The host of a program whose `## Page` component `page` names (`None` for a program that
127    /// mounts nothing by itself): what its reload mounts again, and what `Replay` runs against
128    /// before any `Mount` of the test's own.
129    pub fn of_page(page: Option<String>) -> Self {
130        LoweredHost {
131            mounted_component: page.clone(),
132            page,
133            ..LoweredHost::default()
134        }
135    }
136
137    /// The first action failure no handler absorbed, if any.
138    pub fn unhandled_failure(&self) -> Option<&str> {
139        self.unhandled.as_deref()
140    }
141
142    /// The first action failure no handler absorbed, taken — the program's own failure once the
143    /// program that hosts the page has finished.
144    pub fn take_unhandled_failure(&mut self) -> Option<String> {
145        self.unhandled.take()
146    }
147
148    /// Where the first unabsorbed failure happened, when the engine tracks it.
149    pub fn unhandled_failure_site(&self) -> Option<u32> {
150        self.unhandled_site
151    }
152
153    /// Start the page on `calls`: its main runs — the mount a `## Page` performs by itself, and
154    /// whatever else the program does at the top level — and the page settles. A failing main is
155    /// the program's failure. `auto_mounted` names the `## Page` component the program mounts by
156    /// itself, if any ([`crate::page_host::auto_mounted_page_component`]) — set before `main` runs,
157    /// since a page's own mount is not a [`Drive::Mount`] this host ever sees.
158    pub fn start<L: LoweredCalls + ?Sized>(
159        &mut self,
160        calls: &mut L,
161        auto_mounted: Option<String>,
162        main: impl FnOnce(&mut L) -> Result<(), String>,
163    ) -> Result<(), DriveError> {
164        self.mounted_component = auto_mounted.clone();
165        self.page = auto_mounted;
166        main(calls).map_err(|message| DriveError::Failed(in_failure_context(calls, message)))?;
167        self.hold_contained_failure(calls);
168        Driver { host: self, calls }.settle();
169        Ok(())
170    }
171
172    /// Hold a failure the page's own code contained while it ran ([`LoweredCalls::take_unhandled`])
173    /// as the page's failure, when it is the first: its failure door already ran.
174    fn hold_contained_failure<L: LoweredCalls + ?Sized>(&mut self, calls: &mut L) {
175        if let Some((message, site)) = calls.take_unhandled() {
176            if self.unhandled.is_none() {
177                self.unhandled = Some(message);
178                self.unhandled_site = site;
179            }
180        }
181    }
182
183    /// Deliver a popstate the page is owed and complete every request that has an answer — what the
184    /// host does after anything runs in the page (`Driver::settle`).
185    pub fn settle<L: LoweredCalls + ?Sized>(&mut self, calls: &mut L) {
186        Driver { host: self, calls }.settle();
187    }
188
189    /// Carry one driving statement out on the page `calls` runs, exactly as the browser host
190    /// would, then settle.
191    pub fn drive<L: LoweredCalls + ?Sized>(
192        &mut self,
193        calls: &mut L,
194        d: &Drive,
195    ) -> Result<(), DriveError> {
196        Driver { host: self, calls }.drive(d)
197    }
198}
199
200/// A lowered page and its host, held together by a process that owns both — the bytecode VM's
201/// page, driven from outside the interpreter running the test.
202pub struct LoweredPage<L: LoweredCalls> {
203    calls: L,
204    host: LoweredHost,
205}
206
207impl<L: LoweredCalls> LoweredPage<L> {
208    /// Host a page whose engine has not run yet.
209    pub fn new(calls: L) -> Self {
210        LoweredPage {
211            calls,
212            host: LoweredHost::new(),
213        }
214    }
215
216    /// The engine the page runs on.
217    pub fn calls(&mut self) -> &mut L {
218        &mut self.calls
219    }
220
221    /// The first action failure no handler absorbed, if any.
222    pub fn unhandled_failure(&self) -> Option<&str> {
223        self.host.unhandled_failure()
224    }
225
226    /// Every line the page has shown, newline-joined — the engine's own output convention, so a
227    /// host that also runs a program's plain `Show`s (a `## Main` that both prints and drives a
228    /// page) reports the same text the straight-through compiled path would.
229    pub fn output(&self) -> String {
230        self.calls.lines().join("\n")
231    }
232
233    /// Start the page: see [`LoweredHost::start`].
234    pub fn start(
235        &mut self,
236        auto_mounted: Option<String>,
237        main: impl FnOnce(&mut L) -> Result<(), String>,
238    ) -> Result<(), DriveError> {
239        self.host.start(&mut self.calls, auto_mounted, main)
240    }
241
242    /// Carry one driving statement out: see [`LoweredHost::drive`].
243    pub fn drive(&mut self, d: &Drive) -> Result<(), DriveError> {
244        self.host.drive(&mut self.calls, d)
245    }
246}
247
248/// One statement being carried out: the host's state and the engine the page runs on, borrowed
249/// together for exactly as long as the statement takes.
250struct Driver<'d, L: ?Sized> {
251    host: &'d mut LoweredHost,
252    calls: &'d mut L,
253}
254
255impl<L: LoweredCalls + ?Sized> Driver<'_, L> {
256    /// Carry one driving statement out, exactly as the browser host would, then settle.
257    fn drive(&mut self, d: &Drive) -> Result<(), DriveError> {
258        match d {
259            Drive::Mount { component, at } => self.mount(component, at.as_deref())?,
260            Drive::Click { selector } => {
261                self.default_action(Verb::Click, selector, None);
262                self.click(selector)?
263            }
264            Drive::Submit { selector } => {
265                self.default_action(Verb::Submit, selector, None);
266                self.submit(selector)?
267            }
268            Drive::Press { key, selector } => {
269                self.default_action(Verb::Press, selector, None);
270                self.element_verb(Verb::Press, selector, key, Some(key), true)?;
271            }
272            Drive::Type { text, selector } => {
273                self.default_action(Verb::Type, selector, Some(text));
274                self.element_verb(Verb::Type, selector, text, None, true)?;
275            }
276            Drive::Choose { value, selector } => {
277                self.default_action(Verb::Choose, selector, Some(value));
278                self.element_verb(Verb::Choose, selector, value, None, true)?;
279            }
280            Drive::Hover { selector } => {
281                self.default_action(Verb::Hover, selector, None);
282                let answer = self.dispatch(selector, Verb::Hover.events()[0], "");
283                if let Some(msg) =
284                    answers::answer_failure(answer, Verb::Hover.events(), None, selector, true)
285                {
286                    return Err(DriveError::Failed(msg));
287                }
288            }
289            Drive::Focus { selector } => {
290                dom::element_focus(require_element(selector, "Focus")?);
291                self.deliver_raised_events();
292            }
293            Drive::Blur { selector } => {
294                dom::element_blur(require_element(selector, "Blur")?);
295                self.deliver_raised_events();
296            }
297            Drive::Raise {
298                event,
299                target,
300                value,
301            } => {
302                if dom::query_selector(target) < 0 {
303                    return Err(DriveError::Failed(answers::no_element(target)));
304                }
305                let raise = lgx_lower::raise_fn_name();
306                if self.calls.has(raise) {
307                    let args = vec![text(target), text(event), text(value)];
308                    self.guarded(|c| c.call(raise, args));
309                }
310            }
311            Drive::DropFile { target, file } => {
312                if dom::query_selector(target) < 0 {
313                    return Err(DriveError::Failed(answers::no_element(target)));
314                }
315                dom::set_dropped_file(file.clone());
316                let raise = lgx_lower::raise_fn_name();
317                if self.calls.has(raise) {
318                    let args = vec![text(target), text("filedropped"), text(&file.name)];
319                    self.guarded(|c| c.call(raise, args));
320                }
321            }
322            Drive::Navigate { url } => {
323                dom::navigate(url);
324                self.deliver_route_change();
325            }
326            // The page loads again, as a browser reloads it: a fresh document at the same address
327            // with the test's server still answering (`dom::reload`), a fresh clock, the page's
328            // main run again, and the page it mounts by itself mounted again.
329            Drive::Reload => {
330                dom::reload();
331                answers::reset_clock();
332                self.host.armed.clear();
333                self.host.decided.clear();
334                self.host.now_ms = 0;
335                self.calls.reload().map_err(DriveError::Failed)?;
336                if let Some(page) = self.host.page.clone() {
337                    self.mount(&page, None)?;
338                }
339            }
340            Drive::AdvanceTime { ms } => {
341                logicaffeine_system::testworld::advance_clock(*ms);
342                self.advance_clock(*ms as i64);
343            }
344            Drive::AnswerFetch {
345                method,
346                url,
347                status,
348                body,
349            } => {
350                dom::stub_fetch(method, url, *status, body);
351            }
352            Drive::ServeFetchesWithRoutes => {
353                if !self.calls.has("route") {
354                    return Err(DriveError::Failed(answers::no_routes_to_serve()));
355                }
356                self.host.serve_routes = true;
357            }
358            Drive::Replay { actions } => self.replay(actions)?,
359        }
360        self.settle();
361        Ok(())
362    }
363
364    /// `Replay these actions: a, b, c.` — re-run the mounted component's own actions, by name, in
365    /// order, calling each one's lowered entry (`__act_<C>_<action>`) exactly as the reference
366    /// tier's `replay_actions` (`Handler::Action { args: Vec::new(), .. }`) does: no ARGUMENTS of
367    /// its own, but every `__act_` function's FIRST parameter is the instance key it runs against
368    /// (`lower_component_action`'s own `instance_key_param()` binding, ahead of `action.params`) —
369    /// `""`, the root instance's key, exactly what `root_key()`'s own doc names this driving-verb
370    /// case for: "the driving-verb dispatcher, which matches a selector and so cannot say which
371    /// instance rendered the element it names." `Replay` names no element either.
372    fn replay(&mut self, actions: &[String]) -> Result<(), DriveError> {
373        let Some(component) = self.host.mounted_component.clone() else {
374            return Err(DriveError::Failed(
375                "`Replay these actions:` needs a page to replay them against. Mount the component \
376                 first."
377                    .to_string(),
378            ));
379        };
380        for action in actions {
381            let entry = lgx_lower::action_fn_name(&component, action);
382            if !self.calls.has(&entry) {
383                return Err(DriveError::Failed(format!(
384                    "`{action}` is not an action of the mounted component, so there is nothing to \
385                     replay. Declare it in `Actions:` as `To {action} …:`."
386                )));
387            }
388            self.guarded(|c| c.call(&entry, vec![text("")]));
389        }
390        Ok(())
391    }
392
393    /// `Mount C [at S]` — what the page's main does for a mount: find the parent, call
394    /// `__mount_C(parent)`.
395    fn mount(&mut self, component: &str, at: Option<&str>) -> Result<(), DriveError> {
396        let entry = lgx_lower::mount_fn_name(component);
397        if !self.calls.has(&entry) {
398            return Err(DriveError::Refused(format!(
399                "`Mount {component}` has no lowered entry `{entry}` on this tier"
400            )));
401        }
402        let parent = match at {
403            Some(sel) => {
404                let node = dom::query_selector(sel);
405                if node < 0 {
406                    return Err(DriveError::Failed(answers::mount_target_missing(
407                        component, sel,
408                    )));
409                }
410                node
411            }
412            None => dom::body(),
413        };
414        self.guarded(|c| c.call(&entry, vec![RuntimeValue::Int(parent)]));
415        self.host.mounted_component = Some(component.to_string());
416        Ok(())
417    }
418
419    /// `Click SEL.` — the page hears the click, and the browser performs the click's default
420    /// action: the link the click is on — the element's own `<a href>`, or the one it sits inside
421    /// ([`dom::link_of`]) — is followed unless a listener the click reached cancels it. The link
422    /// needs no listener, since the browser itself answers a click on one.
423    fn click(&mut self, selector: &str) -> Result<(), DriveError> {
424        let node = dom::query_selector(selector);
425        if let Some(control) = (node >= 0).then(|| dom::toggle_control(node)).flatten() {
426            return self.toggle(node, control);
427        }
428        let link = if node >= 0 { dom::link_of(node) } else { None };
429        let prevented = self.element_verb(Verb::Click, selector, "", None, link.is_none())?;
430        match link {
431            Some(link) if link.here && !prevented => self.follow(&link.href),
432            _ => Ok(()),
433        }
434    }
435
436    /// A click on a checkbox or a radio: the browser moves the control itself — a checkbox flips, a
437    /// radio is selected — before the page hears the `click`, and puts it back when a listener
438    /// cancels the click; otherwise `input` and `change` follow. Each event carries the control's
439    /// new state (`"true"`/`"false"`). The browser answers the click, so no listener is required.
440    fn toggle(&mut self, node: i64, control: dom::ToggleControl) -> Result<(), DriveError> {
441        let was = dom::get_checked(node);
442        let now = match control {
443            dom::ToggleControl::Checkbox => !was,
444            dom::ToggleControl::Radio => true,
445        };
446        dom::set_checked(node, now);
447        let state = if now { "true" } else { "false" };
448        let (_, prevented) = self.deliver(node, "click", state);
449        if prevented {
450            dom::set_checked(node, was);
451            return Ok(());
452        }
453        self.deliver(node, "input", state);
454        self.deliver(node, "change", state);
455        Ok(())
456    }
457
458    /// A link followed. A path the page answers itself — the module decides, through
459    /// `__lgx_owns`, the same `matchRoute` its `Navigate to` branches on — is client navigation:
460    /// the location moves in place (`pushState`) and the page's route handlers run. Any other path,
461    /// and every path of a module that answers none, is a document load: another page file's
462    /// document, outside the test ([`dom::assign_location`]).
463    fn follow(&mut self, href: &str) -> Result<(), DriveError> {
464        const OWNS: &str = "__lgx_owns";
465        let path = href.split(['?', '#']).next().unwrap_or(href).to_string();
466        let owned = self.calls.has(OWNS)
467            && matches!(
468                self.guarded(|c| c.call(OWNS, vec![text(&path)])),
469                Some(RuntimeValue::Bool(true))
470            );
471        if owned {
472            dom::navigate(href);
473            self.deliver_route_change();
474        } else {
475            dom::assign_location(href);
476        }
477        Ok(())
478    }
479
480    /// `Submit SEL.` — `requestSubmit` on the form `SEL` is or belongs to, with `SEL` as the
481    /// submitter when it is a submit button. The browser first validates the form
482    /// ([`dom::form_constraints`]): an invalid control is answered with `invalid`, the first of them
483    /// takes focus, and nothing is submitted. A valid form hears `submit`, and one nothing prevents
484    /// is then submitted by the browser: a `get` form navigates with its fields in the query string,
485    /// any other method sends them form-encoded to its action. A handler is an addition to that, so
486    /// a form needs no listener; an element in no form must be answered.
487    fn submit(&mut self, selector: &str) -> Result<(), DriveError> {
488        let target = dom::query_selector(selector);
489        let Some(node) = (target >= 0).then(|| dom::form_of(target)).flatten() else {
490            self.element_verb(Verb::Submit, selector, "", None, true)?;
491            return Ok(());
492        };
493        let submitter = (target != node && dom::submits_its_form(target)).then_some(target);
494        let invalid = dom::form_constraints(node, submitter).map_err(DriveError::Refused)?;
495        if !invalid.is_empty() {
496            let mut first = None;
497            for control in invalid {
498                let (_, cancelled) = self.deliver(control, "invalid", "");
499                if !cancelled && first.is_none() {
500                    first = Some(control);
501                }
502            }
503            if let Some(first) = first {
504                dom::element_focus(first);
505                self.deliver_raised_events();
506            }
507            return Ok(());
508        }
509        if self.deliver(node, "submit", "").1 {
510            return Ok(());
511        }
512        let action = dom::attribute(node, "action").unwrap_or_else(dom::route_path);
513        let method = dom::attribute(node, "method").unwrap_or_else(|| "get".to_string());
514        let body = answers::form_urlencoded(&dom::form_fields(node));
515        if method.eq_ignore_ascii_case("get") {
516            let url = if body.is_empty() {
517                action
518            } else {
519                format!("{action}?{body}")
520            };
521            dom::navigate(&url);
522            self.deliver_route_change();
523        } else {
524            let req = dom::fetch_send(
525                &method.to_uppercase(),
526                &action,
527                &body,
528                vec![(
529                    "content-type".to_string(),
530                    "application/x-www-form-urlencoded".to_string(),
531                )],
532                RuntimeValue::Nothing,
533            );
534            self.follow_post(req);
535        }
536        Ok(())
537    }
538
539    /// A form the browser submitted itself: its request is answered at once, by the rule every
540    /// request is answered by — the test's answer for it, then the program's own routes — and the
541    /// browser shows the answer as the new document ([`dom::follow_response`]), of the answer's own
542    /// type. A request nothing answers stays sent, the page as it was.
543    fn follow_post(&mut self, req: i64) {
544        let Some(r) = dom::fetch_requests().into_iter().find(|r| r.req == req) else {
545            return;
546        };
547        self.host.decided.insert(req);
548        let stubbed = dom::stubbed_answer(req).is_some();
549        let Some((_, body)) = self.answer_for(&r) else {
550            return;
551        };
552        let content_type = if stubbed {
553            dom::STUB_CONTENT_TYPE.to_string()
554        } else {
555            self.route_content_type(&r)
556        };
557        dom::follow_response(&r.url, &content_type, &body);
558    }
559
560    /// The type the program's own routes answer `r` with (`routeContentType`), plain text when the
561    /// program declares none.
562    fn route_content_type(&mut self, r: &dom::FetchRequest) -> String {
563        const ROUTE_CONTENT_TYPE: &str = "routeContentType";
564        if !self.calls.has(ROUTE_CONTENT_TYPE) {
565            return "text/plain; charset=utf-8".to_string();
566        }
567        let args = vec![text(&r.method), text(&r.url)];
568        match self.guarded(|c| c.call(ROUTE_CONTENT_TYPE, args)) {
569            Some(RuntimeValue::Text(t)) => t.to_string(),
570            _ => "text/plain; charset=utf-8".to_string(),
571        }
572    }
573
574    /// An element verb: each of its events delivered at the element `selector` names
575    /// ([`Driver::deliver`]), `value` carried with each — the typed text, the chosen value, the
576    /// pressed key — and what came of them read back into the message the reference tier gives.
577    /// Typing and choosing raise their events only where the control takes them
578    /// ([`dom::answers_by_itself`]) and are then answered by the browser's own default action;
579    /// where it cannot, nothing is raised and the verb is refused. Whether a listener the events
580    /// reached cancelled their default action.
581    fn element_verb(
582        &mut self,
583        verb: Verb,
584        selector: &str,
585        value: &str,
586        key: Option<&str>,
587        must_answer: bool,
588    ) -> Result<bool, DriveError> {
589        let node = dom::query_selector(selector);
590        let entry = matches!(verb, Verb::Type | Verb::Choose);
591        if entry && node >= 0 && !dom::answers_by_itself(verb, selector, value) {
592            return Err(DriveError::Failed(answers::cannot_take(
593                verb, selector, value,
594            )));
595        }
596        let must_answer = must_answer && !entry;
597        let (answer, prevented) = if node < 0 {
598            (Answer::NoElement, false)
599        } else {
600            let (mut answered, mut prevented) = (false, false);
601            for event in verb.events() {
602                let (a, p) = self.deliver(node, event, value);
603                answered |= a;
604                prevented |= p;
605            }
606            (
607                if answered {
608                    Answer::Answered
609                } else {
610                    Answer::Unanswered
611                },
612                prevented,
613            )
614        };
615        match answers::answer_failure(answer, verb.events(), key, selector, must_answer) {
616            Some(msg) => Err(DriveError::Failed(msg)),
617            None => Ok(prevented),
618        }
619    }
620
621    /// Dispatch `event` at `node` as a browser does: along [`dom::dispatch_path`] — the capture
622    /// listeners from the root down, the target, the bubbling listeners back up, ending where one
623    /// stops propagation — each stop reached through the page's node dispatcher, found as the page
624    /// stands when its turn comes. Whether anything answered (a turn that failed answered, its
625    /// failure handed to the page's handler or reported), and whether a listener on the walk
626    /// cancelled the event's default action.
627    fn deliver(&mut self, node: i64, event: &str, value: &str) -> (bool, bool) {
628        let path = dom::dispatch_path(node, event);
629        let dispatch = lgx_lower::dispatch_node_fn_name();
630        let mut answered = false;
631        if self.calls.has(dispatch) {
632            for stop in &path.stops {
633                let at = dom::query_selector(stop);
634                if at < 0 {
635                    continue;
636                }
637                let args = vec![RuntimeValue::Int(at), text(event), text(value)];
638                let answer = self.guarded(|c| c.call(dispatch, args));
639                answered |= !matches!(answer, Some(RuntimeValue::Bool(false)));
640            }
641        }
642        (answered, path.prevented)
643    }
644
645    /// The browser's own default action of `verb` on the element `selector` names, carried out
646    /// before the page hears the verb's own events and in the order a browser performs it: focus
647    /// moves where the verb sends it ([`dom::focus_by`], after [`Verb::focus_target`]), raising
648    /// `blur` and then `focus` on the elements that lost and gained it; then a verb that enters a
649    /// `value` leaves it in the control ([`dom::enter_value`]). Each step's per-verb rule lives in
650    /// its own table, so every verb walks every step.
651    fn default_action(&mut self, verb: Verb, selector: &str, value: Option<&str>) {
652        for (element, event) in dom::focus_by(verb, selector) {
653            self.dispatch_node(element, event);
654        }
655        if let Some(value) = value {
656            dom::enter_value(verb, selector, value);
657        }
658    }
659
660    /// Dispatch `event` at the element `node` itself — one the host already holds, which a selector
661    /// need not name exactly ([`Driver::deliver`]).
662    fn dispatch_node(&mut self, node: i64, event: &str) {
663        self.deliver(node, event, "");
664    }
665
666    /// Dispatch `event` at the element `selector` names, carrying `value` ([`Driver::deliver`]).
667    fn dispatch(&mut self, selector: &str, event: &str, value: &str) -> Answer {
668        let node = dom::query_selector(selector);
669        if node < 0 {
670            return Answer::NoElement;
671        }
672        match self.deliver(node, event, value).0 {
673            true => Answer::Answered,
674            false => Answer::Unanswered,
675        }
676    }
677
678    /// The location moved under the page (a test's `Navigate to`, a submitted `get` form): every
679    /// handler the page registered with `onRoute` runs, in the order it registered them, as a
680    /// browser's `popstate` reaches every listener.
681    fn deliver_route_change(&mut self) {
682        for handler in dom::deliver_pop(&dom::route_path()) {
683            let args = event_args(&handler);
684            self.guarded(|c| c.call_value(&handler, args));
685        }
686    }
687
688    /// Move the page's clock forward by `ms`, as a browser's would over that much time: the page's
689    /// own timers through `__lgx_tick`, which fires what the time that passed makes due, in due
690    /// order; and every interval a program armed itself (`setInterval`), each time it comes due.
691    /// The page's clock is carried up to each interval before it fires, so the two kinds share one
692    /// timeline.
693    fn advance_clock(&mut self, ms: i64) {
694        let until = self.host.now_ms.saturating_add(ms.max(0));
695        loop {
696            self.adopt_intervals();
697            let next = self
698                .host
699                .armed
700                .iter()
701                .enumerate()
702                .filter(|(_, a)| a.period_ms > 0 && a.next_due_ms <= until)
703                .min_by_key(|(i, a)| (a.next_due_ms, *i))
704                .map(|(i, _)| i);
705            let Some(i) = next else { break };
706            let due = self.host.armed[i].next_due_ms;
707            self.pass_page_time(due - self.host.now_ms);
708            self.host.now_ms = due;
709            self.host.armed[i].next_due_ms += self.host.armed[i].period_ms;
710            let handler = self.host.armed[i].handler.clone();
711            let args = event_args(&handler);
712            self.guarded(|c| c.call_value(&handler, args));
713            self.settle();
714        }
715        self.pass_page_time(until - self.host.now_ms);
716        self.host.now_ms = until;
717    }
718
719    /// Hand the page's own clock the `ms` that passed (`__lgx_tick`), when it has one and time moved.
720    fn pass_page_time(&mut self, ms: i64) {
721        let tick = lgx_lower::tick_fn_name();
722        if ms <= 0 || !self.calls.has(tick) {
723            return;
724        }
725        self.guarded(|c| c.call(tick, vec![RuntimeValue::Int(ms)]));
726        self.settle();
727    }
728
729    /// Start the clock on every interval the page armed since the host last looked.
730    fn adopt_intervals(&mut self) {
731        let all = answers::intervals();
732        for interval in all.into_iter().skip(self.host.armed.len()) {
733            self.host.armed.push(Armed {
734                period_ms: interval.period_ms,
735                handler: interval.handler,
736                next_due_ms: self.host.now_ms + interval.period_ms,
737            });
738        }
739    }
740
741    /// Deliver a popstate the location owes the page — a step back taken outside any call into it —
742    /// then complete every request that has an answer, each through the completion closure the page
743    /// handed over with it, until no completion sends a request that has one.
744    fn settle(&mut self) {
745        if dom::take_popped() {
746            self.deliver_route_change();
747        }
748        loop {
749            self.adopt_intervals();
750            let fresh: Vec<dom::FetchRequest> = dom::fetch_requests()
751                .into_iter()
752                .filter(|r| !self.host.decided.contains(&r.req))
753                .collect();
754            if fresh.is_empty() {
755                return;
756            }
757            for r in fresh {
758                self.host.decided.insert(r.req);
759                if r.aborted {
760                    continue;
761                }
762                let Some((status, body)) = self.answer_for(&r) else {
763                    continue;
764                };
765                let closure = dom::begin_fetch_resolve(r.req, status, &[], &body);
766                if let Some(closure) = closure.filter(|c| matches!(c, RuntimeValue::Function(_))) {
767                    let args = vec![RuntimeValue::Int(r.req)];
768                    self.guarded(|c| c.call_value(&closure, args));
769                }
770                dom::end_fetch_resolve();
771            }
772        }
773    }
774
775    /// What answers `r`: a test's stub first, then — when the test asked for it — the program's
776    /// own route table.
777    fn answer_for(&mut self, r: &dom::FetchRequest) -> Option<(i64, String)> {
778        if let Some(answer) = dom::stubbed_answer(r.req) {
779            return Some(answer);
780        }
781        if !self.host.serve_routes {
782            return None;
783        }
784        let args = vec![text(&r.method), text(&r.url)];
785        let body = self
786            .guarded(|c| c.call("route", args.clone()))?
787            .to_display_string();
788        let status = if self.calls.has("routeStatus") {
789            match self.guarded(|c| c.call("routeStatus", args)) {
790                Some(RuntimeValue::Int(n)) => n,
791                _ => 200,
792            }
793        } else {
794            200
795        };
796        Some((status, body))
797    }
798
799    /// One call into the page through the host's single guarded door. The events the page's own
800    /// element commands raised during the call (`focus()` moving focus raises `blur` and `focus`)
801    /// are delivered as it returns, whether it answered or failed, and so is the `popstate` a
802    /// `Go back.` in it raised ([`dom::take_popped`]) — to every route handler the page registered;
803    /// a `Reload the page.` it asked for ([`dom::take_reload_request`]) is then carried out, as the
804    /// browser reloads once the running script is done; a failure is then handed to the page's
805    /// `When an action fails` handler, and one nothing absorbs is reported; and last, the document
806    /// the page asked to load ([`dom::take_load_request`]) replaces it, as the browser commits the
807    /// navigation once the script — its failure handler included — is done.
808    fn guarded(
809        &mut self,
810        call: impl FnOnce(&mut L) -> Result<RuntimeValue, String>,
811    ) -> Option<RuntimeValue> {
812        let outcome = match call(&mut *self.calls) {
813            Ok(v) => Ok(v),
814            Err(message) => Err(in_failure_context(&mut *self.calls, message)),
815        };
816        self.host.hold_contained_failure(&mut *self.calls);
817        self.deliver_raised_events();
818        if dom::take_popped() {
819            self.deliver_route_change();
820        }
821        // The frames this page published to its shared topics: a relay would carry them to every
822        // other page on the topic, and an in-process host drives this one page alone, so there is
823        // no other page to hand them to.
824        let _ = crate::semantics::storage::take_shared_writes();
825        if dom::take_reload_request() {
826            if let Err(DriveError::Failed(message) | DriveError::Refused(message)) =
827                self.drive(&Drive::Reload)
828            {
829                self.on_failure(message);
830            }
831        }
832        let answered = match outcome {
833            Ok(v) => Some(v),
834            Err(message) => {
835                self.on_failure(message);
836                None
837            }
838        };
839        if let Some(url) = dom::take_load_request() {
840            dom::assign_location(&url);
841        }
842        answered
843    }
844
845    /// Deliver every event an element command raised ([`dom::take_raised_events`]) to the element
846    /// it was raised on, until delivering them raises no more.
847    fn deliver_raised_events(&mut self) {
848        loop {
849            let raised = dom::take_raised_events();
850            if raised.is_empty() {
851                return;
852            }
853            for (element, event) in raised {
854                self.dispatch_node(element, event);
855            }
856        }
857    }
858
859    /// `__lgx_on_failure(message)` answers whether a handler absorbed the failure. A failure
860    /// nothing absorbs — no handler, or a handler that itself failed — is the page's to report.
861    fn on_failure(&mut self, message: String) {
862        let site = self.calls.failure_site();
863        let hook = lgx_lower::on_failure_fn_name();
864        let absorbed = self.calls.has(hook)
865            && matches!(
866                self.calls.call(hook, vec![text(&message)]),
867                Ok(RuntimeValue::Bool(true))
868            );
869        if !absorbed && self.host.unhandled.is_none() {
870            self.host.unhandled = Some(message);
871            self.host.unhandled_site = site;
872        }
873    }
874}
875
876/// `message`, named by what the page was doing when it trapped: the context the page left open
877/// (`__lgx_failure_context` — a directive's block, in the sentence every tier uses), taken before
878/// anything else runs in the page.
879fn in_failure_context<L: LoweredCalls + ?Sized>(calls: &mut L, message: String) -> String {
880    let taken = lgx_lower::failure_context_fn_name();
881    if !calls.has(taken) {
882        return message;
883    }
884    match calls.call(taken, Vec::new()) {
885        Ok(RuntimeValue::Text(context)) if !context.is_empty() => format!("{context}{message}"),
886        _ => message,
887    }
888}
889
890/// The element a focus or blur acts on, or the refusal the reference tier gives when there is
891/// none.
892fn require_element(selector: &str, verb: &str) -> Result<i64, DriveError> {
893    let node = dom::query_selector(selector);
894    if node < 0 {
895        return Err(DriveError::Failed(answers::element_required(
896            verb, selector,
897        )));
898    }
899    Ok(node)
900}
901
902/// What a host hands a handler it fires: the event, as one Int, when the handler takes one —
903/// `__logos_call_closure(handle, 0)` — and nothing when it takes none.
904fn event_args(handler: &RuntimeValue) -> Vec<RuntimeValue> {
905    match handler {
906        RuntimeValue::Function(c) if c.param_names.is_empty() => Vec::new(),
907        _ => vec![RuntimeValue::Int(0)],
908    }
909}
910
911fn text(s: &str) -> RuntimeValue {
912    RuntimeValue::Text(Rc::new(s.to_string()))
913}
914
915#[cfg(test)]
916#[path = "../../../logicaffeine_tests/unit/logicaffeine_compile/page_host/lowered/host_tests.rs"]
917mod host_tests;