Skip to content

Releases: codyspate/khoralang

Khora 0.4.0

Choose a tag to compare

@codyspate codyspate released this 09 Oct 20:45

Coming from 0.3.0, read three groups of entries first.

What stops code compiling. A record field is private to its module
unless it is marked pub. Glob imports and a second with or raises
clause are removed. A bare case name in a pattern, a constructor pattern of
the wrong type and two instantiations of one error type in a row are
refused. A Region, a Scope and a Db stay on the fiber that opened
them, and a spawned fiber's error must be Share. Fiber::cancelled is
spelled Fiber::canceled. A handler for Db needs query_each, depth,
savepoint, release and rollback_to. A khora.toml that does not load
stops check, build and the other project commands, and three shapes
0.3.0 warned about are refused.

Wrong answers given with nothing to say so, fixed, so code that ran on
0.3.0 may behave differently: a postgres query could return another
caller's rows; std::db::transaction could lose writes it had answered
Ok; a transaction inside another one committed or rolled back the outer
one; a postgres transaction whose body ignored a failed statement was
answered Ok with nothing committed; the type checker let through a pattern
of the wrong type, a catch that yielded 0 and a raise that no catch
saw; a case name written bare in a pattern matched every value; a catch
arm could read a generic error's field at the wrong type; and a lambda
handed to scoped inside a function with a scope of its own released what
it acquired at the outer scope's end.

Crashes a server operator meets, each of them in 0.3.0: every Router
server died after about 130,000 connections; one client that reset its
connection killed the whole server with SIGPIPE; the otlp exporter
crashed every traced service after about 20,000 spans; a 31 KB request body
of nested brackets, or other long input, ran the stack out and ended the
server; and a fiber on the scheduler backend had an eighth of main's
stack.

Cancellation has its own channel. A canceled fiber stops at its next
cancellation point in every function, whatever the function's raises row,
which changes what SIGTERM, Fiber::join, Fiber::outcome and catch do
in code with no row. On the TechEmpower tests Khora serves more requests a
second than Go on JSON, as many on Fortunes, and fewer on the two database
tests (see Changed).

Breaking

  • A postgres query could return another caller's rows (see Fixed). A
    program that ran with a receive deadline, or on a network that dropped
    reads, may have acted on answers that belonged to other queries. The
    connection that caused it is closed instead of reused, and its pool slot
    reconnects it (see the pool entry below).

  • std::db::transaction could lose a write it had acknowledged with Ok
    (see Fixed). A transaction whose fiber is canceled before its body
    finishes is rolled back, including one canceled while COMMIT waited for
    its reply, which changes nothing if the commit ran. A Db handler has a
    rule to follow for it; see the Db handler entry below.

  • A transaction inside a transaction committed or rolled back the
    enclosing one
    (see Fixed). An inner transaction is a savepoint: its
    writes are committed with the enclosing transaction's or not at all, and
    its failure undoes only its own writes.

  • A postgres transaction whose body ignored a failed statement was
    answered Ok
    (see Fixed). It is answered DbError::RolledBack.

  • A bare name in a pattern that is the name of one of its value's cases is
    refused
    , in match, catch, let and for, at any depth, and for
    cases with a payload as well as without. Red => .. over a Color binds
    every color; the message names the pattern to write, Color::Red
    (FsError::NotFound(_) for a payload case, and the type's module where
    the file never imported it). A bare name that is a type's only case, as
    in catch { Stop => () } over a record Stop, matched what the case
    would have and is refused as well: write _. The check is against the
    value's type, so a case added to a type later makes a binding of the same
    name an error rather than a match. A capitalized bare name that is no case
    of the value's type is refused as well: a name in a pattern that starts with
    a capital letter must be a case. The message reads like an undefined
    name and offers the nearest case, "Color has no case Gren. Did you
    mean Color::Green?", when one is within two edits (or a third of the
    name, if that is more); the language server offers it as a quick fix. A
    const written as a pattern (FAVORITE => .., which binds rather than
    compares) is told to use a guard, n if n == FAVORITE. Bind with a
    lower-case name. 0.3.0 built all of these with an unused-binding
    warning at most, and gave a wrong answer (see Fixed).

  • A constructor pattern of one type, matched against a value of another,
    is refused
    , at any depth, in match, catch, let and a pattern inside
    a tuple or record pattern: "this pattern is a Result case, and the value
    here is a Big". This includes a pattern over a type parameter ("... the
    value here is a T"). Where both types were held on the heap, 0.3.0 built
    the program and read the field out of the wrong constructor (see Fixed).

  • (raise X) catch { X => 7 } is an Int, the type its arms produce.
    0.3.0 typed it as the operand's, which never finishes, so it fitted
    wherever a value was wanted and yielded 0 there (see Fixed). A use that
    fitted only because of that is refused:
    let s: String = (raise X) catch { X => 7 }.

  • A raise of a value whose type is worked out later is charged to the
    catches and the row around it
    , as a raise of a value of known type
    is. On 0.3.0 such an error went past every catch (see Fixed). Programs
    this refuses:

    • the error escapes a closure's catch and the closure is called without
      !: "k can leave this function, so the call needs !";
    • the value turns out to be a second instantiation of an error type the
      catch beside it names: "a catch arm handles one instantiation of a
      type, and this operand raises two; catch them with _, or in two
      catches". A program of this shape whose other branch was the one that
      ran happened to give the right answer, and is refused all the same;
    • the value's type is never worked out at all: "the type of this raised
      value was never worked out";
    • the value turns out to be an error type that the closure's row, already
      fixed by where the closure is passed (attempt, retry, a parameter
      written () -> Int raises Nf), does not carry: "this raise sends Dn,
      and the closure it is in had its error row closed to { Nf: Nf }, which
      does not carry it", naming where the row was fixed and suggesting an
      annotation. Another closure calling this one beside a Dn of its own
      does not count: the row has to carry it.

    0.3.0 built these and crashed, or, where the raising branch never ran or
    the error happened to reach a catch that named it, gave the right answer.

  • An error type appears in a raises row at one instantiation. An arm
    that reads a field of a generic error at a type other than the one raised
    is refused; on 0.3.0 it read the wrong type (see Fixed). So is anything
    that would put two instantiations of one error type in one row, wherever
    they meet:

    • one catch over (if b { fi()! } else { fs()! }), with fi raising
      Gx<Int> and fs raising Gx<String>, when it has a Gx::.. arm or an
      arm that binds the failure (catch { e => .. }), including a _ beside
      a Gx::.. arm;
    • a closure whose body raises both, however it is then used: called,
      passed to attempt or a row-polymorphic function, or given to
      Fiber::spawn;
    • raises E + F instantiated with E and F two instantiations of one
      type;
    • two different error types that share a name, declared in two modules
      (an m::a::E and an m::b::E), wherever the same routes put them in
      one row. An error row is labeled by the type's bare name, so the two
      collide as two instantiations do; a named arm there read one type's
      field as the other's on 0.3.0. The message says they are two types of
      one name.

    One arm reads one layout, and the program cannot say which instantiation
    arrived. Catch each where it is raised, in a catch around each call, or
    with a _ arm alone.

  • A lambda handed to scoped or nursery inside a function with a
    scope or nursery of its own used the enclosing one
    (silent wrong
    answer). In fn f() with { scope: Scope }, scoped(fn () => work()) gave
    work the scope of f, not the one scoped opens, so what work
    acquired was released when f's caller's scope ended rather than when
    that scoped did. The capability scoped or nursery hands its lambda
    is the lambda's own, and a binding of that label outside the lambda does
    not shadow it. A capability the lambda writes by name, scope.defer(..),
    is the binding of that name in scope where the lambda is written, as any
    name is.

  • On the scheduler backend, a finalizer could be skipped (see Fixed). A
    program on KHORA_FIBERS=scheduler whose cleanup was silently skipped
    under load runs it.

  • A record field is private to the module that declares its type, unless
    it is marked pub.
    Outside that module a private field cannot be read,
    assigned, named in a literal or an update, or bound in a pattern, and a
    value with any private field can only be built by its own module. A
    wrapper's value follows the same rule: type Port = Int; can be built with
    Port(n) and opened with match p { Port(n) => .. } only in its module,
    and type UserId = pub Int; opens one to everybody. A case's payload is
    always public. derive(Decode) is refused on a pub type with a private
    field, because it would build one from input wit...

