openkal 0.8: five optional interfaces, admitted against a stated criterion - #13
Merged
Conversation
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
This was referenced Aug 27, 2026
Merged
Merged
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.mdThe 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
openkal.terminalopenkal.streamrather than between implementations, which is the ground clause 6.4 gives for placing positioning inopenkal.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.netopenkal.datagramopenkal.spaceforkby saving its own execution state with the compiler's facilities.openkal.timeoutkal_task_waitalready states one. An expiry iskal_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-linuxagainst the interface, and in both the specification was changed rather than the implementation.openkal.spacewas 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:clonecopies 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.netyielded an ownedkal_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.fshad already answered this: a file is owned, its stream is borrowed throughkal_fs_stream, and the stream is released with the file.openkal.netnow has that shape, with the same wording. The plainer symptom was that the transfer read nothing:Types
struct kal_endpointcarries 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 intypes.hbecause bothopenkal.netandopenkal.datagramuse it and either may be provided without the other.Additions to
openkal.processkal_process_spawn_withstarts a program receiving exactly the directories named, and is the inverse ofkal_fs_preopenunder clause 7.11.kal_process_channelyields 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
-nostdincopenkal-linuxThe 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
forkis 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.