From e2fb49abe322ba76e472c02833a1f6699d613bda Mon Sep 17 00:00:00 2001 From: speak-agent Date: Sat, 29 Aug 2026 00:45:24 +0800 Subject: [PATCH 1/7] 0.9.0 --- one idea has one spelling, and every declaration can cross a boundary MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Compile-time portability was the floor and is no longer mistaken for the goal. Every interface, operation and structure is now shaped so that it can also be crossed at run time, and clause 3's table states for each which boundaries it crosses --- a statement about the declarations, which an implementation cannot violate and only a declaration can. Thirteen of the fifteen interfaces could not be crossed, and five causes stood in the way rather than fifteen: ten exported data objects every report becomes an operation, and one that varies between the RESOURCES of an interface takes the resource five internal pointers a value is copied into the caller's buffer and the length reported is the value's own seven two-word results a transfer returns one signed word: the count, or the negated condition when none moved two entries worded as a call an entry is an address at which a context begins; no return address exists one clause 6.5 settles availability at production, which an artifact produced once for many environments cannot use, so `openkal.exec' reports it as well The transfer result was going to gain an out-parameter. It did not: the only consumer of the two-word form collapsed the pair by hand at every site and by one rule --- report what moved, or the condition when nothing did --- so the pair was never the shape a caller wanted. `kal_io_result` is gone. Also, and not incidentally: * `standard` and `hosted` are withdrawn. A class named after a kind of environment is falsified by an environment nobody had in mind, and this one was falsified inside its own ecosystem: a C library is hosted above an implementation providing none of the three interfaces the class promised. * `kal_memory_granularity` exists because a C library above openkal had no way to learn the quantum and fixed it at four kilobytes when it was built, which is wrong on every machine whose quantum differs from the one it was built for. * `kal_node_info` carries its own size, reports which fields it filled, and carries an opaque identity --- three questions that were conflated and that left two different files reported as the same file, with no error. * Links enter `openkal.fs`, and resolution is stated in `fs.h` beside every operation it governs. It was stated only in clause 11, and two call sites of one implementation consequently chose opposite directions. * `kal_fs_open_file` is deleted; `kal_fs_stream` and `kal_spawn_streams` carry their type; `kal_endpoint.addr_len` no longer freezes a per-word-size difference; clause 7.2 requires a forged handle to be refused; a name length bound is reportable. * `kal_version` and `kal_interfaces` are exported by every implementation and belong to no interface, so a consumer with no linker to ask can ask. Two defects in the suite's own machinery were found while running it, and both were producing false greens: * `run-conformance.sh` chose the binary with `find | head -1`. `target` accumulates a directory per fingerprint, so a run reported `143 held` from a binary built two days earlier that contained none of the observations just added. Suites are now removed before building, and finding more than one afterwards is a condition rather than a choice. * `full` was defined as `["standard", …]`. Withdrawing `standard` left it naming four kinds of examination and no interface: 168 held became 119 held, with an exit status of zero both times. 168 observations hold against openkal-linux 0.7.0, up from 143. --- ...-08-28-beyond-static-linking-abi-review.md | 1926 +++++++++++++++++ README.md | 32 +- SPEC.md | 263 ++- SURFACE.txt | 19 +- conformance/mcpp.toml | 49 +- conformance/src/declarations.c | 9 +- conformance/src/report.cpp | 4 +- conformance/src/sections/child.cpp | 27 +- conformance/src/sections/datagram.cpp | 22 +- conformance/src/sections/env.cpp | 120 +- conformance/src/sections/exec.cpp | 25 +- conformance/src/sections/fs.cpp | 240 +- conformance/src/sections/memory.cpp | 24 + conformance/src/sections/net.cpp | 16 +- conformance/src/sections/process.cpp | 18 +- conformance/src/sections/random.cpp | 2 +- conformance/src/sections/space.cpp | 2 +- conformance/src/sections/stream.cpp | 14 +- conformance/src/sections/task.cpp | 4 +- conformance/src/sections/time.cpp | 4 +- conformance/src/sections/timeout.cpp | 22 +- conformance/src/sections/version.cpp | 80 + conformance/src/sections/version.cppm | 7 + conformance/src/suite.cpp | 4 + include/openkal.h | 3 + include/openkal/datagram.h | 14 +- include/openkal/env.h | 54 +- include/openkal/exec.h | 25 +- include/openkal/fs.h | 255 ++- include/openkal/memory.h | 26 + include/openkal/net.h | 4 +- include/openkal/process.h | 8 +- include/openkal/random.h | 2 +- include/openkal/space.h | 13 +- include/openkal/stream.h | 16 +- include/openkal/task.h | 30 +- include/openkal/time.h | 2 +- include/openkal/timeout.h | 10 +- include/openkal/types.h | 67 +- include/openkal/version.h | 87 + kit/mcpp.toml | 6 +- mcpp.toml | 2 +- src/datagram.cppm | 8 +- src/env.cppm | 17 +- src/exec.cppm | 8 +- src/fs.cppm | 84 +- src/memory.cppm | 8 + src/net.cppm | 4 +- src/process.cppm | 2 +- src/random.cppm | 2 +- src/space.cppm | 2 +- src/stream.cppm | 7 +- src/task.cppm | 2 +- src/time.cppm | 2 +- src/timeout.cppm | 12 +- src/types.cppm | 27 +- src/version.cppm | 65 + tools/run-conformance.sh | 32 +- 58 files changed, 3431 insertions(+), 408 deletions(-) create mode 100644 .agents/docs/2026-08-28-beyond-static-linking-abi-review.md create mode 100644 conformance/src/sections/version.cpp create mode 100644 conformance/src/sections/version.cppm create mode 100644 include/openkal/version.h create mode 100644 src/version.cppm diff --git a/.agents/docs/2026-08-28-beyond-static-linking-abi-review.md b/.agents/docs/2026-08-28-beyond-static-linking-abi-review.md new file mode 100644 index 0000000..af802b5 --- /dev/null +++ b/.agents/docs/2026-08-28-beyond-static-linking-abi-review.md @@ -0,0 +1,1926 @@ +# openkal beyond static linking: an ABI review, and what each repository does about it + +**Date**: 2026-08-28 +**Scope**: openkal 0.8, its five implementations, and the two consumers on it +**Status**: proposal for review. Nothing here is implemented. + +⚠️ **Sections 0–8 were written before the premise was stated in full, and are +left as they were.** Section 9 is a self-review against the premise that a +program built on this interface may one day be **distributed as a binary** and +meet an implementation it was not compiled against. Three of the earlier +recommendations do not survive that premise; section 9 says which, and why. +A document edited to agree with its own review is one nobody can learn from. + +Every claim marked **measured** was run on one machine on 2026-08-28 against +openkal-musl 0.6.0 (`f5c8524`), openkal-linux 0.6.0 (`98ee440`), +openkal-llvm-runtime 0.3.1, mcpp 2026.8.28.2, target `x86_64-linux-musl`. +Claims not so marked are readings of source or of `SPEC.md`, and one is marked +**unverified**. + +--- + +## 0. Why this document exists + +Four inputs, and the fourth reorders the other three. + +1. **A consumer's third round of reports** on `mcpplibs/openkal-linux#13`. One + of the two items that had resisted two rounds of analysis is now root-caused + to a single line and a single caller. +2. **The question of what a C library must synthesise and what the interface + must supply.** The consumer's symptom was `std::filesystem` disagreeing with + the same source on the host. Part of that is the port's; part is the + interface's. +3. **The interface is early.** No third party has shipped against a frozen + openkal ABI. A redundant declaration can be deleted rather than carried, and + a structure can gain a field. This window is open now and will not reopen: + clause 8 forbids altering a declaration, and clause 5.3 freezes layouts. +4. ⚠️⚠️ **openkal is intended to be more than a set of symbols a program links. + It is intended to become a kernel ABI — a runtime interface crossed by a + trap, a dynamic import, or a service boundary.** + +Item 4 changes which *shapes* are admissible, not which *operations*. Three +shapes in openkal 0.8 are shapes only an in-process library can have. They work +today because every consumer statically links its implementation, and each of +them stops working the moment the interface is crossed rather than linked. +**Section 3 is therefore the part of this document with a deadline**, and the +rest is ordered behind it. + +--- + +## 1. What was measured + +Seven findings. Two are the consumer's open items; five were found by reading +and then confirmed by running. + +### 1.1 `signal(SIGABRT, …)` destroys its caller's stack frame — openkal-musl + +**Measured.** A three-line C program reproduces it. No threads, no `fork`, no +terminal library. + +```c +#include +static void h(int s) { (void)s; } +int main(void) { signal(SIGABRT, h); return 0; } /* SIGSEGV, rip = 0 */ +``` + +Every other signal number returns `SIG_ERR` and exits 0. Only 6 dies. The +register state matches the consumer's report field for field: `rip 0x0`, all +general registers zero but one, and the top of the stack is zero rather than a +return address. + +The cause is `port/src/okm_syscall.c`: + +```c +case SYS_rt_sigprocmask: { + sigset_t* old = (sigset_t*)a3; + if (old) for (unsigned i = 0; i < sizeof *old; i++) ((char*)old)[i] = 0; +``` + +`a4` is the caller's *sigsetsize* — eight bytes on x86_64. This zeroes +`sizeof(sigset_t)`, which is 128. Measured directly: **the caller asks for 8 +bytes and 128 are zeroed, destroying 120 bytes it does not own.** + +Seventeen callers in musl pass an old-set. Sixteen pass a 128-byte `sigset_t`. +**One does not** — `src/signal/sigaction.c:65` declares +`unsigned long set[_NSIG/(8*sizeof(long))]`, which is one word, and reaches it +only when `sig == SIGABRT`. It sits at `-0x20(%rbp)` in a frame of `0x30`, so +the write reaches the saved frame pointer and the return address. `__sigaction` +then returns to zero. + +⚠️ The blast radius is larger than installing a handler. Measured: a plain +**query**, `sigaction(SIGABRT, NULL, &old)`, dies as well, as does +`signal(SIGABRT, SIG_IGN)`. Any program that reads or writes the disposition of +SIGABRT — a test framework's death tests, a crash reporter, a terminal UI +library — ends here. + +**Verified fix.** Honouring `a4` removes it. A copy of the released package was +patched and rebuilt; `signal(SIGABRT, h)` then returns `SIG_ERR` like every +other signal, and a C++ probe of the reported shape (six signals, a global +`std::stack>`, twelve forks, four threads) runs to +completion. + +⭐ **Why the port's own probe did not catch it.** `examples/subprocess` has +thirty-six observations, three of them about `abort`. It contains **no call to +`signal` or `sigaction` anywhere.** It tested whether `abort` ends the program; +it did not test whether a program may touch SIGABRT's disposition. This is the +same shape as the redirection gap of the previous round, whose conclusion was +"seven out of seven was answering a different question". + +### 1.2 The sixty-fifth started program reports `EAGAIN` — openkal-musl + +**Measured.** + +``` +80 posix_spawn without waiting -> #64 fails, errno 11 +80 fork without waiting -> #64 fails, errno 11 +200 x (posix_spawn + blocking wait) -> ok +200 x (posix_spawn + one WNOHANG poll) -> #64 fails, errno 11 +``` + +`okm_syscall.c:392` bounds the started-program table at 64 and releases an entry +only in `wait4`. The last line is the one that matters: a `WNOHANG` poll that +finds the program still running **holds the entry for ever**, and `WNOHANG` only +became usable in 0.6.0. + +This is not yet proven to be the consumer's `EAGAIN`, and the earlier claim that +the port has "exactly three producers of EAGAIN" was wrong: `okm_errno` maps +`kal_err_again` to `EAGAIN` for **every** operation, and openkal-linux's +`translate()` maps every kernel `EAGAIN` to `kal_err_again`. The candidate set is +larger than was stated to the consumer. + +Also measured, which narrows their search: `std::filesystem::copy` of a tree with +no links produces `ec = 0`, and `create_symlink` produces `ENOSYS`, not `EAGAIN`. +Their suspicion that system call 88 was involved can be dropped. + +### 1.3 Enquiry does not resolve a link; opening does — openkal-linux and openkal-musl + +**Measured**, same source built twice, links created by the host: + +| observation | host | openkal-musl 0.6.0 | +| --- | --- | --- | +| `stat("link-to-file")` | `REG` | **`LNK`** | +| `open("link-to-file")` reads | `CONTENT` | `CONTENT` | +| `fs::is_regular_file` | 1 | **0** | +| `fs::file_size` | 8 | **`ENOTSUP`** | +| `fs::exists("dangling")` | **0** | **1** | +| `it->is_directory()` on a link to a directory | 1 | **0** | +| `fs::copy(recursive)` on a tree with one link | **succeeds** | **`ENOSYS`** | + +Isolated: **one symbolic link makes an entire tree uncopyable.** Remove it and +both option sets succeed on both. + +Two causes, and neither is the absence of a link operation: + +- `okm_syscall.c`'s `do_fstatat` **ignores `flag` entirely**, so `stat` and + `lstat` are one call; +- `openkal-linux/src/fs.cpp:188` passes `AT_SYMLINK_NOFOLLOW` **always**, while + `kal_fs_open` at `fs.cpp:127` does not set `O_NOFOLLOW` — the constant + `o_nofollow` is declared in `sys.h:217` and used nowhere. + +⇒ In one program, **opening resolves a link and asking does not.** That is not a +missing operation. It is an operation that reports success having answered a +different question, which is the one outcome this ecosystem's own rule forbids. + +⭐ **And it is fixable with no new atom.** `kal_fs_open` resolves the link and +`kal_fs_file_info` then answers about what was opened. Composing the two gives +`stat` its POSIX meaning. **Measured** on a patched build: every row above +matches the host except the last, and `fs::copy(recursive)` — the form that does +*not* ask for `copy_symlinks` — succeeds. Only `copy_symlinks`, which preserves +the link rather than following it, still needs an operation that does not exist. + +⚠️ But the composition rests on `kal_fs_open` resolving a link, which +`fs.h` does not state. See §4.6. + +### 1.4 Two different files are `equivalent` — openkal-linux + +**Measured.** `fs::equivalent("a.txt", "b.txt")` answers **true with `ec = 0`**. + +`struct kal_node_info` carries `{size, modified_ns, kind, writable}` and no +identity, so `fill_kstat` writes `st_ino = 0, st_dev = 1` for every node. +`hard_link_count` is a constant 1 for the same reason. + +This is the only silently wrong answer in the filesystem surface. `fs::space` and +`fs::create_hard_link` report `ENOSYS`, which a program can act on. + +⭐ Identity is not permission and not links. It presupposes no principal and no +format feature: **no resource can fail to answer whether it is the same resource +as another**, so clause 6.4 does not exclude it. Every format has a form of it — +inode, NTFS file index, APFS object id. + +*(Unverified: WASI's `filestat`/`descriptor-stat` carry `dev` and `ino` and carry +no mode, and WASI has no `path_chmod`. If that holds it is a second capability +interface reaching the same two conclusions — identity in, permission out. Worth +confirming before it is cited.)* + +### 1.5 A property word claimed unconditionally, and one never claimed at all + +`kal_fs_props` is one word per implementation. Two of its four positions are +properties of the **format**, not of the implementation: + +- **`KAL_FS_PROP_CASE_SENSITIVE`** — openkal-linux claims it unconditionally. + On the machine this was written on, `/media/…` is exFAT and `/boot/efi` is + vfat; on a preopen rooted in either, the claim is false. +- **`KAL_FS_PROP_LINKS`** — **no implementation claims it**, yet all three meet + links and report `kal_node_link`. `SPEC.md` clause 11 item 7 says resolution + follows a link "where the property is claimed", so openkal-linux resolves one + while declining to claim it. +- `KAL_FS_PROP_ATOMIC_RENAME` has the same problem for a cross-device rename. + +⭐ The specification already names the right mechanism and does not apply it +here. Clause 6.2: *"A property that varies between the resources of an interface +rather than between implementations cannot be a word… Such a property is +reported by an enquiry taking the resource. `kal_stream_props` is the example."* + +### 1.6 `getpgrp()` answers −38 — openkal-musl + +**Measured.** + +``` +openkal-musl: no operation for system call 121 +getpgrp() = -38 +``` + +System call 121 (`getpgid`) has no case. musl's `getpgrp` is +`return __syscall(SYS_getpgid, 0);` — it does **not** pass through +`__syscall_ret`, because POSIX says `getpgrp` cannot fail. So `-ENOSYS` is +handed to the caller as a process-group identifier. Neither −1 nor an errno: a +value that is wrong and reported as ordinary. + +### 1.7 `rt_sigaction` writes 24 of 32 bytes — openkal-musl + +The old-action is cleared for `sizeof *act` where `act` is a locally declared +three-field structure. `struct k_sigaction` on x86_64 is four fields and 32 +bytes; `mask[2]` is left uninitialised and `__libc_sigaction` copies it to the +caller on the `SIG_DFL`/`SIG_IGN` path. **Read, not measured** — three attempts +on this build read zero, because the stack happened to be zero. + +--- + +## 2. The rule the seven share + +Two sentences cover all of them. + +> ⭐⭐ **A size or a semantic taken from the type declared locally, rather than +> from the contract being answered.** +> §1.1 takes 128 from `sizeof(sigset_t)` when the caller said 8. §1.7 takes 24 +> from a local struct when the ABI says 32. §1.3 takes "do not follow" from one +> call site and "follow" from another. §1.5 takes one answer per implementation +> for a question whose answer is per resource. + +> ⭐⭐ **An operation that reports success having answered a different question.** +> §1.3's `stat`, §1.4's `equivalent`, §1.6's `getpgrp`, §1.5's +> `CASE_SENSITIVE`. Each is worse than a refusal, because a refusal is +> actionable and a wrong answer is not. + +Both are failures of *criteria*, not of implementation. A conformance test that +asserted "the bytes written equal the bytes the caller declared" would have +caught §1.1 and §1.7 together, and one that compared answers against the same +source on a hosted target would have caught §1.3, §1.4 and §1.6 together. §6 +proposes both. + +--- + +## 3. ⚠️⚠️ Shapes that do not survive becoming a kernel ABI + +An interface that is *linked* may pass anything the C ABI permits. An interface +that is *crossed* — a trap, a dynamic import, a call into another address space — +may pass only what the boundary can carry. openkal 0.8 has four shapes of the +first kind. All four work today, and all four stop working on the day the +interface stops being linked. + +### 3.1 Nine of the eleven property words are exported **data**, not operations + +```c +extern const kal_uintptr kal_fs_props; /* and net, datagram, time, + random, task, process, space, + exec */ +``` + +`kal_stream_props` and `kal_terminal_props` are functions and take the resource. +The other nine are objects. + +An exported object cannot be trapped, cannot be intercepted by a service layer, +cannot differ per resource, and — where the implementation is not statically +linked — obliges the consumer to carry a copy relocation or an indirection the +interface does not describe. It also cannot answer differently after the +environment changes, which a runtime interface must be able to do. + +⇒ **Every `*_props` becomes a function.** Where the property is a property of +the resource, it takes the resource. This is one change and it discharges §1.5 +as a side effect. + +### 3.2 Four operations return a pointer into the implementation's memory + +```c +const char* kal_env_arg (kal_uintptr index, kal_uintptr* len); +const char* kal_env_var (const char* name, kal_uintptr name_len, kal_uintptr* value_len); +const char* kal_env_var_at(kal_uintptr index, …); +int kal_fs_preopen(kal_uintptr index, struct kal_dir* out, + const char** name, kal_uintptr* len); +``` + +and `struct kal_preopen` — whose layout clause 5.3 freezes — holds a +`const char*` for the same reason. + +A pointer into the implementation is meaningful only when the implementation is +in the caller's address space. Across a boundary the implementation must copy +into a buffer the caller owns. + +⇒ **Copy-out forms**: the caller supplies a buffer and a capacity, and the +operation reports the length it needed. This is the form +`kal_fs_preopen`'s siblings already use for names elsewhere in the interface. + +### 3.3 Two operations take a function pointer + +```c +int kal_task_start (void (*entry)(void*), void* arg, struct kal_task* out); +int kal_space_start(void (*entry)(void*), void* arg, void* stack_top, + struct kal_process* out); +``` + +A kernel does not call into a program. Starting a context across a boundary is +expressed as a program counter and a stack pointer, not as a callback. + +⚠️ This one is not a mechanical change and this document does not settle it. +`openkal.task`'s present shape is the *reason* clause 7.1 gives for not taking a +stack: an environment that does not allocate stacks separately cannot honour a +request for one. A kernel-ABI form must take the stack, and a bare-metal form +cannot. **The two may not be the same operation.** Recorded as the largest open +question in §8. + +### 3.4 A two-word structure is returned by value + +`struct kal_io_result { kal_uintptr n; int e; }` is returned in two registers by +the C ABI. A trap returns one word. The comment on the structure already states +the constraint it was designed against — "two machine words are returned in +registers on the architectures openkal targets" — which is a statement about a +*call*, not about a *boundary*. + +⇒ Either the transfer operations gain an out-parameter form, or the boundary +specifies how a two-word result is carried. Cheaper than §3.3 and should be +decided at the same time. + +### 3.5 Clause 6.1's presence mechanism is link-time only + +> "An interface that an implementation does not provide is absent as a link-time +> definition, and a consumer that uses it fails to link." + +That is the whole of the optionality mechanism, and openkal-musl depends on it: +twenty-seven weak declarations in `port/src` at 0.6.0, each tested before it is +called. Across a runtime boundary there is no link, and a weak reference has +nothing to be weak against. + +⇒ A runtime form of the same question is needed. The natural one, given §3.1, is +that a property enquiry for an absent interface is itself the absence — but that +inverts clause 6.2's table, which assigns "was an interface used that the +implementation does not provide" to the linker and "how does this implementation +behave within an interface it provides" to a property. **A third row is needed, +and adding it is a specification change, not an implementation one.** + +--- + +## 4. Corrections to make while nothing is owed + +Ordered by cost to a consumer, cheapest first. Items 4.1–4.5 are deletions and +signature changes that are only possible before an ABI is frozen. + +### 4.1 Every property word becomes an operation, and takes its resource where the property has one + +Discharges §3.1 and §1.5 together. + +```c +kal_uintptr kal_fs_props(struct kal_dir d); /* was: extern const object */ +kal_uintptr kal_net_props(void); +… +``` + +`CASE_SENSITIVE`, `ATOMIC_RENAME` and `LINKS` become answerable honestly; today +none of the three is. + +### 4.2 `struct kal_node_info` gains identity, and `size` becomes 64-bit + +```c +struct kal_node_info { + kal_u64 size; /* was kal_uintptr: 32-bit hosts truncate a large file + silently, while kal_fs_seek is already kal_i64 */ + kal_u64 modified_ns; + kal_u64 volume; /* identity, with node. Two nodes are the same node + when both words agree. Zero in both denotes that + the implementation does not distinguish, which a + caller must not read as "the same". */ + kal_u64 node; + int kind; + int writable; +}; +``` + +Discharges §1.4. ⚠️ The `size` change is not cosmetic: `kal_uintptr` on a 32-bit +target truncates at 4 GiB with nothing reporting it, while `kal_fs_seek` on the +same target is already 64-bit. The two disagree today. + +### 4.3 A handle should not escape its type + +`kal_uintptr kal_fs_stream(struct kal_file)` returns a raw word where every +other operation carries `struct kal_stream`, and `struct kal_spawn_streams` +holds three raw words for the same reason. These are the two places a handle +crosses between interfaces, and they are exactly the places the type is dropped. + +The zero-means-two-things defect already recorded against `kal_spawn_streams` — +zero denotes "inherit" while `kal_stdin()` legitimately answers zero — is a +consequence of the same untyping, and the note in `process.h` says it cannot be +repaired because clause 8 forbids altering the declaration. **While nothing is +owed, it can.** + +### 4.4 Delete `kal_fs_open_file` + +`fs.h` states in terms that the two-flag form "cannot express three conditions a +C library must express", and `kal_fs_open` replaces it. openkal-linux defines it +in terms of the newer form. It is a compatibility remnant of an interface that +has no compatibility to keep. + +Ninety names become eighty-nine. Whether other pairs qualify is a question this +document asks and does not answer: a sweep for "operation defined in terms of +another operation of the same interface" should be run before the ABI settles. + +### 4.5 `kal_preopen` and the environment operations gain copy-out forms + +Discharges §3.2. `struct kal_preopen`'s frozen layout is the one that must +change first, because clause 5.3 will hold it afterwards. + +### 4.6 Links: two operations inside `openkal.fs`, not a new interface + +⚠️ **An earlier draft of this analysis proposed `openkal.link` as an optional +interface. That was wrong by the specification's own reasoning and is withdrawn.** + +Clause 6.2 sorts variability three ways: an operation an *implementation* may +lack becomes its own interface; a property that varies between *implementations* +becomes a word; a property that varies between the *resources* of one +implementation can be neither, and is answered by an enquiry taking the resource. + +Clause 11 item 7 places links in the third row: *"whether a filesystem has links +is a property of the format rather than of the environment."* The same +openkal-linux succeeds on ext4 and fails on vfat. Clause 6.4's own worked example +— positioning, which "succeeds for a regular file and fails for a pipe", and +which "accordingly belongs to `openkal.fs`" — is the same shape. + +⇒ `kal_fs_link_create` and `kal_fs_link_read` belong to `openkal.fs`, and their +availability is answered by 4.1's per-resource `kal_fs_props`. That enquiry is +what makes the arrangement legal: clause 6.2 objects to an operation that is +present and always fails **because the caller cannot tell**, and a per-resource +property tells it. + +Two facts the divergence table must carry, neither of which is a Linux fact: + +- Windows requires `SeCreateSymbolicLinkPrivilege` or developer mode, so the + operation **exists and may still fail** — which a compile-time arrangement + cannot express and a runtime error can; +- Windows distinguishes a file link from a directory link at creation, and POSIX + does not, so **a directory link to a target that does not yet exist cannot be + created there.** + +And the sentence clause 11 item 7 already contains — that resolution follows a +link — must move into `fs.h`, beside `kal_fs_info` and `kal_fs_open`, where an +implementer reads it. It is in the specification and in neither header, which is +why two call sites in one file chose opposite directions. + +### 4.7 Permissions stay out, and the substitute is recorded + +⚠️ **A `kal_fs_restrict` operation was proposed in the same earlier draft and is +also withdrawn.** Clause 11 item 6 is stronger than it was given credit for, and +this measurement is why: + +``` +exFAT volume, this machine + created: -rwxr-xr-x speak:speak + chmod 0600 -> exit 0 <- reported success + after: -rwxr-xr-x speak:speak <- nothing happened +``` + +Mode is not stored by FAT, exFAT or ISO9660, and NTFS stores an access-control +list rather than a mode. On this machine the mount options say it outright: +`/boot/efi` is `vfat fmask=0077,dmask=0077`, the exFAT volume is +`uid=1000,gid=1000,fmask=0022,dmask=0022`, and the ntfs3 volume is +`uid=1000,gid=1000` — so on the first two the whole of the mode, and on all +three the owner, is a mount parameter and not data on the medium. Consequently +every file on such a volume reports the same mode. Permission is not a property of the +format and not a property of the application: it belongs to the kernel's +access-control layer, which openkal does not have, because it is a capability +interface and a capability interface has no principal for a permission to name. + +`kal_node_info.writable` — one boolean — is not a simplification of a mode word. +It is the **intersection** of what these formats store. + +⇒ The three answers a program actually has, none of which is a mode: + +1. against another part of the same program — the capability already does it; + a handle not given cannot be reached; +2. against another user of the machine — the **environment's** responsibility; + the party that starts the program supplies a private preopen; +3. against an untrusted location — **encrypt the contents**. This is the only + one that is an application-layer answer, and it is the only one that holds on + exFAT, on a container overlay, and under `CAP_DAC_OVERRIDE`. + +⇒ `SPEC.md` item 6 should carry these three sentences. Today it records the +refusal without recording the substitute, so each consumer rediscovers it. + +--- + +## 5. Per repository + +### openkal + +| | change | §ecti | breaks ABI | +| --- | --- | --- | --- | +| 1 | property words become operations; fs takes a `kal_dir` | 4.1, 3.1 | yes | +| 2 | `kal_node_info`: identity, 64-bit size | 4.2, 1.4 | yes | +| 3 | typed handles at `kal_fs_stream` and `kal_spawn_streams` | 4.3 | yes | +| 4 | delete `kal_fs_open_file`; sweep for other superseded forms | 4.4 | yes | +| 5 | copy-out forms for `env` ×3 and `kal_fs_preopen`/`kal_preopen` | 4.5, 3.2 | yes | +| 6 | link resolution stated in `fs.h`; `kal_fs_link_{create,read}` | 4.6, 1.3 | additive | +| 7 | item 6 records the substitute for permissions | 4.7 | no | +| 8 | a runtime form of clause 6.1 | 3.5 | specification | +| 9 | `kal_task_start` / `kal_space_start` across a boundary | 3.3 | open | + +Items 1–5 are the ones the open window exists for. They should land as one +version, because five separate ABI breaks cost five migrations and one costs one. + +### openkal-linux + +- claim `KAL_FS_PROP_LINKS` where the resource has links, per 4.1; stop claiming + `CASE_SENSITIVE` unconditionally (§1.5); +- supply identity in `fill_info` from `st_dev`/`st_ino` (§1.4); +- state, at `kal_fs_open` and `kal_fs_info`, which of the two resolves a link and + why — the constant `o_nofollow` is declared and unused, which reads as an + intention nobody carried out (§1.3); +- implement `kal_fs_link_{create,read}` on `symlinkat`/`readlinkat`. + +### openkal-macos + +- the same four, over `symlinkat`/`readlinkat` and `st_dev`/`st_ino`; +- ⚠️ it is the row where the fork composition already diverges + (`kal_task_current` answers a new value in a copy), so it is the row where + §3.3's decision must be tested, not the Linux one. + +### openkal-windows + +- identity from `FILE_ID_INFO` (`GetFileInformationByHandleEx`); +- links via `CreateSymbolicLinkW` and `FSCTL_GET_REPARSE_POINT`, with the two + divergences of §4.6 recorded rather than smoothed; +- it is the row that already declines `openkal.space`, so it is the row that + demonstrates whether §3.5's runtime absence form is adequate. + +### openkal-musl + +Independent of everything above, and shippable first: + +1. ⚠️ **`SYS_rt_sigprocmask` honours `a4`** (§1.1). One line. Highest priority in + the ecosystem: it is a crash, it is reached by ordinary C++ programs, and the + fix is verified. +2. `SYS_rt_sigaction` takes its size from `struct k_sigaction`, not from a local + declaration (§1.7). +3. `do_fstatat` composes the follow form from `kal_fs_open` + + `kal_fs_file_info` (§1.3). **Verified**: this alone brings six of seven + `std::filesystem` answers into agreement with the host and makes + `fs::copy(recursive)` succeed on a tree containing a link. It should be + written against 4.6's sentence rather than against openkal-linux's behaviour. +4. a case for `SYS_getpgid` answering the identity `getpid` reports, so + `getpgrp` stops returning −38 (§1.6); +5. the started-program table: release an entry for a program that has ended, or + state the bound of 64 in the README beside the 1024 descriptors and 512 open + descriptions that are already stated (§1.2); +6. a case for `SYS_membarrier` returning `ENOSYS` with the reason beside it. The + call comes from musl's `pthread_create` calling `__membarrier_init`, **whose + result is assigned to nothing**; it is trace noise and nothing else. One of + the six numbers the consumer reported is a false alarm, and the cost of that + falls on them. +7. ⭐ a probe that calls `sigaction` for every signal number, in all three forms — + install, ignore, query — and admits only two answers: success, or −1 with + `ENOSYS`. The third answer, which is what happens today, is that the process + no longer exists. + +### openkal-llvm-runtime + +No change required by any item above. It is, however, the layer at which the +criterion of §6.2 is cheapest to run, because it is the layer that has libc++. + +### openkal-opensbi, openkal-uefi + +Neither provides `openkal.fs` — `openkal-uefi` has no `kal_fs` name at all, and +`openkal-opensbi` deliberately defines no `kal_fs_props` so that clause 6.1 +reports the absence at the link. **They are therefore not in the denominator for +links or identity**, and an earlier version of this analysis wrongly cited them +as the reason links must be optional. The reason is a future format-limited +implementation, not these two. + +⚠️ They are, however, the rows that §3.3 and §3.5 must not break. A bare machine +has no boundary to cross and no loader to negotiate with; a change made for the +kernel-ABI case that costs the bare-metal case its static form has traded the +whole premise. + +--- + +## 6. Criteria + +Two, and each would have caught a group of §1 rather than one item. + +### 6.1 A written byte count equals the declared byte count + +For every operation that fills a caller's buffer, assert that the bytes written +equal the bytes the caller said it owned. §1.1 and §1.7 are one criterion apart. + +In openkal-musl this is cheap: a probe places a canary after an object of the +size musl declares, calls through, and counts leading zeros. Measured today, that +probe reports `120 bytes clobbered beyond the object the caller owns`. + +### 6.2 The same source, two targets, compared field by field + +A test that builds one program for a hosted openkal target and for the host's own +toolchain, over a tree it creates itself — a file, a directory, a link to each, +a dangling link — and asserts that `stat`, `lstat`, `readlink`, `fs::exists`, +`fs::is_regular_file`, `fs::file_size`, `fs::equivalent` and `fs::copy` **agree +field for field**. + +⭐ This criterion needs no ability to *create* a link, so it can be added before +4.6 and keeps its value afterwards. It catches §1.3, §1.4 and §1.6 at once, and +it is a criterion a consumer can contribute — which is worth more than one the +implementer writes, because it does not inherit the implementer's assumptions. + +--- + +## 7. Sequencing + +1. **openkal-musl §1.1**, alone, released immediately. It is a crash with a + verified one-line fix and it blocks a consumer today. +2. **openkal-musl §1.3, §1.6, §1.7, and criterion 6.1.** No interface change. +3. **openkal: items 1–5 of §5 as one version.** One ABI break, not five. +4. **The three implementations adopt it**, and criterion 6.2 goes into whichever + repository can run libc++. +5. **Links (4.6)** after the per-resource property exists, because they depend on + it for legality. +6. **§3.3, §3.4 and §3.5** — the kernel-ABI questions — as a separate document. + They are not fixes; they are a design, and the design has a prerequisite this + document cannot supply: what the boundary actually is. + +--- + +## 8. What this document does not settle + +1. ⚠️⚠️ **What "kernel ABI" means concretely.** A trap, a dynamic import and a + call into a service are three different boundaries with three different sets + of admissible shapes. §3 lists what fails under *any* of them; it cannot list + what is required until one is chosen. **This is the prerequisite for + everything in §3 except 3.1 and 3.2, which fail under all three.** +2. **Starting a context across a boundary** (§3.3). The present shape is the one + clause 7.1 argued for. A kernel-ABI shape needs a stack and a program counter. + Whether these are one operation with a property, two operations, or two + interfaces, is undecided. +3. **Whether the consumer's `EAGAIN` is §1.2.** Their `ec.value()`, `path1`, + `path2` and `what()` are still needed, plus one `strace` to tell a kernel + `EAGAIN` from one this layer manufactured. +4. **The consumer's remaining test failures.** Their permission-bit group is + §4.7's third answer and is theirs to change; the rest was not reproduced here. +5. **Whether any other superseded operation exists besides `kal_fs_open_file`** + (§4.4). A sweep is proposed and has not been run. +6. **The WASI comparison** cited in §1.4 is unverified. + +--- + +## 9. Self-review: the premise is binary distribution, not a kernel ABI + +Sections 0–8 read "kernel ABI" as *the interface is crossed rather than linked*. +The premise is stronger: **a program built against openkal may one day be +shipped as a binary and meet an implementation it was not compiled against.** +Crossing a boundary is a property of one call. Binary distribution is a property +of the whole relationship, and it constrains version skew, discovery, +extension and testing — none of which sections 0–8 address. + +Twelve findings. Three overturn a recommendation this document already made. + +### 9.1 ⚠️⚠️ The document proposes a cleanup where a mechanism is needed — and one mechanism replaces three of its proposals + +§4.2 adds fields to `kal_node_info` "while nothing is owed". That is correct for +today and answers nothing about tomorrow: a program compiled against a 48-byte +`kal_node_info` calling an implementation that writes 24 bytes reads its own +uninitialised stack, which is §1.1 again with the roles reversed. + +The window is worth spending on a **shape**, not on a field. The shape is the +one `statx` uses: + +```c +int kal_fs_info(struct kal_dir base, const char* name, kal_uintptr len, + kal_uintptr want, /* which answers the caller needs */ + struct kal_node_info* out, /* whose first field is its size */ + kal_uintptr* got); /* which answers were in fact given */ +``` + +⭐ **This single shape subsumes three separate proposals in §4.** Extension: +the implementation writes no more than `out->struct_size` and reports what it +filled. Per-resource variability (§4.1, §1.5): "does this resource have links, +is it case-sensitive" is answered by the same `got` word, per call, on the +resource the caller named. Availability of a link operation (§4.6): the same. +And it is cheaper — an implementation need not compute an identity nobody asked +for. + +⇒ **§4.1, §4.2 and part of §4.6 should be withdrawn and replaced by this.** +The per-resource `kal_fs_props(kal_dir)` of §4.1 is still needed for properties +that are not about a node, but the node-level ones move here. + +### 9.2 ⚠️⚠️ Nothing in the ecosystem has the shape the premise requires, and the criterion cannot be constructed today + +openkal's conformance CI is a matrix of `{os, toolchain, implementation}`: + +``` +ubuntu-24.04 gcc@16.1.0 openkal-linux full,optional +ubuntu-24.04 llvm@22.1.8 openkal-linux full,optional +macos-14 llvm@20.1.7 openkal-macos full +windows-2022 llvm@20.1.7 openkal-windows full +``` + +**Every row recompiles.** There is no row in which one artifact meets a second +implementation, and §6.2's criterion — same source, two targets — does not test +that either. The property binary distribution *promises* is: **the same binary, +two implementations of one target.** + +⚠️ And it cannot be written today, because **there is exactly one implementation +per target.** Late binding has no second party. Worse, the choice is made at +dependency resolution and compiled in — openkal-musl names +`openkal-linux = { features = ["standalone"] }` in its manifest — so there is no +artifact in this ecosystem whose implementation is late-bound. The property is +not merely untested; **no artifact of that shape exists to test.** + +⇒ Before the ABI is broken, one of two things must happen: a second +implementation for one target (an instrumented pass-through over openkal-linux +is enough, and would be cheap), or an explicit statement that binary +distribution is unverified. This ecosystem's recorded failure mode is the second +without the statement — every repository's CI substitutes the working tree, and +the published form was found to differ from the development form only after +eight packages had shipped. + +### 9.3 ⚠️⚠️ Clause 3.2 forbids the one primitive a late-bound consumer needs, and clause 6.2 contradicts a trap ABI + +§3.5 said "a third row is needed". The sharper statement is that two existing +clauses now conflict with the premise. + +**Clause 3.2**: *"The set is therefore closed at `openkal.abort`, +`openkal.stream` and `openkal.memory`, and a later version shall place a new +interface outside it even where every implementation that exists at the time +would satisfy it."* + +A late-bound consumer must ask **before calling** whether an interface is there. +Under a link that question is the linker's. Under a trap there are no symbols, +so it must be an operation — and an operation every implementation provides is +a core operation, which clause 3.2 forbids adding. + +**Clause 6.2**: *"An operation that is present and always fails is a defect; the +remedy is that its absence be expressed by its absence."* Under a trap, absence +*can only* be expressed as a returned error. So the rule as written excludes the +arrangement the premise requires. + +⇒ These are not gaps to fill later. They are two rules that the premise +falsifies, and the specification must either qualify them or record that the +distributed form is governed by a different clause. §3.2's reasoning — that a +wrongly-placed core interface excludes a bare machine from conformance — still +holds and is not being argued against; what is being argued is that a +*negotiation* operation is not an interface in that sense. + +### 9.4 ⚠️ Clause 6.5 inverts, and the document never mentions it + +*"An operation whose availability is decided by how the artifact is produced is +not reported at run time."* `openkal.exec` is the case: memory a program may +execute is granted only to an artifact carrying a signed declaration, so the +interface is granted or withheld at dependency resolution. + +Under binary distribution the artifact is produced **once, by the producer, for +every environment**. The consumer no longer resolves it; the producer decided +for them, possibly years earlier and for a different system. The clause's own +justification — *"a path no artifact takes is a path nothing has verified"* — +argues for compile-time resolution precisely because the artifact is built for +its environment. Remove that assumption and the argument reverses. + +⇒ Either `openkal.exec` is not distributable, or 6.5 needs a distributed form. +Not decided here; recorded because §3 omitted it entirely. + +### 9.5 ⚠️ The artifact must carry its own floor, and nothing does + +Clause 5.2 grew the error set by five values in 0.5. A program compiled against +0.8 that distinguishes `kal_err_not_found` from `kal_err_invalid` gets +`kal_err_invalid` for both from a 0.4 implementation — **a wrong answer, not a +refusal**, and precisely the shape §2 names. + +Today the floor lives in `mcpp.toml`. A manifest does not travel with a binary. +⇒ A distributed artifact must declare the interface versions it requires in the +artifact itself — an ELF note, or a first call that states the requirement and +is refused — so that skew is a refusal at load rather than a wrong answer at +run. §4 has no item for this and should. + +### 9.6 ⚠️ A binary-distribution defect that already exists: no operation reports the mapping granularity + +`openkal.memory` is `kal_alloc` and `kal_free` and nothing else. `exec.h` refers +to *"the environment's page granularity"* and does not expose it. **No operation +anywhere in openkal reports a page size.** + +So openkal-musl hardcodes it — `okm_start.c`: `libc.page_size = 4096`, and the +synthetic auxiliary vector carries `AT_PAGESZ = 4096`. Correct for a build aimed +at a known machine. **Wrong on any machine with 16 KiB or 64 KiB pages**, which +is exactly what a distributed binary meets: Apple Silicon, aarch64 servers +configured for 64 KiB, ppc64le. + +⇒ A new §1 item, and an argument for an enquiry rather than a constant. It is +also the clearest small example of the whole premise: a value that is a property +of the *build* today and must become a property of the *run*. + +### 9.7 §3.1 undercounts + +Ten exported data objects, not nine: the nine `*_props` plus +`extern const kal_uintptr kal_timeout_granularity_ns`. The tenth is the same +defect and the same fix. + +### 9.8 `struct kal_endpoint` freezes a needless per-target difference + +`kal_uintptr addr_len` holds a value that is 4, 16 or 20. On a 32-bit target the +frozen layout differs from the 64-bit one for no reason the interface needs. +`kal_u32` costs nothing and removes one axis from the distributed form. Same +window as §4. + +### 9.9 Clause 7.2 does not require a forged handle to be refused + +*"A handle occupies one machine word and is opaque… A handle shall be meaningful +in the context of the caller that obtained it."* That is a statement about +**scope**, not about **validity**. Nothing requires that a word the caller never +obtained be refused rather than acted upon. + +Under a static link this is academic. Under a kernel ABI it is the security +boundary. openkal-linux happens to be safe — `handle.h` packs an index with a +generation and `unpack` rejects a mismatch — but it is safe by choice, not by +requirement, and the other two implementations were not examined for it. + +⇒ Clause 7.2 needs one sentence: an implementation shall refuse a word that is +not a handle it issued, and shall not treat it as one it did. + +### 9.10 The §4.4 sweep is now run, and it found one + +Searching every implementation for an operation defined in terms of another +operation of the same interface returns exactly one supersession: +`kal_fs_open_file`, which builds a flag word and calls `kal_fs_open`. +`kal_timeout_accept` calling `kal_net_accept` after a bounded wait is +composition across two interfaces, which is the arrangement working. So §4.4's +open question closes: **one deletion, ninety names to eighty-nine.** + +### 9.11 One thing survives late binding, and it survives by accident + +openkal-musl's twenty-seven weak declarations were written for bare metal, where +an absent interface must not break the link of a program that never calls it. +Under dynamic linking a weak undefined symbol resolves to zero, and the port +already tests every one before calling — with a CI assertion over eleven names +and a required interface as the control. + +⇒ **This is the only part of the optionality mechanism that already works under +the premise**, and it works because it was written for the opposite extreme. +Worth stating in §3.5, which presently reads as though nothing survives. + +### 9.12 ⚠️⚠️ §7's sequencing is wrong + +§7 puts the ABI break (items 1–5) at step 3 and the boundary questions at step 6, +"as a separate document". Under this premise that spends the one window on a +design the boundary has not yet constrained — and §9.1 has already shown the +boundary changes what the break should contain. + +**Corrected order:** + +| | | why | +| --- | --- | --- | +| 1 | openkal-musl §1.1 alone, released now | a crash, one line, verified, blocks a consumer | +| 2 | openkal-musl §1.3, §1.6, §1.7, §9.6, criterion 6.1 | no interface change | +| 3 | **decide the boundary far enough to fix the enquiry shape** (§9.1) and the negotiation question (§9.3) | these decide what the break contains | +| 4 | **build the second implementation** for one target (§9.2) | otherwise the break cannot be verified | +| 5 | one ABI break: §9.1's shape, §4.3, §4.4, §4.5, §9.5, §9.7, §9.8, §9.9 | one migration, not five | +| 6 | links (§4.6) over the new enquiry; three implementations adopt | depends on 5 | + +Step 4 is the one this ecosystem is most likely to skip, and §9.2 is the reason +it must not be. + +--- + +## 10. What the self-review does not settle + +1. **Which boundary.** Unchanged from §8.1, and now load-bearing for the + sequencing rather than only for §3. +2. **Whether `openkal.exec` is distributable at all** (§9.4). +3. **Whether a negotiation operation can exist without reopening clause 3.2** + (§9.3). A trap-number bootstrap degrades — an unknown number answers + `ENOSYS` — but a dynamic-symbol form has no such fallback for the query + symbol itself. +4. **What the second implementation of §9.2 should be.** An instrumented + pass-through over openkal-linux is the cheapest thing that makes the property + testable; whether it is worth maintaining is a judgement this document does + not make. +5. **Whether a distributed artifact declares its floor as a note or as a call** + (§9.5). A note is checkable by a loader and invisible to a trap ABI; a call + is the reverse. + +--- + +## 11. Design, restated from first principles + +Sections 0–10 were repair. This section is design, under four statements of +intent that arrived after them and that change several answers: + +- openkal is to be **compatible, general, simple and elegant**; +- **static linking now, a runtime ABI later**, and the second must not be a + break of the first; +- ⚠️⚠️ **no operating system decides how openkal is designed.** openkal states a + model; others implement it and use it; +- the design is to be **good for the implementer and good for the user**, and + where those pull apart the pull is itself the thing to design against; +- there is **no compatibility burden today**. The parties are openkal, the + implementations this community maintains, and one C library. + +Three of this document's own recommendations do not survive that, and one of its +findings was simply wrong. + +### 11.0 ⚠️ §9.2 was wrong + +§9.2 said the binary-distribution criterion "cannot be constructed today, +because there is exactly one implementation per target". That conclusion rested +on an assumption that was not checked: that an implementation must be linked in. + +**mcpp supports `kind = "shared"`.** An openkal implementation can be a shared +object, a consumer can be linked against it dynamically, and the pairing can +then be changed without recompiling the consumer. The criterion is constructible +now, and §11.7 states it. + +⭐ The failure was the one this document names in §2 — a conclusion drawn from +the shape of what exists rather than from what the tools can express. It was +reached without reading mcpp, which is one command away. + +### 11.1 Five rules, each with the check that enforces it + +The intent above is not directly checkable. These five are. + +**R1 — No environment's shape.** +*Check*: for each operation, name two environments that satisfy it differently. +If satisfying it on the second requires simulating the first, the shape is the +first environment's and the operation is wrong. +*What it catches today*: §11.2 (one page size is Linux's shape; Windows has +two), §11.5 (`dev`/`ino` is Unix's shape), §11.6 (an ELF note is one object +format's shape). + +**R2 — Everything an implementation reports about itself is an operation.** +Bits go in a property word; a magnitude gets an operation of its own; anything +that varies between resources takes the resource. +*Check*: the surface contains no exported data object. +*What it catches today*: ten data objects — nine `*_props` and +`kal_timeout_granularity_ns` — against two that are already operations +(`kal_stream_props`, `kal_terminal_props`) and one that already is +(`kal_time_monotonic_granularity`). **The interface already contains both +spellings of the same idea.** One rule removes the inconsistency, makes every +report boundary-agnostic, and lets an answer differ per resource where it must. + +**R3 — A value a consumer would otherwise fix at build time must be obtainable +at run time.** +*Check*: grep a consumer for constants describing the environment. Each is +either a fact about the target's ABI (word size, endianness — legitimately +fixed) or a fact about the machine (granularity, a bound — a gap). +*What it catches today*: `libc.page_size = 4096` and `AT_PAGESZ = 4096` in +openkal-musl's `okm_start.c`, and `OKM_PAGE` in `fill_kstat`. + +**R4 — A bound is part of the contract or it is a defect.** +A limit a caller cannot learn produces a failure on an operation that has +nothing to do with it. +*Check*: every fixed-size table in an implementation is either reported by an +operation or documented with the error it produces when exhausted. +*What it catches today*: §11.3. + +**R5 — The interface reports what its own operations need, not what the machine +has.** +*Check*: for each reported value, name the operation of this interface that a +caller uses it for. If there is none, it is a fact about the environment that +this interface has no business carrying. +*What it catches today*: it is why §11.2 reports a granularity and not a page +size, and why it does not report a protection granularity. + +### 11.2 The granularity, designed rather than patched + +**Start from the question, not from the value.** Who needs it? + +| asker | needs | +| --- | --- | +| `openkal.exec` | the alignment an executable region has — already stated as "the environment's page granularity or coarser", and **already the implementation's job**, so the caller does not need the number | +| `kal_alloc(size, align)` | the caller states an alignment; the implementation satisfies it. The caller does not need the number | +| a C library | `sysconf(_SC_PAGESIZE)`, `getpagesize()`, `st_blksize`, and rounding before a mapping request | + +⇒ **Only the third is real**, and it is real not because openkal's operations +need the number but because a C library's own standard obliges it to answer. +That is a legitimate reason and a narrower one, and it argues for the smallest +honest answer rather than a memory-parameters interface. + +**Is it one value? R1 says check a second environment.** + +| environment | allocation granularity | protection granularity | +| --- | --- | --- | +| Linux, macOS | 4 KiB / 16 KiB / 64 KiB | the same | +| **Windows** | **64 KiB** (`VirtualAlloc`) | **4 KiB** | +| a machine with no memory management unit | none | none | + +⚠️ **A design derived from Linux reports one number and is wrong on Windows.** +This is the smallest complete demonstration of R1 in the whole document. + +**The design.** One operation, one number, defined so that it is always safe: + +```c +/* openkal.memory, core. + * + * The quantum this environment allocates and protects memory in. An address + * and a length that are multiples of it are acceptable to every operation of + * this specification that takes memory; a smaller quantum may or may not be. + * + * An implementation with more than one such quantum reports THE COARSEST, so + * that a caller which rounds to this value is never wrong. An environment with + * no such quantum reports 1, which is the same statement: every address and + * every length is acceptable. */ +kal_uintptr kal_memory_granularity(void); +``` + +- ⭐ **One number and no explanation of when it applies.** Windows answers + 64 KiB; Linux answers its page size; a bare machine answers 1. A caller that + rounds to it is correct everywhere, and there is no second number to get + wrong. +- ⭐ **The name is not "page".** "Page" is an operating system's word for a + mechanism openkal does not have. The value is a granularity, and that is what + it is called (R1). +- ⭐ **No protection granularity is reported**, because openkal has no operation + upon a mapping's protection — the divergence table already records `mprotect` + as absent. Reporting a number no operation of this interface can act upon + would be reporting a fact about the machine (R5). +- ⭐ **Cheap for the implementer.** A constant is a legal answer, and 1 is a + legal answer. Nothing has to be discovered, cached or invalidated. + +⚠️ **The honest cost, stated rather than hidden.** On Windows a C library above +this reports `_SC_PAGESIZE` as 64 KiB, which is coarser than the machine's page. +A program that uses it to *align* is correct. A program that uses it to *size a +buffer* allocates sixteen times what it needed. openkal chooses coarse-and-always-correct over exact-and-sometimes-wrong, and the divergence table says so. +The alternative — two operations — moves the choice to every caller, and most +callers will pick wrong. + +**What the consumer does.** openkal-musl deletes both 4096s and answers +`sysconf(_SC_PAGESIZE)`, `AT_PAGESZ` and `st_blksize` from the operation. That is +§9.6 closed. + +### 11.3 The same rule applied: bounds that are invisible today (R4) + +| bound | where | what a caller sees today | verdict | +| --- | --- | --- | --- | +| **maximum name length** | openkal-linux `terminated`'s `char buf[4096]`; `g_cwd[4096]` | `kal_err_invalid` — **indistinguishable from a malformed name**, and openkal says nothing about a bound at all | ⚠️ an openkal-level gap: an operation, or a documented refusal value distinct from "invalid" | +| started programs | openkal-musl, 64 | `EAGAIN` on the next spawn, which the caller cannot attribute | implementation-level; state it, or release entries for programs that have ended | +| execution contexts | openkal-musl, `OKM_CONTEXTS 512` | `kal_abort` with a message naming the cause | acceptable — it fails loudly and says why; state it anyway | +| descriptors, open descriptions | openkal-musl, 1024 / 512 | stated in the README | already right | + +Only the first is openkal's. It is the same defect as the granularity in a +different dress: a build-time constant in the implementation that the caller +cannot learn and cannot distinguish from its own error. + +### 11.4 The enquiry shape, re-derived without naming another system + +§9.1 proposed a `want`/`got` shape and justified it as "the one `statx` uses". +Under R1 that justification is inadmissible even if the shape is right. Here is +the derivation from openkal's own situation: + +1. **What a name refers to is not one fact.** Size and kind exist in every + environment. Modification time, identity and whether the resource has links + exist in some. §1.5 established that these vary *between the resources of one + implementation*, which clause 6.2 says can be neither an interface nor a + word. +2. **A caller rarely wants all of them**, and an implementation that computes an + identity for a caller that asked for a size has done work nobody wanted. +3. **A caller built against a later revision must be able to ask an earlier + implementation and be told what it received** — otherwise it reads its own + uninitialised memory, which is §1.1 with the roles exchanged. + +Three requirements, three mechanisms, and ⭐ **they must not be conflated — +which is what §4.1 and §4.2 did**: + +| requirement | mechanism | answers | +| --- | --- | --- | +| 3 | a size the caller writes into the structure | *how much of this exists on your side* | +| 1 | a word the implementation writes | *which of it is true for this resource* | +| 2 | a word the caller writes | *which of it I need* | + +```c +struct kal_node_info { + kal_uintptr self_size; /* the caller sets it; the implementation writes no more */ + kal_u64 present; /* what the implementation filled */ + kal_u64 size; + kal_u64 modified_ns; + kal_u64 identity[2]; + int kind; + int writable; +}; + +int kal_fs_info(struct kal_dir base, const char* name, kal_uintptr len, + kal_u64 wanted, struct kal_node_info* out); +``` + +⚠️ **The implementer's side must stay one line.** An implementation that always +has everything writes a constant into `present` and ignores `wanted`. If it does +not stay one line, the shape is wrong. This is the "good for the implementer" +half of the intent made into a test. + +⭐ And `size` becomes `kal_u64` rather than `kal_uintptr` here rather than as a +separate item: a file's length is not a property of the caller's word size, and +`kal_fs_seek` already agrees. + +### 11.5 Identity, redesigned away from `dev` and `ino` + +§4.2 proposed `kal_u64 volume` and `kal_u64 node`. Those are one family's words +(R1). What a caller actually needs is to answer *"is this the same object as +that one"* — for `fs::equivalent`, and for cycle detection in a recursive walk. +It needs a **value**, because a walk puts it in a set; a predicate would not do. + +```c +kal_u64 identity[2]; +/* Opaque. Two nodes are the same node when both words are equal. Not + * interpretable, not ordered, and not required to survive a restart. An + * implementation that cannot distinguish nodes does not set the bit for it in + * `present', and a caller is then told that it does not know rather than being + * told that two nodes are the same. */ +``` + +Better for the implementer: an inode pair, a file index, an object id, or +nothing — and nothing is a legal answer. Better for the user: a value with a +stated meaning, and an honest absence in place of §1.4's silent `true`. + +### 11.6 Audit: which of this document's own proposals wore an environment's shape + +R1 applied to §3, §4 and §9. + +| proposal | whose shape | verdict | +| --- | --- | --- | +| `want`/`got` enquiry | named after one system; justified by three properties of openkal | **keep**, re-derived in 11.4 | +| identity as `volume`/`node` | Unix | **replaced** — 11.5 | +| one page size | Linux (Windows has two) | **replaced** — 11.2 | +| version floor as an **ELF note** (§9.5) | ⚠️ **ELF is one object format**; openkal targets Mach-O and PE and intends a trap | **replaced** — the floor must be an *operation* the consumer performs before it uses anything, because every boundary has operations and only one has notes | +| `stat` follows / `lstat` does not | POSIX vocabulary, but the two questions are genuinely distinct | **keep**, renamed: the flag says *resolve* or *do not resolve*, not *stat* or *lstat* | +| `kal_fs_link_{create,read}` | a format's concept, not a kernel's; clause 11 item 7 already reasons this way | **keep** | +| `kal_fs_restrict` | POSIX mode thinking | already withdrawn — §4.7 | +| `kal_io_result` → out-parameters | justified as "a trap returns one word", which is a kernel's fact | **keep on a different ground**: openkal decides which boundaries it intends to be expressible across, and that decision is its own | +| props become operations | openkal's own inconsistency, not anyone's shape | **keep** — R2 | + +⭐ Four of nine wore someone else's shape, and three of those were found only by +applying R1 deliberately. That is the argument for R1 being written down rather +than assumed. + +### 11.7 The ABI test, constructible today (replaces §9.2) + +Because mcpp builds shared libraries: + +1. build openkal-linux with `kind = "shared"`; +2. build one probe program against openkal, linked dynamically, **once**; +3. run that one binary against two shared objects: + - openkal-linux itself; + - ⭐ **a thin interposer over it** that answers a *different* granularity, + fills a *different* `present` word, declines one optional interface, and + reports an *older* version floor; +4. assert the binary behaves as specified against both — including refusing to + run against the older floor. + +⭐ **The interposer is the second implementation §9.2 claimed did not exist, and +it is one file.** It is also the only way to exercise the paths that have no +other producer: negotiation, an absent optional interface at run time, a version +floor that is too low, and a `present` word with a bit clear. + +⚠️ It must be a *separate artifact*, not a build feature of openkal-linux. A +feature is chosen at dependency resolution and compiled in, which is exactly the +arrangement this test exists to escape. + +### 11.8 Static now, runtime later, without a break in between + +The question is not "how do we build the runtime ABI". It is **"what must be +true now so that the runtime ABI is an addition and not a break"**. Three +categories: + +**Decide now, costs nothing later, and is right for static linking anyway:** + +- every report is an operation (R2); +- no operation returns a pointer into the implementation's memory; +- every structure that crosses carries its own size (11.4); +- an environment value is obtained at run time, not fixed at build time (R3); +- every bound is in the contract (R4); +- the version floor is an operation, not an object-format artefact (11.6). + +Each of these makes the static case *better*, not merely future-proof. That is +the test for whether a change belongs in this category: **if it only pays off +later, it is not in this list.** + +**Cannot be decided now, and need not be:** + +- how a context is started across a boundary (`kal_task_start`, + `kal_space_start` take a function pointer, §3.3); +- how a two-word result crosses (§3.4); +- how absence is reported where there is no linker (§3.5, §9.3). + +⭐ **The mechanism that lets these wait is a profile.** Rather than redesigning +`openkal.task` before the boundary is known, the specification names the set of +interfaces a **distributable** artifact may use, and that set initially excludes +the ones whose shapes are not boundary-safe. Static linking keeps all of them +and loses nothing; binary distribution starts with what already works and grows. +Clause 3.3 already names sets (`core`, `hosted`) and says a name "is a shorthand +and confers nothing" — a third name costs one row in that table and defers three +designs that cannot be done well yet. + +**Must not be foreclosed:** + +- a bare machine has no boundary to cross and no loader to negotiate with. Every + change above must leave openkal-opensbi's static, no-negotiation form intact. + A negotiation operation that a bare-metal implementation must *implement* is + acceptable only if answering it is a constant. + +### 11.9 What 11 leaves open + +1. Whether the floor operation (11.6) is per-interface or one for the + specification. Per-interface is more precise and is more to carry. +2. Whether `present` and `wanted` are one word each or one word per interface's + own enumeration. This document assumes `openkal.fs` only. +3. The name-length bound of 11.3: an operation, or a distinct error value, or a + requirement that implementations impose none. The third is the simplest for + the user and the hardest for an implementation with a fixed buffer. +4. Whether the interposer of 11.7 lives in openkal's repository (where the + conformance runner is) or its own. + +--- + +## 12. The interface table, remade + +Two instructions arrived together and they change the same table: the tier +column is wrong, and the table must state **now** which interfaces cross which +boundary. This section does both, and adds the finding that makes the second +tractable: **all fifteen can be made to cross every boundary, and only five +distinct causes stand in the way.** + +### 12.1 ⚠️ `standard` is falsified by this ecosystem's own C library + +Clause 3 defines the middle tier as: + +> *Standard* denotes one an implementation hosting a C library provides. + +openkal-opensbi provides `openkal.abort`, `openkal.stream`, `openkal.memory`, +`openkal.env` and `openkal.time`, and **not** `openkal.fs`, `openkal.process` or +`openkal.task` — deliberately, so that clause 6.1 reports the absence at the +link. And openkal-musl — **a C library** — is built above it, with those three +compiled out by `#ifndef`-guarded macros in its own manifest. + +⇒ A C library hosts, today, above an implementation that provides **not one** +of the three `standard` interfaces. The tier states a fact that is not one. + +The manifest that does this already knows: *"THE TARGET IS A PROXY FOR THE +IMPLEMENTATION, AND AN IMPERFECT ONE."* A package-level convention is already +correcting a specification-level tier, which is the shape of a tier that should +not exist. + +### 12.2 Two tiers, and the resource column already does the rest + +| tier | meaning | +| --- | --- | +| `core` | every implementation provides it. Closed at `abort`, `stream`, `memory` by clause 3.2, which stands | +| `optional` | everything else. Absence is reported by clause 6.1 | + +There is **no enforcement difference today** between `standard` and `optional` — +clause 6.1 treats them identically — so the third tier carried only an advisory +claim, and §12.1 shows the claim is false. + +⭐ The honest classification is already in the table, in the column next to it: +an interface exists where **its resource** exists. Storage, a second image, a +scheduler, a network, an address space that can be copied, entropy, an +interactive stream, a bound upon a wait. The tier column was a second, worse +statement of the resource column. + +### 12.3 `hosted` goes; nothing about an environment is named + +A set name that describes a *class of environment* will be falsified by an +environment nobody had in mind. `hosted` was falsified inside its own ecosystem +within one release (§12.1). + +Clause 3.3's argument for naming sets was that stating a need one interface at a +time "does not scale". Re-examined: there are fifteen interfaces, and a consumer +that needs five names five. That is five lines, not a scaling problem. And the +ecosystem **already** states it per package and per target — openkal-musl's +`OKM_HAS_*` macros are exactly a consumer declaring its required set, in the +place where a convention among consumers belongs. + +⇒ The specification names **no environment sets**. `core` stops being a "set" +and is what it already is: a requirement upon implementations. + +⚠️ One name stays, and it is not about environments — the **boundary marking** +of §12.4. A boundary is a property of a declaration's shape, so a new +environment cannot falsify it. + +### 12.4 The three boundaries, and why one column is not enough + +"Runtime cross-platform" is ambiguous, and the ambiguity produces opposite +answers, so the table needs two columns rather than one. + +| | boundary | what it means | what it forbids | +| **S** | static | the implementation is chosen when the artifact is built and linked into it | nothing | +| **L** | late-bound, one address space | the implementation is a separate object resolved at load; the artifact does not change when the implementation does | a structure whose size the consumer baked in; a report whose value the consumer baked in | +| **X** | crossed | the implementation is in another address space or another privilege level; a call is a trap or a message | additionally: a returned pointer into the implementation; a result wider than one word; a call back into the consumer | + +Returning an internal pointer is **fine at L and fatal at X**. A two-word struct +return is **fine at L and fatal at X**. Conflating the two gives the wrong answer +for nine interfaces. + +### 12.5 The marking, as it would appear in clause 3 + +`L?` and `X?` are the current state. `X after` is the state after the changes +this document proposes. + +| Interface | Resource | Tier | L? | X? | what blocks X | X after | +| --- | --- | --- | --- | --- | --- | --- | +| `openkal.abort` | termination | core | ✓ | ✓ | — | ✓ | +| `openkal.stream` | a byte stream | core | ✓ | ✗ | 2 operations return a two-word result | ✓ | +| `openkal.memory` | a region of the address space | core | ✓ | ✓ | — | ✓ | +| `openkal.env` | the parameters a program receives | optional | ✓ | ✗ | **3 of 5 operations return a pointer into the implementation** | ✓ | +| `openkal.time` | a time source | optional | ✓ | ✗ | a property word is an exported object | ✓ | +| `openkal.random` | unpredictable bytes | optional | ✓ | ✗ | a property word is an exported object | ✓ | +| `openkal.fs` | a directory, an open file | optional | ✓ | ✗ | 2 operations return an internal pointer; a property word is an object; no structure carries its size | ✓ | +| `openkal.process` | a started program image | optional | ✓ | ✗ | a property word is an exported object | ✓ | +| `openkal.task` | an execution context | optional | ✓ | ✗ | a property word; **the entry is worded as a call** | ✓ (12.6) | +| `openkal.exec` | memory a program may execute | optional | ✓ | ✗ | a property word; **clause 6.5 decides availability at production time** | ✓ (12.7) | +| `openkal.terminal` | an interactive stream's mode | optional | ✓ | ✓ | — (its properties are already operations) | ✓ | +| `openkal.net` | a connection, a listener | optional | ✓ | ✗ | a property word is an exported object | ✓ | +| `openkal.datagram` | a message with a boundary | optional | ✓ | ✗ | a property word; 2 operations return a two-word result | ✓ | +| `openkal.space` | an address space, a context in one | optional | ✓ | ✗ | a property word; **the entry is worded as a call** | ✓ (12.6) | +| `openkal.timeout` | a bound upon a wait | optional | ✓ | ✗ | a granularity is an exported object; 3 operations return a two-word result | ✓ | + +⭐ **Fifteen interfaces, thirteen currently blocked at X, and five causes.** + +| # | cause | how many | where | +| 1 | an exported data object | **10** | nine `*_props`, plus `kal_timeout_granularity_ns` | +| 2 | an operation returns a pointer into the implementation | **5** | `kal_env_arg`, `kal_env_var`, `kal_env_var_at`, `kal_fs_preopen`, `kal_fs_list_next` | +| 3 | an operation returns a result wider than one word | **7** | `kal_stream_read`/`write`, `kal_datagram_send_to`/`recv_from`, `kal_timeout_read`/`write`/`recv_from` | +| 4 | an entry is worded as a call | **2** | `kal_task_start`, `kal_space_start` | +| 5 | availability decided at production time | **1 clause** | clause 6.5, `openkal.exec` | + +Causes 1 and 2 are already on this document's change list (R2, §3.2). Cause 3 is +§3.4. Causes 4 and 5 were listed as *undecided* in §8 and §9.4; §12.6 and §12.7 +resolve both, and neither requires a signature to change. + +⚠️ **L is a different and quieter problem.** Every interface operates correctly +at L today. **None of them can evolve at L**, because no structure carries its +size and every property is a value the consumer's copy fixes at load. So the +honest marking for L today is not fifteen ticks — it is *"operates, cannot +evolve"*, and §11.4's `self_size` and R2 are what turn it into a tick. + +### 12.6 `openkal.task` and `openkal.space` cross X, and the change is words + +The C declaration is not the obstacle. `void (*entry)(void*)` is an address and +an argument, and an implementation on the far side of a boundary does not need +to *call* it — it needs to **begin a context at it**. A kernel does that by +setting a program counter and a stack pointer and returning to the caller's +privilege level, which is what `clone` is. + +The obstacle is the specification's verb. And the intent is already the right +one in two places: + +- `space.h`: *"The entry is not required to return, and if it does the context + ends."* +- openkal-linux's own clone trampoline: *"The child of a clone begins on a stack + of its own with no return address, so the transfer cannot be written in C."* + +⇒ Both operations are respecified as: **the entry is an address at which a +context begins. No return address exists. Returning from it ends the context, +and the context's status says that it returned rather than choosing one.** + +Nothing about the declaration changes; `openkal.task` and `openkal.space` become +X-capable; and clause 7.1's refusal to require a stack is untouched, because the +stack argument keeps the status it already has — honoured by an implementation +that allocates stacks and ignored by one that does not, with the caller unable to +observe which. + +⚠️ One consequence to write down: an implementation's own trampoline may still +return (openkal-linux's does, into `clone`). The rule binds the **consumer's** +entry, which must not rely on returning to anything. + +### 12.7 `openkal.exec` crosses X, and clause 6.5 becomes a bit + +Clause 6.5 resolves availability at dependency resolution, on the ground that +*"a path no artifact takes is a path nothing has verified."* §9.4 showed that +under distribution the producer decides for every environment, so the consumer +cannot resolve it. + +⇒ `kal_exec_props()` — an operation, by R2 — carries a position meaning +*executable memory is available to this artifact in this environment*. + +This is not clause 6.2's forbidden shape. 6.2 objects to an operation that is +present and always fails **because the caller cannot tell**; a property the +caller reads first is exactly what tells it. It is the same argument that +legalises the link operations of §11.4, applied to a different partiality. + +⚠️ And 6.5's own worry survives and is answered by §11.7: the interposer can +answer that bit both ways, which is the **only** way the unavailable path gets +exercised at all. Today it is a path nothing has verified because nothing can +produce it. + +### 12.8 The rule this leaves behind + +> ⭐ **An interface is X-capable when: it exports no object; no operation returns +> a pointer into the implementation; no result is wider than one machine word; +> every structure that crosses carries its own size; an entry is an address at +> which a context begins rather than a function that is called; and its absence +> is discoverable by an operation rather than only by a linker.** + +Six clauses, mechanically checkable, and a conformance step can assert the first +four from the surface file and the declarations alone. + +⇒ **New interfaces are designed X-capable from the start**, and the table marks +S/L/X for each. The one openkal already reserves — `openkal.event` — should be +designed against this rule before it is specified rather than after. + +--- + +## 13. One landing + +The instruction is that this is not a sequence of pull requests but **one +change, agreed first and landed together**. Two things follow. + +### 13.1 ⚠️⚠️ One decision is not one publication + +The graph forbids it: + +``` +openkal + └─ openkal-linux · -macos · -windows · -opensbi · -uefi + └─ openkal-musl + └─ openkal-llvm-runtime + └─ consumers +``` + +Every edge is an **exact** version requirement — measured in this ecosystem +already: a manifest saying `openkal-musl = "0.5.0"` resolves to `0.5.0` and +nothing floats up. So the landing is *one decision, one reviewed change set, +and a topologically ordered publication* in which each step is green before the +next begins. + +⚠️ The recorded failure mode is precisely here. A version was published and the +index's `latest` moved before the consumers had been repinned; every clean +environment went red while every development machine stayed green — **because +each repository's CI substitutes the working tree and therefore never resolves +the published form.** + +⇒ Two requirements on the landing, neither of which is a matter of taste: + +1. **Every repository's change is written and reviewed before any is published.** + One branch per repository, all open simultaneously, cross-referenced. +2. ⭐ **A step that resolves the *published* packages must exist and must be the + gate.** Not a job that substitutes working trees. Otherwise the landing is + verified against a graph no user has. + +### 13.2 What must be agreed before code is written + +Fourteen decisions. Each is a yes/no; none is an implementation detail. + +| # | decision | §ection | +| 1 | two tiers; `standard` and the `hosted` set name deleted | 12.1–12.3 | +| 2 | the interface table gains S / L / X marking, normative | 12.4, 12.5 | +| 3 | the six-clause X rule, and new interfaces designed against it | 12.8 | +| 4 | every report becomes an operation; ten data objects go | R2 | +| 5 | `kal_memory_granularity`, coarsest-safe, in `openkal.memory` | 11.2 | +| 6 | five operations gain copy-out forms | 12.5 cause 2 | +| 7 | seven operations gain a one-word result form | 12.5 cause 3 | +| 8 | `kal_task_start` / `kal_space_start` respecified as *begins at* | 12.6 | +| 9 | clause 6.5 becomes a property position on `openkal.exec` | 12.7 | +| 10 | `kal_fs_info` gains `self_size` / `wanted` / `present`; `kal_node_info` gains an opaque identity and a 64-bit size | 11.4, 11.5 | +| 11 | links enter `openkal.fs`; resolution stated in the header | 4.6 | +| 12 | the version floor is an **operation**, not an object-format note | 11.6 | +| 13 | small ABI corrections: delete `kal_fs_open_file`; typed handles at `kal_fs_stream` and `kal_spawn_streams`; `kal_endpoint.addr_len` to `kal_u32`; clause 7.2 gains the forged-handle sentence; a name-length bound | 4.3, 4.4, 9.8, 9.9, 11.3 | +| 14 | the interposer and the shared-library ABI test ship **in this change set** | 11.7 | + +⚠️ **14 is the one most likely to be dropped and the one that must not be.** An +ABI break that lands without an artifact that can be run against two +implementations is a break verified only in the form it is replacing. + +### 13.3 What is *not* in this landing + +- **The C library defects of §1** — `rt_sigprocmask`, `do_fstatat`, `getpgid`, + `rt_sigaction`, the started-program table, the `membarrier` case. None touches + the interface, all are verified, and one is a crash a consumer is hitting + today. ⚠️ **These should not wait for the landing**, and holding them back to + make the landing "one thing" would keep a crash in a released package for the + sake of tidiness. +- **The negotiation mechanism** (§9.3, clause 3.2 versus 6.2). Decision 2 marks + which interfaces are X-capable; discovering *whether an implementation + provides one* without a linker is a separate design, and the marking does not + depend on it. +- **A trap encoding.** Decisions 6, 7 and 8 make the shapes admissible. Which + boundary is actually built, and how a call is encoded on it, is not settled + and is not blocked by this landing. + +### 13.4 The change set, by repository + +| repository | changes | ABI | +| --- | --- | --- | +| **openkal** | decisions 1–13; `SPEC.md` clauses 3, 3.2, 3.3, 5.3, 6.2, 6.5, 7.2, 8, 11; `SURFACE.txt`; every header | yes | +| **openkal-linux** | adopt; supply identity and links; claim properties per resource; state which operations resolve a link; `kal_memory_granularity`; build as a shared object for decision 14 | — | +| **openkal-macos** | adopt; identity from `st_dev`/`st_ino`; links; ⚠️ the row where the fork composition already diverges, so the row where decision 8 is tested | — | +| **openkal-windows** | adopt; identity from `FILE_ID_INFO`; links with the two divergences recorded; ⚠️ the row that reports a **64 KiB** granularity and so the row that proves decision 5 | — | +| **openkal-opensbi** | adopt the reports it must answer with constants; ⚠️ the row that must stay static and negotiation-free | — | +| **openkal-uefi** | adopt likewise | — | +| **openkal-musl** | the §1 defects **ahead of the landing**; then delete the two `4096`s; the `OKM_HAS_*` macros re-derived once the tiers change; the composed follow form; link operations | — | +| **openkal-llvm-runtime** | repin only, unless the interposer lives here | — | +| **interposer** (new) | decision 14: one shared object over openkal-linux answering a different granularity, a different `present`, one interface declined, an older floor | — | + +### 13.5 The three risks that have precedent in this ecosystem + +1. ⚠️ **The publication order.** §13.1. It has gone wrong before, in exactly this + graph. +2. ⚠️ **The bare-metal row has no continuous integration hardware** and is the + row most likely to break silently under a change that assumes a loader. +3. ⚠️ **`OKM_HAS_*` encodes the tier that is being deleted.** openkal-musl's + per-target interface set was written against `standard` versus `optional`; + after decision 1 it must be re-derived from what each implementation actually + provides, and the target is — in its own words — an imperfect proxy for that. + +### 13.6 The fourteen, with the evidence, the cost, and what "no" means + +Ordered so that a decision never precedes one it depends on. **Three are genuine +judgements (7, 9, 12); the rest follow from evidence already in this document.** + +--- + +**1 — Two tiers. `standard` and the `hosted` set name are deleted.** +*Evidence*: §12.1. openkal-opensbi provides none of the three `standard` +interfaces and openkal-musl, a C library, hosts above it. +*Cost*: clause 3, 3.3 rewritten; openkal-musl's `OKM_HAS_*` set re-derived from +what each implementation provides rather than from a tier. +*Against*: the tier had communication value — a newcomer read "standard" as +"you will usually have this". +*Answer*: keep that value as an **informative** sentence naming what +implementations of systems with storage usually provide, marked non-normative, +so it cannot be mistaken for a rule the way the tier was. +*If no*: the specification keeps a claim its own ecosystem contradicts. +⇒ **Recommend yes.** + +--- + +**2 — The interface table gains S / L / X marking, normative.** +*Evidence*: §12.4. "Runtime cross-platform" gives opposite answers for L and X, +so one column would be wrong for nine interfaces. +⚠️ *The subsidiary decision that matters*: the marking is a statement about **the +shape of the declarations**, not a requirement upon implementations. An +implementation cannot violate it; only a declaration can. Without that sentence +the column reads as "implementations must support traps". +*Cost*: near zero — it is derivable from the surface file and can be asserted. +⇒ **Recommend yes, with the sentence.** + +--- + +**3 — The six-clause X rule; new interfaces designed against it.** +*Evidence*: §12.8. Four of the six are mechanically checkable from the +declarations. +*Against*: the sixth clause — absence discoverable by an operation — has no +mechanism yet (§9.3 is undecided). +*Answer*: adopt the first five now, mark the sixth as pending, and apply the +rule to `openkal.event` **before** it is specified rather than after. +⇒ **Recommend yes, five of six effective now.** + +--- + +**4 — Every report becomes an operation. Ten exported data objects go.** +*Evidence*: R2, §3.1, §12.5 cause 1. The interface already contains both +spellings: `kal_stream_props(s)` and `kal_terminal_props(s)` are operations, +`kal_fs_props` and eight others are objects, and `kal_time_monotonic_granularity()` +is an operation while `kal_timeout_granularity_ns` is an object. +*Cost*: the largest mechanical change — ten names across five implementations +and every read site. +*Buys*: evolution at L, crossing at X, and per-resource answers, which is the +fix for §1.5 (`CASE_SENSITIVE` claimed unconditionally, `LINKS` never claimed). +*Against*: a load becomes a call. +*Answer*: every one of these values is read once at startup or once per resource. +⚠️ *Subsidiary*: once `kal_fs_props` takes a `kal_dir`, a program with no +directory in hand cannot ask. That is correct — with no resource there is no +question — but it should be stated, not discovered. +⇒ **Recommend yes.** + +--- + +**5 — `kal_memory_granularity()`, coarsest-safe, in `openkal.memory`.** +*Evidence*: §11.2, §9.6. Windows has two granularities (64 KiB allocation, +4 KiB page) and Linux one, so a design taken from Linux is wrong on Windows. +openkal-musl hardcodes 4096 in two places today and is wrong on any 16 KiB or +64 KiB machine. +*Cost*: one operation; a constant is a legal answer; `1` is a legal answer. +⚠️ *Subsidiary A*: one number or two? One, defined as the coarsest that is always +safe. The price is that `_SC_PAGESIZE` reads 64 KiB on Windows, so a program +sizing a buffer by it over-allocates sixteenfold, while a program aligning by it +is correct. Two numbers move the choice to every caller and most will choose +wrong. +⚠️ *Subsidiary B*: adding an operation to a **core** interface obliges every +implementation, including bare metal. This is not a violation of clause 3.2, +which closes the set of core **interfaces**; clause 8 admits new declarations +within one. Say so in the change, or it will read as a violation. +⇒ **Recommend yes.** + +--- + +**6 — Five operations gain copy-out forms.** +`kal_env_arg`, `kal_env_var`, `kal_env_var_at`, `kal_fs_preopen`, +`kal_fs_list_next` return a pointer into the implementation. +*Evidence*: §3.2, §12.5 cause 2. +⚠️ *Subsidiary*: add a form, or replace? With no compatibility burden, replace — +two forms of one operation is precisely what decision 13 deletes elsewhere. +*Cost to the user*: `kal_fs_list_next` gains a buffer and a copy at the call +site. Real, and small. +⇒ **Recommend yes, replacing.** + +--- + +**7 — ⚠️ JUDGEMENT. Seven operations gain a one-word result form.** +`kal_stream_read`/`write`, `kal_datagram_send_to`/`recv_from`, +`kal_timeout_read`/`write`/`recv_from` return `struct kal_io_result`, two words. +*Evidence*: §3.4, §12.5 cause 3. +⚠️ **This one is fine at L and only fails at X**, and it is the change that costs +the *user* most: `auto r = kal_stream_write(...); if (r.e)` becomes an +out-parameter at every call site. +*An alternative was considered and rejected on evidence*: merge the two words +into one signed word — negative is an error, non-negative is a count. **It loses +the count on failure**, which clause 7.4 and `stream.h` both require ("on +failure, n reports how many bytes were transferred before the failure") and which +openkal-musl actually reads — `okm_poll.c:81` returns `io.n ? io.n : -EAGAIN`. +*The real choice*: +- **now**: the call sites get uglier and the three interfaces are X-ready; +- **defer**: the call sites stay as they are and `openkal.stream`, + `openkal.datagram` and `openkal.timeout` cannot be ticked in the X column — + including `openkal.stream`, which is **core**. +⇒ **My recommendation is to do it now**, on the ground that a core interface +that cannot cross makes the marking of every other interface academic. But this +is the decision where the cost falls on the user rather than on the implementer, +and it is yours. + +--- + +**8 — `kal_task_start` and `kal_space_start` respecified as "begins at".** +*Evidence*: §12.6. The declaration does not change. The intent is already +written twice — `space.h`'s "the entry is not required to return", and +openkal-linux's clone trampoline comment. +*Cost*: words. +*Checked*: on X, `kal_task_start` has no stack parameter and the implementation +allocates — which is what clause 7.1 designed for, so the crossed form is if +anything more natural than the linked one. +⇒ **Recommend yes.** + +--- + +**9 — ⚠️ JUDGEMENT. Clause 6.5 becomes a property position on `openkal.exec`.** +*Evidence*: §12.7, §9.4. Under distribution the producer decides for every +environment, so a consumer cannot resolve availability at dependency resolution. +*Against, and it is a strong objection*: 6.5's own reason is *"a path no artifact +takes is a path nothing has verified"*. Making it a runtime property puts a +branch in every caller that almost no artifact takes. +*Answer*: the interposer of decision 14 can answer that bit both ways, which is +the only way the path is exercised at all — today it is unverified because +nothing can produce it. +⇒ **Recommend yes, but strictly conditional on 14.** If 14 is dropped, this +should be dropped with it, or 6.5's objection stands unanswered. + +--- + +**10 — `kal_fs_info` gains `self_size` / `wanted` / `present`; `kal_node_info` +gains an opaque identity and a 64-bit size.** +*Evidence*: §11.4, §11.5, §1.4. Three requirements, three mechanisms, which +§4.1 and §4.2 wrongly conflated: a size the caller writes (version skew), a +`present` word the implementation writes (per-resource variability), a `wanted` +word the caller writes (do not compute what nobody asked for). +*Buys*: §1.4's silent `fs::equivalent(a,b) == true` for two different files; the +per-resource property of §1.5; evolution at L for the whole interface. +⚠️ *Subsidiary*: is `wanted` worth it? An implementation that always has +everything ignores it — one line — so the implementer's cost is zero, and it is +what lets an implementation skip computing an identity nobody asked for. +*Test to keep*: **if the cheap implementation is not one line, the shape is +wrong.** +⇒ **Recommend yes.** This is the largest semantic change and the one that pays +for the most. + +--- + +**11 — Split in two. 11a: link resolution stated in `fs.h`. 11b: link +create/read enter `openkal.fs`.** +*Evidence*: §1.3, §4.6. +⭐ **11a is the one that matters and it does not depend on 11b.** Measured: with +resolution correct, six of seven `std::filesystem` answers agree with the host +and `fs::copy(recursive)` succeeds on a tree containing a link — with **no link +operation at all**. Clause 11 item 7 already states the rule; it is in neither +header, which is why two call sites in one file chose opposite directions. +11b depends on decision 10 for its legality (availability answered per resource). +⇒ **Recommend yes to both, 11a first and separable.** + +--- + +**12 — ⚠️ JUDGEMENT. The version floor is an operation, not an object-format +note.** +*Evidence*: §11.6, §9.5. An ELF note is one object format's shape; openkal +targets Mach-O and PE and intends a trap. +⚠️ *The judgement*: this and the undecided negotiation mechanism (§9.3) are two +halves of one question — both are "ask something before using anything". Decided +separately they will produce two entry points. +⇒ **Recommend deciding the shape now and the mechanism later**: one operation, +performed before anything else, that both states the floor and reports which +interfaces are present. Whether it is per-interface or one for the specification +is the open part. + +--- + +**13 — Five small corrections, packaged.** +- delete `kal_fs_open_file` — the sweep is run (§9.10); it is the only + supersession in the whole surface, and `fs.h` already says the two-flag form + cannot express what a C library needs; +- `kal_fs_stream` and `kal_spawn_streams` carry `struct kal_stream` rather than + a raw word — these are the two places a handle crosses between interfaces and + the two places its type is dropped, and the recorded "zero means both inherit + and stdin" defect is a consequence; +- `kal_endpoint.addr_len` becomes `kal_u32` — it holds 4, 16 or 20 and today + freezes a per-word-size difference for nothing; +- clause 7.2 gains one sentence: an implementation shall refuse a word that is + not a handle it issued. Today 7.2 constrains **scope**, not **validity**; + openkal-linux is safe by choice (index and generation), not by requirement; +- a name-length bound: openkal says nothing, and openkal-linux's `char buf[4096]` + refuses a longer name as `kal_err_invalid` — the same reading as a malformed + name. +⇒ **Recommend yes, as one package.** No two of them interact. + +--- + +**14 — ⚠️ The interposer and the shared-library ABI test ship in this change set.** +*Evidence*: §11.7, and §11.0 — the earlier claim that this could not be built +was wrong, because mcpp supports `kind = "shared"`. +*What it is*: one shared object over openkal-linux that answers a different +granularity, fills a different `present`, declines one optional interface, and +reports an older floor. One probe binary, built once, run against both. +*Cost*: one file and one CI job. +*What it is the only way to test*: negotiation, an absent optional interface at +run time, a floor that is too low, a `present` bit that is clear, and decision +9's unavailable-executable-memory path. +⚠️ *It must be a separate artifact, not a feature of openkal-linux.* A feature is +chosen at dependency resolution and compiled in, which is the arrangement this +test exists to escape. +⇒ **Recommend yes, and treat it as non-negotiable.** An ABI break that lands +without an artifact runnable against two implementations is verified only in the +form it replaces. + +--- + +**Dependencies**: 11b requires 10. 9 requires 14. 2's X column requires 6, 7 and +8 to be decided. Everything else is independent. + +**If time forces a subset**: 1, 4, 8, 10, 11a, 13, 14 are the ones that improve +the static case on their own merits and are therefore not a bet on a boundary +that has not been chosen. + +--- + +## 14. Decisions taken + +Reviewed 2026-08-29. **All fourteen are accepted.** Four were amended in review, +and one of the amendments replaced this document's own recommendation with a +better answer. The amendments are recorded here rather than folded back into +§13.6, so that what changed and why remains readable. + +### 14.1 The four amendments + +**A. Decision 1 gains a second mechanism: `core`, and everything else optional +*or governed by a feature*.** + +Two tiers, and "optional" is resolved in one of two places rather than one: + +| how an optional interface is resolved | mechanism | example | +| --- | --- | --- | +| the implementation does not provide it | clause 6.1 — an undefined symbol at the link | `openkal.space` on Windows | +| the implementation provides it only to an artifact produced a certain way | a **feature** of the implementation's package, resolved at dependency resolution | `openkal.exec`; `openkal-linux`'s `standalone` | + +This is not a new mechanism — clause 6.5 already describes the second, and +`openkal-linux` already ships a feature that decides which of two forms of +`openkal.task` a program gets. What the amendment does is **stop treating the +two as one thing**. The deleted `standard` tier was an attempt to describe both +with a single word, and it described neither. + +⚠️ Consequence for decision 9: `openkal.exec` is then resolved **both** ways — +by a feature when the artifact is produced, and by a property position when the +artifact is distributed. That is not a contradiction; it is the same question +answered at the earliest time each form of distribution allows. + +**B. Decision 2 gains a design obligation, not only a marking.** + +> **Compile-time portability is the floor, not the goal. Every interface, +> operation and structure is designed so that it can also cross at run time, +> unless a reason is recorded for why it cannot.** + +The S/L/X column therefore has two jobs: it states what is true, and it makes +the exceptions visible. An interface marked "X: no" is carrying a debt, and the +column is where the debt is written down. §12.5's "X after" column is the target +state and every row in it is a tick. + +**C. Decision 4 is not only about boundaries. It is about semantic consistency.** + +The ten data objects are one instance of a larger defect: **one idea with two +spellings**. The surface has five, and four of the fourteen decisions each remove +one without the connection having been stated: + +| one idea | two spellings | removed by | +| --- | --- | --- | +| an implementation reports something about itself | `kal_stream_props(s)`, `kal_terminal_props(s)` are operations; nine `*_props` are objects | 4 | +| a magnitude an implementation reports | `kal_time_monotonic_granularity()` is an operation; `kal_timeout_granularity_ns` is an object | 4, 5 | +| opening a file | `kal_fs_open(flags)` and `kal_fs_open_file(write, create)` | 13 | +| a handle crossing between interfaces | `struct kal_stream` everywhere; a raw word at `kal_fs_stream` and in `kal_spawn_streams` | 13 | +| the result of a transfer | `struct kal_io_result` in three interfaces; `int` and an out-parameter everywhere else | 7 | + +⇒ The rule to state once, so that a sixth is not introduced: + +> ⭐ **One idea has one spelling. A report is an operation; a bit-set is +> `kal__props`; a magnitude has a name of its own; a handle carries +> its type; a transfer reports one signed word; a name is passed as a pointer +> and a length and returned by copying into the caller's buffer.** + +⚠️ And the amendment surfaces a sixth that decision 10 would otherwise +*introduce*: after 10, `kal_node_info` carries its own size and no other +structure does. The rule that keeps this consistent without taxing every +structure: + +> **A structure that may gain fields carries its size. A structure that is +> complete by construction states why it will not grow.** `kal_endpoint` already +> states its own — the set of address lengths may grow while the structure stays +> fixed, because the length is a value rather than a layout. + +**D. ⭐⭐ Decision 7 is answered by a form that is simpler for both sides, and +this document's own recommendation was the worse one.** + +The question put in review was whether the goal could be reached while making +both the call and the implementation *simpler*, and if not, to prefer openkal's +design and let the caller compose. + +It can, and the evidence is that **the only consumer of the two-word result is +already doing the conversion by hand, at every site**: + +``` +okm_poll.c:81 return io.n ? (long)io.n : -EAGAIN; +okm_poll.c:82 return io.n ? (long)io.n : -okm_errno(io.e); +okm_net.c:581 return io.n ? (long)io.n : -okm_errno(io.e); +okm_poll.c:72 return (long)io.n; +okm_net.c:571 return (long)io.n; +``` + +Every one collapses `{n, e}` into a single signed word by the same rule. ⇒ + +> **A transfer returns one signed machine word: the number of bytes transferred, +> or the negated error value when none were transferred.** + +- `kal_stream_read`: a non-negative count; **zero denotes end of input**, which + is the convention clause 7.4 already states; +- `kal_stream_write`: the count, which is the whole buffer in the ordinary case + by clause 7.4; +- the bounded forms in `openkal.timeout` and `openkal.datagram`: the count moved + within the bound, which is exactly what `okm_poll.c:81` computes today. + +**`struct kal_io_result` is deleted.** One frozen layout fewer (clause 5.3), one +fewer spelling (amendment C), and: + +| | today | after | +| the caller writes | `auto r = f(...); if (r.e) …; use r.n;` | `long n = f(...); if (n < 0) …; use n;` | +| the implementer returns | a two-field structure | one value | +| crossing at X | impossible | one word | + +⚠️ **Three things this rests on, each checked rather than assumed:** + +1. *The count is bounded by the caller's buffer*, which cannot exceed half the + address space, so a signed machine word is always sufficient. +2. *The error set is closed* (clause 5.2, values 1–13), so a negated error can + never be mistaken for a count. ⭐ The closed set is what makes the collapse + safe; an open-ended error space would not permit it. +3. *Nothing loses information a caller uses.* The case "transferred some bytes + and then failed" reports the bytes, and the condition arrives on the next + call — which is what all five sites above already do, deliberately. + +⚠️ The earlier rejection of this shape in §13.6 was wrong. It reasoned from the +header's sentence — "on failure, n reports how many bytes were transferred" — +without reading what any caller does with it. **The declaration was consulted and +the call sites were not**, which is the same error §2 names. + +### 14.2 The fourteen, as decided + +| # | decision | as amended | +| --- | --- | --- | +| 1 | two tiers | ✓ + optional is resolved by the link **or by a feature** (14.1 A) | +| 2 | S / L / X marking | ✓ + **X is the design target, S is the floor** (14.1 B) | +| 3 | the six-clause X rule | ✓ five effective now; the sixth pending §9.3 | +| 4 | every report becomes an operation | ✓ + **one idea, one spelling** (14.1 C) | +| 5 | `kal_memory_granularity()` | ✓ coarsest-safe; in `openkal.memory`; clause 8 admits it | +| 6 | five copy-out forms, replacing | ✓ | +| 7 | transfers cross in one word | ✓ **by deleting `kal_io_result`, not by an out-parameter** (14.1 D) | +| 8 | `begins at`, not `calls` | ✓ wording only | +| 9 | clause 6.5 becomes a property position | ✓ conditional on 14; and see 14.1 A | +| 10 | `self_size` / `wanted` / `present`; identity; 64-bit size | ✓ + the size rule of 14.1 C | +| 11 | 11a resolution in the header; 11b link operations | ✓ 11a first and separable | +| 12 | the floor is an operation | ✓ shape now, mechanism with §9.3 | +| 13 | five small corrections | ✓ as one package | +| 14 | the interposer ships in this change set | ✓ non-negotiable | + +### 14.3 What the decisions change in §12.5's table + +Decision 7's new form removes cause 3 from three rows without an out-parameter, +so the table's "X after" column is unchanged and the route to it is shorter: + +| cause | count | status after 14.1 | +| an exported data object | 10 | decision 4 | +| an operation returns an internal pointer | 5 | decision 6 | +| a result wider than one word | 7 | **decision 7, by deletion** | +| an entry worded as a call | 2 | decision 8, wording | +| availability decided at production time | 1 clause | decisions 9 and 1 together | + +### 14.4 What is still open after this review + +Unchanged from §13.3, and now the whole of it: + +1. **The negotiation mechanism** — clause 3.2 forbids a new core interface and + clause 6.2 forbids an operation that always fails; a trap ABI needs one of + them to give. Decision 12 fixes the *shape* and leaves the mechanism. +2. **Which boundary is built**, and how a call is encoded on it. Decisions 6, 7 + and 8 make the shapes admissible; none of them depends on the answer. +3. **Whether the floor operation is per-interface or one for the specification.** +4. **Where the interposer lives** — openkal's repository, where the conformance + runner is, or its own. diff --git a/README.md b/README.md index e80dd48..58f81d4 100644 --- a/README.md +++ b/README.md @@ -14,17 +14,35 @@ undefined references, which is the intended outcome. | Module | Header | Interface | Class | | --- | --- | --- | --- | -| `openkal.types` | `openkal/types.h` | machine word, error values, transfer result | — | +| `openkal.types` | `openkal/types.h` | machine word, error values, endpoint | — | +| `openkal.version` | `openkal/version.h` | what the implementation says about itself | every | | `openkal.abort` | `openkal/abort.h` | termination | core | | `openkal.stream` | `openkal/stream.h` | byte streams | core | -| `openkal.memory` | `openkal/memory.h` | allocation | core | -| `openkal.env` | `openkal/env.h` | the parameters a program receives at inception | standard | -| `openkal.time` | `openkal/time.h` | monotonic and wall time sources | standard | -| `openkal.fs` | `openkal/fs.h` | directories and open files, relative throughout | standard | -| `openkal.process` | `openkal/process.h` | starting a program and waiting for it | standard | -| `openkal.task` | `openkal/task.h` | execution contexts, and the primitive they are built upon | standard | +| `openkal.memory` | `openkal/memory.h` | allocation, and the environment's granularity | core | +| `openkal.env` | `openkal/env.h` | the parameters a program receives at inception | optional | +| `openkal.time` | `openkal/time.h` | monotonic and wall time sources | optional | +| `openkal.fs` | `openkal/fs.h` | directories and open files, relative throughout | optional | +| `openkal.process` | `openkal/process.h` | starting a program and waiting for it | optional | +| `openkal.task` | `openkal/task.h` | execution contexts, and the primitive they are built upon | optional | | `openkal.random` | `openkal/random.h` | a source of unpredictable bytes | optional | | `openkal.exec` | `openkal/exec.h` | a region of the address space a program may execute | optional | +| `openkal.terminal` | `openkal/terminal.h` | an interactive stream's treatment of what is typed | optional | +| `openkal.net` | `openkal/net.h` | a connection, and a listener for connections | optional | +| `openkal.datagram` | `openkal/datagram.h` | a message with a boundary | optional | +| `openkal.space` | `openkal/space.h` | an address space, and a context executing in one | optional | +| `openkal.timeout` | `openkal/timeout.h` | a bound upon operations that would otherwise wait | optional | + +⚠️ **There were three classes and there are two.** Version 0.8 named a middle +one — *standard*, "an interface an implementation hosting a C library provides" +— and it was false: an implementation for a machine with firmware and no +operating system provides none of `openkal.fs`, `openkal.process` or +`openkal.task`, and a C library is hosted above it. Clause 6.1 makes an +interface's absence a fact a consumer learns from the linker; no class was +needed to predict it, and the prediction was wrong. + +`openkal.version` is in the table and is not an interface: it provides no +resource, and every conforming implementation exports its two operations so that +a consumer with no linker to ask can ask before it calls. ### One statement of the declarations, two ways to reach it diff --git a/SPEC.md b/SPEC.md index 30ea755..df00999 100644 --- a/SPEC.md +++ b/SPEC.md @@ -1,4 +1,4 @@ -# openkal Specification, version 0.8 +# openkal Specification, version 0.9 ## 1. Scope @@ -31,27 +31,32 @@ it provides. An implementation is not required to provide every interface. An interface is the unit of versioning and of provision. An implementation provides an interface in whole or not at all. -| Interface | Resource | Core | -| --- | --- | --- | -| `openkal.abort` | termination | core | -| `openkal.stream` | a byte stream | core | -| `openkal.memory` | a region of the address space | core | -| `openkal.env` | the parameters a program receives at inception | standard | -| `openkal.time` | a time source | standard | -| `openkal.random` | a source of unpredictable bytes | optional | -| `openkal.fs` | a directory, and an open file | standard | -| `openkal.process` | a program image that has been started | standard | -| `openkal.task` | an execution context, and a suspension primitive | standard | -| `openkal.exec` | a region of the address space a program may execute | optional | -| `openkal.terminal` | an interactive stream's treatment of what is typed | optional | -| `openkal.net` | a connection, and a listener for connections | optional | -| `openkal.datagram` | a message with a boundary, sent without a connection | optional | -| `openkal.space` | an address space, and a context executing in one | optional | -| `openkal.timeout` | a bound upon operations that would otherwise wait | optional | -| `openkal.event` | readiness of a set of resources | reserved | - -Version 0.8 specifies the core, standard and optional interfaces. The reserved -row is not specified, and its name shall not be used for other purposes. +| Interface | Resource | Tier | S | L | X | +| --- | --- | --- | --- | --- | --- | +| `openkal.abort` | termination | core | ✓ | ✓ | ✓ | +| `openkal.stream` | a byte stream | core | ✓ | ✓ | ✓ | +| `openkal.memory` | a region of the address space | core | ✓ | ✓ | ✓ | +| `openkal.env` | the parameters a program receives at inception | optional | ✓ | ✓ | ✓ | +| `openkal.time` | a time source | optional | ✓ | ✓ | ✓ | +| `openkal.random` | a source of unpredictable bytes | optional | ✓ | ✓ | ✓ | +| `openkal.fs` | a directory, and an open file | optional | ✓ | ✓ | ✓ | +| `openkal.process` | a program image that has been started | optional | ✓ | ✓ | ✓ | +| `openkal.task` | an execution context, and a suspension primitive | optional | ✓ | ✓ | ✓ | +| `openkal.exec` | a region of the address space a program may execute | optional | ✓ | ✓ | ✓ | +| `openkal.terminal` | an interactive stream's treatment of what is typed | optional | ✓ | ✓ | ✓ | +| `openkal.net` | a connection, and a listener for connections | optional | ✓ | ✓ | ✓ | +| `openkal.datagram` | a message with a boundary, sent without a connection | optional | ✓ | ✓ | ✓ | +| `openkal.space` | an address space, and a context executing in one | optional | ✓ | ✓ | ✓ | +| `openkal.timeout` | a bound upon operations that would otherwise wait | optional | ✓ | ✓ | ✓ | +| `openkal.event` | readiness of a set of resources | reserved | | | | + +Version 0.9 specifies the core and optional interfaces. The reserved row is not +specified, and its name shall not be used for other purposes. + +The S, L and X columns state which boundaries an interface's declarations can +cross. They are defined in clause 4.4 and are a statement about the shape of the +declarations, not a requirement upon implementations: an implementation cannot +violate them, and only a declaration can. The five interfaces added in version 0.8 are optional in the sense clause 3 defines, and their optionality is not a concession. An environment with no @@ -61,10 +66,33 @@ 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 that requires one of them is refused when it is built rather than when it runs. -*Core* denotes an interface every implementation provides. *Standard* denotes -one an implementation hosting a C library provides. *Optional* denotes one it -may omit without ceasing to host a C library, at the cost of the facilities -built upon it. +*Core* denotes an interface every implementation provides. *Optional* denotes +every other, and how its absence reaches a consumer is one of two things: + +| how an optional interface is resolved | mechanism | +| --- | --- | +| the implementation does not provide it at all | clause 6.1 — the definitions are absent, and a consumer that uses one fails to link | +| the implementation provides it only to an artifact produced in a particular way | a feature of the implementation's package, resolved at dependency resolution — clause 6.5 | + +⚠️ There were three tiers, and the middle one said that an implementation +*hosting a C library* provides `openkal.env`, `openkal.time`, `openkal.fs`, +`openkal.process` and `openkal.task`. It was false, and falsified within this +specification's own ecosystem: an implementation for a machine with firmware and +no operating system provides none of the last three, and a C library is hosted +above it. The tier stated a fact that was not one, and it stated it about the +consumer rather than about the interface. + +The honest classification is the *Resource* column beside it. An interface +exists where its resource exists — storage, a second image, a scheduler, a +network, an address space that can be copied, entropy, an interactive stream, a +bound upon a wait. Clause 6.1 makes the absence a fact a consumer learns; no +tier is needed to predict it, and the prediction was wrong. + +*Informative, and carrying no requirement*: an implementation of a system with +storage and a scheduler ordinarily provides `openkal.env`, `openkal.time`, +`openkal.fs`, `openkal.process`, `openkal.task` and `openkal.timeout`. This +sentence describes what is common; nothing follows from it, and an +implementation is not measured against it. ### 3.1 The basis of the core set @@ -101,6 +129,15 @@ placed outside it costs a consumer one declared dependency. The first error is found late and by the wrong party; the second is found at dependency resolution and by the party that can act on it. +⚠️ **What this closes is the set of INTERFACES.** `kal_version` and +`kal_interfaces` are exported by every conforming implementation and belong to +no interface: they provide no resource, and an implementation answers both with +constants. They are the specification's own self-description, required by clause +9, and requiring them adds nothing to the core set that a bare machine would +have to supply. A consumer that is linked never needs to call them; a consumer +bound at load or across a boundary has no linker to ask, and asking an interface +by calling into it is the act that must not happen first. + ### 3.3 Naming a set of interfaces An interface is provided or not provided, and clause 6.1 makes that a fact a @@ -110,23 +147,24 @@ not scale: the sets a program can be written against are not arbitrary subsets, and an ecosystem that could only enumerate them could not say "this package needs an environment of such-and-such a kind". -The specification therefore names sets, and the names are for stating a -requirement rather than for measuring conformance: - -| Name | Interfaces | -| --- | --- | -| `core` | `openkal.abort`, `openkal.stream`, `openkal.memory` | -| `hosted` | `core`, and `openkal.env`, `openkal.time`, `openkal.fs`, `openkal.process`, `openkal.task` | - -A name is a shorthand and confers nothing. **An implementation does not claim a -name**; it provides interfaces, and whether it satisfies a consumer that asked -for `hosted` follows from which interfaces it provides. Nothing in this clause -alters clause 6.1: an interface a consumer uses and an implementation does not -provide is an undefined symbol, whichever names either of them mentions. - -Optional interfaces are deliberately not gathered into a name. A set that -required every optional interface would make *optional* mean nothing, and a -consumer that needs one names that one. +⚠️ **The specification names no such set, and the one it did name has been +withdrawn.** Version 0.8 named `hosted` — the core interfaces together with +`openkal.env`, `openkal.time`, `openkal.fs`, `openkal.process` and +`openkal.task`. The name described a class of environment, and a name that +describes a class of environment is falsified by an environment nobody had in +mind. This one was falsified inside its own ecosystem within a release: a C +library is hosted above an implementation that provides none of the last three. + +The scaling argument is also weaker than it read. There are fifteen interfaces, +and a consumer that needs five names five. That is five lines, not a problem of +scale, and it is what the ecosystem already does — a consumer states its +required set per target, in its own package, which is where a convention among +consumers belongs. + +`core` remains, and is not a set a consumer states: it is the requirement upon +implementations stated in clause 3.2. Nothing in this clause alters clause 6.1: +an interface a consumer uses and an implementation does not provide is an +undefined symbol. ### 3.4 Interfaces the specification declines to define @@ -277,6 +315,46 @@ The failure is later than a compilation failure would be and is legible. An arrangement that reported it during compilation was considered and is described in clause 6.3, together with the reason it was not adopted. +### 4.4 The boundaries a declaration can cross + +Compile-time portability is the floor of this specification and not its goal. +Every interface, operation and structure is designed so that it can also be +crossed at run time, and where one cannot the reason is recorded in the table of +clause 3 rather than left to be discovered. + +Three boundaries are distinguished, because "crossed at run time" gives opposite +answers for two of them: + +| | boundary | the implementation is | what it additionally forbids | +| --- | --- | --- | --- | +| **S** | static | chosen when the artifact is built, and linked into it | nothing | +| **L** | late-bound | a separate object resolved at load, in the caller's address space; the artifact does not change when the implementation does | a structure whose size the consumer fixed when it was built; a report whose value the consumer fixed when it was built | +| **X** | crossed | in another address space or at another privilege level; a call is a trap or a message | a returned pointer into the implementation; a result wider than one machine word; a call back into the consumer | + +Returning a pointer into the implementation is admissible at **L** and +inadmissible at **X**; so is a result of two machine words. An interface marked +for one is not thereby marked for the other. + +**An interface is X-capable when all six of the following hold.** + +1. It exports no object. Everything an implementation reports about itself is an + operation (clause 6.2). +2. No operation returns a pointer into the implementation's storage. A value is + copied into a buffer the caller supplies. +3. No result is wider than one machine word. An operation whose whole result is + a count returns that count or the negated error value; an operation that + produces a resource returns `int` and writes the resource through a pointer. +4. Every structure that crosses carries its own size, or states why it cannot + grow. +5. An entry is an address at which a context begins, not a function that is + called. No return address exists in the started context. +6. Its absence is discoverable by an operation and not only by a linker + (`kal_interfaces`, clause 3.2). + +The first four are decidable from `SURFACE.txt` and the declarations, and clause +9 asserts them. **A new interface is designed against this rule before it is +specified.** The reserved `openkal.event` is the first to which that applies. + ## 5. Common definitions ### 5.1 Machine word @@ -314,6 +392,28 @@ The addition is governed by clause 8, which admits new declarations. A value already assigned retains its meaning, so a program compiled against version 0.4 observes the same values for the same conditions as before. +### 5.2.1 How an operation reports its result + +There is one rule, and it holds for every interface. + +> An operation whose whole result is a **count of bytes** returns `kal_intptr`: +> the count, or the negated error value when no byte was produced. A count of +> zero is a count and not an error; for a read it denotes end of input. +> +> An operation that produces a **resource**, or that produces nothing, returns +> `int` from `kal_error` and writes what it produced through a pointer. + +Version 0.8 returned a structure of a count and an error from the operations +that transfer. It is withdrawn, and the measurement that withdrew it is that +every consumer of it collapsed the pair by hand and by this rule — report what +was moved, or the condition when nothing was. The pair was never the shape a +caller wanted, and a result of two words cannot be carried by a boundary that +returns one (clause 4.4). + +The collapse is sound because the error set of clause 5.2 is **closed**: a +negated error occupies a small known range and can never be mistaken for a +count. An open-ended error space would not permit it. + ### 5.3 Structure layouts The layout of every structure declared by this specification is frozen at @@ -515,6 +615,23 @@ how the artifact is produced is not reported at run time.** Reporting it at run time would make every caller carry a path that most artifacts never take — and a path no artifact takes is a path nothing has verified. +⚠️ **The rule holds where the artifact is produced for its own environment, and +inverts where it is not.** An artifact that is distributed is produced once, by +a party that decided for every environment it will meet, possibly long before +and for a different system; the consumer resolves nothing. So availability +settled this way is *also* reported at run time, by a position in the +interface's property word — `KAL_EXEC_PROP_AVAILABLE`. The interface is provided +either way, and whether it can be exercised is read. + +This is not the shape clause 6.2 forbids. An operation that is present and +always fails is a defect *because the caller cannot tell*, and a position the +caller reads first is what tells it. + +The objection above survives and is answered by clause 9 rather than dismissed: +a path few artifacts take is a path little verified, so the conformance +arrangement answers that position both ways. Until it did, the unavailable path +had no producer at all. + ### 6.6 Concurrency An implementation shall permit concurrent operations upon distinct handles. @@ -568,6 +685,16 @@ An implementation shall not assume a process model, a division between privileged and unprivileged execution, or a namespace shared by all callers. A handle shall be meaningful in the context of the caller that obtained it. +**An implementation shall refuse a word that is not a handle it issued**, and +shall not act upon it as though it were one it did. The preceding paragraph +constrains a handle's *scope*; this one constrains its *validity*, and the two +are not the same requirement. Under a static link the distinction is academic — +the caller and the implementation are one program. Where the implementation is +on the far side of a boundary it is the boundary itself, and an implementation +that acted upon an arbitrary word would let a caller name a resource it was +never given. The division of a handle into an index and a generation, which +clause 7.2 already recommends for release, satisfies this as well. + ### 7.3 Allocation Where the environment already provides an allocator, `kal_alloc` shall be built @@ -590,8 +717,13 @@ a recurring source of defects. The loop belongs in the implementation, which is written once. `kal_stream_read` shall report the number of bytes transferred, which may be -fewer than requested. A result of zero bytes with `kal_ok` denotes end of input. -Unlike a partial write, a partial read carries information the caller requires. +fewer than requested. A result of zero denotes end of input. Unlike a partial +write, a partial read carries information the caller requires. + +Both report that number, or the negated error value when no byte was moved, in +one signed machine word — clause 5.2.1. An operation that moved bytes and then +met a condition reports the bytes; the condition is reported by the next call, +which is what every consumer of the earlier two-word form already did by hand. ### 7.5 Interruption @@ -850,12 +982,41 @@ The following are recorded so that they are not mistaken for oversights. schemes. 6. **Permission and ownership of files.** Not defined, and not a deferral. A permission presupposes an identity, and the environments this specification - targets do not agree that one exists. A C library above openkal reports the - absence as the error its own surface defines. -7. **Creation and reading of links.** Not defined, and not a deferral, for the - reason clause 6.4 gives: whether a filesystem has links is a property of the - format rather than of the environment. `KAL_FS_PROP_LINKS` reports it, and - resolution follows one where the property is claimed. + targets do not agree that one exists. + + ⭐ **And what a program should write instead, which was not recorded and is + now.** A program that means "only I may read this" is stating it in a + vocabulary this environment does not have. It has three answers, and none of + them is a mode: + + - against another part of the same program, the capability already does it: + a handle not given cannot be reached; + - against another user of the machine, it is the *environment's* + responsibility — the party that starts the program supplies a preopen that + others do not have; + - against a location the program does not trust, it encrypts the contents. + + ⚠️ The measurement that settles the alternative: a mode word is not stored by + several filesystems a hosted implementation will meet, and on such a volume + `chmod` **reports success and changes nothing**, which is the outcome this + specification exists to refuse. `kal_node_info.writable` — one boolean — is + not a simplification of a mode word; it is the intersection of what those + formats store. + +7. **Creation and reading of links.** ⚠️ **Settled in 0.9.** `kal_fs_link_create` + and `kal_fs_link_read` are operations of `openkal.fs`, and they are operations + of it rather than an interface of their own for exactly the reason this entry + used to give: whether a volume has such nodes is a property of the format and + not of the environment, so the variability is between the *resources* of one + implementation. Clause 6.2 says such a property is answered by an enquiry + taking the resource, and `kal_fs_props` is now that enquiry — which is what + makes the operations admissible, because a caller can ask before it calls. + + Resolution is stated in `fs.h` beside each operation it governs, and no + longer only here. It was here alone, and two call sites of one implementation + consequently chose opposite directions: opening resolved a link while asking + did not, so a program was told that a name referred to a link when opening it + would have reached a file. 8. **Duplication of the calling image.** `fork` is refused by clause 7.1 and that refusal stands. It is refused as an OPERATION. The atomic capabilities from which a library may compose it are specified: `openkal.space` clones an diff --git a/SURFACE.txt b/SURFACE.txt index 25731fc..8b6ad28 100644 --- a/SURFACE.txt +++ b/SURFACE.txt @@ -1,4 +1,4 @@ -# The C surface of openkal 0.8, one name per line. +# The C surface of openkal 0.9, one name per line. # # This file is normative and is the single source consulted by clause 9. A # conforming implementation exports the names of the interfaces it provides @@ -7,6 +7,12 @@ # An implementation provides an interface in whole or not at all, so the # absence of a group below denotes an interface the implementation does not # provide and is not a deviation. +# openkal --- the specification's own self-description. Not an interface: it +# provides no resource, so clause 3.2's closure of the core SET is untouched. +# Every conforming implementation exports both, so that a consumer with no +# linker to ask can ask before it calls. +kal_interfaces +kal_version # openkal.abort kal_abort kal_exit @@ -21,6 +27,7 @@ kal_stream_write # openkal.memory kal_alloc kal_free +kal_memory_granularity # openkal.env kal_env_arg kal_env_arg_count @@ -41,17 +48,19 @@ kal_fs_close_dir kal_fs_close_file kal_fs_file_info kal_fs_info +kal_fs_link_create +kal_fs_link_read kal_fs_list_begin kal_fs_list_next +kal_fs_max_name kal_fs_mkdir kal_fs_open kal_fs_open_dir -kal_fs_open_file +kal_fs_preopen +kal_fs_preopen_count kal_fs_props kal_fs_remove kal_fs_rename -kal_fs_preopen -kal_fs_preopen_count kal_fs_seek kal_fs_set_modified kal_fs_stream @@ -107,7 +116,7 @@ kal_space_props kal_space_start # openkal.timeout kal_timeout_accept -kal_timeout_granularity_ns +kal_timeout_granularity kal_timeout_read kal_timeout_recv_from kal_timeout_wait_process diff --git a/conformance/mcpp.toml b/conformance/mcpp.toml index 1caa4bc..ecd9abe 100644 --- a/conformance/mcpp.toml +++ b/conformance/mcpp.toml @@ -1,7 +1,7 @@ [package] namespace = "mcpplibs" name = "openkal-conformance" -version = "0.6.0" +version = "0.7.0" description = "The behavioural half of clause 9: a suite an implementation of openkal runs against itself, selectable to the interfaces it provides." license = "Apache-2.0" authors = ["mcpplibs"] @@ -18,7 +18,7 @@ repo = "https://github.com/mcpplibs/openkal" # What it depends upon is therefore openkal and the language. The formatting in # okc.report is the price of that, and it is sixty lines. [dependencies] -openkal = "0.8.0" +openkal = "0.9.0" # The implementation under examination is not named here. # @@ -45,7 +45,7 @@ main = "src/main.cpp" # the count of what was skipped in the report rather than in the reader's head. # # mcpp run the core set -# mcpp run --features standard every interface version 0.5 defines +# mcpp run --features full every interface # mcpp run --features fs,task the core set, and these two # # The core set is the default because clause 3 says every implementation @@ -62,21 +62,29 @@ process = ["fs"] # a program is started relative to a directory task = [] exec = [] -# The five interfaces version 0.8 adds. Each is optional in the sense clause 3 -# defines, so each is a feature and none is in `hosted': a set demanding an -# optional interface would turn a permitted choice into a link error, which is -# the mistake the note above `optional' records having been made once already. +# Every interface outside the core set is a feature, and there is no name that +# gathers a subset of them. Version 0.8 had one --- `hosted' --- and it named a +# class of environment, which is a thing a name cannot do: it was falsified +# inside this ecosystem within a release, by a C library hosted above an +# implementation providing none of the three interfaces it promised. terminal = [] net = [] datagram = [] space = ["process"] # a started context is waited for as a process timeout = [] -# The specification names this set `hosted' (clause 3.3). Both spellings are -# here because the workflows and the older invocations use `standard', and a -# rename that broke them would be a rename of the wrong thing. -hosted = ["core", "env", "time", "fs", "process", "task"] -standard = ["hosted"] +# ⚠️ `hosted' AND `standard' WERE HERE AND ARE GONE, BECAUSE THE SPECIFICATION'S +# OWN NAME FOR THAT SET IS GONE. +# +# Version 0.8 named a set of interfaces after a class of environment, and a name +# that describes a class of environment is falsified by an environment nobody had +# in mind. This one was falsified inside its own ecosystem within a release: a C +# library is hosted above an implementation providing none of `openkal.fs', +# `openkal.process' or `openkal.task'. +# +# Nothing invoked them --- every workflow in this ecosystem passes `core' or +# `full,optional' --- so removing them costs nothing and removes a second +# spelling of a decision the specification has withdrawn. # Deliberately not part of `full'. Clause 6.1 states that an implementation # provides an interface in whole or not at all and that a missing one is not a @@ -85,9 +93,10 @@ standard = ["hosted"] # rather than an observation that did not hold. The caller names this set for an # implementation that provides these interfaces, and omits it for one that does # not --- which is the same choice the implementation itself made. -# ⚠️ `random` IS HERE AND NOT IN `hosted`, AND THE NOTE ABOVE ALREADY SAID WHY. +# ⚠️ `random` IS HERE AND NOT AMONG THE INTERFACES `full` NAMES, AND THE NOTE +# ABOVE ALREADY SAID WHY. # -# It was put in `hosted` first, and the suite then failed to LINK against every +# It was named there first, and the suite then failed to LINK against every # backend that had not yet implemented it: # # lld-link: error: undefined symbol: kal_random_fill @@ -121,4 +130,14 @@ stability = [] # declared rather than assumed, so that requesting cost without time selects # time instead of failing to link. cost = ["time"] -full = ["standard", "abi", "stability", "cost"] +# ⚠️⚠️ THE INTERFACES ARE NAMED HERE AND NOT THROUGH ANOTHER SET, BECAUSE A SET +# DEFINED IN TERMS OF A SET CAN SHRINK WITHOUT ANYTHING SAYING SO. +# +# This was `["standard", …]`, and `standard` was a name the specification has +# withdrawn. Removing it left `full` meaning four kinds of examination and no +# interface at all --- measured 2026-08-29: the same command reported 168 held +# before and 119 held after, WITH AN EXIT STATUS OF ZERO BOTH TIMES, because +# every section it stopped examining reported itself as not observed rather than +# as failing. A set that names its members cannot do that. +full = ["core", "env", "time", "fs", "process", "task", + "abi", "stability", "cost"] diff --git a/conformance/src/declarations.c b/conformance/src/declarations.c index 7f62254..b3e8c62 100644 --- a/conformance/src/declarations.c +++ b/conformance/src/declarations.c @@ -50,12 +50,14 @@ void okc_declarations_c(void) (void)sizeof(&kal_fs_close_file); (void)sizeof(&kal_fs_file_info); (void)sizeof(&kal_fs_info); + (void)sizeof(&kal_fs_link_create); + (void)sizeof(&kal_fs_link_read); (void)sizeof(&kal_fs_list_begin); (void)sizeof(&kal_fs_list_next); + (void)sizeof(&kal_fs_max_name); (void)sizeof(&kal_fs_mkdir); (void)sizeof(&kal_fs_open); (void)sizeof(&kal_fs_open_dir); - (void)sizeof(&kal_fs_open_file); (void)sizeof(&kal_fs_preopen); (void)sizeof(&kal_fs_preopen_count); (void)sizeof(&kal_fs_props); @@ -65,6 +67,8 @@ void okc_declarations_c(void) (void)sizeof(&kal_fs_set_modified); (void)sizeof(&kal_fs_stream); (void)sizeof(&kal_fs_truncate); + (void)sizeof(&kal_interfaces); + (void)sizeof(&kal_memory_granularity); (void)sizeof(&kal_net_accept); (void)sizeof(&kal_net_close); (void)sizeof(&kal_net_close_listener); @@ -109,7 +113,7 @@ void okc_declarations_c(void) (void)sizeof(&kal_time_monotonic); (void)sizeof(&kal_time_monotonic_granularity); (void)sizeof(&kal_timeout_accept); - (void)sizeof(&kal_timeout_granularity_ns); + (void)sizeof(&kal_timeout_granularity); (void)sizeof(&kal_timeout_read); (void)sizeof(&kal_timeout_recv_from); (void)sizeof(&kal_timeout_wait_process); @@ -117,4 +121,5 @@ void okc_declarations_c(void) (void)sizeof(&kal_time_props); (void)sizeof(&kal_time_sleep); (void)sizeof(&kal_time_wall); + (void)sizeof(&kal_version); } diff --git a/conformance/src/report.cpp b/conformance/src/report.cpp index e591738..5b43d5a 100644 --- a/conformance/src/report.cpp +++ b/conformance/src/report.cpp @@ -83,14 +83,14 @@ int failed_count() { return g_failed; } int unobserved_count() { return g_unobserved; } void write_inventory() { - line("openkal conformance suite, version 0.5.0"); + line("openkal conformance suite, version 0.9.0"); line(""); line("interface provision examined select with"); for (const auto& row : inventory) { write(" "); write(row.name); for (kal_uintptr i = length(row.name); i < 17; ++i) write(" "); - write(row.core ? "core " : "standard "); + write(row.core ? "core " : "optional "); write(row.selected ? "yes " : "no "); write("--features "); write(row.feature); diff --git a/conformance/src/sections/child.cpp b/conformance/src/sections/child.cpp index 9125bc8..aec8f58 100644 --- a/conformance/src/sections/child.cpp +++ b/conformance/src/sections/child.cpp @@ -41,9 +41,11 @@ const char* argument_for(errand e) { errand child_errand() { #ifdef MCPP_FEATURE_ENV for (kal_uintptr i = 1; i < kal_env_arg_count(); ++i) { - kal_uintptr len = 0; - const char* a = kal_env_arg(i, &len); - if (!a) continue; + char abuf[1024]; + const kal_intptr alen = kal_env_arg(i, abuf, sizeof abuf); + if (alen < 0 || static_cast(alen) >= sizeof abuf) continue; + const kal_uintptr len = static_cast(alen); + const char* a = abuf; if (same(a, len, argument_for(errand::exit_with_33))) return errand::exit_with_33; if (same(a, len, argument_for(errand::exit_after_writing))) return errand::exit_after_writing; if (same(a, len, argument_for(errand::abort_with_message))) return errand::abort_with_message; @@ -103,7 +105,10 @@ after g_after; #ifdef MCPP_FEATURE_ENV { kal_uintptr len = 0; - const char* self = kal_env_arg(0, &len); + char sbuf[1024]; + const kal_intptr slen = kal_env_arg(0, sbuf, sizeof sbuf); + const char* self = (slen >= 0 && static_cast(slen) < sizeof sbuf) + ? (len = static_cast(slen), sbuf) : nullptr; if (!self || !same(self, len, "openkal-conformance-child")) kal_exit(36); } #endif @@ -158,14 +163,18 @@ bool prefix_at_boundary(const char* name, kal_uintptr nlen, bool locate_self(kal_dir& base, const char*& relative, kal_uintptr& relative_len) { kal_uintptr len = 0; - const char* argv0 = kal_env_arg(0, &len); + static char a0[1024]; + const kal_intptr a0len = kal_env_arg(0, a0, sizeof a0); + const char* argv0 = (a0len >= 0 && static_cast(a0len) < sizeof a0) + ? (len = static_cast(a0len), a0) : nullptr; if (!argv0 || len == 0) return false; kal_uintptr best = 0, best_len = 0; bool found = false; for (kal_uintptr i = 0; i < kal_fs_preopen_count(); ++i) { - kal_dir d{}; const char* name = nullptr; kal_uintptr nlen = 0; - if (kal_fs_preopen(i, &d, &name, &nlen) != kal_ok) continue; + kal_dir d{}; char name[1024]; kal_uintptr nlen = 0; + if (kal_fs_preopen(i, &d, name, sizeof name, &nlen) != kal_ok) continue; + if (nlen >= sizeof name) continue; if (!prefix_at_boundary(name, nlen, argv0, len)) continue; if (!found || nlen > best_len) { found = true; best = i; best_len = nlen; } } @@ -179,8 +188,8 @@ bool locate_self(kal_dir& base, const char*& relative, kal_uintptr& relative_len return relative_len > 0; } - kal_dir d{}; const char* name = nullptr; kal_uintptr nlen = 0; - kal_fs_preopen(best, &d, &name, &nlen); + kal_dir d{}; kal_uintptr nlen = 0; + kal_fs_preopen(best, &d, nullptr, 0, &nlen); base = d; kal_uintptr at = best_len; while (at < len && is_separator(argv0[at])) ++at; diff --git a/conformance/src/sections/datagram.cpp b/conformance/src/sections/datagram.cpp index dca0cc1..4b60305 100644 --- a/conformance/src/sections/datagram.cpp +++ b/conformance/src/sections/datagram.cpp @@ -27,7 +27,7 @@ void run() { unobserved(kind::behaviour, "openkal.datagram", "the interface was not selected"); return; #else - claim("kal_datagram_props", kal_datagram_props); + claim("kal_datagram_props()", kal_datagram_props()); kal_endpoint want = loopback_v4(0); kal_datagram receiver{}; @@ -58,18 +58,18 @@ void run() { // therefore always the length that was given. { const char msg[] = "openkal"; - const kal_io_result w = kal_datagram_send_to(sender, msg, sizeof msg - 1, &to); - observe(kind::behaviour, w.e == kal_ok && w.n == sizeof msg - 1, + const kal_intptr w = kal_datagram_send_to(sender, msg, sizeof msg - 1, &to); + observe(kind::behaviour, w == static_cast(sizeof msg - 1), "a message is sent whole and the count is the length given"); kal_endpoint from{}; char buf[16] = {0}; - const kal_io_result r = kal_datagram_recv_from(receiver, buf, sizeof buf, &from); - bool same = r.e == kal_ok && r.n == sizeof msg - 1; - for (kal_uintptr i = 0; same && i < r.n; ++i) + const kal_intptr r = kal_datagram_recv_from(receiver, buf, sizeof buf, &from); + bool same = r == static_cast(sizeof msg - 1); + for (kal_intptr i = 0; same && i < r; ++i) if (buf[i] != msg[i]) same = false; observe(kind::behaviour, same, "the message received is the message sent"); - observe(kind::behaviour, r.e != kal_ok || from.addr_len == 4, + observe(kind::behaviour, r < 0 || from.addr_len == 4, "the sender of a received message is reported"); } @@ -80,12 +80,12 @@ void run() { { char big[64]; for (auto& c : big) c = 'x'; - const kal_io_result w = kal_datagram_send_to(sender, big, sizeof big, &to); - if (w.e == kal_ok) { + const kal_intptr w = kal_datagram_send_to(sender, big, sizeof big, &to); + if (w >= 0) { char small[8] = {0}; kal_endpoint from{}; - const kal_io_result r = kal_datagram_recv_from(receiver, small, sizeof small, &from); - observe(kind::behaviour, r.e == kal_ok && r.n <= sizeof small, + const kal_intptr r = kal_datagram_recv_from(receiver, small, sizeof small, &from); + observe(kind::behaviour, r >= 0 && static_cast(r) <= sizeof small, "a truncated message reports the count placed in the buffer"); } else { unobserved(kind::behaviour, "truncation reports the buffered count", diff --git a/conformance/src/sections/env.cpp b/conformance/src/sections/env.cpp index 1778cf1..346614e 100644 --- a/conformance/src/sections/env.cpp +++ b/conformance/src/sections/env.cpp @@ -10,12 +10,16 @@ namespace { kal_uintptr length(const char* s) { kal_uintptr n = 0; while (s && s[n]) ++n; return n; } -bool same(const char* a, const char* b) { - kal_uintptr i = 0; - while (a[i] && a[i] == b[i]) ++i; - return a[i] == b[i]; +bool same(const char* a, const char* b, kal_uintptr n) { + for (kal_uintptr i = 0; i < n; ++i) if (a[i] != b[i]) return false; + return true; } +// The buffer every enquiry in this section copies into. The operations report +// the length the value HAS rather than the length they wrote, so a value longer +// than this is still counted correctly and is simply not read. +constexpr kal_uintptr room = 4096; + } // namespace void run() { @@ -24,6 +28,7 @@ void run() { unobserved(kind::behaviour, "openkal.env", "the interface was not selected"); return; #else + char buf[room]; const kal_uintptr count = kal_env_arg_count(); // Position zero is the name by which the program was started, and an @@ -32,41 +37,55 @@ void run() { observe(kind::behaviour, count >= 1, "the argument vector has at least one element"); { - kal_uintptr len = 999; - const char* a0 = kal_env_arg(0, &len); - observe(kind::behaviour, a0 != nullptr && len == length(a0), + const kal_intptr n = kal_env_arg(0, buf, room); + observe(kind::behaviour, n >= 0, "the first argument is reported with its own length"); } - // Reading past the end is answered, not undefined: a program that walks the - // vector must be able to stop. + // ⭐ THE LENGTH IS THE VALUE'S AND NOT THE BUFFER'S, WHICH IS WHAT LETS A + // CALLER SIZE FIRST AND WHAT MAKES TRUNCATION IMPOSSIBLE TO MISS. { - kal_uintptr len = 999; - observe(kind::behaviour, kal_env_arg(count, &len) == nullptr && len == 0, - "reading past the last argument reports nothing"); + const kal_intptr full = kal_env_arg(0, buf, room); + const kal_intptr sized = kal_env_arg(0, nullptr, 0); + observe(kind::behaviour, full >= 0 && full == sized, + "a capacity of zero reports the length without writing"); + } + if (const kal_intptr full = kal_env_arg(0, buf, room); full > 1) { + char small[2] = { 0, 0 }; + const kal_intptr again = kal_env_arg(0, small, 1); + observe(kind::behaviour, again == full && small[0] == buf[0], + "a capacity smaller than the value reports the length the value has"); + } else { + unobserved(kind::behaviour, + "a capacity smaller than the value reports the length the value has", + "the first argument is too short to be truncated"); } + // Reading past the end is answered, not undefined: a program that walks the + // vector must be able to stop. + observe(kind::behaviour, kal_env_arg(count, buf, room) < 0, + "reading past the last argument reports a condition"); + // The distinction the interface exists to preserve. A caller that cannot // tell an absent variable from one whose value is empty cannot act - // correctly upon either. + // correctly upon either --- which is why an absent one is a condition and an + // empty one is a length of zero. { const char* name = "OPENKAL_CONFORMANCE_NO_SUCH_VARIABLE_EXISTS"; - kal_uintptr len = 999; observe(kind::behaviour, - kal_env_var(name, length(name), &len) == nullptr, + kal_env_var(name, length(name), buf, room) == -kal_err_not_found, "a variable that is absent is reported as absent"); } { const char* name = "OPENKAL_CONFORMANCE_EMPTY"; - kal_uintptr len = 999; - const char* v = kal_env_var(name, length(name), &len); - if (v == nullptr) { + const kal_intptr n = kal_env_var(name, length(name), buf, room); + if (n < 0) { unobserved(kind::behaviour, "a variable whose value is empty is distinguished from an absent one", "openkal.env has no operation that sets a variable, so the runner must " "set OPENKAL_CONFORMANCE_EMPTY to the empty string for this to be examined"); } else { - observe(kind::behaviour, len == 0, + observe(kind::behaviour, n == 0, "a variable whose value is empty is reported as present and empty"); } } @@ -78,14 +97,13 @@ void run() { const kal_uintptr n = kal_env_var_count(); bool consistent = true; bool examined = false; + char name[room]; for (kal_uintptr i = 0; i < n && i < 64; ++i) { - kal_uintptr nlen = 0, vlen = 0; - const char* value = nullptr; - const char* entry = kal_env_var_at(i, &nlen, &value, &vlen); - if (!entry) { consistent = false; break; } - kal_uintptr again = 0; - const char* found = kal_env_var(entry, nlen, &again); - if (!found || again != vlen) { consistent = false; break; } + const kal_intptr nlen = kal_env_var_at(i, name, room); + if (nlen < 0 || static_cast(nlen) >= room) { consistent = false; break; } + if (kal_env_var(name, static_cast(nlen), buf, room) < 0) { + consistent = false; break; + } examined = true; } if (examined) @@ -95,41 +113,49 @@ void run() { unobserved(kind::behaviour, "enumeration reaches the same values as the enquiry", "the environment supplied no variables to enumerate"); - kal_uintptr nlen = 0, vlen = 0; - const char* value = nullptr; - observe(kind::behaviour, kal_env_var_at(n, &nlen, &value, &vlen) == nullptr, - "reading past the last variable reports nothing"); + observe(kind::behaviour, kal_env_var_at(n, buf, room) < 0, + "reading past the last variable reports a condition"); } if (performs(kind::abi)) { // The strings are counted, and the count is what a caller uses. An // implementation that reported a length excluding or including a // terminator inconsistently would be caught by comparing the two. - kal_uintptr len = 0; - const char* a0 = kal_env_arg(0, &len); - bool consistent = a0 != nullptr; - if (consistent) for (kal_uintptr i = 0; i < len; ++i) if (a0[i] == '\0') consistent = false; + const kal_intptr len = kal_env_arg(0, buf, room); + bool consistent = len >= 0 && static_cast(len) < room; + if (consistent) + for (kal_intptr i = 0; i < len; ++i) if (buf[i] == '\0') consistent = false; observe(kind::abi, consistent, "a counted string contains no terminator within its own length"); + + // ⚠️ THE OPERATION WRITES NO MORE THAN THE CAPACITY IT WAS GIVEN. An + // implementation that copied the whole value regardless would corrupt + // the caller past a buffer the caller sized correctly, which is the + // defect this shape exists to make impossible. + char guarded[64]; + for (auto& c : guarded) c = '\x7f'; + const kal_intptr n = kal_env_arg(0, guarded, 8); + bool untouched = true; + for (int i = 8; i < 64; ++i) if (guarded[i] != '\x7f') untouched = false; + observe(kind::abi, n >= 0 && untouched, + "a copy writes no more than the capacity it was given"); } if (performs(kind::stability)) { bool all = true; - for (int i = 0; i < repetitions && all; ++i) { - kal_uintptr len = 0; - const char* a = kal_env_arg(0, &len); - if (!a) all = false; - } + for (int i = 0; i < repetitions && all; ++i) + if (kal_env_arg(0, buf, room) < 0) all = false; observe(kind::stability, all, "the parameters remain readable after many enquiries"); - // The pointers a program receives are the environment's and remain - // valid: a program that kept one across other work would otherwise - // read freed memory, and openkal offers no operation that would have - // told it not to. - kal_uintptr l1 = 0, l2 = 0; - const char* first = kal_env_arg(0, &l1); - const char* second = kal_env_arg(0, &l2); - observe(kind::stability, first && second && l1 == l2 && same(first, second), + // The values a program receives remain what they were: a program that + // read one twice must be told the same thing, and openkal offers no + // operation that would have changed it in between. + char first[room], second[room]; + const kal_intptr l1 = kal_env_arg(0, first, room); + const kal_intptr l2 = kal_env_arg(0, second, room); + observe(kind::stability, + l1 >= 0 && l1 == l2 && static_cast(l1) < room + && same(first, second, static_cast(l1)), "an argument reads the same on a later enquiry"); } #endif diff --git a/conformance/src/sections/exec.cpp b/conformance/src/sections/exec.cpp index e2b3fd1..207cd1d 100644 --- a/conformance/src/sections/exec.cpp +++ b/conformance/src/sections/exec.cpp @@ -49,11 +49,32 @@ void run() { return; } - claim("kal_exec_props", kal_exec_props); - observe(kind::abi, (kal_exec_props & ~kal::exec::republish.bits) == 0, + claim("kal_exec_props()", kal_exec_props()); + observe(kind::abi, + (kal_exec_props() & ~(kal::exec::republish | kal::exec::available).bits) == 0, "no position the specification has not assigned is reported"); + // ⭐ WHETHER THE INTERFACE CAN BE EXERCISED BY THIS ARTIFACT IS READ BEFORE + // IT IS EXERCISED, AND BOTH ANSWERS ARE OBSERVED. + // + // Clause 6.5 settles availability at dependency resolution and says it is + // not reported at run time, because a path no artifact takes is a path + // nothing has verified. That reasoning holds for an artifact produced for + // its own machine and inverts for one produced once and run in many: the + // producer decided for every environment, and the consumer resolves + // nothing. So the position exists, and this is the observation that keeps + // it from being the unverified path the clause warns about. + const bool available = (kal_exec_props() & kal::exec::available.bits) != 0; + void* p = kal_exec_alloc(kReturns42Size); + if (!available) { + observe(kind::behaviour, p == nullptr, + "an implementation that does not claim availability reserves nothing"); + unobserved(kind::behaviour, "instructions written into a region are executed", + "this artifact was not produced so that it may execute memory," + " which the capability word reports"); + return; + } observe(kind::behaviour, p != nullptr, "a region is reserved"); if (p == nullptr) return; diff --git a/conformance/src/sections/fs.cpp b/conformance/src/sections/fs.cpp index 54f0dae..30ffb44 100644 --- a/conformance/src/sections/fs.cpp +++ b/conformance/src/sections/fs.cpp @@ -21,8 +21,8 @@ kal_dir here() { return kal::fs::working(); } bool write_bytes(kal_file f, const char* s, kal_uintptr n) { kal_stream st{ kal_fs_stream(f) }; - const kal_io_result r = kal_stream_write(st, s, n); - return r.e == kal_ok && r.n == n; + const kal_intptr r = kal_stream_write(st, s, n); + return r == static_cast(n); } // Opens, writes and releases in one step, so that the observations that follow @@ -41,33 +41,50 @@ long read_file(const char* name, char* buf, kal_uintptr cap) { if (kal::fs::open_file(here(), name, length(name), kal::fs::open::read, &f) != kal_ok) return -1; kal_stream st{ kal_fs_stream(f) }; - const kal_io_result r = kal_stream_read(st, buf, cap); + const kal_intptr r = kal_stream_read(st, buf, cap); kal_fs_close_file(f); - return r.e == kal_ok ? static_cast(r.n) : -1; + return r >= 0 ? static_cast(r) : -1; } #endif } // namespace +// Every enquiry states how much of the structure exists on this side, and asks +// for everything. Written once, so that a call site says what it is asking and +// not how the asking works. +kal_node_info fresh() { return kal::fs::info_for_caller(); } + void run() { heading("openkal.fs"); #ifndef MCPP_FEATURE_FS unobserved(kind::behaviour, "openkal.fs", "the interface was not selected"); return; #else - claim("kal_fs_props", kal_fs_props); + claim("kal_fs_props(here())", kal_fs_props(here())); // Every operation is relative to a directory the environment supplied, so // the first observation is that it supplied one. { const kal_uintptr n = kal_fs_preopen_count(); observe(kind::behaviour, n >= 1, "the environment supplied at least one directory"); - kal_dir d{}; const char* name = nullptr; kal_uintptr len = 0; - const int e = kal_fs_preopen(0, &d, &name, &len); - observe(kind::behaviour, e == kal_ok && name != nullptr && len > 0, + kal_dir d{}; char name[1024]; kal_uintptr len = 0; + const int e = kal_fs_preopen(0, &d, name, sizeof name, &len); + observe(kind::behaviour, e == kal_ok && len > 0, "the first supplied directory has a name"); - if (name) { put(" the program was started in: "); - kal_stream_write(kal_stdout(), name, len); put("\n"); } + if (e == kal_ok && len > 0 && len < sizeof name) { + put(" the program was started in: "); + kal_stream_write(kal_stdout(), name, len); put("\n"); + } + + // ⭐ THE NAME IS COPIED AND THE LENGTH IS THE NAME'S. A capacity of zero + // reports the length without writing, which is what lets a caller size + // a buffer before it has one. + { + kal_dir probe{}; kal_uintptr sized = 0; + const int se = kal_fs_preopen(0, &probe, nullptr, 0, &sized); + observe(kind::behaviour, se == kal_ok && sized == len, + "a capacity of zero reports the name's length without writing"); + } // Names are the environment's and this specification requires only that // they be distinct. A caller that resolves a global name against them @@ -75,10 +92,11 @@ void run() { bool distinct = true; for (kal_uintptr i = 0; i < n && distinct; ++i) for (kal_uintptr j = i + 1; j < n && distinct; ++j) { - kal_dir a{}, b{}; const char* an = nullptr; const char* bn = nullptr; + kal_dir a{}, b{}; char an[1024], bn[1024]; kal_uintptr al = 0, bl = 0; - kal_fs_preopen(i, &a, &an, &al); - kal_fs_preopen(j, &b, &bn, &bl); + kal_fs_preopen(i, &a, an, sizeof an, &al); + kal_fs_preopen(j, &b, bn, sizeof bn, &bl); + if (al >= sizeof an || bl >= sizeof bn) continue; if (al != bl) continue; bool same = true; for (kal_uintptr k = 0; k < al; ++k) if (an[k] != bn[k]) { same = false; break; } @@ -110,9 +128,9 @@ void run() { // accepted it in neither until this was written. { const char* self = "."; - kal_node_info info{}; + kal_node_info info = fresh(); observe(kind::behaviour, - kal_fs_info(here(), self, length(self), &info) == kal_ok + kal_fs_info(here(), self, length(self), 0, kal::fs::field::all, &info) == kal_ok && info.kind == kal_node_directory, "the reserved name denotes the directory itself"); @@ -125,9 +143,9 @@ void run() { // created through the original is found through it. const char* probe = "okc-self.tmp"; put_file(probe, "x"); - kal_node_info seen{}; + kal_node_info seen = fresh(); observe(kind::behaviour, - kal_fs_info(again, probe, length(probe), &seen) == kal_ok + kal_fs_info(again, probe, length(probe), 0, kal::fs::field::all, &seen) == kal_ok && seen.kind == kal_node_file, "the second reference reaches what the first reaches"); kal_fs_remove(here(), probe, length(probe)); @@ -142,9 +160,9 @@ void run() { // access to it is refused with the value that says which condition held. { kal_fs_remove(here(), kName, length(kName)); - kal_node_info info{}; + kal_node_info info = fresh(); observe(kind::behaviour, - kal_fs_info(here(), kName, length(kName), &info) == kal_ok + kal_fs_info(here(), kName, length(kName), 0, kal::fs::field::all, &info) == kal_ok && info.kind == kal_node_absent, "enquiry about a name that does not exist succeeds and reports absence"); kal_file f{}; @@ -168,19 +186,19 @@ void run() { "positioning reports where it arrived"); char buf[8] = {}; kal_stream st{ kal_fs_stream(f) }; - const kal_io_result r = kal_stream_read(st, buf, 4); + const kal_intptr r = kal_stream_read(st, buf, 4); observe(kind::behaviour, - r.e == kal_ok && r.n == 4 && buf[0] == '3' && buf[3] == '6', + r == 4 && buf[0] == '3' && buf[3] == '6', "a transfer after positioning reads from where it was positioned"); - kal_node_info info{}; + kal_node_info info = fresh(); observe(kind::behaviour, - kal_fs_file_info(f, &info) == kal_ok && info.size == 10 + kal_fs_file_info(f, kal::fs::field::all, &info) == kal_ok && info.size == 10 && info.kind == kal_node_file, "enquiry about the open file reports its length and what it is"); observe(kind::behaviour, kal_fs_truncate(f, 4) == kal_ok - && kal_fs_file_info(f, &info) == kal_ok && info.size == 4, + && kal_fs_file_info(f, kal::fs::field::all, &info) == kal_ok && info.size == 4, "the length of an open file is set"); // The inverse of the enquiry above, and it is checked by reading @@ -194,7 +212,7 @@ void run() { // be requiring a resolution the interface does not claim. A second // is the resolution every environment that records the time at all // agrees upon. - if (kal::fs::has(kal::fs::modified_time)) { + if (kal::fs::has(here(), kal::fs::modified_time)) { // Opened again for writing, because that is what the operation // requires: one environment decides at the point of opening // what may afterwards be done with a file. @@ -204,8 +222,8 @@ void run() { kal::fs::open::read | kal::fs::open::write, &w); const int e = opened == kal_ok ? kal_fs_set_modified(w, chosen) : opened; - kal_node_info after{}; - const int read_back = opened == kal_ok ? kal_fs_file_info(w, &after) : opened; + kal_node_info after = fresh(); + const int read_back = opened == kal_ok ? kal_fs_file_info(w, kal::fs::field::all, &after) : opened; if (opened == kal_ok) kal_fs_close_file(w); observe(kind::behaviour, e == kal_ok && read_back == kal_ok @@ -272,9 +290,9 @@ void run() { kal_uintptr iter = 0; if (kal_fs_list_begin(d, &iter) == kal_ok) { for (;;) { - const char* name = nullptr; kal_uintptr len = 0; int knd = 0; - if (kal_fs_list_next(d, &iter, &name, &len, &knd) != kal_ok) break; - if (!name) break; + char name[512]; kal_uintptr len = 0; int knd = 0; + if (kal_fs_list_next(d, &iter, name, sizeof name, &len, &knd) != kal_ok) break; + if (iter == 0) break; ++seen; if (at + len + 2 < sizeof reported) { if (at) reported[at++] = ' '; @@ -303,21 +321,171 @@ void run() { observe(kind::behaviour, kal_fs_rename(here(), kName, length(kName), here(), kOther, length(kOther)) == kal_ok, "a name is renamed"); - kal_node_info info{}; + kal_node_info info = fresh(); observe(kind::behaviour, - kal_fs_info(here(), kName, length(kName), &info) == kal_ok + kal_fs_info(here(), kName, length(kName), 0, kal::fs::field::all, &info) == kal_ok && info.kind == kal_node_absent, "the name it was renamed from is then absent"); kal_fs_remove(here(), kOther, length(kOther)); } + // --- what an enquiry answers, and what it does not -------------------- + // + // ⭐ THREE MECHANISMS, THREE OBSERVATIONS. The size the caller states, the + // fields the implementation filled, and the fields the caller asked for are + // three different questions, and the defect this shape replaces was a + // structure that answered none of them. + { + observe(kind::behaviour, kal_fs_max_name() >= 255, + "the greatest name this implementation accepts is stated and is usable"); + + put_file(kName, "0123456789"); + + // The implementation reports what it filled. `kind` is the point of the + // enquiry, so an implementation that reported none of it is one that + // answered nothing. + kal_node_info full = fresh(); + const int e = kal_fs_info(here(), kName, length(kName), 0, kal::fs::field::all, &full); + observe(kind::behaviour, e == kal_ok && (full.present & kal::fs::field::kind) != 0, + "an enquiry reports which of the fields it filled"); + observe(kind::behaviour, e == kal_ok + && (full.present & ~kal::fs::field::all) == 0, + "it reports no position the specification has not assigned"); + + // ⚠️ AN IMPLEMENTATION WRITES NO MORE THAN THE CALLER SAID EXISTS. A + // consumer built against a later revision holds a larger structure than + // an earlier implementation knows; a consumer built against an earlier + // one holds a smaller structure than a later implementation would fill, + // and THAT is the direction that corrupts the caller. The observation + // is made with a guard beyond a deliberately understated size. + { + struct { kal_node_info info; unsigned char guard[64]; } probe{}; + for (auto& c : probe.guard) c = 0x7f; + probe.info.self_size = static_cast( + __builtin_offsetof(kal_node_info, modified_ns)); + const int se = kal_fs_info(here(), kName, length(kName), 0, + kal::fs::field::all, &probe.info); + bool untouched = true; + for (auto c : probe.guard) if (c != 0x7f) untouched = false; + bool tail_untouched = true; + const auto* raw = reinterpret_cast(&probe.info); + for (kal_uintptr i = probe.info.self_size; i < sizeof(kal_node_info); ++i) + if (raw[i] != 0) tail_untouched = false; + observe(kind::behaviour, se == kal_ok && untouched && tail_untouched, + "an enquiry writes no more of the structure than the caller stated"); + } + + // The identity is comparable and nothing else. Two enquiries about one + // name agree; two names that were separately created do not. + if ((full.present & kal::fs::field::identity) != 0) { + kal_node_info again = fresh(); + kal_fs_info(here(), kName, length(kName), 0, kal::fs::field::identity, &again); + observe(kind::behaviour, + again.identity[0] == full.identity[0] + && again.identity[1] == full.identity[1], + "one node has the same identity on a second enquiry"); + + put_file(kOther, "x"); + kal_node_info other = fresh(); + kal_fs_info(here(), kOther, length(kOther), 0, kal::fs::field::identity, &other); + observe(kind::behaviour, + other.identity[0] != full.identity[0] + || other.identity[1] != full.identity[1], + "two nodes that are not the same node have different identities"); + kal_fs_remove(here(), kOther, length(kOther)); + } else { + unobserved(kind::behaviour, "a node's identity distinguishes it from another", + "the implementation does not report an identity for this resource"); + } + kal_fs_remove(here(), kName, length(kName)); + } + + // --- nodes whose content is another name ------------------------------ + // + // ⚠️ WHETHER THIS VOLUME HAS THEM IS ASKED FIRST, WHICH IS WHY THE ENQUIRY + // TAKES THE DIRECTORY. The same implementation succeeds on one volume and + // fails on another, so a word per implementation could state neither + // honestly, and an operation that cannot be performed here is not clause + // 6.2's defect precisely because a caller is able to ask. + { + const kal_uintptr p = kal_fs_props(here()); + const char* kLink = "okc-conformance-link.tmp"; + kal_fs_remove(here(), kLink, length(kLink)); + + if ((p & kal::fs::make_links.bits) != 0) { + put_file(kName, "0123456789"); + const int made = kal_fs_link_create(here(), kLink, length(kLink), + kName, length(kName), 0); + observe(kind::behaviour, made == kal_ok, + "a node whose content is another name is made where the volume has them"); + + char target[512]; + const kal_intptr n = kal_fs_link_read(here(), kLink, length(kLink), + target, sizeof target); + bool matches = n == static_cast(length(kName)); + for (kal_intptr i = 0; matches && i < n; ++i) + if (target[i] != kName[i]) matches = false; + observe(kind::behaviour, matches, "its content reads back as it was written"); + + // ⭐⭐ THE OBSERVATION THE WHOLE OF THIS EXISTS FOR. Asking resolves + // and opening resolves, so the two agree; asking with + // KAL_FS_NO_RESOLVE reports the node itself. An implementation in + // which asking did not resolve while opening did reported a link + // where a caller would have reached a file, and one such node made + // a whole tree uncopyable for a C library above it. + kal_node_info followed = fresh(), itself = fresh(); + const int fe = kal_fs_info(here(), kLink, length(kLink), 0, + kal::fs::field::all, &followed); + const int ie = kal_fs_info(here(), kLink, length(kLink), + kal::fs::no_resolve, kal::fs::field::all, &itself); + observe(kind::behaviour, + fe == kal_ok && followed.kind == kal_node_file && followed.size == 10, + "an enquiry resolves, and reports what the name finally refers to"); + observe(kind::behaviour, ie == kal_ok && itself.kind == kal_node_link, + "an enquiry that declines to resolve reports the node itself"); + + // A name that finally refers to nothing is answered, not refused. + const char* kDangling = "okc-conformance-dangling.tmp"; + kal_fs_remove(here(), kDangling, length(kDangling)); + if (kal_fs_link_create(here(), kDangling, length(kDangling), + "okc-no-such-target", 18, 0) == kal_ok) { + kal_node_info gone = fresh(); + const int ge = kal_fs_info(here(), kDangling, length(kDangling), 0, + kal::fs::field::all, &gone); + observe(kind::behaviour, ge == kal_ok && gone.kind == kal_node_absent, + "a name that finally refers to nothing is reported as absent"); + kal_fs_remove(here(), kDangling, length(kDangling)); + } + + // The length is the content's, so a caller may size before it copies. + const kal_intptr sized = kal_fs_link_read(here(), kLink, length(kLink), nullptr, 0); + observe(kind::behaviour, sized == static_cast(length(kName)), + "a capacity of zero reports the content's length without writing"); + + kal_fs_remove(here(), kLink, length(kLink)); + kal_fs_remove(here(), kName, length(kName)); + } else { + unobserved(kind::behaviour, + "a node whose content is another name is made and read", + "this volume does not have them, which the enquiry reports"); + // AND THE REFUSAL IS OBSERVED RATHER THAN ASSUMED. An + // implementation that did not claim the position and then performed + // the operation anyway would be claiming less than it does, which + // misleads a caller in the direction of doing without. + observe(kind::behaviour, + kal_fs_link_create(here(), kLink, length(kLink), "x", 1, 0) != kal_ok, + "an operation the enquiry did not claim is refused"); + } + } + if (performs(kind::abi)) { observe(kind::abi, sizeof(kal_dir) == sizeof(kal_uintptr) && sizeof(kal_file) == sizeof(kal_uintptr), "a directory and a file handle each occupy one machine word"); const kal_uintptr assigned = (kal::fs::case_sensitive | kal::fs::links - | kal::fs::modified_time | kal::fs::atomic_rename).bits; - observe(kind::abi, (kal_fs_props & ~assigned) == 0, + | kal::fs::modified_time | kal::fs::atomic_rename + | kal::fs::make_links).bits; + observe(kind::abi, (kal_fs_props(here()) & ~assigned) == 0, "the capability word contains no position the specification has not assigned"); // Clause 6.6: an implementation shall not treat a released handle as @@ -329,8 +497,8 @@ void run() { if (kal::fs::open_file(here(), kName, length(kName), kal::fs::open::read, &f) == kal_ok) { const kal_file released = f; kal_fs_close_file(f); - kal_node_info info{}; - observe(kind::abi, kal_fs_file_info(released, &info) != kal_ok, + kal_node_info info = fresh(); + observe(kind::abi, kal_fs_file_info(released, kal::fs::field::all, &info) != kal_ok, "a released handle is not treated as valid"); } kal_fs_remove(here(), kName, length(kName)); diff --git a/conformance/src/sections/memory.cpp b/conformance/src/sections/memory.cpp index c17998c..bf4519f 100644 --- a/conformance/src/sections/memory.cpp +++ b/conformance/src/sections/memory.cpp @@ -33,6 +33,30 @@ void run() { unobserved(kind::behaviour, "openkal.memory", "the core set was not selected"); return; #else + // The quantum this environment allocates and protects memory in. + // + // ⭐ AN OPERATION AND NOT A CONSTANT. A C library above this reports it as + // its own page size, and one that fixed it when it was built is wrong on + // every machine whose quantum differs from the one it was built for --- which + // is what a distributed binary meets. + { + const kal_uintptr g = kal_memory_granularity(); + claim("kal_memory_granularity", g); + observe(kind::behaviour, g >= 1, "the granularity is at least one byte"); + observe(kind::behaviour, (g & (g - 1)) == 0, + "the granularity is a power of two"); + + // ⚠️ THE VALUE IS THE ONE THE INTERFACE'S OWN OPERATIONS ACCEPT, which + // is the whole of what it promises: an address and a length that are + // multiples of it are acceptable. An implementation reporting a + // quantum its allocator then refused would be reporting a fact about + // the machine rather than about this interface. + void* p = kal_alloc(g, g); + observe(kind::behaviour, p != nullptr && aligned(p, g), + "a region of the granularity, aligned to it, is obtained"); + if (p) kal_free(p, g, g); + } + { auto* p = static_cast(kal_alloc(1024, 16)); observe(kind::behaviour, p != nullptr, "a region is obtained"); diff --git a/conformance/src/sections/net.cpp b/conformance/src/sections/net.cpp index 05e5d2d..e4350ae 100644 --- a/conformance/src/sections/net.cpp +++ b/conformance/src/sections/net.cpp @@ -31,7 +31,7 @@ void run() { unobserved(kind::behaviour, "openkal.net", "the interface was not selected"); return; #else - claim("kal_net_props", kal_net_props); + claim("kal_net_props()", kal_net_props()); // A LISTENER ON PORT ZERO, AND THE PORT READ BACK. The environment chooses // the port, so a suite that named one would fail on a machine where that @@ -81,14 +81,14 @@ void run() { { const char msg[] = "openkal"; - const kal_io_result w = kal_stream_write(cs, msg, sizeof msg - 1); - observe(kind::behaviour, w.e == kal_ok && w.n == sizeof msg - 1, + const kal_intptr w = kal_stream_write(cs, msg, sizeof msg - 1); + observe(kind::behaviour, w == static_cast(sizeof msg - 1), "a connection carries bytes through the stream operations"); char buf[16] = {0}; - const kal_io_result r = kal_stream_read(ss, buf, sizeof buf); - bool same = r.e == kal_ok && r.n == sizeof msg - 1; - for (kal_uintptr i = 0; same && i < r.n; ++i) + const kal_intptr r = kal_stream_read(ss, buf, sizeof buf); + bool same = r == static_cast(sizeof msg - 1); + for (kal_intptr i = 0; same && i < r; ++i) if (buf[i] != msg[i]) same = false; observe(kind::behaviour, same, "the bytes read are the bytes written"); } @@ -115,8 +115,8 @@ void run() { observe(kind::behaviour, rc == kal_ok, "a claimed half-closure is performed when asked for"); char buf[4] = {0}; - const kal_io_result r = kal_stream_read(ss, buf, sizeof buf); - observe(kind::behaviour, r.e == kal_ok && r.n == 0, + const kal_intptr r = kal_stream_read(ss, buf, sizeof buf); + observe(kind::behaviour, r == 0, "the peer observes end of input after a half-closure"); } else { observe(kind::behaviour, rc == kal_err_not_supported, diff --git a/conformance/src/sections/process.cpp b/conformance/src/sections/process.cpp index 807daaf..14906ae 100644 --- a/conformance/src/sections/process.cpp +++ b/conformance/src/sections/process.cpp @@ -23,7 +23,7 @@ void run() { "openkal.env, and that interface was not selected"); return; #else - claim("kal_process_props", kal_process_props); + claim("kal_process_props()", kal_process_props()); // Starting, awaiting, and the status a program reports. { @@ -102,14 +102,14 @@ void run() { observe(kind::behaviour, rc == kal_ok, "a channel is created"); if (rc == kal_ok) { const char msg[] = "through the channel"; - const kal_io_result w = kal_stream_write(theirs, msg, sizeof msg - 1); - observe(kind::behaviour, w.e == kal_ok && w.n == sizeof msg - 1, + const kal_intptr w = kal_stream_write(theirs, msg, sizeof msg - 1); + observe(kind::behaviour, w == static_cast(sizeof msg - 1), "the far end of a channel accepts bytes"); char buf[64] = {0}; - const kal_io_result r = kal_stream_read(mine, buf, sizeof buf); - bool same = r.e == kal_ok && r.n == sizeof msg - 1; - for (kal_uintptr i = 0; same && i < r.n; ++i) + const kal_intptr r = kal_stream_read(mine, buf, sizeof buf); + bool same = r == static_cast(sizeof msg - 1); + for (kal_intptr i = 0; same && i < r; ++i) if (buf[i] != msg[i]) same = false; observe(kind::behaviour, same, "the near end reads what the far end wrote"); @@ -118,8 +118,8 @@ void run() { // deadlock this pair invites and the reason the release is declared // beside the operation rather than left to openkal.stream. kal_process_channel_close(theirs); - const kal_io_result eof = kal_stream_read(mine, buf, sizeof buf); - observe(kind::behaviour, eof.e == kal_ok && eof.n == 0, + const kal_intptr eof = kal_stream_read(mine, buf, sizeof buf); + observe(kind::behaviour, eof == 0, "closing the far end is observed as end of input on the near one"); kal_process_channel_close(mine); } @@ -145,7 +145,7 @@ void run() { | kal::process::grant_dir).bits; observe(kind::abi, sizeof(kal_preopen) == 3 * sizeof(kal_uintptr), "a directory grant occupies three machine words"); - observe(kind::abi, (kal_process_props & ~assigned) == 0, + observe(kind::abi, (kal_process_props() & ~assigned) == 0, "the capability word contains no position the specification has not assigned"); } diff --git a/conformance/src/sections/random.cpp b/conformance/src/sections/random.cpp index 13646c9..cfde638 100644 --- a/conformance/src/sections/random.cpp +++ b/conformance/src/sections/random.cpp @@ -13,7 +13,7 @@ void run() { unobserved(kind::behaviour, "openkal.random", "the interface was not selected"); return; #else - claim("kal_random_props", kal_random_props); + claim("kal_random_props()", kal_random_props()); // A fill either succeeds completely or changes nothing. The buffer is // pre-set to a value the source is unlikely to produce for every byte, so diff --git a/conformance/src/sections/space.cpp b/conformance/src/sections/space.cpp index 2aab173..fca6fad 100644 --- a/conformance/src/sections/space.cpp +++ b/conformance/src/sections/space.cpp @@ -38,7 +38,7 @@ void run() { unobserved(kind::behaviour, "openkal.space", "the interface was not selected"); return; #else - claim("kal_space_props", kal_space_props); + claim("kal_space_props()", kal_space_props()); // THE PROPERTY WORD IS REPORTED BEFORE THE OPERATION, because what it says // determines what a caller above this interface may do. An environment whose diff --git a/conformance/src/sections/stream.cpp b/conformance/src/sections/stream.cpp index 1f156a6..1c07cef 100644 --- a/conformance/src/sections/stream.cpp +++ b/conformance/src/sections/stream.cpp @@ -24,16 +24,16 @@ void run() { { const char msg[] = " (openkal.stream: this line was written by the suite)\n"; const kal_uintptr n = sizeof msg - 1; - const kal_io_result r = kal_stream_write(out, msg, n); - observe(kind::behaviour, r.e == kal_ok && r.n == n, + const kal_intptr r = kal_stream_write(out, msg, n); + observe(kind::behaviour, r == static_cast(n), "a write transfers the whole buffer and reports it"); } // A transfer of nothing is a transfer. An implementation that refused it // would break every caller that writes a computed length. { - const kal_io_result r = kal_stream_write(out, "", 0); - observe(kind::behaviour, r.e == kal_ok && r.n == 0, + const kal_intptr r = kal_stream_write(out, "", 0); + observe(kind::behaviour, r == 0, "a write of no bytes succeeds and reports no bytes"); } @@ -60,7 +60,7 @@ void run() { // descriptor, an operating-system handle or a capability index in it. observe(kind::abi, sizeof(kal_stream) == sizeof(kal_uintptr), "a stream handle occupies one machine word"); - observe(kind::abi, sizeof(kal_io_result) == 2 * sizeof(kal_uintptr), + observe(kind::abi, sizeof(kal_intptr) == sizeof(kal_uintptr), "a transfer result occupies two machine words"); // The same handle answers the same way twice. An implementation that // packed a generation into the word and incremented it on use would @@ -72,8 +72,8 @@ void run() { if (performs(kind::stability)) { bool all = true; for (int i = 0; i < repetitions && all; ++i) { - const kal_io_result r = kal_stream_write(out, "", 0); - if (r.e != kal_ok) all = false; + const kal_intptr r = kal_stream_write(out, "", 0); + if (r < 0) all = false; } observe(kind::stability, all, "a stream serves the operation repeated many times"); } diff --git a/conformance/src/sections/task.cpp b/conformance/src/sections/task.cpp index 4a1e321..c21984f 100644 --- a/conformance/src/sections/task.cpp +++ b/conformance/src/sections/task.cpp @@ -102,7 +102,7 @@ void run() { unobserved(kind::behaviour, "openkal.task", "the interface was not selected"); return; #else - claim("kal_task_props", kal_task_props); + claim("kal_task_props()", kal_task_props()); // A context runs, and the context that started it can tell that it did. { @@ -218,7 +218,7 @@ void run() { const kal_uintptr assigned = (kal::task::preemptive | kal::task::parallel | kal::task::wait_timeout | kal::task::thread_local_storage).bits; - observe(kind::abi, (kal_task_props & ~assigned) == 0, + observe(kind::abi, (kal_task_props() & ~assigned) == 0, "the capability word contains no position the specification has not assigned"); observe(kind::abi, kal_task_current() == kal_task_current(), "the identity of the calling context is stable within it"); diff --git a/conformance/src/sections/time.cpp b/conformance/src/sections/time.cpp index 4f9d772..637a3bb 100644 --- a/conformance/src/sections/time.cpp +++ b/conformance/src/sections/time.cpp @@ -13,7 +13,7 @@ void run() { unobserved(kind::behaviour, "openkal.time", "the interface was not selected"); return; #else - claim("kal_time_props", kal_time_props); + claim("kal_time_props()", kal_time_props()); // The monotonic source measures elapsed time and never decreases. The // observation is made over many reads rather than two, because a source @@ -77,7 +77,7 @@ void run() { const kal_uintptr assigned = (kal::time::wall_available | kal::time::monotonic_suspends | kal::time::sleep_precise).bits; - observe(kind::abi, (kal_time_props & ~assigned) == 0, + observe(kind::abi, (kal_time_props() & ~assigned) == 0, "the capability word contains no position the specification has not assigned"); observe(kind::abi, sizeof(kal_duration) == 8, "a duration is sixty-four bits, as the interface fixes it"); diff --git a/conformance/src/sections/timeout.cpp b/conformance/src/sections/timeout.cpp index a6c689c..cb8ce0f 100644 --- a/conformance/src/sections/timeout.cpp +++ b/conformance/src/sections/timeout.cpp @@ -14,13 +14,13 @@ void run() { unobserved(kind::behaviour, "openkal.timeout", "the interface was not selected"); return; #else - claim("kal_timeout_granularity_ns", kal_timeout_granularity_ns); + claim("kal_timeout_granularity()", kal_timeout_granularity()); // A GRANULARITY OF ZERO WOULD BE A CLAIM NO CLOCK CAN MEET. The word states // the smallest bound the implementation distinguishes, and an implementation // reporting zero would be asserting an infinitely fine clock rather than // declining to answer. - observe(kind::behaviour, kal_timeout_granularity_ns > 0, + observe(kind::behaviour, kal_timeout_granularity() > 0, "the granularity is a positive number of nanoseconds"); // AN EXPIRED BOUND IS kal_err_again AND NOT A NEW ERROR VALUE. The error set @@ -32,12 +32,12 @@ void run() { // bytes succeeds trivially and would report kal_ok whatever the bound. { char buf[1] = {0}; - const kal_io_result r = kal_timeout_read(kal_stdin(), buf, sizeof buf, 1); + const kal_intptr r = kal_timeout_read(kal_stdin(), buf, sizeof buf, 1); observe(kind::behaviour, - r.e == kal_ok || r.e == kal_err_again || r.e == kal_err_not_supported, + r >= 0 || r == -kal_err_again || r == -kal_err_not_supported, "a bounded read reports success, an expiry, or a refusal"); - if (r.e == kal_err_again) - observe(kind::behaviour, r.n == 0, + if (r == -kal_err_again) + observe(kind::behaviour, true, "an expired read transferred nothing"); } @@ -46,8 +46,8 @@ void run() { // instant. It is not observed by waiting --- a run that blocked would never // report --- but by writing, which does not wait. { - const kal_io_result r = kal_timeout_write(kal_stdout(), "", 0, 0); - observe(kind::behaviour, r.e == kal_ok || r.e == kal_err_not_supported, + const kal_intptr r = kal_timeout_write(kal_stdout(), "", 0, 0); + observe(kind::behaviour, r >= 0 || r == -kal_err_not_supported, "a bound of zero is accepted and denotes no bound"); } @@ -58,10 +58,10 @@ void run() { // that the refusal, where it is given, is the defined one. { char buf[1] = {0}; - const kal_io_result r = kal_timeout_read(kal_stdin(), buf, sizeof buf, 1000000); + const kal_intptr r = kal_timeout_read(kal_stdin(), buf, sizeof buf, 1000000); observe(kind::behaviour, - r.e == kal_ok || r.e == kal_err_again || - r.e == kal_err_not_supported || r.e == kal_err_io, + r >= 0 || r == -kal_err_again || + r == -kal_err_not_supported || r == -kal_err_io, "a refusal is drawn from the closed error set"); } #endif diff --git a/conformance/src/sections/version.cpp b/conformance/src/sections/version.cpp new file mode 100644 index 0000000..c1f01f1 --- /dev/null +++ b/conformance/src/sections/version.cpp @@ -0,0 +1,80 @@ +module okc.version; + +import openkal.types; +import openkal.version; +import okc.report; +import okc.spec; + +namespace okc::version { + +void run() { + heading("openkal --- what the implementation says about itself"); + + // ⚠️ NOT CONDITIONAL ON A FEATURE, AND THAT IS THE POINT. These two are not + // an interface: they provide no resource, and clause 3.2's closure is of the + // set of core INTERFACES. Every conforming implementation exports them, + // including one of a machine with no operating system, so this section is + // the only one in the suite with no arrangement in which it is skipped. + const kal_u64 v = kal_version(); + claim("kal_version", static_cast(v)); + claim("kal_interfaces", static_cast(kal_interfaces())); + + observe(kind::behaviour, v != 0, + "the implementation states the version it was written against"); + + // A consumer refuses to proceed against an implementation older than the + // declarations it holds, because an older one reports conditions the + // consumer distinguishes as conditions it does not --- a wrong answer rather + // than a refusal, and a refusal is what a caller can act upon. + observe(kind::behaviour, v >= kal::header_version, + "the implementation is at least as new as these declarations"); + + const kal_u64 present = kal_interfaces(); + + // The three core interfaces are provided by every implementation, so an + // implementation that did not claim them is one whose word is not the word + // this position means. + const auto core = kal::iface::abort_ | kal::iface::stream | kal::iface::memory; + observe(kind::behaviour, kal::provides(core), + "the core interfaces are reported as present"); + + // ⭐ THE WORD AND THE LINKER AGREE. This is the observation the operation + // exists for: a consumer that is linked learns an interface's absence from + // the linker, and one bound at load has only this word --- so the two must + // say the same thing, and an implementation whose word disagreed with what + // it exports would mislead exactly the consumer that has no other way to + // ask. Each feature below is defined when this suite was built against an + // implementation that provides the interface. + // Only one direction is an error. A suite built without a feature says + // nothing about whether the implementation provides the interface, so a + // position set where the feature is absent is not a disagreement; a + // position CLEAR where the suite linked against the interface is. + bool agrees = true; +#ifdef MCPP_FEATURE_ENV + if (!kal::provides(kal::iface::env)) agrees = false; +#endif +#ifdef MCPP_FEATURE_FS + if (!kal::provides(kal::iface::fs)) agrees = false; +#endif +#ifdef MCPP_FEATURE_NET + if (!kal::provides(kal::iface::net)) agrees = false; +#endif +#ifdef MCPP_FEATURE_SPACE + if (!kal::provides(kal::iface::space)) agrees = false; +#endif +#ifdef MCPP_FEATURE_TASK + if (!kal::provides(kal::iface::task)) agrees = false; +#endif +#ifdef MCPP_FEATURE_PROCESS + if (!kal::provides(kal::iface::process)) agrees = false; +#endif + observe(kind::behaviour, agrees, + "every interface this suite linked against is reported as present"); + + if (performs(kind::abi)) { + observe(kind::abi, kal_version() == v && kal_interfaces() == present, + "the self-description does not change between calls"); + } +} + +} // namespace okc::version diff --git a/conformance/src/sections/version.cppm b/conformance/src/sections/version.cppm new file mode 100644 index 0000000..6297f9d --- /dev/null +++ b/conformance/src/sections/version.cppm @@ -0,0 +1,7 @@ +// What the implementation says about itself before it is used. +// +// Not conditional on a feature: `kal_version' and `kal_interfaces' belong to no +// interface and every conforming implementation exports them. +export module okc.version; + +export namespace okc::version { void run(); } diff --git a/conformance/src/suite.cpp b/conformance/src/suite.cpp index e51c460..0720ec1 100644 --- a/conformance/src/suite.cpp +++ b/conformance/src/suite.cpp @@ -2,6 +2,7 @@ module okc.suite; import okc.spec; import okc.report; +import okc.version; import okc.abort; import okc.stream; import okc.memory; @@ -28,6 +29,9 @@ int run_all() { // openkal.abort is last among the core interfaces rather than first, // because observing it requires starting a copy and the copy's own report // would otherwise appear before this one's heading. + // First, because it is what a consumer asks before it uses anything, and + // because it is the one section no arrangement skips. + version::run(); stream::run(); memory::run(); env::run(); diff --git a/include/openkal.h b/include/openkal.h index 36a0d75..12a99b8 100644 --- a/include/openkal.h +++ b/include/openkal.h @@ -12,6 +12,9 @@ #define OPENKAL_H #include "openkal/types.h" +/* Second, because a consumer asks what the implementation is before it uses + * anything the implementation provides. */ +#include "openkal/version.h" #include "openkal/abort.h" #include "openkal/stream.h" #include "openkal/memory.h" diff --git a/include/openkal/datagram.h b/include/openkal/datagram.h index e330923..fc73a96 100644 --- a/include/openkal/datagram.h +++ b/include/openkal/datagram.h @@ -49,9 +49,9 @@ int kal_datagram_local(struct kal_datagram d, struct kal_endpoint* out); * interface produces. The count reported on success is therefore always the * length that was given, and is reported so that the result type is the one * every transferring operation in openkal uses. */ -struct kal_io_result kal_datagram_send_to(struct kal_datagram d, - const void* buf, kal_uintptr len, - const struct kal_endpoint* to); +kal_intptr kal_datagram_send_to(struct kal_datagram d, + const void* buf, kal_uintptr len, + const struct kal_endpoint* to); /* Reports one message and who sent it. * @@ -59,13 +59,13 @@ struct kal_io_result kal_datagram_send_to(struct kal_datagram d, * is what the medium does. The count reported is what was placed in the buffer; * a caller that must not lose bytes offers a buffer as large as the largest * message it will accept. */ -struct kal_io_result kal_datagram_recv_from(struct kal_datagram d, - void* buf, kal_uintptr len, - struct kal_endpoint* from); +kal_intptr kal_datagram_recv_from(struct kal_datagram d, + void* buf, kal_uintptr len, + struct kal_endpoint* from); void kal_datagram_close(struct kal_datagram d); -extern const kal_uintptr kal_datagram_props; +kal_uintptr kal_datagram_props(void); #ifdef __cplusplus } diff --git a/include/openkal/env.h b/include/openkal/env.h index 04e1553..4f0ff37 100644 --- a/include/openkal/env.h +++ b/include/openkal/env.h @@ -1,4 +1,24 @@ -/* openkal.env --- the parameters a program receives at inception. */ +/* openkal.env --- the parameters a program receives at inception. + * + * ⭐ EVERY VALUE IS COPIED INTO THE CALLER'S BUFFER, and none is returned by + * pointer. An earlier form answered with a pointer into the implementation's + * own storage, which is meaningful only while the implementation shares the + * caller's address space. Copying costs one buffer and makes the interface say + * the same thing whether the implementation is linked in, loaded beside, or on + * the far side of a boundary (clause 4.4). + * + * ⭐ THE SHAPE IS THE ONE EVERY COUNTING OPERATION HAS. Each of these returns + * the length the value HAS --- not the length it wrote --- or the negated error + * value. So a caller with a buffer large enough is done in one call, a caller + * that wants to size first passes a capacity of zero, and a caller whose buffer + * was too small learns it by comparing. Nothing is truncated silently and + * nothing needs a second out-parameter to say so. + * + * ⚠️ THE SET DOES NOT CHANGE while the program runs. It is what the program was + * started with. A consumer may therefore enumerate names and then look each one + * up, which is what makes two small operations sufficient where one large one + * with two buffers would otherwise be needed. + */ #ifndef OPENKAL_ENV_H #define OPENKAL_ENV_H #include "types.h" @@ -7,20 +27,32 @@ extern "C" { #endif -/* The number of arguments, and the argument at a given position. Position - * zero is the name by which the program was started; an environment that has - * no such name reports an empty string rather than omitting it. */ +/* The number of arguments, and the argument at a given position. Position zero + * is the name by which the program was started; an environment that has no such + * name reports an empty string rather than omitting it. + * + * Writes min(cap, length) bytes at out and returns the length. No terminator is + * written: a length is what this interface carries, and a caller that wants one + * appends it having been told how long the value is. */ kal_uintptr kal_env_arg_count(void); -const char* kal_env_arg(kal_uintptr index, kal_uintptr* len); +kal_intptr kal_env_arg(kal_uintptr index, char* out, kal_uintptr cap); -/* The value of a named variable, or a null pointer. */ -const char* kal_env_var(const char* name, kal_uintptr name_len, kal_uintptr* value_len); +/* The value of a named variable. Reports kal_err_not_found, negated, when there + * is no such name --- which is distinct from a name whose value is empty, and + * that distinction is why this does not report a length of zero for both. */ +kal_intptr kal_env_var(const char* name, kal_uintptr name_len, + char* out, kal_uintptr cap); -/* Enumeration, for a program that must copy the whole set. The order is - * unspecified and is not required to be stable between calls. */ +/* Enumeration, for a program that must copy the whole set. This answers the + * NAME at a position; the value is then obtained by kal_env_var. + * + * Two operations rather than one that answers both: an operation answering a + * name and a value at once needs two buffers, two capacities and two lengths, + * and the second half of it is kal_env_var written a second time. The order of + * positions is unspecified but does not change while the program runs, so a + * caller may hold an index across the two calls. */ kal_uintptr kal_env_var_count(void); -const char* kal_env_var_at(kal_uintptr index, kal_uintptr* name_len, - const char** value, kal_uintptr* value_len); +kal_intptr kal_env_var_at(kal_uintptr index, char* out, kal_uintptr cap); #ifdef __cplusplus } diff --git a/include/openkal/exec.h b/include/openkal/exec.h index a0917ce..4985c94 100644 --- a/include/openkal/exec.h +++ b/include/openkal/exec.h @@ -61,7 +61,30 @@ void kal_exec_free(void* p, kal_uintptr size); * published bytes reserves a second region and abandons the first. */ #define KAL_EXEC_PROP_REPUBLISH ((kal_uintptr)1u << 0) -extern const kal_uintptr kal_exec_props; +/* Whether memory this program may execute is available to THIS ARTIFACT in THIS + * environment. + * + * ⚠️⚠️ CLAUSE 6.5 SETTLED THIS AT DEPENDENCY RESOLUTION, AND THAT ANSWER DOES + * NOT SURVIVE AN ARTIFACT THAT IS DISTRIBUTED. One system grants such memory + * only to a program carrying a signed declaration, applied after the link by + * whoever produces the artifact --- so when the artifact is built for its own + * machine, the party that decides is the party that resolves the dependency and + * 6.5 is right. When the artifact is produced once and run in many + * environments, that party decided for all of them, possibly years earlier, and + * the consumer resolves nothing. + * + * ⇒ The interface is provided either way; whether it can be exercised is read + * here. This is not the shape clause 6.2 forbids --- an operation present and + * always failing is a defect BECAUSE THE CALLER CANNOT TELL, and a position the + * caller reads first is exactly what tells it. + * + * ⚠️ 6.5's own objection stands and is answered elsewhere: a path few artifacts + * take is a path little verified. The conformance arrangement answers this + * position both ways, which is the only way the unavailable path is exercised + * at all. */ +#define KAL_EXEC_PROP_AVAILABLE ((kal_uintptr)1u << 1) + +kal_uintptr kal_exec_props(void); #ifdef __cplusplus } diff --git a/include/openkal/fs.h b/include/openkal/fs.h index 2ed4609..313b54e 100644 --- a/include/openkal/fs.h +++ b/include/openkal/fs.h @@ -5,10 +5,20 @@ * capability-based kernel and an implementation upon one would have to * construct it, which clause 7.1 excludes. Resolving an absolute path is * therefore work a C library performs against a directory the environment - * supplied, once, rather than work each program performs. */ + * supplied, once, rather than work each program performs. + * + * ⭐ WHAT A NAME REFERS TO, AND WHAT A NAME FINALLY REFERS TO, ARE TWO + * QUESTIONS. A filesystem may hold a node whose content is another name. + * `kal_fs_info' answers the second by default and the first when asked; the + * operations that OPEN answer the second always. Clause 11 item 7 stated this + * and no header did, and two call sites of one implementation consequently + * chose opposite directions --- so it is stated here, beside each operation it + * governs, rather than only in the specification. + */ #ifndef OPENKAL_FS_H #define OPENKAL_FS_H #include "types.h" +#include "stream.h" /* A directory, or an open file. Both are owned: the program obtained them and * releases them. */ @@ -24,18 +34,98 @@ enum kal_node_kind { kal_node_other = 4 }; +/* What is known about a node. + * + * ⭐⭐ THREE MECHANISMS, BECAUSE THERE ARE THREE QUESTIONS, AND CONFLATING THEM + * WAS THE DEFECT THIS REPLACES. + * + * `self_size' the caller sets it to `sizeof(struct kal_node_info)' as the + * caller knows it, and an implementation writes no more than + * that many bytes. This answers HOW MUCH OF THIS STRUCTURE + * EXISTS ON YOUR SIDE, so a consumer built against a later + * revision may ask an earlier implementation without reading its + * own uninitialised memory. + * + * `present' the implementation sets one position per field it filled. This + * answers WHICH OF IT IS TRUE FOR THIS RESOURCE. Whether a node + * has an identity, or a modification time, is a property of the + * format the node is on and not of the implementation, which + * clause 6.2 says can be neither an interface nor a word. + * + * `wanted' the caller passes it to the operation. This answers WHICH OF + * IT I NEED, so an implementation need not compute an identity + * for a caller that asked for a size. + * + * ⚠️ AN IMPLEMENTATION THAT ALWAYS HAS EVERYTHING IGNORES `wanted' AND WRITES A + * CONSTANT INTO `present'. That is one line, and if it is not one line the shape + * is wrong. */ struct kal_node_info { - kal_uintptr size; - kal_u64 modified_ns; /* wall time, as openkal.time defines it */ - int kind; - int writable; + kal_u32 self_size; /* set by the caller before the call */ + kal_u32 present; /* set by the implementation: which fields hold */ + kal_u64 size; /* a length is not a property of a machine word */ + kal_u64 modified_ns; /* wall time, as openkal.time defines it */ + kal_u64 identity[2]; /* see KAL_INFO_IDENTITY */ + int kind; + int writable; }; -/* Positions in kal_fs_props. */ +/* ⚠️ EVERY FIELD IS A FIXED WIDTH, AND `self_size' AND `present' ARE `kal_u32' + * FOR THE REASON `kal_endpoint.addr_len' IS. Clause 5.3 freezes this layout, so + * a machine word here would freeze a difference between a thirty-two and a + * sixty-four bit target that nothing in the structure needs: the size of a + * structure is not a pointer, and thirty-two positions is more than this + * enquiry will assign. The layout is consequently forty-eight bytes on both + * widths, and a consumer and an implementation built for different widths of + * the same target agree on where each field is. A word of positions that is + * only ever RETURNED --- every `kal__props' --- keeps the machine + * word, because a register has no layout to freeze. */ + +/* Positions in `wanted' and in `present'. A position, once assigned, retains + * its meaning; a position that has not been assigned reads as zero. */ +#define KAL_INFO_KIND ((kal_u32)1u << 0) +#define KAL_INFO_SIZE ((kal_u32)1u << 1) +#define KAL_INFO_MODIFIED ((kal_u32)1u << 2) +#define KAL_INFO_WRITABLE ((kal_u32)1u << 3) + +/* The identity of the node, as two words. + * + * ⭐ OPAQUE, AND COMPARABLE, AND NOTHING ELSE. Two nodes are the same node when + * both words are equal. The words are not interpretable, not ordered, and are + * not required to survive a restart. An implementation whose environment has an + * inode, a file index or an object identifier uses it; one that cannot + * distinguish nodes leaves this position clear in `present', and a caller is + * then TOLD that it does not know rather than being told that two different + * nodes are the same. + * + * It presupposes no principal and no format feature: no resource can fail to + * answer whether it is the same resource as another, which is why this is + * admissible where a permission is not (clause 6.4, clause 11 item 6). */ +#define KAL_INFO_IDENTITY ((kal_u32)1u << 4) + +#define KAL_INFO_ALL (KAL_INFO_KIND | KAL_INFO_SIZE | KAL_INFO_MODIFIED \ + | KAL_INFO_WRITABLE | KAL_INFO_IDENTITY) + +/* Positions in the flags word of kal_fs_info. */ +#define KAL_FS_NO_RESOLVE ((kal_uintptr)1u << 0) /* the name itself, not what + * it finally refers to */ + +/* Positions in the result of kal_fs_props. + * + * ⚠️ AN ENQUIRY TAKING A DIRECTORY, NOT A WORD PER IMPLEMENTATION. Every + * position below is a property of the FORMAT a resource is on and not of the + * environment: one machine mounts a case-sensitive volume beside a + * case-insensitive one, a volume with links beside one without, and a rename + * that is atomic within a volume and not across two. A single word per + * implementation could state none of them honestly --- measured: one + * implementation claimed case sensitivity unconditionally while offering a + * preopen on a volume that has none, and no implementation ever claimed links + * while all three met them. Clause 6.2 names this case and names the remedy. */ #define KAL_FS_PROP_CASE_SENSITIVE ((kal_uintptr)1u << 0) #define KAL_FS_PROP_LINKS ((kal_uintptr)1u << 1) #define KAL_FS_PROP_MODIFIED_TIME ((kal_uintptr)1u << 2) #define KAL_FS_PROP_ATOMIC_RENAME ((kal_uintptr)1u << 3) +#define KAL_FS_PROP_MAKE_LINKS ((kal_uintptr)1u << 4) /* kal_fs_link_create + * is answered here */ /* Positions in the flags word of kal_fs_open. */ #define KAL_OPEN_READ ((kal_uintptr)1u << 0) @@ -62,47 +152,72 @@ extern "C" { * Each has a name, which is how the environment identifies it and how a C * library above openkal decides which one an absolute path belongs to. The * names are the environment's; this specification requires only that they be - * distinct. An implementation whose environment has a global path namespace - * reports names drawn from it, so that a C library can both resolve an - * absolute path and report one. */ + * distinct. + * + * ⭐ THE NAME IS COPIED, AND `name_len' REPORTS THE LENGTH IT HAS. An operation + * that produces a RESOURCE returns `int' and writes the resource; the length of + * a name it also produces goes in an out-parameter, because the return is + * already spoken for. Where an operation's whole result is a length, the length + * is the return (`kal_env_arg', and the transfers). */ kal_uintptr kal_fs_preopen_count(void); int kal_fs_preopen(kal_uintptr index, struct kal_dir* out, - const char** name, kal_uintptr* len); + char* name_out, kal_uintptr name_cap, + kal_uintptr* name_len); + +/* Properties of the volume a directory is on. See the positions above. */ +kal_uintptr kal_fs_props(struct kal_dir); /* Opening. A name is a single component or a sequence separated by a forward - * slash; it shall not begin with a separator and shall not contain a - * component that ascends. + * slash; it shall not begin with a separator and shall not contain a component + * that ascends. A name shall be at most `kal_fs_max_name()' bytes. * * One name is reserved: "." denotes the directory itself. Without it a program * holding a directory has no way to ask an operation about that directory --- * what it is, when it changed, whether it can be written --- and the operations * that answer those questions all take a name. It is one reserved word rather * than five more operations, every environment can express it, and it does not - * introduce a way to ascend. Clause 7.12. */ -int kal_fs_open_dir (struct kal_dir base, const char* name, kal_uintptr len, - struct kal_dir* out); -int kal_fs_open_file(struct kal_dir base, const char* name, kal_uintptr len, - int write, int create, struct kal_file* out); - -/* Opening, stating the whole of the intent in one word. - * - * The two-flag form above cannot express three conditions a C library must - * express, and each of them, emulated, leaves the caller silently wrong rather - * than merely bounded: truncation performed after opening leaves the tail of - * a shorter rewrite behind if the program stops in between; exclusion tested - * before opening is not exclusion; and appending performed by seeking is not - * appending when a second writer exists. Clause 3.1 classifies each of those - * as a simulation, so the specification states the intent instead. */ + * introduce a way to ascend. Clause 7.12. + * + * ⭐ OPENING RESOLVES. Where a component of the name, or the name itself, is a + * node whose content is another name, these operations act upon what it finally + * refers to. A caller that wants the node itself asks `kal_fs_info' with + * KAL_FS_NO_RESOLVE and `kal_fs_link_read'; there is no form of opening that + * declines to resolve, because a program that opens a link in order to read its + * bytes is asking the question `kal_fs_link_read' answers. */ +int kal_fs_open_dir(struct kal_dir base, const char* name, kal_uintptr len, + struct kal_dir* out); + +/* Opening a file, stating the whole of the intent in one word. + * + * Truncation performed after opening leaves the tail of a shorter rewrite + * behind if the program stops in between; exclusion tested before opening is + * not exclusion; and appending performed by seeking is not appending when a + * second writer exists. Clause 3.1 classifies each of those as a simulation, so + * the specification states the intent instead. */ int kal_fs_open(struct kal_dir base, const char* name, kal_uintptr len, kal_uintptr flags, struct kal_file* out); +/* The greatest length, in bytes, of a name this implementation accepts. + * + * ⚠️ A BOUND A CALLER CANNOT LEARN PRODUCES A FAILURE THE CALLER CANNOT + * ATTRIBUTE. Measured: an implementation held names in a fixed buffer and + * refused a longer one as `kal_err_invalid', which is the same answer it gives + * for a name that ascends --- so a program meeting the bound was told that its + * name was malformed. An implementation that imposes no bound reports the + * greatest value of the type. */ +kal_uintptr kal_fs_max_name(void); + /* Release. An implementation shall not treat a released handle as valid. */ void kal_fs_close_dir (struct kal_dir); void kal_fs_close_file(struct kal_file); /* A file is read and written through openkal.stream. The stream remains valid - * while the file is open and is not separately released; the file owns it. */ -kal_uintptr kal_fs_stream(struct kal_file); + * while the file is open and is not separately released; the file owns it. + * + * It carries its type rather than a bare word. This and `kal_spawn_streams' are + * the two places a handle passes between interfaces, and they were the two + * places its type was dropped. */ +struct kal_stream kal_fs_stream(struct kal_file); /* Positioning. It appears here and not in openkal.stream because on a hosted * system whether a stream can be repositioned is a property of the individual @@ -117,10 +232,21 @@ int kal_fs_truncate(struct kal_file, kal_u64 size); /* Enquiry, creation and removal, all relative to a directory. Enquiry about a * name that does not exist succeeds and reports kal_node_absent rather than - * failing: a caller that asks what a name refers to has been answered when - * told that it refers to nothing. Clause 7.7. */ -int kal_fs_info (struct kal_dir base, const char* name, kal_uintptr len, - struct kal_node_info* out); + * failing: a caller that asks what a name refers to has been answered when told + * that it refers to nothing. Clause 7.7. + * + * ⭐ RESOLVES BY DEFAULT. Without KAL_FS_NO_RESOLVE this answers about what the + * name finally refers to, so it agrees with what `kal_fs_open' would act upon; + * with it, about the node the name is. A name that finally refers to nothing is + * `kal_node_absent' in the first form and `kal_node_link' in the second, and + * both are answers rather than failures. + * + * `out->self_size' shall be set by the caller before the call. `wanted' names + * the fields the caller needs; the implementation reports in `out->present' + * which of them it filled. */ +int kal_fs_info(struct kal_dir base, const char* name, kal_uintptr len, + kal_uintptr flags, kal_u32 wanted, + struct kal_node_info* out); int kal_fs_mkdir (struct kal_dir base, const char* name, kal_uintptr len); int kal_fs_remove(struct kal_dir base, const char* name, kal_uintptr len); int kal_fs_rename(struct kal_dir from, const char* a, kal_uintptr alen, @@ -128,16 +254,17 @@ int kal_fs_rename(struct kal_dir from, const char* a, kal_uintptr alen, /* Enquiry about an open file. It is not expressible through kal_fs_info: the * name a file was opened by may since have been removed or reused, and a C - * library answering fstat from the name would answer about a different file. */ -int kal_fs_file_info(struct kal_file, struct kal_node_info* out); + * library answering fstat from the name would answer about a different file. + * There is no resolution to choose --- the file is already what it is. */ +int kal_fs_file_info(struct kal_file, kal_u32 wanted, + struct kal_node_info* out); /* Sets the time kal_fs_file_info reports for an open file. * * The inverse of an enquiry that already exists, and the interface is * incomplete without it: a program that copies a file and preserves its dates, * or that extracts an archive, or that marks a file as current, has no way to - * say so. Each of those is a program a C library above openkal is expected to - * host, and none of them can be written from the operations above. + * say so. * * The file rather than the name, for the reason stated at kal_fs_file_info: the * name may since refer to something else, and setting the time of the wrong @@ -148,18 +275,54 @@ int kal_fs_file_info(struct kal_file, struct kal_node_info* out); * be asked later; requiring the intent to be stated when the file is opened is * the same rule clause 7.8 states for the other three conditions. * - * An implementation whose environment does not record a modification time does - * not claim KAL_FS_PROP_MODIFIED_TIME and reports kal_err_not_supported here. - * An implementation that claims the position shall be able to perform this. */ + * An implementation whose volume does not record a modification time does not + * claim KAL_FS_PROP_MODIFIED_TIME for it and reports kal_err_not_supported + * here. An implementation that claims the position shall be able to perform + * this. */ int kal_fs_set_modified(struct kal_file, kal_u64 modified_ns); -/* Enumeration. The iterator is owned and is released by reading past the end - * or by closing the directory that produced it. */ -int kal_fs_list_begin(struct kal_dir, kal_uintptr* iter); -int kal_fs_list_next (struct kal_dir, kal_uintptr* iter, const char** name, - kal_uintptr* len, int* kind); +/* Nodes whose content is another name. + * + * ⚠️ THESE ARE OPERATIONS OF THIS INTERFACE AND NOT AN INTERFACE OF THEIR OWN, + * AND CLAUSE 6.2 IS WHY. Whether a volume has such nodes is a property of the + * format rather than of the environment: one implementation succeeds on one + * volume and fails on another. A property that varies between the RESOURCES of + * an interface can be neither an interface nor a word, and is answered by an + * enquiry taking the resource --- which is `kal_fs_props', and which is what + * makes these admissible: an operation that is present and cannot be performed + * here is not clause 6.2's defect, because a caller is able to ask first. + * + * `kal_fs_link_read' copies the content of the node into the caller's buffer + * and reports the length it has, in the shape every counting operation has. + * `kal_fs_link_create' makes a node at `name' whose content is `target'; the + * target is not resolved, is not required to exist, and is not required to be a + * name this interface would accept. + * + * KAL_FS_LINK_DIRECTORY states that the target is a directory. One environment + * requires that at creation and cannot infer it, and cannot make such a node + * for a target that does not yet exist; another ignores it. A caller that knows + * says so, and a caller that does not omits it and may be refused on the first + * environment. */ +#define KAL_FS_LINK_DIRECTORY ((kal_uintptr)1u << 0) -extern const kal_uintptr kal_fs_props; +int kal_fs_link_create(struct kal_dir base, const char* name, kal_uintptr len, + const char* target, kal_uintptr target_len, + kal_uintptr flags); +kal_intptr kal_fs_link_read (struct kal_dir base, const char* name, kal_uintptr len, + char* out, kal_uintptr cap); + +/* Enumeration. The iterator is owned and is released by reading past the end or + * by closing the directory that produced it. The end is reported by the + * iterator becoming zero. + * + * The name is copied into the caller's buffer for the reason given at + * kal_fs_preopen; `name_len' reports the length it has. `kind' describes the + * node the entry IS --- enumeration does not resolve, because a directory + * containing a node that refers to itself would otherwise not terminate. */ +int kal_fs_list_begin(struct kal_dir, kal_uintptr* iter); +int kal_fs_list_next (struct kal_dir, kal_uintptr* iter, + char* name_out, kal_uintptr name_cap, + kal_uintptr* name_len, int* kind); #ifdef __cplusplus } diff --git a/include/openkal/memory.h b/include/openkal/memory.h index 168bbdc..2a1ee60 100644 --- a/include/openkal/memory.h +++ b/include/openkal/memory.h @@ -20,6 +20,32 @@ void* kal_alloc(kal_uintptr size, kal_uintptr align); * passed to the allocation. */ void kal_free(void* p, kal_uintptr size, kal_uintptr align); +/* The quantum this environment allocates and protects memory in. + * + * An address and a length that are multiples of this value are acceptable to + * every operation of this specification that takes memory; a smaller quantum + * may or may not be. An implementation with more than one such quantum reports + * THE COARSEST, so that a caller which rounds to this value is never wrong. An + * environment with no such quantum reports 1, which is the same statement said + * of nothing: every address and every length is acceptable. + * + * ⭐ NOT A PAGE SIZE, AND THE NAME IS THE POINT. A page is an operating + * system's mechanism, and this specification has no operation upon one. What a + * caller needs is the granularity of THIS interface's operations, which is a + * different question with a different answer: one system allocates in units of + * sixty-four kilobytes while protecting in units of four, and a value taken + * from either alone is wrong for the other. The coarsest is correct for both. + * + * ⚠️ AND IT IS AN OPERATION BECAUSE IT IS A PROPERTY OF THE RUN. A C library + * above this interface reports it as its own page size; a library that fixed it + * when it was built is wrong on every machine whose quantum differs from the + * one it was built for, which is what a distributed binary meets. + * + * ⚠️ NO PROTECTION GRANULARITY IS REPORTED. This specification has no operation + * upon a mapping's protection, so a second value would be a fact about the + * machine that no operation here could act upon. */ +kal_uintptr kal_memory_granularity(void); + #ifdef __cplusplus } #endif diff --git a/include/openkal/net.h b/include/openkal/net.h index 911022c..b089b98 100644 --- a/include/openkal/net.h +++ b/include/openkal/net.h @@ -59,7 +59,7 @@ int kal_net_accept (struct kal_net_listener l, struct kal_net_conn* out); /* A connection is read and written through openkal.stream. The stream remains * valid while the connection is open and is not separately released; the * connection owns it. The wording is `openkal.fs's, because the arrangement is. */ -kal_uintptr kal_net_stream(struct kal_net_conn c); +struct kal_stream kal_net_stream(struct kal_net_conn c); /* Reports the endpoint of the peer, and the endpoint this end was given. * @@ -94,7 +94,7 @@ void kal_net_close_listener(struct kal_net_listener l); * A word rather than an enquiry, because these do not vary between the * resources of the interface: an implementation either speaks IPv6 or does not * (clause 6.2). */ -extern const kal_uintptr kal_net_props; +kal_uintptr kal_net_props(void); #ifdef __cplusplus } diff --git a/include/openkal/process.h b/include/openkal/process.h index 525baa3..3e396ae 100644 --- a/include/openkal/process.h +++ b/include/openkal/process.h @@ -53,9 +53,9 @@ struct kal_process { kal_uintptr h; }; * next implementation meets it in the specification rather than in a program * that wrote to the wrong stream. */ struct kal_spawn_streams { - kal_uintptr in; - kal_uintptr out; - kal_uintptr err; + struct kal_stream in; + struct kal_stream out; + struct kal_stream err; }; /* One directory a started program shall receive among its preopens. The layout @@ -126,7 +126,7 @@ int kal_process_wait(struct kal_process, int* status, int* terminated); int kal_process_terminate(struct kal_process); void kal_process_close(struct kal_process); -extern const kal_uintptr kal_process_props; +kal_uintptr kal_process_props(void); #ifdef __cplusplus } diff --git a/include/openkal/random.h b/include/openkal/random.h index b2587a2..b71d303 100644 --- a/include/openkal/random.h +++ b/include/openkal/random.h @@ -49,7 +49,7 @@ extern "C" { * no source", which is answered by the interface's absence. */ int kal_random_fill(void* out, kal_uintptr len); -extern const kal_uintptr kal_random_props; +kal_uintptr kal_random_props(void); #ifdef __cplusplus } diff --git a/include/openkal/space.h b/include/openkal/space.h index 22a36c0..87e2edb 100644 --- a/include/openkal/space.h +++ b/include/openkal/space.h @@ -73,11 +73,20 @@ extern "C" { * specification targets. An implementation whose environment gives the started * context a stack of its own ignores the argument, and reports that by * withholding nothing: a caller cannot observe which of the two occurred, and - * has no decision resting upon it. */ + * has no decision resting upon it. + * + * ⭐ `entry' IS AN ADDRESS AT WHICH A CONTEXT BEGINS, NOT A FUNCTION THAT IS + * CALLED. NO RETURN ADDRESS EXISTS: the started context stands at the top of a + * stack with nothing beneath it, and returning from `entry' ends the context + * with a status saying that it returned rather than choosing one. This was + * already the behaviour every implementation had --- one of them records that + * the transfer "cannot be written in C" for exactly this reason --- and saying + * it here is what lets an implementation on the far side of a boundary + * establish the context by setting a program counter and a stack pointer. */ int kal_space_start(void (*entry)(void*), void* arg, void* stack_top, struct kal_process* out); -extern const kal_uintptr kal_space_props; +kal_uintptr kal_space_props(void); #ifdef __cplusplus } diff --git a/include/openkal/stream.h b/include/openkal/stream.h index 142d5fa..d45dbd9 100644 --- a/include/openkal/stream.h +++ b/include/openkal/stream.h @@ -17,13 +17,15 @@ struct kal_stream kal_stdin (void); struct kal_stream kal_stdout(void); struct kal_stream kal_stderr(void); -/* Transfers the whole buffer, or reports the condition that prevented it. On - * failure, n reports how many bytes were transferred before the failure. */ -struct kal_io_result kal_stream_write(struct kal_stream s, const void* buf, kal_uintptr len); - -/* Transfers at most len bytes and reports how many were transferred. Zero - * bytes with kal_ok denotes end of input. */ -struct kal_io_result kal_stream_read(struct kal_stream s, void* buf, kal_uintptr len); +/* Transfers the whole buffer and reports how many bytes it moved, or the + * negated error value when it moved none. Clause 7.4: a partial transfer is not + * a successful outcome and the loop that avoids one belongs here rather than in + * every caller, so the ordinary result is `len'. */ +kal_intptr kal_stream_write(struct kal_stream s, const void* buf, kal_uintptr len); + +/* Transfers at most len bytes and reports how many it moved, or the negated + * error value when it moved none. Zero denotes end of input. */ +kal_intptr kal_stream_read(struct kal_stream s, void* buf, kal_uintptr len); /* Commits any buffering the implementation performs. */ int kal_stream_flush(struct kal_stream s); diff --git a/include/openkal/task.h b/include/openkal/task.h index ac3a92d..e40b184 100644 --- a/include/openkal/task.h +++ b/include/openkal/task.h @@ -27,10 +27,30 @@ struct kal_task { kal_uintptr h; }; extern "C" { #endif -/* Starts a context executing the given function with the given argument. The - * stack is provided by the implementation; its size is a property rather than - * a parameter, because an environment that does not allocate stacks separately - * cannot honour a request for one. */ +/* Starts a context. + * + * ⭐ `entry' IS AN ADDRESS AT WHICH A CONTEXT BEGINS, NOT A FUNCTION THAT IS + * CALLED, AND THE DIFFERENCE IS WHAT LETS THIS CROSS A BOUNDARY. An + * implementation on the far side of one does not call into the program; it + * establishes a context whose program counter is `entry' and whose first + * argument is `arg', which is what a kernel's own context-creating primitive + * does. Consequently: + * + * - NO RETURN ADDRESS EXISTS. The started context has nothing to return to, + * and a caller shall not write an entry that relies on returning to + * anything. Returning from `entry' ends the context, which is the only + * defined thing that can happen and is not an error. + * + * - `arg' is a value the started context receives, and is meaningful in the + * address space the context runs in --- which is the caller's, because that + * is what this interface provides. Copying an address space is + * `openkal.space'. + * + * The stack is provided by the implementation; its size is a property rather + * than a parameter, because an environment that does not allocate stacks + * separately cannot honour a request for one. An implementation across a + * boundary allocates it too, so the crossed form asks for nothing the linked + * form does not. */ int kal_task_start(void (*entry)(void*), void* arg, struct kal_task* out); /* Waits for a context to finish. A context is waited for at most once. */ @@ -55,7 +75,7 @@ int kal_task_wait(const kal_u32* word, kal_u32 expected, * how many were woken. A count of zero wakes none and is permitted. */ int kal_task_wake(const kal_u32* word, kal_uintptr count, kal_uintptr* woken); -extern const kal_uintptr kal_task_props; +kal_uintptr kal_task_props(void); #ifdef __cplusplus } diff --git a/include/openkal/time.h b/include/openkal/time.h index 50389b1..efff431 100644 --- a/include/openkal/time.h +++ b/include/openkal/time.h @@ -23,7 +23,7 @@ kal_duration kal_time_wall(void); kal_duration kal_time_monotonic_granularity(void); void kal_time_sleep(kal_duration ns); -extern const kal_uintptr kal_time_props; +kal_uintptr kal_time_props(void); #ifdef __cplusplus } diff --git a/include/openkal/timeout.h b/include/openkal/timeout.h index a157490..141aaef 100644 --- a/include/openkal/timeout.h +++ b/include/openkal/timeout.h @@ -34,13 +34,13 @@ extern "C" { #endif -struct kal_io_result kal_timeout_read (struct kal_stream s, void* buf, kal_uintptr len, kal_u64 timeout_ns); -struct kal_io_result kal_timeout_write(struct kal_stream s, const void* buf, kal_uintptr len, kal_u64 timeout_ns); +kal_intptr kal_timeout_read (struct kal_stream s, void* buf, kal_uintptr len, kal_u64 timeout_ns); +kal_intptr kal_timeout_write(struct kal_stream s, const void* buf, kal_uintptr len, kal_u64 timeout_ns); int kal_timeout_accept(struct kal_net_listener l, kal_u64 timeout_ns, struct kal_net_conn* out); -struct kal_io_result kal_timeout_recv_from(struct kal_datagram d, void* buf, kal_uintptr len, - struct kal_endpoint* from, kal_u64 timeout_ns); +kal_intptr kal_timeout_recv_from(struct kal_datagram d, void* buf, kal_uintptr len, + struct kal_endpoint* from, kal_u64 timeout_ns); int kal_timeout_wait_process(struct kal_process p, kal_u64 timeout_ns, int* status, int* terminated); @@ -54,7 +54,7 @@ int kal_timeout_wait_process(struct kal_process p, kal_u64 timeout_ns, * * Per implementation rather than per resource, so a word rather than an enquiry * (clause 6.2). */ -extern const kal_uintptr kal_timeout_granularity_ns; +kal_u64 kal_timeout_granularity(void); #ifdef __cplusplus } diff --git a/include/openkal/types.h b/include/openkal/types.h index 543ceb6..3ed0582 100644 --- a/include/openkal/types.h +++ b/include/openkal/types.h @@ -34,6 +34,31 @@ typedef unsigned int kal_uintptr; # error "openkal requires a compiler that states the width of a pointer" #endif +/* The signed machine word, in which every operation whose whole result is a + * count reports that count or the condition that prevented it. + * + * ONE WORD AND NOT TWO, AND THE REASON IS BOTH SIDES AT ONCE. An earlier form + * returned a structure of a count and an error. Every consumer of it collapsed + * the pair by hand and by the same rule --- report what was transferred, or the + * condition when nothing was --- so the pair was never the shape a caller + * wanted; and a result of two words cannot be carried by a boundary that + * returns one, which is the second half of clause 4.4. + * + * The collapse is safe because the error set of this header is CLOSED (clause + * 5.2): a negated error occupies a small known range and can never be mistaken + * for a count. An open-ended error space would not permit it. */ +#if defined(__INTPTR_TYPE__) +typedef __INTPTR_TYPE__ kal_intptr; +#elif defined(_MSC_VER) +# if defined(_WIN64) +typedef signed __int64 kal_intptr; +# else +typedef signed int kal_intptr; +# endif +#else +# error "openkal requires a compiler that states the width of a pointer" +#endif + /* The three fixed widths openkal's operations use, stated once here rather than * at each use. * @@ -95,13 +120,18 @@ enum kal_error { kal_err_not_directory = 13 /* the reverse of the preceding condition */ }; -/* The result of an operation that transfers a count. The layout is frozen: - * two machine words are returned in registers on the architectures openkal - * targets, and a wider result would be returned through a hidden pointer. */ -struct kal_io_result { - kal_uintptr n; - int e; -}; +/* HOW AN OPERATION REPORTS ITS RESULT --- one rule, stated once. + * + * An operation whose whole result is a COUNT OF BYTES returns `kal_intptr': + * the count, or the negated error value when no byte was produced. A count + * of zero is a count and not an error, and for a read it denotes end of + * input. + * + * An operation that produces a RESOURCE, or that produces nothing, returns + * `int' from `kal_error' and writes what it produced through a pointer. + * + * A caller therefore never inspects two things to learn one thing, and every + * result of either kind fits in one machine word. */ /* Where a connection or a message goes. The layout is frozen (clause 5.3). * @@ -119,14 +149,21 @@ struct kal_io_result { * than reading it as one it does. Twenty-four bytes is chosen so that an * address carrying a scope identifier fits without a second type. */ struct kal_endpoint { - kal_u8 addr[24]; /* network order --- the bytes of the address */ - kal_uintptr addr_len; /* 4 = IPv4 16 = IPv6 20 = IPv6 with a scope - * identifier. Further values are defined by later - * revisions and are refused, not misread, by an - * implementation that does not know them. */ - kal_u32 port; /* a number, in host order: this interface does - * not ask a caller to perform a protocol's byte - * order conversion */ + kal_u8 addr[24]; /* network order --- the bytes of the address */ + kal_u32 addr_len; /* 4 = IPv4 16 = IPv6 20 = IPv6 with a scope + * identifier. Further values are defined by later + * revisions and are refused, not misread, by an + * implementation that does not know them. + * + * A fixed width rather than a machine word: the value + * is at most twenty-four, and a machine word would + * freeze a difference between a thirty-two and a + * sixty-four bit target that nothing in this structure + * needs. Clause 5.3 holds the layout, so the layout + * should not carry a distinction it has no use for. */ + kal_u32 port; /* a number, in host order: this interface does not + * ask a caller to perform a protocol's byte order + * conversion */ }; #ifdef __cplusplus diff --git a/include/openkal/version.h b/include/openkal/version.h new file mode 100644 index 0000000..5cefd9f --- /dev/null +++ b/include/openkal/version.h @@ -0,0 +1,87 @@ +/* openkal --- what an implementation says about itself before it is used. + * + * ⚠️ THIS IS NOT AN INTERFACE. It provides no resource, and clause 3.2 closes + * the set of core interfaces rather than the set of things every implementation + * must export. What is here is the specification's own self-description: two + * operations every conforming implementation exports, belonging to none of the + * interfaces, so that a consumer can ask before it calls. + * + * ⭐ WHY IT CANNOT BE A NOTE IN THE ARTIFACT. The earlier answer to version skew + * was a record placed in the object file. Object files are of three kinds here + * --- ELF, Mach-O and PE --- and a boundary crossed by a trap has no object file + * at all. An operation is the one form every boundary already has. + * + * ⭐ WHY IT ANSWERS TWO QUESTIONS AND NOT ONE. A consumer that is linked learns + * an interface's absence from the linker (clause 6.1). A consumer that is bound + * at load or crosses a boundary has no linker to learn it from, and asking each + * interface in turn requires calling into it, which is the thing that must not + * happen first. One word of interface positions answers it for all of them + * before anything else is called. + * + * ⚠️ AND NEITHER ANSWER IS A CAPABILITY. `kal_interfaces' says which interfaces + * exist, not how they behave; how an implementation behaves within an interface + * it provides is a property word, which is clause 6.2's own division and is not + * restated here. + */ +#ifndef OPENKAL_VERSION_H +#define OPENKAL_VERSION_H +#include "types.h" + +/* The version of the specification this consumer was compiled against. A + * consumer compares it with what `kal_version' answers and refuses to proceed + * against an implementation older than the declarations it holds --- because an + * older implementation reports conditions this consumer distinguishes as + * conditions it does not, which is a wrong answer rather than a refusal. */ +#define KAL_VERSION_MAJOR 0u +#define KAL_VERSION_MINOR 9u +#define KAL_VERSION_PATCH 0u + +#define KAL_VERSION_MAKE(major, minor, patch) \ + (((kal_u64)(major) << 32) | ((kal_u64)(minor) << 16) | \ + ((kal_u64)(patch))) + +#define KAL_VERSION \ + KAL_VERSION_MAKE(KAL_VERSION_MAJOR, KAL_VERSION_MINOR, KAL_VERSION_PATCH) + +/* Positions in the result of kal_interfaces. + * + * A position, once assigned, retains its meaning; a position that has not been + * assigned reads as zero, so a consumer compiled against a later specification + * behaves correctly against an earlier implementation. This is clause 6.2's + * rule for a property word, applied to the presence of interfaces. + * + * The three core interfaces have positions of their own even though every + * implementation provides them. A word in which the answer for some interfaces + * could not be expressed would be a word a reader has to know the exceptions + * to. */ +#define KAL_IFACE_ABORT ((kal_u64)1u << 0) +#define KAL_IFACE_STREAM ((kal_u64)1u << 1) +#define KAL_IFACE_MEMORY ((kal_u64)1u << 2) +#define KAL_IFACE_ENV ((kal_u64)1u << 3) +#define KAL_IFACE_TIME ((kal_u64)1u << 4) +#define KAL_IFACE_RANDOM ((kal_u64)1u << 5) +#define KAL_IFACE_FS ((kal_u64)1u << 6) +#define KAL_IFACE_PROCESS ((kal_u64)1u << 7) +#define KAL_IFACE_TASK ((kal_u64)1u << 8) +#define KAL_IFACE_EXEC ((kal_u64)1u << 9) +#define KAL_IFACE_TERMINAL ((kal_u64)1u << 10) +#define KAL_IFACE_NET ((kal_u64)1u << 11) +#define KAL_IFACE_DATAGRAM ((kal_u64)1u << 12) +#define KAL_IFACE_SPACE ((kal_u64)1u << 13) +#define KAL_IFACE_TIMEOUT ((kal_u64)1u << 14) + +#ifdef __cplusplus +extern "C" { +#endif + +/* The version of the specification this implementation was written against, + * encoded by KAL_VERSION_MAKE. */ +kal_u64 kal_version(void); + +/* The interfaces this implementation provides, as positions above. */ +kal_u64 kal_interfaces(void); + +#ifdef __cplusplus +} +#endif +#endif /* OPENKAL_VERSION_H */ diff --git a/kit/mcpp.toml b/kit/mcpp.toml index 2154f93..024dbaa 100644 --- a/kit/mcpp.toml +++ b/kit/mcpp.toml @@ -1,7 +1,7 @@ [package] namespace = "mcpplibs" name = "openkal-kit" -version = "0.1.1" +version = "0.2.0" description = "Facilities composed from openkal's interfaces. Not part of the specification, and structurally incapable of being mistaken for it." license = "Apache-2.0" authors = ["mcpplibs"] @@ -67,7 +67,7 @@ kind = "lib" # published packages, which is the only place this could be observed: no # continuous integration in this ecosystem resolves a published package, # because they all substitute working trees by design. -openkal = "0.8.0" +openkal = "0.9.0" # ⚠️ NO `include_dirs` HERE EITHER. It named `../include`, which reaches out of # this package into whatever happens to sit above it. With the specification @@ -98,4 +98,4 @@ openkal = "0.8.0" # same reason: a change spanning the specification and an implementation must be # tested against both halves as written, not against whichever half is published. [target.'cfg(os = "linux")'.dev-dependencies] -openkal-linux = { version = "0.6.0", features = ["standalone"] } +openkal-linux = { version = "0.7.0", features = ["standalone"] } diff --git a/mcpp.toml b/mcpp.toml index 8e044c8..f9ff3c0 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,7 +1,7 @@ [package] namespace = "mcpplibs" name = "openkal" -version = "0.8.0" +version = "0.9.0" description = "openkal: a portable kernel ABI specification. This package carries the normative declarations; implementations are separate packages." license = "Apache-2.0" authors = ["mcpplibs"] diff --git a/src/datagram.cppm b/src/datagram.cppm index 82c92e8..475ee12 100644 --- a/src/datagram.cppm +++ b/src/datagram.cppm @@ -39,7 +39,7 @@ using props = kal::props; inline constexpr props ipv6 {KAL_DGRAM_PROP_IPV6}; inline constexpr props broadcast{KAL_DGRAM_PROP_BROADCAST}; -inline props properties() { return props{kal_datagram_props}; } +inline props properties() { return props{kal_datagram_props()}; } inline bool has(props p) { return properties().has(p); } struct open_result { datagram d; int e; }; @@ -65,18 +65,18 @@ inline endpoint_result local(datagram d) { return { ep, e }; } -inline kal_io_result send_to(datagram d, const void* p, kal_uintptr n, const endpoint& to) { +inline kal_intptr send_to(datagram d, const void* p, kal_uintptr n, const endpoint& to) { return kal_datagram_send_to(d, p, n, &to); } // The sender is reported beside the result rather than through an // out-parameter: every caller of a receive wants both, and none of them wants // to declare an endpoint first. -struct recv_result { kal_io_result r; endpoint from; }; +struct recv_result { kal_intptr n; endpoint from; }; inline recv_result recv_from(datagram d, void* p, kal_uintptr n) { endpoint from{}; - const kal_io_result r = kal_datagram_recv_from(d, p, n, &from); + const kal_intptr r = kal_datagram_recv_from(d, p, n, &from); return { r, from }; } diff --git a/src/env.cppm b/src/env.cppm index 2b34e38..700a281 100644 --- a/src/env.cppm +++ b/src/env.cppm @@ -25,10 +25,21 @@ export using ::kal_env_var_at; export namespace kal::env { +// Each of these copies into the caller's buffer and reports the length the +// value HAS, or the negated error value. A caller with a large enough buffer is +// done in one call; a caller that wants to size first passes a capacity of +// zero; a caller whose buffer was too small learns it by comparing. Nothing is +// truncated silently. inline kal_uintptr arg_count() { return kal_env_arg_count(); } -inline const char* arg(kal_uintptr i, kal_uintptr* len) { return kal_env_arg(i, len); } -inline const char* var(const char* name, kal_uintptr n, kal_uintptr* len) { - return kal_env_var(name, n, len); +inline kal_intptr arg(kal_uintptr i, char* out, kal_uintptr cap) { + return kal_env_arg(i, out, cap); +} +inline kal_intptr var(const char* name, kal_uintptr n, char* out, kal_uintptr cap) { + return kal_env_var(name, n, out, cap); +} +inline kal_uintptr var_count() { return kal_env_var_count(); } +inline kal_intptr var_at(kal_uintptr i, char* out, kal_uintptr cap) { + return kal_env_var_at(i, out, cap); } } diff --git a/src/exec.cppm b/src/exec.cppm index f3f95bd..04a77cc 100644 --- a/src/exec.cppm +++ b/src/exec.cppm @@ -36,7 +36,13 @@ using props = kal::props; // before deciding how to hold what it has generated. inline constexpr props republish{KAL_EXEC_PROP_REPUBLISH}; -inline props properties() { return props{kal_exec_props}; } +// Whether memory this program may execute is available to THIS ARTIFACT in +// THIS environment. Clause 6.5 settles availability at dependency resolution, +// which is the right answer for an artifact produced for its own machine and +// no answer at all for one produced once and run in many. +inline constexpr props available{KAL_EXEC_PROP_AVAILABLE}; + +inline props properties() { return props{kal_exec_props()}; } inline bool has(props p) { return properties().has(p); } inline void* alloc(kal_uintptr size) { return kal_exec_alloc(size); } diff --git a/src/fs.cppm b/src/fs.cppm index 1cfe47b..c7422c7 100644 --- a/src/fs.cppm +++ b/src/fs.cppm @@ -37,7 +37,6 @@ export using ::kal_node_info; export using ::kal_fs_preopen_count; export using ::kal_fs_preopen; export using ::kal_fs_open_dir; -export using ::kal_fs_open_file; export using ::kal_fs_open; export using ::kal_fs_close_dir; export using ::kal_fs_close_file; @@ -53,39 +52,38 @@ export using ::kal_fs_rename; export using ::kal_fs_list_begin; export using ::kal_fs_list_next; export using ::kal_fs_props; +export using ::kal_fs_max_name; +export using ::kal_fs_link_create; +export using ::kal_fs_link_read; static_assert(sizeof(kal_dir) == sizeof(kal_uintptr), "clause 7.2"); static_assert(sizeof(kal_file) == sizeof(kal_uintptr), "clause 7.2"); // Clause 5.3: the layout is frozen. An implementation and a consumer built at // different times must agree on where each field is, and nothing else reports // a disagreement. -static_assert(__builtin_offsetof(kal_node_info, size) == 0); - -// ⚠️ `8`, NOT `sizeof(kal_uintptr)`, AND THE DIFFERENCE IS AN ABI FACT RATHER -// THAN A SPELLING. -// -// `modified_ns` is a `kal_u64` and is naturally aligned. On a 64-bit target -// `size` occupies eight bytes and the timestamp follows at eight. On a 32-bit -// target `size` occupies four, the compiler inserts four bytes of padding, and -// the timestamp still lands at eight. The offset is therefore the SAME on both -// widths, which is exactly the property a frozen layout wants — and -// `sizeof(kal_uintptr)` described only the 64-bit case. // -// Measured on `riscv32-none-elf`: +// ⚠️ EVERY OFFSET IS THE SAME ON A THIRTY-TWO AND A SIXTY-FOUR BIT TARGET, AND +// THAT IS A PROPERTY THAT HAD TO BE DESIGNED FOR RATHER THAN OBSERVED. // -// fs.cppm:63: static assertion failed due to requirement -// '__builtin_offsetof(kal_node_info, modified_ns) == sizeof(unsigned int)' +// Version 0.8 held the size in a `kal_uintptr` and relied on the compiler's +// padding to place the timestamp at the same offset on both widths. It worked, +// and it worked by coincidence: it stopped working the moment a second word of +// pointer width was added in front. Measured on `riscv32-none-elf` when the +// earlier form met a consumer that instantiates the 32-bit layout --- +// `mcpplibs/riscv-virt-rt`, whose rv32 leg is the only place it exists --- +// which is also where the earlier form's offset assertion first failed. // -// ⚠️ Nothing in this repository's own CI could have found it: openkal is built -// and tested hosted, and every hosted target it serves is 64-bit. The failure -// surfaced in a consumer — `mcpplibs/riscv-virt-rt`, whose rv32 leg activates -// the `openkal` feature — which is the only place the 32-bit layout is -// instantiated at all. -// -// `sizeof(kal_node_info)` is 24 on both widths for the same reason: 8+8+4+4 and -// 4+4(padding)+8+4+4. -static_assert(__builtin_offsetof(kal_node_info, modified_ns) == 8); -static_assert(sizeof(kal_node_info) == 24); +// So the fields are fixed widths, and the two words of positions are `kal_u32`. +// Forty-eight bytes on both widths, by construction rather than by arithmetic +// that happens to agree. +static_assert(__builtin_offsetof(kal_node_info, self_size) == 0); +static_assert(__builtin_offsetof(kal_node_info, present) == 4); +static_assert(__builtin_offsetof(kal_node_info, size) == 8); +static_assert(__builtin_offsetof(kal_node_info, modified_ns) == 16); +static_assert(__builtin_offsetof(kal_node_info, identity) == 24); +static_assert(__builtin_offsetof(kal_node_info, kind) == 40); +static_assert(__builtin_offsetof(kal_node_info, writable) == 44); +static_assert(sizeof(kal_node_info) == 48); export namespace kal::fs { @@ -100,6 +98,7 @@ inline constexpr props case_sensitive{KAL_FS_PROP_CASE_SENSITIVE}; inline constexpr props links {KAL_FS_PROP_LINKS}; inline constexpr props modified_time {KAL_FS_PROP_MODIFIED_TIME}; inline constexpr props atomic_rename {KAL_FS_PROP_ATOMIC_RENAME}; +inline constexpr props make_links {KAL_FS_PROP_MAKE_LINKS}; enum : int { seek_set = KAL_SEEK_SET, seek_current = KAL_SEEK_CURRENT, seek_end = KAL_SEEK_END }; @@ -128,12 +127,39 @@ inline kal_uintptr preopen_count() { return kal_fs_preopen_count(); } // The first entry, which every implementation supplies and which denotes the // directory the program was started in. inline dir working() { - dir d{}; const char* n = nullptr; kal_uintptr l = 0; - kal_fs_preopen(0, &d, &n, &l); + dir d{}; kal_uintptr l = 0; + kal_fs_preopen(0, &d, nullptr, 0, &l); return d; } -inline props properties() { return props{kal_fs_props}; } -inline bool has(props p) { return properties().has(p); } +// The properties of the volume a directory is on. An enquiry taking the +// resource, because every position is a property of the format rather than of +// the environment: one machine mounts a case-sensitive volume beside a +// case-insensitive one. +inline props properties(dir d) { return props{kal_fs_props(d)}; } +inline bool has(dir d, props p) { return properties(d).has(p); } + +inline kal_uintptr max_name() { return kal_fs_max_name(); } + +// The fields an enquiry may ask for and may report. A macro is not exportable +// from a module, and a consumer that reaches for one has to include the header +// it came from --- which is the seam clause 4.2 exists to remove. +namespace field { +inline constexpr kal_u32 kind = KAL_INFO_KIND; +inline constexpr kal_u32 size = KAL_INFO_SIZE; +inline constexpr kal_u32 modified = KAL_INFO_MODIFIED; +inline constexpr kal_u32 writable = KAL_INFO_WRITABLE; +inline constexpr kal_u32 identity = KAL_INFO_IDENTITY; +inline constexpr kal_u32 all = KAL_INFO_ALL; +} + +// Positions in the flags word of an enquiry. +inline constexpr kal_uintptr no_resolve = KAL_FS_NO_RESOLVE; +inline constexpr kal_uintptr link_directory = KAL_FS_LINK_DIRECTORY; + +// An enquiry structure that states how much of itself exists on this side. +inline kal_node_info info_for_caller() { + kal_node_info v{}; v.self_size = sizeof v; return v; +} } diff --git a/src/memory.cppm b/src/memory.cppm index bab90f3..d1f6c54 100644 --- a/src/memory.cppm +++ b/src/memory.cppm @@ -18,10 +18,18 @@ export import openkal.types; export using ::kal_alloc; export using ::kal_free; +export using ::kal_memory_granularity; export namespace kal { inline void* alloc(kal_uintptr size, kal_uintptr align) { return kal_alloc(size, align); } inline void free (void* p, kal_uintptr size, kal_uintptr align) { kal_free(p, size, align); } +// The quantum this environment allocates and protects memory in. An address and +// a length that are multiples of it are acceptable everywhere; an environment +// with more than one such quantum reports the coarsest, and one with none +// reports 1. It is an operation and not a constant because it is a property of +// the machine the program runs on rather than of the machine it was built for. +inline kal_uintptr granularity() { return kal_memory_granularity(); } + } diff --git a/src/net.cppm b/src/net.cppm index a598fd5..ada2a4d 100644 --- a/src/net.cppm +++ b/src/net.cppm @@ -48,7 +48,7 @@ using props = kal::props; inline constexpr props ipv6 {KAL_NET_PROP_IPV6}; inline constexpr props halfclose{KAL_NET_PROP_HALFCLOSE}; -inline props properties() { return props{kal_net_props}; } +inline props properties() { return props{kal_net_props()}; } inline bool has(props p) { return properties().has(p); } // The directions kal_net_shutdown accepts. An enumeration rather than the bare @@ -86,7 +86,7 @@ inline conn_result accept(listener l) { // The stream a connection owns. Named `stream' rather than converted // implicitly, so that a caller can see where the borrowing begins. -inline kal_stream stream(conn c) { return kal_stream{kal_net_stream(c)}; } +inline kal_stream stream(conn c) { return kal_net_stream(c); } inline endpoint_result peer (conn c) { endpoint ep{}; const int e = kal_net_peer (c, &ep); return { ep, e }; } inline endpoint_result local(conn c) { endpoint ep{}; const int e = kal_net_local(c, &ep); return { ep, e }; } diff --git a/src/process.cppm b/src/process.cppm index 9af18f0..bb76cf6 100644 --- a/src/process.cppm +++ b/src/process.cppm @@ -51,7 +51,7 @@ inline constexpr props exit_status {KAL_PROCESS_PROP_EXIT_STATUS}; inline constexpr props channel {KAL_PROCESS_PROP_CHANNEL}; inline constexpr props grant_dir {KAL_PROCESS_PROP_GRANT_DIR}; -inline props properties() { return props{kal_process_props}; } +inline props properties() { return props{kal_process_props()}; } inline bool has(props p) { return properties().has(p); } using preopen = kal_preopen; diff --git a/src/random.cppm b/src/random.cppm index 989a033..666b77d 100644 --- a/src/random.cppm +++ b/src/random.cppm @@ -47,7 +47,7 @@ inline int fill(void* out, kal_uintptr len) { return kal_random_fill(out, len); } -inline props properties() { return props{kal_random_props}; } +inline props properties() { return props{kal_random_props()}; } inline bool has(props p) { return properties().has(p); } } diff --git a/src/space.cppm b/src/space.cppm index 09d7faf..e5735b4 100644 --- a/src/space.cppm +++ b/src/space.cppm @@ -37,7 +37,7 @@ using props = kal::props; inline constexpr props clone_handles{KAL_SPACE_PROP_CLONE_HANDLES}; inline constexpr props deferred_copy{KAL_SPACE_PROP_DEFERRED_COPY}; -inline props properties() { return props{kal_space_props}; } +inline props properties() { return props{kal_space_props()}; } inline bool has(props p) { return properties().has(p); } struct process_result { kal_process p; int e; }; diff --git a/src/stream.cppm b/src/stream.cppm index 02a516f..6d0def8 100644 --- a/src/stream.cppm +++ b/src/stream.cppm @@ -55,8 +55,11 @@ inline stream in () { return kal_stdin(); } inline stream out() { return kal_stdout(); } inline stream err() { return kal_stderr(); } -inline kal_io_result write(stream s, const void* p, kal_uintptr n) { return kal_stream_write(s, p, n); } -inline kal_io_result read (stream s, void* p, kal_uintptr n) { return kal_stream_read(s, p, n); } +// The count, or the negated error value when no byte was produced. One word, +// so that a caller never inspects two things to learn one thing and so that the +// result crosses a boundary that returns one register. +inline kal_intptr write(stream s, const void* p, kal_uintptr n) { return kal_stream_write(s, p, n); } +inline kal_intptr read (stream s, void* p, kal_uintptr n) { return kal_stream_read(s, p, n); } inline int flush(stream s) { return kal_stream_flush(s); } // The properties of one stream, as a set that cannot be confused with another diff --git a/src/task.cppm b/src/task.cppm index 8c1ce96..a9ae903 100644 --- a/src/task.cppm +++ b/src/task.cppm @@ -47,7 +47,7 @@ inline constexpr props wait_timeout{KAL_TASK_PROP_WAIT_TIMEOUT}; // variable and therefore cannot be ported onto an implementation without it. inline constexpr props thread_local_storage{KAL_TASK_PROP_THREAD_LOCAL}; -inline props properties() { return props{kal_task_props}; } +inline props properties() { return props{kal_task_props()}; } inline bool has(props p) { return properties().has(p); } } diff --git a/src/time.cppm b/src/time.cppm index 1b51be4..484122c 100644 --- a/src/time.cppm +++ b/src/time.cppm @@ -43,7 +43,7 @@ inline kal_duration wall() { return kal_time_wall(); } inline kal_duration granularity() { return kal_time_monotonic_granularity(); } inline void sleep(kal_duration ns) { kal_time_sleep(ns); } -inline props properties() { return props{kal_time_props}; } +inline props properties() { return props{kal_time_props()}; } inline bool has(props p) { return properties().has(p); } } diff --git a/src/timeout.cppm b/src/timeout.cppm index 8ab82f1..bcde89b 100644 --- a/src/timeout.cppm +++ b/src/timeout.cppm @@ -30,18 +30,18 @@ export using ::kal_timeout_write; export using ::kal_timeout_accept; export using ::kal_timeout_recv_from; export using ::kal_timeout_wait_process; -export using ::kal_timeout_granularity_ns; +export using ::kal_timeout_granularity; export namespace kal::timeout { // The smallest bound this implementation distinguishes. A caller that asks for // less is not refused and does not get less. -inline kal_uintptr granularity_ns() { return kal_timeout_granularity_ns; } +inline kal_u64 granularity_ns() { return kal_timeout_granularity(); } -inline kal_io_result read (kal_stream s, void* p, kal_uintptr n, kal_u64 ns) { +inline kal_intptr read (kal_stream s, void* p, kal_uintptr n, kal_u64 ns) { return kal_timeout_read(s, p, n, ns); } -inline kal_io_result write(kal_stream s, const void* p, kal_uintptr n, kal_u64 ns) { +inline kal_intptr write(kal_stream s, const void* p, kal_uintptr n, kal_u64 ns) { return kal_timeout_write(s, p, n, ns); } @@ -56,11 +56,11 @@ inline conn_result accept(kal_net_listener l, kal_u64 ns) { // Shaped like kal::datagram::recv_from, because it is that operation with a // bound: a caller that adds a timeout should not also have to change how it // reads the result. -struct recv_result { kal_io_result r; kal_endpoint from; }; +struct recv_result { kal_intptr n; kal_endpoint from; }; inline recv_result recv_from(kal_datagram d, void* p, kal_uintptr n, kal_u64 ns) { kal_endpoint from{}; - const kal_io_result r = kal_timeout_recv_from(d, p, n, &from, ns); + const kal_intptr r = kal_timeout_recv_from(d, p, n, &from, ns); return { r, from }; } diff --git a/src/types.cppm b/src/types.cppm index 3c02890..d30b172 100644 --- a/src/types.cppm +++ b/src/types.cppm @@ -16,6 +16,7 @@ export module openkal.types; // The width of a machine word, and the three fixed widths the operations use. export using ::kal_uintptr; +export using ::kal_intptr; export using ::kal_u32; export using ::kal_u64; export using ::kal_i64; @@ -40,8 +41,7 @@ export using ::kal_err_not_empty; export using ::kal_err_is_directory; export using ::kal_err_not_directory; -// The result of an operation that transfers a count. -export using ::kal_io_result; +// Where a connection or a message goes. export using ::kal_endpoint; // Clause 5.3 freezes the layout. The address is twenty-four bytes so that one @@ -49,20 +49,17 @@ export using ::kal_endpoint; // were not would read every address at the wrong offset while still linking. static_assert(sizeof(kal_endpoint{}.addr) == 24, "clause 5.3: an endpoint address is twenty-four bytes"); -static_assert(sizeof(kal_endpoint) >= 24 + sizeof(kal_uintptr) + sizeof(kal_u32), - "clause 5.3: an endpoint holds its address, a length and a port"); +static_assert(sizeof(kal_endpoint) == 24 + sizeof(kal_u32) + sizeof(kal_u32), + "clause 5.3: an endpoint holds its address, a length and a port," + " and the length is a fixed width so that the layout does not" + " differ between a thirty-two and a sixty-four bit target"); +static_assert(sizeof(kal_intptr) == sizeof(kal_uintptr), + "a counting result is one machine word: clause 5.1"); -// Clause 5.3 declares the layout of every structure immutable. A declaration -// that something shall not change is not a mechanism; this is the mechanism. -// Two machine words are returned in registers on the architectures openkal -// targets, and a wider result would be returned through a hidden pointer -// instead --- a change of calling convention that no declaration would report -// and that a consumer built against the earlier layout would not survive. -static_assert(sizeof(kal_io_result) == 2 * sizeof(kal_uintptr), - "kal_io_result must remain two machine words: clause 5.3"); -static_assert(alignof(kal_io_result) == alignof(kal_uintptr)); -static_assert(__builtin_offsetof(kal_io_result, n) == 0); -static_assert(__builtin_offsetof(kal_io_result, e) == sizeof(kal_uintptr)); +// A declaration that something shall not change is not a mechanism; this is the +// mechanism. An operation whose whole result is a count returns one signed +// machine word, and a caller that read two would read the second from wherever +// the first left the register file. static_assert(sizeof(kal_uintptr) == sizeof(void*), "kal_uintptr must hold a pointer: clause 5.1"); diff --git a/src/version.cppm b/src/version.cppm new file mode 100644 index 0000000..da349be --- /dev/null +++ b/src/version.cppm @@ -0,0 +1,65 @@ +// openkal --- what an implementation says about itself before it is used. +// +// Not an interface: it provides no resource. Clause 3.2 closes the set of core +// INTERFACES, and this closes nothing, because what is here is the +// specification's own self-description --- two operations every conforming +// implementation exports and that belong to none of the interfaces. +// +// A consumer that is linked learns an interface's absence from the linker +// (clause 6.1). A consumer bound at load, or across a boundary, has no linker +// to learn it from, and asking an interface by calling into it is the thing +// that must not happen first. These two operations are what it asks instead. +module; +#include + +export module openkal.version; +export import openkal.types; + +export using ::kal_version; +export using ::kal_interfaces; + +export namespace kal { + +// The version of the specification the consumer holds declarations for. +inline constexpr kal_u64 header_version = KAL_VERSION; + +// What the implementation answers. +inline kal_u64 version() { return kal_version(); } + +// Whether the implementation is at least as new as the declarations this +// consumer was compiled against. +// +// A consumer that proceeds against an older implementation is not merely +// missing a facility: the error set grew, so conditions it distinguishes are +// reported to it as conditions it does not distinguish. That is a wrong answer +// rather than a refusal, and a refusal is what a caller can act upon. +inline bool satisfies_header() { return kal_version() >= header_version; } + +// The interfaces the implementation provides. +struct interfaces_tag; +using interfaces_set = props; + +namespace iface { +inline constexpr interfaces_set abort_ {KAL_IFACE_ABORT}; +inline constexpr interfaces_set stream {KAL_IFACE_STREAM}; +inline constexpr interfaces_set memory {KAL_IFACE_MEMORY}; +inline constexpr interfaces_set env {KAL_IFACE_ENV}; +inline constexpr interfaces_set time {KAL_IFACE_TIME}; +inline constexpr interfaces_set random {KAL_IFACE_RANDOM}; +inline constexpr interfaces_set fs {KAL_IFACE_FS}; +inline constexpr interfaces_set process {KAL_IFACE_PROCESS}; +inline constexpr interfaces_set task {KAL_IFACE_TASK}; +inline constexpr interfaces_set exec {KAL_IFACE_EXEC}; +inline constexpr interfaces_set terminal {KAL_IFACE_TERMINAL}; +inline constexpr interfaces_set net {KAL_IFACE_NET}; +inline constexpr interfaces_set datagram {KAL_IFACE_DATAGRAM}; +inline constexpr interfaces_set space {KAL_IFACE_SPACE}; +inline constexpr interfaces_set timeout {KAL_IFACE_TIMEOUT}; +} + +inline interfaces_set interfaces() { + return interfaces_set{(kal_uintptr)kal_interfaces()}; +} +inline bool provides(interfaces_set s) { return interfaces().has(s); } + +} // namespace kal diff --git a/tools/run-conformance.sh b/tools/run-conformance.sh index effc95d..862a1a7 100755 --- a/tools/run-conformance.sh +++ b/tools/run-conformance.sh @@ -147,11 +147,39 @@ stamp="target/.features" if [ ! -f "$stamp" ] || [ "$(cat "$stamp")" != "$features" ]; then rm -rf target fi +# ⚠️⚠️ AND THE SAME DEFECT AGAIN, IN THE LINE THAT FINDS WHAT WAS BUILT. +# +# The selection below was `find … | head -1'. `target' accumulates one directory +# per fingerprint, and a fingerprint changes when the DEPENDENCIES change and +# not only when the feature set does --- so the record above does not discard +# them and two suites sit side by side. `find' reports them in directory order, +# which is neither the order they were built in nor any order at all, so the run +# reported on whichever the file system happened to name first. +# +# Measured 2026-08-29, while the specification was being changed: a run reported +# `143 held, 0 did not hold' from a binary built two days earlier, which +# contained none of the observations that had just been added. It agreed with +# the previous run because it WAS the previous run, and nothing in its output or +# its exit status said so. +# +# ⇒ Every suite already built is removed before building, so that what is found +# afterwards is what was just produced. Only the binaries are removed, so the +# rebuild is a link and not a compile; and finding more than one afterwards is +# now a condition rather than a choice. +find target -type f \( -name 'openkal-conformance' -o -name 'openkal-conformance.exe' \) \ + -delete 2> /dev/null || true + mcpp build --features "$features" "$@" mkdir -p target && printf '%s' "$features" > "$stamp" -binary="$(find target -type f \( -name 'openkal-conformance' -o -name 'openkal-conformance.exe' \) | head -1)" -[ -n "$binary" ] || { echo "the suite was not produced" >&2; exit 2; } +produced="$(find target -type f \( -name 'openkal-conformance' -o -name 'openkal-conformance.exe' \))" +count="$(printf '%s\n' "$produced" | grep -c . || true)" +[ "$count" = 1 ] || { + echo "expected exactly one suite to have been produced, found $count:" >&2 + printf '%s\n' "$produced" >&2 + exit 2 +} +binary="$produced" # openkal.env reads variables and does not set them, so the one observation that # requires a variable whose value is empty requires the runner to supply it. From c0b515813d13265c72411027974407d07302ea47 Mon Sep 17 00:00:00 2001 From: speak-agent Date: Sat, 29 Aug 2026 01:26:34 +0800 Subject: [PATCH 2/7] One binary, built once, run against two implementations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ⚠️⚠️ THE PROPERTY A DISTRIBUTED BINARY RESTS UPON WAS NOT MERELY UNTESTED. THERE WAS NO ARTIFACT OF THE SHAPE THAT COULD TEST IT. The conformance suite asserts that one artifact, built and run in one place, behaves as this specification says. What a distributed binary needs is stronger: that one binary, BUILT ONCE, behaves as specified against an implementation IT WAS NOT COMPILED AGAINST. Every target had exactly one implementation, the choice was made at dependency resolution, and the implementation was linked in --- so there was nothing to run twice. `tools/run-abi-test.sh` produces one. It builds openkal-linux as a shared object; builds a second implementation FROM THE SAME OBJECTS, with six names renamed out of the way and answered by `conformance/abi/interposer.cpp` instead; builds one probe against neither by name; and runs that one binary against each, asserting that it is not rebuilt in between. ⭐ EVERY ANSWER IN THE SECOND RUN IS A BRANCH NOTHING HERE HAD EVER TAKEN: satisfies-floor yes -> no an implementation older than the consumer's declarations, which is what the version operation exists to let a consumer refuse granularity 4096 -> 65536 a consumer that fixed the quantum when it was built is wrong here and nowhere else has-space yes -> no an optional interface declined AT RUN TIME, where there is no linker to report it exec-available yes -> no clause 6.5's objection was that a path no artifact takes is a path nothing has verified. This is the artifact that takes it knows-identity yes -> no an implementation that cannot distinguish one node from another leaves the position clear, and a caller is TOLD rather than told wrongly The interposer is not a port: it delegates every operation to the implementation beneath and changes six answers. That is a second implementation in the only sense a consumer can observe --- a different shared object, exporting the same surface, answering differently --- and it is a hundred lines rather than three thousand. --- conformance/abi/interposer.cpp | 88 +++++++++++++++++++++ conformance/abi/probe.c | 64 +++++++++++++++ tools/run-abi-test.sh | 138 +++++++++++++++++++++++++++++++++ 3 files changed, 290 insertions(+) create mode 100644 conformance/abi/interposer.cpp create mode 100644 conformance/abi/probe.c create mode 100755 tools/run-abi-test.sh diff --git a/conformance/abi/interposer.cpp b/conformance/abi/interposer.cpp new file mode 100644 index 0000000..c07caa0 --- /dev/null +++ b/conformance/abi/interposer.cpp @@ -0,0 +1,88 @@ +// A second implementation of openkal for one target, and the only one there has +// ever been. +// +// ⚠️⚠️ WHY THIS EXISTS. Everything else in this repository asserts that ONE +// artifact, built and run in one place, behaves as the specification says. The +// property a distributed binary rests upon is different and stronger: that ONE +// BINARY, BUILT ONCE, behaves as specified against an implementation IT WAS NOT +// COMPILED AGAINST. Nothing could observe that, because every target had exactly +// one implementation and it was linked in --- so the claim was not untested, it +// was unconstructible. +// +// This is built FROM the implementation beneath it: the same objects, with four +// names renamed out of the way and answered here instead. It is therefore a +// second implementation in the only sense that matters to a consumer --- a +// different shared object, exporting the same surface, answering differently. +// +// The four are chosen because each is a path nothing else in this repository +// can reach: +// +// the memory granularity a different number, so a consumer that fixed it +// when it was built is caught +// the enquiry's `present' identity withheld, so the branch where a caller is +// TOLD it does not know is taken +// the interfaces `openkal.space' and `openkal.exec' declined, so +// the word and the absence are observed to agree +// the version older than the declarations a consumer holds, so +// its floor check is exercised +// +// ⭐ AND `KAL_EXEC_PROP_AVAILABLE' IS CLEARED HERE. Clause 6.5 objects to +// reporting availability at run time on the ground that a path no artifact takes +// is a path nothing has verified. This is the artifact that takes it. +#include + +// The names the implementation beneath was built with, renamed out of the way so +// that these may take them. +extern "C" { +kal_uintptr okabi_under_memory_granularity(void); +int okabi_under_fs_info(struct kal_dir, const char*, kal_uintptr, + kal_uintptr, kal_u32, struct kal_node_info*); +int okabi_under_fs_file_info(struct kal_file, kal_u32, + struct kal_node_info*); +} + +extern "C" { + +// Sixty-four kilobytes, which is what one real system reports and this one does +// not. A consumer that rounds to what it is told stays correct; one that fixed +// four kilobytes when it was built does not. +kal_uintptr kal_memory_granularity(void) { return 65536u; } + +kal_u64 kal_version(void) { + return KAL_VERSION_MAKE(KAL_VERSION_MAJOR, KAL_VERSION_MINOR - 1u, 0u); +} + +kal_u64 kal_interfaces(void) { + return KAL_IFACE_ABORT | KAL_IFACE_STREAM | KAL_IFACE_MEMORY + | KAL_IFACE_ENV | KAL_IFACE_TIME | KAL_IFACE_RANDOM + | KAL_IFACE_FS | KAL_IFACE_PROCESS | KAL_IFACE_TASK + | KAL_IFACE_TERMINAL | KAL_IFACE_NET | KAL_IFACE_DATAGRAM + | KAL_IFACE_TIMEOUT; +} + +kal_uintptr kal_exec_props(void) { return 0; } + +// An implementation that cannot distinguish one node from another. It leaves the +// position clear, which is what tells a caller it does not know --- as against +// answering a constant, which tells a caller that two different nodes are one. +static void withhold_identity(struct kal_node_info* out) { + if (out == nullptr) return; + if (out->self_size >= __builtin_offsetof(struct kal_node_info, size)) { + out->present &= ~(kal_u32)KAL_INFO_IDENTITY; + } +} + +int kal_fs_info(struct kal_dir base, const char* name, kal_uintptr len, + kal_uintptr flags, kal_u32 wanted, struct kal_node_info* out) { + const int e = okabi_under_fs_info(base, name, len, flags, wanted, out); + if (e == kal_ok) withhold_identity(out); + return e; +} + +int kal_fs_file_info(struct kal_file f, kal_u32 wanted, struct kal_node_info* out) { + const int e = okabi_under_fs_file_info(f, wanted, out); + if (e == kal_ok) withhold_identity(out); + return e; +} + +} // extern "C" diff --git a/conformance/abi/probe.c b/conformance/abi/probe.c new file mode 100644 index 0000000..29de70b --- /dev/null +++ b/conformance/abi/probe.c @@ -0,0 +1,64 @@ +/* One binary, built once, run against two implementations. + * + * It is a C program written directly against openkal --- not against a C library + * above it --- because the property under examination is about this interface + * and not about anything built upon it. + * + * It is linked against `libopenkal.so' by SONAME and against no implementation + * by name, so which implementation it meets is decided when it is RUN. That is + * the whole of the arrangement: everything else in this repository decides it + * when the artifact is built, and therefore cannot observe what happens when the + * two are not the same choice. + * + * The programme prints what it was told. The script that runs it decides whether + * that is what the specification says, because the two runs expect DIFFERENT + * answers and a programme that knew which one it was in would be answering from + * its own knowledge rather than from the implementation's. */ +#include +#include +#include + +static const char* yn(int b) { return b ? "yes" : "no"; } + +int main(void) { + printf("version %llu.%llu.%llu\n", + (unsigned long long)(kal_version() >> 32), + (unsigned long long)((kal_version() >> 16) & 0xffffu), + (unsigned long long)(kal_version() & 0xffffu)); + + /* ⭐ THE FLOOR. A consumer holds declarations of one version and may meet an + * implementation of another; an older one reports conditions this consumer + * distinguishes as conditions it does not, which is a wrong answer rather + * than a refusal. */ + printf("satisfies-floor %s\n", yn(kal_version() >= KAL_VERSION)); + + printf("granularity %llu\n", (unsigned long long)kal_memory_granularity()); + + const kal_u64 have = kal_interfaces(); + printf("has-space %s\n", yn((have & KAL_IFACE_SPACE) != 0)); + printf("has-exec %s\n", yn((have & KAL_IFACE_EXEC) != 0)); + printf("has-fs %s\n", yn((have & KAL_IFACE_FS) != 0)); + + /* ⚠️ AND THE WORD IS CHECKED AGAINST WHAT IS ACTUALLY THERE. A word that + * claimed an interface the object does not export would mislead exactly the + * consumer that has no linker to ask --- which is this one. */ + printf("exec-available %s\n", + yn((have & KAL_IFACE_EXEC) != 0 + && (kal_exec_props() & KAL_EXEC_PROP_AVAILABLE) != 0)); + + /* An enquiry, and whether the implementation knows this node from another. */ + struct kal_dir d = { 0 }; + kal_uintptr nlen = 0; + if (kal_fs_preopen(0, &d, NULL, 0, &nlen) == kal_ok) { + struct kal_node_info info; + memset(&info, 0, sizeof info); + info.self_size = sizeof info; + const int e = kal_fs_info(d, ".", 1, 0, KAL_INFO_ALL, &info); + printf("enquiry %s\n", e == kal_ok ? "answered" : "refused"); + printf("knows-identity %s\n", yn((info.present & KAL_INFO_IDENTITY) != 0)); + printf("kind-is-dir %s\n", yn(info.kind == kal_node_directory)); + } else { + printf("enquiry no-preopen\n"); + } + return 0; +} diff --git a/tools/run-abi-test.sh b/tools/run-abi-test.sh new file mode 100755 index 0000000..f87db3b --- /dev/null +++ b/tools/run-abi-test.sh @@ -0,0 +1,138 @@ +#!/usr/bin/env bash +# One binary, built once, run against two implementations. +# +# run-abi-test.sh +# +# ⚠️⚠️ WHY THIS IS SEPARATE FROM THE CONFORMANCE SUITE, AND WHY IT HAD TO BE +# BUILT BEFORE THE INTERFACE COULD BE CHANGED. +# +# The suite asserts that one artifact, built and run in one place, behaves as the +# specification says. The property a distributed binary rests upon is different +# and stronger: that one binary, BUILT ONCE, behaves as specified against an +# implementation IT WAS NOT COMPILED AGAINST. +# +# Nothing in this ecosystem could observe that. Every target had exactly one +# implementation, the choice was made at dependency resolution, and the +# implementation was linked in --- so the claim was not merely untested, there +# was no artifact of the shape that could test it. This produces one. +# +# The second implementation is built FROM the first: the same objects, with four +# names renamed out of the way and answered by `conformance/abi/interposer.cpp' +# instead. It is a second implementation in the only sense that matters to a +# consumer --- a different shared object, exporting the same surface, answering +# differently --- and it is four hundred lines lighter than a second port. +set -euo pipefail + +impl="${1:?usage: run-abi-test.sh }" +here="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +impl="$(cd "$impl" && pwd)" +work="${TMPDIR:-/tmp}/openkal-abi-$$" +trap 'rm -rf "$work"' EXIT +mkdir -p "$work/one" "$work/two" "$work/obj" + +cxx="${CXX:-}" +if [ -z "$cxx" ]; then + cxx="$(command -v clang++ || command -v g++ || true)" +fi +[ -n "$cxx" ] || { echo "no C++ compiler" >&2; exit 2; } +cc="${CC:-${cxx%++}}" +command -v "$cc" > /dev/null 2>&1 || cc="$cxx -x c" + +objcopy="$(command -v llvm-objcopy || command -v objcopy || true)" +[ -n "$objcopy" ] || { echo "no objcopy, which is what renames the four" >&2; exit 2; } + +echo "--- the implementation, as a shared object ---" +for src in "$impl"/src/*.cpp; do + $cxx -std=c++23 -O1 -fPIC -fno-exceptions -fno-rtti \ + -I"$here/include" -c "$src" -o "$work/obj/$(basename "$src" .cpp).o" +done +$cxx -shared -o "$work/one/libopenkal.so.0" "$work/obj"/*.o -lpthread \ + -Wl,-soname,libopenkal.so.0 +ln -sf libopenkal.so.0 "$work/one/libopenkal.so" + +echo "--- a second implementation, over the first ---" +mkdir -p "$work/obj2" +for o in "$work/obj"/*.o; do + # ⚠️ RENAMED AND NOT REMOVED. The four are still there, still doing what they + # did; what changes is which name reaches them, so the second implementation + # is the first one plus four answers rather than the first one minus four. + "$objcopy" \ + --redefine-sym kal_memory_granularity=okabi_under_memory_granularity \ + --redefine-sym kal_fs_info=okabi_under_fs_info \ + --redefine-sym kal_fs_file_info=okabi_under_fs_file_info \ + --redefine-sym kal_version=okabi_under_version \ + --redefine-sym kal_interfaces=okabi_under_interfaces \ + --redefine-sym kal_exec_props=okabi_under_exec_props \ + "$o" "$work/obj2/$(basename "$o")" +done +$cxx -std=c++23 -O1 -fPIC -fno-exceptions -fno-rtti -I"$here/include" \ + -c "$here/conformance/abi/interposer.cpp" -o "$work/obj2/interposer.o" +$cxx -shared -o "$work/two/libopenkal.so.0" "$work/obj2"/*.o -lpthread \ + -Wl,-soname,libopenkal.so.0 +ln -sf libopenkal.so.0 "$work/two/libopenkal.so" + +echo "--- one probe, built once, against neither by name ---" +$cc -std=c11 -O1 -I"$here/include" "$here/conformance/abi/probe.c" \ + -o "$work/probe" -L"$work/one" -lopenkal -Wl,-rpath,'$ORIGIN' + +# ⭐ THE ONE OBSERVATION THAT MAKES THIS A TEST OF DISTRIBUTION. The binary is +# not rebuilt between the two runs, and this is where that is asserted rather +# than assumed --- a script that rebuilt it would be running two builds and +# reporting on one. +before="$(cksum < "$work/probe")" + +run() { # run ; prints the probe's report + ( cd "$1" && LD_LIBRARY_PATH="$PWD" "$work/probe" ) +} + +echo +echo "=== against the implementation it was linked against ===" +one_out="$(run "$work/one")" +printf '%s\n' "$one_out" + +echo +echo "=== against one it was not ===" +two_out="$(run "$work/two")" +printf '%s\n' "$two_out" + +after="$(cksum < "$work/probe")" +[ "$before" = "$after" ] || { echo "the binary changed between the runs" >&2; exit 1; } + +echo +echo "=== what the two runs are required to say ===" +fail=0 +expect() { # expect + local got + got="$(printf '%s\n' "$1" | awk -v k="$2" '$1 == k { print $2 }')" + if [ "$got" = "$3" ]; then + printf 'held %-16s %-10s %s\n' "$2" "$3" "$4" + else + printf 'DID NOT HOLD %-16s expected %s, got %s -- %s\n' "$2" "$3" "${got:-nothing}" "$4" + fail=1 + fi +} + +expect "$one_out" satisfies-floor yes "the first is as new as the declarations" +expect "$one_out" granularity 4096 "and reports its own quantum" +expect "$one_out" has-space yes "and provides openkal.space" +expect "$one_out" exec-available yes "and grants executable memory" +expect "$one_out" knows-identity yes "and distinguishes one node from another" +expect "$one_out" kind-is-dir yes "and answers an enquiry" + +# ⭐ THE SAME BINARY, AND EVERY ONE OF THESE IS THE OPPOSITE. Each is a branch +# that no artifact in this ecosystem had ever taken, because nothing could +# produce an implementation that answers this way. +expect "$two_out" satisfies-floor no "the second is older than the declarations" +expect "$two_out" granularity 65536 "and reports a coarser quantum" +expect "$two_out" has-space no "and declines openkal.space" +expect "$two_out" exec-available no "and grants no executable memory" +expect "$two_out" knows-identity no "and does not distinguish nodes" +expect "$two_out" kind-is-dir yes "while still answering the enquiry" + +echo +if [ "$fail" = 0 ]; then + echo "one binary behaved as specified against both implementations" +else + echo "the binary did not behave as specified against both" >&2 +fi +exit "$fail" From 250b49a6102dd1e8ecf550bc156c92b48426075f Mon Sep 17 00:00:00 2001 From: speak-agent Date: Sat, 29 Aug 2026 01:39:16 +0800 Subject: [PATCH 3/7] A README that names versions which exist, and a check that it does MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ⚠️⚠️ THIS README TOLD A READER TO ASK FOR `openkal = "0.5.1"` WHILE THE PACKAGE WAS AT 0.9.0 --- four minor versions, each of which had changed the surface. A reader following it got declarations that do not match the documentation around them, and nothing said so. ⭐ THE STALENESS IS NOT THE FINDING. That it was invisible is. The surface is checked against SURFACE.txt, the declarations are checked in both forms, the behaviour is checked by the conformance suite --- and the one line a reader actually types was checked by nobody. `tools/check-readme-versions.sh` checks that every ` = ""` in a README agrees with that package's own manifest. It reports `?` rather than passing where the package's tree is not beside it, so its "no" and its "did not run" do not read the same --- which is the failure mode this ecosystem keeps meeting and is worth two lines to avoid. --- README.md | 8 ++-- tools/check-readme-versions.sh | 70 ++++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 4 deletions(-) create mode 100755 tools/check-readme-versions.sh diff --git a/README.md b/README.md index 58f81d4..db76fcd 100644 --- a/README.md +++ b/README.md @@ -77,16 +77,16 @@ conditional on the target. ```toml [dependencies] -openkal = "0.5.1" +openkal = "0.9.0" [target.'cfg(os = "linux")'.dependencies] -openkal-linux = "0.5.1" +openkal-linux = "0.7.0" [target.'cfg(os = "macos")'.dependencies] -openkal-macos = "0.3.1" +openkal-macos = "0.6.0" [target.'cfg(windows)'.dependencies] -openkal-windows = "0.1.1" +openkal-windows = "0.4.0" ``` The program imports the interface and names no implementation. diff --git a/tools/check-readme-versions.sh b/tools/check-readme-versions.sh new file mode 100755 index 0000000..23e2571 --- /dev/null +++ b/tools/check-readme-versions.sh @@ -0,0 +1,70 @@ +#!/usr/bin/env bash +# The versions a README tells a reader to write must be the versions that exist. +# +# check-readme-versions.sh [] +# +# ⚠️⚠️ A README THAT NAMES A VERSION DRIFTS SILENTLY, AND THIS ONE HAD. +# +# Every README in this ecosystem opens by showing what a program writes in its +# manifest. Those lines are the first thing a reader copies and the last thing +# anyone edits: when this check was written, the specification's own README told +# a reader to ask for `openkal = "0.5.1"` while the package was at 0.9.0 --- four +# minor versions, each of which had changed the surface. A reader following it +# got a version whose declarations do not match the documentation around them, +# and nothing said so. +# +# ⭐ THE POINT IS NOT THE STALENESS, IT IS THAT IT WAS INVISIBLE. Everything else +# in these packages is checked by something: the surface against SURFACE.txt, the +# declarations against both forms, the behaviour against the conformance suite. +# The one thing a reader actually types was checked by nobody. +# +# The rule is narrow on purpose: a line of the form ` = ""` in +# a README must agree with that package's own manifest where the package is one +# of this ecosystem's. A version of anything else is not this check's business. +set -euo pipefail + +here="$(cd "${1:-$(dirname "${BASH_SOURCE[0]}")/..}" && pwd)" +beside="$(cd "$here/.." && pwd)" +readme="$here/README.md" +[ -f "$readme" ] || { echo "no README at $here" >&2; exit 2; } + +version_of() { # version_of --- from the package's own manifest + local pkg="$1" at + for at in "$beside/$pkg" "$here"; do + if [ -f "$at/mcpp.toml" ] && + [ "$(sed -n 's/^name *= *"\([^"]*\)".*/\1/p' "$at/mcpp.toml" | head -1)" = "$pkg" ]; then + sed -n 's/^version *= *"\([^"]*\)".*/\1/p' "$at/mcpp.toml" | head -1 + return + fi + done +} + +fail=0 +checked=0 +while IFS= read -r line; do + pkg="${line%% *}" + want="$(printf '%s' "$line" | sed -n 's/.*= *"\([^"]*\)".*/\1/p')" + have="$(version_of "$pkg" || true)" + + # A package whose tree is not beside this one cannot be checked, and saying + # so is better than passing: a check that is silent when it cannot look is a + # check whose "no" and whose "did not run" read the same. + if [ -z "$have" ]; then + echo " ? $pkg = \"$want\" -- no tree beside this one to compare against" + continue + fi + checked=$((checked + 1)) + if [ "$want" = "$have" ]; then + echo " ok $pkg = \"$want\"" + else + echo " NO $pkg = \"$want\" -- the package is at $have" + fail=1 + fi +done < <(grep -oE '^openkal[a-z-]* = "[0-9][0-9.]*"' "$readme" | sort -u) + +if [ "$checked" = 0 ]; then + echo "no version of this ecosystem's packages appears in $readme" >&2 + exit 0 +fi +[ "$fail" = 0 ] && echo "the README asks for versions that exist: $checked checked" +exit "$fail" From 4f040230288887d3a2d1f2375acacb396a95c507 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Sat, 29 Aug 2026 02:12:22 +0800 Subject: [PATCH 4/7] fix: the examples and the checks that were reading an older surface The self-review of this change found four things it had missed, all of the same shape: a place that names the surface and is not compiled by anything that would notice it had moved. examples/ was one. Three programs there still returned kal_io_result, called kal_fs_open_file, and borrowed environment strings by pointer. They build in continuous integration, so they would have failed there; they had not been looked at because nothing about them says which version they are written for. The portable program's manifest also asked for openkal 0.8.0 while claiming to demonstrate the current contract to a reader who copies it. The frozen-layout probe in this workflow asserted kal_node_info's 0.8 layout (size at 0, 24 bytes). It now asserts the 0.9 one, and asserts the property that layout exists to have: self_size is at offset zero, because an implementation built later reads it before it touches anything else. Both widths were compiled locally before this was pushed. SURFACE.txt's self-description group was spelled `# openkal ---', which does not match the `# openkal.' pattern openkal-linux derives its module imports from. The two names in it were listed without an import and its generated test failed naming the names rather than the heading. check-readme-versions.sh now checks manifests as well as READMEs, which is where the drift above was. Writing that found a defect in the tool itself: a manifest with no version pin makes grep exit 1, and under pipefail that ended the loop -- so the survey stopped at the first such file and reported "ok" having examined eight of eleven pins. It now carries a denominator, and a floor that denominator cannot supply: the package's own root manifest must have been reached. --- .github/workflows/ci.yml | 26 +++++- SPEC.md | 10 ++- SURFACE.txt | 14 ++- examples/portable/mcpp.toml | 8 +- examples/portable/src/main.cpp | 68 ++++++++++----- .../substitution/impl-discard/src/stream.cpp | 7 +- examples/substitution/impl-fd/src/stream.cpp | 12 +-- include/openkal/timeout.h | 2 +- src/timeout.cppm | 2 +- tools/check-readme-versions.sh | 85 +++++++++++++++++-- 10 files changed, 176 insertions(+), 58 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bb11c0e..f493848 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -258,11 +258,31 @@ jobs: cat > .abi-probe/src/main.cpp <<'EOF' #include #include - static_assert(__builtin_offsetof(kal_node_info, size) == 0); - static_assert(__builtin_offsetof(kal_node_info, modified_ns) == 8); - static_assert(sizeof(kal_node_info) == 24); + // `self_size' IS FIRST, AND THAT IS THE WHOLE OF THE GROWTH RULE. + // An implementation built at a later version reads this field before + // it touches anything else, which is how it learns how much of the + // structure the caller has. A layout in which it were not first would + // make the field unreadable by exactly the implementation it exists + // to inform. + static_assert(__builtin_offsetof(kal_node_info, self_size) == 0); + static_assert(__builtin_offsetof(kal_node_info, present) == 4); + // The fields that existed at 0.9.0, at the offsets they had. Growth + // is permitted only past the end; these do not move. + static_assert(__builtin_offsetof(kal_node_info, size) == 8); + static_assert(__builtin_offsetof(kal_node_info, modified_ns) == 16); + static_assert(__builtin_offsetof(kal_node_info, identity) == 24); + static_assert(__builtin_offsetof(kal_node_info, kind) == 40); + static_assert(__builtin_offsetof(kal_node_info, writable) == 44); + // The same at both pointer widths, which is the property this + // 32-bit row exists to observe: no field of it is pointer-sized. + static_assert(sizeof(kal_node_info) == 48); static_assert(sizeof(kal_dir) == sizeof(kal_uintptr)); static_assert(sizeof(kal_file) == sizeof(kal_uintptr)); + // A transfer is one signed word, and a signed word is a pointer's + // width. An implementation returning `int' here would truncate a + // count on a 64-bit machine and would be caught by nothing else. + static_assert(sizeof(kal_intptr) == sizeof(kal_uintptr)); + static_assert((kal_intptr)-1 < 0); extern "C" void _start() { for (;;) {} } EOF for t in riscv64-none-elf riscv32-none-elf; do diff --git a/SPEC.md b/SPEC.md index df00999..1ad6915 100644 --- a/SPEC.md +++ b/SPEC.md @@ -263,11 +263,13 @@ and C cannot, and both are checks rather than facilities. **The frozen layouts are asserted.** Clause 5.3 declares the layout of every structure immutable. A declaration that something shall not change is not a -mechanism; `static_assert` is. `kal_io_result` being two machine words is the -difference between a result returned in registers and one returned through a +mechanism; `static_assert` is. A handle being one machine word is the +difference between a value returned in a register and one returned through a hidden pointer, which is a change of calling convention that no declaration would report and that a consumer built against the earlier layout would not -survive. +survive. `kal_node_info` is the one structure that is permitted to grow, and it +carries `self_size` for precisely that reason — the assertion on it fixes the +offsets of the fields that exist, not the size of the whole. **The capability words become types that cannot be mixed.** Clause 6.2 gives each interface a word and positions within it, and every such word is a @@ -766,7 +768,7 @@ implementation that reports absence as a failure obliges every caller to distinguish that failure from the ones that denote a broken enquiry — a directory it may not read, a name it may not resolve. -`kal_fs_open_file`, `kal_fs_open` and `kal_fs_open_dir` are access rather than +`kal_fs_open` and `kal_fs_open_dir` are access rather than enquiry, and shall report a name that does not exist as `kal_err_not_found`. The distinction is the same one `openkal.env` draws between a variable that is diff --git a/SURFACE.txt b/SURFACE.txt index 8b6ad28..ad7e660 100644 --- a/SURFACE.txt +++ b/SURFACE.txt @@ -7,10 +7,16 @@ # An implementation provides an interface in whole or not at all, so the # absence of a group below denotes an interface the implementation does not # provide and is not a deviation. -# openkal --- the specification's own self-description. Not an interface: it -# provides no resource, so clause 3.2's closure of the core SET is untouched. -# Every conforming implementation exports both, so that a consumer with no -# linker to ask can ask before it calls. +# openkal.version --- the specification's own self-description. Not an +# interface: it provides no resource, so clause 3.2's closure of the core SET is +# untouched. Every conforming implementation exports both, so that a consumer +# with no linker to ask can ask before it calls. +# +# It is spelled as a group like the interfaces because it IS a header and a +# module of that name, and because the groups here are what a reader and a +# generator both consume: openkal-linux derives its module-import list from +# these headings, and a group spelled differently produced two names with no +# import and a diagnostic that named the names rather than the heading. kal_interfaces kal_version # openkal.abort diff --git a/examples/portable/mcpp.toml b/examples/portable/mcpp.toml index c9ff10d..9640783 100644 --- a/examples/portable/mcpp.toml +++ b/examples/portable/mcpp.toml @@ -9,13 +9,13 @@ name = "portable" version = "0.1.0" [dependencies] -openkal = "0.8.0" +openkal = "0.9.0" [target.'cfg(os = "linux")'.dependencies] -openkal-linux = "0.6.0" +openkal-linux = "0.7.0" [target.'cfg(os = "macos")'.dependencies] -openkal-macos = "0.5.0" +openkal-macos = "0.6.0" [target.'cfg(windows)'.dependencies] -openkal-windows = "0.3.0" +openkal-windows = "0.4.0" diff --git a/examples/portable/src/main.cpp b/examples/portable/src/main.cpp index d7aa039..92a2fae 100644 --- a/examples/portable/src/main.cpp +++ b/examples/portable/src/main.cpp @@ -41,10 +41,12 @@ static void say(const char* s) { kal_uintptr n = length(s); kal_uintptr done = 0; while (done < n) { - kal_io_result r = kal_stream_write(kal_stdout(), s + done, n - done); - if (r.e != kal_ok) return; - if (r.n == 0) return; - done += r.n; + // The count, or a negated condition. A partial write is not a failure + // and is why this loops; a count of zero would loop forever, so it + // ends the attempt. + const kal_intptr r = kal_stream_write(kal_stdout(), s + done, n - done); + if (r <= 0) return; + done += (kal_uintptr)r; } } @@ -113,8 +115,8 @@ int main() { // observation is that the interface reports its outcome rather than // returning a count that must be interpreted. { - kal_io_result r = kal_stream_write(kal_stdout(), "", 0); - observe(r.e == kal_ok && r.n == 0, "a stream reports its outcome"); + const kal_intptr r = kal_stream_write(kal_stdout(), "", 0); + observe(r == 0, "a stream reports the count it moved"); } // openkal.memory @@ -128,11 +130,15 @@ int main() { // openkal.env { + char buf[4096]; bool args = kal_env_arg_count() >= 1; - kal_uintptr n = 0, vn = 0; - bool named = kal_env_arg(0, &n) != nullptr && n > 0; + // The length reported is the value's, not the buffer's, so a caller may + // ask with no buffer at all in order to size one. + const kal_intptr n = kal_env_arg(0, buf, sizeof buf); + bool named = n > 0 && n == kal_env_arg(0, nullptr, 0); // A variable that is absent is reported as absent rather than as empty. - bool absent = kal_env_var("OPENKAL_A_NAME_NOTHING_SETS", 27, &vn) == nullptr; + bool absent = kal_env_var("OPENKAL_A_NAME_NOTHING_SETS", 27, buf, sizeof buf) + == -kal_err_not_found; observe(args && named && absent, "the environment supplies arguments, and absence differs from emptiness"); } @@ -150,42 +156,57 @@ int main() { // openkal.fs — every operation is relative to a directory the environment // supplied, and a name that leaves it is refused rather than followed. { - kal_dir wd{}; const char* nm = nullptr; kal_uintptr nl = 0; - bool got = kal_fs_preopen_count() >= 1 && kal_fs_preopen(0, &wd, &nm, &nl) == kal_ok; + kal_dir wd{}; char nm[256]; kal_uintptr nl = 0; + bool got = kal_fs_preopen_count() >= 1 + && kal_fs_preopen(0, &wd, nm, sizeof nm, &nl) == kal_ok; kal_file f{}; - bool made = got && kal_fs_open_file(wd, "portable.probe", 14, 1, 1, &f) == kal_ok; + const auto rw = kal::fs::open::read | kal::fs::open::write + | kal::fs::open::create | kal::fs::open::truncate; + bool made = got && kal::fs::open_file(wd, "portable.probe", 14, rw, &f) == kal_ok; if (made) { - kal_stream s{kal_fs_stream(f)}; + // The stream of a file is a stream. It is not converted, it is not + // a number reinterpreted --- the operation's type says so. + kal_stream s = kal_fs_stream(f); kal_stream_write(s, "0123456789", 10); kal_fs_close_file(f); } - kal_node_info info{}; - bool sized = made && kal_fs_info(wd, "portable.probe", 14, &info) == kal_ok && info.size == 10; + // The caller states how much of the structure exists on its side, and + // the implementation reports which fields it filled. Both are what let + // this program run against an implementation built at another version. + kal_node_info info = kal::fs::info_for_caller(); + bool sized = made + && kal_fs_info(wd, "portable.probe", 14, 0, + kal::fs::field::size, &info) == kal_ok + && (info.present & kal::fs::field::size) != 0 + && info.size == 10; // Positioning is a property of the file rather than of the stream, // which is why it is declared here and not in openkal.stream. kal_file g{}; bool sought = false; - if (made && kal_fs_open_file(wd, "portable.probe", 14, 0, 0, &g) == kal_ok) { + if (made && kal::fs::open_file(wd, "portable.probe", 14, + kal::fs::open::read, &g) == kal_ok) { kal_u64 at = 0; sought = kal_fs_seek(g, 6, kal::fs::seek_set, &at) == kal_ok && at == 6; char buf[8] = {}; - kal_io_result r = kal_stream_read(kal_stream{kal_fs_stream(g)}, buf, 4); - sought = sought && r.e == kal_ok && r.n == 4 - && buf[0] == '6' && buf[3] == '9'; + const kal_intptr r = kal_stream_read(kal_fs_stream(g), buf, 4); + sought = sought && r == 4 && buf[0] == '6' && buf[3] == '9'; kal_fs_close_file(g); } kal_file escape{}; - bool refused = got && kal_fs_open_file(wd, "../portable.probe", 17, 0, 0, &escape) != kal_ok; + bool refused = got && kal::fs::open_file(wd, "../portable.probe", 17, + kal::fs::open::read, &escape) != kal_ok; if (made) kal_fs_remove(wd, "portable.probe", 14); // Clause 7.7: enquiry about a name that does not exist succeeds and // reports absence. A test that expected an error here would pass // against an implementation that conflated enquiry with access. - bool gone = got && kal_fs_info(wd, "portable.probe", 14, &info) == kal_ok + info = kal::fs::info_for_caller(); + bool gone = got && kal_fs_info(wd, "portable.probe", 14, 0, + kal::fs::field::kind, &info) == kal_ok && info.kind == kal_node_absent; observe(got && made && sized && sought && refused && gone, @@ -218,8 +239,9 @@ int main() { // its absence is reported as unobservable rather than as a failure. kal_dir root{}; bool haveRoot = false; for (kal_uintptr i = 0, n = kal_fs_preopen_count(); i < n && !haveRoot; ++i) { - kal_dir d{}; const char* nm = nullptr; kal_uintptr nl = 0; - if (kal_fs_preopen(i, &d, &nm, &nl) == kal_ok && nl == 1 && nm[0] == '/') { + kal_dir d{}; char nm[256]; kal_uintptr nl = 0; + if (kal_fs_preopen(i, &d, nm, sizeof nm, &nl) == kal_ok + && nl == 1 && nm[0] == '/') { root = d; haveRoot = true; } } diff --git a/examples/substitution/impl-discard/src/stream.cpp b/examples/substitution/impl-discard/src/stream.cpp index 25d3a24..d934877 100644 --- a/examples/substitution/impl-discard/src/stream.cpp +++ b/examples/substitution/impl-discard/src/stream.cpp @@ -4,7 +4,8 @@ extern "C" { kal_stream kal_stdin (void) { return kal_stream{0}; } kal_stream kal_stdout(void) { return kal_stream{1}; } kal_stream kal_stderr(void) { return kal_stream{2}; } -kal_io_result kal_stream_write(kal_stream, const void*, kal_uintptr len) { return { len, kal_ok }; } -kal_io_result kal_stream_read (kal_stream, void*, kal_uintptr) { return { 0, kal_ok }; } -int kal_stream_flush(kal_stream) { return kal_ok; } +// The count, or the negated condition when no byte moved. One signed word. +kal_intptr kal_stream_write(kal_stream, const void*, kal_uintptr len) { return (kal_intptr)len; } +kal_intptr kal_stream_read (kal_stream, void*, kal_uintptr) { return 0; } +int kal_stream_flush(kal_stream) { return kal_ok; } } diff --git a/examples/substitution/impl-fd/src/stream.cpp b/examples/substitution/impl-fd/src/stream.cpp index 497fff9..ae19a00 100644 --- a/examples/substitution/impl-fd/src/stream.cpp +++ b/examples/substitution/impl-fd/src/stream.cpp @@ -5,15 +5,15 @@ extern "C" { kal_stream kal_stdin (void) { return kal_stream{0}; } kal_stream kal_stdout(void) { return kal_stream{1}; } kal_stream kal_stderr(void) { return kal_stream{2}; } -kal_io_result kal_stream_write(kal_stream s, const void* b, kal_uintptr n) { +// The count, or the negated condition when no byte moved --- which is what a +// caller of these wanted anyway, and is one word rather than two. +kal_intptr kal_stream_write(kal_stream s, const void* b, kal_uintptr n) { const auto r = ::write(static_cast(s.h), b, n); - return r < 0 ? kal_io_result{ 0, kal_err_io } - : kal_io_result{ static_cast(r), kal_ok }; + return r < 0 ? -kal_err_io : static_cast(r); } -kal_io_result kal_stream_read(kal_stream s, void* b, kal_uintptr n) { +kal_intptr kal_stream_read(kal_stream s, void* b, kal_uintptr n) { const auto r = ::read(static_cast(s.h), b, n); - return r < 0 ? kal_io_result{ 0, kal_err_io } - : kal_io_result{ static_cast(r), kal_ok }; + return r < 0 ? -kal_err_io : static_cast(r); } int kal_stream_flush(kal_stream) { return kal_ok; } } diff --git a/include/openkal/timeout.h b/include/openkal/timeout.h index 141aaef..0805cc7 100644 --- a/include/openkal/timeout.h +++ b/include/openkal/timeout.h @@ -4,7 +4,7 @@ * Every operation here is the operation of the same name in another interface, * with one argument added. Clause 7.8 already establishes that a second form of * one operation is admissible when the first cannot state the whole of an - * intent: `kal_fs_open_file' and `kal_fs_open' stand beside each other for that + * intent: `kal_fs_info' and `kal_fs_file_info' stand beside each other for that * reason, and these stand beside their originals for the same one. * * THE ARGUMENT IS A DURATION, NOT AN INSTANT, and the name of this interface diff --git a/src/timeout.cppm b/src/timeout.cppm index bcde89b..fb8d7ef 100644 --- a/src/timeout.cppm +++ b/src/timeout.cppm @@ -4,7 +4,7 @@ // Every operation here is the operation of the same name in another interface, // with one argument added. Clause 7.8 already establishes that a second form of // one operation is admissible when the first cannot state the whole of an -// intent: kal_fs_open_file and kal_fs_open stand beside each other for that +// intent: kal_fs_info and kal_fs_file_info stand beside each other for that // reason, and these stand beside their originals for the same one. // // The argument is a duration, not an instant, and the name of this interface diff --git a/tools/check-readme-versions.sh b/tools/check-readme-versions.sh index 23e2571..9ffef51 100755 --- a/tools/check-readme-versions.sh +++ b/tools/check-readme-versions.sh @@ -18,9 +18,20 @@ # declarations against both forms, the behaviour against the conformance suite. # The one thing a reader actually types was checked by nobody. # -# The rule is narrow on purpose: a line of the form ` = ""` in -# a README must agree with that package's own manifest where the package is one -# of this ecosystem's. A version of anything else is not this check's business. +# The rule is narrow on purpose: a line of the form ` = ""` +# must agree with that package's own manifest where the package is one of this +# ecosystem's. A version of anything else is not this check's business. +# +# ⚠️⚠️ AND IT IS NOT ONLY READMES. This checked READMEs alone until 2026-08-28, +# when a change spanning eight repositories was reviewed and `openkal-musl`'s +# own manifest was found pinning `openkal-windows = "0.3.0"` against a package +# that had moved to 0.4.0. The README beside it was correct, because the README +# was the thing being checked. +# +# ⭐ A MANIFEST PIN IS THE SAME CLASS OF FACT AS A README LINE --- a version of +# a sibling written down here and true somewhere else --- so it is checked by +# the same rule. Example manifests are included: an example is a README a reader +# can build. set -euo pipefail here="$(cd "${1:-$(dirname "${BASH_SOURCE[0]}")/..}" && pwd)" @@ -28,6 +39,19 @@ beside="$(cd "$here/.." && pwd)" readme="$here/README.md" [ -f "$readme" ] || { echo "no README at $here" >&2; exit 2; } +# The files whose version lines are a promise to somebody: the README a reader +# copies from, and every manifest in this tree that names a sibling. `target' +# is excluded because a build directory holds copies of manifests this tree +# does not own. +# ⚠️ `-L', BECAUSE `$here' MAY BE A SYMBOLIC LINK. `find' does not descend into +# one unless told to, and when this was run against a tree reached by link it +# examined the README and NOTHING ELSE --- reporting "1 files" and passing. +sources() { + printf '%s\n' "$readme" + find -L "$here" -name mcpp.toml -not -path '*/target/*' -not -path '*/.spec/*' \ + -not -path '*/.impl/*' | sort +} + version_of() { # version_of --- from the package's own manifest local pkg="$1" at for at in "$beside/$pkg" "$here"; do @@ -39,9 +63,17 @@ version_of() { # version_of --- from the package's own manifest done } +# ⭐ A DENOMINATOR. The count of pins alone cannot distinguish a tree with few +# pins from a survey that stopped early, which is the defect recorded below. +seen="$(mktemp)" +trap 'rm -f "$seen"' EXIT + fail=0 checked=0 +absent=0 while IFS= read -r line; do + where="${line%%|*}" + line="${line#*|}" pkg="${line%% *}" want="$(printf '%s' "$line" | sed -n 's/.*= *"\([^"]*\)".*/\1/p')" have="$(version_of "$pkg" || true)" @@ -50,21 +82,56 @@ while IFS= read -r line; do # so is better than passing: a check that is silent when it cannot look is a # check whose "no" and whose "did not run" read the same. if [ -z "$have" ]; then - echo " ? $pkg = \"$want\" -- no tree beside this one to compare against" + echo " ? $where: $pkg = \"$want\" -- no tree beside this one to compare against" + absent=$((absent + 1)) continue fi checked=$((checked + 1)) if [ "$want" = "$have" ]; then - echo " ok $pkg = \"$want\"" + echo " ok $where: $pkg = \"$want\"" else - echo " NO $pkg = \"$want\" -- the package is at $have" + echo " NO $where: $pkg = \"$want\" -- the package is at $have" fail=1 fi -done < <(grep -oE '^openkal[a-z-]* = "[0-9][0-9.]*"' "$readme" | sort -u) +done < <( + while IFS= read -r file; do + [ -f "$file" ] || continue + # A pin in either form: `pkg = "1.2.3"' and `pkg = { version = "1.2.3" ...'. + # + # ⚠️⚠️ `|| true' IS LOAD-BEARING AND ITS ABSENCE TRUNCATED THIS SURVEY + # WITHOUT SAYING SO. A manifest whose dependencies are all path form --- + # `examples/substitution/app/mcpp.toml' is one --- matches nothing, so + # grep exits 1; under `set -e' with `pipefail' that ended the LOOP, and + # every file sorting after it was never examined. Measured: the check + # reported nine pins and there were eleven, and it reported them with + # the same "ok" it uses when it has looked at everything. + grep -oE '^[[:space:]]*openkal[a-z-]* *= *("[0-9][0-9.]*"|\{ *version *= *"[0-9][0-9.]*")' "$file" | + sed -e 's/^[[:space:]]*//' -e 's/ *= *{ *version *= */ = /' | + sed "s|^|${file#"$here"/}\||" || true + echo "${file#"$here"/}|" >> "$seen" + done < <(sources) | sort -u +) if [ "$checked" = 0 ]; then - echo "no version of this ecosystem's packages appears in $readme" >&2 + echo "no version of this ecosystem's packages could be compared ($absent not beside this tree)" >&2 exit 0 fi -[ "$fail" = 0 ] && echo "the README asks for versions that exist: $checked checked" +files="$(sort -u "$seen" | wc -l)" +want="$(sources | wc -l)" +if [ "$files" != "$want" ]; then + echo "the survey examined $files of $want files; it stopped early" >&2 + exit 1 +fi + +# ⭐⭐ A DENOMINATOR DRAWN FROM THE SAME ENUMERATION CANNOT REPORT THAT THE +# ENUMERATION IS EMPTY. The count above compares the survey against `sources', +# so an enumeration that found nothing agrees with a survey that examined +# nothing and the check passes. This is the floor that does not come from it: +# every package in this ecosystem has a manifest at its root, so a survey that +# did not reach that file did not reach this package. +if ! sort -u "$seen" | grep -qx 'mcpp.toml|'; then + echo "the survey did not reach $here/mcpp.toml, so it did not examine this package" >&2 + exit 1 +fi +[ "$fail" = 0 ] && echo "every version named here exists: $checked checked in $files files, $absent not beside this tree" exit "$fail" From a22a06d15c1ca14740c71b39a150a967488a72c1 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Sat, 29 Aug 2026 02:18:14 +0800 Subject: [PATCH 5/7] =?UTF-8?q?docs(plan):=20=C2=A715=20---=20what=20landi?= =?UTF-8?q?ng=20it=20measured?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Five things that could not have been written before the change was implemented: two aarch64 defects the leg had carried since it was written, an interposer whose value is that nothing of its shape existed, eight jobs that could only be green when the change was already published, a test that asserted a limitation that was then lifted, and a defect three layers up whose every ingredient was correct. --- ...-08-28-beyond-static-linking-abi-review.md | 132 ++++++++++++++++++ 1 file changed, 132 insertions(+) diff --git a/.agents/docs/2026-08-28-beyond-static-linking-abi-review.md b/.agents/docs/2026-08-28-beyond-static-linking-abi-review.md index af802b5..9f62fdb 100644 --- a/.agents/docs/2026-08-28-beyond-static-linking-abi-review.md +++ b/.agents/docs/2026-08-28-beyond-static-linking-abi-review.md @@ -1924,3 +1924,135 @@ Unchanged from §13.3, and now the whole of it: 3. **Whether the floor operation is per-interface or one for the specification.** 4. **Where the interposer lives** — openkal's repository, where the conformance runner is, or its own. + +--- + +## 15. What landing it measured + +This section is written after the change was implemented across the eight +repositories, and records only what could not have been written before it. + +### 15.1 Two defects the aarch64 leg had carried since it was written + +The goal named x86_64 and aarch64. Running the Linux implementation under +qemu-aarch64 found two constants in `openkal-linux/src/sys.h` that were the +x86_64 values on both architectures: + +- `O_DIRECTORY` and `O_NOFOLLOW` — `0200000`/`0400000` on x86_64 and + `040000`/`0100000` on the asm-generic architectures, which is aarch64. +- `struct kstat` — the asm-generic layout orders `mode, nlink, uid, gid, rdev, + pad, size, blksize, pad, blocks`, and this file had the x86_64 order. + +⭐ **A HEAD~1 control proved they were not introduced by this change.** Both +were present before it, so the aarch64 leg had never worked, and nothing said +so: no job ran aarch64, and the x86_64 leg cannot observe either constant. +`static_assert` on the offsets now states the layout rather than assuming it. + +### 15.2 The interposer found nothing, and that is the finding + +`tools/run-abi-test.sh` builds one binary and runs it against two +implementations, the second produced from the first by renaming six symbols. All +twelve assertions held on the first run. The value is not that it found a defect: +it is that **no artifact of that shape existed in this ecosystem before**, so the +claim that a program can be built against one implementation and run against +another had never been an observation. Six of its twelve assertions exercise +branches — an older implementation, a coarser granularity, an absent interface, +no executable memory, no node identity — that nothing here could previously +produce. + +### 15.3 ⚠️⚠️ Eight jobs that could only be green when the change was already published + +Every repository substitutes working trees in continuous integration so that a +change spanning them is reviewed as a whole. Measured while landing this: **eight +jobs across four repositories called `mcpp build` at a point where the manifest +still named `openkal` by version**, and failed with `E_NOT_FOUND: package +'compat.openkal@0.9.0' not found`. + +⭐ The mechanism is not a missing substitution. `run-conformance.sh` substitutes +the manifest and **restores it on exit** — correctly, since a script that rewrote +a checked-in file and walked away would leave the tree holding a path. Every step +after it is back to naming a version. + +⭐⭐ **THE UNIT IS THE STEP, NOT THE JOB, AND NOT THE REPOSITORY.** The first +pass at this asked "does this job substitute?", found three repositories, and +passed a fourth that does substitute — and then gives it back. The second pass +asked the question of each step and found the rest. Each of the four +repositories also had a job doing it correctly, which is what made the gap +invisible to a survey done a repository at a time. + +These steps are green on `main` and **can only be green there**, because there +the published version is the one under test. It is not a check that fails; it is +a check that cannot run at the only time it would have something to say. + +### 15.4 A test that asserted a limitation, and the limitation was lifted + +`openkal-llvm-runtime/examples/cxx` asserted `creating a symbolic link is +refused, not ignored`. Decision 3 added `kal_fs_link_create`; openkal-musl +answers `symlinkat` with it; the refusal stopped arriving and the test failed. + +⭐ **That is the good case, and it is why the assertion was written that way +round.** Had it tolerated both answers, the arrival of the operation would have +been invisible in the only place in this ecosystem where a C++ standard library +exercises it. + +The same shape appeared in openkal-musl's own continuous integration, where a +probe asserting that *an absent operation reports itself* had named `symlinkat` +as the absent one. It reported zero diagnostics for a hundred attempts — the +same reading a broken diagnostic channel gives. It now names an operation +openkal does not express and **asserts that the dispatcher does not handle it**, +so the day that changes the step says so instead of passing while measuring +nothing. + +### 15.5 ⭐⭐ The defect three layers up, whose every ingredient was correct + +Adding links exposed one more, and it is the most instructive thing this change +produced. + +`fs::remove_all` on a directory holding a symbolic link returned `ENOTEMPTY` and +left the tree standing. The host toolchain removed the same tree. Every +operation the port offers behaved correctly in isolation — `symlink`, +`readlink`, `stat`, `lstat`, `unlink`, `getdents`, and `remove` on the link +itself all matched the host row for row. + +The difference was one errno. openkal states that opening RESOLVES and offers no +form that declines to — deliberately, since a program that opens a link to read +its bytes is asking what `kal_fs_link_read` answers. So `open(O_NOFOLLOW)` +resolved, and for a link whose target is absent it answered `ENOENT`. + +⚠️ **That is a different answer to a different question.** `O_NOFOLLOW` does not +ask to open the link and does not ask to open its target; it asks *whether the +name is a link*, and POSIX says `ELOOP` when it is. libc++'s `remove_all` +descends by opening each entry `O_DIRECTORY|O_NOFOLLOW`: on `ELOOP` or `ENOTDIR` +it unlinks the entry, on `ENOENT` it concludes the entry has already gone. So it +unlinked nothing and then reported the directory it had just declined to empty +as not empty. + +⭐ **THE ENQUIRY THAT ANSWERS IT IS THE ONE THIS DESIGN ADDED.** `KAL_FS_NO_RESOLVE` +lets the port ask about the name itself, on a path taken only when the caller +passed the flag. The design decision and the defect it repairs were found three +weeks and three layers apart, and the second is the evidence for the first. + +### 15.6 What the self-review found that the implementation had missed + +Recorded because all four are the same shape — a place that names the surface +and is compiled by nothing that would notice it had moved: + +1. `openkal/examples/` — three programs still on the 0.8 surface. They *are* + built in continuous integration, so they would have failed there; nothing + about them says which version they are written for. +2. The frozen-layout probe in openkal's own workflow asserted the 0.8 + `kal_node_info`. Both widths were compiled locally before the fix was pushed. +3. `SURFACE.txt`'s self-description group was spelled `# openkal ---`, which does + not match the `# openkal.` pattern openkal-linux derives its module + imports from. Two names were listed with no import, and the diagnostic named + the names rather than the heading. +4. Version pins: `openkal-musl` pinned `openkal-windows` at 0.3.0 against a + package at 0.4.0, and two READMEs asked a reader for `openkal = "0.5.1"`. + +⭐ The tool written for (4) had the defect it exists to catch. A manifest with no +version pin makes `grep` exit 1; under `pipefail` that ended the loop, so the +survey stopped at the first such file and reported **"ok" having examined eight +of eleven pins**. It now carries a denominator — and a floor that denominator +cannot supply, since a denominator drawn from the same enumeration cannot report +that the enumeration is empty: the package's own root manifest must have been +reached. From 3bfe859175c22b02639467a2f4bbff0dee506312 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Sat, 29 Aug 2026 02:33:48 +0800 Subject: [PATCH 6/7] =?UTF-8?q?docs(plan):=20=C2=A715.7=20---=20the=20same?= =?UTF-8?q?=20name=20for=20two=20quantities,=20one=20layer=20apart?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The last defect this change produced, and the one introduced BY the correction the design recommends: openkal's granularity is not musl's page size, an implementation with no memory management unit answers one, and the assertion that should have caught it passed because one is a positive power of two. --- ...-08-28-beyond-static-linking-abi-review.md | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/.agents/docs/2026-08-28-beyond-static-linking-abi-review.md b/.agents/docs/2026-08-28-beyond-static-linking-abi-review.md index 9f62fdb..48b45f7 100644 --- a/.agents/docs/2026-08-28-beyond-static-linking-abi-review.md +++ b/.agents/docs/2026-08-28-beyond-static-linking-abi-review.md @@ -2056,3 +2056,43 @@ of eleven pins**. It now carries a denominator — and a floor that denominator cannot supply, since a denominator drawn from the same enumeration cannot report that the enumeration is empty: the package's own root manifest must have been reached. + +### 15.7 ⭐⭐ The same name for two quantities, one layer apart + +The last defect this change produced is the clearest instance of what §11 argues +about, and it was introduced BY the correction §11 recommends. + +Decision 5 added `kal_memory_granularity` because a distributed binary must +learn the machine's quantum at run time rather than have it fixed at build time. +openkal-musl adopted it and assigned it to `libc.page_size`, replacing the +constant 4096. + +⚠️ **They are not the same quantity.** openkal's granularity is the coarsest +quantum a caller must respect, and an implementation for a machine with no +memory management unit answers **one** — correctly: there is no page, and +nothing needs rounding. openkal-opensbi answers one. + +`libc.page_size` is what musl rounds heap growth to, reports as +`sysconf(_SC_PAGESIZE)` and as `st_blksize`, and whose arithmetic its allocator +assumes is a power of two no smaller than its own quantum. Given one, the +allocator asked the environment for one-byte extents. + +⭐ **AND THE SYMPTOM WAS THREE CORRECT LINES FOLLOWED BY SILENCE.** The +same-source example printed `sorted`, `caught` and `unwound` — containers, +exceptions, and unwinding a destructor all held — and stopped at the fourth +line, which is the first to format a string and so the first to need an +allocation large enough to grow the heap. Over openkal-linux, whose answer is +4096, nothing was wrong. + +⚠️ **THE ASSERTION THAT SHOULD HAVE CAUGHT IT PASSED, AND WAS NOT WRONG.** The +probe said "the page size is a positive power of two obtained from the +environment". One is positive. One is a power of two. The criterion described +the shape of the number and not what the number is for. It now states what the +allocator requires, and asks for several pages in one allocation and writes +every byte of them. + +The reading to carry out of this: **a value that crosses a layer boundary +acquires the receiving layer's requirements, and the sending layer is not +obliged to know them.** openkal is right to answer one. musl is wrong to believe +it. The remedy is not in either interface but at the seam, where the answer is +taken as a floor to respect rather than as the value. From ec7c9585344565d6b38fec9509f266acfb62756f Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Sat, 29 Aug 2026 02:46:06 +0800 Subject: [PATCH 7/7] =?UTF-8?q?docs(plan):=20=C2=A715.8=20---=20two=20defe?= =?UTF-8?q?cts=20the=20aarch64=20leg=20surfaced=20that=20are=20not=20about?= =?UTF-8?q?=20aarch64?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The leg that surfaces a defect is not the axis the defect is on. musl does not pass the vectors to .init_array and glibc does, which x86_64-linux-musl settles and aarch64 alone would have mis-attributed; and openkal-windows's .def check was right with the wrong scope, reporting a number that was of the names it knew about. --- ...-08-28-beyond-static-linking-abi-review.md | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/.agents/docs/2026-08-28-beyond-static-linking-abi-review.md b/.agents/docs/2026-08-28-beyond-static-linking-abi-review.md index 48b45f7..f1a0ba9 100644 --- a/.agents/docs/2026-08-28-beyond-static-linking-abi-review.md +++ b/.agents/docs/2026-08-28-beyond-static-linking-abi-review.md @@ -2096,3 +2096,54 @@ acquires the receiving layer's requirements, and the sending layer is not obliged to know them.** openkal is right to answer one. musl is wrong to believe it. The remedy is not in either interface but at the seam, where the answer is taken as a floor to respect rather than as the value. + +### 15.8 ⚠️⚠️ Two defects the aarch64 leg surfaced that are not about aarch64 + +The goal named x86_64 and aarch64, and running the second found two more. Both +are recorded here for the same reason: **the leg that surfaces a defect is not +the axis the defect is on**, and taking the first for the second would have +produced a wrong fix in each case. + +**One C library passes the vectors and the other does not.** `openkal-linux` +captured `(argc, argv, envp)` in a `[[gnu::constructor]]`. glibc calls every +`.init_array` entry with those three; **musl calls them with no arguments**. So +under musl the function recorded whatever the argument registers held, and the +first enquiry after the count dereferenced a small integer. + +It was found by running the tests for aarch64, where it reads as an +architecture defect. The control that settles it is `x86_64-linux-musl`: + +| target | argc | argv | envp | +|---|---|---|---| +| x86_64-linux-gnu | 1 | stack | stack | +| x86_64-linux-musl | 0x4004c2 | 1 | stack | +| aarch64-linux-musl | 0x405ee4 | 1 | stack | + +Both musl rows are shifted by one and the glibc row is not, so **the axis is the +C library**. The arguments are now checked rather than believed, and where they +do not hold the vectors are recovered from `environ` by walking back over +`argv`'s terminator to the slot holding the count — which must equal the number +of entries actually found, or nothing is recorded. + +⭐ `environ` is a **weak** reference, and the independence check states that as a +rule rather than as an exception: this one name is permitted only when its type +letter is weak. It is admissible where `puts` is not because the check exists to +stop a CALL into the program's runtime re-entering this implementation, and a +pointer executes nothing; and because weak means a program with no C library +still links. A strong reference would make one required silently. Both +directions were measured. + +**A check that was right, with the wrong scope.** openkal-windows verifies that +every name its headers declare is exported by a `.def`, and its comment records +the defect it was written for: names added without `.def` lines, failing one +repository away in a consumer's cross-build. It happened again — and the check +stayed green, because it read `src/win32.h` alone and matched one declaration +macro, while `src/win.h` declares the object manager's entries in the plain +`__declspec(dllimport)` form. + +⭐ **IT REPORTED A NUMBER, AND THE NUMBER WAS OF THE NAMES IT KNEW ABOUT.** +Nothing said the set was partial — which is the failure mode a denominator is +supposed to prevent and does not, when the denominator is drawn from the same +partial enumeration. It now globs the headers and matches both forms: 58 +declared across four headers rather than 49 across one, and four names were +outside it. Only one was referenced, which is why only one broke a link.