Read more

Khora 0.4.0-rc.2

Khora 0.4.0-rc.2 Pre-release
Pre-release

Choose a tag to compare

@codyspate codyspate released this 09 Oct 15:45

Coming from 0.3.0, read three groups of entries first.

What stops code compiling. A record field is private to its module
unless it is marked pub. Glob imports and a second with or raises
clause are removed. A bare case name in a pattern, a constructor pattern of
the wrong type and two instantiations of one error type in a row are
refused. A Region, a Scope and a Db stay on the fiber that opened
them, and a spawned fiber's error must be Share. Fiber::cancelled is
spelled Fiber::canceled. A handler for Db needs query_each, depth,
savepoint, release and rollback_to. A khora.toml that does not load
stops check, build and the other project commands, and three shapes
0.3.0 warned about are refused.

Wrong answers given with nothing to say so, fixed, so code that ran on
0.3.0 may behave differently: a postgres query could return another
caller's rows; std::db::transaction could lose writes it had answered
Ok; a transaction inside another one committed or rolled back the outer
one; a postgres transaction whose body ignored a failed statement was
answered Ok with nothing committed; the type checker let through a pattern
of the wrong type, a catch that yielded 0 and a raise that no catch
saw; a case name written bare in a pattern matched every value; a catch
arm could read a generic error's field at the wrong type; and a lambda
handed to scoped inside a function with a scope of its own released what
it acquired at the outer scope's end.

Crashes a server operator meets, each of them in 0.3.0: every Router
server died after about 130,000 connections; one client that reset its
connection killed the whole server with SIGPIPE; the otlp exporter
crashed every traced service after about 20,000 spans; a 31 KB request body
of nested brackets, or other long input, ran the stack out and ended the
server; and a fiber on the scheduler backend had an eighth of main's
stack.

Cancellation has its own channel. A canceled fiber stops at its next
cancellation point in every function, whatever the function's raises row,
which changes what SIGTERM, Fiber::join, Fiber::outcome and catch do
in code with no row. On the TechEmpower tests Khora serves more requests a
second than Go on JSON, as many on Fortunes, and fewer on the two database
tests (see Changed).

