calendar
All repositories: gitoria
162.8 KB
// The framework's server half — the SERVER REALM's entrypoint, and the EDGE// MODULE for the carriers the manifest declares.// Its jobs: answer a URL with the HTML of a constructed component (SSR), ship// the browser the client, the View tree and the state to continue from, carry// the boundary in both directions (a websocket on the same port, a POST for a// peer that has none), and run the server faces an inbound emit names.import { NativeWebSocketServer, Response, randomToken } from 'hl:http1'import HlwView from './view.hl'import HlwRouter from './router.hl'import HlwSessions from './sessions.hl'import HlwSession from './session.hl'// the Style walk — named Css here because `Style` is a well-known MEMBER nameimport HlwCss from './css.hl'import HlwCompile from './compile.hl'import { readFile, exists, watch } from 'hl:fs'import { file, env, args } from 'hl:proc'import { now } from 'hl:time'import { sha256 } from 'hl:crypto'import { newline, tab } from './view.hl'Hybrid routes = [] // bound by `new WebFrameworkServer(routes = …)`// THE PROJECT'S STYLES FILE, declared by the app as a property of the framework:// `new WebFrameworkServer(…, styles = './styles.hl')`. A file of statics and one// `Style { }` root member (COMPONENTS §4) — the design tokens and the global// rules, under every component's own Style in the one stylesheet. null: none.styles = null // the app's styles file: the class it imported, or a path// WHO AN OUTWARD EVENT IS FOR (creator, 2026-09-12): per event, a function the app// declares — `audience = { postCreated = (p, session) => … }` — called for every// connection that has a listener for the event mounted, with the event's// arguments and that connection's session; a true answer sends the frame. An// event with no entry reaches the emitting connection only. Only the app knows// who is entitled; the face stays pure; the framework applies the answer.Hybrid audience = {}// THE SESSION STORE (the archive's manifest members, mission 252/255): nothing said →// `<project>/.sessions`; a path → that path; `false` → memory only. The three clocks// are seconds; null takes the layer's defaults (14 days valid, 15 min resident, sweep// every 5 min).// THE DEV WATCHER (the archive's `watch`): every save under the project arrives as an// event; a save that changes the analysis re-reads the graph, drops every cache and tells// every open tab to reload. Off by default — a deploy never re-analyses itself.Boolean minify = false // production: one-line HTML, compact JavaScript modulesBoolean watchMode = truelastChange = 0 // epoch ms of the last save acted on (the debounce)String | Boolean sessionDir = nullNumber sessionMaxAge = nullNumber sessionIdle = nullNumber sweepInterval = null// THE SESSION COOKIE'S NAME (ticket #10). Cookies ignore the port, so two apps on one// host that both answered `hlsid` overwrote each other's sessions; an app names its own.// null: `hlsid`.String sessionCookie = null// THE COOKIE'S `Secure` FLAG (ticket #27): true when the app is reached over https —// behind a TLS proxy the server itself speaks plain http and cannot tell, so the app// says so. The browser then never sends the session over plain http. false: not set.Boolean sessionSecure = false// THE COOKIE'S `Domain` (ticket #44): 'example.org' shares one session with every// <sub>.example.org, which a host-only cookie cannot. null: host-only, no attribute.String sessionDomain = null\* THE PORT: the app's own `--port=N` argument (hybriel hands every argumentafter the entry to the program, creator ruling 2026-09-27), else theconstructor's, else HL_PORT (hybriel resolves PORT and `.env` into it),else 8080. *\Number port = null// THE OFFLINE ALLOW-LIST (COMPONENTS §10, D38): the pages this app promises with the// network down, named as the route table names them — the imported class or a path// string. null: no service worker, no cache, no IndexedDB, nothing on the device.offline = null// THE DEAD-SOCKET CLOCK (COMPONENTS §10), in seconds. The browser pings every `pingEvery`,// and a socket whose pong has not come back within `pongWithin` is taken for dead: it is// closed, and the reconnect and the queue's replay start. A network that drops without a// close frame is noticed within pingEvery + pongWithin (35 s by default). null: 25 and 10.Number pingEvery = nullNumber pongWithin = null// AN UPLOAD THAT GOES SILENT — no progress, no answer — is read as dropped after this// many seconds (COMPONENTS §10, an emit's file). null: 15.Number uploadStall = null// THE LARGEST FILE AN EMIT MAY CARRY (ticket #94), in bytes: a bigger one is refused// at its first request, and the emit ends with that error. 1 GiB.Number uploadMax = 1073741824String host = null // the interface to bind; null → HL_HOST/HOST, the manifest's `host`, 0.0.0.0// THE ONE RUNTIME URL, and the reason it is a member and not a literal in two// places: a module's `import … from "./hl-runtime.js"` resolves against the// MODULE's own url, so a server answering modules at two directory depths hands// the browser two runtime urls — two ES-module instances, two `globalEvents`// buses, two `__hlRealm` variables, and a crossing emit announced on a bus the// edge module never taps. Every module this server compiles is handed this one// specifier (hlJs's third argument).// THE DIRECTORY THE FRAMEWORK ANSWERS FROM, and the reason it is its own member:// a package's browser half is served BESIDE THE RUNTIME, and the compiler derives// that url from the runtime's own (`js_module.zig browserHalfUrl`:// `<dirname(runtime)>/<pkg>/client.js`). Two sides deriving one url from one// directory is what keeps the module's `import` and this server's route table// from drifting apart.static halfDir = '/__hl'static runtimeUrl = halfDir + '/hl-runtime.js'clientUrl = halfUrl('web')styleUrl = halfDir + '/app.css'// THE BUILD'S VERSION IS IN EVERY URL THE BROWSER CACHES (after ticket #82): the// runtime, the package halves, the component modules and the stylesheet were served// at fixed urls, and a browser or Cloudflare kept the old client against a new// deploy's pages. Each is now asked for as `<url>?v=<hash>`, the hash of everything// this build serves, and answered with a year's `immutable` caching — a new build is// a new url. The modules are COMPILED against this mark and it is replaced with the// hash when they are served: the hash is of the compiled texts, so it cannot be in// them while they are compiled, and a binary's baked modules carry the mark too (the// one form both paths share). The route patterns stay the bare paths.static buildMark = '?v=__hlbuild__'static runtimeSpec = runtimeUrl + buildMark// the web app manifest and the service worker's entry script (COMPONENTS §10)static manifestUrl = halfDir + '/manifest.webmanifest'static workerUrl = halfDir + '/sw.js'// THE FRAMEWORK'S OWN URLS ARE ROUTES (creator: "just use routes"), appended to// the app's table with `routes[] = …` — the append that takes on a caller's// pinned argument during construction, where `routes = routes + […]` is dropped.// Three of them here — the runtime, the REST carrier and the app's one// stylesheet — and then one per PACKAGE BROWSER HALF the app's graph holds,// appended below where the graph is known. There is no catch-all that compiles// any graph file a url names.routes[] = { pattern = runtimeUrl framework = true function = (route, req) => {return new Response(runtime, { headers = cached(req, 'text/javascript; charset=utf-8') })} }// THE REST CARRIER, the same emit/ack pair for a peer that cannot hold a socket// (SPEC: `POST /__hl/emit`).// ITS SESSION IS THE COOKIE'S (creator, 2026-09-14): this is an HTTP request like// the page request, so it resolves the SAME session the page route would for that// cookie — a `session.user` a face writes here is what the next document reads.// Before this the face was handed null and a login over the fallback was lost.// AN OUTWARD EMIT RAISED UNDER IT RIDES BACK IN THE ANSWER: there is no// connection to push to, so the frames a face raised for the CALLER are// collected while it runs and shipped in the ack as `emits`, which the client// half delivers before it resolves the emit. The audience fan-out to the other// connections of that user is the same walk it always was.routes[] = { pattern = '/__hl/emit' framework = true function = (route, req) => {if (req.method != 'POST') { return notFound(req.path) }let s = sessions.resolve(req.headers['cookie'] != null ? req.headers['cookie'] : req.headers['Cookie'])let fresh = s == nullif (fresh) { s = sessions.mint() }// the session travels in a MEMBER, not an argument: a call argument is a copy,// an instance inside it included, and a copied session loses the face's writepostSession = spostHost = hostOf(req.headers['host'])postHeaders = requestHeaders(req.headers)let res = new Response(inbound(null, JSON.parse(req.body)), { headers = { 'Content-Type' = 'application/json; charset=utf-8' } })if (fresh) { res.headers['Set-Cookie'] = sessions.cookieHeader(s.id) }return res} }// A FILE IN AN EMIT TRAVELS OVER HTTP (ticket #94, creator 2026-09-25): the client half// sends each file of an `emit server …` here, in parts, before the emit itself; the emit's// frame then names the upload, and `inbound` hands the face the file in its place.// POST /__hl/upload { name, type, size, q, n } → { id, have } (an upload; see uploadRequest)// POST /__hl/upload?id=&at= one part, raw → { have } (appended when `at` is what arrived so far)// GET /__hl/upload?id= → { have } (after a dropped connection)// An upload belongs to the session of the cookie that started it, and only an emit// of that session can claim it. A part cut short by a dropped connection keeps what// arrived; the client asks for `have` and goes on from there.routes[] = { pattern = '/__hl/upload' framework = true function = (route, req) => {let s = sessions.resolve(req.headers['cookie'] != null ? req.headers['cookie'] : req.headers['Cookie'])let fresh = s == nullif (fresh) { s = sessions.mint() }let res = uploadRequest(&s, req)if (fresh) { res.headers['Set-Cookie'] = sessions.cookieHeader(s.id) }return res} }// THE APP'S ONE STYLESHEET — the `styles` file's rules and every mounted// component's `Style { }`, walked in style.hl and cached after the first ask.routes[] = { pattern = styleUrl framework = true function = (route, req) => {return new Response(stylesheet(), { headers = cached(req, 'text/css; charset=utf-8') })} }// THE COMPONENT MODULES ARE ROUTES TOO — the framework's, not the app's: one per// file a page route mounts and per wrapper above it (the graph's `parent`// chain), served at the file's own `.hl` url as the browser's projection. A// browser executes a module by its MIME type, so `text/javascript` at// `/components/home.hl` is a module, and the url is the path the author wrote.// Nothing else is served as a module: a file no page route reaches has no url.// THE LANGUAGE GRAPH, AND THE REALMS ARE THIS PACKAGE'S (creator, 2026-09-12: "the project.hl// defines no realms so the framework just fills them itself"). This file is the server// realm's entrypoint, client.hl beside it the browser's; the app reaches both by// importing hl:web, and the graph follows that reference into the package.graph = hlProject(realms = {server = { entrypoint = './WebFramework.hl' }client = { entrypoint = './client.hl' }}transports = [['websocket' 'client' 'server']['websocket' 'server' 'client']['rest' 'client' 'server']])gen = graph.analysisGen // the syntax reflection's handle: the routes below read Views// TWO WEB FRAMEWORKS IN ONE APP IS NOT AN APP (creator, 15 Sep: project.hl switched// to the other framework the repo had then while styles.hl still said 'hl:web/css').// Both packages load, both `on server page` faces answer the same navigation, and// the second answers on a blank instance — a NotCallable deep inside the other framework's dispatch, which// says nothing about the two import rows that caused it. So it is refused HERE,// before anything is served, naming both rows.oneFrameworkOnly()// A VALUE-FORM EMIT HAS ONE ANSWERER (SPEC "An emit ANSWERS", ticket #20): checked here,// before anything is served, because the wire does not say which form an emit wasoneAnswererEach()// THE APP'S SETTINGS come from the file that constructed this one when they were not// passed (the archive's `configure()`): `styles`, `audience`, `sessionDir`, the clocks,// `watchMode`, `minify`, `port`cfg = hlConstructor()if (cfg != null) {if (styles == null && cfg.styles != null) { styles = cfg.styles }if (cfg.audience != null && audience.keys().length == 0) { audience = cfg.audience }if (sessionDir == null && cfg.sessionDir != null) { sessionDir = cfg.sessionDir }if (sessionMaxAge == null && cfg.sessionMaxAge != null) { sessionMaxAge = cfg.sessionMaxAge }if (sessionIdle == null && cfg.sessionIdle != null) { sessionIdle = cfg.sessionIdle }if (sweepInterval == null && cfg.sweepInterval != null) { sweepInterval = cfg.sweepInterval }if (sessionCookie == null && cfg.sessionCookie != null) { sessionCookie = cfg.sessionCookie }if (cfg.sessionSecure != null && sessionSecure == false) { sessionSecure = cfg.sessionSecure }if (sessionDomain == null && cfg.sessionDomain != null) { sessionDomain = cfg.sessionDomain }if (cfg.watchMode != null) { watchMode = cfg.watchMode }if (cfg.minify != null) { minify = cfg.minify }if (cfg.port != null) { port = cfg.port }if (offline == null && cfg.offline != null) { offline = cfg.offline }if (pingEvery == null && cfg.pingEvery != null) { pingEvery = cfg.pingEvery }if (pongWithin == null && cfg.pongWithin != null) { pongWithin = cfg.pongWithin }if (uploadStall == null && cfg.uploadStall != null) { uploadStall = cfg.uploadStall }if (cfg.uploadMax != null) { uploadMax = cfg.uploadMax }}served = {}mounted = []for (r of routes) { if (r.component != null) { mounted[] = keyOf(r.component) } }for (k of mounted) {let key = k// a file the graph does not hold is refused at its route row, below (servedKeys// → routeComponentKey: this walk reads the evaluated table, which has no lines)if (graph.files[key] == null) { key = null }while (key != null) {serveComponent(key)let child = keykey = graph.files[key].parent// A PARENT IS A COMPONENT (COMPONENTS §5): one without a View wraps nothing, and// every page under it answered a 500 while the boot said nothing (322 row 13)if (key != null && viewHandle(key) == 0) {hlError("hl:web: the `parent` clause of " + child + " names " + key + ", which declares no View: a parent is a component whose View places `slot` where the page renders")}}}// EVERY PACKAGE'S BROWSER HALF IS SERVED, BY ONE RULE. A package whose browser// side is written in this language ships it as `client.hl` beside its// plugin.json; the compiler compiles it like any other file and writes the// importing module `import { … } from "<halfDir>/<pkg>/client.js"`// (js_module.zig browserHalf / browserHalfUrl). Something has to answer that url,// and this is it — for EVERY such package the graph holds, because the rule// belongs to the language and not to one package. hl:web's own half is the first// case of it and no longer a hard-coded route of its own; hl:dnd is the second.//// The graph is the whole guard: a key is here only because the entry's closure// REACHED that package's half through an import, which is the same reason a// component has a module url. A package nothing imports is served nothing.for (gk of graph.files.keys()) {if (gk.startsWith('hl:') && gk.endsWith('/client.hl')) { serveHalf(gk) }}// ---- INSTALLABLE, AND OFFLINE (COMPONENTS §10, tickets #95–#97) ------------------// Two declarations in the app's manifest, and nothing written by hand in the app:// · `appIcons` (with `appTitle`, and optionally `appShortName`, `appThemeColor`,// `appBackgroundColor`, `appManifest`) makes the app INSTALLABLE — the web app// manifest below, linked from every page's head with its apple-touch-icon;// · `offline = [ … ]` makes the pages it names work OFFLINE — the service worker// (worker.hl) that keeps the shell and those pages, and in the client half the// IndexedDB copy of their state and the queue of the emits they make.// An app that declares neither is served nothing of either.offlineKeys = offlinePages()queueEvents = offlineKeys != null ? queueable() : []if (installable()) {routes[] = { pattern = manifestUrl framework = true function = (route, req) => {return new Response(JSON.stringify(webManifest()), { headers = { 'Content-Type' = 'application/manifest+json; charset=utf-8' 'Cache-Control' = 'no-cache' } })} }}if (offlineKeys != null) {// the ENTRY SCRIPT is served at the framework's directory, and its scope is the// whole app: `Service-Worker-Allowed` is what lets a script under /__hl/ say soroutes[] = { pattern = workerUrl framework = true function = (route, req) => {return new Response(workerEntry(), { headers = { 'Content-Type' = 'text/javascript; charset=utf-8' 'Cache-Control' = 'no-cache' 'Service-Worker-Allowed' = '/' } })} }routes[] = { pattern = halfDir + '/web/worker.js' framework = true function = (route, req) => {return new Response(workerModule(), { headers = cached(req, 'text/javascript; charset=utf-8') })} }}// ---- WHAT THIS APP SERVES, OFF THE ANALYSIS ALONE (mission 315) ----------------// These four are STATIC, so they answer with no instance and without constructing// anything: the compiler evaluates `servedModulesOf(graph)` at BUILD time to bake// each module's text, and this file's own root uses the very same functions at// boot. One definition, two callers — a second derivation would drift, and a// compiled binary would then serve text generated against different urls.//// The seed is the entry's route table AS WRITTEN, read through `hlSyntax`, not// the evaluated `routes` member: evaluating it means constructing the app, and// constructing this class binds a socket. The graph carries the entry's record// since 2026-09-16, which is what makes that readable at all.// A ROUTE'S COMPONENT IS A PAGE: a file without a View answered every request with a 500// ("declares no View", raised without a span) while the boot said nothing (mission 322// row 13). Refused at the row instead.static routePage = (gen, entryKey, valueNode, key) => {if (!hasViewMember(gen, key)) { hlSourceError(gen, entryKey, valueNode, key + " declares no View, so it is no page: a route's component is a file whose View renders") }return key}// The key a route row's `component` names: an imported class, or a path string.// Anything else is a located refusal AT THE ROW — never silently unserved.static routeComponentKey = (graph, gen, entryKey, imports, valueNode) => {let v = hlSyntax(gen, entryKey, valueNode)if (v == null) { return null }if (v.tag == 'literal' && v.kind == 'string') {let t = v.textlet k = t.startsWith('./') ? t.slice(2) : t// A PATH NO FILE OF THE APP IMPORTS IS NOT IN THE GRAPH: the analysis never read// it, so nothing can be served for it — refused here, at the row, instead of a// null deep in the framework once the server is already up (STATUS §2 item 0)if (graph.files[k] == null) { hlSourceError(gen, entryKey, valueNode, "the app never imports " + t + ", so the analysis never read it and the app would serve no module for it: import it in this file (`import Page from '" + t + "'`) and write `component = Page`") }return routePage(gen, entryKey, valueNode, k)}if (v.tag == 'identifier') {for (imp of imports) {if (imp.default == v.name && imp.key != null) { return routePage(gen, entryKey, valueNode, imp.key) }}}hlSourceError(gen, entryKey, valueNode, "a route's component must be an imported component class or a path string — this one the analysis cannot resolve to a file, so the app would serve no module for it")return null}// does the file at `key` declare a View? (a component, as opposed to a plain class)static hasViewMember = (gen, key) => {for (m of hlMembers(gen, key)) {if (m.name == 'View') { return true }}return false}// `key` and every component it composes, once each — the same walk// `serveComponent` performs, as a function of the graph.static collectServed = (graph, gen, out, key) => {if (key == null || graph.files[key] == null) { return null }for (k of out) { if (k == key) { return null } }out[] = keyfor (imp of graph.files[key].imports) {if (imp.default != null && imp.key != null && hasViewMember(gen, imp.key)) {collectServed(graph, gen, out, imp.key)}}return null}// Every key this app answers a module for: each route's component and the wrapper// chain above it, the components those compose, and every package's browser half.static servedKeys = (graph) => {let gen = graph.analysisGenlet entryKey = graph.manifest.filelet out = []if (entryKey != null && graph.files[entryKey] != null) {let imports = graph.files[entryKey].importslet routesNode = 0for (m of hlMembers(gen, entryKey)) {if (m.name == 'routes') { routesNode = m.node }}if (routesNode != 0) {let arr = hlSyntax(gen, entryKey, routesNode)if (arr != null && arr.tag == 'array_expression') {for (e of arr.elements) {let row = hlSyntax(gen, entryKey, e)if (row != null && row.tag == 'object_expression') {for (pe of row.entries) {let prop = hlSyntax(gen, entryKey, pe)if (prop != null && prop.tag == 'object_property' && prop.key == 'component') {let key = routeComponentKey(graph, gen, entryKey, imports, prop.value)// the wrapper chain above it, deepest last — a page is// served inside whatever wraps itlet k = keywhile (k != null) {collectServed(graph, gen, out, k)k = graph.files[k] != null ? graph.files[k].parent : null}}}}}}}}return out}// EVERY PACKAGE'S BROWSER HALF, BY ONE RULE — a package whose browser side is// written in this language ships it as `client.hl`, the compiler writes// `<halfDir>/<pkg>/client.js` into every importing module, and something has to// answer that url. The graph is the whole guard: a key is here only because the// entry's closure reached that package.//// SEPARATE FROM `servedKeys` ON PURPOSE: a half gets a ROUTE but is not a module// url of this app, so it is not in the map `hlJs` is handed. That asymmetry is// the existing `serveHalf`/`serveComponent` split, stated rather than inherited// by accident — measured 2026-09-16, when folding the two together made the// analysis set one key wider than the route walk's.static packageHalfKeys = (graph) => {let out = []for (gk of graph.files.keys()) {if (gk.startsWith('hl:') && gk.endsWith('/client.hl')) { out[] = gk }}return out}// The browser realm's NAME, off the graph — the declared realm whose entrypoint// is this framework's client half. Static, so the compiler can ask it too.static clientRealmOf = (graph) => {for (name of graph.realms.keys()) {if (graph.realms[name].entrypoint == 'hl:web/client.hl') { return name }}return null}// EVERY MODULE THIS APP SERVES, COMPILED — what a native binary answers `hlJs`// with (mission 315). The emitter needs the runtime specifier, the browser// realm and the served-url map; all three are functions of the analysis, so the// whole map is, and the COMPILER can build it without an instance.//// `minify` is NOT: it is an app setting bound at construction, and the analysis// cannot evaluate a construction. The value used is recorded beside each// module so a binary asked for a different one refuses at the call instead of// answering text generated the other way.// HOW THE APP ASKED FOR ITS MODULES. A build bakes ONE form of every module and// a binary answering the other form would be different bytes, so the build must// read the app's own answer — and the app writes it where it configures the// framework: `new WebFramework(… minify = true …)` in the manifest. The analysis// carries a construction's named arguments, so this is a READ of the same syntax// `servedKeys` reads for the routes, not a second declaration.//// A COMPUTED `minify` IS NOT READ HERE, and nothing is guessed from it: a build// cannot evaluate an expression the running app has not reached yet. Such an app// bakes the unminified form, exactly as every app did before this, and if it// then asks for the other form at run time the binary says so at the request —// "this binary carries the module compiled the other way", located, which is// the refusal that already exists and the one measured on the creator's social// app (2026-09-17). Writing the literal in the manifest is what removes it.static manifestMinify = (graph) => {let gen = graph.analysisGenlet key = graph.manifest.fileif (key == null || graph.files[key] == null) { return false }for (m of hlMembers(gen, key)) {let s = hlSyntax(gen, key, m.node)if (s != null && s.tag == 'new_expression' && s.named != null) {for (h of s.named) {let p = hlSyntax(gen, key, h)if (p != null && p.tag == 'object_property' && p.key == 'minify') {let v = hlSyntax(gen, key, p.value)if (v != null && v.tag == 'literal' && v.kind == 'boolean') { return v.text == 'true' }}}}}return false}static bakeModules = (graph) => {let realm = clientRealmOf(graph)let served = servedModulesOf(graph)let min = manifestMinify(graph)let out = {}for (k of servedKeys(graph)) {if (graph.files[k] != null) {let p = graph.files[k].pathout[p] = {text = hlJs(p, min, runtimeSpec, realm, served)minify = minruntime = runtimeSpecrealm = realm}}}// EVERY PACKAGE HALF TOO. The root walk above serves `<halfDir>/<pkg>/client.js`// for every `hl:<pkg>/client.hl` the graph holds, and a compiled binary answers// from BAKED text alone — so a bake that skipped them served the page and then// 404'd on the module the page imports, leaving the client half of the app// dead with nothing reported (measured on projects/framework, 2026-09-16).// Same rule, same list, one loop later: what the server routes, the build bakes.// AN OFFLINE APP'S SERVICE WORKER MODULE (COMPONENTS §10), under its own pathif (manifestOffline(graph)) {let wp = workerPathOf(graph)out[wp] = {text = hlJs(wp, min, runtimeSpec, realm, served)minify = minruntime = runtimeSpecrealm = realm}}for (gk of graph.files.keys()) {if (gk.startsWith('hl:') && gk.endsWith('/client.hl') && graph.files[gk] != null) {let hp = graph.files[gk].pathif (out[hp] == null) {out[hp] = {text = hlJs(hp, min, runtimeSpec, realm, served)minify = minruntime = runtimeSpecrealm = realm}}}}return out}// file path → the url this server answers its module at. What `hlJs` is handed,// and what the compiler bakes against.static servedModulesOf = (graph) => {let out = {}for (k of servedKeys(graph)) {if (graph.files[k] != null) { out[graph.files[k].path] = '/' + k + buildMark }}return out}// ONE PACKAGE HALF'S ROUTE. It is a FUNCTION and not the body of the loop above// because the url's route closure must hold THIS key: a `let` in a loop body is// one binding the closures share, so every route built that way would serve the// last package's half (measured 2026-09-14 — both /__hl/web/client.js and// /__hl/dnd/client.js answered with hl:web's module). A parameter is a fresh// binding per call, which is the same reason serveComponent below is a function.serveHalf(key) {routes[] = { pattern = halfUrl(key.slice(3, key.length - 10)) framework = true function = (route, req) => { return serveHl('/' + key, req) } }return null}// a mounted file's module route, and the routes of the components it composes// (an import with a View), recursively — off the graph, once eachserveComponent(key) {if (served[key] != null) { return null }served[key] = trueroutes[] = { pattern = modulePath(key) framework = true function = (route, req) => { return serveHl(req.path, req) } }for (imp of graph.files[key].imports) {if (imp.default != null && imp.key != null && viewHandle(imp.key) != 0) { serveComponent(imp.key) }}return null}// THE PROJECT DIRECTORY: this file lives in a package, so the app's directory is read// off a file that is the app's — the first route component's key against its pathroot = projectRoot()view = new HlwViewcss = new HlwCss(minify = minify, view = view)compiler = new HlwCompilesessions = new HlwSessions(dir = sessionDirPath(), maxAge = sessionMaxAge != null ? sessionMaxAge : 1209600, idle = sessionIdle != null ? sessionIdle : 900, sweepEvery = sweepInterval != null ? sweepInterval : 300, cookie = cookieName(), secure = sessionSecure, domain = sessionDomain != null ? sessionDomain : '')sessions.open()// THE STYLES FILE IS NAMED AT BOOT, not at the first request for the stylesheet: a value// that names no file, or a file the graph does not hold, is the app's mistake and the// app does not start with it (ticket #12)if (styles != null) {if (hlTypeName(styles) != 'String' && hlTypeName(styles) != 'Class') { refuseAtSetting('styles', "hl:web: styles is the styles file — the class the app imported (`styles = Styles`) or a path string ('./styles.hl') — and this one is a " + hlTypeName(styles)) }if (graph.files[keyOf(styles)] == null) { refuseAtSetting('styles', "hl:web: styles names " + keyOf(styles) + ", which is not in the project graph — import it from the app's entry (`import Styles from './styles.hl'`) and write `styles = Styles`") }}// the whole tree, nothing excluded: dot directories (the session store) are skipped by// the walker's own rule, and a custom store inside the tree is the app's choice to pay forif (watchMode) { __native("eventloop.register", watch(root), "hlFileChanged") }sheetText = null // the stylesheet, walked once at the first request for itrouter = new HlwRouter(routes = routes)trees = {} // key → the View tree, built onceslotAt = {} // key → where its View places its one `slot`styleNames = {} // key → the rule names of the file's Style, off the syntaxstyleRefs = {} // key → [{ tag rule }] the View wrotestyleTags = {} // key → the element names this file's View rendersstyleIds = {} // key → the literal `id` values this file's View writes (R3)staticHolders = {} // key → an instance of a file whose statics another file importsmodules = {} // key → the browser module (JavaScript text), compiled once at bootbuildHash = null // the hash of everything this build serves, in every cached url (build())workerCompiled = null // the service worker's module as compiled (offline apps only, workerText())shipped = {} // key → the module as served: its build marks replaced with the hashservedUrls = null // file path → the url this server answers its module at, for hlJsruntime = hlJsRuntime() // the runtime library every module imports as "./hl-runtime.js"peers = {} // connection key → the WsClient, while it is openpeerSession = {} // connection key → its session, from the tab's `hello` (null = anonymous)peerHost = {} // connection key → the host its upgrade request named (null = none sent)peerHeaders = {} // connection key → its upgrade request's headers (requestHeaders)peerMounts = {} // connection key → the component keys the tab has mounted// SESSIONS (web/sessions.hl): named by the cookie, at the page request and at the// socket's handshake alike. Nothing about a session ever reaches page script.sending = null // the WsClient whose inbound emit is being handled — the peer// an outward emit reaches, and null under the POST fallback// THE POST FALLBACK'S PEER IS ITS OWN ANSWER. While a face dispatched over// `POST /__hl/emit` runs, `posting` is true and every outward emit meant for the// CALLER is appended here instead of pushed; `inbound` ships the list in the ack.Boolean posting = falsepostEmits = []postSession = null // the cookie's session of the POST being dispatchedpostHost = null // the host of the POST being dispatchedfaceHost = null // the host of the carrier whose inbound emit is being dispatchedpostHeaders = null // the headers of the POST being dispatched (requestHeaders)faceHeaders = null // the headers of the carrier whose inbound emit is being dispatched// EXACTLY ONCE (COMPONENTS §10): a queued emit carries a stable id `q`, and the ids handled// are kept per session, the last `handledKept` of them (Session.handled). A connection// with no session keeps its ids here, in one list under the same bound.Number handledKept = 200anonHandled = []// WHAT A KEPT ID WAS ANSWERED (ticket #107). An emit in flight when its socket closed// is sent again with its id, and its caller is still waiting — for a page, say. The// first delivery's ack went out on the dead socket, so the repeat is answered from// here: `answers[session id + ' ' + q]` is that ack as sent (its `i` replaced), or// `running` while the first delivery has not finished, which answers the repeat// `later` and the client asks again. In memory, the last `handledKept` of them: a// repeat comes seconds after the first try, and a restart loses nothing a live peer// still waits for but the ack, which is then plain `ok`, as before.answers = {}answerKeys = []uploads = {} // 'u' + upload id → { sid, name, type, size, have, parts, at, key }: a file on its way in (ticket #94)uploadKeys = {} // 'k' + session id + ' ' + q + ' ' + n → the upload id the n-th file of emit q went up underuploadParts = {} // 'u' + id + ':' + n → the n-th part as it arrived, a Bytes// ONE PORT, TWO CARRIERS. hl:http1's WebSocket engine answers the Upgrade// inline on the port this server already bound, so `websocket` and `rest` are// the same listener — which is why the manifest can declare both for the same// direction and this file needs no second server.// EVERY VIEW IS BUILT AT BOOT, not at the first request for its route: a View that// reads a name its file declares nowhere (checkRead) is the app's mistake, and the// app does not start with it — the port is bound on the line below this pass.// THE TWO DERIVATIONS MUST AGREE (mission 315). `served` is built above from the// EVALUATED route table; `servedKeys(graph)` derives the same set from the// analysis alone, and the compiler bakes each module's text against the second.// If they ever disagree the binary would serve modules generated against urls// this server does not answer, so the disagreement is a refusal, here, at boot.for (k of servedKeys(graph)) {if (served[k] == null) {hlError('hl:web: the analysis says this app serves ' + k + ', the route walk did not — the two derivations of the served set disagree')}}for (k of served.keys()) {let seen = falsefor (a of servedKeys(graph)) { if (a == k) { seen = true } }if (seen == false) {let why = ''for (imp of graph.files[k] != null ? [] : []) { why = why }hlError('hl:web: the route walk serves ' + k + ', the analysis did not — the two derivations of the served set disagree')}}for (k of served.keys()) { if (viewHandle(k) != 0) { tree(k) } }// A COMPONENT-REFERENCE CYCLE IS REFUSED HERE (COMPONENTS §12): a View that renders// itself, directly or through another, is a mount that never ends — it overflowed the// stack at the first request and the server died of a segfault with nothing said// (mission 322 row 1).acyclic = {}for (k of served.keys()) { if (viewHandle(k) != 0) { referenceCycle(k, [], []) } }// …and a page a shell wraps is placed at the shell's slot, inside its body: a page whose// root is `body` there emitted a second, nested <body> (mission 322 row 5)for (k of mounted) {if (graph.files[k] != null && graph.files[k].parent != null && viewHandle(k) != 0 && bodyRooted(k)) {hlError("hl:web: " + k + " is wrapped by " + graph.files[k].parent + " (its `parent` clause), but its View's root is `body { … }`, the page's body: the shell owns the body, so write the page's root as an element (`main { … }`, `section { … }`)")}}// THE SHEET IS WALKED AT BOOT, so the rules it drops are listed at boot (COMPONENTS §4,// css.hl's own header): walked at the first request for it, a dead rule was listed only// once some browser had asked, and a boot log said nothing (mission 322 row 11)stylesheet()// EVERY SERVED MODULE IS COMPILED AT BOOT: a free name in a View handler is a located// compile error (COMPONENTS §6b), and compiled at the first request for the module it// was a 500 behind a page that had already answered 200 (mission 322 row 14)for (k of served.keys()) { module(k) }// THE INTERFACE IS THE OPERATOR'S (ticket #24), on the ladder graph.zig's OperatorBind// states: the constructor's `host` > HL_HOST (the binary fills it from HOST) > the// manifest's > 0.0.0.0. Binding the default regardless put an app meant to sit behind// a proxy on every interface of the machine.if (host == null) { host = env('HL_HOST') }if (host == null && cfg != null && cfg.host != null) { host = cfg.host }if (host == null) { host = '0.0.0.0' }// A --PORT AFTER THE ENTRY OUTRANKS THE APP'S OWN PORT (creator ruling// 2026-09-27), but `port` ITSELF CANNOT SAY SO: the reference app passes an// explicit `port = …` to `new WebFramework(…)` (server.hl, computed from// HL_FW_PORT so a test run can sit beside a developer's own instance), which// PINS the member for the whole construction (SPEC.md "The file root is the// constructor" — "a root assignment to [a pinned] property… is a no-op").// `port = port_arg` below was exactly that no-op: `hybriel server.hl// --port=11499` still bound 44333, server.hl's own default, because this// line's write never landed. `boundPort`, a plain local nothing ever pins,// carries the actual ladder instead — CLI argument, else the (possibly// pinned) member, else HL_PORT, else 8080 — and IS what binds and what the// boot line names.let port_arg = portArgument()let boundPort = port_argif (boundPort == null) { boundPort = port }if (boundPort == null && env('HL_PORT') != null) { boundPort = toNumber(env('HL_PORT')) }if (boundPort == null) { boundPort = 8080 }http = new NativeWebSocketServer(port = boundPort, host = host)echo 'framework listening on ' + boundPortmodule('web/client.hl')// ---- the browser's modules: compiled IN PROCESS, served from memory --------------// The JavaScript target is this binary's own function (hlJs), the same one `--js`// runs from the command line. No child process, no build directory.//// THE CLIENT is compiled at boot — every page needs it, and it carries the View// and the Router with it (one module, three classes). A COMPONENT is compiled ON// DEMAND, at the request for its own `.hl` url, and kept: a route nobody visits// costs nothing, and the browser asks for a component's module the first time it// mounts one.//// THE TABLE THE EMITTER WRITES IS DROPPED. `hlJs` puts each file's members,// handlers and methods — every node of every initializer, with the names it reads —// into the module, because the old hl:web's browser half reduces that table on every boot.// This framework asked the same questions at COMPILE time and wrote the answers into// the component's own statements, so the table is dead weight in the page: 58 KB of// syntax on the reference app's home page alone. What the framework SERVES is its own// text, so it leaves that statement out; the language is not asked to change, and// The old hl:web kept its table untouched.// THE TABLE STATEMENT'S NAME in this module: `hlTablesDef`, or in a compact module the// alias its import clause gives it (`hlTablesDef as _51`). Each statement stands at the// start of its own line, which is how it is found — an alias alone is a suffix of other// names (`$_51(` is a compiler temporary).tablesCall(text) {let alias = text.indexOf('hlTablesDef as ')if (alias < 0) { return newline + 'hlTablesDef(' }let first = alias + 15let stop = firstwhile (stop < text.length && text[stop] != ',' && text[stop] != '}') { stop = stop + 1 }return newline + text.slice(first, stop) + '('}// the `fns` of the file's own table — the first table statement the module carries is// the file's; the ones after it are the classes it bundlestableFns(text) {let at = text.indexOf(tablesCall(text))if (at < 0) { return [] }let open = text.indexOf('{', at)let end = text.indexOf(');', at)if (open < 0 || end < 0) { return [] }let t = JSON.parse(text.slice(open, end))return t.fns == null ? [] : t.fns}withoutTables(text) {// one per FILE the module carries — the client half bundles three classes. A compact// module calls it by its alias, and was served with every table until ticket #98's// follow-up: 50 KB and more of syntax on a minified app's pagelet call = tablesCall(text)let out = textlet at = out.indexOf(call)while (at >= 0) {let end = out.indexOf(');', at)if (end < 0) { return out }// the newline before it stays: it ends the statement aboveout = out.slice(0, at + 1) + out.slice(end + 2)at = out.indexOf(call)}return out}// THE APP RUNS ON ONE WEB FRAMEWORK. Which packages are frameworks is READ OFF THE// GRAPH and not off a list this file keeps: a package the app's entry reached that// holds a `WebFramework.hl` is one. hl:dnd, hl:http1 and every other package an app// imports hold none and are untouched by this.//// The refusal names the ROW of each import, because the mistake IS an import line —// a sub-file import (`'hl:web/css'`) counts as much as the package itself, which is// exactly the case that made it: a lone braced import of a stylesheet helper puts the// whole package into the graph, and with it a second navigation face.oneFrameworkOnly() {let frameworks = {}for (gk of graph.files.keys()) {if (gk.startsWith('hl:') && gk.endsWith('/WebFramework.hl')) { frameworks[gk.slice(0, gk.length - 16)] = true }}if (frameworks.keys().length < 2) { return null }// the first row of the app's OWN files that reached each packagelet sites = {}for (k of graph.files.keys()) {if (!k.startsWith('hl:')) {for (imp of graph.files[k].imports) {for (pkg of frameworks.keys()) {if (sites[pkg] == null && (imp.source == pkg || imp.source.startsWith(pkg + '/'))) {sites[pkg] = { key = k; node = imp.node; where = k + ':' + imp.line + ':' + imp.col + " imports '" + imp.source + "'" }}}}}}let said = ''for (pkg of frameworks.keys()) {if (said != '') { said = said + ', and ' }if (sites[pkg] == null) { said = said + "'" + pkg + "' (reached through a package, not from a file of the app)" }else { said = said + sites[pkg].where }}let msg = 'this app imports TWO web frameworks: ' + said + '. Both packages load, both answer a page navigation, and the second one answers on a blank instance. An app runs on ONE of them — switch EVERY hl: import in EVERY file of the app to the same package, a sub-file import like \'/css\' included. Switching one line is not switching the app.'// THE CARET GOES ON ONE OF THE ROWS, and the sentence names them all: two// import lines are equally the mistake, and a diagnostic can only underline one.for (pkg of frameworks.keys()) {if (sites[pkg] != null) { hlSourceError(gen, sites[pkg].key, sites[pkg].node, msg) }}hlError(msg)return null}// ONE ANSWERER PER VALUE-FORM EMIT. SPEC: "if more than one class in the destination// realm declares a non-tap handler for the event, the VALUE form is refused at the emit// site naming both. As a statement it stays a broadcast and every handler still fires."// The statement form is what `inbound` does — every face runs. The value form cannot be// told apart on the wire, so every `x = emit server ev()` the app's files hold is read// off the analysis here, and one whose event two files answer is refused at its site,// naming both, before anything is served. Until ticket #20 both faces ran and the first// answer was taken in silence.oneAnswererEach() {let realm = serverRealm()for (k of graph.files.keys()) {if (!k.startsWith('hl:')) {for (e of hlEvents(gen, k).emits) {if (e.value == true && e.realm == realm) {let owners = facesOf(e.event)if (owners.length > 1) {let named = ''for (o of owners) { named = named == '' ? o : named + ' and ' + o }let msg = "hl:web: " + k + ':' + e.line + ':' + e.col + " — this emit waits for the answer of '" + e.event + "', and " + owners.length + " files answer it in the '" + realm + "' realm: " + named + ". A value-form emit has ONE answerer — rename the event in one of them, or emit it as a statement (a broadcast every handler receives)"hlSourceError(gen, k, e.node, msg)hlError(msg)}}}}}return null}// WHAT THIS SERVER SERVES AS A MODULE OF ITS OWN, by the file's path (mission 314// rule 0). Handed to `hlJs`, it is what turns `import PostCard from './post_card.hl'`// into an ES import of `/components/post_card.hl` instead of PostCard's whole class// text copied into the importing module — EditHistory's class shipped three times on// the social demo's home page before this. The urls are THIS file's (`moduleUrl`);// the compiler invents none, and a file this server does not answer for is inlined// exactly as it always was.servedModules() {if (servedUrls == null) {let out = {}for (k of served.keys()) { out[pathOf(k)] = modulePath(k) + buildMark }servedUrls = out}return servedUrls}// A key the graph holds no file for is answered `null`, and the caller makes// that a 404: a file the entry's closure never referenced is not part of this// app, which is what keeps this from being a read of any path a url asks for.module(key) {if (modules[key] == null) {let f = graph.files[key]if (f == null) { return null }// THE BROWSER GETS THE BROWSER'S PROJECTION, and nothing else. hlJs's// fourth argument is the realm the module is produced for: a handler// written `on server …` is not emitted at all, and a member whose// initializer reaches a name the browser does not have is declared// without one, for this server to lay over the instance with the state// it already ships. The realm NAME comes off the manifest (clientRealm()// above), never a literal — a project names its own realms.let text = hlJs(f.path, minify, runtimeSpec, clientRealm(), servedModules())// AND THE COMPILE STEP'S OWN OUTPUT BESIDE IT (mission 313): a component with a// View carries, after its class, the paint statements this framework compiled// from that View — one per bound spot, with its read written in (compile.hl).// The browser calls them instead of walking the tree at paint time. Text// produced here and served from memory like the module itself; nothing is// written to disk and no JavaScript file lives under plugins/.// THE FUNCTION VALUES' SITES are the one part of that table the browser still// needs (tickets #98, #99): the compile step turns them into write setslet fns = tableFns(text)text = withoutTables(text)if (viewHandle(key) != 0) { text = text + newline + compiler.module(gen, key, tree(key), fns) }modules[key] = text}return modules[key]}// the file the graph knows under this key — the graph carries every file's own pathpathOf(key) {let f = graph.files[key]if (f == null) { hlError('the graph holds no file ' + key + ' — nothing referenced it from the entry') }return f.path}// THE BROWSER'S REALM, off the manifest: the realm whose declared entrypoint is// this framework's client half. The framework's own two files ARE the two// realms' entrypoints, so this is a lookup and never a guess.clientRealm() {for (name of graph.realms.keys()) {if (graph.realms[name].entrypoint == 'hl:web/client.hl') { return name }}hlError('the manifest declares no realm whose entrypoint is ./client.hl — the browser half of this framework IS a realm, and a page cannot say which realm it is running in until the manifest names it')}// THIS realm, off the graph: the declared realm whose entrypoint REACHES this// file (the app's entry imports it). A face names it, and so does the refusal// when nothing answers.serverRealm() {let mine = graph.files['hl:web/WebFramework.hl'].realmsif (mine.length == 1) { return mine[0] }hlError('the manifest declares no realm whose entrypoint reaches hl:web/WebFramework.hl — this file cannot dispatch a face without knowing which realm it is')}// the app's directory: a route component is the app's file, its key project-relativeprojectRoot() {for (r of routes) {if (r.component != null) { return rootOf(keyOf(r.component)) }}// AN APP OF FUNCTION ROUTES ONLY (ticket #19) has no component to read it off, and// needs no other: the manifest is the app's file too, and the graph knows its keyif (graph.manifest.file != null && graph.files[graph.manifest.file] != null) { return rootOf(graph.manifest.file) }hlError('hl:web: the route table names no component and the graph names no manifest file, so the app\'s directory cannot be read off either')}// a project-relative key against its absolute path: what is left is the project's directoryrootOf(key) {let path = pathOf(key)// A COMPILED BINARY'S PATH IS ITS KEY (mission 318): it carries no// build directory, so the file's path and its key are the same// string and the root is the process's own directory. The// interpreter's path is the script's absolute one, and the key is// its tail — the root is what stands before it.if (path == key) { return '.' }return path.slice(0, path.length - key.length - 1)}// THE KEY OF A FILE THE APP NAMED — as the class it imported (`component = SortList`,// `styles = Styles`: the import is the guarantee the graph reached it) or as a path// ('./components/home.hl'). A class knows its file (`file(cls)`), and the graph knows// the file's key.keyOf(named) {if (hlTypeName(named) == 'Class') {let path = file(named)for (k of graph.files.keys()) {if (graph.files[k].path == path) { return k }}hlError('hl:web: the class at ' + path + ' is not in the project graph — import it from the app\'s entry')}// ANYTHING ELSE NAMES NO FILE (ticket #12): `styles = {}` booted and every request// for the stylesheet was then a NotCallable on this line, with no line of the app'sif (hlTypeName(named) != 'String') { hlError("hl:web: a file the app names — a route's component, or `styles` — is the class it imported or a path string ('./styles.hl'), and this one is a " + hlTypeName(named)) }if (named.startsWith('./')) { return named.slice(2) }return named}// ---- the View tree: the member's syntax, reflected into plain hybrids ----------viewHandle(key) {for (m of hlMembers(gen, key)) {if (m.name == 'View') { return m.node }}return 0}tree(key) {if (trees[key] != null) { return trees[key] }let handle = viewHandle(key)if (handle == 0) { hlError('component ' + key + ' declares no View') }let v = hlSyntax(gen, key, handle)let nodes = []for (h of v.entries) {let n = node(key, h, [], false, [])if (n != null) { nodes.push(n) }}trees[key] = nodesreturn nodes}// one entry of a hybrid literal → a tree node (null for an attribute: the// element that owns it reads it)node(key, handle, path, inFor, rows) {let n = hlSyntax(gen, key, handle)if (n == null) { return null }if (n.tag == 'object_property') {let v = hlSyntax(gen, key, n.value)if (v.tag == 'object_expression') {let childKey = componentKeyOf(key, n.key)if (childKey != null) { return component(key, n.key, childKey, v, v, path, inFor, rows) }return element(key, n.key, v, path, inFor, rows)}return null}if (n.tag == 'literal') { return { k = 'text'; text = n.text; kind = n.kind; } }if (n.tag == 'identifier') {// A BARE CAPITALISED REFERENCE IS A COMPOSITION WITH AN EMPTY FILL (§12:// "`Name { }` is the same reference as bare `Name`"). It is not a read, so it// is answered before the read rules below refuse the name.let bareKey = componentKeyOf(key, n.name)if (bareKey != null) { return component(key, n.name, bareKey, null, n, path, inFor, rows) }if (n.name == 'slot') { oneSlot(key, n) }// AN IMPORTED STATIC IN A VIEW IS FOLDED (the archive did the same at build):// `h1 { siteTitle }` with `import { siteTitle } from '../store.hl'` is not a// member of this class, it is a constant of another file — read off that// file's class once, here, and written into the tree as textreturn nameRead(key, n.name, rows, n)}if (n.tag == 'view_for') {// `for (row of list) { … }` — the list is a member (or a row field), the// body's entries are rendered once per entry with `row` bound to itlet body = hlSyntax(gen, key, n.body)let children = []// THE ROW VARIABLE IS IN SCOPE INSIDE THE BODY, and nowhere else: the list is// read in the scope that stands around the `for`let list = ref(key, n.list, rows)let inner = rows.slice(0)inner.push(n.varName)for (h of body.entries) {let c = node(key, h, path, true, inner)if (c != null) { children.push(c) }}// THE SITE, as every element node carries one: the id the module's tables// file this `for`'s row under, so the browser reads what the list depends// on from the compiler instead of guessing it off `list.name`.return { k = 'for'; row = n.varName; list = list; body = children; site = baseName(pathOf(key)) + ':' + n.line + ':' + n.col; }}if (n.tag == 'view_if') {let then = []let cons = hlSyntax(gen, key, n.consequent)for (h of cons.entries) {let c = node(key, h, path, inFor, rows)if (c != null) { then.push(c) }}let other = []let alt = hlSyntax(gen, key, n.alternate) // null when there is no elseif (alt != null) {if (alt.tag == 'view_if') {other.push(node(key, n.alternate, path, inFor, rows))} else {for (h of alt.entries) {let c = node(key, h, path, inFor, rows)if (c != null) { other.push(c) }}}}// A BRANCH CONDITION IS A READ (COMPONENTS: a member or a loop path).// Anything else came back null here and the branch rendered nothing, on the// server and after hydration, with no word said (ticket #17: `if (!flag)`).let cond = ref(key, n.condition, rows)if (cond == null) {let c = hlSyntax(gen, key, n.condition)hlError("hl:web: " + baseName(pathOf(key)) + ':' + c.line + ':' + c.col + " — a View `if` takes a member, a field path or a `for` row variable as its condition, and this one is an expression: declare it at the root of " + baseName(pathOf(key)) + " (`showIt = !flag`) and write `if (showIt)`")}return { k = 'if'; cond = cond; then = then; other = other; site = baseName(pathOf(key)) + ':' + n.line + ':' + n.col; }}if (n.tag == 'member_expression') {// `p.title` — a field path off a member or a row variablelet r = ref(key, handle, rows)if (r != null) { return r }}if (n.tag == 'on_statement') {// a DOM event handled by the literal that carries it: the browser finds the// literal in the instance's View by the path of keys and fires the event at it// THE SITE IS THE HANDLER'S ID: the tables file this handler's write set// under it, and the browser repaints exactly those members when it runs.return { k = 'on'; event = n.event; site = baseName(pathOf(key)) + ':' + n.line + ':' + n.col; }}return { k = 'other'; tag = n.tag; }}// A COMPONENT HAS EXACTLY ONE SLOT (COMPONENTS §2 table, §12): a second one rendered the// child a second time, with no word said (mission 322 row 4)oneSlot(key, n) {let here = baseName(pathOf(key)) + ':' + n.line + ':' + n.colif (slotAt[key] != null) {hlError("hl:web: " + here + " — a second `slot` in this View (the first is at " + slotAt[key] + "): a component has exactly one slot, where the page it wraps or the fill it is given renders")}slotAt[key] = herereturn null}isMember(key, name) {for (m of hlMembers(gen, key)) { if (m.name == name) { return true } }return false}// THE READ OF A BARE NAME IN A VIEW — wherever the name stands. A text child, an// attribute's value and a reference-site binding are ONE lookup with ONE refusal://// a `for` row variable standing around the read → read at render, from the row// a member of this file → read at render, from the instance// a name this file imports by BRACE → a STATIC of another file:// constant-folded here, at build// anything else → the located refusal (checkRead)//// An ATTRIBUTE used to skip the middle two and record every name as a member read,// so `a { href = brandHref }` on a braced import asked the instance for a member it// does not have and the renderer raised error.UndefinedProperty with view.hl's line// and nothing of the app's — while the same name as a TEXT CHILD rendered fine// (creator, routger migration W8). A read is a read; where it stands decides// nothing about what it names.nameRead(key, name, rows, at) {if (rows != null && rows.includes(name)) { return { k = 'member'; name = name; } }if (isMember(key, name)) { return { k = 'member'; name = name; } }// A BRACED IMPORT FOLDS EVEN WHEN ITS VALUE IS NULL: the name resolved, so the// read is answered here and never reaches the instance (a null static used to// fall through to a member read and raise the same UndefinedProperty).if (importsName(key, name)) {let folded = importedStatic(key, name)// THE VALUE AS WELL AS THE TEXT: an attribute and a text child want the text,// and a reference binding wants the value the author's file declared (§12)return { k = 'text'; text = folded == null ? '' : '' + folded; kind = 'folded'; value = folded; }}checkRead(key, name, rows, at)return { k = 'member'; name = name; }}// A VIEW READS ONLY WHAT ITS FILE DECLARES. A name that is neither a member of// this class, nor a static it imports by brace, nor a `for` row variable standing// around this read, is a typo or a missing declaration — refused HERE, at the// build of the tree (boot), naming the file and the line, instead of a 500 out of// the renderer with the language's UndefinedProperty and no line of the app's.checkRead(key, name, rows, n) {if (rows != null && rows.includes(name)) { return null }if (isMember(key, name)) { return null }if (importsName(key, name)) { return null }let hint = name == 'session' ? "`session = null` to receive the connection's session" : name == 'host' ? "`host = null` to receive the request's host" : name == 'headers' ? "`headers = null` to receive the request's headers" : "`" + name + " = null`"hlError("hl:web: " + baseName(pathOf(key)) + ':' + n.line + ':' + n.col + " — the View reads '" + name + "', which this file declares nowhere: declare it at the file's root (" + hint + "), or write the name it was meant to be")}// does this file import that name by brace from another file?\* `--port=N` or `--port N` among the program's arguments, or null. *\portArgument() {let given = args()let i = 0while (i < given.length) {let a = given[i]let text = nullif (a.startsWith('--port=')) {text = a.slice(7)} else if (a == '--port' && i + 1 < given.length) {text = given[i + 1]}if (text != null) {let digits = text.length > 0 && text.length <= 5let b = toBytes(text)let k = 0while (k < b.length) {if (b[k] < 48 || b[k] > 57) { digits = false }k = k + 1}if (!digits || toNumber(text) > 65535) {hlError("hl:web: invalid --port value '" + text + "' — a port is a number 0-65535")}return toNumber(text)}i = i + 1}return null}importsName(key, name) {for (imp of graph.files[key].imports) {if (imp.key != null && imp.names.includes(name)) { return true }}return false}// the value of a static this file imports by brace from another .hl file, or null —// read off an instance of that file (a class value takes no computed index, an// instance does; the file is constructed once, its statics were evaluated at load)importedStatic(key, name) {for (imp of graph.files[key].imports) {if (imp.key != null && imp.names.includes(name)) {if (staticHolders[imp.key] == null) { staticHolders[imp.key] = construct(imp.key, constructionArgs(imp.key, {}, anonSession(), null, null)) }return staticHolders[imp.key][name]}}return null}// a READ in the View: a member (`title`), or a field path (`p.title`) whose root// is a member or a `for` row variable — the client decides which at render timeref(key, handle, rows) {let n = hlSyntax(gen, key, handle)if (n.tag == 'identifier') { checkRead(key, n.name, rows, n) return { k = 'member'; name = n.name; } }// A LIST WRITTEN IN PLACE (`for (n of [1, 2])`) is a literal, read as its value: the// same folded 'text' node a folded static is, so every reader takes it the way it takes// that one. Its entries are literals — anything else has no value before a read.if (n.tag == 'array_expression') {let items = []for (e of n.elements) {let el = hlSyntax(gen, key, e)if (el == null || el.tag != 'literal') {hlError("hl:web: " + baseName(pathOf(key)) + ':' + n.line + ':' + n.col + " — a list written in a View can hold literals only; declare it as a member to loop over anything else")}items.push(view.literalValue(el.kind, el.text))}return { k = 'text'; text = ''; kind = 'folded'; value = items; }}if (n.tag == 'member_expression' && !n.computed) {let obj = hlSyntax(gen, key, n.object)let prop = hlSyntax(gen, key, n.property)if (obj.tag == 'identifier') { checkRead(key, obj.name, rows, obj) return { k = 'field'; name = obj.name; path = [prop.name]; } }let inner = ref(key, n.object, rows)if (inner != null && inner.k == 'field') {let path = inner.path.slice(0)path.push(prop.name)return { k = 'field'; name = inner.name; path = path; }}}return null}// THE COMPOSED CHILD: an entry named after a class the file imports, whose file// has a View. `Child { count = count on done(v) { … } }` — the entries are the// BINDINGS (host values handed to the child's construction as named arguments)// and the reference-site handlers. The graph says which name is which file.componentKeyOf(key, name) {for (imp of graph.files[key].imports) {if (imp.default == name && imp.key != null && viewHandle(imp.key) != 0) { return imp.key }}return null}// THE REFERENCE'S SITE IS IN ITS PATH. A path of class names alone made two// references of one class under one parent (`body/Card` twice) the SAME node key,// so both collapsed onto one mount and one of the two fills was lost. A reference// is a construction: each one is its own mount, so each one is its own path.componentStep(cls, at) {return cls + '@' + at.line + ':' + at.col}// is this entry a `Style.rule` reference? (the `Style` of THIS file, by name)styleRef(key, e) {let obj = hlSyntax(gen, key, e.object)return obj.tag == 'identifier' && obj.name == 'Style'}// THE REFERENCE'S POSITIONAL SIGNATURE (§12: "a reference is a construction, so its// POSITIONAL entries bind to the class's declared properties in declaration order —// the same rule `new X(a, b)` follows").//// MEASURED, not guessed (2026-09-13, this worker): `new X(a, b, …)` fills the class's// root PROPERTY DECLARATIONS in SOURCE ORDER, one slot each — `on` handlers and// methods take no slot, and `hlMembers` returns exactly that list in exactly that// order, so the tree builder reads the signature straight off it. Three of those// declarations are not properties of the COMPOSITION and are left out here://// `View` and `Style` — what the component renders and paints WITH, not what it is// constructed from. The language does give them a slot (a// bare `new` binds them), but §12 says `Card { "text" }` with// no declared property takes its text as FILL, and a// component always declares a View: counting it would make// the positional-literal fill unreachable in every component// there is.// `slot` — the late-bound child, not state (§12); and with the fill// rendered where the child places it the two readings emit// the same HTML anyway.// every `static` — a static belongs to the CLASS (§12 build-time constants), so// a reference cannot seed one for every other reference.//// A NAMED assignment does NOT free its slot — measured: `new Q('n1', beta = 'B', 'n2')`// puts 'n2' in the SECOND declaration and 'B' in beta. It is applied AFTER the// positional ones and wins, which is why the positional bindings are emitted first// below. (COMPONENTS.md:711 reads "a property the reference also assigns by name is// not in the signature"; the interpreter says otherwise and the parenthetical in the// same line — "named is applied after positional; the name wins, as in `new`" — is// what this follows.)positionalSignature(key) {let out = []for (m of hlMembers(gen, key)) {if (!m.isStatic && m.name != 'View' && m.name != 'Style' && m.name != 'slot') { out.push(m.name) }}return out}component(key, cls, childKey, v, at, path, inFor, rows) {let bindings = []let positional = []let ons = []let fill = []let here = path.slice(0)here.push(componentStep(cls, at))let sig = positionalSignature(childKey)let taken = 0if (v != null) {for (h of v.entries) {let e = hlSyntax(gen, key, h)if (e.tag == 'object_property') {let val = hlSyntax(gen, key, e.value)// A BINDING NAMES A DECLARED MEMBER OF THE CHILD (COMPONENTS §12): the value// otherwise went into the construction and nowhere, with no word said// (mission 322 row 12). An element entry is the fill, not a binding.if (val.tag != 'object_expression' && !isMember(childKey, e.key)) {hlError("hl:web: " + baseName(pathOf(key)) + ':' + e.line + ':' + e.col + " — " + cls + " has no member '" + e.key + "' to bind: " + baseName(pathOf(childKey)) + " declares " + positionalSignature(childKey).join(', '))}// A LITERAL ON A REFERENCE KEEPS ITS TYPE (§12 "typed, not stringified"):// the binding carries the literal's KIND, and `view.refValue` turns the// pair into the value the child's member is given. Written as text alone,// `Level2 { n = 1 }` handed the child the String "1" and `n * 97` refused// (measured 2026-09-14, demo-nested wall 3).if (val.tag == 'literal') { bindings.push({ name = e.key; text = val.text; kind = val.kind; }) }else if (val.tag == 'identifier') {// a binding is a read at the reference site, so it is the same lookuplet r = nameRead(key, val.name, rows, val)// …and a folded static crosses as its VALUE, for the same reasonif (r.k == 'text') { bindings.push({ name = e.key; text = r.text; kind = 'folded'; value = r.value; }) }else { bindings.push({ name = e.key; member = val.name; }) }}else if (val.tag == 'member_expression') { bindings.push({ name = e.key; ref = ref(key, e.value, rows); }) }else {// `Card { span { … } }` — an element entry is not a binding, it is the FILLlet c = node(key, h, here, inFor, rows)// …and an EXPRESSION is neither, so it is refused rather than dropped:// the same rule an attribute follows (mission 312 wall 4).if (c == null) {hlError("hl:web: " + baseName(pathOf(key)) + ':' + val.line + ':' + val.col + " — '" + e.key + "' is bound to an expression on the reference " + cls + ", and a binding is a literal, a member or a field path: declare the expression at the root of " + baseName(pathOf(key)) + " (`" + e.key + "Value = …`) and write `" + e.key + " = " + e.key + "Value`")}fill.push(c)}} else if (e.tag == 'on_statement') {// A HANDLER WRITTEN ON A REFERENCE IS THE HOST'S, bound on the child's ROOT// element. The reference is a literal of THIS file's View, so its behaviour// is minted from THIS file's site and its body reaches this file through the// owner rule — the same path an element's own handler takes. The child's own// handler on that element is not replaced: both are listeners on one element// and both run, the child's first, because it was bound when the child's tree// was claimed. (Until 2026-09-13 the event was collected here and dropped: a// reference with `on submit` never bound, and a form submitted natively.)// …AND ITS SITE, because the tables file this handler's write set// under it: the browser repaints exactly what it wrote (mission 309).ons.push({ event = e.event; site = baseName(pathOf(key)) + ':' + e.line + ':' + e.col; })} else if (e.tag == 'member_expression' && !e.computed && styleRef(key, e)) {// a `Style.rule` written on a reference: the reference renders no element// of its own, so there is nothing for the class to land on (§12)hlError("hl:web: " + baseName(pathOf(key)) + ':' + at.line + ':' + at.col + " — a Style rule cannot be written on the component reference '" + cls + "': a reference renders no element of its own. Write the rule on an element inside " + baseName(pathOf(childKey)) + ", or name a '#" + cls + "' local rule in this file")} else {// AN ORDERED ENTRY. A literal or a read is the next declared property's// value while the signature has room — `Button { "SD" }` is `Button// { label = "SD" }` — and everything else, plus everything that EXCEEDS// the signature, is the FILL: a fragment of THIS file's View, with its// reads, its handlers and its `for` rows (§12 "host content").let c = node(key, h, here, inFor, rows)if (c != null) {if (taken < sig.length && (c.k == 'text' || c.k == 'member' || c.k == 'field')) {// the same rule as the named form above: a literal keeps its kind and a// folded static crosses as its valueif (c.k == 'text') { positional.push({ name = sig[taken]; text = c.text; kind = c.kind; value = c.value; }) }else if (c.k == 'member') { positional.push({ name = sig[taken]; member = c.name; }) }else { positional.push({ name = sig[taken]; ref = c; }) }taken = taken + 1} else { fill.push(c) }}}}}// THE NAMED ASSIGNMENTS COME LAST, so the name wins over the positional entry that// landed on the same property (measured: that is what `new` does)let allBindings = []for (b of positional) { allBindings.push(b) }for (b of bindings) { allBindings.push(b) }bindings = allBindings// FILLING WHAT PLACES NO SLOT IS A LOCATED ERROR (§12): the child would drop the// content silently, which is exactly the bug this rule was written for.if (fill.length > 0 && !isMember(childKey, 'slot')) {hlError("hl:web: " + baseName(pathOf(key)) + ':' + at.line + ':' + at.col + " — " + cls + " is filled here, but " + baseName(pathOf(childKey)) + " declares no 'slot' to fill: declare `slot = null` there and place `slot` in its View, or write the content outside the reference")}let classes = []if (styleRules(key).includes('#' + cls) && !viewIds(key).includes(cls)) {classes.push(view.localClass(key))for (t of rootTags(childKey)) { noteRef(key, t, '#' + cls) }}// THE NODE NAMES ITS CHILD, it does not carry it. What the browser needs to build// a child the server never mounted — the module url and the View tree — stands// ONCE in the seed's blueprint table, keyed by this `key` (blueprintsIn below).// The node used to carry that pair itself, and only when it stood inside a `for`:// a child deeper in a minted child's own tree therefore had nothing to build from// and was silently missing after a push (a comment card's like button and its// edit history, creator's social app, 2026-09-13).// THE REFERENCE'S OWN SITE: `file:line:col` of the reference literal, the id the// JavaScript target mints ITS behaviour class under — what a handler written on// the reference is built from, in the host's frame (client.hl bindRefOns)let site = baseName(pathOf(key)) + ':' + at.line + ':' + at.colreturn { k = 'component'; cls = cls; key = childKey; path = here; bindings = bindings; ons = ons; classes = classes; row = inFor; fill = fill; site = site; }}// ---- Style, the View side (§4, as the archive did it) --------------------------// The tree builder writes the class on the element and REPORTS what it wrote// (`styleRefs[key]` = [{ tag rule }]) and the element names it rendered// (`styleTags[key]`); web/style.hl spells the selectors from those. Three ways// a rule reaches an element: `Style.rule` written on it (class = the tag-prefix// strip of the rule name), a `#rule` local of this file matching the element by// NAME (class = this file's four-character class; `#Child` matches a composed// child's root elements), and — decided in style.hl, no class — a rule named// like an element of this file's own View.styleRules(key) {if (styleNames[key] != null) { return styleNames[key] }let names = []for (m of hlMembers(gen, key)) {if (m.name == 'Style') {let v = hlSyntax(gen, key, m.node)for (h of v.entries) {let e = hlSyntax(gen, key, h)if (e.tag == 'object_property' && !names.includes(e.key)) { names.push(e.key) }}}}styleNames[key] = namesreturn names}noteRef(key, tag, rule) {if (styleRefs[key] == null) { styleRefs[key] = [] }for (r of styleRefs[key]) { if (r.tag == tag && r.rule == rule) { return null } }styleRefs[key].push({ tag = tag; rule = rule; })return null}noteTag(key, tag) {if (styleTags[key] == null) { styleTags[key] = [] }if (!styleTags[key].includes(tag)) { styleTags[key].push(tag) }return null}// `#name` IS AN ID WHEN THE VIEW HAS ONE (creator's ruling, ticket #51 / R3): a// `#name` rule targets the element with `id = 'name'` if this file's View writes// that id, and is the file-local class of the elements named `name` otherwise. The// ids are read off the View's SYNTAX before the tree is built, because an element// named `name` may stand before the element that carries the id. Only a literal id// counts — a member-bound one has no value until render. A component reference's// own `id = …` is a binding for the child, not an element of this View; its fill is.viewIds(key) {if (styleIds[key] != null) { return styleIds[key] }let out = []let handle = viewHandle(key)if (handle != 0) { idsIn(key, hlSyntax(gen, key, handle), false, &out) }styleIds[key] = outreturn out}idsIn(key, block, isRef, &out) {if (block == null || block.entries == null) { return null }for (h of block.entries) {let n = hlSyntax(gen, key, h)if (n != null && n.tag == 'object_property') {let v = hlSyntax(gen, key, n.value)if (v.tag == 'object_expression') { idsIn(key, v, componentKeyOf(key, n.key) != null, &out) }else if (!isRef && n.key == 'id' && v.tag == 'literal' && !out.includes(v.text)) { out.push(v.text) }} else if (n != null && n.tag == 'view_for') {idsIn(key, hlSyntax(gen, key, n.body), false, &out)} else if (n != null && n.tag == 'view_if') {idsIn(key, hlSyntax(gen, key, n.consequent), false, &out)let alt = hlSyntax(gen, key, n.alternate)if (alt != null && alt.tag == 'view_if') { idsIn(key, { entries = [n.alternate] }, false, &out) }else { idsIn(key, alt, false, &out) }}}return null}// the root element tags of a component's tree (a composed child under `#Child`)rootTags(key) {let out = []for (n of tree(key)) { if (n.k == 'el' && !out.includes(n.tag)) { out.push(n.tag) } }return out}baseName(p) {let i = p.lastIndexOf('/')return i < 0 ? p : p.slice(i + 1)}element(key, tag, v, path, inFor, rows) {let attrs = []let children = []let here = path.slice(0)here.push(tag)let domTag = view.domTag(tag)// `body { }` IS THE PAGE'S BODY, and only as the View's root (COMPONENTS §6): below// the root it rendered a second, nested <body> no browser keeps (mission 322 row 5)if (domTag == 'body' && path.length > 0) {hlError("hl:web: " + baseName(pathOf(key)) + ':' + v.line + ':' + v.col + " — a `body` element below the View's root: `body { … }` is the page's body and stands only as a View's root")}noteTag(key, tag)let classes = []for (h of v.entries) {let e = hlSyntax(gen, key, h)let styled = falseif (e.tag == 'member_expression' && !e.computed) {let obj = hlSyntax(gen, key, e.object)let prop = hlSyntax(gen, key, e.property)if (obj.tag == 'identifier' && obj.name == 'Style') {if (!styleRules(key).includes(prop.name)) { hlError("no Style rule named '" + prop.name + "' in " + key) }if (prop.name.startsWith('#')) { hlError("'" + prop.name + "' in " + key + " is a LOCAL rule and applies by NAMING the element, not by being referenced — every '" + prop.name.slice(1) + "' this file's View renders already carries it; delete the reference") }let cls = view.kebab(view.className(prop.name, tag)) // the class is kebab-case (creator, 2026-09-12)if (!classes.includes(cls)) { classes.push(cls) }noteRef(key, tag, prop.name)styled = true}}if (styled) {// a Style reference is not a child} else if (e.tag == 'object_property') {let val = hlSyntax(gen, key, e.value)if (val.tag == 'literal' && view.isBoolAttr(e.key) && val.kind == 'boolean') {// A BOOLEAN ATTRIBUTE IS ITS PRESENCE (ticket #33): `disabled = true` is the// attribute, `disabled = false` its absence — decided here, once, for every// writer of the markupif (val.text == 'true') { attrs.push({ name = e.key; text = ''; }) }} else if (val.tag == 'literal') {attrs.push({ name = e.key; text = val.text; })} else if (val.tag == 'identifier') {// THE SAME LOOKUP A TEXT CHILD USES: a folded static becomes the// attribute's literal text, a member or a row stays a readlet r = nameRead(key, val.name, rows, val)if (r.k == 'text' && view.isBoolAttr(e.key)) { if (view.boolOn(r.value)) { attrs.push({ name = e.key; text = ''; }) } }else if (r.k == 'text') { attrs.push({ name = e.key; text = r.text; }) }else { attrs.push({ name = e.key; member = val.name; }) }} else if (val.tag == 'member_expression') {attrs.push({ name = e.key; ref = ref(key, e.value, rows); })} else {let c = node(key, h, here, inFor, rows)// AN EXPRESSION AS AN ATTRIBUTE VALUE IS REFUSED HERE, AND WAS DROPPED IN// SILENCE (mission 312 wall 4). `span { id = 'L3t' + n }` rendered no `id`// at all and said nothing — the element came back with empty attributes and// the author had no line to look at. A View attribute takes a literal, a// member or a field path; anything else is a member of this file, declared// at its root. The nested-element form (`div { span { … } }`) is the entry// whose value is a hybrid, and it is what `node` answers for above.if (c == null) {hlError("hl:web: " + baseName(pathOf(key)) + ':' + val.line + ':' + val.col + " — the attribute '" + e.key + "' is given an expression, and a View attribute is a literal, a member or a field path: declare the expression at the root of " + baseName(pathOf(key)) + " (`" + e.key + "Value = …`) and write `" + e.key + " = " + e.key + "Value`")}children.push(c)}} else {let c = node(key, h, here, inFor, rows)if (c != null) { children.push(c) }}}// a `#tag` LOCAL rule of this file matches this element by name: the file's class// — unless the View writes `id = 'tag'` somewhere, then the rule is that id's (R3)if (styleRules(key).includes('#' + tag) && !viewIds(key).includes(tag)) {let cls = view.localClass(key)if (!classes.includes(cls)) { classes.push(cls) }noteRef(key, domTag, '#' + tag)}if (classes.length > 0) {let joined = ''for (c of classes) { joined = joined == '' ? c : joined + ' ' + c }let merged = falsefor (a of attrs) {if (a.name == 'class' && a.text != null) { a.text = a.text + ' ' + joined merged = true }}if (!merged) { attrs.push({ name = 'class'; text = joined; }) }}// (§6 sibling entries: inside the literal `tag` would name the entry just written,// so the source key is taken before the literal is built)let srcKey = tag// THE SITE: `file:line:col` of the literal, the id the JavaScript target mints the// literal's class under — the browser builds this element's literal from it// (hlLiteralNew), so a row created after a list changed has a literal toolet site = baseName(pathOf(key)) + ':' + v.line + ':' + v.colreturn { k = 'el'; tag = domTag; key = srcKey; path = here; attrs = attrs; children = view.implied(domTag, children, here, site); site = site; }}// ---- an instance and its state -------------------------------------------------// THE ROUTE'S GROUPS ARE THE CONSTRUCTION'S NAMED ARGUMENTS (creator ruling// 2026-09-12): `:id` is a parameter of the class like any other, set and pinned// before its root runs, so `_post = db.fetch(id)` on line 1 sees it. Nothing is// laid over an instance afterwards, and nothing is spelled with a sigil.construct(key, params) {return hlLoad(pathOf(key), params)}// THE VIEW IS COMPILER OUTPUT, NOT A VALUE — and so, for a render, is the Style.// This half reads the View's SYNTAX (`tree()` below, through hlSyntax) and never its// value; `state()` ships neither to the browser; the stylesheet is folded into class// names at build, by a walk that constructs its OWN instance and reads `Style` there.// Evaluating them for a render therefore built a behaviour literal per element per// `for` row, per request, for nobody: half the construction time of a 2000-row// component, measured 2026-09-14.//// The framework says so in the one way the language already offers: it PINS them.// An explicitly passed construction value outranks every computed default (the// creator's rule of 2026-07-26), and a pinned member does not compute its default at// all. No language rule about a View was needed and none was added.renderArgs(key, args) {let out = argsif (declares(key, 'View')) { out.View = null }if (declares(key, 'Style')) { out.Style = null }return out}// does the file-class declare a member of this name?declares(key, name) {for (m of hlMembers(gen, key)) { if (m.name == name && !m.isStatic) { return true } }return false}// EVERY SERVER REFERENCE SITE of `key` (compile.hl `derivList`'s `srv`: an initializer// that calls an imported class) that transitively reads `root` — the same closure// client.hl's `moved()` walks locally for an ordinary derivation, run here instead// because a server reference site must never run in the browser (see that file's// `d.srv` branch). The VALUES come from `state` at the call site, already correct —// `mount()` just constructed it — this only decides WHICH of its keys belong in// `sync`, the same as `moved()` decides which of an instance's members to repaint.serverSyncNames(key, root) {let list = compiler.tableOf(gen, key).derivationslet moved = [root]let rounds = 0let again = truewhile (again && rounds <= list.length) {again = falserounds = rounds + 1for (d of list) {if (!moved.includes(d.name)) {for (r of compiler.memberNames(d.reads)) {if (r != d.name && moved.includes(r)) { moved.push(d.name) again = true }}}}}let names = []for (d of list) { if (d.serverSite && d.name != root && moved.includes(d.name)) { names.push(d.name) } }return names}// THE FRAMEWORK'S OWN CONSTRUCTIONS — the sheet walk (a component is constructed to// read its `Style`), an imported static's holder — happen outside any request, and a// class's root runs at construction: `me = session.user` there would meet a null,// which a real render never has (every request carries a session, minted if the// browser brought none). So they are handed an ANONYMOUS session — nobody logged in,// never saved — and the root takes its logged-out branch (the creator, 2026-09-13:// /__hl/app.css was a 500 while the page it styles was 200).anon = nullanonSession() {if (anon == null) { anon = new HlwSession(created = now(), seen = now()) }return anon}// THE SESSION AT CONSTRUCTION. A component that declares a member `session` is// constructed with the connection's session as that named argument — the same// instance a server face gets as its trailing argument, and nothing ambient: a// shell reads `me = session.user` at its root on the server, and a reload renders// logged in (measured by the creator 2026-09-13: the shell rendered logged-out// after a reload while the faces posted as alice). The browser gets only what a// page may see of it (`state()` below), never the id the cookie carries.constructionArgs(key, params, s, host, hdrs) {let wantsSession = declares(key, 'session')let wantsHost = declares(key, 'host')let wantsHeaders = declares(key, 'headers')if (!wantsSession && !wantsHost && !wantsHeaders) { return params }let args = {}for (k of params.keys()) { args[k] = params[k] }if (wantsSession) { args.session = s }if (wantsHost) { args.host = host }if (wantsHeaders) { args.headers = hdrs }return args}// THE HOST AT CONSTRUCTION (ticket #74), by the same rule: a component that declares// a member `host` is constructed with the host the request named — the `Host`// header without its port — so an app answering one address per subdomain renders// the right page on the server. A navigation takes it from ITS carrier: the socket's// upgrade request, or the POST's own. Null where the request named none, and for the// framework's own constructions above, which answer no request.hostOf(header) {if (header == null || header == '') { return null }let h = header.trim()// an IPv6 literal keeps its brackets: `[::1]:8080` is `[::1]`if (h.startsWith('[')) {let close = h.indexOf(']')return close < 0 ? h : h.slice(0, close + 1)}let colon = h.indexOf(':')return colon < 0 ? h : h.slice(0, colon)}// THE REQUEST'S HEADERS AT CONSTRUCTION (ticket #105), by the same rule again: a// component that declares a member `headers` is constructed with the request's// headers as a hash, every name lowercase — `headers['accept-language']`. A// navigation takes them from its carrier, as it takes the host: the socket's upgrade// request, or the POST's own, so they are what this browser sends. The cookie and// the credentials are LEFT OUT: a member is state, state is shipped to the page, and// nothing about a session ever reaches page script (`session = null` is the way to// it). An empty hash where the request named none; null for the framework's own// constructions, which answer no request.requestHeaders(raw) {let out = {}if (raw == null) { return out }for (k of raw.keys()) {let name = k.toLowerCase()if (name != 'cookie' && name != 'authorization' && name != 'proxy-authorization') { out[name] = raw[k] }}return out}state(key, page) {let out = {}for (m of hlMembers(gen, key)) {if (m.name != 'View' && m.name != 'Style' && !m.isStatic) {// A FUNCTION MEMBER IS NOT STATE. JSON has no form for it, and shipping// it as null would PIN null over the browser's own declaration (a// construction argument outranks the default) — so it is left out, and// the browser's instance evaluates `hideBadge = () => { … }` itself.if (m.name == 'session') {// the session's PUBLIC part: who is logged in — the id stays on the serverout.session = page.session != null ? { user = page.session.user } : null} else if (hlTypeName(page[m.name]) != 'Function') { out[m.name] = page[m.name] }}}return out}// everything the browser needs about one mounted component — and its children:// every component node of its tree is a mount of its own, constructed with the// bindings evaluated against THIS instance, keyed by the node's pathmount(key, params, s, host, hdrs) {return mountIn(key, params, s, host, hdrs, [])}// `chain`: the keys of the mounts above this one — what tells a component that renders// itself (#111) where its recursion standsmountIn(key, params, s, host, hdrs, chain) {let page = construct(key, renderArgs(key, constructionArgs(key, params, s, host, hdrs)))let line = chain.slice(0)line.push(key)let m = { key = key; params = params; view = tree(key); state = state(key, page); page = page; kids = {}; session = s; host = host; headers = hdrs; above = line; }mountKids(&m, m.view, {}, '', true)return m}// `rowKey` is what tells one row's child from the next one's: the index of every// enclosing `for` iteration, appended to the component node's path. A component// outside every `for` has the empty rowKey and keeps the key it always had.// `standing`: every `if` on the way down took this branch. A branch that does not stand is// mounted too — the browser switches to it without asking — EXCEPT a reference to a// component already above it: a recursive component (#111) mounted in both arms of every// `if` doubled per level, and under an `if` whose data does not shrink it never ended.// The browser builds such a child itself when its branch comes to stand.mountKids(&m, nodes, rows, rowKey, standing) {for (n of nodes) {if (n.k == 'component' && !standing && m.above.includes(n.key)) {// not mounted: see above} else if (n.k == 'component') {let args = {}for (b of n.bindings) {if (b.text != null) { args[b.name] = view.refValue(b) }else if (b.member != null) { args[b.name] = view.value({ k = 'member'; name = b.member; }, m.page, rows) }else if (b.ref != null) { args[b.name] = view.value(b.ref, m.page, rows) }}m.kids[view.kidKey(n.path) + rowKey] = mountIn(n.key, args, m.session, m.host, m.headers, m.above)// THE FILL IS THIS FILE'S FRAGMENT, not the child's: a component standing// inside it is a child of THIS mount, evaluated in THIS scopeif (n.fill != null) { mountKids(&m, n.fill, rows, rowKey, standing) }} else if (n.k == 'el') {mountKids(&m, n.children, rows, rowKey, standing)} else if (n.k == 'if') {let taken = view.value(n.cond, m.page, rows)mountKids(&m, n.then, rows, rowKey, standing && taken)mountKids(&m, n.other, rows, rowKey, standing && !taken)} else if (n.k == 'for') {// ONE MOUNT PER ROW: the row is the scope the bindings are evaluated in,// so the child is constructed once per entry, with that entry's valueslet entries = view.value(n.list, m.page, rows)if (entries != null) {let ri = 0for (entry of entries) {let inner = rows + {}inner[n.row] = entry// THE ROW'S KEY, not its index: a child of a row travels with the// record it was mounted for (view.rowKey, the one rule the server's// HTML walk and the browser's region read too)mountKids(&m, n.body, inner, rowKey + '#' + view.rowKey(entry, ri), standing)ri = ri + 1}}}}return null}// the route component and, if it declares one, its wrapping shell.// THE PARENT IS THE GATE (COMPONENTS §5, creator 2026-08-26): a shell whose render// places no `slot` — none in its View, or one inside a region that stands false for// this request — gets no page. The page is then not CONSTRUCTED, not only not shown:// its root does not run, and nothing of it reaches the document or the seed. The// shell is built first and rendered to learn that; `placed` is the browser's answer// when it already has one (a navigation into its own shell, or a region that opened),// and the server's render is not asked then.mounts(m, s, host, hdrs, placed) {let key = keyOf(m.route.component)let out = { page = null; shell = null; shellHtml = null; }let w = hlFile(gen, key).wrapperlet open = w == null || placed == trueif (w != null) { out.shell = mount(w, m.params, s, host, hdrs) }if (w != null && placed == null) {out.shellHtml = renderOf(out.shell, shellRoot(out.shell), minify ? null : tab, slotMarker)open = out.shellHtml.indexOf(slotMarker) >= 0}if (open) { out.page = mount(key, m.params, s, host, hdrs) }return out}// where the page goes in the shell's first render: the page's HTML replaces itstatic slotMarker = '<!--hl:slot-->'// `body { … }` as the shell's View root IS the page body (COMPONENTS §6): its// children are rendered into the document's body instead of nesting oneshellRoot(shell) {let root = shell.viewif (root.length == 1 && root[0].k == 'el' && root[0].tag == 'body') { return root[0].children }return root}// A MOUNT IS NOT ITS TREE. What the browser needs per mount is what makes THIS one// this one — its key, the route's params, the state the server evaluated, its// children — while the View tree belongs to the COMPONENT and stands once in the// seed's blueprint table under the same key. Shipping it per mount put the same tree// in the payload as many times as the page mounted that component.ship(mount) {let kids = {}for (k of mount.kids.keys()) { kids[k] = ship(mount.kids[k]) }return { key = mount.key; params = shipParams(mount.params); state = mount.state; module = moduleUrl(mount.key); kids = kids; }}\* A FUNCTION-VALUED BINDING IS NOT SHIPPED — the same rule `state` keeps for afunction MEMBER, for the same reason. A routine a host hands a child(`PostForm { onPosted = ... }`) has no JSON form, and shipping it as nullwould PIN null over the browser's own binding: a construction argumentoutranks what the client evaluates. It is left out, and the browser binds itfrom the host instance when it builds the mirror, exactly as it does for amember. Until mission 320 it rode the wire as null because `JSON.stringify`invented that null; the language refuses to now, which is what made the lievisible. *\shipParams(params) {let out = {}for (k of params.keys()) {if (hlTypeName(params[k]) != 'Function') { out[k] = params[k] }}return out}// A COMPONENT IS SERVED AT ITS OWN `.hl` URL. A browser executes a module by// its MIME type, never by its extension, so `text/javascript` on// `/components/home.hl` is a module — and the url the page loads is then the// path the author wrote in the route table, with nothing renamed in between.// The route that answers it is the APP's (a `function` route in the manifest),// because where an app serves its modules from is the app's to say.modulePath(key) {return '/' + key}// the url a module is LOADED at: its path and the build's version (see buildMark)moduleUrl(key) {return modulePath(key) + version()}// `?v=<hash>`: THE BUILD'S VERSION. The hash is over every text this app serves// under it — the runtime, each component module and package half as compiled (the// marks still in them), and the stylesheet — so any change to any of them, a// new compiler's output included, is a new version. Computed at the first ask and// again after a watched save; one hash for the whole build, because the modules// import each other and a per-module hash would have to be in its importers' text.version() {if (buildHash == null) {let all = runtime + stylesheet()for (k of served.keys()) { all = all + module(k) }for (k of packageHalfKeys(graph)) { all = all + module(k) }// AN OFFLINE APP'S WORKER AND MANIFEST ARE PART OF THE BUILD: the worker's entry// script names this hash, so a changed worker or a changed icon list is a new// script to the browser, which installs it and drops the old build's cacheif (offlineKeys != null) { all = all + workerText() }if (installable()) { all = all + JSON.stringify(webManifest()) }buildHash = sha256(all).slice(0, 16)}return '?v=' + buildHash}// the headers of a versioned answer: a request naming THIS build's version is cached// for a year and never revalidated; one naming another (or none) must not be, or a// cache would keep this build's text under a url the next build reusescached(req, type) {let h = { 'Content-Type' = type }h['Cache-Control'] = req.query != null && req.query.v == buildHash && buildHash != null ? 'public, max-age=31536000, immutable' : 'no-cache'return h}// A PACKAGE'S BROWSER HALF IS SERVED BESIDE THE RUNTIME. Not a convention this// file invents: the compiler derives the specifier it writes into every importing// module from the runtime url it is handed, and for the same reason that url is// one member here — one url means ONE ES-module instance, and two would be two// copies of the package's browser state. This is that derivation, on this side.halfUrl(pkg) {return halfDir + '/' + pkg + '/client.js'}// The module of the `.hl` url a request names — the on-demand half of the// transpile. The app's function route hands the path here.serveHl(urlPath, req) {let key = urlPath.slice(1)if (shipped[key] == null) {let js = module(key)if (js == null) { return notFound(urlPath) }shipped[key] = js.replaceAll(buildMark, version())}return new Response(shipped[key], { headers = cached(req, 'text/javascript; charset=utf-8') })}// ---- the stylesheet -------------------------------------------------------------// A `Style { }` member is an ORDINARY member of the file's class, so its value// is what a construction produces — the statics it reads already folded, the// `+` rule merge already the language's. That is the whole input; style.hl does// the walk. The project's styles file comes first so its global rules stand// under every component's, then every component this server can mount, in the// order `served` holds them (the pages and the wrapper chain above them — the// same set that has a browser module).stylesheet() {if (sheetText == null) {css.gen = gencss.view = viewlet files = []if (styles != null) {// THE STYLES FILE: root members, its inherits first. It has to be in the// graph (the app imports it) — its rules are read in authored order off// the syntax and their values off the constructed instance.let tokenKeys = [] // the token files already walkedfor (sk of inheritChain(keyOf(styles))) {if (graph.files[sk] == null) { hlError("the styles file " + sk + " is not in the project graph — import it from the app's entry so its rules can be read in authored order") }// A TOKEN FILE the styles file member-imports (`import { dark } from './tokens.hl'`,// there `static dark = var('#123')`) is walked BEFORE it, so its var() tokens are// named and written into :root first. Before ticket #39 part 2 only the chain's own// non-static members were named, and every imported token was an unknown id: a 500// "a css variable is used before any styles file declared it". (The architect's// patch from mission 021 of the tickets app, taken upstream.)for (tk of tokenFilesOf(sk)) {if (!tokenKeys.includes(tk)) {tokenKeys.push(tk)files.push({ key = tk; cls = graph.files[tk].class; rules = tokenRules(tk); aliases = []; refs = []; tags = []; hasView = false; isStyles = true; })}}let inst = hlLoad(pathOf(sk), {})let rules = []let own = {}for (m of hlMembers(gen, sk)) {// D32: `mode = 'static'` makes EVERY root member static (the file// directive's whole point — a token file need not repeat the keyword),// so `m.isStatic` cannot be what decides whether a member is a css// rule/token: that used to exclude every rule of a mode='static' file// and emit none of them. What a rule IS — a var() token or a block —// is a question about its VALUE, which css.hl already answers below and// in sheet() (isBlock / isVar); static or not, a plain-scalar helper// constant folds into nothing there regardless.if (m.node != 0) {let v = hlSyntax(gen, sk, m.node)// a merged rule (`a = b + { … }`) is a block too (css.entriesOf)rules.push({ key = m.name; node = m.node; value = inst[m.name]; isBlock = v != null && (v.tag == 'object_expression' || (v.tag == 'binary_expression' && css.isHybridValue(inst[m.name]))); })own[m.name] = true}}// EACH FILE OF THE CHAIN IS ITS OWN INSTANCE, so a token it INHERITS is a second// CssVar with its own id: `body { color = dark }` in the child held an id the// walker never named (ticket #39 part 1). The inherited tokens are registered// under their names as ALIASES — named, never written into :root a second time.// (D32, as above: `css.isVar` alone decides it, not `m.isStatic`.)let aliases = []for (pk of inheritChain(sk)) {if (pk != sk) {for (m of hlMembers(gen, pk)) {if (own[m.name] == null && css.isVar(inst[m.name])) { aliases.push({ key = m.name; value = inst[m.name]; }) }}}}// the styles file's ROOT is collected here and not by css.entriesOf, so the// same refusal has to be raised here: two `@media` root members are one// name with one value and would emit the first block emptycss.guardMedia(sk, rules)files.push({ key = sk; cls = graph.files[sk].class; rules = rules; aliases = aliases; refs = []; tags = []; hasView = false; isStyles = true; })}}for (k of served.keys()) {if (hasStyle(k)) {tree(k) // the refs and tags are collected while the tree is builtlet inst = construct(k, constructionArgs(k, {}, anonSession(), null, null))files.push({ key = k; cls = graph.files[k].class; rules = css.entriesOf(k, styleNode(k), inst.Style); refs = styleRefs[k] != null ? styleRefs[k] : []; tags = styleTags[k] != null ? styleTags[k] : []; ids = viewIds(k); hasView = viewHandle(k) != 0; isStyles = false; })}}sheetText = css.sheet(files)}return sheetText}// the .hl files a styles file member-imports that hold var() tokens (a package like// hl:web/css is skipped), in import ordertokenFilesOf(key) {let out = []for (imp of graph.files[key].imports) {if (imp.key != null && !imp.key.startsWith('hl:') && imp.names != null && imp.names.length > 0 && graph.files[imp.key] != null) {if (!out.includes(imp.key) && tokenRules(imp.key).length > 0) { out.push(imp.key) }}}return out}// a token file's var() tokens as styles-file root rules, in authored order — EVERY static// token of the file, not only the imported names (a token may name another). The values// are the file's statics, evaluated once per process: the same CssVar instances, with the// same ids, the importer holds.tokenRules(key) {let rules = []let inst = nullfor (m of hlMembers(gen, key)) {if (m.isStatic && m.node != 0) {if (inst == null) { inst = hlLoad(pathOf(key), {}) }let v = inst[m.name]if (css.isVar(v)) { rules.push({ key = m.name; node = m.node; value = v; isBlock = false; }) }
Only the first lines are shown.
Branches
- mainmain branch
Latest commits
- 99c73346antcolony#40: history (LOG.md), worker briefs (missions/) and reports moved here from antcolony, numbered per project; old numbers in antcolony docs/mission-map.mdmre
- 76edaa62calendar: Hybriel master ff51cf46 (re-vendor round, static workaround removed)mre
- 90a3fc2cdeploy.sh: back up live storage/.sessions/.env before every deploy (newest 5 kept)mre
- 6722b72ddeploy.sh: never send .git or .gitignore to Byrodinmre
- be099807State of 2026-09-27, before the move to gitoriamre