Skip to content

Releases: slate-language/slate

slate 0.0.34 — class properties, symbol-named operators, return-type inference

Choose a tag to compare

@edadma edadma released this 06 Sep 03:37

Classes gain properties and operators named for the symbol they overload, the checker learns to
read a function's answer off its own body, and eighteen builtins arrive together.

Class properties: get and set

A class body can declare a computed property instead of a plain field:

class Rect
    var w
    var h

    get area(self) = self.w * self.h

    get width(self) = self.w

    set width(self, v)
        self.w = v

r.area and r.width call the getter; r.width = 12 calls the setter. async get/async set are
refused at the point of declaration — a property read has no way to be awaited by its caller.

Operators are methods named for the symbol — BREAKING

A class overloads + - * / % == < <= > >= <=> and unary - by naming a method after the operator
itself, not by a word:

class Money
    var cents

    +(self, o) = Money(self.cents + o.cents)
    unary_-(self) = Money(-self.cents)
    <=>(self, o) = self.cents - o.cents

The word-shaped hooks this replaces — plus, equals, and the rest — are gone. Any class that
overloaded an operator through one of the old names stops being called for it; rename the method to
its symbol.

Return-type inference, and if/match typed by their value

A function's answer is inferred from its body rather than needing an annotation, and an if or
match used for its value is typed from what its branches actually produce. Type narrowing follows a
test down the branch it holds on:

widen(x: string | number) = if x is string then len(x) else x * 2

x is string in the then branch and number in the else, so both calls type-check.

Eighteen new builtins, and cookies on slate:dom

  • Promise combinators: all, allSettled, race, any.
  • Strings: padStart, padEnd, replaceAll, includes.
  • Collections: groupBy, zip, unique, chunk, count, partition, minBy, maxBy.
  • Numbers: toFixed, formatNumber.
  • slate:dom: cookies(), cookie(name), setCookie, deleteCookie.

Alongside these: the JavaScript back end refuses await outside an async function at compile time
rather than at run time; imageShape reads any PNG header rather than a fixed subset; ESM resolution
finds node's own built-in modules; and browser fetch resolves a relative URL against the page rather
than refusing it.

0.0.32's path-traversal-safe files() and 0.0.33's test-hook event-loop fix are already out and
unchanged here.

Installing

brew update
brew upgrade slate

The formula names the same nine Homebrew libraries the last release did.

The compiler floor is sysl 0.0.105.

slate 0.0.33 — slatelang.dev goes live, and a test's hooks share its event loop

Choose a tag to compare

@edadma edadma released this 06 Sep 00:38

The documentation goes live, and a test's hooks stop being three separate programs. Two items,
and the first of them is the reason this release exists at all: https://slatelang.dev is deployed
by this tag.

https://slatelang.dev is live

The site is built from the pages already in this repository. site/content is a symlink to
../docs, so there is exactly one copy of every page: the file GitHub renders in the tree is the
file the site builds, and tests_docs.sysl is the check that both are true. Nothing was copied and
none of the existing prose moved — every page gained title and weight frontmatter, which is what
orders the sidebar, and that is all.

Three sections are new, and every fenced program in them is run by the suite like the rest — 24
runnable blocks and 5 refusals between them:

section what it is
Getting Started install, a first program, the test runner, the JavaScript back end
A tour for JavaScript and TypeScript people what differs, and why — the parts that are not JavaScript wearing a different hat
Packages sluice, pg, logger, lath, mortar

.github/workflows/docs.yml builds on dev and deploys only on stable, so a doc change merges
without going live and the site documents the released language rather than the integration branch.
The wordmark is "slate" in Fraunces Italic converted to outlines, one path per glyph, and
site/grammars/slate.tmLanguage.json is slate's own highlighting — the keywords the lexer actually
has, s"..." with its holes, elements, slate: module names and _.

A test's hooks and its body share one event-loop scope

A @setup, the test it prepares and its @teardown are one scope on the interpreter now, and
the reference page says so where it said nothing before. slate test drained the loop after every phase, so the
three were three separate programs and a hook could not do either of the two things a hook is for:

@setup
open_a_listener() =
    server = listen(0, conn -> close(conn))

@teardown
give_back_what_the_test_took() =
    if armed != null then clearTimeout(armed)

    close(server)