Breaking

  • A postgres query could return another caller's rows (see Fixed). A
    program that ran with a receive deadline, or on a network that dropped
    reads, may have acted on answers that belonged to other queries. The
    connection that caused it is closed instead of reused, and its pool slot
    reconnects it (see the pool entry below).

  • std::db::transaction could lose a write it had acknowledged with Ok
    (see Fixed). A transaction whose fiber is canceled before its body
    finishes is rolled back, including one canceled while COMMIT waited for
    its reply, which changes nothing if the commit ran. A Db handler has a
    rule to follow for it; see the Db handler entry below.

  • A transaction inside a transaction committed or rolled back the
    enclosing one
    (see Fixed). An inner transaction is a savepoint: its
    writes are committed with the enclosing transaction's or not at all, and
    its failure undoes only its own writes.

  • A postgres transaction whose body ignored a failed statement was
    answered Ok
    (see Fixed). It is answered DbError::RolledBack.

  • A bare name in a pattern that is the name of one of its value's cases is
    refused
    , in match, catch, let and for, at any depth, and for
    cases with a payload as well as without. Red => .. over a Color binds
    every color; the message names the pattern to write, Color::Red
    (FsError::NotFound(_) for a payload case, and the type's module where
    the file never imported it). A bare name that is a type's only case, as
    in catch { Stop => () } over a record Stop, matched what the case
    would have and is refused as well: write _. The check is against the
    value's type, so a case added to a type later makes a binding of the same
    name an error rather than a match. A capitalized bare name that is no case
    of the value's type is refused as well: a name in a pattern that starts with
    a capital letter must be a case. The message reads like an undefined
    name and offers the nearest case, "Color has no case Gren. Did you
    mean Color::Green?", when one is within two edits (or a third of the
    name, if that is more); the language server offers it as a quick fix. A
    const written as a pattern (FAVORITE => .., which binds rather than
    compares) is told to use a guard, n if n == FAVORITE. Bind with a
    lower-case name. 0.3.0 built all of these with an unused-binding
    warning at most, and gave a wrong answer (see Fixed).

  • A constructor pattern of one type, matched against a value of another,
    is refused
    , at any depth, in match, catch, let and a pattern inside
    a tuple or record pattern: "this pattern is a Result case, and the value
    here is a Big". This includes a pattern over a type parameter ("... the
    value here is a T"). Where both types were held on the heap, 0.3.0 built
    the program and read the field out of the wrong constructor (see Fixed).

  • (raise X) catch { X => 7 } is an Int, the type its arms produce.
    0.3.0 typed it as the operand's, which never finishes, so it fitted
    wherever a value was wanted and yielded 0 there (see Fixed). A use that
    fitted only because of that is refused:
    let s: String = (raise X) catch { X => 7 }.

  • A raise of a value whose type is worked out later is charged to the
    catches and the row around it
    , as a raise of a value of known type
    is. On 0.3.0 such an error went past every catch (see Fixed). Programs
    this refuses:

    • the error escapes a closure's catch and the closure is called without
      !: "k can leave this function, so the call needs !";
    • the value turns out to be a second instantiation of an error type the
      catch beside it names: "a catch arm handles one instantiation of a
      type, and this operand raises two; catch them with _, or in two
      catches". A program of this shape whose other branch was the one that
      ran happened to give the right answer, and is refused all the same;
    • the value's type is never worked out at all: "the type of this raised
      value was never worked out";
    • the value turns out to be an error type that the closure's row, already
      fixed by where the closure is passed (attempt, retry, a parameter
      written () -> Int raises Nf), does not carry: "this raise sends Dn,
      and the closure it is in had its error row closed to { Nf: Nf }, which
      does not carry it", naming where the row was fixed and suggesting an
      annotation. Another closure calling this one beside a Dn of its own
      does not count: the row has to carry it.

    0.3.0 built these and crashed, or, where the raising branch never ran or
    the error happened to reach a catch that named it, gave the right answer.

  • An error type appears in a raises row at one instantiation. An arm
    that reads a field of a generic error at a type other than the one raised
    is refused; on 0.3.0 it read the wrong type (see Fixed). So is anything
    that would put two instantiations of one error type in one row, wherever
    they meet:

    • one catch over (if b { fi()! } else { fs()! }), with fi raising
      Gx<Int> and fs raising Gx<String>, when it has a Gx::.. arm or an
      arm that binds the failure (catch { e => .. }), including a _ beside
      a Gx::.. arm;
    • a closure whose body raises both, however it is then used: called,
      passed to attempt or a row-polymorphic function, or given to
      Fiber::spawn;
    • raises E + F instantiated with E and F two instantiations of one
      type;
    • two different error types that share a name, declared in two modules
      (an m::a::E and an m::b::E), wherever the same routes put them in
      one row. An error row is labeled by the type's bare name, so the two
      collide as two instantiations do; a named arm there read one type's
      field as the other's on 0.3.0. The message says they are two types of
      one name.

    One arm reads one layout, and the program cannot say which instantiation
    arrived. Catch each where it is raised, in a catch around each call, or
    with a _ arm alone.

  • A lambda handed to scoped or nursery inside a function with a
    scope or nursery of its own used the enclosing one
    (silent wrong
    answer). In fn f() with { scope: Scope }, scoped(fn () => work()) gave
    work the scope of f, not the one scoped opens, so what work
    acquired was released when f's caller's scope ended rather than when
    that scoped did. The capability scoped or nursery hands its lambda
    is the lambda's own, and a binding of that label outside the lambda does
    not shadow it. A capability the lambda writes by name, scope.defer(..),
    is the binding of that name in scope where the lambda is written, as any
    name is.

  • On the scheduler backend, a finalizer could be skipped (see Fixed). A
    program on KHORA_FIBERS=scheduler whose cleanup was silently skipped
    under load runs it.

  • A record field is private to the module that declares its type, unless
    it is marked pub.
    Outside that module a private field cannot be read,
    assigned, named in a literal or an update, or bound in a pattern, and a
    value with any private field can only be built by its own module. A
    wrapper's value follows the same rule: type Port = Int; can be built with
    Port(n) and opened with match p { Port(n) => .. } only in its module,
    and type UserId = pub Int; opens one to everybody. A case's payload is
    always public. derive(Decode) is refused on a pub type with a private
    field, because it would build one from input wit...

Read more

Khora v0.4.0-rc.1

Khora v0.4.0-rc.1 Pre-release
Pre-release

Choose a tag to compare

@codyspate codyspate released this 04 Oct 03:39

Coming from 0.3.0, read three groups of entries first.

What stops code compiling. A record field is private to its module
unless it is marked pub. Glob imports and a second with or raises
clause are removed. A bare case name in a pattern, a constructor pattern of
the wrong type and two instantiations of one error type in a row are
refused. A Region, a Scope and a Db stay on the fiber that opened
them, and a spawned fiber's error must be Share. Fiber::cancelled is
spelled Fiber::canceled. A handler for Db needs query_each, depth,
savepoint, release and rollback_to. A khora.toml that does not load
stops check, build and the other project commands, and three shapes
0.3.0 warned about are refused.

Wrong answers given with nothing to say so, fixed, so code that ran on
0.3.0 may behave differently: a postgres query could return another
caller's rows; std::db::transaction could lose writes it had answered
Ok; a transaction inside another one committed or rolled back the outer
one; a postgres transaction whose body ignored a failed statement was
answered Ok with nothing committed; the type checker let through a pattern
of the wrong type, a catch that yielded 0 and a raise that no catch
saw; a case name written bare in a pattern matched every value; a catch
arm could read a generic error's field at the wrong type; and a lambda
handed to scoped inside a function with a scope of its own released what
it acquired at the outer scope's end.

Crashes a server operator meets, each of them in 0.3.0: every Router
server died after about 130,000 connections; one client that reset its
connection killed the whole server with SIGPIPE; the otlp exporter
crashed every traced service after about 20,000 spans; a 31 KB request body
of nested brackets, or other long input, ran the stack out and ended the
server; and a fiber on the scheduler backend had an eighth of main's
stack.

Cancellation has its own channel. A canceled fiber stops at its next
cancellation point in every function, whatever the function's raises row,
which changes what SIGTERM, Fiber::join, Fiber::outcome and catch do
in code with no row. On the TechEmpower tests Khora serves more requests a
second than Go on JSON, as many on Fortunes, and fewer on the two database
tests (see Changed).

