Skip to content

openkal 0.8: five optional interfaces, admitted against a stated criterion - #13

Merged
Sunrisepeak merged 9 commits into
mainfrom
feat/openkal-0.8
Aug 27, 2026
Merged

openkal 0.8: five optional interfaces, admitted against a stated criterion#13
Sunrisepeak merged 9 commits into
mainfrom
feat/openkal-0.8

Conversation

@Sunrisepeak

@Sunrisepeak Sunrisepeak commented Aug 27, 2026

Copy link
Copy Markdown
Member

Version 0.8 adds terminal control, connections, datagrams, address spaces, and a bound upon operations that would otherwise wait without end.

Design record: 2026-08-27-openkal-ecosystem-design-plan.md

The criterion each candidate was held to

An interface enters the specification only when it is a minimal capability that every kernel has, cannot be composed from the atoms already specified, and is usable in both directions: composable from above and implementable from below. What can be composed belongs in a library above the line rather than in the specification.

Three candidates did not meet it and are recorded in clause 6.3 rather than adopted.

Decomposition

Interface Why it is its own interface
openkal.terminal The behaviour varies between the RESOURCES of openkal.stream rather than between implementations, which is the ground clause 6.4 gives for placing positioning in openkal.fs. The pair is get/set and not two setters: a program that turns line editing off must be able to restore what was there rather than a default.
openkal.net Positioning applies to a file and not to a connection; half-closure to a connection and not to a file. Clause 3.4 already anticipated the separation.
openkal.datagram A message boundary is not a property a byte stream has; ordering is not a property a datagram has. It is also the easier half to provide, so an implementation supplying only this one is ordinary rather than deficient.
openkal.space Clause 7.1 refuses to require the duplication of an address space AND its execution state. What is specified is the first half; a library reaches fork by saving its own execution state with the compiler's facilities.
openkal.timeout A duration and not an instant, because kal_task_wait already states one. An expiry is kal_err_again; the closed error set required no addition.

Two defects the first implementation found, that reading did not

Both were found by building openkal-linux against the interface, and in both the specification was changed rather than the implementation.

openkal.space was two operations and is now one. It separated the copying of an address space from the starting of a context in it. No environment this specification targets has that pair as a primitive: clone copies the space and begins execution in the copy, and there is no form that does the first without the second. An implementation asked to separate them would have to start a context anyway, park it upon a waiting primitive, and build a channel by which to tell it what to run — a mechanism reconstructed, which clause 7.1 identifies as a fault in the shape of the specification. The separated form also had a question it could not answer: whether the copy a context sees is the one taken at the first call or the second.

openkal.net yielded an owned kal_stream, which cannot be implemented under clause 7.2. A released handle must not be treated as valid, and a stream handle is whatever the environment's transfer operations take — on a system of descriptors, a number reused as soon as it is closed. There is nowhere to carry a generation. openkal.fs had already answered this: a file is owned, its stream is borrowed through kal_fs_stream, and the stream is released with the file. openkal.net now has that shape, with the same wording. The plainer symptom was that the transfer read nothing:

FAIL: the bytes read are the bytes written
FAIL: the peer observes end of input after a half-closure

Types

struct kal_endpoint carries bytes and a length rather than a tagged union of families. The length is a VALUE rather than a layout, so the set of lengths defined may grow while clause 5.3 holds the structure fixed. An implementation refuses a length it does not know rather than reading it as one it does. It is declared in types.h because both openkal.net and openkal.datagram use it and either may be provided without the other.

Additions to openkal.process

kal_process_spawn_with starts a program receiving exactly the directories named, and is the inverse of kal_fs_preopen under clause 7.11. kal_process_channel yields a pair of streams of which one end crosses a spawn boundary; both ends are owned, and the release is declared beside them because a parent that does not release the far end never observes the end of input on its own. Both are second declarations rather than arguments added to existing ones, because clause 8 forbids altering one that exists.

Verification