A listener opened in a setup hung the run forever — the setup's own drain waited for a handle that
never closes on its own. And a timer armed in the body was waited out before the teardown that
was going to clear it, so setTimeout(never, 3000) failed the test three seconds later on what the
timer did.

Each phase is now run only as far as its own answer settling, and the loop is drained once, after the
teardown, down to what was outstanding when the setup began. @setupAll/@teardownAll bracket the
file the same way, and their floor is what keeps a store opened once for a whole file out of every
test's drain.

slate js had all of this right already — node's loop is one scope and $suite awaits each
phase — so this was the interpreter alone, and the divergence was invisible to any single-host run.

Installing

brew update
brew upgrade slate

The formula names the same nine Homebrew libraries 0.0.32 did — nothing was added to or dropped from
the link line.

The compiler floor is sysl 0.0.105.

slate 0.0.32 — a traversal fix in the static file server, and Argon2id under its own name

Choose a tag to compare

@edadma edadma released this 05 Sep 23:10

A security fix in the static file server, and Argon2id moving to the module that says which
algorithm it is.
Small beside 0.0.31, and the first item is the reason it is not waiting.

files(root) served files above the root — upgrade

Every 0.0.31-and-earlier server using files() should upgrade. safePath cut the request path
on / and percent-decoded each part afterwards, so the .. check ran on text that was not ..
yet. Both

GET /%2e%2e/secret.txt HTTP/1.1
GET /..%2fsecret.txt HTTP/1.1

passed it and then decoded into a climb: against a live serve(port, files("./public")) each
handed back a file above the root with a 200. A URL parser will not build either request — it
resolves .. and reads %2e%2e as a dotted segment before a byte reaches the network, which is why
curl needs --path-as-is — so nothing that goes through fetch could have found this, and the
test that reproduces it writes the request line onto a socket.

Every part is decoded before it is judged now, and the answer is 403 — a refusal rather than a
repair, which is the rule the file already stated — for a part that decodes to .., a decoded part
carrying /, \ or a NUL byte, and a % that is not followed by two hex digits. An empty or .
part is still dropped, and an escape meaning an ordinary character is still decoded and served:
/a%2etxt is a.txt. The joined path is then normalised and checked to be under a root normalised
once at files(), so a later mistake in the part rules is a 403 rather than a file.

A symlink is still not resolved: the check is on the name the request wrote, so a link inside the
root pointing out of it is the operator's choice, as it is in nginx. The router's :name decoding is
the same shape and is deliberately left alone — that path is cut on the separators the client
actually wrote and nothing in it is judged.

slate:password is gone; Argon2id is slate:crypto's

A breaking change, and the only one. The module was one algorithm behind four generic names, next
door to a module that spells sha256 and pbkdf2 out. It now reads as what it is:

0.0.31 0.0.32
hash(p) argon2(p)
hashStrong(p) argon2(p, { memoryCost: 65536, timeCost: 3 })
check(r, p) argon2Verify(r, p)
needsRehash(r) argon2NeedsRehash(r)
import { argon2, argon2Verify } from slate:crypto

async main()
    val stored = await argon2("correct horse")

    print(await argon2Verify(stored, "correct horse"))

The PHC bytes are unchanged, so every record already written keeps verifying. A stored
$argon2id$v=19$m=19456,t=2,p=1$… needs no migration and argon2NeedsRehash answers false about
it, exactly as needsRehash did.

hashStrong does not survive and does not need to: the parameters are a record now — memoryCost,
timeCost, parallelism, hashLength, salt, node's names, so what is written here is what
the documentation everybody already reads calls them — and the heavy profile is two numbers rather
than a second name for one pair of them. An unknown key is refused, a login thought to have been
strengthened and silently not being the worst shape that mistake takes.

And it crosses to the JavaScript back end, which is what the work was for. node carries Argon2id
in its core crypto as of node 24, and node and monocypher derive the same bytes: a record written
over a fixed salt is compared as text across the two hosts, and each verifies a record the other
made. Under an older node the two names fault with a sentence saying so. A browser has no Argon2 at
all
and crypto.subtle is not a near miss — PBKDF2 and HKDF are fast by construction — so both
refuse in a page rather than deriving something weaker. argon2NeedsRehash works everywhere, reading
the record's own parameters and deriving nothing.