Breaking

  • A postgres query could return another caller's rows (see Fixed). A
    program that ran with a receive deadline, or on a network that dropped
    reads, may have acted on answers that belonged to other queries. The
    connection that caused it is closed instead of reused, and its pool slot
    reconnects it (see the pool entry below).

  • std::db::transaction could lose a write it had acknowledged with Ok
    (see Fixed). A transaction whose fiber is canceled before its body
    finishes is rolled back, including one canceled while COMMIT waited for
    its reply, which changes nothing if the commit ran. A Db handler has a
    rule to follow for it; see the Db handler entry below.

  • A transaction inside a transaction committed or rolled back the
    enclosing one
    (see Fixed). An inner transaction is a savepoint: its
    writes are committed with the enclosing transaction's or not at all, and
    its failure undoes only its own writes.

  • A postgres transaction whose body ignored a failed statement was
    answered Ok
    (see Fixed). It is answered DbError::RolledBack.

  • A bare name in a pattern that is the name of one of its value's cases is
    refused
    , in match, catch, let and for, at any depth, and for
    cases with a payload as well as without. Red => .. over a Color binds
    every color; the message names the pattern to write, Color::Red
    (FsError::NotFound(_) for a payload case, and the type's module where
    the file never imported it). A bare name that is a type's only case, as
    in catch { Stop => () } over a record Stop, matched what the case
    would have and is refused as well: write _. The check is against the
    value's type, so a case added to a type later makes a binding of the same
    name an error rather than a match. A capitalized bare name that is no case
    of the value's type is refused as well: a name in a pattern that starts with
    a capital letter must be a case. The message reads like an undefined
    name and offers the nearest case, "Color has no case Gren. Did you
    mean Color::Green?", when one is within two edits (or a third of the
    name, if that is more); the language server offers it as a quick fix. A
    const written as a pattern (FAVORITE => .., which binds rather than
    compares) is told to use a guard, n if n == FAVORITE. Bind with a
    lower-case name. 0.3.0 built all of these with an unused-binding
    warning at most, and gave a wrong answer (see Fixed).

  • A constructor pattern of one type, matched against a value of another,
    is refused
    , at any depth, in match, catch, let and a pattern inside
    a tuple or record pattern: "this pattern is a Result case, and the value
    here is a Big". This includes a pattern over a type parameter ("... the
    value here is a T"). Where both types were held on the heap, 0.3.0 built
    the program and read the field out of the wrong constructor (see Fixed).

  • (raise X) catch { X => 7 } is an Int, the type its arms produce.
    0.3.0 typed it as the operand's, which never finishes, so it fitted
    wherever a value was wanted and yielded 0 there (see Fixed). A use that
    fitted only because of that is refused:
    let s: String = (raise X) catch { X => 7 }.

  • A raise of a value whose type is worked out later is charged to the
    catches and the row around it
    , as a raise of a value of known type
    is. On 0.3.0 such an error went past every catch (see Fixed). Programs
    this refuses:

    • the error escapes a closure's catch and the closure is called without
      !: "k can leave this function, so the call needs !";
    • the value turns out to be a second instantiation of an error type the
      catch beside it names: "a catch arm handles one instantiation of a
      type, and this operand raises two; catch them with _, or in two
      catches". A program of this shape whose other branch was the one that
      ran happened to give the right answer, and is refused all the same;
    • the value's type is never worked out at all: "the type of this raised
      value was never worked out";
    • the value turns out to be an error type that the closure's row, already
      fixed by where the closure is passed (attempt, retry, a parameter
      written () -> Int raises Nf), does not carry: "this raise sends Dn,
      and the closure it is in had its error row closed to { Nf: Nf }, which
      does not carry it", naming where the row was fixed and suggesting an
      annotation. Another closure calling this one beside a Dn of its own
      does not count: the row has to carry it.

    0.3.0 built these and crashed, or, where the raising branch never ran or
    the error happened to reach a catch that named it, gave the right answer.

  • An error type appears in a raises row at one instantiation. An arm
    that reads a field of a generic error at a type other than the one raised
    is refused; on 0.3.0 it read the wrong type (see Fixed). So is anything
    that would put two instantiations of one error type in one row, wherever
    they meet:

    • one catch over (if b { fi()! } else { fs()! }), with fi raising
      Gx<Int> and fs raising Gx<String>, when it has a Gx::.. arm or an
      arm that binds the failure (catch { e => .. }), including a _ beside
      a Gx::.. arm;
    • a closure whose body raises both, however it is then used: called,
      passed to attempt or a row-polymorphic function, or given to
      Fiber::spawn;
    • raises E + F instantiated with E and F two instantiations of one
      type;
    • two different error types that share a name, declared in two modules
      (an m::a::E and an m::b::E), wherever the same routes put them in
      one row. An error row is labeled by the type's bare name, so the two
      collide as two instantiations do; a named arm there read one type's
      field as the other's on 0.3.0. The message says they are two types of
      one name.

    One arm reads one layout, and the program cannot say which instantiation
    arrived. Catch each where it is raised, in a catch around each call, or
    with a _ arm alone.

  • A lambda handed to scoped or nursery inside a function with a
    scope or nursery of its own used the enclosing one
    (silent wrong
    answer). In fn f() with { scope: Scope }, scoped(fn () => work()) gave
    work the scope of f, not the one scoped opens, so what work
    acquired was released when f's caller's scope ended rather than when
    that scoped did. The capability scoped or nursery hands its lambda
    is the lambda's own, and a binding of that label outside the lambda does
    not shadow it. A capability the lambda writes by name, scope.defer(..),
    is the binding of that name in scope where the lambda is written, as any
    name is.

  • On the scheduler backend, a finalizer could be skipped (see Fixed). A
    program on KHORA_FIBERS=scheduler whose cleanup was silently skipped
    under load runs it.

  • A record field is private to the module that declares its type, unless
    it is marked pub.
    Outside that module a private field cannot be read,
    assigned, named in a literal or an update, or bound in a pattern, and a
    value with any private field can only be built by its own module. A
    wrapper's value follows the same rule: type Port = Int; can be built with
    Port(n) and opened with match p { Port(n) => .. } only in its module,
    and type UserId = pub Int; opens one to everybody. A case's payload is
    always public. derive(Decode) is refused on a pub type with a private
    field, because it would build one from input wit...

Read more

Khora for VS Code vscode-v0.3.3

Pre-release

Choose a tag to compare

@github-actions github-actions released this 26 Sep 20:20

The Khora extension for Visual Studio Code.

Upgrading from 0.3.0 or earlier

The extension's ID is khora.khora. Version 0.3.0 was published as
khora-lang.khora, which VS Code treats as a different extension, so remove
it first or both will run:

code --uninstall-extension khora-lang.khora

Changes in 0.3.3

  • A new extension page. The page shown in VS Code and on the
    Marketplace describes what the extension does and how to set it up: every
    feature, the settings, the commands and troubleshooting. Notes for people
    working on the extension itself are in DEVELOPING.md in the repository.
    No change to what the extension does.

Changes in 0.3.2

  • The language server starts. 0.3.0 and 0.3.1 launched khora lsp --stdio, which
    every released toolchain refuses with unexpected argument '--stdio', so
    the server exited before it answered and nothing but syntax colouring
    worked. It launches khora lsp.

Changes in 0.3.1

  • The publisher is khora, the name the project owns on the Marketplace.
  • Install Khora, offered when no khora is found, opens the installation
    page on khoralang.com. In 0.3.0 it opened a domain that does not resolve.
  • The status bar shows which toolchain answered, and turns yellow when a
    project pins a version that is not installed.
  • Highlighting for character literals ('a'), row declarations, /// doc
    comments, string interpolation, and the ?, .. and comparison operators.
  • Requires VS Code 1.82 or later.

Install

Download khora-vscode-<version>.vsix below, then:

code --install-extension khora-vscode-<version>.vsix

or in VS Code: Extensions, the ... menu, Install from VSIX.

Then fully quit and reopen VS Code — extensions are scanned at startup, so
reloading the window is not enough. code --list-extensions should list
khora.khora.