declarations 90 names, compiled with -nostdinc
types 149 declarations, no backend type named
conformance 100 observations held, 0 did not hold against openkal-linux
terminal its interactive path observed under a real pty, not only the refusal path

The conformance sections name properties through the modules rather than through the macros: a macro does not cross a module boundary, and a section is a module.

Clause 6.3 gains three entries and clause 11 gains six. The clause 11 entry on duplication of the calling image states that fork is refused as an OPERATION and that the atoms from which a library may compose it are now specified, so that a later reader does not conclude the capability was excluded along with the operation.

Paired with mcpplibs/openkal-linux#15.

The five are terminal control, connections, datagrams, address spaces, and a
bound upon operations that would otherwise wait without end. Each is optional in
the sense clause 3 defines: an environment with no network is not deficient for
providing neither openkal.net nor openkal.datagram, and one with no memory
management unit cannot provide openkal.space and is not deficient either. Clause
6.1 expresses each absence as the absence of a definition at the link, so a
program requiring one is refused when it is built rather than when it runs.

The admission criterion applied to each was the same, and three candidates did
not meet it. An interface enters the specification only when it is a minimal
capability that every kernel has, cannot be composed from the atoms already
specified, and is usable in both directions --- composable from above and
implementable from below. What can be composed belongs in a library above the
line.

## Decomposition

openkal.terminal is a separate interface rather than operations upon
openkal.stream, for the reason clause 6.4 gives when it places positioning in
openkal.fs: the behaviour varies between the RESOURCES of the stream interface
rather than between implementations. The pair is get/set and not two setters,
because a program that turns line editing off must be able to restore what was
there rather than a default.

openkal.net and openkal.datagram are separate from each other on the same
grounds. A message boundary is not a property a byte stream has; ordering is not
a property a datagram has. Datagrams are also the easier half to provide, so an
implementation supplying only that one is ordinary rather than deficient.

openkal.space is not fork. Clause 7.1 refuses to require the duplication of an
address space AND its execution state; what is specified is the first half
alone. A library above the interface reaches fork by saving its own execution
state before the clone and restoring it in the new context, which is composition
performed with the compiler's own facilities.

openkal.timeout states a duration rather than an instant, because kal_task_wait
already does and a second spelling of one idea is what this specification most
consistently refuses. An expired bound is kal_err_again; the closed error set of
clause 5.2 required no addition.

## Types

struct kal_endpoint carries bytes and a length rather than a tagged union of
families. The length distinguishes one kind of address from another and is a
VALUE rather than a layout, so the set of lengths defined may grow while clause
5.3 continues to hold the structure fixed. An implementation refuses a length it
does not know rather than reading it as one it does. It is declared in types.h
because both openkal.net and openkal.datagram use it and either may be provided
without the other.

## Additions to openkal.process

kal_process_spawn_with starts a program receiving exactly the directories named,
and is the inverse of kal_fs_preopen under clause 7.11. kal_process_channel
yields a pair of streams of which one end crosses a spawn boundary; both ends are
owned, and the release is declared beside them because a parent that does not
release the far end never observes the end of input on its own. Both are second
declarations rather than arguments added to existing ones, because clause 8
forbids altering one that exists.

## Verification

  declarations   91 names, compiled without the environment's headers
  types         150 declarations, no backend type named
  modules        the five new interfaces compile

The two entries clause 6.3 gains record readiness notification and an instant
rather than a duration, with the property of implementations that excluded each.
Clause 11 gains six entries. The entry on duplication of the calling image states
that fork is refused as an OPERATION and that the atoms from which a library may
compose it are now specified, so that a later reader does not conclude the
capability was excluded along with the operation.
Withdrawn while the first implementation was being written, which is the point
at which the defect became visible.

The interface separated the copying of an address space from the starting of a
context in it, so that a caller held a space as a handle and started a context in
it afterwards. No environment this specification targets has that pair as a
primitive. On Linux the copy and the start are one act: clone(2) copies the
address space and begins execution in the copy, and there is no form that does
the first without the second.

