1use 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
23pub 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 fn reload(&mut self) -> Result<(), String>;
35 fn lines(&self) -> &[String];
37 fn failure_site(&self) -> Option<u32> {
40 None
41 }
42 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#[derive(Clone, Debug, PartialEq, Eq)]
79pub enum DriveError {
80 Failed(String),
83 Refused(String),
85}
86
87struct Armed {
89 period_ms: i64,
90 handler: RuntimeValue,
91 next_due_ms: i64,
92}
93
94#[derive(Default)]
98pub struct LoweredHost {
99 now_ms: i64,
101 armed: Vec<Armed>,
102 decided: BTreeSet<i64>,
106 serve_routes: bool,
107 unhandled: Option<String>,
109 unhandled_site: Option<u32>,
111 mounted_component: Option<String>,
115 page: Option<String>,
118}
119
120impl LoweredHost {
121 pub fn new() -> Self {
123 LoweredHost::default()
124 }
125
126 pub fn of_page(page: Option<String>) -> Self {
130 LoweredHost {
131 mounted_component: page.clone(),
132 page,
133 ..LoweredHost::default()
134 }
135 }
136
137 pub fn unhandled_failure(&self) -> Option<&str> {
139 self.unhandled.as_deref()
140 }
141
142 pub fn take_unhandled_failure(&mut self) -> Option<String> {
145 self.unhandled.take()
146 }
147
148 pub fn unhandled_failure_site(&self) -> Option<u32> {
150 self.unhandled_site
151 }
152
153 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 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 pub fn settle<L: LoweredCalls + ?Sized>(&mut self, calls: &mut L) {
186 Driver { host: self, calls }.settle();
187 }
188
189 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
200pub struct LoweredPage<L: LoweredCalls> {
203 calls: L,
204 host: LoweredHost,
205}
206
207impl<L: LoweredCalls> LoweredPage<L> {
208 pub fn new(calls: L) -> Self {
210 LoweredPage {
211 calls,
212 host: LoweredHost::new(),
213 }
214 }
215
216 pub fn calls(&mut self) -> &mut L {
218 &mut self.calls
219 }
220
221 pub fn unhandled_failure(&self) -> Option<&str> {
223 self.host.unhandled_failure()
224 }
225
226 pub fn output(&self) -> String {
230 self.calls.lines().join("\n")
231 }
232
233 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 pub fn drive(&mut self, d: &Drive) -> Result<(), DriveError> {
244 self.host.drive(&mut self.calls, d)
245 }
246}
247
248struct Driver<'d, L: ?Sized> {
251 host: &'d mut LoweredHost,
252 calls: &'d mut L,
253}
254
255impl<L: LoweredCalls + ?Sized> Driver<'_, L> {
256 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 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 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 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 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 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 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 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 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 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 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 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 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 fn dispatch_node(&mut self, node: i64, event: &str) {
663 self.deliver(node, event, "");
664 }
665
666 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 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 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 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 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 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 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 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 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 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 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
876fn 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
890fn 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
902fn 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;