Repository navigation
v0.2.0
Editor work, a manifest change, and -- from five rounds of strangers building
real programs against the published documentation -- four runtime failures and
an installer that could not be run as root.
The one to read first is the denial of service in std::json::parse: a service
built the way cookbook/json-api describes could be stopped by a single
request carrying a long string.
Breaking
-
std::fs::fold_linesraisesIoErroron bytes that are not UTF-8, where
it used to trap. The old behaviour wasSIGABRTand exit 134, which no
program could catch;read_texton the same file already raised. A tool that
means "this file is not text" can now say so and choose its own exit status.Listed as breaking because a program written around the trap — a wrapper
script watching for 134, or anything relying on the process dying — sees a
catchable failure and exit 1 instead. -
[toolchain] versionis now required, andeditionis gone. Both halves
of one change: two fields were answering the question which Khora builds
this? and only one of them could.edition = "2026"named a year rather than a compiler. Nothing read it — it
was inert by design, waiting for an editions mechanism that does not exist —
so an unknown value had nothing to go wrong with, which is why it took until
0.1.0for anybody to notice thatedition = "1999"built without a word.
It is removed. A manifest that still carries the line gets a warning naming
what replaced it, and builds.[toolchain] versionnames a compiler that exists and selects the binary that
runs: the build, the tests,khora fmtand the editor's language server all
follow it. Requiring it is what makes "this project builds the same way on
your machine" true by default rather than by convention. A project without one
now stops, with the two lines to add in the message — the table name, the key
and the quoting are not things anybody should have to guess at.Two consequences worth knowing:
- In a workspace the pin belongs at the root. The search walks up from
wherever you are to the nearest manifest that has one, and it no longer
stops at the first manifest it finds — which it did, so a member with no
[toolchain]reported that the project pinned nothing while the root above
it pinned something. - The machine default now only governs directories that are not projects.
Inside a project the pin answers, because there is always a pin.
khora newwrites the pin, and does not write one into a member of a
workspace that already has it. Run it against an existing project and the
message tells you what to add. - In a workspace the pin belongs at the root. The search walks up from
-
A pin may be
latestorlatest.rc. The newest toolchain installed on
this machine, release candidates excluded or included.They are deliberately not reproducible, which is the opposite of what a
pin is for, and they exist anyway: the project that most wants to build
against whatever compiler was installed this morning is the one developing the
compiler. Write a version for anything you want built the same way twice.Both resolve against installed toolchains and never over the network — a
channel that asked GitHub for the newest release would put an HTTP request in
front of everykhorainvocation, including the ones an editor makes on every
keystroke.This needed the version ordering fixed first.
VersionderivedOrd, which
comparespre: Option<String>withNonebelowSome— so0.2.0sorted
below0.2.0-rc.1, and "the newest stable release" would have chosen a
release candidate. It now implements the precedence rules from the
specification, including thatrc.2 < rc.10. -
Two rules moved from the code generator into the type checker, so
khora checknow refuses programs it used to accept. Raising a type no
typedeclaration names —raises String— and an integer literal wider
than anInt. Neither has ever built: the code generator refused both, and
the only thing that changed is when you are told.It is listed here rather than under Fixed because of one workflow: a project
whose CI runskhora checkand notkhora buildwill see a failure it did
not see before. That project was already broken; it just had no way to know.
It is also why this is0.2.0and not0.1.1: before 1.0 the minor is
where a breaking change goes, and a new refusal is breaking to somebody even
when every program it refuses was already wrong.The reason it matters beyond the two rules is that
khora-lsppublishes what
the parser, the checker and the lints say and nothing from lowering — so a
rule enforced during code generation is one no editor can show, at any
point, on any platform.scripts/check-backend-rules.shis a new gate that
makes every one of the 52 remaining backend refusals a decision somebody has
recorded, so a third does not arrive unnoticed.
Fixed
-
std::json::parsecould be killed by one long string, which made it a
remote denial of service. The scanner inside a string literal recursed once
per ordinary character, so the depth of the recursion was the length of the
string: 50 KB was fatal on the main thread and about 11 KB inside a request
fiber, where the stack is smaller. No malformed input was needed, only a long
one — andcookbook/json-apitells the reader to set
Router::holding(1048576), so any service accepting a name, a description or
a base64 field could be stopped by a single unauthenticated request. Inside a
fiber there was no message at all, just a dead process.It is a loop now, and 2 MB parses. Escapes still recurse, once per escape,
which is bounded by how many the document has rather than by its length. -
The stack-exhaustion note said the cause was most likely your own code.
It namedstdas walking "lists and strings with loops", which was true of
everything exceptjson::parse— the one function instdthat could
actually do this. A reader whose service died parsing a request body was sent
to audit their handlers. The note now names both possibilities and neither as
the likely one, because without a backtrace it cannot know which. -
A listener that could not bind said nothing and the process did not stop.
Two halves. The runtime'sa fiber ended with an error nobody was waiting fornow prints when the fiber finishes, not when its handle is released —
a server holds its handle and polls a stop flag, so the one shape every
service is written in was the one the message could not reach. And
Fiber::finishedis new, because nothing could ask whether a spawned fiber
was still running:join,waitand letting the handle go all block. A
supervisor loop can now notice its listener died and exit instead of turning
for ever. -
A service could not be restarted on the same port for about a minute.
listen_onset noSO_REUSEADDR, so a port left inTIME_WAITby the
program's own previous run was refused. First start worked, every restart
inside the window did not, which is the edit-stop-start loop. Fixed on Linux
and macOS — with the BSD constants there, which are not Linux's — and
deliberately not on Windows, where the same option permits binding over a
live socket and the rebind being worked around is not refused anyway. -
khora checkandkhora testpassed wherekhora buildfailed on the
same source. A package with anything undersrc/binbuilt each program
with the package's ownsrc/main.khcorrectly excluded, and its test modules
left in — so asrc/main_test.khimported a module that was no longer in the
compilation. A test module is now excluded too, recognised by containing a
testblock rather than by its file name. -
install.shandkhora toolchain installfailed when run as root, which
is how a container, aDockerfileRUNline and most CI steps install
anything.tarrestores an archive's stored ownership when it believes it is
root, the release archives carry the build machine's uid, and every entry
failed withCannot change ownership to uid 1001. At uid 1000 tar ignores
stored ownership, which is why installing by hand always worked. Both now
pass--no-same-owner. -
An unreadable token is reported where it is.
0xFF,0b1010and1 @ 2
all used to come back asthis `{` is never closed, naming a brace on the
line above — an underline your editor drew on code that was correct. Numeric
bases Khora does not have are now named as such, and say what it does have:
decimal digits, with_free to separate them. -
An unreachable
matcharm names its cause.Red => 1where
Colour::Redwas meant reported the next arm — the one written correctly —
as unreachable. A bare name in a pattern is a binding, so it had matched
every colour. The message now says so and gives the line to write. -
A declaration keyword from another language is redirected.
struct,
class,enum,interface,func,var,namespaceandasyncname the
Khora spelling instead of expected a declaration.enumshows the variant
syntax, because being told only "writetype" leaves you looking for a
separate word for cases;asyncis told the distinction does not exist here
rather than that it is a rename.
Changed
-
A span knows what it is inside, and
std::logcorrelates by itself.
std::trace::currentis the span the running fiber is in:aroundinstalls
one for the duration of a body and restores the enclosing one on every path
out, a handler'sstarttakes the trace id, the sampling decision and the
parent from it, and a fiber inherits its spawner's span when it is created,
so work that fans out stays one trace. Until nowSpan::parentwas0
everywhere and a nestedaroundbegan a second trace.This changes what a
Log::jsonline looks like. A line logged inside a
span now carries"trace_id"and"span_id"after"message"; a line
logged outside every span is unchanged, because the fields are absent rather
than zero. Anything parsing those lines by position rather than by key will
need to look again; anything indexing them by key gains two fields it
probably already expects.A handler written outside
stdkeeps working and keeps its old behaviour —
starting a fresh trace per span — until itsstartcallscurrent.
packages/otlpdoes now.
Added
-
Fiber::finished, which answers whether a fiber has finished without
waiting for it to. The question a supervisor loop needs and the only one
every other entry point answered by blocking. -
std::trace::current,Context::trace_idandContext::span_id. The
last two render the ids as OpenTelemetry writes them, thirty-two and sixteen
lower-case hex digits, which is the form a collector's search box takes.
Documentation
-
Cancelling a fiber whose body is a nursery never returns, and that is now
on the limitations page with a reproduction and the workaround. The runtime
has described it in a comment since before 0.1.0 — a nursery checks for its
own cancellation between rounds of waiting, so one already blocked on
children that never finish never checks again — but the page listing nursery
surprises did not mention the only one that hangs.Ctrl-Chandling built on
Fiber::canceldeadlocks; cancel through a flag the work itself reads. -
The installation page names what the install script needs.
curlor
wget,tar, and CA certificates: a slim container image has none of them,
so the first command on the page could not run there and the page said
nothing about it. -
khoralang.com/docs/servesv0.1, the documentation for the released
compiler, withnextbeside it for the one being written./docs/used to
redirect tonext, which afterv0.1.0published meant the front door sent
every reader to pages marked as describing a compiler they could not install.
A released tree is cut from its tag and records which one; a section is per
release allowed to break, so the major from 1.0 and the minor before it. -
Cutting a release now cuts its documentation tree.
release.yml
mentioned the documentation nowhere, so the versioned tree was twogit
commands a maintainer was trusted to remember, described in a comment.
scripts/cut-docs.shdoes it from the tag and the workflow opens a pull
request rather than pushing.
Editor
-
Sixty-two new assists, bringing the total offered under the cursor from
two to sixty-four. They are in ten groups: control flow, bindings,
capabilities and failures, matching, patterns, imports, declarations, types,
calls and pipelines, statements, literals and documentation.The ones worth finding first:
- Extract into a function, on an expression or a block. The
withand
raisesclauses are written from what the calls inside actually demanded —
the checker records that at every call site, so the signature is correct by
construction rather than inferred a second time by an editor. - Write out the cases a
_arm covers. A wildcard is how amatchstops
being exhaustive without stopping compiling: add a variant, and every
matchthat names its cases fails loudly while everymatchending in_
sends the new one down the default path in silence. - Lift a lambda into a function, with the parameter and answer types the
checker gave it. catchandattemptin both directions, awithin both spellings, and a
handler written from an effect declaration.
Every editor gets all of them. There is one implementation and it is
khora lsp;editors/is configuration. - Extract into a function, on an expression or a block. The