An implementation asked to provide the separated form would therefore have to
start a context anyway, park it upon a waiting primitive, and build a channel by
which to tell it what to run afterwards. Clause 7.1 states the test and the
verdict: an implementation that must maintain a registry in order to satisfy the
specification indicates that the specification has taken a shape borrowed from
one environment, and the shape is at fault rather than the implementation.

The separated form also had an unstated question it could not answer. If a
caller copies a space, modifies its own memory, and then starts a context, the
copy the context sees is the one taken at the first call under one reading and at
the second under the other. The single operation does not have the question.

  removed   struct kal_space, kal_space_clone, kal_space_destroy
  retained  kal_space_start, now taking the entry directly, and kal_space_props

Clause 6.3 records the withdrawn form among the mechanisms considered and not
adopted, with the property of implementations that excluded it, so that a later
reader proposing it again finds the measurement rather than repeating it.

The conformance section was rewritten around the exit status. A started context
has an address space of its own, so a store it performs is not visible to the
caller; how it ended is the one thing it can report, and the section therefore
observes that the entry received its argument by having the entry choose its exit
code from it.

  declarations   89 names, compiled without the environment's headers
  types         146 declarations, no backend type named
Found by writing the first implementation, which is where a defect of this kind
becomes visible: reading the interface did not reveal it, and building it did on
the first run.

openkal.net yielded a kal_stream and declared it owned. That cannot be
implemented under clause 7.2, which requires that a released handle not be
treated as valid. A stream handle is whatever the environment's transfer
operations take --- on a system of descriptors, a number the environment reuses
as soon as it is closed --- so there is nowhere to carry the generation that
makes a released handle detectable. An owned handle can carry one; a borrowed
stream cannot.

openkal.fs had already answered the same question. A file is owned, its stream is
borrowed from it through kal_fs_stream, and the stream is released with the file
rather than separately. openkal.net now has that shape exactly, with the same
wording, because it is the same arrangement.

  added     struct kal_net_conn, kal_uintptr kal_net_stream(struct kal_net_conn)
  changed   connect, accept, peer, local, shutdown and close take a connection
  changed   kal_timeout_accept yields a connection

The first form also failed a plainer test. The Linux implementation packed the
connection into the handle scheme, kal_stream_read took the packed word as a
descriptor, and the transfer read nothing:

    FAIL: the bytes read are the bytes written
    FAIL: the peer observes end of input after a half-closure

Both observations hold under the corrected shape.

The conformance sections name the properties through the modules rather than
through the macros. A macro does not cross a module boundary, and a section is a
module; the C form's KAL_ names are for a consumer written in C.

  declarations  90 names, compiled without the environment's headers
  types        149 declarations, no backend type named
  conformance  100 observations held, 0 did not hold, against openkal-linux
The specification admits an interface only when it is a minimal capability every
kernel has and cannot be composed from the interfaces already present. That rule
keeps openkal implementable on a machine with firmware and nothing else, and it
leaves a gap: a program wanting to carry bytes between two of its own contexts,
or to turn "127.0.0.1:8080" into an endpoint, has an answer in POSIX and none
here, because both are composed rather than primitive.

The gap was being filled by the port layer. There is one port layer today and
what it composes is POSIX, so a native openkal program either wrote the
composition again or took a whole C library. This is where it is written once.

## The contract form decides what is normative

Clause 10 states that openkal's contract is a C application binary interface.
The kit deliberately is not one: C++ modules in `namespace kal::kit`, exporting
no name beginning with `kal_`. A C++ name is mangled to `_ZN3kal3kit…`, so
`check-surface.sh --complete` does not read a program that links the kit as an
implementation which has added names.

"Is this normative" is therefore answered by the shape of what is exported rather
than by a sentence saying it is not. A sentence can be overlooked; a mangled name
cannot become a C symbol. The consequence is that the kit MAY evolve, which
clause 8 forbids the specification from doing.