slate add records the whole graph

slate.sum is now written even where the command changes no byte of the manifest. The package cache
is shared between projects, so a version somebody else had already fetched left the sum file as
incomplete as it found it, and slate deps then reported for ever that a dependency was not
recorded.

Installing

brew update
brew upgrade slate

The formula names the same nine Homebrew libraries 0.0.31 did — nothing was added to or dropped from
the link line.

The compiler floor is sysl 0.0.105.

slate 0.0.31 — five new modules, a server on the JavaScript host, and Unicode where ASCII used to be

Choose a tag to compare

@edadma edadma released this 05 Sep 22:07

Five new built-in modules, a server on the JavaScript host, and Unicode where ASCII used to be.
The largest release since the language got a name, and every item of it is something a consumer —
sluice, lath, mortar, the board — asked for by not being able to write something.

slate:sqlite, the database that needs no server

Eight natives are the floor — the connection, the statement and the row being C's — and everything a
program touches is written in slate. One export, because a database is one object: exec,
query, run, transaction, compiledWith and close are methods on it. They could not have been
exports whatever the design — close is slate:net's name, query is slate:dom's and run is
slate:process's, and every built-in module declares into one scope — so it reads the way
slate-language/pg does and the two databases a program
might reach for are not learned twice.

Statements are compiled once per piece of SQL and kept, bounded so a program building SQL in a loop
does not accumulate one per string. transaction commits on a return and rolls back on a fault.

Under slate js this is node's node:sqlite, so the module is whole on both hosts and a browser
refuses naming it. node exposes no sqlite3_bind_parameter_count, so the runtime works the count out
from the SQL itself — without that the two back ends would disagree about the commonest mistake there
is, node binding SQL NULL for a ? nobody gave. Eight shapes of SQL are compared against SQLite's own
answer; the ninth is select "hello", a string literal in the build macOS ships and no such column
in node's, which is two builds of one library rather than anything slate can settle.

slate:lmdb, a store a server can restart over

Every server slate is aimed at wants the same three things and had nowhere to put them: a session
store, a rate-limit bucket per client, and a replay ring for the events a reconnecting browser missed.
A read takes no lock and blocks nothing, which is the shape a request handler needs.

MDB_NOTLS is always on and is not an option. LMDB's default gives each thread one reader slot,
so a second read transaction on a thread that has one is refused — and the message, "Invalid reuse of
reader locktable slot"
, reads as a damaged lock file rather than the design decision it is. slate is
one thread with an event loop on it, so two handlers each holding a reader is the ordinary case.

A key that is not there is null and deleting one is false; everything else faults with a sentence,
and the two worth catching name the knob — a full map names mapSize, a reader asked to write names
lmdbWrite. No JavaScript host has LMDB and all nineteen names refuse there.

slate:image — PNG, JPEG, GIF and WebP

An image is a record{ width, height, channels, pixels } — and not an opaque handle, because
the pixels are the point: a program wants to read them, store them, send them, or build one itself. A
thumbnail is encodeJPEG(resizeImage(readImage(upload).value, 200, 200), 80) with nothing in the
middle to give back.

readImage and imageShape answer results and the other three fault, which is the library's rule:
bytes from an upload are a 400 to send, and a record the program built that lies about its own
dimensions is a defect. imageShape exists for the size guard — the pixels are
width * height * channels bytes however small the file was, so a 4 KB PNG claiming 20,000 square is
1.2 GB the moment anything decodes it.

WebP is what a browser writes when a page re-encodes a photograph before uploading, and stb has
never read one — so it is a second binding, over libwebp, routed by the twelve-byte RIFF header rather
than by trying stb and catching the refusal. encodeWebP(image, quality) and
encodeWebP(image, { lossless: true }) are the writing half. libwebp has no greyscale, so one and two
channels are converted here with stb_image's own luminance weights: readImage(upload, 1) means one
thing whatever somebody uploaded. An animated WebP is refused by both readers, naming the demuxer that
reading one would take.

slate:zstd, and Content-Encoding: zstd on a response

Zstandard fills the gap the other two encodings leave between them: brotli at quality 5 is the slow end
of what a request handler can afford and deflate is the fast end and compresses worse than either,
where zstd at level 3 is several times faster than deflate and compresses better. slate:http writes
Content-Encoding: zstd for a client that asks for it and brotli otherwise, and Accept-Encoding is
now read once per name — so zstd;q=0, br gets brotli rather than nothing.

