Skip to content

v0.2.0

Choose a tag to compare

@codyspate codyspate released this 12 Sep 01:43
· 189 commits to main since this release

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_lines raises IoError on bytes that are not UTF-8, where
    it used to trap.
    The old behaviour was SIGABRT and exit 134, which no
    program could catch; read_text on 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] version is now required, and edition is 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.0 for anybody to notice that edition = "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] version names a compiler that exists and selects the binary that
    runs: the build, the tests, khora fmt and 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 new writes 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.

  • A pin may be latest or latest.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 every khora invocation, including the ones an editor makes on every
    keystroke.

    This needed the version ordering fixed first. Version derived Ord, which
    compares pre: Option<String> with None below Some — so 0.2.0 sorted
    below 0.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 that rc.2 < rc.10.

  • Two rules moved from the code generator into the type checker, so
    khora check now refuses programs it used to accept.
    Raising a type no
    type declaration names — raises String — and an integer literal wider
    than an Int. 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 runs khora check and not khora build will 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 is 0.2.0 and not 0.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-lsp publishes 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.sh is 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::parse could 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 — and cookbook/json-api tells 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 named std as walking "lists and strings with loops", which was true of
    everything except json::parse — the one function in std that 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's a fiber ended with an error nobody was waiting for now 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::finished is new, because nothing could ask whether a spawned fiber
    was still running: join, wait and 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_on set no SO_REUSEADDR, so a port left in TIME_WAIT by 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 check and khora test passed where khora build failed on the
    same source.
    A package with anything under src/bin built each program
    with the package's own src/main.kh correctly excluded, and its test modules
    left in — so a src/main_test.kh imported a module that was no longer in the
    compilation. A test module is now excluded too, recognised by containing a
    test block rather than by its file name.

  • install.sh and khora toolchain install failed when run as root, which
    is how a container, a Dockerfile RUN line and most CI steps install
    anything. tar restores an archive's stored ownership when it believes it is
    root, the release archives carry the build machine's uid, and every entry
    failed with Cannot 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, 0b1010 and 1 @ 2
    all used to come back as this `{` 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 match arm names its cause. Red => 1 where
    Colour::Red was 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, namespace and async name the
    Khora spelling instead of expected a declaration. enum shows the variant
    syntax, because being told only "write type" leaves you looking for a
    separate word for cases; async is told the distinction does not exist here
    rather than that it is a rename.

Changed

  • A span knows what it is inside, and std::log correlates by itself.
    std::trace::current is the span the running fiber is in: around installs
    one for the duration of a body and restores the enclosing one on every path
    out, a handler's start takes 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 now Span::parent was 0
    everywhere and a nested around began a second trace.

    This changes what a Log::json line 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 std keeps working and keeps its old behaviour —
    starting a fresh trace per span — until its start calls current.
    packages/otlp does 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_id and Context::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-C handling built on
    Fiber::cancel deadlocks; cancel through a flag the work itself reads.

  • The installation page names what the install script needs. curl or
    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/ serves v0.1, the documentation for the released
    compiler
    , with next beside it for the one being written. /docs/ used to
    redirect to next, which after v0.1.0 published 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 two git
    commands a maintainer was trusted to remember, described in a comment.
    scripts/cut-docs.sh does 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 with and
      raises clauses 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 a match stops
      being exhaustive without stopping compiling: add a variant, and every
      match that names its cases fails loudly while every match ending 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.
    • catch and attempt in both directions, a with in 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.