## What is in it

openkal.kit.endpoint writes an endpoint down and reads one back. Clause 3.4
excludes name resolution from the specification, and that exclusion leaves every
caller of openkal.net with the same small problem: a configuration file says
"127.0.0.1:8080" and kal_net_connect takes bytes and a number. The rejections are
what makes it worth having --- a parser accepting "1.2.3" as 1.2.0.3, or
"300.1.1.1" as 44.1.1.1, turns a mistyped configuration into a connection
somewhere else and reports nothing at the time.

openkal.kit.channel carries bytes between two contexts of one address space, from
a region of openkal.memory and openkal.task's wait and wake. It is deliberately
not kal_process_channel: that interface crosses a spawn boundary and is a kernel
facility because a child cannot be handed a pointer into its parent's memory.
This one needs nothing from the kernel but a place to sleep.

Two details are load-bearing and are recorded where they are made. The close is
tested AFTER the indices, because a writer that filled the ring and closed in the
same breath leaves bytes behind it and a reader seeing the close first would
report an end of input that lost data. Both indices are woken on close, because a
writer waiting for room and a reader waiting for bytes must both learn that no
more is coming.

## Verification

The tests are behavioural. A test that named each entity and did nothing with it
would compile against a package whose every operation did nothing.

The channel is exercised with a hundred thousand bytes through a four-kilobyte
ring, across two contexts, in blocks that are not a divisor of the ring. A
payload that fitted would never make the writer wait, and the sleeping and waking
the module exists for would go unobserved.

    openkal-kit: every observation held

tools/run-kit-tests.sh substitutes working trees for both the specification and
the implementation, as run-conformance.sh does and for the same reason: a change
spanning two repositories must be tested against both halves as written. The
substitution is asserted rather than assumed --- one that matched nothing would
leave the manifest naming a published version, and the run would report on that
one while appearing to report on this branch --- and a trap restores the
manifests, because leaving a local absolute path in a public repository is a
mistake this repository has made once already.
They are examined in that interface's section rather than in one of their own,
because they are part of it. Clause 6.1 makes an interface provided IN PART a
deviation, so an implementation reaching this section has undertaken to provide
them and the suite is entitled to observe them.

The observations are of behaviour: a channel carries bytes, and closing the far
end is observed as end of input on the near one. That last is what the release
exists for --- a parent which does not release the far end after a spawn never
observes the end of input, which is the deadlock this pair invites and the reason
the release is declared beside the operation rather than left to openkal.stream.

⚠️ THE ABI CHECK WOULD HAVE FAILED, AND ONLY WITH `abi` SELECTED. It compares the
capability word against the set of positions the specification has assigned, and
that set was written out by name:

    DID NOT HOLD [abi] the capability word contains no position the
                       specification has not assigned

An implementation claiming the two positions version 0.8 adds was therefore
reported as claiming something unassigned. Measured by narrowing the set again
and watching the observation fail, so that the widening is known to be what
carries it rather than assumed.

That is the second time in this release that adding to an existing interface
reached further than adding a new one. The first was the surface checker, which
found three backends providing openkal.process in part; this one is in the suite
those backends run against themselves.

  143 observations held, 0 did not hold, with full,optional and the five
  interfaces version 0.8 adds
The claim is that the kit cannot be mistaken for an implementation because it
exports no name beginning with kal_, and the claim holds: measured on its
objects, zero of the defined names do.

⚠️ The REASON given was wrong in its particulars. It said a C++ name is mangled
to `_ZN3kal3kit…`, and these objects contain no such symbol: the operations are
inline and are emitted into consumers rather than exported, so what the objects
define are module initialisers like `_ZGIW7openkalW3kitW7channel`.

An argument that rests on how a compiler mangles names is worth checking against
what the compiler emitted. The structural conclusion is unchanged --- C++
mangling cannot produce a bare kal_ symbol --- but a reader following the comment
to verify it would have looked for something that is not there.
Three changes, and the second is why the first two were needed.

