Skip to main content

logicaffeine_language/parser/lgx_lower/
events.rs

1use super::sites::split_keyed_listener_event;
2use super::*;
3use crate::ast::component::{ComponentDef, Trigger};
4use crate::ast::stmt::{BinaryOpKind, Expr, Literal, Stmt, TypeExpr};
5use crate::ast::view::BoundInput;
6use crate::lgx::html::EventModifier;
7use logicaffeine_base::Symbol;
8
9/// One attached listener, as the dispatcher needs to call it back.
10///
11/// ★ IT CARRIES WHAT TO CALL, NOT A CLOSURE. A module cannot hold a listener closure on the browser
12/// tier — `Map of Text to fn(…)` is refused by that backend ("unsupported indirect call"), measured
13/// rather than assumed. But the dispatcher never needed the closure: it needs to know WHICH handler
14/// and FOR WHICH INSTANCE, and both of those are data. The closure stays where it belongs, on the
15/// element, for the browser to call; this is the parallel record a driving verb resolves through.
16#[derive(Clone)]
17pub enum ListenerSite {
18    /// `on-<event>={action}` and `When "<sel>" is <event>:` — run that action's turn.
19    ///
20    /// `key` is the KEY MODIFIER this listener waits for (`on-keydown.enter` → `enter`), or
21    /// `None` for a handler that answers any press. It is compared case-insensitively against
22    /// the key the dispatch carries, because a browser reports `KeyboardEvent.key` as `"Enter"`
23    /// while the author writes `.enter` — the reason the two ends meet at all.
24    Action {
25        component: Symbol,
26        action: Symbol,
27        argc: usize,
28        key: Option<Symbol>,
29        /// Whether the handler's LAST parameter is what the event CARRIED rather than something
30        /// captured where it was rendered. A dropped file's name is not state and not a prop — it
31        /// arrives with the event — so the arm reads the dispatcher's value slot for it.
32        carries_value: bool,
33        /// For an `on-<event>.once` handler, the render's local holding the listener's identity
34        /// (see [`once_spent_binding`]); the registration records it against the listener key and
35        /// the arm spends it.
36        once: Option<Symbol>,
37    },
38    /// `bind={field}` — store the delivered value, then repaint.
39    Bind {
40        component: Symbol,
41        field: Symbol,
42        checked: bool,
43    },
44    /// A click on a bound checkbox or radio. Not a listener the page wrote: it is the BROWSER's
45    /// default action, which happens before any handler runs, and an in-process host performs it
46    /// itself (`page_host`'s `Driver::toggle`). Registered so the node-keyed lookup finds it like
47    /// any other.
48    DefaultToggle {
49        component: Symbol,
50        field: Symbol,
51        radio: bool,
52    },
53    /// `on-<event>={<callbackProp>}` — a component's own element calling a callback it was HANDED
54    /// rather than one of its own actions (`docs/frontend.md`'s "Passing actions down"). The arm
55    /// calls the prop's trampoline (`Parser::callback_trampoline`) for the registering instance,
56    /// with the `argc` arguments the render recorded; `key` is the key modifier, as on
57    /// [`Self::Action`].
58    CallbackProp {
59        component: Symbol,
60        prop: Symbol,
61        argc: usize,
62        key: Option<Symbol>,
63        once: Option<Symbol>,
64    },
65    /// A `scroll` the page heard where `When the user scrolls to the bottom:` waits: the arm asks
66    /// the geometry how far the scrolled node is from its end, and runs the handler's turn only
67    /// within 32px of it — the threshold is compiled LOGOS, the host only measures.
68    ScrollNear { component: Symbol, action: Symbol },
69}
70
71impl ListenerSite {
72    /// The render-local `.once` identity this site records at registration, if it is `.once`.
73    pub(crate) fn once_local(&self) -> Option<Symbol> {
74        match self {
75            ListenerSite::Action { once, .. } | ListenerSite::CallbackProp { once, .. } => *once,
76            ListenerSite::Bind { .. }
77            | ListenerSite::DefaultToggle { .. }
78            | ListenerSite::ScrollNear { .. } => None,
79        }
80    }
81
82    /// The one key this site answers, when it waits for one.
83    pub(crate) fn waits_for(&self) -> Option<Symbol> {
84        match self {
85            ListenerSite::Action { key, .. } | ListenerSite::CallbackProp { key, .. } => *key,
86            ListenerSite::Bind { .. }
87            | ListenerSite::DefaultToggle { .. }
88            | ListenerSite::ScrollNear { .. } => None,
89        }
90    }
91}
92
93/// The drag events a `When a file is dropped on "<sel>":` target cancels at registration. A browser
94/// makes an element a drop target only while `dragenter` and `dragover` are cancelled on it, and
95/// performs its own default for an uncancelled `drop` (it opens the file in the tab).
96pub const DROP_TARGET_CANCELS: &[&str] = &["dragenter", "dragover", "drop"];
97
98/// A callback's TAG: the position of `(component, action)` among every action the program declares,
99/// component by component in declaration order. One global numbering, so the placement that records
100/// a tag and the trampoline that reads it back agree without either knowing the other's signature.
101pub(crate) fn action_tag(
102    all: &[ComponentDef<'_>],
103    component: Symbol,
104    action: Symbol,
105) -> Option<i64> {
106    all.iter()
107        .flat_map(|c| c.actions.iter().map(move |a| (c.name, a.name)))
108        .position(|(c, a)| c == component && a == action)
109        .map(|i| i as i64)
110}
111
112/// Every `(tag, component, action)` among `owners` whose parameters match `signature` — the arms of
113/// a callback trampoline. An action of another shape cannot be what the callback means: the
114/// placement gate refuses binding one.
115pub(crate) fn callback_candidates<'a>(
116    all: &[ComponentDef<'a>],
117    owners: &[Symbol],
118    signature: &[&'a TypeExpr<'a>],
119) -> Vec<(i64, Symbol, Symbol)> {
120    let mut out = Vec::new();
121    let mut tag = 0i64;
122    for c in all {
123        for a in &c.actions {
124            let fits = owners.contains(&c.name)
125                && a.params.len() == signature.len()
126                && a.params
127                    .iter()
128                    .zip(signature.iter())
129                    .all(|((_, p), s)| param_shapes_match(p, s));
130            if fits {
131                out.push((tag, c.name, a.name));
132            }
133            tag += 1;
134        }
135    }
136    out
137}
138
139impl<'a, 'ctx, 'int> super::super::Parser<'a, 'ctx, 'int> {
140    /// The identity a placement records for each callback prop it hands down, against the CHILD's
141    /// instance key: which action the prop means (its [`action_tag`]) and whose instance that action
142    /// runs against. A value naming one of `owner`'s actions records that action and `owner`'s own
143    /// key; a value forwarding one of `owner`'s own callback props copies through the identity
144    /// `owner` was itself handed. `callback_binding_refusal` has already refused every other value.
145    pub(crate) fn callback_prop_registration(
146        &mut self,
147        value: &'a Expr<'a>,
148        owner: &ComponentDef<'a>,
149        child: Symbol,
150        prop: Symbol,
151        child_key: &'a Expr<'a>,
152        all: &[ComponentDef<'a>],
153    ) -> Vec<Stmt<'a>> {
154        let Some(child_def) = all.iter().find(|d| d.name == child) else {
155            return Vec::new();
156        };
157        let Some((_, ty)) = child_def.props.iter().find(|(p, _)| *p == prop).copied() else {
158            return Vec::new();
159        };
160        if !is_callback_type(ty, self.interner) {
161            return Vec::new();
162        }
163        let (tag_expr, key_expr) = match callback_source(value, owner, self.interner) {
164            Some(CallbackSource::Action(action)) => {
165                let Some(tag) = action_tag(all, owner.name, action.name) else {
166                    return Vec::new();
167                };
168                (self.te_int(tag), self.key_expr())
169            }
170            Some(CallbackSource::Forwarded(own)) => {
171                let oname = self.interner.resolve(owner.name).to_string();
172                let pname = self.interner.resolve(own).to_string();
173                (
174                    self.callback_identity_read(&oname, &pname, true),
175                    self.callback_identity_read(&oname, &pname, false),
176                )
177            }
178            None => return Vec::new(),
179        };
180        let component = self.interner.resolve(child).to_string();
181        let p = self.interner.resolve(prop).to_string();
182        let tag_map = self.interner.intern(&callback_tag_binding(&component, &p));
183        let key_map = self.interner.intern(&callback_key_binding(&component, &p));
184        vec![
185            Stmt::SetIndex {
186                collection: self.te_id(tag_map),
187                index: child_key,
188                value: tag_expr,
189            },
190            Stmt::SetIndex {
191                collection: self.te_id(key_map),
192                index: child_key,
193                value: key_expr,
194            },
195        ]
196    }
197
198    /// `item <this render's own key> of __P_<Component>_<prop>_TAG` (or `_KEY`) — this instance's
199    /// own recorded identity for one of its callback props. `want_tag` selects which of the pair.
200    fn callback_identity_read(
201        &mut self,
202        component: &str,
203        prop: &str,
204        want_tag: bool,
205    ) -> &'a Expr<'a> {
206        let name = if want_tag {
207            callback_tag_binding(component, prop)
208        } else {
209            callback_key_binding(component, prop)
210        };
211        let global = self.interner.intern(&name);
212        let key = self.key_expr();
213        self.ctx.alloc_imperative_expr(Expr::Index {
214            collection: self.te_id(global),
215            index: key,
216        })
217    }
218
219    /// Every module-level binding `def`'s callback props need: the identity pair for each one
220    /// (`__P_<C>_<prop>_TAG`/`_KEY`, keyed by instance), and — for a prop an element of `def` calls
221    /// as a handler — one map per parameter, keyed by listener site, where the render records the
222    /// arguments it evaluated ([`callback_arg_binding`], the twin of [`listener_arg_binding`]).
223    ///
224    /// Declared whether or not `def` is `stateful`: a component with no `State:` and no `Actions:`
225    /// still calls the callbacks it was handed (`docs/frontend.md`'s "Passing actions down").
226    pub(crate) fn callback_prop_declarations(
227        &mut self,
228        def: &ComponentDef<'a>,
229        all: &[ComponentDef<'a>],
230    ) -> Vec<Stmt<'a>> {
231        let mut out = Vec::new();
232        let component = self.interner.resolve(def.name).to_string();
233        let map_of = self.interner.intern("mapOf");
234        let root = self.interner.intern("");
235        for (prop, _) in callback_props(def, self.interner) {
236            let p = self.interner.resolve(prop).to_string();
237            let tag_var = self.interner.intern(&callback_tag_binding(&component, &p));
238            let tag_value = self.te_call(map_of, vec![self.te_text(root), self.te_int(0)]);
239            out.push(Stmt::Let {
240                var: tag_var,
241                ty: None,
242                value: tag_value,
243                mutable: true,
244            });
245            let key_var = self.interner.intern(&callback_key_binding(&component, &p));
246            let key_value = self.te_call(map_of, vec![self.te_text(root), self.te_text(root)]);
247            out.push(Stmt::Let {
248                var: key_var,
249                ty: None,
250                value: key_value,
251                mutable: true,
252            });
253        }
254        for (var, ty) in self.callback_arg_maps(def, all) {
255            let seed = self.zero_of_type(ty);
256            let value = self.te_call(map_of, vec![self.te_int(0), seed]);
257            out.push(Stmt::Let {
258                var,
259                ty: None,
260                value,
261                mutable: true,
262            });
263        }
264        out
265    }
266
267    /// The `_TAG`/`_KEY` identity maps of every callback prop `def` declares — keyed by instance, for
268    /// `sweep_gone` to retire a gone instance's entries from, beside its state and prop maps.
269    pub(crate) fn callback_prop_identity_map_names(
270        &mut self,
271        def: &ComponentDef<'a>,
272    ) -> Vec<Symbol> {
273        let mut out = Vec::new();
274        let component = self.interner.resolve(def.name).to_string();
275        for (prop, _) in callback_props(def, self.interner) {
276            let p = self.interner.resolve(prop).to_string();
277            out.push(self.interner.intern(&callback_tag_binding(&component, &p)));
278            out.push(self.interner.intern(&callback_key_binding(&component, &p)));
279        }
280        out
281    }
282
283    /// `__ARG_<C>_<prop>_cb<i>` for every parameter of every callback prop `def`'s own elements call
284    /// as a handler, with the parameter's type. Keyed by LISTENER SITE (an `Int`), like
285    /// [`listener_arg_binding`]'s maps — so `lower_component_render`'s retire loop removes a stale
286    /// site's entries from these exactly as it does from an action's.
287    pub(crate) fn callback_arg_maps(
288        &mut self,
289        def: &ComponentDef<'a>,
290        all: &[ComponentDef<'a>],
291    ) -> Vec<(Symbol, &'a TypeExpr<'a>)> {
292        let mut out = Vec::new();
293        let component = self.interner.resolve(def.name).to_string();
294        for prop in callback_prop_targets(def) {
295            let Some(signature) = callback_signature(all, def.name, prop, self.interner) else {
296                continue;
297            };
298            let p = self.interner.resolve(prop).to_string();
299            for (i, ty) in signature.into_iter().enumerate() {
300                out.push((
301                    self.interner
302                        .intern(&callback_arg_binding(&component, &p, i)),
303                    ty,
304                ));
305            }
306        }
307        out
308    }
309
310    /// `__cb_<C>_<prop>(key, a…)` — run whatever action instance `key`'s callback prop `prop` means.
311    ///
312    /// ★ ONE CALL PATH FOR EVERY CALLER. A browser's listener, a driving verb's dispatch and a turn
313    /// that calls the prop all arrive here with the instance key: the identity the placement
314    /// recorded names the action by its tag and the instance it runs against by key, and an arm per
315    /// candidate calls that action's turn. No closure is ever handed to a render, stored in a map, or
316    /// found at run time — which is what lets one child be placed with two different actions, repaint
317    /// with its callbacks intact, and build for the browser. An instance with no recorded identity
318    /// runs nothing.
319    pub(crate) fn callback_trampoline(
320        &mut self,
321        def: &ComponentDef<'a>,
322        prop: Symbol,
323        all: &[ComponentDef<'a>],
324        owners: &[Symbol],
325    ) -> Stmt<'a> {
326        let component = self.interner.resolve(def.name).to_string();
327        let p = self.interner.resolve(prop).to_string();
328        let signature = callback_signature(all, def.name, prop, self.interner).unwrap_or_default();
329        let key = self.interner.intern(instance_key_param());
330        let tag_map = self.interner.intern(&callback_tag_binding(&component, &p));
331        let key_map = self.interner.intern(&callback_key_binding(&component, &p));
332        let known = self.ctx.alloc_imperative_expr(Expr::Contains {
333            collection: self.te_id(tag_map),
334            value: self.te_id(key),
335        });
336        let unknown = self.ctx.alloc_imperative_expr(Expr::Not { operand: known });
337        let mut body = vec![Stmt::If {
338            cond: unknown,
339            then_block: self.alloc_block(vec![Stmt::Return { value: None }]),
340            else_block: None,
341        }];
342        let tag_local = self.interner.intern("__lgx_cbtag");
343        let tag_read = self.ctx.alloc_imperative_expr(Expr::Index {
344            collection: self.te_id(tag_map),
345            index: self.te_id(key),
346        });
347        body.push(Stmt::Let {
348            var: tag_local,
349            ty: None,
350            value: tag_read,
351            mutable: false,
352        });
353        let owner_local = self.interner.intern("__lgx_cbkey");
354        let owner_read = self.ctx.alloc_imperative_expr(Expr::Index {
355            collection: self.te_id(key_map),
356            index: self.te_id(key),
357        });
358        body.push(Stmt::Let {
359            var: owner_local,
360            ty: None,
361            value: owner_read,
362            mutable: false,
363        });
364        let mut params = vec![(key, self.text_type())];
365        let mut call_args = vec![self.te_id(owner_local)];
366        for (i, ty) in signature.iter().enumerate() {
367            let name = self.interner.intern(&format!("__lgx_cb{i}"));
368            params.push((name, *ty));
369            call_args.push(self.te_id(name));
370        }
371        let mut chain: Vec<Stmt<'a>> = Vec::new();
372        for (tag, owner_c, owner_a) in callback_candidates(all, owners, &signature)
373            .into_iter()
374            .rev()
375        {
376            let oc = self.interner.resolve(owner_c).to_string();
377            let oa = self.interner.resolve(owner_a).to_string();
378            let act = self.interner.intern(&action_fn_name(&oc, &oa));
379            let cond = self.te_bin(BinaryOpKind::Eq, self.te_id(tag_local), self.te_int(tag));
380            let run = self.alloc_block(vec![Stmt::Call {
381                function: act,
382                args: call_args.clone(),
383            }]);
384            let otherwise = if chain.is_empty() {
385                None
386            } else {
387                Some(self.alloc_block(chain))
388            };
389            chain = vec![Stmt::If {
390                cond,
391                then_block: run,
392                else_block: otherwise,
393            }];
394        }
395        body.extend(chain);
396        let name = self
397            .interner
398            .intern(&callback_trampoline_fn_name(&component, &p));
399        Stmt::FunctionDef {
400            name,
401            generics: Vec::new(),
402            params,
403            body: self.alloc_block(body),
404            return_type: None,
405            is_native: false,
406            native_path: None,
407            is_exported: false,
408            export_target: None,
409            opt_flags: crate::optimization::OptimizationConfig::all_on(),
410        }
411    }
412
413    /// The VALUE of `component`'s callback prop `prop` for the instance in scope — a closure over
414    /// that prop's trampoline, bound to this instance's key. What a turn binds the prop's name to,
415    /// so an action body reads a callback prop the way it reads any other.
416    pub(crate) fn callback_invoker(
417        &mut self,
418        def: &ComponentDef<'a>,
419        prop: Symbol,
420    ) -> &'a Expr<'a> {
421        let all = self.lgx_all_components.clone();
422        let signature = callback_signature(&all, def.name, prop, self.interner).unwrap_or_default();
423        let component = self.interner.resolve(def.name).to_string();
424        let p = self.interner.resolve(prop).to_string();
425        let trampoline = self
426            .interner
427            .intern(&callback_trampoline_fn_name(&component, &p));
428        let mut params = Vec::new();
429        let mut args = vec![self.key_expr()];
430        for (i, ty) in signature.into_iter().enumerate() {
431            let name = self.interner.intern(&format!("__lgx_cb{i}"));
432            params.push((name, ty));
433            args.push(self.te_id(name));
434        }
435        let body = self.alloc_block(vec![Stmt::Call {
436            function: trampoline,
437            args,
438        }]);
439        self.ctx.alloc_imperative_expr(Expr::Closure {
440            params,
441            body: crate::ast::stmt::ClosureBody::Block(body),
442            return_type: None,
443        })
444    }
445
446    /// `__lgx_dispatch(selector, event, value) -> Bool` — did anything answer that act? Emitted in
447    /// two halves: this one finds the element, and `__lgx_dispatch_node(node, event, value)` —
448    /// pushed beside it — raises the event on the element itself, the half a host holding the
449    /// element calls directly.
450    ///
451    /// ★★ IT RESOLVES THE NODE AND LOOKS IT UP. It used to be a table of STATIC arms, one per
452    /// `(selector text, event)` the program literally wrote, and the walk that built them emitted an
453    /// arm only where the element carried a LITERAL `id`. So a driving verb could not reach an
454    /// element identified by a class, or by a computed id — which is every row of every list — and a
455    /// page the browser runs perfectly could not be driven by a test.
456    ///
457    /// ★ AND ONCE A COMPONENT COULD BE PLACED TWICE IT WAS WORSE THAN INCOMPLETE. A selector is a
458    /// way to FIND an element, not an identity: two rows rendered from one component carry the same
459    /// markup, so a selector cannot say which was clicked. The static table resolved to the
460    /// COMPONENT and ran against the ROOT instance — finding the element, picking the wrong record,
461    /// and painting. A loud gap had rotted into a silent wrong answer because a feature landed
462    /// beneath it, with nothing edited and nothing turning red.
463    ///
464    /// Now: resolve the selector to a NODE at run time, look `"{node}:{event}"` up in the table each
465    /// listener registered as it was attached, and run the handler it names against the instance key
466    /// recorded beside it. Matching by the element itself answers both questions at once.
467    ///
468    /// ★ NO NEW BUILTIN AND NO HOST IMPORT. The old premise — this tier cannot call a closure it
469    /// found at run time — is TRUE and stays true: a `Map of Text to fn(…)` is refused by the wasm
470    /// backend, measured. But it never needed the closure. It needed to know WHICH handler and FOR
471    /// WHICH INSTANCE, and both of those are data.
472    pub(crate) fn lower_lgx_dispatcher(&mut self, all: &[ComponentDef<'a>]) -> Option<Stmt<'a>> {
473        // An empty listener set still yields a dispatcher — one that answers `false` to
474        // everything, which is the truth and is callable. See the note at the emission site.
475        if self.lgx_components.is_empty() {
476            return None;
477        }
478        if all
479            .iter()
480            .any(|d| self.lgx_components.contains(&d.name) && has_any_listener_site(d))
481        {
482            let forget = self.forget_listener_fn(all);
483            self.deferred_functions.push(forget);
484        }
485        // ★ WHAT EVERY PAINT CALLS AFTER ITS COMMIT, emitted once: the commit itself, the fragment
486        // registry and the sweep of the instances a commit removed, and the drain of the directives
487        // a paint drew.
488        let commit = self.lower_lgx_commit();
489        self.deferred_functions.push(commit);
490        if self.instances_can_leave() {
491            self.declare_frag_registry();
492            let own = self.lower_lgx_own_frag();
493            self.deferred_functions.push(own);
494            let gone = self.lower_lgx_gone();
495            self.deferred_functions.push(gone);
496        }
497        if !self.drawn_directives().is_empty() {
498            self.declare_directive_queues();
499            let drain = self.lower_lgx_drain_directives();
500            self.deferred_functions.push(drain);
501        }
502        let selector = self.interner.intern("__lgx_sel");
503        let event = self.interner.intern("__lgx_ev");
504        let node = self.interner.intern("__lgx_node");
505        let lookup = self.interner.intern("__lgx_lk");
506        let id = self.interner.intern("__lgx_id");
507        let key = self.interner.intern(instance_key_param());
508        let mut body: Vec<Stmt<'a>> = Vec::new();
509
510        if uses_outbox(all) {
511            body.push(self.outbox_flush_arm(event));
512        }
513
514        // ★ A MESSAGE IS ONE-TO-MANY AND THE TABLE BELOW IS ONE-TO-ONE. Every other event this
515        // dispatcher routes resolves an ELEMENT, and an element has one handler for one event, so
516        // the `(node, event) -> listener` table below holds exactly one id and one instance key.
517        // A subscription is the opposite shape by definition: every live instance of every
518        // component that named the topic is meant to hear it. Registering them all into a
519        // one-slot table meant the LAST one registered answered and the rest heard nothing — two
520        // `<Tile/>`s in a list, one `Publish`, and only the second reset. It needed two instances
521        // to show, so every single-instance parity test passed while the feature was half-built.
522        //
523        // The fan-out belongs HERE rather than at `Publish`, because `Raise "message:<topic>"`
524        // from a `## Test` arrives the same way and is owed the same delivery. The browser's two
525        // halves already fan out on their own — `addEventListener` keeps a list, and the relay
526        // keeps `subs` per topic — so this is the in-page half catching up with them, not a new
527        // rule.
528        //
529        // It runs BEFORE the table lookup and returns, so a subscription never consults the slot
530        // it shares; and it walks `__KEYS_<C>`, which the unmount sweep keeps to exactly the
531        // instances on the page, so an instance that has left is not delivered to — skipping only
532        // the seed key, whose stamp is `-1`.
533        let val = self.lgx_value_param();
534        let mut topics: Vec<(String, Vec<(Symbol, usize)>)> = Vec::new();
535        for d in all.iter().filter(|d| keeps_instance_registry(d)) {
536            for (index, handler) in d.events.iter().enumerate() {
537                let Trigger::Message { topic, .. } = &handler.trigger else {
538                    continue;
539                };
540                let Expr::Literal(Literal::Text(t)) = topic else {
541                    continue;
542                };
543                let name = self.interner.resolve(*t).to_string();
544                match topics.iter_mut().find(|(n, _)| *n == name) {
545                    Some((_, subs)) => subs.push((d.name, index)),
546                    None => topics.push((name, vec![(d.name, index)])),
547                }
548            }
549        }
550        for (topic, subs) in topics {
551            let ev_name = self.interner.intern(&format!("message:{topic}"));
552            let is_it = self.te_bin(BinaryOpKind::Eq, self.te_id(event), self.te_text(ev_name));
553            let mut fan: Vec<Stmt<'a>> = Vec::new();
554            for (component, index) in subs {
555                let name = self.interner.resolve(component).to_string();
556                let keys = self.interner.intern(&keys_binding(&name));
557                let seen = self.interner.intern(&seen_binding(&name));
558                let k = self.interner.intern("__lgx_mk");
559                let stamp = self.ctx.alloc_imperative_expr(Expr::Index {
560                    collection: self.te_id(seen),
561                    index: self.te_id(k),
562                });
563                let live = self.te_bin(BinaryOpKind::NotEq, stamp, self.te_int(-1));
564                let act = self
565                    .interner
566                    .intern(&action_fn_name(&name, &format!("__e{index}")));
567                let call = Stmt::Call {
568                    function: act,
569                    args: vec![self.te_id(k), self.te_id(val)],
570                };
571                let then_block = self.alloc_block(vec![call]);
572                let inner = self.alloc_block(vec![Stmt::If {
573                    cond: live,
574                    then_block,
575                    else_block: None,
576                }]);
577                fan.push(Stmt::Repeat {
578                    pattern: crate::ast::stmt::Pattern::Identifier(k),
579                    iterable: self.te_id(keys),
580                    body: inner,
581                });
582            }
583            fan.push(Stmt::Return {
584                value: Some(self.te_bool(true)),
585            });
586            body.push(Stmt::If {
587                cond: is_it,
588                then_block: self.alloc_block(fan),
589                else_block: None,
590            });
591        }
592
593        // ★ A SELECTOR FINDS THE ELEMENT; THE EVENT HAPPENS TO THE ELEMENT. Everything from here on
594        // is the node half, `__lgx_dispatch_node(node, event, value)`, which a host that already
595        // holds the element — the one a focus change blurred or focused — calls directly. This half
596        // resolves the selector and hands the node over.
597        let query = self.interner.intern("querySelector");
598        let dispatch_node = self.interner.intern(dispatch_node_fn_name());
599        let found = self.te_call(query, vec![self.te_id(selector)]);
600        let carried = self.lgx_value_param();
601        let handed = self.te_call(
602            dispatch_node,
603            vec![found, self.te_id(event), self.te_id(carried)],
604        );
605        body.push(Stmt::Return {
606            value: Some(handed),
607        });
608        let selector_body = std::mem::take(&mut body);
609        // ★ THE EVENT NAME BECOMES ITS CODE, ONCE. Registration built this key from a name known
610        // at compile time; dispatch has only the runtime name, so it is turned back into a code by
611        // a chain over exactly the events this program registers. Once per dispatch — against once
612        // per listener per paint, which is what building a `"{node}:{event}"` string cost.
613        //
614        // ★ A LISTENER WAITING FOR ONE KEY HAS A CODE OF ITS OWN (`keyed_listener_event`), so one
615        // element can wait for Enter and for Escape at once: each keyed listener is its own table
616        // entry, found by the event AND the key the host delivered. A press answers the listener
617        // waiting for its key, and then the one waiting for any key — both, as every listener the
618        // press reaches runs.
619        let code = self.interner.intern("__lgx_code");
620        let keyed = self.interner.intern("__lgx_keyed");
621        body.push(Stmt::Let {
622            var: code,
623            ty: None,
624            value: self.te_int(-1),
625            mutable: true,
626        });
627        body.push(Stmt::Let {
628            var: keyed,
629            ty: None,
630            value: self.te_int(-1),
631            mutable: true,
632        });
633        let value = self.lgx_value_param();
634        let lower = self.interner.intern("lower");
635        for (i, entry) in self.lgx_listener_events.clone().iter().enumerate() {
636            let entry = self.interner.resolve(*entry).to_string();
637            let (cond, target) = match split_keyed_listener_event(&entry) {
638                Some((name, want)) => {
639                    let name = self.interner.intern(name);
640                    let want = self.interner.intern(want);
641                    let is_event =
642                        self.te_bin(BinaryOpKind::Eq, self.te_id(event), self.te_text(name));
643                    let pressed = self.te_call(lower, vec![self.te_id(value)]);
644                    let is_key = self.te_bin(BinaryOpKind::Eq, pressed, self.te_text(want));
645                    (self.te_bin(BinaryOpKind::And, is_event, is_key), keyed)
646                }
647                None => {
648                    let name = self.interner.intern(&entry);
649                    (
650                        self.te_bin(BinaryOpKind::Eq, self.te_id(event), self.te_text(name)),
651                        code,
652                    )
653                }
654            };
655            let set = Stmt::Set {
656                target,
657                value: self.te_int(i as i64),
658            };
659            body.push(Stmt::If {
660                cond,
661                then_block: self.alloc_block(vec![set]),
662                else_block: None,
663            });
664        }
665        let answer = self.interner.intern(answer_fn_name());
666        let answered = self.interner.intern("__lgx_answered");
667        body.push(Stmt::Let {
668            var: answered,
669            ty: None,
670            value: self.te_bool(false),
671            mutable: true,
672        });
673        // ★ EVERY LISTENER THE EVENT REACHES, IN THE ORDER THEY REGISTERED: on the keyed slot and
674        // then the plain one, the listener the slot names and then each that spilled after it. The
675        // whole queue is read before any of them runs, as a browser fixes an event's listeners when
676        // it starts delivering it: a listener whose turn redraws the element leaves the rest of
677        // this delivery as it found it, and one that turn retired answers nothing.
678        //
679        // ★ THE SLOT SPACE IS THE PROGRAM'S OWN ENTRY COUNT, known here, where every entry is: a
680        // slot `site * __LISTENER_SPACE + code` never reaches the next site's slots however many
681        // events and keys the program listens for. Declared FIRST among the deferred statements, so
682        // it precedes every function that reads it.
683        //
684        // ★ THE HOST NAMES THE NODE; THE LISTENERS ARE KEYED BY ITS SITE. The retained tree knows
685        // the node by the id the host does (`vdNodeOfWire`) and the site it carries (`vdSiteOf`) —
686        // the site every listener on it registered against — and a node with no site answers
687        // nothing.
688        if !self.lgx_listener_events.is_empty() {
689            let space = self.interner.intern(listener_space_binding());
690            let entries = self.te_int(self.lgx_listener_events.len() as i64);
691            self.deferred_functions.insert(
692                0,
693                Stmt::Let {
694                    var: space,
695                    ty: None,
696                    value: entries,
697                    mutable: false,
698                },
699            );
700            let more = self.interner.intern(listener_spill_binding());
701            let queue = self.interner.intern("__lgx_queue");
702            let int_name = self.interner.intern("Int");
703            let mut queued: Vec<Stmt<'a>> = Vec::new();
704            queued.push(Stmt::Let {
705                var: queue,
706                ty: None,
707                value: self.new_seq_expr(int_name),
708                mutable: true,
709            });
710            let anchor = self.interner.intern("__lgx_anchor");
711            let vnode = self.vd_expr("vdNodeOfWire", vec![self.te_id(node)]);
712            let site = self.vd_expr("vdSiteOf", vec![vnode]);
713            body.push(Stmt::Let {
714                var: anchor,
715                ty: None,
716                value: site,
717                mutable: false,
718            });
719            let plain_slot = self.interner.intern("__lgx_cs");
720            let scaled = self.te_bin(
721                BinaryOpKind::Multiply,
722                self.te_id(anchor),
723                self.te_id(space),
724            );
725            let composed = self.te_bin(BinaryOpKind::Add, scaled, self.te_id(code));
726            body.push(Stmt::Let {
727                var: plain_slot,
728                ty: None,
729                value: composed,
730                mutable: false,
731            });
732            for (which, slot, spilled) in [
733                (keyed, "__lgx_ks", "__lgx_ksp"),
734                (code, "__lgx_cs", "__lgx_csp"),
735            ] {
736                let slot = self.interner.intern(slot);
737                let spilled = self.interner.intern(spilled);
738                let scaled = self.te_bin(
739                    BinaryOpKind::Multiply,
740                    self.te_id(anchor),
741                    self.te_id(space),
742                );
743                let composed = self.te_bin(BinaryOpKind::Add, scaled, self.te_id(which));
744                let has_more = self.ctx.alloc_imperative_expr(Expr::Contains {
745                    collection: self.te_id(more),
746                    value: self.te_id(slot),
747                });
748                let after = self.ctx.alloc_imperative_expr(Expr::Index {
749                    collection: self.te_id(more),
750                    index: self.te_id(slot),
751                });
752                let each_after = Stmt::Repeat {
753                    pattern: crate::ast::stmt::Pattern::Identifier(spilled),
754                    iterable: after,
755                    body: self.alloc_block(vec![Stmt::Push {
756                        value: self.te_id(spilled),
757                        collection: self.te_id(queue),
758                    }]),
759                };
760                let mut enqueue = Vec::new();
761                if slot != plain_slot {
762                    enqueue.push(Stmt::Let {
763                        var: slot,
764                        ty: None,
765                        value: composed,
766                        mutable: false,
767                    });
768                }
769                enqueue.push(Stmt::Push {
770                    value: self.te_id(slot),
771                    collection: self.te_id(queue),
772                });
773                enqueue.push(Stmt::If {
774                    cond: has_more,
775                    then_block: self.alloc_block(vec![each_after]),
776                    else_block: None,
777                });
778                let registered = self.te_bin(BinaryOpKind::GtEq, self.te_id(which), self.te_int(0));
779                queued.push(Stmt::If {
780                    cond: registered,
781                    then_block: self.alloc_block(enqueue),
782                    else_block: None,
783                });
784            }
785            let each = self.interner.intern("__lgx_each");
786            let held = self.interner.intern(listener_held_binding());
787            let left = self.interner.intern(listener_left_binding());
788            let holds = self.ctx.alloc_imperative_expr(Expr::Index {
789                collection: self.te_id(held),
790                index: self.te_id(each),
791            });
792            let already = self.ctx.alloc_imperative_expr(Expr::Contains {
793                collection: self.te_id(held),
794                value: self.te_id(each),
795            });
796            let one_more = self.te_bin(BinaryOpKind::Add, holds, self.te_int(1));
797            let hold = Stmt::If {
798                cond: already,
799                then_block: self.alloc_block(vec![Stmt::SetIndex {
800                    collection: self.te_id(held),
801                    index: self.te_id(each),
802                    value: one_more,
803                }]),
804                else_block: Some(self.alloc_block(vec![Stmt::SetIndex {
805                    collection: self.te_id(held),
806                    index: self.te_id(each),
807                    value: self.te_int(1),
808                }])),
809            };
810            queued.push(Stmt::Repeat {
811                pattern: crate::ast::stmt::Pattern::Identifier(each),
812                iterable: self.te_id(queue),
813                body: self.alloc_block(vec![hold]),
814            });
815            let asked = self.te_call(
816                answer,
817                vec![self.te_id(each), self.te_id(value), self.te_id(node)],
818            );
819            let either = self.te_bin(BinaryOpKind::Or, asked, self.te_id(answered));
820            queued.push(Stmt::Repeat {
821                pattern: crate::ast::stmt::Pattern::Identifier(each),
822                iterable: self.te_id(queue),
823                body: self.alloc_block(vec![Stmt::Set {
824                    target: answered,
825                    value: either,
826                }]),
827            });
828            // Let go of each lookup; the last holder of one retired meanwhile forgets it now.
829            let forget = self.interner.intern(forget_listener_fn_name());
830            let remaining = self.interner.intern("__lgx_holds");
831            let holds = self.ctx.alloc_imperative_expr(Expr::Index {
832                collection: self.te_id(held),
833                index: self.te_id(each),
834            });
835            let fewer = self.te_bin(BinaryOpKind::Subtract, holds, self.te_int(1));
836            let was_left = self.ctx.alloc_imperative_expr(Expr::Contains {
837                collection: self.te_id(left),
838                value: self.te_id(each),
839            });
840            let last = self.te_bin(BinaryOpKind::Eq, self.te_id(remaining), self.te_int(0));
841            let release = vec![
842                Stmt::Let {
843                    var: remaining,
844                    ty: None,
845                    value: fewer,
846                    mutable: false,
847                },
848                Stmt::If {
849                    cond: last,
850                    then_block: self.alloc_block(vec![
851                        Stmt::Remove {
852                            value: self.te_id(each),
853                            collection: self.te_id(held),
854                        },
855                        Stmt::If {
856                            cond: was_left,
857                            then_block: self.alloc_block(vec![
858                                Stmt::Remove {
859                                    value: self.te_id(each),
860                                    collection: self.te_id(left),
861                                },
862                                Stmt::Call {
863                                    function: forget,
864                                    args: vec![self.te_id(each)],
865                                },
866                            ]),
867                            else_block: None,
868                        },
869                    ]),
870                    else_block: Some(self.alloc_block(vec![Stmt::SetIndex {
871                        collection: self.te_id(held),
872                        index: self.te_id(each),
873                        value: self.te_id(remaining),
874                    }])),
875                },
876            ];
877            queued.push(Stmt::Repeat {
878                pattern: crate::ast::stmt::Pattern::Identifier(each),
879                iterable: self.te_id(queue),
880                body: self.alloc_block(release),
881            });
882            // ★ ONE LISTENER NEEDS NO QUEUE. With no listener waiting for a key of this event and
883            // nothing spilled on the plain slot, the slot's own listener is the whole delivery: it
884            // is answered directly, and a click — nearly every event a page hears — allocates
885            // nothing here.
886            let unkeyed = self.te_bin(BinaryOpKind::Lt, self.te_id(keyed), self.te_int(0));
887            let spilled_here = self.ctx.alloc_imperative_expr(Expr::Contains {
888                collection: self.te_id(more),
889                value: self.te_id(plain_slot),
890            });
891            let alone = self.ctx.alloc_imperative_expr(Expr::Not {
892                operand: spilled_here,
893            });
894            let single = self.te_bin(BinaryOpKind::And, unkeyed, alone);
895            let asked = self.te_call(
896                answer,
897                vec![self.te_id(plain_slot), self.te_id(value), self.te_id(node)],
898            );
899            let registered = self.te_bin(BinaryOpKind::GtEq, self.te_id(code), self.te_int(0));
900            let direct = Stmt::If {
901                cond: registered,
902                then_block: self.alloc_block(vec![Stmt::Set {
903                    target: answered,
904                    value: asked,
905                }]),
906                else_block: None,
907            };
908            body.push(Stmt::If {
909                cond: single,
910                then_block: self.alloc_block(vec![direct]),
911                else_block: Some(self.alloc_block(queued)),
912            });
913        }
914        body.push(Stmt::Return {
915            value: Some(self.te_id(answered)),
916        });
917
918        // `__lgx_answer(lookup, value) -> Bool` — the listener one table entry names, run for the
919        // instance that registered it; `false` when the entry names nothing.
920        let mut arms: Vec<Stmt<'a>> = Vec::new();
921        // Nothing registered for this element and event: the page does not answer that act.
922        // Reported as a VALUE rather than a failure, so the caller can try the second event a user
923        // action raises before deciding the page answers neither.
924        let fns = self.interner.intern(listener_fn_binding());
925        let known = self.ctx.alloc_imperative_expr(Expr::Contains {
926            collection: self.te_id(fns),
927            value: self.te_id(lookup),
928        });
929        let unknown = self.ctx.alloc_imperative_expr(Expr::Not { operand: known });
930        arms.push(Stmt::If {
931            cond: unknown,
932            then_block: self.alloc_block(vec![Stmt::Return {
933                value: Some(self.te_bool(false)),
934            }]),
935            else_block: None,
936        });
937        let read_id = self.ctx.alloc_imperative_expr(Expr::Index {
938            collection: self.te_id(fns),
939            index: self.te_id(lookup),
940        });
941        arms.push(Stmt::Let {
942            var: id,
943            ty: None,
944            value: read_id,
945            mutable: false,
946        });
947        let keys = self.interner.intern(listener_key_binding());
948        let read_key = self.ctx.alloc_imperative_expr(Expr::Index {
949            collection: self.te_id(keys),
950            index: self.te_id(lookup),
951        });
952        // ★ BOUND AS `__lgx_key`, so every helper below — `state_write`, `repaint_def`, the action
953        // call — acts on the instance that registered the listener, with no special casing.
954        arms.push(Stmt::Let {
955            var: key,
956            ty: None,
957            value: read_key,
958            mutable: false,
959        });
960        for (which, site) in self.lgx_listeners.clone().iter().enumerate() {
961            let arm = self.dispatch_site_arm(which, site);
962            arms.push(arm);
963        }
964        arms.push(Stmt::Return {
965            value: Some(self.te_bool(false)),
966        });
967
968        let text_ty: &'a TypeExpr<'a> = self
969            .ctx
970            .alloc_type_expr(TypeExpr::Primitive(self.interner.intern("Text")));
971        let int_ty: &'a TypeExpr<'a> = self
972            .ctx
973            .alloc_type_expr(TypeExpr::Primitive(self.interner.intern("Int")));
974        let bool_ty: &'a TypeExpr<'a> = self
975            .ctx
976            .alloc_type_expr(TypeExpr::Primitive(self.interner.intern("Bool")));
977        let node_half = Stmt::FunctionDef {
978            name: dispatch_node,
979            generics: Vec::new(),
980            params: vec![
981                (node, int_ty),
982                (event, text_ty),
983                (self.lgx_value_param(), text_ty),
984            ],
985            body: self.alloc_block(body),
986            return_type: Some(bool_ty),
987            is_native: false,
988            native_path: None,
989            is_exported: false,
990            export_target: None,
991            opt_flags: crate::optimization::OptimizationConfig::all_on(),
992        };
993        self.deferred_functions.push(node_half);
994        let value = self.lgx_value_param();
995        self.deferred_functions.push(Stmt::FunctionDef {
996            name: answer,
997            generics: Vec::new(),
998            params: vec![(lookup, int_ty), (value, text_ty), (node, int_ty)],
999            body: self.alloc_block(arms),
1000            return_type: Some(bool_ty),
1001            is_native: false,
1002            native_path: None,
1003            is_exported: false,
1004            export_target: None,
1005            opt_flags: crate::optimization::OptimizationConfig::all_on(),
1006        });
1007        let name = self.interner.intern(dispatch_fn_name());
1008        Some(Stmt::FunctionDef {
1009            name,
1010            generics: Vec::new(),
1011            params: vec![
1012                (selector, text_ty),
1013                (event, text_ty),
1014                (self.lgx_value_param(), text_ty),
1015            ],
1016            body: self.alloc_block(selector_body),
1017            return_type: Some(bool_ty),
1018            is_native: false,
1019            native_path: None,
1020            is_exported: false,
1021            export_target: None,
1022            opt_flags: crate::optimization::OptimizationConfig::all_on(),
1023        })
1024    }
1025
1026    /// `If event is "__lgx_outbox": deliver everything queued; Return true.` — the flush a turn's
1027    /// epilogue asks for once it has committed (`queue_element_event`).
1028    ///
1029    /// The outbox is TAKEN before anything is delivered: each delivery runs another turn, which may
1030    /// queue events of its own and flush them at its own epilogue, so this walks a snapshot while
1031    /// the lists themselves start empty again for whatever the deliveries queue.
1032    fn outbox_flush_arm(&mut self, event: Symbol) -> Stmt<'a> {
1033        let flush = self.interner.intern(outbox_flush_event());
1034        let is_flush = self.te_bin(BinaryOpKind::Eq, self.te_id(event), self.te_text(flush));
1035        let mut arm: Vec<Stmt<'a>> = Vec::new();
1036        let count = self.interner.intern(outbox_count_binding());
1037        let taken_count = self.interner.intern("__lgx_on");
1038        arm.push(Stmt::Let {
1039            var: taken_count,
1040            ty: None,
1041            value: self.te_id(count),
1042            mutable: false,
1043        });
1044        arm.push(Stmt::Set {
1045            target: count,
1046            value: self.te_int(0),
1047        });
1048        let mut taken = Vec::new();
1049        for (list, local) in [
1050            (outbox_selector_binding(), "__lgx_os"),
1051            (outbox_event_binding(), "__lgx_oe"),
1052            (outbox_value_binding(), "__lgx_ov"),
1053        ] {
1054            let list = self.interner.intern(list);
1055            let local = self.interner.intern(local);
1056            arm.push(Stmt::Let {
1057                var: local,
1058                ty: None,
1059                value: self.te_id(list),
1060                mutable: false,
1061            });
1062            let empty = self.empty_outbox_list();
1063            arm.push(Stmt::Set {
1064                target: list,
1065                value: empty,
1066            });
1067            taken.push(local);
1068        }
1069        let at = self.interner.intern("__lgx_oi");
1070        arm.push(Stmt::Let {
1071            var: at,
1072            ty: None,
1073            value: self.te_int(1),
1074            mutable: true,
1075        });
1076        let entry: Vec<&'a Expr<'a>> = taken
1077            .iter()
1078            .map(|list| {
1079                self.ctx.alloc_imperative_expr(Expr::Index {
1080                    collection: self.te_id(*list),
1081                    index: self.te_id(at),
1082                })
1083            })
1084            .collect();
1085        let dispatch = self.interner.intern(dispatch_fn_name());
1086        let step = self.te_bin(BinaryOpKind::Add, self.te_id(at), self.te_int(1));
1087        let deliver = self.alloc_block(vec![
1088            Stmt::Call {
1089                function: dispatch,
1090                args: entry,
1091            },
1092            Stmt::Set {
1093                target: at,
1094                value: step,
1095            },
1096        ]);
1097        let more = self.te_bin(BinaryOpKind::LtEq, self.te_id(at), self.te_id(taken_count));
1098        arm.push(Stmt::While {
1099            cond: more,
1100            body: deliver,
1101            decreasing: None,
1102        });
1103        arm.push(Stmt::Return {
1104            value: Some(self.te_bool(true)),
1105        });
1106        Stmt::If {
1107            cond: is_flush,
1108            then_block: self.alloc_block(arm),
1109            else_block: None,
1110        }
1111    }
1112
1113    /// The closure a host invokes for a registration the dispatcher cannot answer from the node
1114    /// alone — a window event, a relay topic, a route change, a timer, a drop whose file the host
1115    /// reads, a scroll whose geometry decides whether it counts. It takes the EVENT.
1116    ///
1117    /// ★ IT MUST TAKE ONE PARAMETER. A host calls it as `__logos_call_closure(handle, ev)`, so a
1118    /// zero-parameter closure is an arity mismatch that TRAPS the moment the host fires it.
1119    pub(crate) fn listener_closure(&mut self, call: Stmt<'a>) -> &'a Expr<'a> {
1120        self.host_callback_closure(vec![call])
1121    }
1122
1123    /// [`Self::listener_closure`], for a body of more than one statement (a fetch completion's
1124    /// decode-then-dispatch, say) — the SAME single-`Int`-parameter shape either way. Every closure
1125    /// this lowering hands to a callback-registering host import (`onRoute`, a window event, a
1126    /// timer, a fetch completion) is built by one of these two, never `Expr::Closure` directly: the
1127    /// wasm trampoline (`__logos_call_closure`) only dispatches a closure taking exactly one `Int`,
1128    /// and a bare literal builder one line away from this one is exactly how the route-change
1129    /// closure went missing its param and trapped every module that fired it.
1130    pub(crate) fn host_callback_closure(&mut self, call: Vec<Stmt<'a>>) -> &'a Expr<'a> {
1131        let ev_param = self.interner.intern("__lgx_event");
1132        let int_ty: &'a TypeExpr<'a> = self
1133            .ctx
1134            .alloc_type_expr(TypeExpr::Primitive(self.interner.intern("Int")));
1135        let body = self.alloc_block(call);
1136        self.ctx.alloc_imperative_expr(Expr::Closure {
1137            params: vec![(ev_param, int_ty)],
1138            body: crate::ast::stmt::ClosureBody::Block(body),
1139            return_type: None,
1140        })
1141    }
1142
1143    /// `listenFlags(<node>, "<event>", <bits>)` — the host is told only that `node` answers `event`
1144    /// and how the event is dispatched there (`EventModifier::host_listen_bits`). What answers it is
1145    /// the entry this paint wrote into the listener table, which `__lgx_dispatch_node` reads when
1146    /// the host hands the event over, so a browser's event and a driving verb run the same arm.
1147    pub(crate) fn listen_flags_call(
1148        &mut self,
1149        node: &'a Expr<'a>,
1150        event: Symbol,
1151        bits: u8,
1152    ) -> Stmt<'a> {
1153        let flagged = self.interner.intern("listenFlags");
1154        let event_text = self.te_text(event);
1155        Stmt::Call {
1156            function: flagged,
1157            args: vec![node, event_text, self.te_int(i64::from(bits))],
1158        }
1159    }
1160
1161    /// The key an `on-keydown.enter={…}` directive waits for, as a browser reports it
1162    /// (`KeyboardEvent.key`), or `None` for a handler that answers any key. Read from the
1163    /// event-modifier catalogue, the one every reader of a handler's qualifiers reads.
1164    pub(crate) fn handler_key(&mut self, modifiers: &[EventModifier]) -> Option<Symbol> {
1165        EventModifier::key_of(modifiers).map(|k| self.interner.intern(&k))
1166    }
1167
1168    /// `If __lgx_id is <which>: <run it>; Return true.` — one registered listener.
1169    pub(crate) fn dispatch_site_arm(&mut self, which: usize, site: &ListenerSite) -> Stmt<'a> {
1170        let id = self.interner.intern("__lgx_id");
1171        let cond = self.te_bin(BinaryOpKind::Eq, self.te_id(id), self.te_int(which as i64));
1172        let mut run: Vec<Stmt<'a>> = Vec::new();
1173        match site {
1174            ListenerSite::Action {
1175                component,
1176                action,
1177                argc,
1178                carries_value,
1179                once,
1180                ..
1181            } => {
1182                if once.is_some() {
1183                    run.extend(self.dispatch_once_gate(*component));
1184                }
1185                let name = self.interner.resolve(*component).to_string();
1186                let a = self.interner.resolve(*action).to_string();
1187                let act = self.interner.intern(&action_fn_name(&name, &a));
1188                let lookup = self.interner.intern("__lgx_lk");
1189                let mut call_args = vec![self.key_expr()];
1190                for i in 0..*argc {
1191                    let var = self.interner.intern(&listener_arg_binding(&name, &a, i));
1192                    call_args.push(self.ctx.alloc_imperative_expr(Expr::Index {
1193                        collection: self.te_id(var),
1194                        index: self.te_id(lookup),
1195                    }));
1196                }
1197                if *carries_value {
1198                    let carried = self.lgx_value_param();
1199                    call_args.push(self.te_id(carried));
1200                }
1201                run.push(Stmt::Call {
1202                    function: act,
1203                    args: call_args,
1204                });
1205            }
1206            // The write-back function does the typing itself (`bound_value` recovers a Bool from
1207            // the Text the host delivers), so the arm hands over the raw value exactly as the
1208            // browser's own listener does. One conversion, one place.
1209            ListenerSite::Bind {
1210                component,
1211                field,
1212                checked: _,
1213            } => {
1214                let name = self.interner.resolve(*component).to_string();
1215                let write = self
1216                    .interner
1217                    .intern(&bind_fn_name(&name, self.interner.resolve(*field)));
1218                let value = self.lgx_value_param();
1219                let key = self.key_expr();
1220                run.push(Stmt::Call {
1221                    function: write,
1222                    args: vec![key, self.te_id(value)],
1223                });
1224            }
1225            // ★ THE BROWSER'S OWN ACT, THEN THE BIND TURN. A click moves the box — a checkbox flips,
1226            // a radio is selected — and the browser then raises `input`, which is what the bound
1227            // field's write-back answers, carrying `"true"`/`"false"`; so the arm hands the new
1228            // state to the same `__bind_<C>_<field>` turn a keystroke runs: written back, watched,
1229            // journaled `bind`.
1230            ListenerSite::DefaultToggle {
1231                component,
1232                field,
1233                radio,
1234            } => {
1235                let name = self.interner.resolve(*component).to_string();
1236                let write = self
1237                    .interner
1238                    .intern(&bind_fn_name(&name, self.interner.resolve(*field)));
1239                let yes = self.interner.intern("true");
1240                let now = self.interner.intern("__lgx_now");
1241                run.push(Stmt::Let {
1242                    var: now,
1243                    ty: None,
1244                    value: self.te_text(yes),
1245                    mutable: true,
1246                });
1247                if !*radio {
1248                    let no = self.interner.intern("false");
1249                    let current = self.state_read(&name, *field);
1250                    let flip = Stmt::Set {
1251                        target: now,
1252                        value: self.te_text(no),
1253                    };
1254                    run.push(Stmt::If {
1255                        cond: current,
1256                        then_block: self.alloc_block(vec![flip]),
1257                        else_block: None,
1258                    });
1259                }
1260                let key = self.key_expr();
1261                run.push(Stmt::Call {
1262                    function: write,
1263                    args: vec![key, self.te_id(now)],
1264                });
1265            }
1266            // The trampoline resolves the identity the placement recorded; an instance with none
1267            // recorded is a site nothing answers, reported as `false` like any unregistered one.
1268            ListenerSite::CallbackProp {
1269                component,
1270                prop,
1271                argc,
1272                once,
1273                ..
1274            } => {
1275                let cname = self.interner.resolve(*component).to_string();
1276                let pname = self.interner.resolve(*prop).to_string();
1277                let tag_map = self.interner.intern(&callback_tag_binding(&cname, &pname));
1278                let this_key = self.key_expr();
1279                let has_identity = self.ctx.alloc_imperative_expr(Expr::Contains {
1280                    collection: self.te_id(tag_map),
1281                    value: this_key,
1282                });
1283                let missing = self.ctx.alloc_imperative_expr(Expr::Not {
1284                    operand: has_identity,
1285                });
1286                run.push(Stmt::If {
1287                    cond: missing,
1288                    then_block: self.alloc_block(vec![Stmt::Return {
1289                        value: Some(self.te_bool(false)),
1290                    }]),
1291                    else_block: None,
1292                });
1293                if once.is_some() {
1294                    run.extend(self.dispatch_once_gate(*component));
1295                }
1296                let lookup = self.interner.intern("__lgx_lk");
1297                let mut call_args = vec![self.key_expr()];
1298                for i in 0..*argc {
1299                    let var = self
1300                        .interner
1301                        .intern(&callback_arg_binding(&cname, &pname, i));
1302                    call_args.push(self.ctx.alloc_imperative_expr(Expr::Index {
1303                        collection: self.te_id(var),
1304                        index: self.te_id(lookup),
1305                    }));
1306                }
1307                let trampoline = self
1308                    .interner
1309                    .intern(&callback_trampoline_fn_name(&cname, &pname));
1310                run.push(Stmt::Call {
1311                    function: trampoline,
1312                    args: call_args,
1313                });
1314            }
1315            ListenerSite::ScrollNear { component, action } => {
1316                let name = self.interner.resolve(*component).to_string();
1317                let a = self.interner.resolve(*action).to_string();
1318                let act = self.interner.intern(&action_fn_name(&name, &a));
1319                let node = self.interner.intern("__lgx_node");
1320                let gap_of = self.interner.intern("scrollGap");
1321                let gap = self.te_call(gap_of, vec![self.te_id(node)]);
1322                let far = self.te_bin(BinaryOpKind::Gt, gap, self.te_int(32));
1323                run.push(Stmt::If {
1324                    cond: far,
1325                    then_block: self.alloc_block(vec![Stmt::Return {
1326                        value: Some(self.te_bool(false)),
1327                    }]),
1328                    else_block: None,
1329                });
1330                run.push(Stmt::Call {
1331                    function: act,
1332                    args: vec![self.key_expr()],
1333                });
1334            }
1335        }
1336        run.push(Stmt::Return {
1337            value: Some(self.te_bool(true)),
1338        });
1339        Stmt::If {
1340            cond,
1341            then_block: self.alloc_block(run),
1342            else_block: None,
1343        }
1344    }
1345
1346    /// `Let id be item __lgx_lk of __LISTENER_ONCE. If __LGX_ONCE contains id: Return false.` and
1347    /// then spend it — a `.once` listener answers the first event that passes its key gate, and
1348    /// every later one finds nothing to answer it, exactly as a spent browser listener does.
1349    fn dispatch_once_gate(&mut self, component: Symbol) -> Vec<Stmt<'a>> {
1350        let id = self.interner.intern("__lgx_oid");
1351        let lookup = self.interner.intern("__lgx_lk");
1352        let ids = self.interner.intern(listener_once_binding());
1353        let read = self.ctx.alloc_imperative_expr(Expr::Index {
1354            collection: self.te_id(ids),
1355            index: self.te_id(lookup),
1356        });
1357        let mut out = vec![Stmt::Let {
1358            var: id,
1359            ty: None,
1360            value: read,
1361            mutable: false,
1362        }];
1363        out.push(self.once_spent_guard(id));
1364        out.extend(self.spend_once(component, id));
1365        out
1366    }
1367
1368    /// `If __LGX_ONCE contains <id>: Return false.` — a spent `.once` listener answers nothing.
1369    pub(crate) fn once_spent_guard(&mut self, id: Symbol) -> Stmt<'a> {
1370        let spent = self.interner.intern(once_spent_binding());
1371        let is_spent = self.ctx.alloc_imperative_expr(Expr::Contains {
1372            collection: self.te_id(spent),
1373            value: self.te_id(id),
1374        });
1375        Stmt::If {
1376            cond: is_spent,
1377            then_block: self.alloc_block(vec![Stmt::Return {
1378                value: Some(self.te_bool(false)),
1379            }]),
1380            else_block: None,
1381        }
1382    }
1383
1384    /// Mark the `.once` listener `id` spent, and — for a component whose instances can leave the
1385    /// page — record it against the instance in scope, so the unmount sweep forgets it.
1386    pub(crate) fn spend_once(&mut self, component: Symbol, id: Symbol) -> Vec<Stmt<'a>> {
1387        let spent = self.interner.intern(once_spent_binding());
1388        let mut out = vec![Stmt::SetIndex {
1389            collection: self.te_id(spent),
1390            index: self.te_id(id),
1391            value: self.te_bool(true),
1392        }];
1393        if !self.once_recorded(component) {
1394            return out;
1395        }
1396        let name = self.interner.resolve(component).to_string();
1397        let record = self.interner.intern(&once_record_binding(&name));
1398        let list = self.interner.intern("__lgx_ol");
1399        let text = self.interner.intern("Text");
1400        let fresh = self.new_seq_expr(text);
1401        out.push(Stmt::Let {
1402            var: list,
1403            ty: None,
1404            value: fresh,
1405            mutable: true,
1406        });
1407        let key = self.key_expr();
1408        let known = self.ctx.alloc_imperative_expr(Expr::Contains {
1409            collection: self.te_id(record),
1410            value: key,
1411        });
1412        let key = self.key_expr();
1413        let held = self.ctx.alloc_imperative_expr(Expr::Index {
1414            collection: self.te_id(record),
1415            index: key,
1416        });
1417        out.push(Stmt::If {
1418            cond: known,
1419            then_block: self.alloc_block(vec![Stmt::Set {
1420                target: list,
1421                value: held,
1422            }]),
1423            else_block: None,
1424        });
1425        out.push(Stmt::Push {
1426            value: self.te_id(id),
1427            collection: self.te_id(list),
1428        });
1429        let key = self.key_expr();
1430        out.push(Stmt::SetIndex {
1431            collection: self.te_id(record),
1432            index: key,
1433            value: self.te_id(list),
1434        });
1435        out
1436    }
1437
1438    /// Whether `component` keeps a per-instance record of the `.once` listeners it spent: it has
1439    /// one, and its instances can leave the page for the sweep to forget them.
1440    pub(crate) fn once_recorded(&self, component: Symbol) -> bool {
1441        self.lgx_all_components
1442            .iter()
1443            .any(|d| d.name == component && has_once_handler(d))
1444            && unmountable_components(&self.lgx_all_components).contains(&component)
1445    }
1446
1447    /// The dispatcher's third parameter: what the user typed, for a `bind` to store.
1448    pub(crate) fn lgx_value_param(&mut self) -> Symbol {
1449        self.interner.intern("__lgx_val")
1450    }
1451
1452    pub(crate) fn te_bool(&self, b: bool) -> &'a Expr<'a> {
1453        self.ctx
1454            .alloc_imperative_expr(Expr::Literal(Literal::Boolean(b)))
1455    }
1456
1457    /// `__lgx_raise(event)` — a WINDOW event, delivered to the page.
1458    ///
1459    /// ★ IT ALWAYS REPORTS "ANSWERED", and that is the difference from every other driver. A click
1460    /// on an element nothing listens to is a test naming the wrong element; going offline when the
1461    /// page does not care about the network is an ordinary thing that happens, and failing there
1462    /// would make a test that sets the scene before mounting anything impossible to write.
1463    ///
1464    /// There is no missing-element check either: the page always exists.
1465    pub(crate) fn lower_lgx_raise(&mut self) -> Stmt<'a> {
1466        let selector = self.interner.intern("__lgx_sel");
1467        let event = self.interner.intern("__lgx_event");
1468        // The value slot the dispatcher already has — what a `Raise … with "photo.png".` carries,
1469        // and empty for the events that bring nothing.
1470        let carried = self.lgx_value_param();
1471        let dispatch = self.interner.intern(dispatch_fn_name());
1472        let two = self.te_int(2);
1473        let body = vec![
1474            Stmt::Call {
1475                function: dispatch,
1476                args: vec![self.te_id(selector), self.te_id(event), self.te_id(carried)],
1477            },
1478            Stmt::Return { value: Some(two) },
1479        ];
1480        self.lgx_driver_fn(raise_fn_name(), vec![selector, event, carried], body)
1481    }
1482
1483    pub(crate) fn lgx_driver_fn(
1484        &mut self,
1485        name: &str,
1486        params: Vec<Symbol>,
1487        body: Vec<Stmt<'a>>,
1488    ) -> Stmt<'a> {
1489        let text_ty: &'a TypeExpr<'a> = self
1490            .ctx
1491            .alloc_type_expr(TypeExpr::Primitive(self.interner.intern("Text")));
1492        let int_ty: &'a TypeExpr<'a> = self
1493            .ctx
1494            .alloc_type_expr(TypeExpr::Primitive(self.interner.intern("Int")));
1495        let name = self.interner.intern(name);
1496        Stmt::FunctionDef {
1497            name,
1498            generics: Vec::new(),
1499            params: params.into_iter().map(|p| (p, text_ty)).collect(),
1500            body: self.alloc_block(body),
1501            return_type: Some(int_ty),
1502            is_native: false,
1503            native_path: None,
1504            is_exported: false,
1505            export_target: None,
1506            opt_flags: crate::optimization::OptimizationConfig::all_on(),
1507        }
1508    }
1509
1510    /// `__bind_<Component>_<field>(v)` — store the keystroke, then repaint.
1511    pub(crate) fn lower_bind_writeback(
1512        &mut self,
1513        def: &ComponentDef<'a>,
1514        field: Symbol,
1515        kind: BoundInput,
1516    ) -> Option<Stmt<'a>> {
1517        let component = self.interner.resolve(def.name).to_string();
1518        let param = self.interner.intern("__lgx_typed");
1519        // A checkbox arrives as the TEXT of its state, because the host has one event-value
1520        // channel; storing that text straight into a Bool field would make `"false"` truthy.
1521        let stored = self.bound_value(kind, param);
1522        // ★ A KEYSTROKE IS A TURN. Read state in, set the field, write state back, note "bind",
1523        // patch or repaint — the shape `turn_body_with` is the one home of, and the one place a `When <field> changes:`
1524        // watcher runs, so typing into a bound field wakes the sentence watching it.
1525        let set = Stmt::Set {
1526            target: field,
1527            value: stored,
1528        };
1529        let body = self.turn_body_with(def, self.alloc_block(vec![set]), true, Some("bind"))?;
1530        let text_ty: &'a TypeExpr<'a> = self
1531            .ctx
1532            .alloc_type_expr(TypeExpr::Primitive(self.interner.intern("Text")));
1533        let name = self
1534            .interner
1535            .intern(&bind_fn_name(&component, self.interner.resolve(field)));
1536        Some(Stmt::FunctionDef {
1537            name,
1538            generics: Vec::new(),
1539            // WHICH INSTANCE first, then the delivered value — the same shape every turn takes, so
1540            // a keystroke routed through a driving verb and one from a real listener land in the
1541            // same record.
1542            params: vec![
1543                (self.interner.intern(instance_key_param()), text_ty),
1544                (param, text_ty),
1545            ],
1546            body,
1547            return_type: None,
1548            is_native: false,
1549            native_path: None,
1550            is_exported: false,
1551            export_target: None,
1552            opt_flags: crate::optimization::OptimizationConfig::all_on(),
1553        })
1554    }
1555
1556    /// What a bound control's reported state MEANS as a value. The host has one event-value
1557    /// channel and it carries Text, so a checkbox reports `"true"`/`"false"` and the Bool is
1558    /// recovered here, in the write-back every route into a bound field goes through.
1559    pub(crate) fn bound_value(&mut self, kind: BoundInput, param: Symbol) -> &'a Expr<'a> {
1560        match kind {
1561            k if k.writes_checked() => {
1562                let t = self.interner.intern("true");
1563                self.te_bin(BinaryOpKind::Eq, self.te_id(param), self.te_text(t))
1564            }
1565            _ => self.te_id(param),
1566        }
1567    }
1568}