Under slate js this is node's zlib. node's decompressor is silent about a frame that ends in the
middle
— an empty buffer and no error, where libzstd says srcSize_wrong — so the runtime reads the
frame header and measures what came back against what it claimed.

slate:net and slate:llhttp on the JavaScript back end

The whole of slate:net was owed under slate js, and what that cost is not slate:net:
slate:http is written in slate over listen, onBytes, send and close, so no server could be
started there at all
— and lath, whose router is checked by rendering a page through a real request,
had two tests skipping on every run for want of a listener.

slate:llhttp is a parser here rather than a binding — node's own is behind
internalBinding('http_parser'), which is why undici carries llhttp compiled to wasm — so the HTTP/1.1
request grammar is written out, refusals and all: 400 for bytes that were not HTTP, 431 for a head over
the limit, 413 for a body over it, and the two smuggling shapes llhttp refuses by construction. TLS is
still owed and says so.

Case, whitespace and the normal forms are Unicode's

upper, lower and trim were the ASCII range and are the whole database now; normalize(s, form)
and casefold(s) are new. What decided every detail is that a JavaScript host answers all of this
natively, so the back end carries a table of the hundred and two characters that uppercase to more than
one, writes out the final-sigma condition, and spells trim out on both hosts because ECMAScript's
whitespace is not the database's. Every code point there is was run through both back ends and compared.

Placeholder lambdas

map(xs, _.name)
filter(ns, _ > 3)
sorted(ps, _.age < _.age)

A _ where a value goes becomes a parameter of a function nobody wrote, whose body is the smallest
scope around it: a call's argument, a bracketed group, or the value of a binding, an assignment or a
return. Every _ is a parameter of its own, left to right, which is Scala's rule and the
notation's one surprise — _ > 3 && _ < 9 is a function of two parameters, and the checker says so
where it is written. A lone _ standing as the whole of a scope is handed outward, so f(_) names f
and add(_, 1) is the partial application it reads as. A _ no scope took in is refused with a
sentence pointing at it.

Desugared in the parser into the same Lambda node a written lambda makes, so the checker, the machine
and the JavaScript back end learn nothing.

Test hooks, assertFaults and --only

@setup and @teardown run before and after each @test in a file; @setupAll and @teardownAll
run once, either side of the whole file. A fault in a setup fails the tests it guards and the failure
names the setup
; a skip in one leaves them out with its reason; a teardown runs however the test
went and its own fault is a verdict of its own.

assertFaults(fn) and assertFaults(fn, message) are the assertion that cannot be written as a
condition — an async call's fault arrives a turn later, so it answers a promise the test awaits.
slate test --only <substring> runs the tests whose name contains it; a filter matching nothing says
so and exits non-zero.

slate:dom can read a page a server rendered

nodeKind, property, createComment and splitText, and before them dispatch, observe and
events. tagName answers null for a text node and for a comment alike, so a reconciler walking a
server's markup read a comment as a piece of text; attribute goes on saying what the markup said
however much has been typed into the field since, so nothing could read back what setProperty writes;
and one run of server text has to become the two children a component rendered. dispatch(node, event)
sends what on reads, observe is a MutationObserver, and events(url, options) is the reading end
of slate:http's sse.

Smaller things

  • slate.sum records the whole dependency graph. A package's own manifest arrives with the
    package, so the first resolution of a cold cache saw only what the project itself declared, and
    slate deps then reported a transitive dependency as not recorded for ever. fetch_graph resolves,
    fetches, and resolves again until a round arrives with nothing new.
  • slate --help and -h print the usage. They fell through to the last arm of the command line and
    answered cannot read --help: no such file or directory.
  • writeBytes and appendBytes in slate:fs, with their Sync twins.
  • A third position on indexOf and lastIndexOf, over both arrays and strings, with a
    Boyer–Moore–Horspool search under the string forms.
  • stat's missing modified in the JavaScript host, and onSignal/offSignal there.
  • close(server) cuts a stream that never ends, rather than waiting for it.
  • A JavaScript back-end defect the placeholder work turned up: js_pat.sysl emitted ["_"] for
    both Wild and Bind, so val [_, second] = [1, 2] bound second to 1 under slate js and to 2
    under the interpreter.