It needs the toolchain

The extension is a client. It starts khora lsp, a subcommand of the compiler,
and shows what the compiler says — so it needs khora on your PATH. Install
it from the toolchain releases,
or point khora.server.path at an executable, which is what to do when working
on the compiler itself.

The two are released separately and on purpose. This extension has no compiler
in it, versions on its own, and changes for its own reasons; a fix to hover
rendering should not wait for a language release or drag one along.

What you get

Everything but the syntax highlighting is answered by the compiler, over LSP,
from the same queries khora check runs. There is no second implementation to
drift.

  • Errors and warnings as you type, including lints
  • Hover types, go to definition, find references, rename
  • Completion: methods after ., a type's own items after ::, a module's
    exports inside an import list
  • Semantic highlighting the resolver decides — a local told from an import,
    which no regex can do
  • Inlay hints showing what a call costs — the with { .. } capabilities it
    requires and the errors it raises, which is the thing worth reading in a
    language with effects
  • Signature help, quick fixes, document and workspace symbols, run lenses
  • Format on save, by khora fmt, scoped to .kh only

Settings

khora.server.path an executable to use instead of khora on PATH
khora.trace.server protocol traffic, into the server's output channel

Two commands, both under Khora: in the palette — Restart Language Server,
for after rebuilding the compiler, and Show Language Server Output.

Khora for VS Code vscode-v0.3.2

Pre-release

Choose a tag to compare

@github-actions github-actions released this 26 Sep 00:14

The Khora extension for Visual Studio Code.

Upgrading from 0.3.0 or earlier

The extension's ID is khora.khora. Version 0.3.0 was published as
khora-lang.khora, which VS Code treats as a different extension, so remove
it first or both will run:

code --uninstall-extension khora-lang.khora

Changes in 0.3.2

  • The language server starts. 0.3.0 and 0.3.1 launched khora lsp --stdio, which
    every released toolchain refuses with unexpected argument '--stdio', so
    the server exited before it answered and nothing but syntax colouring
    worked. It launches khora lsp.

Changes in 0.3.1

  • The publisher is khora, the name the project owns on the Marketplace.
  • Install Khora, offered when no khora is found, opens the installation
    page on khoralang.com. In 0.3.0 it opened a domain that does not resolve.
  • The status bar shows which toolchain answered, and turns yellow when a
    project pins a version that is not installed.
  • Highlighting for character literals ('a'), row declarations, /// doc
    comments, string interpolation, and the ?, .. and comparison operators.
  • Requires VS Code 1.82 or later.

Install

Download khora-vscode-<version>.vsix below, then:

code --install-extension khora-vscode-<version>.vsix

or in VS Code: Extensions, the ... menu, Install from VSIX.

Then fully quit and reopen VS Code — extensions are scanned at startup, so
reloading the window is not enough. code --list-extensions should list
khora.khora.

It needs the toolchain

The extension is a client. It starts khora lsp, a subcommand of the compiler,
and shows what the compiler says — so it needs khora on your PATH. Install
it from the toolchain releases,
or point khora.server.path at an executable, which is what to do when working
on the compiler itself.

The two are released separately and on purpose. This extension has no compiler
in it, versions on its own, and changes for its own reasons; a fix to hover
rendering should not wait for a language release or drag one along.

What you get

Everything but the syntax highlighting is answered by the compiler, over LSP,
from the same queries khora check runs. There is no second implementation to
drift.

  • Errors and warnings as you type, including lints
  • Hover types, go to definition, find references, rename
  • Completion: methods after ., a type's own items after ::, a module's
    exports inside an import list
  • Semantic highlighting the resolver decides — a local told from an import,
    which no regex can do
  • Inlay hints showing what a call costs — the with { .. } capabilities it
    requires and the errors it raises, which is the thing worth reading in a
    language with effects
  • Signature help, quick fixes, document and workspace symbols, run lenses
  • Format on save, by khora fmt, scoped to .kh only

Settings

khora.server.path an executable to use instead of khora on PATH
khora.trace.server protocol traffic, into the server's output channel

Two commands, both under Khora: in the palette — Restart Language Server,
for after rebuilding the compiler, and Show Language Server Output.

Khora for VS Code vscode-v0.3.1

Pre-release

Choose a tag to compare

@github-actions github-actions released this 25 Sep 23:55

The Khora extension for Visual Studio Code.

Upgrading from 0.3.0

The extension's ID is khora.khora. Version 0.3.0 was published as
khora-lang.khora, which VS Code treats as a different extension, so remove
it first or both will run:

code --uninstall-extension khora-lang.khora

Changes in 0.3.1

  • The publisher is khora, the name the project owns on the Marketplace.
  • Install Khora, offered when no khora is found, opens the installation
    page on khoralang.com. In 0.3.0 it opened a domain that does not resolve.
  • The status bar shows which toolchain answered, and turns yellow when a
    project pins a version that is not installed.
  • Highlighting for character literals ('a'), row declarations, /// doc
    comments, string interpolation, and the ?, .. and comparison operators.
  • Requires VS Code 1.82 or later.

Install

Download khora-vscode-<version>.vsix below, then:

code --install-extension khora-vscode-<version>.vsix

or in VS Code: Extensions, the ... menu, Install from VSIX.

Then fully quit and reopen VS Code — extensions are scanned at startup, so
reloading the window is not enough. code --list-extensions should list
khora.khora.

It needs the toolchain

The extension is a client. It starts khora lsp, a subcommand of the compiler,
and shows what the compiler says — so it needs khora on your PATH. Install
it from the toolchain releases,
or point khora.server.path at an executable, which is what to do when working
on the compiler itself.

The two are released separately and on purpose. This extension has no compiler
in it, versions on its own, and changes for its own reasons; a fix to hover
rendering should not wait for a language release or drag one along.

What you get

Everything but the syntax highlighting is answered by the compiler, over LSP,
from the same queries khora check runs. There is no second implementation to
drift.

  • Errors and warnings as you type, including lints
  • Hover types, go to definition, find references, rename
  • Completion: methods after ., a type's own items after ::, a module's
    exports inside an import list
  • Semantic highlighting the resolver decides — a local told from an import,
    which no regex can do
  • Inlay hints showing what a call costs — the with { .. } capabilities it
    requires and the errors it raises, which is the thing worth reading in a
    language with effects
  • Signature help, quick fixes, document and workspace symbols, run lenses
  • Format on save, by khora fmt, scoped to .kh only

Settings

khora.server.path an executable to use instead of khora on PATH
khora.trace.server protocol traffic, into the server's output channel

Two commands, both under Khora: in the palette — Restart Language Server,
for after rebuilding the compiler, and Show Language Server Output.

v0.3.0

Choose a tag to compare

