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..f1a0ba9 --- /dev/null +++ b/.agents/docs/2026-08-28-beyond-static-linking-abi-review.md @@ -0,0 +1,2149 @@ +# 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. + +--- + +## 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. + +### 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. + +### 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. 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/README.md b/README.md index e80dd48..db76fcd 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 @@ -59,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/SPEC.md b/SPEC.md index 30ea755..1ad6915 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 @@ -225,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 @@ -277,6 +317,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 +394,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 +617,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 +687,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 +719,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 @@ -634,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 @@ -850,12 +984,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..ad7e660 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,18 @@ # 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.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 kal_abort kal_exit @@ -21,6 +33,7 @@ kal_stream_write # openkal.memory kal_alloc kal_free +kal_memory_granularity # openkal.env kal_env_arg kal_env_arg_count @@ -41,17 +54,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 +122,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/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/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/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.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..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 @@ -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..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 @@ -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/check-readme-versions.sh b/tools/check-readme-versions.sh new file mode 100755 index 0000000..9ffef51 --- /dev/null +++ b/tools/check-readme-versions.sh @@ -0,0 +1,137 @@ +#!/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 ` = ""` +# 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)" +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 + 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 +} + +# ⭐ 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)" + + # 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 " ? $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 $where: $pkg = \"$want\"" + else + echo " NO $where: $pkg = \"$want\" -- the package is at $have" + fail=1 + fi +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 could be compared ($absent not beside this tree)" >&2 + exit 0 +fi +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" 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" 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.