Installing

brew update
brew upgrade slate

The formula now names zstd, lmdb and webp beside th...

Read more

slate 0.0.30 — asset imports, a bytes body, and a builtin that holds its arguments

Choose a tag to compare

@edadma edadma released this 05 Sep 16:01

Ten things two frameworks asked for, one of which is a defect in the interpreter that had been
there since builtins could take a callback at all.

A builtin holds its arguments across a callback that collects

["a", "b", "c"].map(r -> allocate()) could free the array it was walking. args is a buffer
the collector does not walk, and by the time a builtin runs the call has already taken the callee and
its arguments off the operand stack — so for the length of a native call the only thing holding them
was that buffer. Every builtin that answers without running slate code was fine; the ten or so that
call back into it were not.

What it looked like was never a memory failure, which is why it survived so long:

error: this array was changed while `map` was walking it

about a literal nothing in the program can reach. Binding the array to a val moved the failure onto
the callback"this function takes from 1 to 0 arguments" — the freed slot having been taken by
something else and read back as a function of another arity. Binding both made it pass, and the
JavaScript back end passed throughout.

The rooting is at the dispatch and not in each builtin, which is the point: the hole was in map,
and it was in filter, reduce, sort, find, flatMap, forEach, some and every too. A rule
written out per builtin had already been half-applied, which is how map came to be the one without
it. Found by lath's reconciler.

entries and keys no longer report a proto nobody wrote

entries(One(5)) answered [["a", 5], ["proto", <data One>]], so an object copied out of a data
value was not the value it copied, and has(p, "(class)") was true of a name no program can write.
The printer had hidden both since protos shipped; the six walks over an object had not.

keys, values, entries, has, len and without now read one answer, and it is the printer's:
a proto on a class instance or a data variant is machinery, and a proto a program wrote on a
plain object is a field.

with on a data value refuses a field the type does not have

One(5) with { id: "3" } answered One(5, "3") — a variant carrying a field its own declaration does
not have, printed as though there were a second positional one. Nothing else in the language can build
such a value. A field the type does have still updates, which is what with is for.

Importing a file that is not slate

import styles from "./button.css"
import template from "./welcome.html"

A quoted path naming anything but .sl or .slx is an asset, and one name takes the whole of it as
a string.
The file is read while the program is compiled and travels inside it, so nothing sits
beside the binary at run time and six files importing one stylesheet is one string — and under
slate js the text is written into the emitted program, byte for byte.

The extension decides, never what the writer meant, and each form is refused in the other's:
import { helper } from "./styles.css" says there are no names in it to take, and a bare name asked of
slate source says slate has no default export.

A package's assets are the package's own: a .css shipped beside a .slx is imported by that file,
relatively, and handed on as an ordinary exported value — so the package system had to learn nothing.

Two refusals, both before the program runs: a file that is not there, named beside the file that asked
for it, and a file that is not UTF-8, which is refused rather than mangled — slate has one text
type, and a program that wants bytes wants readBytes.

req.bytes, and a body that is not text

req.body was the only reading of a request body and it is from_utf8(...).unwrap_or("") — so a
PNG posted to a route was the same value as a request that carried no body at all: no header, no
status, no fault. req.bytes is beside it now and is what actually arrived; req.body is unchanged,
so nothing already written moved.

req.address

slate:http told a handler the method, the path and the headers and nothing about who connected,
so a rate limiter, an allow-list and a log line each had only whatever a proxy wrote into a header.
remoteAddress(conn) is the slate:net half; a listener has no other end and answers null.

An IPv4 client of a dual-stack server reads as 127.0.0.1, not ::ffff:127.0.0.1: listen binds
:: so that localhost reaches the server, and an allow-list written against the address a client
dialled has to match.

skip(reason) in the test runner

@test
a_server_answers_what_it_is_asked() =
    if !canListen() then skip("this host has no listener")

    assertEq(ask("/"), "hello")
  skip  tests/api.sl :: a_server_answers_what_it_is_asked   this host has no listener

7 passed, 1 skipped

A third verdict and not a kind of pass, so a suite that quietly stopped running half of itself
cannot report a page of greens. It raises, so nothing after it runs; a catch does not get it, which
is exit's rule; and a run that skipped nothing says exactly what it always said.