@codyspate codyspate released this 23 Sep 06:32

Graceful shutdown, a way to ask a cancelled fiber what it ended as, native
libraries on the link line — and another round of strangers building real
programs against the published documentation, in which the tools that tell you
whether your program works turned out to be the ones lying.

The one to read first is signal handling. SIGTERM and SIGINT used to end a
Khora program where it stood; they now cancel it, its finalizers run, and it
exits 130. That is the reason for this release, and it changes the exit status
a supervisor sees, so it is under Breaking as well as Added.

Breaking

  • On Linux and macOS, SIGTERM and SIGINT unwind the program and exit
    130, where they used to kill it outright.
    A program whose main has a
    raises row now runs its finalizers on systemctl stop, docker stop, a
    Kubernetes rollout or Ctrl-C, and ends with exit status 130 rather than
    being killed by the signal. Added, below, has what the program observes.

    Listed as breaking for two reasons. The status changes: a wrapper script, a
    health check or a restart policy that matched on "killed by signal 15" or on
    exit status 143 sees an ordinary exit 130 instead. And the first signal is
    now a request: a program busy in work with no cancellation point — see the
    limits under Added — keeps running until that work ends or a second signal
    arrives, where it used to die at once. A program whose main has no
    raises row is unaffected: it still dies at the first signal, exactly as
    before, with no finalizers.

  • Fiber::wait is a cancellation point, and needs a failure channel. A
    fiber parked in wait could not be stopped before, which would have made
    graceful shutdown work for some programs and not others. The waiter can be
    cancelled while it is parked even when the fiber it waits on cannot fail, so
    khora check refuses a wait in a function with no raises clause and no
    enclosing catch, at the call, with or without a !. Inside a closure that
    raises nothing, put a catch around the call or give the closure a failing
    type. A generic function whose only channel is a row variable is refused by
    khora build rather than khora check; Limitations has the shape.

    A wait on a fiber whose body can fail raises that fiber's row, as join
    always has: write Fiber::wait(f)! in a function that raises it. Give
    main a raises row and catch the cancellation where the program can
    answer it; that row is also what signal handling needs.

  • A test suite that was green may be red after upgrading. khora test
    reported verdicts under the wrong names when test blocks were spread across
    files — Fixed has the details — so a failing test could be reported ok
    under another test's name. Three tests in Khora's own standard library suite
    had been failing all along and reporting success this way. A test that fails
    now was failing before; read it rather than the report you had.

  • Listing a directory that exists but cannot be read raises
    IoError::Failed, not IoError::NotFound.
    Code that reads NotFound as
    "absent, so create it" or "absent, so skip it" now takes the other branch on
    an unreadable directory. Fixed has the bug.

  • A row written in a local type annotation is checked, so khora check
    refuses programs it used to accept.
    A let annotation naming a function
    type with a with or raises clause, or a type argument carrying a row —
    List<Job<Int, { Bad: Bad }>> — was read as a type that agrees with
    anything. Passing such a value to a parameter declaring a different row
    compiled clean. It is now refused: at the let when the annotation disagrees
    with the value, and at the call when the parameter does.

    Listed as breaking because a program that compiled may not; it was already
    wrong, and the annotation it wrote was never being read. Fixed, below, has
    the other half: programs this reading accepts that were refused before.

  • where, yield, macro, unsafe and unstable are reserved. Khora
    has no editions, so a keyword claimed after 1.0 would break every program
    that used the word as a name with no way to migrate. These five are held
    back now, while it is cheap; none of them means anything yet, and none is
    highlighted as a keyword. Using one as a name is an error saying so, rather
    than a parse error:

    error: `yield` is reserved for a future version of Khora and cannot be used as
           a name. ...
    

    Rename the binding, field or function.

  • A dependency contributes only the modules under its own name. A package
    named csv publishes csv and every csv::*; a module beside it with any
    other name — a csv_test holding the author's tests, or a helpers the
    author never meant to publish — is no longer compiled into your build. It
    was, with three consequences: a consumer could import a library's test
    module and use types written for tests, khora test in a consumer ran every
    dependency's suite — so somebody else's failing test failed your run — and
    khora doc published a page for each test module in a library's public API.

    Listed as breaking because a program that imported such a module no longer
    compiles, and a library whose public modules are not rooted at its package
    name cannot be imported until its author moves them under it. A library that
    puts its tests in the same file as its code, as khora new --lib does, is
    imported as before.

  • A directory named build is skipped when collecting sources, at any
    depth, alongside target and .git. khora build writes its output to
    build/, and a stray .kh file there was compiled and its errors reported
    against your program. The Getting started page already said build/ was
    skipped; the compiler now agrees.

    Listed as breaking because a package that keeps modules in a directory named
    build — src/build/ — will find them missing. Rename the directory.

Fixed

  • khora test and khora bench reported results under the wrong names when
    blocks were spread across files.
    Test keys were numbered per file, so the
    nth block of one file and the nth of another shared one, and the lookup that
    paired a compiled body with its name ignored which file it came from.

    The visible effects: a failing test reported ok with the run exiting 0, a
    test that cannot fail reported as failing, totals that described a suite
    that did not exist, and a Dict insertion benchmark that appeared to run six
    times faster with four times the keys. The measurements themselves were
    correct; only the
    names on them were wrong, so there was nothing in the output to suggest a
    mix-up. Also under Breaking, because a verdict you have been reading may not
    say what you thought it said.

  • A record returned from a fiber could abort the process. Joining a fiber
    whose answer was a record holding a computed String printed the right
    answer and then khora runtime: drop of an object whose refcount is already zero, exit 134, with khora check clean. The value's contents were not
    counted when it was read out of the fiber, so two owners released them.
    20000 spawn-and-join cycles now hold steady in memory, so this is fixed
    rather than traded for a leak.

  • A path containing a non-ASCII character took the program down. Every
    filesystem call checks its path against the permission grant, and that check
    cut multi-byte characters in half: one Résumé.pdf in a directory being
    walked ended the program with this slice cuts a character in half, from
    inside std, uncatchable, naming no path. Any path is now accepted, and a
    path outside the grant is still refused.

  • A directory that cannot be read was reported as not there. Listing a
    locked directory raised IoError::NotFound, which sends somebody to check a
    path that is exactly where they left it instead of at the permission bit.
    Files already drew this distinction; directories now draw it too, and an
    absent directory still raises NotFound. Also under Breaking, because code
    that branched on NotFound behaves differently.

  • Cancelling a fiber whose body is a nursery returns. It hung:
    Fiber::cancel returned and Fiber::wait never did, with no message,
    because the nursery's children were never told. The cancellation now reaches
    them, and cancelling through a nursery costs what cancelling a directly
    spawned child does. This is what makes a deadline expressible — see the new
    Timeouts and cancellation recipe — and what signal handling is built on.

  • Cancelling a fiber that is serving connections stops it. Fiber::cancel
    on a fiber in Router::listen hung the process for ever with nothing on any
    stream, even for an idle listener: main ran to its last line and never
    exited. A fiber waiting to accept a connection or to send now stops when it
    is cancelled, on both fiber backends. The documentation had described this as
    an abort with a named message, which it never was, and recommended
    Fiber::detach instead, which leaves the listener just as stuck; both are
    corrected.

  • A let annotation that names a row is read. Annotating a list of
    closures with the row they actually raise —
    let handlers: List<(Int) -> () raises Boom> = [...] — and then iterating
    it failed with an error about Iterator::next and impl resolution, three
    lines from anything you wrote. The row was being thrown away. It now checks
    clean, and a for over a list of row-carrying values is no longer refused.

    The same annotations were accepted when they named the wrong row, which is
    the silently wrong half of this, and is under Breaking.

  • A wrapper type is constructed by name wherever it is imported.
    pub type Money = Int; is built Money(499), as the Types page teaches, but
    from any module other than the declaring one that failed with *the type of
    this expression was never work...

