slate 0.0.27 — browser parity
slate 0.0.27 — browser parity
The principle this release was cut to: the only built-ins allowed to fail in a browser are things
a browser does not have. A host that has a thing only in a different shape — asynchronous where
slate's signature is synchronous — does not have it, and refuses with a sentence naming why. Seven
items were measured against that rule and six of them turned out to be work owed rather than a
limit; what is left refuses in its own words instead of promising a release that is never coming.
Behaviour changes
A builtin is a parameter of the emitted program, not a name taken from globalThis. slate js
used to install every builtin into the host's global scope, which made slate the owner of
setTimeout, fetch, close and two hundred other names for the whole process — so a host API
that called one got slate's. node's own WebSocket is what found it: its handshake calls
setTimeout and calls .unref() on what comes back, and slate answers its own integer id, so the
socket never opened and nothing anywhere named a timer. In a browser the same thing takes those
names from every other script on the page. The emitted program is now a function whose parameters
are the builtins, and a program may still declare a name a builtin has.
A repeated response header from fetch is joined with ", ", and Set-Cookie is a list. It
used to keep the last, which silently threw one away — Link, Vary and Via all repeat.
Set-Cookie is excluded from that by RFC 9110's own note and the reason is arithmetic: a cookie
carries commas inside itself, so two joined that way cannot be taken apart again by anything.
slate:brotli refuses under slate js naming brotli, rather than saying it is not built yet.
No JavaScript host has a brotli encoder and none is coming, so the old sentence was a promise nobody
could keep. It points at slate:gzip, which is the compression a browser does have.
slate:gzip is new and every one of its four names answers a promise, on both hosts. A
browser's compression is a TransformStream and there is no synchronous door to it. Nothing held
the signature yet, so it was written promise-shaped everywhere rather than synchronous in one place
and refusing in the other. slate:brotli keeps its synchronous signature, which already exists.
What is now whole in the JavaScript back end
slate:time's calendar, overIntl. 2985 of 2985 offset readings agree across 2000–2025.slate:crypto, written out rather than built on WebCrypto —crypto.subtleis promise-only,
and the signatures here are not.slate:jwtworks underslate jsas a consequence.slate:regex, by translating the pattern into aRegExprather than handing it over. Three
constructs both engines compile and quietly match differently (\s,., and^/$underm)
are rewritten;\p{...}is translated rather than refused.slate:ws's client, over the host's ownWebSocket. The server half cannot exist in a
browser, nothing there being able to listen.slate:gzip, overCompressionStream— with the gzip container parsed by slate on both back
ends, so every refusal about a header, a trailer or a limit is the same sentence wherever a
program runs.fetch, over the host's own. It was the last global the back end owed.monotonic, pinned on both hosts.
What refuses, and what each refusal says
Every one of these is a thing a JavaScript host does not have, in the shape slate asks for:
| name | why |
|---|---|
abbrev, isDST |
Intl gives no zone abbreviation and no DST flag; the offset-comparison rule is unsound in both directions |
slate:jwt's RS/PS/ES algorithms |
the asymmetric half needs crypto.subtle, which is promise-only, and sign answers bytes. HS256/384/512 are whole |
slate:brotli's compress/decompress |
no JavaScript host has a brotli encoder |
slate:ws's accept (and listen under it) |
a browser cannot listen |
slate:ws's ping |
a browser writes the protocol's control frames itself |
fetch's trust |
no host lets a program add a trust anchor for one request |
the possessive quantifier, atomic group, branch reset, recursion, conditional, \K, \G, \C, \X, and a pattern-wide (?i) |
a RegExp has nothing to mean by them, and each is refused where the pattern is written, naming the construct |
Differences that are documented rather than closed
One more is worth separating from that list because it is not a refusal at all: an unbounded
lookbehind is refused by BOTH back ends, PCRE2 being the stricter engine there and RegExp taking
anything — so it is refused here too, to keep the two agreeing. A bounded one compiles on both.
docs/reference/javascript.md names all of them: the redirect rule is the host's; Set-Cookie is
absent in a browser, being a forbidden response-header name; a deflate body that will not inflate
gets one sentence where the interpreter has three; print of a promise says <promise>; the two
back ends read two copies of tzdata and they drift at projected dates; and three regex readings
under i and around unset groups.
Under the hood
The OWED list is checked as three sets — every name on it is a real builtin, every builtin
resolves in the runtime or is on it, and every name in $b is reachable now that a builtin is a
parameter. The check found five natives in neither the runtime nor the list.
A differential corpus of twenty-two programs runs under both back ends and must say the same
thing byte for byte; docs/ is executable and every fenced block runs.