without(o, key)

The one object operation there was no way to write. keys, values, entries and has read and
with writes, so a table that had to forget something rebuilt itself out of everything else — which
is what sluice's session store does. A new object, as with's answer is.

insertBefore and removeChild in slate:dom

setChildren writes the whole list and these move one node. A keyed reconciler moving three rows
of a thousand should not make the page do a thousand pieces of work — jsdom's MutationObserver
counted ~1001 records for exactly that. before of null appends, and a node already in the page
moves rather than being copied, so it keeps its focus and its scroll position.

Two things the parser was refusing

then may be indented under the line its if began on, as else and elif already could:

val kind = if len(headers) == 0
    then "empty"
    else "carrying"

And an element may stand where any other value may. return <p>x</p> was a bare return followed
by wreckage, because the table saying which tokens begin an expression had never learned about one —
and the complaint, "this is where the statement should have ended" pointing at the <, sent the
reader to look at the element rather than at the return.

slate 0.0.29 — base64url, and a source that is told

Choose a tag to compare

@edadma edadma released this 05 Sep 12:20

Two things a framework asked for, and neither could be had from outside the compiler: a base64url
a program can reach, and a streamed response that tells its source the reader has gone.

base64url in slate:url

import { base64urlEncode, base64urlDecode } from slate:url

val cookie = base64urlEncode(hmac("sha256", secret, payload))
val r = base64urlDecode(fromTheClient)

RFC 4648 §5's alphabet — base64 with - for + and _ for /and no padding at all, a =
being exactly what the alphabet exists to avoid.

Encode takes a string or an array of bytes, and a string is its UTF-8 bytes: the same reading
encodeComponent gives one beside it.

Decode answers a result whose value is BYTES. Text encoded this way arrives from outside — a
cookie, a token, a signature somebody sent — so being malformed is a condition the caller was always
going to deal with rather than a fault. And what was encoded is as likely to be a digest as a
sentence, so answering text would be guessing; fromBytes is one call and answers a result of its
own, so the two compose.

Three things are refused, each with its own sentence: a character outside the alphabet, =
included; a length one past a multiple of four, which carries no whole byte in its last character
and so is truncated rather than merely odd; and bits past the last whole byte that are not zero,
or two spellings would decode to the same bytes — which is how a signature check is walked past in a
format that compares its tokens as text.

slate:jwt had the only encoder there was and it was private. Three places in
sluice wanted one and wrote hex instead, paying a third
more bytes on a cookie that is already percent-encoded. That module now imports the shared pair; the
algorithm is the one it carried.

slate:url still asks nothing of the host, so this works under the interpreter, under node and in a
browser with no branch anywhere.

A streamed source is told when its reader has gone

A source may have a close, and the server calls it where a streamed response ends with the source
unexhausted
— the client hung up, the socket was closed under the response, the peer reset the
stream, or the source itself faulted. A source that ran to done is told nothing, having finished.

subscribe(topic)
    async pull()
        { done: false, value: await take(topic) }

    shut()
        forget(topic)

    { next: pull, close: shut }

app.get("/events", req -> sse(subscribe("orders")))

It is optional and is asked for exactly as next is, so every source already written keeps
working: a generator has no close, and neither has an object that does not name one.

Until now a writer that stopped pulling said nothing at all, and a source is usually a
subscription to something — a topic, a query, the tail of a file. sluice's event hub is the case that
named it: close() was the handler's to call from a place that could not tell the client had gone,
so a subscriber nobody closed stayed on its topic for the life of the program, one per browser tab
ever opened. Its subscribe already answers { next, close, dropped }, so an event stream behind it
stops leaking with nothing written there at all.

Both writers do it, and HTTP/2 has two ways out that HTTP/1.1 has not — a session that went away
under the response, and a peer that reset this one stream while the connection carries on. sse
forwards the message to the source it was given, without which the protocol would stop at the
wrapper. It is called once, whichever way the response ended, so a source counting its own
readers cannot go wrong.

Also in this release

docs/library/url.md documents the pair and docs/library/http.md the source protocol, both run by
the suite as usual. tests/js/p24.sl joins the differential corpus, so base64url is pinned against
RFC 4648's published vectors on both back ends rather than against a round trip that could be wrong
twice.