Read more

v0.3.0-rc.1

v0.3.0-rc.1 Pre-release
Pre-release

Choose a tag to compare

@codyspate codyspate released this 23 Sep 05:50

Graceful shutdown, a way to ask a cancelled fiber what it ended as, native
libraries on the link line — and another round of strangers building real
programs against the published documentation, in which the tools that tell you
whether your program works turned out to be the ones lying.

The one to read first is signal handling. SIGTERM and SIGINT used to end a
Khora program where it stood; they now cancel it, its finalizers run, and it
exits 130. That is the reason for this release, and it changes the exit status
a supervisor sees, so it is under Breaking as well as Added.

Breaking

  • On Linux and macOS, SIGTERM and SIGINT unwind the program and exit
    130, where they used to kill it outright.
    A program whose main has a
    raises row now runs its finalizers on systemctl stop, docker stop, a
    Kubernetes rollout or Ctrl-C, and ends with exit status 130 rather than
    being killed by the signal. Added, below, has what the program observes.

    Listed as breaking for two reasons. The status changes: a wrapper script, a
    health check or a restart policy that matched on "killed by signal 15" or on
    exit status 143 sees an ordinary exit 130 instead. And the first signal is
    now a request: a program busy in work with no cancellation point — see the
    limits under Added — keeps running until that work ends or a second signal
    arrives, where it used to die at once. A program whose main has no
    raises row is unaffected: it still dies at the first signal, exactly as
    before, with no finalizers.

  • Fiber::wait is a cancellation point, and needs a failure channel. A
    fiber parked in wait could not be stopped before, which would have made
    graceful shutdown work for some programs and not others. The waiter can be
    cancelled while it is parked even when the fiber it waits on cannot fail, so
    khora check refuses a wait in a function with no raises clause and no
    enclosing catch, at the call, with or without a !. Inside a closure that
    raises nothing, put a catch around the call or give the closure a failing
    type. A generic function whose only channel is a row variable is refused by
    khora build rather than khora check; Limitations has the shape.

    A wait on a fiber whose body can fail raises that fiber's row, as join
    always has: write Fiber::wait(f)! in a function that raises it. Give
    main a raises row and catch the cancellation where the program can
    answer it; that row is also what signal handling needs.

  • A test suite that was green may be red after upgrading. khora test
    reported verdicts under the wrong names when test blocks were spread across
    files — Fixed has the details — so a failing test could be reported ok
    under another test's name. Three tests in Khora's own standard library suite
    had been failing all along and reporting success this way. A test that fails
    now was failing before; read it rather than the report you had.

  • Listing a directory that exists but cannot be read raises
    IoError::Failed, not IoError::NotFound.
    Code that reads NotFound as
    "absent, so create it" or "absent, so skip it" now takes the other branch on
    an unreadable directory. Fixed has the bug.

  • A row written in a local type annotation is checked, so khora check
    refuses programs it used to accept.
    A let annotation naming a function
    type with a with or raises clause, or a type argument carrying a row —
    List<Job<Int, { Bad: Bad }>> — was read as a type that agrees with
    anything. Passing such a value to a parameter declaring a different row
    compiled clean. It is now refused: at the let when the annotation disagrees
    with the value, and at the call when the parameter does.

    Listed as breaking because a program that compiled may not; it was already
    wrong, and the annotation it wrote was never being read. Fixed, below, has
    the other half: programs this reading accepts that were refused before.

  • where, yield, macro, unsafe and unstable are reserved. Khora
    has no editions, so a keyword claimed after 1.0 would break every program
    that used the word as a name with no way to migrate. These five are held
    back now, while it is cheap; none of them means anything yet, and none is
    highlighted as a keyword. Using one as a name is an error saying so, rather
    than a parse error:

    error: `yield` is reserved for a future version of Khora and cannot be used as
           a name. ...
    

    Rename the binding, field or function.

  • A dependency contributes only the modules under its own name. A package
    named csv publishes csv and every csv::*; a module beside it with any
    other name — a csv_test holding the author's tests, or a helpers the
    author never meant to publish — is no longer compiled into your build. It
    was, with three consequences: a consumer could import a library's test
    module and use types written for tests, khora test in a consumer ran every
    dependency's suite — so somebody else's failing test failed your run — and
    khora doc published a page for each test module in a library's public API.

    Listed as breaking because a program that imported such a module no longer
    compiles, and a library whose public modules are not rooted at its package
    name cannot be imported until its author moves them under it. A library that
    puts its tests in the same file as its code, as khora new --lib does, is
    imported as before.

  • A directory named build is skipped when collecting sources, at any
    depth, alongside target and .git. khora build writes its output to
    build/, and a stray .kh file there was compiled and its errors reported
    against your program. The Getting started page already said build/ was
    skipped; the compiler now agrees.

    Listed as breaking because a package that keeps modules in a directory named
    build — src/build/ — will find them missing. Rename the directory.

