Skip to main content

logicaffeine_language/parser/lgx_lower/
server.rs

1use super::render::LowerTrail;
2use super::*;
3use crate::ast::component::{ComponentDef, ComponentKind, RouteBody};
4use crate::ast::stmt::{BinaryOpKind, Expr, Stmt, TypeExpr};
5use crate::ast::view::ViewNode;
6use logicaffeine_base::Symbol;
7
8/// The function one of a component's actions lowers to.
9/// The `Request -> Response` the route table synthesizes, and what a bare `Serve on port N.`
10/// serves with. Named here so the parser and the lowering cannot spell it differently.
11pub const ROUTE_HANDLER: &str = "__lgx_route_handler";
12
13/// The three walks over the route table. Each takes the WHOLE `Request` — see the wrappers at the
14/// end of `lower_lgx_routes` for why the three-argument conveniences are built on top of these
15/// rather than the other way around.
16pub const ROUTE_BODY_WALK: &str = "__lgx_route_for";
17
18pub const ROUTE_STATUS_WALK: &str = "__lgx_route_status_for";
19
20pub const ROUTE_CTYPE_WALK: &str = "__lgx_route_ctype_for";
21
22/// The FOURTH walk: the headers of the response the matched route answered.
23///
24/// ★ ONE WALK, TWO CONSUMERS, and that is the whole reason it exists rather than a `routeHeader`
25/// that reaches in on its own. `__lgx_route_handler` puts these on the response it hands the
26/// socket — which is how a `Set-Cookie` a route set actually reaches a browser — and
27/// `routeHeader(method, url, name)` reads one out by name so a test can see it without a server.
28/// Built arm-for-arm beside the other three, so it cannot disagree with them about which route
29/// matched.
30pub const ROUTE_HEADERS_WALK: &str = "__lgx_route_headers_for";
31
32/// ★ THE SERVING WALK — the ONE `__lgx_route_handler` runs, answering a whole `Response`.
33///
34/// The other four answer one question each and are what `route(…)`, `routeStatus(…)`,
35/// `routeContentType(…)` and `routeHeader(…)` are built on. The handler must not compose them:
36/// `routeStatusWith` and `routeContentTypeWith` answer per-route CONSTANTS and never call a route's
37/// function, which is exactly what made "the handler runs once per request" true — and a `Response`
38/// route breaks it, because its status and content type are FIELDS OF THE ANSWER. Reading them
39/// through those walks would call the route once per walk, so a login would execute four times for
40/// one request: harmless for setting a cookie, a bug for writing a session row.
41///
42/// So this walk exists, built in the SAME loop from the SAME conditions as the other four, and it
43/// calls each matched route exactly once. `## Before each request` runs once in it too, which the
44/// old body-and-status pair never managed either.
45pub const ROUTE_RESPONSE_WALK: &str = "__lgx_route_response_for";
46
47/// The module global a `## Session` lives in. Named `session` on purpose: it makes
48/// `the session's user` an ordinary possessive rather than a sentence form of its own.
49pub const SESSION_VAR: &str = "session";
50
51pub const SESSION_SECRET_VAR: &str = "__lgx_secret";
52
53pub const SESSION_LOADED_VAR: &str = "__lgx_session_loaded";
54
55pub const SESSION_PAYLOAD_FN: &str = "__lgx_session_payload";
56
57pub const SESSION_LOAD_FN: &str = "__lgx_session_load";
58
59pub const SESSION_SAVE_FN: &str = "__lgx_session_save";
60
61/// The one cookie. See `lower_session` for why there is exactly one.
62pub const SESSION_COOKIE: &str = "lgxsession";
63
64/// The hidden field every session carries, whether the author declared it or not: the value a
65/// visitor's CSRF token is derived from.
66///
67/// ★ A NONCE RATHER THAN THE SESSION'S CONTENTS, and the difference is the whole protection.
68/// Deriving the token from the fields would hand every ANONYMOUS visitor the same token — they all
69/// carry the same empty session — and a token every visitor shares is one an attacker reads off
70/// their own page. It is minted the first time `csrf()` is called on a visit, which is the moment
71/// a page needs one and not before, so a visitor who never sees a form is never given a cookie.
72pub const SESSION_NONCE_FIELD: &str = "__csrfnonce";
73
74/// The form field and the header a token may arrive in. `_csrf` is the one `csrf_lock` already
75/// requires of every state-changing `<form>`; the header is how a scripted `fetch` sends one.
76pub const CSRF_FIELD: &str = "_csrf";
77
78pub const CSRF_HEADER: &str = "x-csrf-token";
79
80/// What a page calls to get its own token: `value={csrf()}`.
81pub const CSRF_FN: &str = "csrf";
82
83/// Where a server with a `## Session` answers the visitor's own CSRF token — the value a page
84/// composed around it (`<meta name="csrf-token">`) hands its scripted requests. The path is the
85/// router's own, never a route an author declares.
86pub const CSRF_TOKEN_PATH: &str = "/__logos/csrf";
87
88/// Whether a lowered program declares a `## Session` — read off the program itself, because the
89/// session's loader is defined exactly when one is. A serving tier that composes page documents
90/// asks the router for the visitor's token ([`CSRF_TOKEN_PATH`]) only then: without a session there
91/// is no token to ask for, and asking would run the handler for every page served.
92pub fn declares_session(stmts: &[Stmt<'_>], interner: &logicaffeine_base::Interner) -> bool {
93    stmts.iter().any(|s| {
94        matches!(s, Stmt::FunctionDef { name, .. } if interner.resolve(*name) == SESSION_LOAD_FN)
95    })
96}
97
98/// ★ `components` IS NOT OPTIONAL CONTEXT — IT DECIDES THE ANSWER.
99///
100/// A `-> View` route's template is checked by the server-side walk, and that walk refuses a
101/// component it CANNOT SEE (rightly: whether an unseen child renders on a server is unknown). This
102/// function used to call the no-list form, so **every `## Route … -> View:` that placed a component
103/// was quietly dropped from the router** — the route never reached the dispatcher, and
104/// `route("GET", "/report")` came back as an unknown function, or as a 404 for a route the author
105/// can see in their file. Which is the whole shape a page is BUILT out of: a row, a card, a field.
106///
107/// So every caller hands over the components it can see. A caller that genuinely has none (a route
108/// checked on its own) passes an empty slice and gets the honest, conservative refusal.
109/// The kinds of field a `## Session` may carry.
110///
111/// Deliberately three. A session rides in a COOKIE — a few hundred bytes a visitor holds and a
112/// browser sends on every request — so it is the wrong place for a list, a record or a map, and
113/// accepting one would be inviting a page to put its database in a header. Anything else is
114/// refused BY NAME at compile time rather than encoded as something plausible.
115#[derive(Clone, Copy, PartialEq, Eq, Debug)]
116pub enum SessionFieldKind {
117    Text,
118    Int,
119    Bool,
120}
121
122/// Where a route parameter's value comes from at dispatch.
123///
124/// ★ THREE-STATE ON PURPOSE, replacing a `from_path: bool`. A boolean could only ever ask "path or
125/// not", so the arrival of a third source had nowhere to go but a second boolean beside the first —
126/// and a pair of booleans has four states of which one is nonsense. Making the question total means
127/// a fourth source cannot be added without every `match` on it failing to compile, which is the
128/// same discipline `is_void` and `action_verb_tier_lock` already hold elsewhere.
129#[derive(Clone, Copy, PartialEq, Eq, Debug)]
130pub enum ParamFrom {
131    /// A `:name` segment of the route's own path pattern.
132    Path,
133    /// The query string, or the request body for a method that carries one.
134    Extra,
135    /// The whole request, bound as-is.
136    Request,
137}
138
139impl<'a, 'ctx, 'int> super::super::Parser<'a, 'ctx, 'int> {
140    /// Where a route parameter's raw text comes from: a `:name` slot in the PATH, or the
141    /// QUERY when the pattern has no slot for it.
142    ///
143    /// The path wins when a name appears in both — a path segment is part of the route's
144    /// IDENTITY, while a query is decoration any client may append to any URL, so letting
145    /// `?id=99` override `/todos/7` would let a caller aim a request at one record and have it
146    /// served another.
147    /// ★ `## Session` — a visitor's state, carried between requests in ONE signed cookie.
148    ///
149    /// It parsed and did NOTHING before this: `## Session\n    a user, which is Text.` compiled
150    /// clean, ran, and the declaration was discarded as prose — the exact failure the four server
151    /// headers were fixed for, where an author is told their intent was understood and it was not.
152    ///
153    /// What gets built, and why each piece is the shape it is:
154    ///
155    /// * A module global named `session`, so `the session's user` is ORDINARY LOGOS — a possessive
156    ///   on a record, the same form `todo's id` uses everywhere else. No new sentence form, no
157    ///   special case in the parser, and every tier that can read a record global reads this one.
158    /// * `__lgx_session_load` and `__lgx_session_save`, called by the router: the load runs in the
159    ///   walks' shared prelude, so it happens once per request before anything dispatches, and the
160    ///   save runs in the handler, where the response is finally in hand.
161    /// * ONE cookie. `HttpResponse.headers` is a `BTreeMap`, so a response carries exactly one
162    ///   `Set-Cookie` — a second silently overwrites the first. One is not a simplification here,
163    ///   it is the only correct number, and it is why the CSRF token is DERIVED from the session
164    ///   rather than being a cookie of its own.
165    /// * A payload of hex fields joined by `.`. Hex because a cookie value may not contain `;`,
166    ///   `,`, `"`, a space or a control character and a session holds text a visitor typed; the
167    ///   separator is safe precisely because no hex digit is a `.`, so there is nothing to escape
168    ///   and no escaping bug to have.
169    /// * The cookie is re-issued only when the payload CHANGED. Setting it on every reply would
170    ///   put a `Set-Cookie` on every 404 and every asset, and would silently overwrite a cookie a
171    ///   route set for itself — there is only the one slot.
172    pub(crate) fn lower_session(&mut self) -> crate::parser::ParseResult<()> {
173        use crate::analysis::registry::{FieldType, TypeDef};
174        if !self.session_declared {
175            return Ok(());
176        }
177        let session_ty = self.interner.intern("Session");
178        let declared: Vec<(Symbol, FieldType)> = match self.type_registry.as_ref().and_then(|r| {
179            r.iter_types()
180                .find(|(n, _)| **n == session_ty)
181                .map(|(_, d)| d.clone())
182        }) {
183            Some(TypeDef::Struct {
184                fields, generics, ..
185            }) if generics.is_empty() => fields.iter().map(|f| (f.name, f.ty.clone())).collect(),
186            // A `## Session` whose fields the record scanner could not read at all. It has already
187            // reported that; synthesizing a session with no fields on top would bury it.
188            _ => return Ok(()),
189        };
190        let mut fields: Vec<(Symbol, SessionFieldKind)> = Vec::new();
191        for (name, ty) in &declared {
192            let named = match ty {
193                FieldType::Primitive(s) | FieldType::Named(s) => self.interner.resolve(*s),
194                _ => "",
195            };
196            let kind = match named.to_ascii_lowercase().as_str() {
197                "text" => SessionFieldKind::Text,
198                "int" | "nat" => SessionFieldKind::Int,
199                "bool" => SessionFieldKind::Bool,
200                _ => {
201                    let field = self.interner.resolve(*name).to_string();
202                    let span = self
203                        .type_registry
204                        .as_ref()
205                        .and_then(|r| r.field_span(session_ty, *name))
206                        .unwrap_or_default();
207                    return Err(crate::error::ParseError {
208                        kind: crate::error::ParseErrorKind::Custom(format!(
209                            "`## Session` carries `{field}`, which a cookie cannot hold. A session \
210                             travels in a few hundred bytes the visitor keeps and the browser \
211                             sends with every single request, so it holds Text, Int and Bool and \
212                             nothing larger. What does the NEXT request actually need to know? \
213                             Usually that is an id — keep the id in the session and look the rest \
214                             up from it."
215                        )),
216                        span,
217                    });
218                }
219            };
220            fields.push((*name, kind));
221        }
222        if fields.is_empty() {
223            return Ok(());
224        }
225
226        let text_ty: &'a TypeExpr<'a> = self
227            .ctx
228            .alloc_type_expr(TypeExpr::Primitive(self.interner.intern("Text")));
229        let session_named: &'a TypeExpr<'a> = self.ctx.alloc_type_expr(TypeExpr::Named(session_ty));
230        let response_named: &'a TypeExpr<'a> = self
231            .ctx
232            .alloc_type_expr(TypeExpr::Named(self.interner.intern("Response")));
233        let request_named: &'a TypeExpr<'a> = self
234            .ctx
235            .alloc_type_expr(TypeExpr::Named(self.interner.intern("Request")));
236        let blank = self.interner.intern("");
237        let session_var = self.interner.intern(SESSION_VAR);
238        let secret_var = self.interner.intern(SESSION_SECRET_VAR);
239        let loaded_var = self.interner.intern(SESSION_LOADED_VAR);
240
241        // The globals: the secret (read ONCE, at startup), the session itself, and the payload as
242        // it arrived — which is what "did this request change anything?" is answered against.
243        let secret_call = {
244            let f = self.interner.intern("sessionSecret");
245            self.te_call(f, Vec::new())
246        };
247        self.deferred_functions.push(Stmt::Let {
248            var: secret_var,
249            ty: Some(text_ty),
250            value: secret_call,
251            mutable: true,
252        });
253        let fresh = self.fresh_session(session_ty, &fields);
254        self.deferred_functions.push(Stmt::Let {
255            var: session_var,
256            ty: Some(session_named),
257            value: fresh,
258            mutable: true,
259        });
260        self.deferred_functions.push(Stmt::Let {
261            var: loaded_var,
262            ty: Some(text_ty),
263            value: self.te_text(blank),
264            mutable: true,
265        });
266
267        // `__lgx_session_payload()` — every field as hex, joined by `.`.
268        let dot = self.interner.intern(".");
269        let hex = self.interner.intern("hexText");
270        let mut payload: Option<&'a Expr<'a>> = None;
271        for (name, _) in &fields {
272            let read = self.ctx.alloc_imperative_expr(Expr::FieldAccess {
273                object: self.te_id(session_var),
274                field: *name,
275            });
276            // `"" + value` is how a non-Text is shown, the same way a bare-scalar route answers.
277            let shown = self.te_bin(BinaryOpKind::Add, self.te_text(blank), read);
278            let piece = self.te_call(hex, vec![shown]);
279            payload = Some(match payload {
280                None => piece,
281                Some(acc) => {
282                    let with_dot = self.te_bin(BinaryOpKind::Add, acc, self.te_text(dot));
283                    self.te_bin(BinaryOpKind::Add, with_dot, piece)
284                }
285            });
286        }
287        let payload = payload.expect("at least one field");
288        let payload_fn = self.interner.intern(SESSION_PAYLOAD_FN);
289        self.deferred_functions.push(Stmt::FunctionDef {
290            name: payload_fn,
291            generics: Vec::new(),
292            params: Vec::new(),
293            body: self.alloc_block(vec![Stmt::Return {
294                value: Some(payload),
295            }]),
296            return_type: Some(text_ty),
297            is_native: false,
298            native_path: None,
299            is_exported: false,
300            export_target: None,
301            opt_flags: crate::optimization::OptimizationConfig::all_on(),
302        });
303
304        // `__lgx_session_load(request)` — verify the cookie, fill the fields, and record what
305        // arrived. A cookie is attacker-controlled input, so a signature that does not check is
306        // not an error and not a crash: it is simply a visitor with no session.
307        let req_p = self.interner.intern("__lgx_sreq");
308        let mut load: Vec<Stmt<'a>> = Vec::new();
309        let fresh = self.fresh_session(session_ty, &fields);
310        load.push(Stmt::Set {
311            target: session_var,
312            value: fresh,
313        });
314        let cookie_name = self.interner.intern(SESSION_COOKIE);
315        let cookie_of = self.interner.intern("cookieOf");
316        let raw = self.te_call(
317            cookie_of,
318            vec![self.te_id(req_p), self.te_text(cookie_name)],
319        );
320        let verify = self.interner.intern("verifySession");
321        let carried = self.interner.intern("__lgx_scarried");
322        let mut filled: Vec<Stmt<'a>> = Vec::new();
323        for (i, (name, kind)) in fields.iter().enumerate() {
324            let reader = match kind {
325                SessionFieldKind::Text => "sessionField",
326                SessionFieldKind::Int => "sessionInt",
327                SessionFieldKind::Bool => "sessionBool",
328            };
329            let f = self.interner.intern(reader);
330            // LOGOS indices count from 1, and the payload's fields are read with `item i of`.
331            let idx = self.te_int(i as i64 + 1);
332            let value = self.te_call(f, vec![self.te_id(carried), idx]);
333            filled.push(Stmt::SetField {
334                object: self.te_id(session_var),
335                field: *name,
336                value,
337            });
338        }
339        let some_arm = crate::ast::stmt::MatchArm {
340            enum_name: None,
341            variant: Some(self.interner.intern("Some")),
342            bindings: vec![(carried, carried)],
343            body: self.alloc_block(filled),
344        };
345        let none_arm = crate::ast::stmt::MatchArm {
346            enum_name: None,
347            variant: Some(self.interner.intern("None")),
348            bindings: Vec::new(),
349            body: self.alloc_block(Vec::new()),
350        };
351        load.push(Stmt::Inspect {
352            target: self.te_call(verify, vec![self.te_id(secret_var), raw]),
353            arms: vec![some_arm, none_arm],
354            has_otherwise: false,
355        });
356        // ★ RECORDED AFTER LOADING, WHETHER OR NOT A COOKIE ARRIVED. The question the save asks is
357        // "did this REQUEST change the session", so the baseline has to be the session as it was
358        // when the request started — which for a visitor with no cookie is the empty one, not the
359        // empty STRING. Recording `""` instead would make every anonymous visit look like a
360        // change and put a `Set-Cookie` on every reply.
361        let now = self.te_call(payload_fn, Vec::new());
362        load.push(Stmt::Set {
363            target: loaded_var,
364            value: now,
365        });
366        let load_fn = self.interner.intern(SESSION_LOAD_FN);
367        self.deferred_functions.push(Stmt::FunctionDef {
368            name: load_fn,
369            generics: Vec::new(),
370            params: vec![(req_p, request_named)],
371            body: self.alloc_block(load),
372            return_type: None,
373            is_native: false,
374            native_path: None,
375            is_exported: false,
376            export_target: None,
377            opt_flags: crate::optimization::OptimizationConfig::all_on(),
378        });
379
380        // `csrf()` — the token a form puts in its `_csrf` field, minted on first use.
381        //
382        // Minting HERE, rather than when the session loads, is what keeps the promise that a
383        // visitor who never sees a form is never given a cookie: asking for a token is the moment
384        // one is needed. And because minting writes to the session, the save below issues the
385        // cookie on that very response — no separate "start a session" step to forget.
386        let nonce_field = self.interner.intern(SESSION_NONCE_FIELD);
387        let read_nonce = self.ctx.alloc_imperative_expr(Expr::FieldAccess {
388            object: self.te_id(session_var),
389            field: nonce_field,
390        });
391        let unminted = self.te_bin(BinaryOpKind::Eq, read_nonce, self.te_text(blank));
392        let fresh_nonce = {
393            let f = self.interner.intern("randomSecret");
394            self.te_call(f, Vec::new())
395        };
396        let mint = self.alloc_block(vec![Stmt::SetField {
397            object: self.te_id(session_var),
398            field: nonce_field,
399            value: fresh_nonce,
400        }]);
401        let read_nonce = self.ctx.alloc_imperative_expr(Expr::FieldAccess {
402            object: self.te_id(session_var),
403            field: nonce_field,
404        });
405        let token = {
406            let f = self.interner.intern("csrfToken");
407            self.te_call(f, vec![self.te_id(secret_var), read_nonce])
408        };
409        let csrf_fn = self.interner.intern(CSRF_FN);
410        self.deferred_functions.push(Stmt::FunctionDef {
411            name: csrf_fn,
412            generics: Vec::new(),
413            params: Vec::new(),
414            body: self.alloc_block(vec![
415                Stmt::If {
416                    cond: unminted,
417                    then_block: mint,
418                    else_block: None,
419                },
420                Stmt::Return { value: Some(token) },
421            ]),
422            return_type: Some(text_ty),
423            is_native: false,
424            native_path: None,
425            is_exported: false,
426            export_target: None,
427            opt_flags: crate::optimization::OptimizationConfig::all_on(),
428        });
429
430        // `__lgx_session_save(response)` — re-issue the cookie only if the payload changed.
431        let resp_p = self.interner.intern("__lgx_sresp");
432        let now_var = self.interner.intern("__lgx_snow");
433        let mut save: Vec<Stmt<'a>> = Vec::new();
434        let now = self.te_call(payload_fn, Vec::new());
435        save.push(Stmt::Let {
436            var: now_var,
437            ty: Some(text_ty),
438            value: now,
439            mutable: false,
440        });
441        let same = self.te_bin(
442            BinaryOpKind::Eq,
443            self.te_id(now_var),
444            self.te_id(loaded_var),
445        );
446        let unchanged = self.alloc_block(vec![Stmt::Return {
447            value: Some(self.te_id(resp_p)),
448        }]);
449        save.push(Stmt::If {
450            cond: same,
451            then_block: unchanged,
452            else_block: None,
453        });
454        let sign = self.interner.intern("signSession");
455        let signed = self.te_call(sign, vec![self.te_id(secret_var), self.te_id(now_var)]);
456        let with_cookie = self.interner.intern("withCookie");
457        let set = self.te_call(
458            with_cookie,
459            vec![self.te_id(resp_p), self.te_text(cookie_name), signed],
460        );
461        save.push(Stmt::Return { value: Some(set) });
462        let save_fn = self.interner.intern(SESSION_SAVE_FN);
463        self.deferred_functions.push(Stmt::FunctionDef {
464            name: save_fn,
465            generics: Vec::new(),
466            params: vec![(resp_p, response_named)],
467            body: self.alloc_block(save),
468            return_type: Some(response_named),
469            is_native: false,
470            native_path: None,
471            is_exported: false,
472            export_target: None,
473            opt_flags: crate::optimization::OptimizationConfig::all_on(),
474        });
475        Ok(())
476    }
477
478    /// `a new Session with <every field at its empty value>` — a visitor who has never been seen.
479    pub(crate) fn fresh_session(
480        &mut self,
481        session_ty: Symbol,
482        fields: &[(Symbol, SessionFieldKind)],
483    ) -> &'a Expr<'a> {
484        let init: Vec<(Symbol, &'a Expr<'a>)> = fields
485            .iter()
486            .map(|(name, kind)| {
487                let zero = match kind {
488                    SessionFieldKind::Text => {
489                        let blank = self.interner.intern("");
490                        self.te_text(blank)
491                    }
492                    SessionFieldKind::Int => self.te_int(0),
493                    SessionFieldKind::Bool => self.te_bool(false),
494                };
495                (*name, zero)
496            })
497            .collect();
498        self.ctx.alloc_imperative_expr(Expr::New {
499            type_name: session_ty,
500            type_args: Vec::new(),
501            init_fields: init,
502        })
503    }
504
505    /// `a new Map of Text to Text` — the headers of an answer that set none.
506    pub(crate) fn empty_header_map(&mut self) -> &'a Expr<'a> {
507        let map_ty = self.interner.intern("Map");
508        let text_arg = TypeExpr::Primitive(self.interner.intern("Text"));
509        self.ctx.alloc_imperative_expr(Expr::New {
510            type_name: map_ty,
511            type_args: vec![text_arg.clone(), text_arg],
512            init_fields: Vec::new(),
513        })
514    }
515
516    /// `a new Response with status <status> and contentType <ctype> and body <body>`, no headers.
517    pub(crate) fn response_value(
518        &mut self,
519        status: &'a Expr<'a>,
520        ctype: &'a Expr<'a>,
521        body: &'a Expr<'a>,
522    ) -> &'a Expr<'a> {
523        let headers = self.empty_header_map();
524        self.ctx.alloc_imperative_expr(Expr::New {
525            type_name: self.interner.intern("Response"),
526            type_args: Vec::new(),
527            init_fields: vec![
528                (self.interner.intern("status"), status),
529                (self.interner.intern("contentType"), ctype),
530                (self.interner.intern("body"), body),
531                (self.interner.intern("headers"), headers),
532            ],
533        })
534    }
535
536    /// The router's OWN answer — a 404, a 405, a 400, a moved page. Always plain text, because
537    /// that is what those answers are.
538    pub(crate) fn plain_response(&mut self, code: i64, body: &'a Expr<'a>) -> &'a Expr<'a> {
539        let plain = self.interner.intern("text/plain; charset=utf-8");
540        let ctype = self.te_text(plain);
541        let status = self.te_int(code);
542        self.response_value(status, ctype, body)
543    }
544
545    /// The type of a header table, `Map of Text to Text`.
546    pub(crate) fn header_map_type(&mut self) -> &'a TypeExpr<'a> {
547        let text = TypeExpr::Primitive(self.interner.intern("Text"));
548        let params = self.ctx.alloc_type_exprs(vec![text.clone(), text]);
549        let base = self.interner.intern("Map");
550        self.ctx.alloc_type_expr(TypeExpr::Generic { base, params })
551    }
552
553    pub(crate) fn route_param_value(
554        &mut self,
555        from: ParamFrom,
556        pattern: Symbol,
557        name: Symbol,
558        path_p: Symbol,
559        query_p: Symbol,
560        body_p: Option<Symbol>,
561        req_p: Symbol,
562    ) -> &'a Expr<'a> {
563        // The request is the dispatcher's own parameter, so a route asking for it is handed the
564        // very value the walk was called with — headers, query and all.
565        if from == ParamFrom::Request {
566            return self.te_id(req_p);
567        }
568        let name_text = self.te_text(name);
569        if from == ParamFrom::Path {
570            let get = self.interner.intern("routeParam");
571            let pat = self.te_text(pattern);
572            return self.te_call(get, vec![pat, name_text, self.te_id(path_p)]);
573        }
574        // ★ A METHOD THAT CARRIES A BODY READS ITS EXTRAS FROM THE BODY. That is where a
575        // `<form method="post">` puts them and where a scripted JSON post puts them; a query
576        // string on a POST is neither, and reading one would leave every ordinary form silently
577        // handing the route empty values.
578        match body_p {
579            Some(body) => {
580                let get = self.interner.intern("bodyParam");
581                self.te_call(get, vec![self.te_id(body), name_text])
582            }
583            None => {
584                let get = self.interner.intern("queryParam");
585                self.te_call(get, vec![self.te_id(query_p), name_text])
586            }
587        }
588    }
589
590    /// ★ ON A SERVER A VIEW IS ITS HTML — every `-> View` function gains a twin answering its markup.
591    ///
592    /// `## To answer (who: Text) -> View:` returns a view — DATA on every lowered tier, one
593    /// `__View` variant per template (`views.rs`). A route's answer, `Show`, a test want the markup,
594    /// so each such function gains a TWIN, `__lgx_viewhtml_<f>`, taking the same parameters and
595    /// answering the view it returns drawn by the serving walk and serialized (`serve_markup`). The
596    /// router calls the twin; the original stays the view, which a template's hole or a browser
597    /// view places as nodes.
598    ///
599    /// A function is twinned when every template it can answer is one the server can draw. Twins
600    /// exist where a server renders — a program with a route or an `html of` — and where no page
601    /// can place a view at all, so every reader wants its markup; a page's own bundle carries none.
602    pub(crate) fn lower_view_function_twins(&mut self, statements: &[Stmt<'a>]) {
603        use crate::ast_depth::{for_each_node, Visited};
604        if self.lgx_views.functions.is_empty()
605            || !(self.renders_on_a_server(statements) || no_page_places_a_view(statements))
606        {
607            return;
608        }
609        let fns: Vec<(Symbol, Vec<(Symbol, &'a TypeExpr<'a>)>, &'a [Stmt<'a>])> = statements
610            .iter()
611            .filter_map(|s| match s {
612                Stmt::FunctionDef {
613                    name, params, body, ..
614                } if self.lgx_views.functions.contains(name) => {
615                    Some((*name, params.clone(), *body))
616                }
617                _ => None,
618            })
619            .collect();
620        let names: Vec<Symbol> = fns.iter().map(|(n, ..)| *n).collect();
621        let defs: Vec<ComponentDef<'a>> = statements
622            .iter()
623            .filter_map(|s| match s {
624                Stmt::ComponentDef(d) => Some((**d).clone()),
625                _ => None,
626            })
627            .collect();
628        // Every view function `stmts` reads: called, or named as a value.
629        let reads = |stmts: &[Stmt<'a>]| -> Vec<Symbol> {
630            let mut out: Vec<Symbol> = Vec::new();
631            for_each_node(stmts, |node| {
632                let named = match node {
633                    Visited::Expr(Expr::Call { function, .. })
634                    | Visited::Stmt(Stmt::Call { function, .. }) => *function,
635                    Visited::Expr(Expr::Identifier(sym)) => *sym,
636                    _ => return,
637                };
638                if names.contains(&named) && !out.contains(&named) {
639                    out.push(named);
640                }
641            });
642            out
643        };
644        let templates = self.lgx_views.templates.clone();
645        let calls: Vec<Vec<Symbol>> = fns
646            .iter()
647            .map(
648                |(name, _, body)| match templates.iter().find(|t| t.name == *name) {
649                    Some(t) => {
650                        let mut held = Vec::new();
651                        super::views::view_holes(&t.view.nodes, &mut |hole| {
652                            if let Expr::Call { function, .. } = hole {
653                                if names.contains(function) && !held.contains(function) {
654                                    held.push(*function);
655                                }
656                            }
657                        });
658                        held
659                    }
660                    None => reads(body),
661                },
662            )
663            .collect();
664        let mut twinned: Vec<Symbol> = fns
665            .iter()
666            .filter(
667                |(name, ..)| match templates.iter().find(|t| t.name == *name) {
668                    Some(t) => {
669                        view_html_lowerable_with(&t.view.nodes, &defs, self.interner).is_ok()
670                    }
671                    None => true,
672                },
673            )
674            .map(|(name, ..)| *name)
675            .collect();
676        loop {
677            let before = twinned.len();
678            let keep: Vec<Symbol> = fns
679                .iter()
680                .zip(&calls)
681                .filter(|((name, ..), called)| {
682                    twinned.contains(name) && called.iter().all(|c| twinned.contains(c))
683                })
684                .map(|((name, ..), _)| *name)
685                .collect();
686            twinned = keep;
687            if twinned.len() == before {
688                break;
689            }
690        }
691        let text_ty = self.text_type();
692        for (name, params, _) in fns.iter().filter(|(name, ..)| twinned.contains(name)) {
693            let twin = self
694                .interner
695                .intern(&view_html_fn_name(self.interner.resolve(*name)));
696            let view = self.interner.intern("__lgx_answered");
697            let args: Vec<&'a Expr<'a>> = params.iter().map(|(p, _)| self.te_id(*p)).collect();
698            let answered = self.te_call(*name, args);
699            let mut body = vec![Stmt::Let {
700                var: view,
701                ty: None,
702                value: answered,
703                mutable: false,
704            }];
705            let blank = self.interner.intern("");
706            let place = Stmt::Call {
707                function: self.interner.intern(place_view_fn_name()),
708                args: vec![self.te_id(view), self.te_text(blank), self.te_int(1)],
709            };
710            let out = self.interner.intern("__lgx_html");
711            self.serve_markup(vec![place], out, &mut body);
712            body.push(Stmt::Return {
713                value: Some(self.te_id(out)),
714            });
715            self.deferred_functions.push(Stmt::FunctionDef {
716                name: twin,
717                generics: Vec::new(),
718                params: params.clone(),
719                body: self.alloc_block(body),
720                return_type: Some(text_ty),
721                is_native: false,
722                native_path: None,
723                is_exported: false,
724                export_target: None,
725                opt_flags: crate::optimization::OptimizationConfig::all_on(),
726            });
727        }
728    }
729
730    /// In a program no page places a view in, every reader of one outside the view functions —
731    /// `## Main`, an ordinary function — reads its markup: each such read goes through the twin.
732    pub(crate) fn read_views_as_markup(&mut self, statements: &mut [Stmt<'a>]) {
733        if !no_page_places_a_view(statements) {
734            return;
735        }
736        let twins = self.served_view_twins(statements);
737        if twins.is_empty() {
738            return;
739        }
740        for stmt in statements.iter_mut() {
741            match stmt {
742                Stmt::FunctionDef { name, body, .. } => {
743                    if !self.lgx_views.functions.contains(name) {
744                        *body = self.serve_views_through_twins(body, &twins);
745                    }
746                }
747                other => {
748                    let alone = self.alloc_block(vec![other.clone()]);
749                    let mapped = self.serve_views_through_twins(alone, &twins);
750                    if let Some(first) = mapped.first() {
751                        *other = first.clone();
752                    }
753                }
754            }
755        }
756    }
757
758    /// The twin a `-> View` function is served through, when it has one.
759    pub(crate) fn view_html_twin(&self, function: Symbol) -> Option<Symbol> {
760        let twin = self
761            .interner
762            .lookup(&view_html_fn_name(self.interner.resolve(function)))?;
763        self.deferred_functions
764            .iter()
765            .any(|s| matches!(s, Stmt::FunctionDef { name, .. } if *name == twin))
766            .then_some(twin)
767    }
768
769    /// `body` with every read of a twinned view function taken through its twin.
770    pub(crate) fn serve_views_through_twins(
771        &mut self,
772        body: &'a [Stmt<'a>],
773        twins: &[(Symbol, Symbol)],
774    ) -> &'a [Stmt<'a>] {
775        if twins.is_empty() {
776            return body;
777        }
778        let arenas = crate::ast_map::Arenas {
779            exprs: self.ctx.imperative_exprs.expect("imperative arenas"),
780            stmts: self.ctx.stmts.expect("imperative arenas"),
781            types: self.ctx.type_exprs.expect("imperative arenas"),
782        };
783        crate::ast_map::AstMap::map_block(&mut TwinCalls { arenas, twins }, body)
784    }
785
786    /// Every `-> View` function in `statements` that is served through a twin, with its twin.
787    pub(crate) fn served_view_twins(&self, statements: &[Stmt<'a>]) -> Vec<(Symbol, Symbol)> {
788        statements
789            .iter()
790            .filter_map(|s| match s {
791                Stmt::FunctionDef {
792                    name, return_type, ..
793                } if self.is_view_function(*name, *return_type) => {
794                    self.view_html_twin(*name).map(|twin| (*name, twin))
795                }
796                _ => None,
797            })
798            .collect()
799    }
800
801    pub(crate) fn lower_lgx_routes(&mut self, statements: &[Stmt<'a>]) {
802        struct Route {
803            method: String,
804            path: Symbol,
805            fname: Symbol,
806            /// (name, is_int, from_path) — `from_path` is false for a parameter the path
807            /// pattern has no `:name` slot for, which is read from the QUERY instead. Decided
808            /// here, at compile time, because the pattern is known: a runtime fallback would
809            /// have to guess which source an empty value came from.
810            params: Vec<(Symbol, bool, ParamFrom)>,
811            literal: bool,
812            /// The record this route answers, if it answers one — the dispatcher wraps the call
813            /// in that record's derived encoder so the wire gets JSON.
814            /// The record a route's answer is encoded through, and whether it is a LIST of them
815            /// (`toJsonSeq_<T>` rather than `toJson_<T>`) — a collection endpoint's shape.
816            encode: Option<(Symbol, bool)>,
817            /// Does this method carry a request BODY? `POST`/`PUT`/`PATCH` do, and a declared
818            /// parameter the path has no slot for is read from it rather than from the query —
819            /// which is what a `<form method="post">` and a scripted JSON post both send.
820            has_body: bool,
821            /// The `Content-Type` this route's answer goes out with — decided from its RETURN
822            /// TYPE, where the author already said what they are sending. A server that types
823            /// every answer `text/plain` makes a browser show JSON as text and refuse to render
824            /// HTML, so the type has to travel with the body.
825            content_type: &'static str,
826            /// Answers a bare JSON scalar (a number, a boolean): no encoder to call, but the
827            /// value still has to become Text, and it goes out as JSON rather than as prose.
828            bare_scalar: bool,
829            /// Answers a whole `Response` — a status, a content type, a body and headers the
830            /// route decided for itself.
831            ///
832            /// ★ A `Response` IS THE ANSWER, NOT A VALUE TO ENCODE, and conflating those was the
833            /// whole bug. `route_lowerable` asked "is this a record?" and said yes; the emitter
834            /// asked "is this record JSON-encodable?" and said no, because `Response` holds a
835            /// `Map` and a `Seq of Int`. So `toJson_Response` was demanded, never derived, and
836            /// every such route died at `Unknown function`. Two questions that should be one.
837            ///
838            /// Everything downstream reads a FIELD of it rather than encoding it, which is what
839            /// lets a route carry a status, a `Location`, or a `Set-Cookie` at all.
840            is_response: bool,
841        }
842        // Nothing to do, and — importantly — nothing to ALLOCATE. Allocating the shared type
843        // exprs before this check made every program touch the type-expr arena, including the
844        // logic-mode parsers that build an `AstContext` without one ("type_exprs arena not
845        // initialized"). A lowering for a construct the file does not use must cost nothing.
846        if !statements.iter().any(|s| {
847            matches!(
848                s,
849                Stmt::RouteDef { .. } | Stmt::MiddlewareDef { .. } | Stmt::RedirectDef { .. }
850            )
851        }) {
852            return;
853        }
854        let text_ty: &'a TypeExpr<'a> = self
855            .ctx
856            .alloc_type_expr(TypeExpr::Primitive(self.interner.intern("Text")));
857        let int_ty: &'a TypeExpr<'a> = self
858            .ctx
859            .alloc_type_expr(TypeExpr::Primitive(self.interner.intern("Int")));
860        let view_twins = self.served_view_twins(statements);
861        let record_names: Vec<Symbol> = self
862            .type_registry
863            .as_ref()
864            .map(|r| {
865                r.iter_types()
866                    .filter(|(_, d)| {
867                        matches!(d, crate::analysis::registry::TypeDef::Struct { generics, .. }
868                            if generics.is_empty())
869                    })
870                    .map(|(n, _)| *n)
871                    .collect()
872            })
873            .unwrap_or_default();
874        let is_record = |s: Symbol| record_names.contains(&s);
875        // The components this file declares — what the server-side walk needs in order to say
876        // whether a `-> View` route's template can be drawn. `lower_lgx_components` has already
877        // run, so a child's own renderability is known before its route asks.
878        let defs: Vec<ComponentDef<'a>> = statements
879            .iter()
880            .filter_map(|s| match s {
881                Stmt::ComponentDef(d) => Some((**d).clone()),
882                _ => None,
883            })
884            .collect();
885        let mut routes: Vec<Route> = Vec::new();
886        for stmt in statements {
887            let Stmt::RouteDef {
888                method,
889                path,
890                params,
891                return_type,
892                body,
893                span,
894            } = stmt
895            else {
896                continue;
897            };
898            if route_lowerable(
899                params,
900                *return_type,
901                body,
902                self.interner,
903                &is_record,
904                &defs,
905                *span,
906            )
907            .is_err()
908            {
909                continue;
910            }
911            let m = self.interner.resolve(*method).to_ascii_uppercase();
912            let p = self.interner.resolve(*path).to_string();
913            let fname = self.interner.intern(&route_fn_name(&m, &p));
914            let segments: Vec<&str> = p.split('/').collect();
915            let bound: Vec<(Symbol, bool, ParamFrom)> = params
916                .iter()
917                .map(|(n, ty)| {
918                    let named = |k: &str| {
919                        matches!(ty, TypeExpr::Primitive(s) | TypeExpr::Named(s)
920                        if self.interner.resolve(*s).eq_ignore_ascii_case(k))
921                    };
922                    let slot = format!(":{}", self.interner.resolve(*n));
923                    let from = if named("Request") {
924                        ParamFrom::Request
925                    } else if segments.iter().any(|seg| *seg == slot) {
926                        ParamFrom::Path
927                    } else {
928                        ParamFrom::Extra
929                    };
930                    (*n, named("Int"), from)
931                })
932                .collect();
933            // A route declared `-> View` serves the markup, whether a template draws it or its
934            // statements return one.
935            let serves_view =
936                matches!(**body, RouteBody::View(_)) || self.returns_a_view(*return_type);
937            // A statements body is the function's body as written, reading a view through its
938            // twin. A TEMPLATE body becomes a Text accumulator the HTML walk appends to — the same
939            // tree, a different destination, because a server has no document to build.
940            let (fn_body, fn_ret) = match &**body {
941                // ★ THE HANDLER CARRIES THE TYPE ITS ROUTE DECLARES. This used to be hard-coded to
942                // `Text`, so `-> Seq of Todo` produced a function declared `-> Text` whose body
943                // answers a list — a contradiction the tree-walker and the VM infer their way past
944                // and the AOT-Rust type checker, the tier a server actually SHIPS as, refuses.
945                // Encoding was never the handler's job: the DISPATCHER wraps this call in the
946                // derived encoder (`encode`, just below), which is what puts Text on the wire.
947                RouteBody::Statements(block) => {
948                    let block = self.serve_views_through_twins(block, &view_twins);
949                    (
950                        block,
951                        if serves_view {
952                            Some(text_ty)
953                        } else {
954                            return_type.or(Some(text_ty))
955                        },
956                    )
957                }
958                RouteBody::View(view) => {
959                    let out = self.interner.intern("__lgx_html");
960                    let owner = self.template_owner(fname, params.to_vec(), view);
961                    let mut draw = Vec::new();
962                    let mut sheets_of: Vec<Symbol> = Vec::new();
963                    for child in placed_components(&view.nodes) {
964                        for styled in self.styled_reachable(child, &defs) {
965                            if !sheets_of.contains(&styled) {
966                                sheets_of.push(styled);
967                            }
968                        }
969                    }
970                    for styled in sheets_of {
971                        for sheet in self.served_sheets(styled, &defs) {
972                            draw.push(sheet);
973                        }
974                    }
975                    draw.extend(self.serving_draw(&view.nodes, &owner, &defs));
976                    let mut html = Vec::new();
977                    self.serve_markup(draw, out, &mut html);
978                    html.push(Stmt::Return {
979                        value: Some(self.te_id(out)),
980                    });
981                    (self.alloc_block(html), Some(text_ty))
982                }
983            };
984            self.deferred_functions.push(Stmt::FunctionDef {
985                name: fname,
986                generics: Vec::new(),
987                params: params.iter().map(|(n, ty)| (*n, *ty)).collect(),
988                body: fn_body,
989                return_type: fn_ret,
990                is_native: false,
991                native_path: None,
992                is_exported: false,
993                export_target: None,
994                opt_flags: crate::optimization::OptimizationConfig::all_on(),
995            });
996            let encode = match (&**body, return_type) {
997                // A route serving a view already answers HTML; wrapping it in a JSON encoder would
998                // serve a quoted document.
999                _ if serves_view => None,
1000                // ★ BEFORE the record arm, because `Response` IS one and would otherwise be sent
1001                // to an encoder that is never derived. It is the answer itself; its fields are
1002                // read, not serialised.
1003                (_, Some(TypeExpr::Primitive(sym) | TypeExpr::Named(sym)))
1004                    if self.interner.resolve(*sym).eq_ignore_ascii_case("Response") =>
1005                {
1006                    None
1007                }
1008                (_, Some(TypeExpr::Primitive(sym) | TypeExpr::Named(sym))) if is_record(*sym) => {
1009                    Some((*sym, false))
1010                }
1011                // A list of records answers as a JSON ARRAY, through the list encoder emitted
1012                // beside that record's own.
1013                (_, Some(TypeExpr::Generic { base, params }))
1014                    if self.interner.resolve(*base).eq_ignore_ascii_case("Seq")
1015                        && params.len() == 1 =>
1016                {
1017                    match &params[0] {
1018                        TypeExpr::Primitive(s) | TypeExpr::Named(s) if is_record(*s) => {
1019                            Some((*s, true))
1020                        }
1021                        _ => None,
1022                    }
1023                }
1024                _ => None,
1025            };
1026            // The router synthesizes this encoder's CALL, so it never appears in the source the
1027            // author wrote — the derive's reachability scan cannot see it and has to be told.
1028            if let Some((rec, _)) = encode {
1029                if !self.json_encoders_needed.contains(&rec) {
1030                    self.json_encoders_needed.push(rec);
1031                }
1032            }
1033            let has_body = matches!(m.as_str(), "POST" | "PUT" | "PATCH");
1034            let is_response = !serves_view
1035                && matches!(return_type, Some(TypeExpr::Primitive(sym) | TypeExpr::Named(sym))
1036                    if self.interner.resolve(*sym).eq_ignore_ascii_case("Response"));
1037            let bare_scalar = !serves_view
1038                && matches!(return_type, Some(TypeExpr::Primitive(sym) | TypeExpr::Named(sym))
1039                    if json_bare_scalar_name(self.interner.resolve(*sym)));
1040            // A `Response` names its OWN content type, so the per-route constant is only a
1041            // fallback for the walks that never see the value; the arms below read the field.
1042            let content_type = if serves_view {
1043                "text/html; charset=utf-8"
1044            } else if encode.is_some() || bare_scalar {
1045                "application/json"
1046            } else {
1047                "text/plain; charset=utf-8"
1048            };
1049            routes.push(Route {
1050                method: m,
1051                path: *path,
1052                fname,
1053                params: bound,
1054                literal: !p.contains(':'),
1055                encode,
1056                has_body,
1057                content_type,
1058                bare_scalar,
1059                is_response,
1060            });
1061        }
1062        // A file may declare redirects (or middleware) and no routes at all; it still needs a
1063        // dispatcher, or `route(…)` would be an unknown function.
1064        if routes.is_empty()
1065            && !statements
1066                .iter()
1067                .any(|s| matches!(s, Stmt::MiddlewareDef { .. } | Stmt::RedirectDef { .. }))
1068        {
1069            return;
1070        }
1071        let method_p = self.interner.intern("__lgx_method");
1072        // The dispatcher is handed a URL, and a URL is a path AND a query. Splitting it here —
1073        // once, before anything matches — is what lets `/search?q=hi` reach the route at
1074        // `/search`: comparing the whole URL 404s every link a `get` form, a share and an
1075        // analytics tag produce, which is most of the links on a real site.
1076        let url_p = self.interner.intern("__lgx_url");
1077        let path_p = self.interner.intern("__lgx_path");
1078        let query_p = self.interner.intern("__lgx_query");
1079        // ★ THE BODY IS A THIRD INPUT, and it is ADDITIVE. `route(method, url)` stays exactly
1080        // what it was — every test, every `Serve fetches with routes`, every page that asks a
1081        // question of the table keeps working — and `routeWith(method, url, body)` is the same
1082        // walk with the body in hand. Changing the arity of `route` instead would have churned
1083        // every call site in the suite to say `""` in a place nobody was asking about.
1084        let body_p = self.interner.intern("__lgx_body");
1085        // ★ THE WALKS TAKE THE REQUEST AND READ THEIR OWN INPUTS OFF IT. Method, url and body used
1086        // to be three parameters, which is every field of a request EXCEPT the two a session needs
1087        // — so no route could read a cookie and no guard could see a header. Binding them here, in
1088        // the prelude all three walks share, means the request is in scope for the whole table.
1089        let req_p = self.interner.intern("__lgx_req");
1090        // ★ THE SESSION LOADS IN THE PRELUDE, so it is in scope for the whole table and happens
1091        // ONCE per request — the handler runs only the serving walk, and that walk's prelude is
1092        // this one. Doing it inside each route instead would mean a route that forgot the call
1093        // silently saw an empty session, which is the class of bug this campaign keeps closing.
1094        let has_session = self.session_declared;
1095        let split_url = |me: &mut Self| -> Vec<Stmt<'a>> {
1096            let path_of = me.interner.intern("pathOf");
1097            let query_string = me.interner.intern("queryString");
1098            let field = |me: &mut Self, f: &str| -> &'a Expr<'a> {
1099                let field = me.interner.intern(f);
1100                let object = me.te_id(req_p);
1101                me.ctx
1102                    .alloc_imperative_expr(Expr::FieldAccess { object, field })
1103            };
1104            let m = field(me, "method");
1105            let u = field(me, "path");
1106            let b = field(me, "body");
1107            let mut out = vec![
1108                Stmt::Let {
1109                    var: method_p,
1110                    ty: Some(text_ty),
1111                    value: m,
1112                    mutable: false,
1113                },
1114                Stmt::Let {
1115                    var: url_p,
1116                    ty: Some(text_ty),
1117                    value: u,
1118                    mutable: false,
1119                },
1120                Stmt::Let {
1121                    var: body_p,
1122                    ty: Some(text_ty),
1123                    value: b,
1124                    mutable: false,
1125                },
1126                Stmt::Let {
1127                    var: path_p,
1128                    ty: Some(text_ty),
1129                    value: me.te_call(path_of, vec![me.te_id(url_p)]),
1130                    mutable: false,
1131                },
1132                Stmt::Let {
1133                    var: query_p,
1134                    ty: Some(text_ty),
1135                    value: me.te_call(query_string, vec![me.te_id(url_p)]),
1136                    mutable: false,
1137                },
1138            ];
1139            if has_session {
1140                let load = me.interner.intern(SESSION_LOAD_FN);
1141                let req = me.te_id(req_p);
1142                out.push(Stmt::Call {
1143                    function: load,
1144                    args: vec![req],
1145                });
1146            }
1147            out
1148        };
1149        let mut arms: Vec<Stmt<'a>> = split_url(self);
1150        // The status companion, built arm for arm beside the body — see `routeStatus`.
1151        let mut status_arms: Vec<Stmt<'a>> = split_url(self);
1152        // And the CONTENT TYPE companion, built in the same loop for the same reason: three
1153        // answers about one request that must agree about which route matched.
1154        let mut ctype_arms: Vec<Stmt<'a>> = split_url(self);
1155        // And the HEADERS companion — see `ROUTE_HEADERS_WALK`.
1156        let mut header_arms: Vec<Stmt<'a>> = split_url(self);
1157        // And the SERVING walk — see `ROUTE_RESPONSE_WALK`. This is the one a real request runs.
1158        let mut response_arms: Vec<Stmt<'a>> = split_url(self);
1159        let mut next = 0usize;
1160
1161        // ★ `routeStatus` MUST ANSWER THE STATUS A REAL REQUEST GETS, CSRF INCLUDED. It used to
1162        // skip straight to matching routes, so a state-changing route guarded by `## Session`
1163        // reported its ordinary success code to `routeStatus` regardless of whether the request
1164        // carried a valid token — the one framework promise this whole file exists to keep
1165        // ("the compiler checks the wire") broken for the one query whose entire job is "what
1166        // status code does this request get". A test written against `routeStatus` to prove a
1167        // route is CSRF-protected passed whether or not it was. The guard is built once and
1168        // pushed onto both walks, so the two can no longer read differently.
1169        // `as_status`: `response_arms` returns a `Response` on refusal, `status_arms` returns the
1170        // bare `Int` its function is declared to answer — the same gate, typed for its own walk.
1171        let csrf_guard = |me: &mut Self, as_status: bool| -> Vec<Stmt<'a>> {
1172            let refusal = |me: &mut Self| -> &'a Expr<'a> {
1173                if as_status {
1174                    me.te_int(403)
1175                } else {
1176                    let code = me.interner.intern("403");
1177                    let body_text = me.te_text(code);
1178                    me.plain_response(403, body_text)
1179                }
1180            };
1181            let m_upper = me.interner.intern("__lgx_method_upper");
1182            let upper = {
1183                let f = me.interner.intern("upper");
1184                me.te_call(f, vec![me.te_id(method_p)])
1185            };
1186            let mut out = vec![Stmt::Let {
1187                var: m_upper,
1188                ty: Some(text_ty),
1189                value: upper,
1190                mutable: false,
1191            }];
1192            let blank = me.interner.intern("");
1193            let secret_var = me.interner.intern(SESSION_SECRET_VAR);
1194            let session_var = me.interner.intern(SESSION_VAR);
1195            let nonce_field = me.interner.intern(SESSION_NONCE_FIELD);
1196            let sent = me.interner.intern("__lgx_csrf_sent");
1197            let mut guard: Vec<Stmt<'a>> = Vec::new();
1198            let from_body = {
1199                let f = me.interner.intern("bodyParam");
1200                let name = me.interner.intern(CSRF_FIELD);
1201                me.te_call(f, vec![me.te_id(body_p), me.te_text(name)])
1202            };
1203            guard.push(Stmt::Let {
1204                var: sent,
1205                ty: Some(text_ty),
1206                value: from_body,
1207                mutable: true,
1208            });
1209            // A scripted `fetch` has no form to put a hidden field in, so it sends a header.
1210            let from_header = {
1211                let f = me.interner.intern("headerOf");
1212                let name = me.interner.intern(CSRF_HEADER);
1213                me.te_call(f, vec![me.te_id(req_p), me.te_text(name)])
1214            };
1215            let none_sent = me.te_bin(BinaryOpKind::Eq, me.te_id(sent), me.te_text(blank));
1216            guard.push(Stmt::If {
1217                cond: none_sent,
1218                then_block: me.alloc_block(vec![Stmt::Set {
1219                    target: sent,
1220                    value: from_header,
1221                }]),
1222                else_block: None,
1223            });
1224            // ★ NO NONCE IS A REFUSAL, not a pass. A visitor with no session has a nonce of `""`,
1225            // and `csrfToken(secret, "")` is a value anybody can compute — so accepting it would
1226            // leave exactly the hole this is here to close.
1227            let read_nonce = me.ctx.alloc_imperative_expr(Expr::FieldAccess {
1228                object: me.te_id(session_var),
1229                field: nonce_field,
1230            });
1231            let unknown = me.te_bin(BinaryOpKind::Eq, read_nonce, me.te_text(blank));
1232            let refused = refusal(me);
1233            guard.push(Stmt::If {
1234                cond: unknown,
1235                then_block: me.alloc_block(vec![Stmt::Return {
1236                    value: Some(refused),
1237                }]),
1238                else_block: None,
1239            });
1240            let read_nonce = me.ctx.alloc_imperative_expr(Expr::FieldAccess {
1241                object: me.te_id(session_var),
1242                field: nonce_field,
1243            });
1244            let valid = {
1245                let f = me.interner.intern("csrfValid");
1246                me.te_call(f, vec![me.te_id(secret_var), read_nonce, me.te_id(sent)])
1247            };
1248            let bad = me.te_bin(BinaryOpKind::Eq, valid, me.te_bool(false));
1249            let refused = refusal(me);
1250            guard.push(Stmt::If {
1251                cond: bad,
1252                then_block: me.alloc_block(vec![Stmt::Return {
1253                    value: Some(refused),
1254                }]),
1255                else_block: None,
1256            });
1257            let mut changes: Option<&'a Expr<'a>> = None;
1258            for m in ["POST", "PUT", "PATCH", "DELETE"] {
1259                let sym = me.interner.intern(m);
1260                let is = me.te_bin(BinaryOpKind::Eq, me.te_id(m_upper), me.te_text(sym));
1261                changes = Some(match changes {
1262                    None => is,
1263                    Some(acc) => me.te_bin(BinaryOpKind::Or, acc, is),
1264                });
1265            }
1266            let changes = changes.expect("four methods");
1267            out.push(Stmt::If {
1268                cond: changes,
1269                then_block: me.alloc_block(guard),
1270                else_block: None,
1271            });
1272            out
1273        };
1274
1275        // `## Before each request` runs FIRST, in file order, and `some(response)` short-circuits.
1276        //
1277        // Before MATCHING, not before each route: a guard that only ran on paths some route
1278        // claimed could not redirect an unauthenticated visitor away from a URL that does not
1279        // exist — and would leak which URLs DO exist by answering them differently.
1280        let middleware: Vec<&Stmt<'a>> = statements
1281            .iter()
1282            .filter(|s| matches!(s, Stmt::MiddlewareDef { .. }))
1283            .collect();
1284        if !middleware.is_empty() {
1285            // ★ A GUARD IS HANDED THE REAL REQUEST. It used to be handed one FABRICATED here from
1286            // the method and the path, with empty `headers` and empty `query` — so the campaign's
1287            // own headline guard, `currentUser(request) is none`, answered `none` for every request
1288            // that would ever be made, and a site "protected" by it was open. Worse, the fabricated
1289            // binding was pushed into `arms` ALONE while the status walk referred to it, so any
1290            // program with a guard emitted a `routeStatusWith` naming an undeclared variable —
1291            // invisible because every middleware test asks `route(…)` and none asks `routeStatus(…)`.
1292            // Both faults are gone: the headers and the query below are the REAL ones, read off
1293            // the walks' own parameter.
1294            //
1295            // ★ THE PATH IS STILL THE SPLIT PATH, and that is a deliberate carry-over rather than
1296            // an oversight — `a_middleware_guard_sees_the_path_without_the_query` pins it, and it
1297            // is right: a guard comparing `request's path` to `"/admin"` must not be defeated by
1298            // appending `?x=1`. So the guard is handed the request it should have had all along —
1299            // every field real, with the one field the router has already split kept split.
1300            let req = self.interner.intern("__lgx_guard_request");
1301            let derived = {
1302                let read = |me: &mut Self, f: &str| -> &'a Expr<'a> {
1303                    let field = me.interner.intern(f);
1304                    let object = me.te_id(req_p);
1305                    me.ctx
1306                        .alloc_imperative_expr(Expr::FieldAccess { object, field })
1307                };
1308                let headers = read(self, "headers");
1309                let query = read(self, "query");
1310                self.ctx.alloc_imperative_expr(Expr::New {
1311                    type_name: self.interner.intern("Request"),
1312                    type_args: Vec::new(),
1313                    init_fields: vec![
1314                        (self.interner.intern("method"), self.te_id(method_p)),
1315                        (self.interner.intern("path"), self.te_id(path_p)),
1316                        (self.interner.intern("body"), self.te_id(body_p)),
1317                        (self.interner.intern("headers"), headers),
1318                        (self.interner.intern("query"), query),
1319                    ],
1320                })
1321            };
1322            // ★ BOUND INTO ALL THREE WALKS. The old binding went into `arms` alone while the
1323            // status walk referred to it, which is exactly the bug described above; adding a
1324            // fourth walk without adding it here would reopen it in the headers walk.
1325            for dest in [
1326                &mut arms,
1327                &mut status_arms,
1328                &mut header_arms,
1329                &mut response_arms,
1330            ] {
1331                dest.push(Stmt::Let {
1332                    var: req,
1333                    ty: None,
1334                    value: derived,
1335                    mutable: false,
1336                });
1337            }
1338            for (i, m) in middleware.iter().enumerate() {
1339                let Stmt::MiddlewareDef { param, body } = m else {
1340                    continue;
1341                };
1342                let fname = self.interner.intern(&format!("__middleware_{i}"));
1343                let body = self.serve_views_through_twins(body, &view_twins);
1344                // A guard answers `Option of Response` whatever its header spells: the early answer, or none.
1345                let answers = {
1346                    let response = TypeExpr::Named(self.interner.intern("Response"));
1347                    let params = self.ctx.alloc_type_exprs(vec![response]);
1348                    self.ctx.alloc_type_expr(TypeExpr::Generic {
1349                        base: self.interner.intern("Option"),
1350                        params,
1351                    })
1352                };
1353                self.deferred_functions.push(Stmt::FunctionDef {
1354                    name: fname,
1355                    generics: Vec::new(),
1356                    params: vec![*param],
1357                    body,
1358                    return_type: Some(answers),
1359                    is_native: false,
1360                    native_path: None,
1361                    is_exported: false,
1362                    export_target: None,
1363                    opt_flags: crate::optimization::OptimizationConfig::all_on(),
1364                });
1365                let answered = self.interner.intern("__lgx_answer");
1366                let body_field = self.interner.intern("body");
1367                let taken = self.ctx.alloc_imperative_expr(Expr::FieldAccess {
1368                    object: self.te_id(answered),
1369                    field: body_field,
1370                });
1371                let some_arm = crate::ast::stmt::MatchArm {
1372                    enum_name: None,
1373                    variant: Some(self.interner.intern("Some")),
1374                    bindings: vec![(answered, answered)],
1375                    body: self.alloc_block(vec![Stmt::Return { value: Some(taken) }]),
1376                };
1377                // A `None` arm is REQUIRED even though it does nothing: an `Inspect` whose value
1378                // matches no arm is a runtime error, so leaving it out would turn "this guard
1379                // declined" into a crash.
1380                let none_arm = crate::ast::stmt::MatchArm {
1381                    enum_name: None,
1382                    variant: Some(self.interner.intern("None")),
1383                    bindings: Vec::new(),
1384                    body: self.alloc_block(Vec::new()),
1385                };
1386                arms.push(Stmt::Inspect {
1387                    target: self.te_call(fname, vec![self.te_id(req)]),
1388                    arms: vec![some_arm, none_arm],
1389                    has_otherwise: false,
1390                });
1391                // A guard that ANSWERS carries its own status — `unauthorized()` is a 401, and
1392                // reporting 200 for it would tell the page the request succeeded.
1393                let status_field = self.interner.intern("status");
1394                let its_status = self.ctx.alloc_imperative_expr(Expr::FieldAccess {
1395                    object: self.te_id(answered),
1396                    field: status_field,
1397                });
1398                let some_status = crate::ast::stmt::MatchArm {
1399                    enum_name: None,
1400                    variant: Some(self.interner.intern("Some")),
1401                    bindings: vec![(answered, answered)],
1402                    body: self.alloc_block(vec![Stmt::Return {
1403                        value: Some(its_status),
1404                    }]),
1405                };
1406                let none_status = crate::ast::stmt::MatchArm {
1407                    enum_name: None,
1408                    variant: Some(self.interner.intern("None")),
1409                    bindings: Vec::new(),
1410                    body: self.alloc_block(Vec::new()),
1411                };
1412                status_arms.push(Stmt::Inspect {
1413                    target: self.te_call(fname, vec![self.te_id(req)]),
1414                    arms: vec![some_status, none_status],
1415                    has_otherwise: false,
1416                });
1417                // ★ AND ITS HEADERS. A guard that answers `redirectTo("/login")` carries the
1418                // `Location` in a header, so dropping them here would send every unauthenticated
1419                // visitor a 302 to nowhere — the browser stays exactly where it was and the site
1420                // looks broken rather than protected.
1421                let headers_field = self.interner.intern("headers");
1422                let its_headers = self.ctx.alloc_imperative_expr(Expr::FieldAccess {
1423                    object: self.te_id(answered),
1424                    field: headers_field,
1425                });
1426                let some_headers = crate::ast::stmt::MatchArm {
1427                    enum_name: None,
1428                    variant: Some(self.interner.intern("Some")),
1429                    bindings: vec![(answered, answered)],
1430                    body: self.alloc_block(vec![Stmt::Return {
1431                        value: Some(its_headers),
1432                    }]),
1433                };
1434                let none_headers = crate::ast::stmt::MatchArm {
1435                    enum_name: None,
1436                    variant: Some(self.interner.intern("None")),
1437                    bindings: Vec::new(),
1438                    body: self.alloc_block(Vec::new()),
1439                };
1440                header_arms.push(Stmt::Inspect {
1441                    target: self.te_call(fname, vec![self.te_id(req)]),
1442                    arms: vec![some_headers, none_headers],
1443                    has_otherwise: false,
1444                });
1445                // ★ AND THE SERVING WALK TAKES THE ANSWER WHOLE — one call, every field. The three
1446                // walks above each ask this guard the same question again to read one field off the
1447                // same answer, which is fine for a test asking `routeStatus(…)` and wrong for a
1448                // real request: a guard that counts attempts or writes a log line would run four
1449                // times for one visit. A real request runs THIS arm and only this one.
1450                let some_whole = crate::ast::stmt::MatchArm {
1451                    enum_name: None,
1452                    variant: Some(self.interner.intern("Some")),
1453                    bindings: vec![(answered, answered)],
1454                    body: self.alloc_block(vec![Stmt::Return {
1455                        value: Some(self.te_id(answered)),
1456                    }]),
1457                };
1458                let none_whole = crate::ast::stmt::MatchArm {
1459                    enum_name: None,
1460                    variant: Some(self.interner.intern("None")),
1461                    bindings: Vec::new(),
1462                    body: self.alloc_block(Vec::new()),
1463                };
1464                response_arms.push(Stmt::Inspect {
1465                    target: self.te_call(fname, vec![self.te_id(req)]),
1466                    arms: vec![some_whole, none_whole],
1467                    has_otherwise: false,
1468                });
1469            }
1470        }
1471
1472        // ★ CSRF IS CHECKED BEFORE ANY ROUTE MATCHES, in the SERVING walk AND in `routeStatus`.
1473        //
1474        // `csrf_lock` already makes forgetting the token a COMPILE ERROR — a state-changing
1475        // `<form>` must carry a `_csrf` field or the page does not build. But its own refusal ends
1476        // *"and check it with `csrfValid` in the route that answers"*, so the framework guaranteed
1477        // the token was SENT and left checking it to the author, in every route, forever. That is
1478        // the same checklist item one layer down, and the one route that forgets is the one that
1479        // gets exploited.
1480        //
1481        // ★ `routeStatus` GETS IT TOO, because it is the one question-walk whose entire job is
1482        // "what status code does this request get" — and a request denied for a missing or
1483        // invalid token gets 403 whether the answer comes from a browser or from `routeStatus` in
1484        // a `## Test`. `route`, `routeContentType` and `routeHeader` stay as they were: each asks
1485        // a structural question about the URL a route answers, not about one incoming request.
1486        if has_session {
1487            status_arms.extend(csrf_guard(self, true));
1488            response_arms.extend(csrf_guard(self, false));
1489        }
1490
1491        // ★ THE VISITOR'S TOKEN, AT THE ROUTER'S OWN PATH. A page's scripted request carries the
1492        // token in `x-csrf-token`, read from its document's `<meta name="csrf-token">`; whatever
1493        // composes that document asks here. Answering is `csrf()` — the visit's nonce is minted and
1494        // the cookie set on this very response, as for a form — and the answer is never stored by
1495        // a cache, which would hand one visitor's token to the next. Pushed into every walk, so
1496        // `route`, `routeStatus`, `routeContentType` and `routeHeaders` agree with what is served.
1497        if has_session {
1498            let get = self.interner.intern("GET");
1499            let reserved = self.interner.intern(CSRF_TOKEN_PATH);
1500            let plain = self.interner.intern("text/plain; charset=utf-8");
1501            let is_get = self.te_bin(BinaryOpKind::Eq, self.te_id(method_p), self.te_text(get));
1502            let at_path = self.te_bin(BinaryOpKind::Eq, self.te_id(path_p), self.te_text(reserved));
1503            let cond = self.te_bin(BinaryOpKind::And, is_get, at_path);
1504            let csrf_fn = self.interner.intern(CSRF_FN);
1505            let no_store = |me: &mut Self, headers: Symbol| -> Stmt<'a> {
1506                let name = me.interner.intern("cache-control");
1507                let value = me.interner.intern("no-store");
1508                Stmt::SetIndex {
1509                    collection: me.te_id(headers),
1510                    index: me.te_text(name),
1511                    value: me.te_text(value),
1512                }
1513            };
1514            let token = self.te_call(csrf_fn, Vec::new());
1515            let body_then = self.alloc_block(vec![Stmt::Return { value: Some(token) }]);
1516            arms.push(Stmt::If {
1517                cond,
1518                then_block: body_then,
1519                else_block: None,
1520            });
1521            let status_then = self.alloc_block(vec![Stmt::Return {
1522                value: Some(self.te_int(200)),
1523            }]);
1524            status_arms.push(Stmt::If {
1525                cond,
1526                then_block: status_then,
1527                else_block: None,
1528            });
1529            let ct_then = self.alloc_block(vec![Stmt::Return {
1530                value: Some(self.te_text(plain)),
1531            }]);
1532            ctype_arms.push(Stmt::If {
1533                cond,
1534                then_block: ct_then,
1535                else_block: None,
1536            });
1537            let hmap = self.interner.intern("__lgx_csrf_headers");
1538            let fresh = self.empty_header_map();
1539            let mark = no_store(self, hmap);
1540            let back = Stmt::Return {
1541                value: Some(self.te_id(hmap)),
1542            };
1543            let header_then = self.alloc_block(vec![
1544                Stmt::Let {
1545                    var: hmap,
1546                    ty: None,
1547                    value: fresh,
1548                    mutable: true,
1549                },
1550                mark,
1551                back,
1552            ]);
1553            header_arms.push(Stmt::If {
1554                cond,
1555                then_block: header_then,
1556                else_block: None,
1557            });
1558            let answer = self.interner.intern("__lgx_csrf_answer");
1559            let token = self.te_call(csrf_fn, Vec::new());
1560            let served = self.response_value(self.te_int(200), self.te_text(plain), token);
1561            let served_headers = self.ctx.alloc_imperative_expr(Expr::FieldAccess {
1562                object: self.te_id(answer),
1563                field: self.interner.intern("headers"),
1564            });
1565            let cache = self.interner.intern("cache-control");
1566            let store = self.interner.intern("no-store");
1567            let mark = Stmt::SetIndex {
1568                collection: served_headers,
1569                index: self.te_text(cache),
1570                value: self.te_text(store),
1571            };
1572            let back = Stmt::Return {
1573                value: Some(self.te_id(answer)),
1574            };
1575            let response_then = self.alloc_block(vec![
1576                Stmt::Let {
1577                    var: answer,
1578                    ty: None,
1579                    value: served,
1580                    mutable: true,
1581                },
1582                mark,
1583                back,
1584            ]);
1585            response_arms.push(Stmt::If {
1586                cond,
1587                then_block: response_then,
1588                else_block: None,
1589            });
1590        }
1591
1592        // (1) and (2): literals before patterns, so specificity beats file order.
1593        for pass_literal in [true, false] {
1594            for r in routes.iter().filter(|r| r.literal == pass_literal) {
1595                let m_sym = self.interner.intern(&r.method);
1596                let same_method =
1597                    self.te_bin(BinaryOpKind::Eq, self.te_id(method_p), self.te_text(m_sym));
1598                let matches_path = if r.literal {
1599                    self.te_bin(BinaryOpKind::Eq, self.te_id(path_p), self.te_text(r.path))
1600                } else {
1601                    let f = self.interner.intern("matchRoute");
1602                    self.te_call(f, vec![self.te_text(r.path), self.te_id(path_p)])
1603                };
1604                let cond = self.te_bin(BinaryOpKind::And, same_method, matches_path);
1605
1606                // ★ THE SERVING ARM — see `ROUTE_RESPONSE_WALK`. It rebuilds its own parameter
1607                // bindings because the body arm's parsed locals are block-scoped, and it calls the
1608                // route's function EXACTLY ONCE: a `Response` route hands its answer straight back,
1609                // and every other route has its answer wrapped in the status and content type the
1610                // companion walks report as constants. Built here, from `cond`, so it can never
1611                // disagree with them about which route matched.
1612                {
1613                    let mut rbody: Vec<Stmt<'a>> = Vec::new();
1614                    let mut rargs: Vec<&'a Expr<'a>> = Vec::new();
1615                    for (name, is_int, from) in &r.params {
1616                        let body_src = r.has_body.then_some(body_p);
1617                        let raw = self.route_param_value(
1618                            *from, r.path, *name, path_p, query_p, body_src, req_p,
1619                        );
1620                        if *is_int {
1621                            let is_int_fn = self.interner.intern("isIntText");
1622                            let ok = self.te_call(is_int_fn, vec![raw]);
1623                            let bad = self.te_bin(BinaryOpKind::Eq, ok, self.te_bool(false));
1624                            let four = self.interner.intern("400");
1625                            let text = self.te_text(four);
1626                            let refused = self.plain_response(400, text);
1627                            let then = self.alloc_block(vec![Stmt::Return {
1628                                value: Some(refused),
1629                            }]);
1630                            rbody.push(Stmt::If {
1631                                cond: bad,
1632                                then_block: then,
1633                                else_block: None,
1634                            });
1635                            let parse = self.interner.intern("parseInt");
1636                            rargs.push(self.te_call(parse, vec![raw]));
1637                        } else {
1638                            rargs.push(raw);
1639                        }
1640                    }
1641                    let rcall = self.te_call(r.fname, rargs);
1642                    let served = if r.is_response {
1643                        rcall
1644                    } else {
1645                        let ranswer = match r.encode {
1646                            Some((record, as_list)) => {
1647                                let prefix = if as_list { "toJsonSeq_" } else { "toJson_" };
1648                                let enc = self
1649                                    .interner
1650                                    .intern(&format!("{prefix}{}", self.interner.resolve(record)));
1651                                self.te_call(enc, vec![rcall])
1652                            }
1653                            None if r.bare_scalar => {
1654                                let empty = self.interner.intern("");
1655                                self.te_bin(BinaryOpKind::Add, self.te_text(empty), rcall)
1656                            }
1657                            None => rcall,
1658                        };
1659                        let ct = self.interner.intern(r.content_type);
1660                        let ctype = self.te_text(ct);
1661                        let ok = self.te_int(200);
1662                        self.response_value(ok, ctype, ranswer)
1663                    };
1664                    rbody.push(Stmt::Return {
1665                        value: Some(served),
1666                    });
1667                    let rthen = self.alloc_block(rbody);
1668                    response_arms.push(Stmt::If {
1669                        cond,
1670                        then_block: rthen,
1671                        else_block: None,
1672                    });
1673                }
1674
1675                let mut body: Vec<Stmt<'a>> = Vec::new();
1676                let mut args: Vec<&'a Expr<'a>> = Vec::new();
1677                for (name, is_int, from) in &r.params {
1678                    let raw = self.lgx_temp(&mut next);
1679                    let body_src = r.has_body.then_some(body_p);
1680                    let value = self
1681                        .route_param_value(*from, r.path, *name, path_p, query_p, body_src, req_p);
1682                    // The binding's type must match what `route_param_value` actually hands back:
1683                    // the raw TEXT of a path/query/body segment for every other origin, but the
1684                    // whole `Request` itself, unconverted, when the parameter asks for it.
1685                    let param_ty = if *from == ParamFrom::Request {
1686                        self.ctx
1687                            .alloc_type_expr(TypeExpr::Named(self.interner.intern("Request")))
1688                    } else {
1689                        text_ty
1690                    };
1691                    body.push(Stmt::Let {
1692                        var: raw,
1693                        ty: Some(param_ty),
1694                        value,
1695                        mutable: false,
1696                    });
1697                    if *is_int {
1698                        // A path segment that is not the declared type is a BAD REQUEST, not a
1699                        // 404 and emphatically not a silent zero: `parseInt("abc")` is 0, and
1700                        // serving todo 0 to someone who asked for "abc" is a wrong answer dressed
1701                        // as a right one. The round trip is the check — a value parses iff
1702                        // printing it back gives the same text.
1703                        // `parseInt` is strict and TRAPS on text that is not a number — right
1704                        // for a program reading its own data, wrong for a server reading a
1705                        // stranger's URL, where the answer must be 400 rather than a dead
1706                        // process. `isIntText` (web.lg) asks first.
1707                        let parse = self.interner.intern("parseInt");
1708                        let is_int = self.interner.intern("isIntText");
1709                        let ok = self.te_call(is_int, vec![self.te_id(raw)]);
1710                        let bad = self.te_bin(BinaryOpKind::Eq, ok, self.te_bool(false));
1711                        let four = self.interner.intern("400");
1712                        let then_block = self.alloc_block(vec![Stmt::Return {
1713                            value: Some(self.te_text(four)),
1714                        }]);
1715                        body.push(Stmt::If {
1716                            cond: bad,
1717                            then_block,
1718                            else_block: None,
1719                        });
1720                        let conv = self.lgx_temp(&mut next);
1721                        body.push(Stmt::Let {
1722                            var: conv,
1723                            ty: Some(int_ty),
1724                            value: self.te_call(parse, vec![self.te_id(raw)]),
1725                            mutable: false,
1726                        });
1727                        args.push(self.te_id(conv));
1728                    } else {
1729                        args.push(self.te_id(raw));
1730                    }
1731                }
1732                let call = self.te_call(r.fname, args);
1733                // A `Response` route answers its own body; the status, content type and headers
1734                // are read from the same value by the companion walks below.
1735                if r.is_response {
1736                    // Each walk reads ONE FIELD of the same answer. They are built here, in the
1737                    // one loop that decides which route matched, so a `Response` route cannot be
1738                    // added to the body and forgotten in the status — the discipline the three
1739                    // walks already follow.
1740                    //
1741                    // ★ AND `__lgx_route_handler` CALLS NONE OF THESE FOR SUCH A ROUTE — it runs
1742                    // the serving arm above, once. These three exist for the conveniences a test
1743                    // calls directly (`routeStatus(…)`, `routeContentType(…)`, `routeHeader(…)`),
1744                    // where calling the route again to read one more field of its answer is the
1745                    // point rather than a cost. See `ROUTE_RESPONSE_WALK` for why the handler must
1746                    // not be built out of them.
1747                    let field_of = |me: &mut Self, call: &'a Expr<'a>, f: &str| {
1748                        let field = me.interner.intern(f);
1749                        me.ctx.alloc_imperative_expr(Expr::FieldAccess {
1750                            object: call,
1751                            field,
1752                        })
1753                    };
1754                    let taken = field_of(self, call, "body");
1755                    body.push(Stmt::Return { value: Some(taken) });
1756                    let then_block = self.alloc_block(body);
1757                    arms.push(Stmt::If {
1758                        cond,
1759                        then_block,
1760                        else_block: None,
1761                    });
1762
1763                    // The companions rebuild their own arguments: the body arm's parsed `Int`
1764                    // locals are block-scoped and cannot be reached from here.
1765                    for (field, dest) in [("status", 0u8), ("contentType", 1u8), ("headers", 2u8)] {
1766                        let mut cbody: Vec<Stmt<'a>> = Vec::new();
1767                        let mut cargs: Vec<&'a Expr<'a>> = Vec::new();
1768                        for (name, is_int, from) in &r.params {
1769                            let body_src = r.has_body.then_some(body_p);
1770                            let raw = self.route_param_value(
1771                                *from, r.path, *name, path_p, query_p, body_src, req_p,
1772                            );
1773                            if *is_int {
1774                                let is_int_fn = self.interner.intern("isIntText");
1775                                let ok = self.te_call(is_int_fn, vec![raw]);
1776                                let bad = self.te_bin(BinaryOpKind::Eq, ok, self.te_bool(false));
1777                                let four: &'a Expr<'a> = match dest {
1778                                    0 => self.te_int(400),
1779                                    1 => {
1780                                        let plain =
1781                                            self.interner.intern("text/plain; charset=utf-8");
1782                                        self.te_text(plain)
1783                                    }
1784                                    _ => self.empty_header_map(),
1785                                };
1786                                let then =
1787                                    self.alloc_block(vec![Stmt::Return { value: Some(four) }]);
1788                                cbody.push(Stmt::If {
1789                                    cond: bad,
1790                                    then_block: then,
1791                                    else_block: None,
1792                                });
1793                                let parse = self.interner.intern("parseInt");
1794                                cargs.push(self.te_call(parse, vec![raw]));
1795                            } else {
1796                                cargs.push(raw);
1797                            }
1798                        }
1799                        let ccall = self.te_call(r.fname, cargs);
1800                        let read = field_of(self, ccall, field);
1801                        cbody.push(Stmt::Return { value: Some(read) });
1802                        let cthen = self.alloc_block(cbody);
1803                        let arm = Stmt::If {
1804                            cond,
1805                            then_block: cthen,
1806                            else_block: None,
1807                        };
1808                        match dest {
1809                            0 => status_arms.push(arm),
1810                            1 => ctype_arms.push(arm),
1811                            _ => header_arms.push(arm),
1812                        }
1813                    }
1814                    continue;
1815                }
1816                let answer = match r.encode {
1817                    Some((record, as_list)) => {
1818                        let prefix = if as_list { "toJsonSeq_" } else { "toJson_" };
1819                        let enc = self
1820                            .interner
1821                            .intern(&format!("{prefix}{}", self.interner.resolve(record)));
1822                        self.te_call(enc, vec![call])
1823                    }
1824                    // A bare scalar has no encoder — its LOGOS display already IS its JSON — but
1825                    // the dispatcher answers Text, so it still has to be shown.
1826                    None if r.bare_scalar => {
1827                        let empty = self.interner.intern("");
1828                        self.te_bin(BinaryOpKind::Add, self.te_text(empty), call)
1829                    }
1830                    None => call,
1831                };
1832                body.push(Stmt::Return {
1833                    value: Some(answer),
1834                });
1835                let then_block = self.alloc_block(body);
1836                arms.push(Stmt::If {
1837                    cond,
1838                    then_block,
1839                    else_block: None,
1840                });
1841                // The SAME arm, answering the code. Built here, in the one walk that decides
1842                // which route matches, so a route can never be added to the body and forgotten
1843                // in the status. The 400 check is repeated rather than shared because the status
1844                // needs no parsed value — only the question of whether the segment IS one.
1845                let mut status_body: Vec<Stmt<'a>> = Vec::new();
1846                for (name, is_int, from) in &r.params {
1847                    if !*is_int {
1848                        continue;
1849                    }
1850                    let body_src = r.has_body.then_some(body_p);
1851                    let raw = self
1852                        .route_param_value(*from, r.path, *name, path_p, query_p, body_src, req_p);
1853                    let is_int_fn = self.interner.intern("isIntText");
1854                    let ok = self.te_call(is_int_fn, vec![raw]);
1855                    let bad = self.te_bin(BinaryOpKind::Eq, ok, self.te_bool(false));
1856                    let four = self.te_int(400);
1857                    let then = self.alloc_block(vec![Stmt::Return { value: Some(four) }]);
1858                    status_body.push(Stmt::If {
1859                        cond: bad,
1860                        then_block: then,
1861                        else_block: None,
1862                    });
1863                }
1864                status_body.push(Stmt::Return {
1865                    value: Some(self.te_int(200)),
1866                });
1867                let status_then = self.alloc_block(status_body);
1868                status_arms.push(Stmt::If {
1869                    cond,
1870                    then_block: status_then,
1871                    else_block: None,
1872                });
1873                let ct = self.interner.intern(r.content_type);
1874                let ct_then = self.alloc_block(vec![Stmt::Return {
1875                    value: Some(self.te_text(ct)),
1876                }]);
1877                ctype_arms.push(Stmt::If {
1878                    cond,
1879                    then_block: ct_then,
1880                    else_block: None,
1881                });
1882            }
1883        }
1884
1885        // A `## Redirect /old -> /new.` answers ANY method: a moved URL has moved for everyone,
1886        // and answering 405 would tell the caller the path exists under some other method, which
1887        // is not what happened. It sits AFTER the real routes, so adding a redirect can never
1888        // silently take away a page some route already serves.
1889        for stmt in statements {
1890            let Stmt::RedirectDef { from, to } = stmt else {
1891                continue;
1892            };
1893            let same = self.te_bin(BinaryOpKind::Eq, self.te_id(path_p), self.te_text(*from));
1894            let answer = self
1895                .interner
1896                .intern(&format!("302 {}", self.interner.resolve(*to)));
1897            let then_block = self.alloc_block(vec![Stmt::Return {
1898                value: Some(self.te_text(answer)),
1899            }]);
1900            arms.push(Stmt::If {
1901                cond: same,
1902                then_block,
1903                else_block: None,
1904            });
1905            let moved = self.alloc_block(vec![Stmt::Return {
1906                value: Some(self.te_int(302)),
1907            }]);
1908            status_arms.push(Stmt::If {
1909                cond: same,
1910                then_block: moved,
1911                else_block: None,
1912            });
1913            // The BODY stays `"302 /new"` — `route("GET", "/old")` has always answered that string
1914            // and tests read it — and the handler turns it into a real `Location` header, which it
1915            // can tell apart from a route that redirected itself because that one arrives with the
1916            // header already set.
1917            let moved_body = self.te_text(answer);
1918            let moved_res = self.plain_response(302, moved_body);
1919            let rthen = self.alloc_block(vec![Stmt::Return {
1920                value: Some(moved_res),
1921            }]);
1922            response_arms.push(Stmt::If {
1923                cond: same,
1924                then_block: rthen,
1925                else_block: None,
1926            });
1927        }
1928
1929        // A `## Error page STATUS` renders in place of the bare status. Collected by STATUS, so
1930        // declaring only one leaves the other bare rather than silently borrowing the page that
1931        // was declared: "this URL does not exist" and "this URL does not do that" are different
1932        // mistakes and deserve different pages.
1933        let mut error_pages: Vec<(u16, Symbol)> = Vec::new();
1934        for stmt in statements {
1935            let Stmt::ComponentDef(def) = stmt else {
1936                continue;
1937            };
1938            let ComponentKind::ErrorPage { status } = def.kind else {
1939                continue;
1940            };
1941            let Some(view) = &def.view else { continue };
1942            if view_html_lowerable(&view.nodes, self.interner).is_err() {
1943                continue;
1944            }
1945            let fname = self.interner.intern(&format!("__error_page_{status}"));
1946            let out = self.interner.intern("__lgx_html");
1947            let mut html: Vec<Stmt<'a>> = Vec::new();
1948            // An error page's `State:` fields are ordinary locals, exactly as a component's are
1949            // when it renders.
1950            for field in def.state.iter().filter(|f| !f.derived) {
1951                let bound = self.state_field_let(field, field.init, true);
1952                html.push(bound);
1953            }
1954            let owner = self.template_owner(fname, Vec::new(), view);
1955            let draw = self.serving_draw(&view.nodes, &owner, &defs);
1956            self.serve_markup(draw, out, &mut html);
1957            html.push(Stmt::Return {
1958                value: Some(self.te_id(out)),
1959            });
1960            self.deferred_functions.push(Stmt::FunctionDef {
1961                name: fname,
1962                generics: Vec::new(),
1963                params: Vec::new(),
1964                body: self.alloc_block(html),
1965                return_type: Some(text_ty),
1966                is_native: false,
1967                native_path: None,
1968                is_exported: false,
1969                export_target: None,
1970                opt_flags: crate::optimization::OptimizationConfig::all_on(),
1971            });
1972            error_pages.push((status, fname));
1973        }
1974        // ★ AN ERROR PAGE IS A PAGE, AND ITS CONTENT TYPE HAS TO SAY SO. The router's own answers
1975        // — a bare status, a redirect — are plain text because that is what they are; a declared
1976        // `## Error page` renders MARKUP, and served as `text/plain` a browser shows a visitor the
1977        // literal characters `<h1>Gone</h1>`. Whether a page was declared for a status is a fact
1978        // this lowering already has, collected just above, and never asked.
1979        let has_page = |code: u16| error_pages.iter().any(|(s, _)| *s == code);
1980        let answer_ctype = |me: &mut Self, code: u16| -> &'a Expr<'a> {
1981            let ct = if has_page(code) {
1982                me.interner.intern("text/html; charset=utf-8")
1983            } else {
1984                me.interner.intern("text/plain; charset=utf-8")
1985            };
1986            me.te_text(ct)
1987        };
1988        let status_answer = |me: &mut Self, code: u16| -> &'a Expr<'a> {
1989            match error_pages.iter().find(|(s, _)| *s == code) {
1990                Some((_, f)) => me.te_call(*f, Vec::new()),
1991                None => {
1992                    let bare = me.interner.intern(&code.to_string());
1993                    me.te_text(bare)
1994                }
1995            }
1996        };
1997
1998        // (3) A path that EXISTS but not under this method is 405.
1999        let mut seen: Vec<Symbol> = Vec::new();
2000        for r in &routes {
2001            if seen.contains(&r.path) {
2002                continue;
2003            }
2004            seen.push(r.path);
2005            let cond = if r.literal {
2006                self.te_bin(BinaryOpKind::Eq, self.te_id(path_p), self.te_text(r.path))
2007            } else {
2008                let f = self.interner.intern("matchRoute");
2009                self.te_call(f, vec![self.te_text(r.path), self.te_id(path_p)])
2010            };
2011            let answer = status_answer(self, 405);
2012            let then_block = self.alloc_block(vec![Stmt::Return {
2013                value: Some(answer),
2014            }]);
2015            arms.push(Stmt::If {
2016                cond,
2017                then_block,
2018                else_block: None,
2019            });
2020            let wrong = self.alloc_block(vec![Stmt::Return {
2021                value: Some(self.te_int(405)),
2022            }]);
2023            status_arms.push(Stmt::If {
2024                cond,
2025                then_block: wrong,
2026                else_block: None,
2027            });
2028            let served = status_answer(self, 405);
2029            let ct = answer_ctype(self, 405);
2030            let code = self.te_int(405);
2031            let refused = self.response_value(code, ct, served);
2032            let rthen = self.alloc_block(vec![Stmt::Return {
2033                value: Some(refused),
2034            }]);
2035            response_arms.push(Stmt::If {
2036                cond,
2037                then_block: rthen,
2038                else_block: None,
2039            });
2040            // ★ ALWAYS, NOT ONLY WHEN A 405 PAGE EXISTS. The content-type walk's fall-through is
2041            // the 404's answer, so a 405 with no arm would inherit whatever the 404 says — declare
2042            // a `## Error page 404` and every 405 on the site starts claiming to be HTML. The two
2043            // statuses are separate declarations, so each answers for itself.
2044            let ct = answer_ctype(self, 405);
2045            let ct_then = self.alloc_block(vec![Stmt::Return { value: Some(ct) }]);
2046            ctype_arms.push(Stmt::If {
2047                cond,
2048                then_block: ct_then,
2049                else_block: None,
2050            });
2051        }
2052
2053        // (4) Nothing matched.
2054        let not_found = status_answer(self, 404);
2055        arms.push(Stmt::Return {
2056            value: Some(not_found),
2057        });
2058        status_arms.push(Stmt::Return {
2059            value: Some(self.te_int(404)),
2060        });
2061        let missing = status_answer(self, 404);
2062        let ct = answer_ctype(self, 404);
2063        let code = self.te_int(404);
2064        let missing = self.response_value(code, ct, missing);
2065        response_arms.push(Stmt::Return {
2066            value: Some(missing),
2067        });
2068        // `routeStatus(method, path)` — the CODE the router answers with, beside the body.
2069        //
2070        // `route(…)` answers Text, so its status and its body were one string: a page fetching a
2071        // URL nothing serves heard `"404"` as a BODY and could only find out by failing to decode
2072        // it. Both are produced by the ONE walk above, arm for arm, so they cannot disagree about
2073        // which route matched.
2074        // Everything the router answers ITSELF — 404, 405, a redirect, a middleware refusal —
2075        // is plain text, because that is what those answers are.
2076        // The fall-through is the 404, so it answers what a 404 answers.
2077        let tail_ct = answer_ctype(self, 404);
2078        ctype_arms.push(Stmt::Return {
2079            value: Some(tail_ct),
2080        });
2081        let no_headers = self.empty_header_map();
2082        header_arms.push(Stmt::Return {
2083            value: Some(no_headers),
2084        });
2085        let request_walk_ty: &'a TypeExpr<'a> = self
2086            .ctx
2087            .alloc_type_expr(TypeExpr::Named(self.interner.intern("Request")));
2088        let ctype_name = self.interner.intern(ROUTE_CTYPE_WALK);
2089        self.deferred_functions.push(Stmt::FunctionDef {
2090            name: ctype_name,
2091            generics: Vec::new(),
2092            params: vec![(req_p, request_walk_ty)],
2093            body: self.alloc_block(ctype_arms),
2094            return_type: Some(text_ty),
2095            is_native: false,
2096            native_path: None,
2097            is_exported: false,
2098            export_target: None,
2099            opt_flags: crate::optimization::OptimizationConfig::all_on(),
2100        });
2101        let response_named: &'a TypeExpr<'a> = self
2102            .ctx
2103            .alloc_type_expr(TypeExpr::Named(self.interner.intern("Response")));
2104        let response_walk = self.interner.intern(ROUTE_RESPONSE_WALK);
2105        self.deferred_functions.push(Stmt::FunctionDef {
2106            name: response_walk,
2107            generics: Vec::new(),
2108            params: vec![(req_p, request_walk_ty)],
2109            body: self.alloc_block(response_arms),
2110            return_type: Some(response_named),
2111            is_native: false,
2112            native_path: None,
2113            is_exported: false,
2114            export_target: None,
2115            opt_flags: crate::optimization::OptimizationConfig::all_on(),
2116        });
2117        let headers_ty = self.header_map_type();
2118        let headers_name = self.interner.intern(ROUTE_HEADERS_WALK);
2119        self.deferred_functions.push(Stmt::FunctionDef {
2120            name: headers_name,
2121            generics: Vec::new(),
2122            params: vec![(req_p, request_walk_ty)],
2123            body: self.alloc_block(header_arms),
2124            return_type: Some(headers_ty),
2125            is_native: false,
2126            native_path: None,
2127            is_exported: false,
2128            export_target: None,
2129            opt_flags: crate::optimization::OptimizationConfig::all_on(),
2130        });
2131        let status_name = self.interner.intern(ROUTE_STATUS_WALK);
2132        self.deferred_functions.push(Stmt::FunctionDef {
2133            name: status_name,
2134            generics: Vec::new(),
2135            params: vec![(req_p, request_walk_ty)],
2136            body: self.alloc_block(status_arms),
2137            return_type: Some(int_ty),
2138            is_native: false,
2139            native_path: None,
2140            is_exported: false,
2141            export_target: None,
2142            opt_flags: crate::optimization::OptimizationConfig::all_on(),
2143        });
2144        // The two-argument conveniences: the same questions, asked without a body. `route(m, u)`
2145        // is what a test, a page's `Serve fetches with routes`, and anything else that just wants
2146        // to know what a URL answers has always called, and it keeps meaning exactly that.
2147        for (short, long) in [
2148            ("route", "routeWith"),
2149            ("routeStatus", "routeStatusWith"),
2150            ("routeContentType", "routeContentTypeWith"),
2151        ] {
2152            let short_sym = self.interner.intern(short);
2153            let long_sym = self.interner.intern(long);
2154            let blank = self.interner.intern("");
2155            let ret: &'a TypeExpr<'a> = if short == "routeStatus" {
2156                int_ty
2157            } else {
2158                text_ty
2159            };
2160            let call = self.te_call(
2161                long_sym,
2162                vec![self.te_id(method_p), self.te_id(url_p), self.te_text(blank)],
2163            );
2164            self.deferred_functions.push(Stmt::FunctionDef {
2165                name: short_sym,
2166                generics: Vec::new(),
2167                params: vec![(method_p, text_ty), (url_p, text_ty)],
2168                body: self.alloc_block(vec![Stmt::Return { value: Some(call) }]),
2169                return_type: Some(ret),
2170                is_native: false,
2171                native_path: None,
2172                is_exported: false,
2173                export_target: None,
2174                opt_flags: crate::optimization::OptimizationConfig::all_on(),
2175            });
2176        }
2177
2178        // ★ `Serve on port 8080.` — the whole server, with no handler to write.
2179        //
2180        // A `## Route` table already says everything a request needs: which function answers,
2181        // what status it answers with, and what the answer IS. Making the author then hand-write
2182        // a `Request -> Response` that calls `route(…)` is ceremony that can only be got wrong —
2183        // and got wrong QUIETLY, by forgetting the content type or the redirect's `Location`, so
2184        // a browser shows JSON as text or follows a redirect to nowhere.
2185        //
2186        // Built from the same three walks, so it cannot disagree with them about anything.
2187        let handler_name = self.interner.intern(ROUTE_HANDLER);
2188        let request_p = self.interner.intern("__lgx_request_in");
2189        let s_var = self.interner.intern("__lgx_hs");
2190        let b_var = self.interner.intern("__lgx_hb");
2191        let c_var = self.interner.intern("__lgx_hc");
2192        let mut hbody: Vec<Stmt<'a>> = Vec::new();
2193        // ★ THE HANDLER PASSES THE REQUEST STRAIGHT THROUGH. It used to pull the method, the path
2194        // and the body out and ask the three-argument form — which is every field of a request
2195        // except the two that carry a cookie and a query, so the handler was the exact place the
2196        // headers were being dropped on a real server.
2197        // ★ ONE CALL. The four question-walks are NOT composed here — see `ROUTE_RESPONSE_WALK`:
2198        // doing that would run a `Response` route four times and its guards four times for a
2199        // single visit. The handler asks the serving walk once and reads the answer it got.
2200        let h_var = self.interner.intern("__lgx_hh");
2201        let served_var = self.interner.intern("__lgx_hres");
2202        let response_named: &'a TypeExpr<'a> = self
2203            .ctx
2204            .alloc_type_expr(TypeExpr::Named(self.interner.intern("Response")));
2205        let serve_call = {
2206            let f = self.interner.intern(ROUTE_RESPONSE_WALK);
2207            self.te_call(f, vec![self.te_id(request_p)])
2208        };
2209        hbody.push(Stmt::Let {
2210            var: served_var,
2211            ty: Some(response_named),
2212            value: serve_call,
2213            mutable: false,
2214        });
2215        let field_of = |me: &mut Self, f: &str| -> &'a Expr<'a> {
2216            let field = me.interner.intern(f);
2217            let object = me.te_id(served_var);
2218            me.ctx
2219                .alloc_imperative_expr(Expr::FieldAccess { object, field })
2220        };
2221        let read_status = field_of(self, "status");
2222        let read_body = field_of(self, "body");
2223        let read_ctype = field_of(self, "contentType");
2224        let read_headers = field_of(self, "headers");
2225        let headers_ty = self.header_map_type();
2226        hbody.push(Stmt::Let {
2227            var: s_var,
2228            ty: Some(int_ty),
2229            value: read_status,
2230            mutable: false,
2231        });
2232        hbody.push(Stmt::Let {
2233            var: b_var,
2234            ty: Some(text_ty),
2235            value: read_body,
2236            mutable: false,
2237        });
2238        hbody.push(Stmt::Let {
2239            var: c_var,
2240            ty: Some(text_ty),
2241            value: read_ctype,
2242            mutable: false,
2243        });
2244        hbody.push(Stmt::Let {
2245            var: h_var,
2246            ty: Some(headers_ty),
2247            value: read_headers,
2248            mutable: false,
2249        });
2250        let loc_var = self.interner.intern("__lgx_hloc");
2251        let header_in = self.interner.intern("headerIn");
2252        let location = self.interner.intern("location");
2253        let read_loc = self.te_call(header_in, vec![self.te_id(h_var), self.te_text(location)]);
2254        hbody.push(Stmt::Let {
2255            var: loc_var,
2256            ty: Some(text_ty),
2257            value: read_loc,
2258            mutable: false,
2259        });
2260        // A redirect answers `"<status> <where>"`, and a browser told to move without being told
2261        // WHERE simply stays put. So the moved answers are rebuilt through `redirectTo`, which
2262        // sets the `Location` header the status is meaningless without.
2263        // ★ THE SESSION IS SAVED ON THE WAY OUT, at every exit the handler has. Wrapping only the
2264        // ordinary one would mean a login that answers `redirectTo("/")` — which is exactly what a
2265        // login answers, POST/redirect/GET being the whole point — set no cookie and logged nobody
2266        // in. The save itself is a no-op when nothing changed, so wrapping every exit costs a
2267        // string comparison and cannot be got wrong by forgetting one.
2268        let saved = |me: &mut Self, value: &'a Expr<'a>| -> &'a Expr<'a> {
2269            if !has_session {
2270                return value;
2271            }
2272            let f = me.interner.intern(SESSION_SAVE_FN);
2273            me.te_call(f, vec![value])
2274        };
2275        let redirect_to = self.interner.intern("redirectTo");
2276        let permanent_to = self.interner.intern("permanentRedirectTo");
2277        let cut_after = self.interner.intern("cutAfter");
2278        let space = self.interner.intern(" ");
2279        let where_to = self.te_call(cut_after, vec![self.te_id(b_var), self.te_text(space)]);
2280        // ★ ONLY WHEN THE ANSWER CARRIES NO `Location` OF ITS OWN. The router's own moved answer
2281        // is the string `"302 /new"` and has no headers, so it must be rebuilt to get one. A ROUTE
2282        // that answered `redirectTo("/")` already has the header and an empty body — rebuilding
2283        // that one would take `cutAfter("", " ")`, hand `redirectTo` an empty target, and send the
2284        // browser a redirect to nowhere. Reading which case it is off the headers rather than off
2285        // the status is what tells the two apart.
2286        let blank_sym = self.interner.intern("");
2287        let no_location = self.te_bin(
2288            BinaryOpKind::Eq,
2289            self.te_id(loc_var),
2290            self.te_text(blank_sym),
2291        );
2292        let moved_302 = self.te_bin(BinaryOpKind::Eq, self.te_id(s_var), self.te_int(302));
2293        let moved_301 = self.te_bin(BinaryOpKind::Eq, self.te_id(s_var), self.te_int(301));
2294        let is_302 = self.te_bin(BinaryOpKind::And, moved_302, no_location);
2295        let is_301 = self.te_bin(BinaryOpKind::And, moved_301, no_location);
2296        hbody.push(Stmt::If {
2297            cond: is_302,
2298            then_block: {
2299                let moved = self.te_call(redirect_to, vec![where_to]);
2300                let moved = saved(self, moved);
2301                self.alloc_block(vec![Stmt::Return { value: Some(moved) }])
2302            },
2303            else_block: None,
2304        });
2305        hbody.push(Stmt::If {
2306            cond: is_301,
2307            then_block: {
2308                let moved = self.te_call(permanent_to, vec![where_to]);
2309                let moved = saved(self, moved);
2310                self.alloc_block(vec![Stmt::Return { value: Some(moved) }])
2311            },
2312            else_block: None,
2313        });
2314        let response_ty = self.interner.intern("Response");
2315        let answer = self.ctx.alloc_imperative_expr(Expr::New {
2316            type_name: response_ty,
2317            type_args: Vec::new(),
2318            init_fields: vec![
2319                (self.interner.intern("status"), self.te_id(s_var)),
2320                (self.interner.intern("contentType"), self.te_id(c_var)),
2321                (self.interner.intern("body"), self.te_id(b_var)),
2322                (self.interner.intern("headers"), self.te_id(h_var)),
2323            ],
2324        });
2325        let answer = saved(self, answer);
2326        hbody.push(Stmt::Return {
2327            value: Some(answer),
2328        });
2329        let request_ty: &'a TypeExpr<'a> = self
2330            .ctx
2331            .alloc_type_expr(TypeExpr::Named(self.interner.intern("Request")));
2332        let response_ty_expr: &'a TypeExpr<'a> =
2333            self.ctx.alloc_type_expr(TypeExpr::Named(response_ty));
2334        self.deferred_functions.push(Stmt::FunctionDef {
2335            name: handler_name,
2336            generics: Vec::new(),
2337            params: vec![(request_p, request_ty)],
2338            body: self.alloc_block(hbody),
2339            return_type: Some(response_ty_expr),
2340            is_native: false,
2341            native_path: None,
2342            is_exported: false,
2343            export_target: None,
2344            opt_flags: crate::optimization::OptimizationConfig::all_on(),
2345        });
2346        let name = self.interner.intern(ROUTE_BODY_WALK);
2347        self.deferred_functions.push(Stmt::FunctionDef {
2348            name,
2349            generics: Vec::new(),
2350            params: vec![(req_p, request_walk_ty)],
2351            body: self.alloc_block(arms),
2352            return_type: Some(text_ty),
2353            is_native: false,
2354            native_path: None,
2355            is_exported: false,
2356            export_target: None,
2357            opt_flags: crate::optimization::OptimizationConfig::all_on(),
2358        });
2359
2360        // ★ `routeWith(method, url, body)` AND ITS TWO SIBLINGS ARE WRAPPERS NOW, not the walks.
2361        //
2362        // They mean exactly what they always meant, and every caller — every test, every
2363        // `Serve fetches with routes`, `fullstack_todo_lock`'s dispatcher probe — keeps working
2364        // unchanged. The direction is what matters: a wrapper can always BUILD a request out of
2365        // three fields, while a three-field walk can never RECOVER the two fields it was not given.
2366        // Putting the whole request at the bottom is what lets a route bind `Request`, read a
2367        // cookie and carry a session; the conveniences sit on top with empty headers, which is
2368        // honest, because a caller who passed only a method and a url genuinely sent no headers.
2369        for (wrapper, walk, ret) in [
2370            ("routeWith", ROUTE_BODY_WALK, text_ty),
2371            ("routeStatusWith", ROUTE_STATUS_WALK, int_ty),
2372            ("routeContentTypeWith", ROUTE_CTYPE_WALK, text_ty),
2373        ] {
2374            let map_ty = self.interner.intern("Map");
2375            let text_arg = TypeExpr::Primitive(self.interner.intern("Text"));
2376            let empty_map = self.ctx.alloc_imperative_expr(Expr::New {
2377                type_name: map_ty,
2378                type_args: vec![text_arg.clone(), text_arg],
2379                init_fields: Vec::new(),
2380            });
2381            let built = self.ctx.alloc_imperative_expr(Expr::New {
2382                type_name: self.interner.intern("Request"),
2383                type_args: Vec::new(),
2384                init_fields: vec![
2385                    (self.interner.intern("method"), self.te_id(method_p)),
2386                    (self.interner.intern("path"), self.te_id(url_p)),
2387                    (self.interner.intern("body"), self.te_id(body_p)),
2388                    (self.interner.intern("headers"), empty_map),
2389                    (self.interner.intern("query"), empty_map),
2390                ],
2391            });
2392            let walk_sym = self.interner.intern(walk);
2393            let call = self.te_call(walk_sym, vec![built]);
2394            let wrapper_sym = self.interner.intern(wrapper);
2395            self.deferred_functions.push(Stmt::FunctionDef {
2396                name: wrapper_sym,
2397                generics: Vec::new(),
2398                params: vec![(method_p, text_ty), (url_p, text_ty), (body_p, text_ty)],
2399                body: self.alloc_block(vec![Stmt::Return { value: Some(call) }]),
2400                return_type: Some(ret),
2401                is_native: false,
2402                native_path: None,
2403                is_exported: false,
2404                export_target: None,
2405                opt_flags: crate::optimization::OptimizationConfig::all_on(),
2406            });
2407        }
2408
2409        // ★ `routeHeader(method, url, name)` — what a route SET, readable without a server.
2410        //
2411        // The handler already puts these on the wire; this is the same table asked the same
2412        // question from a test, which is what makes "the login set a cookie" an assertion rather
2413        // than something you have to start a process and dial a socket to find out.
2414        let name_p = self.interner.intern("__lgx_hname");
2415        for (wrapper, with_body) in [("routeHeaderWith", true), ("routeHeader", false)] {
2416            let body_expr = if with_body {
2417                let text_arg = TypeExpr::Primitive(self.interner.intern("Text"));
2418                let empty_map = self.ctx.alloc_imperative_expr(Expr::New {
2419                    type_name: self.interner.intern("Map"),
2420                    type_args: vec![text_arg.clone(), text_arg],
2421                    init_fields: Vec::new(),
2422                });
2423                let built = self.ctx.alloc_imperative_expr(Expr::New {
2424                    type_name: self.interner.intern("Request"),
2425                    type_args: Vec::new(),
2426                    init_fields: vec![
2427                        (self.interner.intern("method"), self.te_id(method_p)),
2428                        (self.interner.intern("path"), self.te_id(url_p)),
2429                        (self.interner.intern("body"), self.te_id(body_p)),
2430                        (self.interner.intern("headers"), empty_map),
2431                        (self.interner.intern("query"), empty_map),
2432                    ],
2433                });
2434                let walk = self.interner.intern(ROUTE_HEADERS_WALK);
2435                let table = self.te_call(walk, vec![built]);
2436                let read = self.interner.intern("headerIn");
2437                self.te_call(read, vec![table, self.te_id(name_p)])
2438            } else {
2439                let blank = self.interner.intern("");
2440                let long = self.interner.intern("routeHeaderWith");
2441                self.te_call(
2442                    long,
2443                    vec![
2444                        self.te_id(method_p),
2445                        self.te_id(url_p),
2446                        self.te_text(blank),
2447                        self.te_id(name_p),
2448                    ],
2449                )
2450            };
2451            let params = if with_body {
2452                vec![
2453                    (method_p, text_ty),
2454                    (url_p, text_ty),
2455                    (body_p, text_ty),
2456                    (name_p, text_ty),
2457                ]
2458            } else {
2459                vec![(method_p, text_ty), (url_p, text_ty), (name_p, text_ty)]
2460            };
2461            let wrapper_sym = self.interner.intern(wrapper);
2462            self.deferred_functions.push(Stmt::FunctionDef {
2463                name: wrapper_sym,
2464                generics: Vec::new(),
2465                params,
2466                body: self.alloc_block(vec![Stmt::Return {
2467                    value: Some(body_expr),
2468                }]),
2469                return_type: Some(text_ty),
2470                is_native: false,
2471                native_path: None,
2472                is_exported: false,
2473                export_target: None,
2474                opt_flags: crate::optimization::OptimizationConfig::all_on(),
2475            });
2476        }
2477    }
2478
2479    /// Every STYLED component this root can reach, itself first, in placement order.
2480    ///
2481    /// The same closure `html_reachable` walks, kept separate because that one answers a yes/no
2482    /// about one name and this one needs the whole set in a stable order — a page's stylesheets
2483    /// must come out the same on every build, or two identical sources produce two different files.
2484    pub(crate) fn styled_reachable(&self, root: Symbol, all: &[ComponentDef<'a>]) -> Vec<Symbol> {
2485        let mut seen = vec![root];
2486        let mut queue = vec![root];
2487        while let Some(cur) = queue.pop() {
2488            let Some(def) = all.iter().find(|d| d.name == cur) else {
2489                continue;
2490            };
2491            let Some(view) = def.view.as_ref() else {
2492                continue;
2493            };
2494            for child in placed_components(&view.nodes) {
2495                if !seen.contains(&child) {
2496                    seen.push(child);
2497                    queue.push(child);
2498                }
2499            }
2500        }
2501        seen.retain(|n| all.iter().any(|d| d.name == *n && d.style.is_some()));
2502        seen
2503    }
2504
2505    /// Is this component REACHED by an `html of` — itself named by one, or placed (transitively)
2506    /// in the view of something that is?
2507    ///
2508    /// ★ THE DEMAND HAS TO FOLLOW THE TREE. `html of App` is the only thing an author writes, and
2509    /// a page is built out of rows and cards nobody names — so a rule that fired only on the
2510    /// spelled name emitted the page's own renderer and none of the children it calls, which is a
2511    /// call to a function that does not exist. Same shape as the JSON derive closing over
2512    /// record-typed fields: the seed is what the source says, and the set is its CLOSURE.
2513    pub(crate) fn html_reachable(
2514        &self,
2515        name: Symbol,
2516        all: &[ComponentDef<'a>],
2517        routed: &[Symbol],
2518    ) -> bool {
2519        if self.names_html_of(name) || routed.contains(&name) {
2520            return true;
2521        }
2522        // ★ A `## Route … -> View:` IS A SECOND ROOT. Its template lowers through the same server
2523        // walk and calls `__html_<Child>` for every component it places, so a page reached only
2524        // from a route used to compile into a call to a function nothing emitted — the route built
2525        // and the server answered with a missing name. `routed` carries those roots.
2526        let mut seeds: Vec<Symbol> = all
2527            .iter()
2528            .filter(|d| self.names_html_of(d.name))
2529            .map(|d| d.name)
2530            .collect();
2531        seeds.extend(routed.iter().copied());
2532        let mut seen: Vec<Symbol> = seeds.clone();
2533        while let Some(cur) = seeds.pop() {
2534            let Some(def) = all.iter().find(|d| d.name == cur) else {
2535                continue;
2536            };
2537            let Some(view) = def.view.as_ref() else {
2538                continue;
2539            };
2540            for child in placed_components(&view.nodes) {
2541                if child == name {
2542                    return true;
2543                }
2544                if !seen.contains(&child) {
2545                    seen.push(child);
2546                    seeds.push(child);
2547                }
2548            }
2549        }
2550        false
2551    }
2552
2553    /// Does the program say `html of <name>` anywhere?
2554    ///
2555    /// Scanned from the TOKEN STREAM, so it is whole-word by construction — the same way the
2556    /// JSON derive finds the encoders a program reaches.
2557    pub(crate) fn names_html_of(&self, name: Symbol) -> bool {
2558        self.tokens.windows(3).any(|w| {
2559            self.interner
2560                .resolve(w[0].lexeme)
2561                .eq_ignore_ascii_case("html")
2562                && self
2563                    .interner
2564                    .resolve(w[1].lexeme)
2565                    .eq_ignore_ascii_case("of")
2566                && w[2].lexeme == name
2567        })
2568    }
2569
2570    /// Whether the program renders markup on a server: a `## Route`, or an `html of` anywhere.
2571    pub(crate) fn renders_on_a_server(&self, statements: &[Stmt<'a>]) -> bool {
2572        statements
2573            .iter()
2574            .any(|s| matches!(s, Stmt::RouteDef { .. }))
2575            || self.tokens.windows(2).any(|w| {
2576                self.interner
2577                    .resolve(w[0].lexeme)
2578                    .eq_ignore_ascii_case("html")
2579                    && self
2580                        .interner
2581                        .resolve(w[1].lexeme)
2582                        .eq_ignore_ascii_case("of")
2583            })
2584    }
2585
2586    /// The component a template is drawn as: its parameters its props, and nothing of its own —
2587    /// no state, no actions, no style. A route's template, a `Head:`, a `-> View` function's.
2588    pub(crate) fn template_owner(
2589        &self,
2590        name: Symbol,
2591        params: Vec<(Symbol, &'a TypeExpr<'a>)>,
2592        view: &crate::ast::view::ViewDef<'a>,
2593    ) -> ComponentDef<'a> {
2594        ComponentDef {
2595            kind: crate::ast::component::ComponentKind::Component,
2596            name,
2597            props: params,
2598            uses: Vec::new(),
2599            state: Vec::new(),
2600            events: Vec::new(),
2601            actions: Vec::new(),
2602            view: Some(view.clone()),
2603            head: None,
2604            style: None,
2605            span: view.span,
2606        }
2607    }
2608
2609    /// ★ ONE RENDERER, TWO DESTINATIONS. `draw` — records written onto the tape by the serving walk
2610    /// (`LowerTrail::serving`) — is drawn into a root no host holds on a tape of its own, committed,
2611    /// serialized by `vdSerializeChildren`, and the root discarded, leaving the markup in `out`. The markup is the
2612    /// retained tree the browser's paint of the same records would hold, serialized as a host
2613    /// serializes a document — escaping, void elements, attribute order and all — so a served page
2614    /// and a painted one are one document by construction, and nothing of the drawing (tree, tape,
2615    /// patch stream) outlives it.
2616    pub(crate) fn serve_markup(
2617        &mut self,
2618        draw: Vec<Stmt<'a>>,
2619        out: Symbol,
2620        body: &mut Vec<Stmt<'a>>,
2621    ) {
2622        let root = self.interner.intern("__lgx_served");
2623        let problem = self.interner.intern("__lgx_problem");
2624        let stash = self.interner.intern("__lgx_outer_tape");
2625        // ★ RE-ENTRANT: a root may be drawn while another draw is in flight — `html of` in a
2626        // served component's state, `{raw html of C}` in a painted view — so the tape in flight is
2627        // set aside, and put back once this root is serialized, exactly as it was.
2628        let stashed = self.vd_expr("vdTapeStash", Vec::new());
2629        body.push(Stmt::Let {
2630            var: stash,
2631            ty: None,
2632            value: stashed,
2633            mutable: false,
2634        });
2635        let detached = self.vd_expr("vdNewRoot", vec![self.te_int(-1)]);
2636        body.push(Stmt::Let {
2637            var: root,
2638            ty: None,
2639            value: detached,
2640            mutable: false,
2641        });
2642        body.extend(draw);
2643        let committed = self.vd_expr("vdCommit", vec![self.te_id(root)]);
2644        body.push(Stmt::Let {
2645            var: problem,
2646            ty: None,
2647            value: committed,
2648            mutable: false,
2649        });
2650        let blank = self.interner.intern("");
2651        let refused = self.te_bin(
2652            BinaryOpKind::NotEq,
2653            self.te_id(problem),
2654            self.te_text(blank),
2655        );
2656        let give_up = vec![
2657            self.vd_call("vdDiscardRoot", vec![self.te_id(root)]),
2658            self.vd_call("vdTapeUnstash", vec![self.te_id(stash)]),
2659            Stmt::Fail {
2660                reason: self.te_id(problem),
2661            },
2662        ];
2663        body.push(Stmt::If {
2664            cond: refused,
2665            then_block: self.alloc_block(give_up),
2666            else_block: None,
2667        });
2668        let markup = self.vd_expr("vdSerializeChildren", vec![self.te_id(root)]);
2669        let text_ty = self.text_type();
2670        body.push(Stmt::Let {
2671            var: out,
2672            ty: Some(text_ty),
2673            value: markup,
2674            mutable: false,
2675        });
2676        body.push(self.vd_call("vdDiscardRoot", vec![self.te_id(root)]));
2677        body.push(self.vd_call("vdTapeUnstash", vec![self.te_id(stash)]));
2678    }
2679
2680    /// The records of every stylesheet the root `root` reaches, scoped as the browser scopes it —
2681    /// written once, ahead of the markup, as the mount writes them (`repaint_def`).
2682    pub(crate) fn served_sheets(
2683        &mut self,
2684        root: Symbol,
2685        all: &[ComponentDef<'a>],
2686    ) -> Vec<Stmt<'a>> {
2687        let mut out = Vec::new();
2688        for styled in self.styled_reachable(root, all) {
2689            let Some(sheet) = all
2690                .iter()
2691                .find(|d| d.name == styled)
2692                .and_then(|d| d.style.clone())
2693            else {
2694                continue;
2695            };
2696            let class = scope_class(self.interner.resolve(styled));
2697            let css = self.interner.resolve(sheet.css).to_string();
2698            let Ok(scoped) = crate::lgx::css::scope(&css, &class) else {
2699                continue;
2700            };
2701            let tag = self.interner.intern("style");
2702            let css_sym = self.interner.intern(&scoped);
2703            out.push(self.vd_call("vdOpen", vec![self.te_text(tag)]));
2704            out.push(self.vd_call("vdText", vec![self.te_text(css_sym)]));
2705            out.push(self.vd_call("vdClose", Vec::new()));
2706        }
2707        out
2708    }
2709
2710    /// `nodes` as `owner` draws them, by the serving walk — the records only; the caller decides
2711    /// where they are drawn.
2712    pub(crate) fn serving_draw(
2713        &mut self,
2714        nodes: &[ViewNode<'a>],
2715        owner: &ComponentDef<'a>,
2716        all: &[ComponentDef<'a>],
2717    ) -> Vec<Stmt<'a>> {
2718        let mut draw = Vec::new();
2719        let mut next = 0usize;
2720        let outer_locals = std::mem::take(&mut self.lgx_views.locals);
2721        self.lower_lgx_nodes(
2722            nodes,
2723            &mut next,
2724            &mut draw,
2725            all,
2726            owner,
2727            &LowerTrail::serving(),
2728        );
2729        self.lgx_views.locals = outer_locals;
2730        draw
2731    }
2732
2733    /// Why `html of <def>`, or a server root that places `def`, cannot be served — the constructs
2734    /// the server has no rendering for, named in the author's words.
2735    fn refuse_serving(
2736        &self,
2737        def: &ComponentDef<'a>,
2738        all: &[ComponentDef<'a>],
2739    ) -> crate::parser::ParseResult<()> {
2740        let Some(view) = &def.view else { return Ok(()) };
2741        if let Err(why) = view_html_lowerable_with(&view.nodes, all, self.interner) {
2742            let name = self.interner.resolve(def.name).to_string();
2743            return Err(crate::error::ParseError {
2744                kind: crate::error::ParseErrorKind::Custom(format!(
2745                    "`html of {name}` asks for this component as server-rendered markup, and {}. \
2746                     A server has no document, so the server-side walk covers a smaller set of the \
2747                     view tree than a browser does — and rendering the page SHORT would be worse \
2748                     than saying so, because the reader would see a plausible document with a \
2749                     piece missing and nothing anywhere would say which. In a browser this view is \
2750                     ordinary: `Mount {name}.` renders it in full. So either render it there, or \
2751                     replace the construct in `{name}`'s view that the server cannot draw.",
2752                    why.message
2753                )),
2754                span: why.span,
2755            });
2756        }
2757        Ok(())
2758    }
2759
2760    /// `__serve_<C>(props…, slot, blocks…)` for every component a server root reaches, and
2761    /// `__html_<C>()` for every component `html of` names.
2762    ///
2763    /// `__serve_<C>` is `C`'s view drawn by the serving walk over its INITIAL state — its fields
2764    /// bound as locals from their initializers, the derived ones after them — writing the records
2765    /// its markup is made of and nothing a live document needs. Nothing is kept between two
2766    /// calls: a server answering two requests draws two independent instances, because an instance
2767    /// never outlives the call that draws it. `__html_<C>()` draws it into a root of its own and
2768    /// answers the serialized markup (`serve_markup`), stylesheets first.
2769    ///
2770    /// ★ EMITTED ON USE, NOT ON DECLARATION. A page that only mounts a component carries no server
2771    /// render of it; only a root the server draws — `html of`, a `## Route … -> View:`, a `-> View`
2772    /// function's template — demands one, with the closure of what it places.
2773    pub(crate) fn lower_component_serve(
2774        &mut self,
2775        def: &ComponentDef<'a>,
2776        all: &[ComponentDef<'a>],
2777        routed: &[Symbol],
2778    ) -> crate::parser::ParseResult<()> {
2779        let Some(view) = &def.view else { return Ok(()) };
2780        if !self.html_reachable(def.name, all, routed) {
2781            return Ok(());
2782        }
2783        self.refuse_serving(def, all)?;
2784        let component = self.interner.resolve(def.name).to_string();
2785        let depth = self.interner.intern(draw_depth_param());
2786        let mut body: Vec<Stmt<'a>> = Vec::new();
2787        if on_placement_cycle(def, all) {
2788            body.push(self.refuse_past_render_depth(depth, def.name));
2789        }
2790        for field in def.state.iter().filter(|f| !f.derived) {
2791            let bound = self.state_field_let(field, field.init, true);
2792            body.push(bound);
2793        }
2794        for field in def.state.iter().filter(|f| f.derived) {
2795            let bound = self.state_field_let(field, field.init, false);
2796            body.push(bound);
2797        }
2798        let mut next = 0usize;
2799        let outer_locals = std::mem::take(&mut self.lgx_views.locals);
2800        let trail = LowerTrail::serving_at(depth);
2801        match super::render::extended_layout(&view.nodes)
2802            .and_then(|l| all.iter().find(|d| d.name == l))
2803        {
2804            // `{extends L}`: the blocks this view fills are drawn here, in its own scope, as ranges;
2805            // the layout draws itself around them, one deeper, with this component's `{children}`
2806            // as its own.
2807            Some(layout) => {
2808                let mut args =
2809                    vec![self.te_bin(BinaryOpKind::Add, self.te_id(depth), self.te_int(1))];
2810                let slot = self.interner.intern(render_slot_param());
2811                args.push(self.te_id(slot));
2812                for block in slot_blocks(layout) {
2813                    let fill = view.nodes.iter().enumerate().find_map(|(at, n)| match n {
2814                        ViewNode::Block {
2815                            name,
2816                            default: Some(b),
2817                            ..
2818                        } if *name == block => Some((at, &b[..])),
2819                        _ => None,
2820                    });
2821                    args.push(match fill {
2822                        Some((at, b)) => {
2823                            let branch = trail.branch(&format!("{at}."));
2824                            self.slot_range(b, &mut next, &mut body, all, def, &branch)
2825                        }
2826                        None => self.te_int(0),
2827                    });
2828                }
2829                let serve = self
2830                    .interner
2831                    .intern(&serve_fn_name(self.interner.resolve(layout.name)));
2832                body.push(Stmt::Call {
2833                    function: serve,
2834                    args,
2835                });
2836            }
2837            None => self.lower_lgx_nodes(&view.nodes, &mut next, &mut body, all, def, &trail),
2838        }
2839        self.lgx_views.locals = outer_locals;
2840        let int_ty = self.int_type();
2841        let mut params = vec![(depth, int_ty)];
2842        for (prop, ty) in &def.props {
2843            if is_callback_type(ty, self.interner) {
2844                continue;
2845            }
2846            let ty = self.lowered_type(ty);
2847            params.push((*prop, ty));
2848        }
2849        params.push((self.interner.intern(render_slot_param()), int_ty));
2850        for block in slot_blocks(def) {
2851            params.push((
2852                self.interner
2853                    .intern(&block_slot_param(self.interner.resolve(block))),
2854                int_ty,
2855            ));
2856        }
2857        self.deferred_functions.push(Stmt::FunctionDef {
2858            name: self.interner.intern(&serve_fn_name(&component)),
2859            generics: Vec::new(),
2860            params,
2861            body: self.alloc_block(body),
2862            return_type: None,
2863            is_native: false,
2864            native_path: None,
2865            is_exported: false,
2866            export_target: None,
2867            opt_flags: crate::optimization::OptimizationConfig::all_on(),
2868        });
2869        if !self.names_html_of(def.name) {
2870            return Ok(());
2871        }
2872        // `html of <C>`: the root has no caller, so no props, no children, and every block its own.
2873        if let Some((prop, _)) = def.props.first() {
2874            let prop = self.interner.resolve(*prop).to_string();
2875            return Err(crate::error::ParseError {
2876                kind: crate::error::ParseErrorKind::Custom(format!(
2877                    "`html of {component}` draws `{component}` as a page nobody placed, and `{component}` \
2878                     takes `{prop}`, which only a placement hands it. Place `<{component} {prop}={{…}}/>` in \
2879                     a component `html of` draws, or in a `## Route … -> View:` template."
2880                )),
2881                span: def.span,
2882            });
2883        }
2884        let text_ty = self.text_type();
2885        let out = self.interner.intern("__lgx_html");
2886        let mut draw = self.served_sheets(def.name, all);
2887        let mut args = vec![self.te_int(1)];
2888        args.push(self.te_int(0));
2889        args.extend(self.block_slot_zeros(def));
2890        draw.push(Stmt::Call {
2891            function: self.interner.intern(&serve_fn_name(&component)),
2892            args,
2893        });
2894        let mut body = Vec::new();
2895        self.serve_markup(draw, out, &mut body);
2896        body.push(Stmt::Return {
2897            value: Some(self.te_id(out)),
2898        });
2899        self.deferred_functions.push(Stmt::FunctionDef {
2900            name: self.interner.intern(&html_fn_name(&component)),
2901            generics: Vec::new(),
2902            params: Vec::new(),
2903            body: self.alloc_block(body),
2904            return_type: Some(text_ty),
2905            is_native: false,
2906            native_path: None,
2907            is_exported: false,
2908            export_target: None,
2909            opt_flags: crate::optimization::OptimizationConfig::all_on(),
2910        });
2911        Ok(())
2912    }
2913}
2914
2915/// Whether no definition in `statements` draws a `View:` — a program with no page to place a view
2916/// in, where every reader of a view reads its markup.
2917pub(crate) fn no_page_places_a_view(statements: &[Stmt<'_>]) -> bool {
2918    statements
2919        .iter()
2920        .all(|s| !matches!(s, Stmt::ComponentDef(d) if d.view.is_some()))
2921}
2922
2923/// The function a `-> View` function's served half lowers to: the same view as HTML text.
2924///
2925/// Named apart from a component's `__html_<Name>`, so a function and a component of one name never
2926/// collide.
2927pub fn view_html_fn_name(function: &str) -> String {
2928    format!("__lgx_viewhtml_{function}")
2929}
2930
2931/// Route every call of a view function that has a twin to its twin — on a server, reading a view
2932/// means reading its markup (`lower_view_function_twins`).
2933struct TwinCalls<'a, 't> {
2934    arenas: crate::ast_map::Arenas<'a>,
2935    twins: &'t [(Symbol, Symbol)],
2936}
2937
2938impl<'a, 't> TwinCalls<'a, 't> {
2939    fn twin_of(&self, function: Symbol) -> Option<Symbol> {
2940        self.twins
2941            .iter()
2942            .find(|(f, _)| *f == function)
2943            .map(|(_, twin)| *twin)
2944    }
2945}
2946
2947impl<'a, 't> crate::ast_map::AstMap<'a> for TwinCalls<'a, 't> {
2948    fn arenas(&self) -> crate::ast_map::Arenas<'a> {
2949        self.arenas
2950    }
2951
2952    fn map_expr(&mut self, e: &'a Expr<'a>) -> &'a Expr<'a> {
2953        if let Expr::Call { function, args } = e {
2954            if let Some(twin) = self.twin_of(*function) {
2955                let args = args.iter().map(|a| self.map_expr(a)).collect();
2956                return self.arenas.exprs.alloc(Expr::Call {
2957                    function: twin,
2958                    args,
2959                });
2960            }
2961        }
2962        crate::ast_map::walk_expr(self, e)
2963    }
2964
2965    fn map_stmt(&mut self, s: &Stmt<'a>) -> Stmt<'a> {
2966        if let Stmt::Call { function, args } = s {
2967            if let Some(twin) = self.twin_of(*function) {
2968                let args = args.iter().map(|a| self.map_expr(a)).collect();
2969                return Stmt::Call {
2970                    function: twin,
2971                    args,
2972                };
2973            }
2974        }
2975        crate::ast_map::walk_stmt(self, s)
2976    }
2977}