slate 0.0.28 — the client-side gap

Choose a tag to compare

@edadma edadma released this 05 Sep 09:14

The client-side gap. slate could build a page and could not read one; it can now, which is what
makes hydration possible — and a link behave like a link.

Behaviour changes

A call the program wrote is strict on both back ends. The JavaScript back end checked no arity at
all: f(1, 2) to a one-parameter f dropped the 2 and ran. It is a fault now, as it always was
under the interpreter. A function with no parameters carried no signature at all, which is why a
component given props was the silent case.

A native calls your function with as many arguments as it declares. on(node, "click", () -> …)
is what a person writes and now what runs; declaring more than the native has is a fault naming the
native. The checker follows the machine through a seam, fits_callback, so a program-declared
(integer, integer) -> integer still refuses n -> n.

A browser's print is the print dialog, and slate was reaching for it. A page's every
print("hi") opened a printer chooser and wrote nothing. It writes to the console now. A document is
what tells a browser from quickjs, whose print is a writer.

Connecting to a literal address no longer touches the resolver. connect("127.0.0.1", p) asked
getaddrinfo what 127.0.0.1 means — a call of unbounded latency in front of a connection whose
destination was never in doubt. Anything inet_pton refuses still goes the long way, so 127.1 and
2130706433 are unchanged.

slate:dom can read a page

children(node), tagName(node), nodeText(node) and attribute(node, name). Every one of the
module's thirteen document names either made a node or changed one, so a framework adopting markup a
server rendered had nothing to walk it with.

A handle for an element the program never created is not a new kind of value — byId and query
have minted one since the module shipped — so this is the read side of an arrangement that already
existed. children answers every child node and not only the elements, because a text node between
two elements is a position; tagName answers null for one, which is how the two are told apart;
attribute answers true for a bare attribute, which is the reading setAttribute writes.

parent, the siblings and an innerHTML reader are deliberately absent: a page is walked downwards
from something the program already holds.

lath 0.3.0 is what this is for: hydrate(el, host)
adopts a server's markup instead of rebuilding it, and makes no DOM mutations at all — measured
with a MutationObserver rather than asserted.

A click says which button and which modifiers

mods{ meta, ctrl, shift, alt } — and button join type, value, checked, key, stop
and prevent on the record a handler is given.

A link cannot be written without them. A framework intercepting a click has to let a cmd-click, a
ctrl-click, a shift-click and a middle click through to the browser, and until now it could not tell
any of them from a plain click — so a router would have swallowed the most ordinary thing anybody
does to a link.

slate:url

percentDecode, encodeComponent, parseQuery and parsePairs, in a module of their own.

They were slate:http's, exported from it with a comment saying a framework over that module needs
the same decoder its router uses. That was true and it was not enough: the JavaScript back end
emits the whole of an imported module
, so a browser page importing slate:http to reach two of
them went from 340,761 bytes to 579,710 — a file server and an HTTP/2 speaker downloaded to read a
query string.

slate:http imports them back and exports all four under their old names, so nothing written against
it changed. Import them from slate:url in anything that is not a server.

Also in this release

The OWED list is checked as three sets, which found five natives that were in neither the runtime
nor the list. A diagnostic about a function names it. slate:gzip, fetch, the ws client and the
calendar are all whole under slate js — see the parity
page
.

slate 0.0.27 — browser parity

Choose a tag to compare

@edadma edadma released this 05 Sep 04:52

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, over Intl. 2985 of 2985 offset readings agree across 2000–2025.
  • slate:crypto, written out rather than built on WebCrypto — crypto.subtle is promise-only,
    and the signatures here are not. slate:jwt works under slate js as a consequence.
  • slate:regex, by translating the pattern into a RegExp rather than handing it over. Three
    constructs both engines compile and quietly match differently (\s, ., and ^/$ under m)
    are rewritten; \p{...} is translated rather than refused.
  • slate:ws's client, over the host's own WebSocket. The server half cannot exist in a
    browser, nothing there being able to listen.
  • slate:gzip, over CompressionStream — 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.

slate 0.0.26

Choose a tag to compare

@edadma edadma released this 04 Sep 22:56

What changes behaviour

Read these first: each one refuses or reshapes something 0.0.25 accepted.

hash, hashStrong and check in slate:password answer promises. Hashing a password is
supposed to take a tenth of a second and a loop is one thread, so on the loop that was a tenth of a
second in which the server answered nobody — ten simultaneous logins were ten seconds of a dead
process. The derivation now runs on libuv's thread pool. await hash(p) and await check(r, p) is
what a login path writes.

  • The promises carry the value, not a result. await hash(p) is the record and await check(r, p)
    is a boolean, where a file read answers { ok, value }. A value the program built cannot fail to be
    read back, and the failures it does have are faults raised where the call is written.
  • needsRehash is unchanged and synchronous. It reads the parameters out of a record and compares
    them — microseconds, no derivation — so a promise there would be ceremony.
  • A malformed record is still a fault, raised before anything is queued. A corrupted row has not
    become a rejected promise.

Every argument must fit the type its type parameter was solved to. pair[T](a: T, b: T) given
pair(1, "x") is refused, naming the parameter, what it was taken to be, and the argument that
disagrees. This reverses 0.0.25's "a conflict widens into a union": widening made such a call mean
something the reader almost never wanted. A union is still an answer where the program declared one.

An object literal written where a shape is expected may carry only the fields that shape names.
use({ colour: "red" }) at a parameter wanting { color: string } is refused, and the message names
the near spelling. Only a literal at the spot — a value reaching the same parameter through a name is
structural as before, a literal built by a spread is a merge rather than a literal, and a literal
nested inside one is untouched. It is TypeScript's excess-property check, drawn in the same place: a
literal built for this call serves nobody else, so an extra field in it is intentional and wrong.

close(server) ends the connections the server accepted, not only the listening socket. A program
that had closed everything it owned could still not exit. close now stops the server accepting,
closes every idle connection at once, and lets one with a request in flight finish that response.
There is no keep-alive after close. The HTTP/2 speaker gained the idle clock the HTTP/1.1 speaker
has always had.

upper and lower are ASCII again. sysl 0.0.101 quietly made sysl.text.to_upper walk the
Unicode database, so upper("héllo") became HÉLLO under the interpreter while slate js still
answered HéLLO. The two back ends have to agree, and ASCII is what the JavaScript one has always
documented itself as doing.

What is faster

A local and a parameter are numbered slots on the operand stack, rather than names in a scope
object built per call. A chunk that needs no scope at all builds none; a chunk that needs one for a
single captured name still puts the rest of its locals in cells; and a block pushes a scope only
where it declares a name into one.

Measured against the globals.sl control, on alternating builds of the same programs: calls.sl
442 → 279 ms, closures.sl about 47% off, nested.sl about 10%. arith.sl does not move, its loop
having no parameters in it.

Also

  • A val shadowing a parameter is legal slate and was a JavaScript syntax error.
  • Four checker fixes found by writing real programs against it.

Installing

brew update
brew upgrade slate
which slate
brew test slate

slate 0.0.25

Choose a tag to compare

@edadma edadma released this 04 Sep 02:22

slate:time is half there on the JavaScript back end. An instant and a duration are a
microsecond BigInt with a millisecond clock, on both hosts, and no Date is built at all beyond
Date.now() — the civil breakdown is the interpreter's own algorithm rather than the host's
calendar, so the two agree byte for byte. The zones and the calendar are still owed and say so in a
sentence rather than being names that are not there.

A streamed body over HTTP/2, in both directions. sse, a response that arrives in pieces, and
serveStream all work over h2 exactly as they do over 1.1: a source is handed over frame by frame
as it lands, with the heartbeat a DATA frame of its own. It is sh.sysl.nghttp2 0.3.0 underneath —
the data provider, NGHTTP2_ERR_DEFERRED and nghttp2_session_resume_data — and the two caveats
docs/library/http.md carried are gone. Every write on an h2 connection is awaited now, its failure
read as the client having gone.

Three defects fixed on the way, each found by writing the feature: at on an instant reached the
array builtin of the same name; sleep refused a duration; and a method a kind does not have said
two different sentences on the two back ends.

And the nine source files that were over the thousand-line rule are split — one pure-refactor
commit each, every seam counted with the comments stripped in both directions. Nothing is over a
thousand lines and the largest result is 835.

1772 tests green. lath 37, pg 86, sluice 81 and logger 12 pass against this binary, the last
two under slate test --js as well, which they did not before.