Fixed

  • khora test and khora bench reported results under the wrong names when
    blocks were spread across files.
    Test keys were numbered per file, so the
    nth block of one file and the nth of another shared one, and the lookup that
    paired a compiled body with its name ignored which file it came from.

    The visible effects: a failing test reported ok with the run exiting 0, a
    test that cannot fail reported as failing, totals that described a suite
    that did not exist, and a Dict insertion benchmark that appeared to run six
    times faster with four times the keys. The measurements themselves were
    correct; only the
    names on them were wrong, so there was nothing in the output to suggest a
    mix-up. Also under Breaking, because a verdict you have been reading may not
    say what you thought it said.

  • A record returned from a fiber could abort the process. Joining a fiber
    whose answer was a record holding a computed String printed the right
    answer and then khora runtime: drop of an object whose refcount is already zero, exit 134, with khora check clean. The value's contents were not
    counted when it was read out of the fiber, so two owners released them.
    20000 spawn-and-join cycles now hold steady in memory, so this is fixed
    rather than traded for a leak.

  • A path containing a non-ASCII character took the program down. Every
    filesystem call checks its path against the permission grant, and that check
    cut multi-byte characters in half: one Résumé.pdf in a directory being
    walked ended the program with this slice cuts a character in half, from
    inside std, uncatchable, naming no path. Any path is now accepted, and a
    path outside the grant is still refused.

  • A directory that cannot be read was reported as not there. Listing a
    locked directory raised IoError::NotFound, which sends somebody to check a
    path that is exactly where they left it instead of at the permission bit.
    Files already drew this distinction; directories now draw it too, and an
    absent directory still raises NotFound. Also under Breaking, because code
    that branched on NotFound behaves differently.

  • Cancelling a fiber whose body is a nursery returns. It hung:
    Fiber::cancel returned and Fiber::wait never did, with no message,
    because the nursery's children were never told. The cancellation now reaches
    them, and cancelling through a nursery costs what cancelling a directly
    spawned child does. This is what makes a deadline expressible — see the new
    Timeouts and cancellation recipe — and what signal handling is built on.

  • Cancelling a fiber that is serving connections stops it. Fiber::cancel
    on a fiber in Router::listen hung the process for ever with nothing on any
    stream, even for an idle listener: main ran to its last line and never
    exited. A fiber waiting to accept a connection or to send now stops when it
    is cancelled, on both fiber backends. The documentation had described this as
    an abort with a named message, which it never was, and recommended
    Fiber::detach instead, which leaves the listener just as stuck; both are
    corrected.

  • A let annotation that names a row is read. Annotating a list of
    closures with the row they actually raise —
    let handlers: List<(Int) -> () raises Boom> = [...] — and then iterating
    it failed with an error about Iterator::next and impl resolution, three
    lines from anything you wrote. The row was being thrown away. It now checks
    clean, and a for over a list of row-carrying values is no longer refused.

    The same annotations were accepted when they named the wrong row, which is
    the silently wrong half of this, and is under Breaking.

  • A wrapper type is constructed by name wherever it is imported.
    pub type Money = Int; is built Money(499), as the Types page teaches, but
    from any module other than the declaring one that failed with *the type of
    this expression was never work...

Read more

v0.2.0

Choose a tag to compare

@codyspate codyspate released this 12 Sep 01:43

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...

Read more

v0.1.0

Choose a tag to compare

@codyspate codyspate released this 03 Sep 22:55

v0.1.0

The first release with a plain version number. 241 commits since
v0.1.0-rc.3, and the reason it took three candidates to get here is that
each one was scored against docs/release-readiness.md item by item and each
scoring found things the previous one had asserted rather than checked.

This is 0.1.0, and the number is the promise. The language may break
before 1.0, and this release breaks things rc.3 accepted. What it will not
do is break them quietly: CHANGELOG.md lists every one, and anything that
used to produce a silently wrong answer is listed as breaking as well as
fixed, because code written around it behaves differently now.

Install

curl -fsSL https://raw.githubusercontent.com/codyspate/khoralang/main/install.sh | sh
irm https://raw.githubusercontent.com/codyspate/khoralang/main/install.ps1 | iex

Three targets: x86_64-unknown-linux-gnu, x86_64-pc-windows-msvc and
aarch64-apple-darwin. They are exactly what the release workflow builds,
packages, unpacks somewhere else and compiles a program with before anything
is published. Linux arm64, cross-compilation, musl and WebAssembly are named
as out of scope with the reason at https://khoralang.com/docs/next/deployment/supported-targets/,
because an unchecked platform in a supported table is worse than an absent
one.

What changed since rc.3

The full list is in CHANGELOG.md. The four things a program
is most likely to notice:

  • std::schema replaces std::json's decoding, and a type is its own
    schema.
    derive(Decode, Encode) on a record gives you the same reader for
    a request body, a database row, an environment block and an LLM's answer,
    and it reports a bad value as [2].amount should be a whole number rather
    than dropping it. FromJson/ToJson, std::ai::Extract and std::config's
    eight accessor functions are all deleted in favour of it.
  • khora doc no longer defaults to this repository's layout. It defaulted
    to std and to website/content/docs/stdlib/api, resolved against wherever
    you happened to be standing, and its stale sweep deleted markdown files it
    had not written. It is package-relative now and refuses outside a package.
  • Row carries its column names, so a query's answer reads through the
    same schema as everything else.
  • std::log. Khora had no way to write to standard error at all, which
    two of the people who tried building command-line tools with it reported
    independently: a program's diagnostics and its answer shared one stream, and
    the first > out.txt anybody typed swallowed the diagnostics. Logging is a
    capability, emits one JSON object per line, and takes the clock as a value
    so a log line can be asserted against.

Also here: an epoll backend on Linux, which is where the portable poll
reactor had a scaling ceiling worth removing (macOS and Windows still run
the portable one — kqueue is the same shape and unwritten, IOCP is a
different interface rather than a backend swap); an OTLP trace exporter in
packages/otlp; TLS read deadlines; and a language server that does the
things people expect one to do — completion that writes the import, code
actions that fill in a missing handler or a missing record field, extract
variable and extract function.

What is not done

docs/release-readiness.md scores 200 of 222. The twenty-two open items are
each written out there with what specifically is left; the ones worth knowing
before you build on this:

  • macOS has not had the scheduler soak run against it. Linux and Windows
    have. Nothing here can run macOS, which is also why there is no kqueue
    backend.
  • A bounded_nursery(n) admits n + 1 children, because Fiber::spawn
    starts the child before adopt blocks. Subtract one where the limit is a
    real resource.
  • The two fiber backends are distinguishable by a program, which the
    concurrency reference says twice they are not. Under
    KHORA_FIBERS=scheduler a cancelled fiber's sleep returns early; under
    the default thread backend it runs to completion.
  • Cancellation lands only where a fiber checks for it — a !, or a loop
    back-edge. A long sleep is not a cancellation point.
  • A type name in std can collide with one of your own and crash the
    program at run time
    rather than failing to compile. Erratum 79. This is
    why the logging level type is called Severity.
  • Thread-affine foreign libraries are untested against fiber migration.

https://khoralang.com/docs/next/limitations/ carries these and the rest, and
docs/errata.md is the list of things known to be wrong.

Documentation

https://khoralang.com/docs/ now serves next — a version, rather than the whole
site — and every page says so. A section per stable major gets cut when v1
ships; before then there is one tree, because there is one compiler anybody
can install and the language may still change under it.