THE ENGINE. MCPP_VERSION moves to 2026.8.27.1, which is now published, and the
repository variable MCPP_SOURCE_REF is cleared, so this repository tests the
released engine rather than a branch of it.

THE EXAMPLE HAD DRIFTED THREE SPECIFICATION VERSIONS. examples/portable named
`openkal = "0.5.0"` while declaring itself the program that demonstrates the
current contract to a reader who would copy it. The backends it named were
equally old.

NOTHING BUILT IT, WHICH IS WHY. Its README stated that "an implementation's
continuous integration fetches this source ... builds it against itself, and
asserts the lines below". Measured across all six implementation repositories,
the word `portable` appears in none of their workflows. The sentence described
an arrangement that existed nowhere, and a claim with no criterion behind it
cannot notice its own subject going stale.

The conformance job now builds and runs it against each implementation it
tests, substituting the working trees as the conformance runner already does.
The criterion carries a denominator: a program that failed to start would print
none of its lines, and a check for the absence of failures would hold over an
empty log, so the line count is asserted before the count of failures.

Measured before the step was written --- the source needed no change, only its
manifest: over openkal 0.8 and openkal-linux as written here it prints all eight
interface lines and `observations that did not hold: 0`.

Two details the measurement settled. The substitution uses relative paths,
because this job runs on Windows where MSYS reports `/d/a/...`, which the build
tool does not resolve, and because a relative path cannot put one machine's
directory into a file that might be committed. It passes no `standalone`
feature: that feature makes the implementation supply `_start`, which collides
with the C library's crt1.o on a hosted toolchain --- `multiple definition of
_start`, measured --- and the conformance runner defaults to no features for
the same reason.
The portable step rewrote the example's manifest and not the implementation's,
so one package was reachable two ways and the build refused:

    error: dependency 'mcpplibs.openkal' is requested as both a path dep (by
           'portable') and a git dep (by 'mcpplibs.openkal-linux@path').

The refusal is correct --- two routes to one package are two versions of it ---
and the omission was mine rather than the example's. The local measurement that
preceded this step did substitute both; only the step did not.

Both manifests now name the same directory by relative path, as
tools/run-conformance.sh already does for the suite, and each substitution is
asserted rather than assumed.
Building it for the first time found that it did not compile under MSVC:

    error C3861: '__atomic_load_n': identifier not found
    error C2065: '__ATOMIC_RELAXED': undeclared identifier
    error C2065: '__UINT64_TYPE__': undeclared identifier

⚠️ NAMING NO SYSTEM CALL IS NOT THE SAME AS NAMING NO COMPILER. The program's
opening sentence claims it names no implementation, no operating system, no
descriptor number and no system call, and all of that was true. It was written
in GCC and Clang spellings throughout, so the one program whose premise is
portability failed on one of the three compilers its own specification is
tested against. Two of the five conformance rows would have caught it on any
day since it was written; neither ever built it.

The integer types become the specification's own. openkal/types.h already
carries the MSVC branch, so `kal_u32` and `kal_u64` are the portable spellings,
and using them keeps this program inside the vocabulary it exists to
demonstrate. `kal_task_wait` takes `const kal_u32*`, which the lock word now is
by declaration rather than by coincidence.

The atomics become the language's. `std::atomic_ref` refers to the existing
word rather than replacing it, which is what allows `kal_task_wait` to take its
address, and it is freestanding in C++23 --- so depending on it does not
contradict what this program shows. Suspension is a kernel facility and openkal
offers it; an atomic operation is a language facility and openkal is right not
to.

Measured over openkal-linux before pushing: all eight interface lines and
`observations that did not hold: 0`, including the contended-counter
observation, which is the one that exercises the rewritten lock rather than
merely compiling it.
@Sunrisepeak
Sunrisepeak merged commit 5fdb4e0 into main Aug 27, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants