From c3baf22fc30d9cc4b9d5ef74df8555f1d1165e5c Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Sun, 30 Aug 2026 16:26:00 +0800 Subject: [PATCH 1/5] 0.10.0 --- five declarations, each added because its absence was a wrong answer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #22, #23, #24. ⭐⭐ WHAT THE FIVE HAVE IN COMMON. Not one of them was a facility nobody had asked for. Each was reached by a C library above this interface, found to have nothing here to reach, and answered ANYWAY --- wrongly, and without telling its caller. A specification that has no operation for X does not cause consumers not to do X. It causes them to do X badly, out of sight. `kal_fs_lock' / `kal_fs_unlock' + KAL_FS_PROP_LOCKS openkal-musl answered `fcntl(F_SETLK)' with success and took no lock: two programs held one exclusive lock and neither could find out. Measured against a host. ⭐ AND THIS IS THE OPPOSITE OF THE PERMISSION ENTRY, which is why clause 11 now has them next to each other. Permission was declined because the environments do not agree that an identity exists. Every environment this specification targets locks a byte range and spells it almost identically. What was missing was a word, not a capability. ⭐ Release upon the holder's end is REQUIRED rather than observed, and that requirement is why this cannot be composed above the line: a caller can build exclusion out of KAL_OPEN_EXCLUSIVE and a name, and nothing then releases that name when its holder dies. ⚠️ The holder is the `kal_file' and NOT the program. One environment's oldest form releases every lock a program holds upon a node as soon as it closes ANY descriptor for it --- so a library that opened one file twice destroyed its own lock. An implementation shall not expose that; the environments that have the older form also have a newer one whose holder is exactly the open file. `kal_process_spawn_bound' + KAL_PROCESS_PROP_BOUND_LIFETIME Clause 7.1 declines to replace a running image, and that stands. The consequence this specification had NOT recorded is that a C library asked for `execve' composes it, and the composition leaves THREE images where a system with the operation has two --- and a signal reaches the middle one. Measured with a host as control: identical status words, opposite outcomes; the caller is told the program died on the signal it sent while the program runs to completion, unsupervised. `kal_process_terminate' was not at fault. What was missing was a way to SAY the thing `execve' means. `kal_task_parallelism' KAL_TASK_PROP_PARALLEL says WHETHER and not HOW MANY, so `hardware_concurrency' answered 1 with no error and a program sizing a pool of workers got one worker. ⭐ Zero is "cannot say" and is distinct from one: an environment with one processor and an environment that will not answer call for different behaviour. `kal_fs_set_modified_at' `kal_fs_set_modified' is stated on an open FILE and a directory is opened as a `kal_dir', so this interface had NO route to a directory's time at all --- while `kal_fs_info' reports one. A consumer that stamps a lock directory drove one implementation to open the directory for READING and set the time on that, outside anything stated here. ⚠️ A divergence caused by a missing declaration is a defect of the specification, and is recorded as one. `kal_fs_capacity' + KAL_FS_PROP_CAPACITY `std::filesystem::space' had nothing beneath it. --- what this does NOT do -------------------------------------------------- ⚠️ THE TWO SPAWN ADDITIONS DO NOT COMBINE, AND THAT IS DELIBERATE. There is no form that both grants directories and binds a lifetime. Declaring every combination is how an interface acquires four spawns and then eight, so the combination is declared when something needs it. What a caller must not do meanwhile is take `kal_process_spawn_with' and assume the binding. Clause 8 is satisfied: five declarations added, none altered, no layout touched. Clause 6.2 is satisfied by the property words --- an operation present and not performable here is not a defect, because a caller can ask first, which is exactly the argument clause 11 already records for the link operations. --- README.md | 8 +-- SPEC.md | 57 +++++++++++++++++- SURFACE.txt | 8 ++- conformance/mcpp.toml | 2 +- conformance/src/declarations.c | 6 ++ examples/portable/mcpp.toml | 8 +-- include/openkal/fs.h | 104 +++++++++++++++++++++++++++++++++ include/openkal/process.h | 48 +++++++++++++++ include/openkal/task.h | 18 ++++++ include/openkal/version.h | 2 +- kit/mcpp.toml | 4 +- mcpp.toml | 2 +- src/fs.cppm | 4 ++ src/process.cppm | 1 + src/task.cppm | 1 + 15 files changed, 257 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 5f6b77c..de4178d 100644 --- a/README.md +++ b/README.md @@ -77,16 +77,16 @@ conditional on the target. ```toml [dependencies] -openkal = "0.9.0" +openkal = "0.10.0" [target.'cfg(os = "linux")'.dependencies] -openkal-linux = "0.7.1" +openkal-linux = "0.9.0" [target.'cfg(os = "macos")'.dependencies] -openkal-macos = "0.6.1" +openkal-macos = "0.7.0" [target.'cfg(windows)'.dependencies] -openkal-windows = "0.4.0" +openkal-windows = "0.5.0" ``` The program imports the interface and names no implementation. diff --git a/SPEC.md b/SPEC.md index 1ad6915..806ab67 100644 --- a/SPEC.md +++ b/SPEC.md @@ -1,4 +1,4 @@ -# openkal Specification, version 0.9 +# openkal Specification, version 0.10 ## 1. Scope @@ -50,7 +50,7 @@ provides an interface in whole or not at all. | `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 +Version 0.10 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 @@ -1033,3 +1033,56 @@ The following are recorded so that they are not mistaken for oversights. general mechanism for passing a handle to a context in another space is not defined by this version. It is the question `openkal.space` reaches first and is not peculiar to it. +10. **Exclusion upon a range of a file.** ⚠️ **Settled in 0.10.** `kal_fs_lock` + and `kal_fs_unlock` are operations of `openkal.fs`, admitted on exactly the + grounds entry 7 records for links: whether a *volume* can exclude is a + property of the format rather than of the environment, so `kal_fs_props` + answers it and a caller asks before it calls. + + ⭐ **And this one is the opposite of entry 6, which is why the two are next + to each other.** Permission was declined because the environments do not + agree that an identity exists. Every environment this specification targets + locks a byte range, and spells it almost identically. What was missing was a + word, not a capability. + + ⚠️ **What its absence cost, and it was not a refusal.** A C library above + this interface answers `fcntl(F_SETLK)`. With nothing here to answer it + with, one returned success and took no lock — **two programs held one + exclusive lock and neither could find out**. Measured against a host. This + entry exists so that "a specification with no operation for X" is not + mistaken for "consumers of it will simply not do X". + + ⭐ **Release upon the holder's end is required rather than observed**, and + that requirement is the whole reason the operation cannot be composed above + the line: a caller can build exclusion out of `KAL_OPEN_EXCLUSIVE` and a + name, and nothing then releases that name when its holder dies. +11. **A started program that does not outlive its caller.** ⚠️ **Settled in + 0.10.** `kal_process_spawn_bound`. + + Clause 7.1 declines to replace a running image, and that stands. The + consequence, which this entry did not previously record, is that a C library + asked for `execve` composes it — and the composition leaves **three** images + where a system with the operation has two: the caller, a copy that waits, + and the program. + + ⚠️ **A signal reaches the middle one.** `kal_process_terminate` upon the + identifier the caller holds terminates the waiter; measured with a host as + control, the caller is told the program died on the signal it sent while the + program runs to completion, unsupervised. The termination operation was not + at fault — it was asked to terminate one started program and did. What was + missing was a way to *say* the thing `execve` means. +12. **How many contexts run at once.** ⚠️ **Settled in 0.10.** + `kal_task_parallelism`. `KAL_TASK_PROP_PARALLEL` says *whether* and not *how + many*, and a C library above had nowhere else to look: `hardware_concurrency` + answered 1 with no error, so a program sizing a pool of workers got one + worker and no way to know. ⭐ Zero means "cannot say" and is distinct from + one, because an environment with one processor and an environment that will + not answer call for different behaviour. +13. **The time of a name, and the size of a volume.** ⚠️ **Settled in 0.10.** + `kal_fs_set_modified_at` and `kal_fs_capacity`. The first exists because + `kal_fs_set_modified` is stated on an open *file* while a directory is + opened as a `kal_dir` — so this interface had no route to a directory's time + at all, and a consumer that stamps a lock directory drove one implementation + to open the directory for *reading* and set the time on that, outside + anything stated here. A divergence caused by a missing declaration is a + defect of the specification and is recorded as one. diff --git a/SURFACE.txt b/SURFACE.txt index ad7e660..10048cb 100644 --- a/SURFACE.txt +++ b/SURFACE.txt @@ -1,4 +1,4 @@ -# The C surface of openkal 0.9, one name per line. +# The C surface of openkal 0.10, 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 @@ -50,6 +50,7 @@ kal_time_wall kal_random_fill kal_random_props # openkal.fs +kal_fs_capacity kal_fs_close_dir kal_fs_close_file kal_fs_file_info @@ -58,6 +59,7 @@ kal_fs_link_create kal_fs_link_read kal_fs_list_begin kal_fs_list_next +kal_fs_lock kal_fs_max_name kal_fs_mkdir kal_fs_open @@ -69,20 +71,24 @@ kal_fs_remove kal_fs_rename kal_fs_seek kal_fs_set_modified +kal_fs_set_modified_at kal_fs_stream kal_fs_truncate +kal_fs_unlock # openkal.process kal_process_channel kal_process_channel_close kal_process_close kal_process_props kal_process_spawn +kal_process_spawn_bound kal_process_spawn_with kal_process_terminate kal_process_wait # openkal.task kal_task_current kal_task_join +kal_task_parallelism kal_task_props kal_task_start kal_task_wait diff --git a/conformance/mcpp.toml b/conformance/mcpp.toml index ecd9abe..696400f 100644 --- a/conformance/mcpp.toml +++ b/conformance/mcpp.toml @@ -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.9.0" +openkal = "0.10.0" # The implementation under examination is not named here. # diff --git a/conformance/src/declarations.c b/conformance/src/declarations.c index b3e8c62..624d1c1 100644 --- a/conformance/src/declarations.c +++ b/conformance/src/declarations.c @@ -46,6 +46,7 @@ void okc_declarations_c(void) (void)sizeof(&kal_exec_publish); (void)sizeof(&kal_exit); (void)sizeof(&kal_free); + (void)sizeof(&kal_fs_capacity); (void)sizeof(&kal_fs_close_dir); (void)sizeof(&kal_fs_close_file); (void)sizeof(&kal_fs_file_info); @@ -54,6 +55,7 @@ void okc_declarations_c(void) (void)sizeof(&kal_fs_link_read); (void)sizeof(&kal_fs_list_begin); (void)sizeof(&kal_fs_list_next); + (void)sizeof(&kal_fs_lock); (void)sizeof(&kal_fs_max_name); (void)sizeof(&kal_fs_mkdir); (void)sizeof(&kal_fs_open); @@ -65,8 +67,10 @@ void okc_declarations_c(void) (void)sizeof(&kal_fs_rename); (void)sizeof(&kal_fs_seek); (void)sizeof(&kal_fs_set_modified); + (void)sizeof(&kal_fs_set_modified_at); (void)sizeof(&kal_fs_stream); (void)sizeof(&kal_fs_truncate); + (void)sizeof(&kal_fs_unlock); (void)sizeof(&kal_interfaces); (void)sizeof(&kal_memory_granularity); (void)sizeof(&kal_net_accept); @@ -85,6 +89,7 @@ void okc_declarations_c(void) (void)sizeof(&kal_process_close); (void)sizeof(&kal_process_props); (void)sizeof(&kal_process_spawn); + (void)sizeof(&kal_process_spawn_bound); (void)sizeof(&kal_process_spawn_with); (void)sizeof(&kal_process_terminate); (void)sizeof(&kal_process_wait); @@ -101,6 +106,7 @@ void okc_declarations_c(void) (void)sizeof(&kal_stream_write); (void)sizeof(&kal_task_current); (void)sizeof(&kal_task_join); + (void)sizeof(&kal_task_parallelism); (void)sizeof(&kal_task_props); (void)sizeof(&kal_task_start); (void)sizeof(&kal_task_wait); diff --git a/examples/portable/mcpp.toml b/examples/portable/mcpp.toml index f48fb2c..3968e93 100644 --- a/examples/portable/mcpp.toml +++ b/examples/portable/mcpp.toml @@ -9,13 +9,13 @@ name = "portable" version = "0.1.0" [dependencies] -openkal = "0.9.0" +openkal = "0.10.0" [target.'cfg(os = "linux")'.dependencies] -openkal-linux = "0.7.1" +openkal-linux = "0.9.0" [target.'cfg(os = "macos")'.dependencies] -openkal-macos = "0.6.1" +openkal-macos = "0.7.0" [target.'cfg(windows)'.dependencies] -openkal-windows = "0.4.0" +openkal-windows = "0.5.0" diff --git a/include/openkal/fs.h b/include/openkal/fs.h index 313b54e..760254b 100644 --- a/include/openkal/fs.h +++ b/include/openkal/fs.h @@ -126,6 +126,11 @@ struct kal_node_info { #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 */ +#define KAL_FS_PROP_LOCKS ((kal_uintptr)1u << 5) /* kal_fs_lock and + * kal_fs_unlock are + * answered here. 0.10 */ +#define KAL_FS_PROP_CAPACITY ((kal_uintptr)1u << 6) /* kal_fs_capacity is + * answered here. 0.10 */ /* Positions in the flags word of kal_fs_open. */ #define KAL_OPEN_READ ((kal_uintptr)1u << 0) @@ -281,6 +286,105 @@ int kal_fs_file_info(struct kal_file, kal_u32 wanted, * this. */ int kal_fs_set_modified(struct kal_file, kal_u64 modified_ns); +/* Sets the time a NAME's node reports. Version 0.10. + * + * ⚠️⚠️ A SECOND DECLARATION BECAUSE THE FIRST CANNOT REACH A DIRECTORY, AND THE + * GAP WAS FOUND BY A CONSUMER RATHER THAN HERE. + * + * The operation above is stated on an open FILE, for a reason that remains + * good. But a directory is opened by `kal_fs_open_dir', which yields a + * `kal_dir', and there is no form of the operation above that takes one --- so + * this interface had no route at all to a directory's time, while `kal_fs_info' + * reports one perfectly well. + * + * ⭐ MEASURED THREE LAYERS UP. A consumer takes a lock by making a directory and + * refreshes the lock by stamping it. Reading the stamp worked; writing it could + * not be expressed, and openkal-musl reached it by opening the directory for + * READING and setting the time on that --- which Linux and macOS perform and + * which this interface does not sanction. That divergence exists because this + * declaration did not. + * + * The name is resolved, as opening resolves. Where the name refers to a FILE the + * effect is that of the operation above; the file-taking form remains for a + * caller that holds one and does not want the name resolved again. + * + * An implementation that does not claim KAL_FS_PROP_MODIFIED_TIME for the volume + * reports kal_err_not_supported. */ +int kal_fs_set_modified_at(struct kal_dir base, const char* name, kal_uintptr len, + kal_u64 modified_ns); + +/* Excluding other holders from a range of a file. Version 0.10. + * + * ⚠️⚠️ ADDED BECAUSE ITS ABSENCE WAS SILENTLY UNSAFE ONE LAYER UP, AND THE + * ADMISSIBILITY ARGUMENT IS THE ONE THE LINK OPERATIONS BELOW ALREADY MAKE. + * + * A C library above this interface answers `fcntl(F_SETLK)'. With nothing here + * to answer it with, openkal-musl returned success and took no lock: two + * programs held one exclusive lock and neither could find out. Measured against + * the host, and it is the shape this specification exists to exclude --- an + * answer that is not true, given to a caller with no way to check it. + * + * ⭐ AND IT IS NOT THE `chmod' CASE, WHICH IS WHY IT IS HERE AND THAT IS NOT. + * A permission operation was declined because a FAT volume, a UEFI system + * partition and a Windows access-control list do not share a model. Every + * environment this specification targets locks a byte range and spells it + * almost identically. What was missing was a word, not a capability. + * + * ⚠️ Whether a VOLUME can is a property of the format rather than of the + * environment, exactly as it is for links --- a network volume may not, and a + * read-only medium need not. So it is an operation of this interface answered by + * `kal_fs_props' and not an interface of its own, which is what clause 6.2 + * requires and what makes it admissible: a caller asks first. + * + * `mode' is KAL_LOCK_SHARED or KAL_LOCK_EXCLUSIVE, optionally with KAL_LOCK_WAIT. + * Without KAL_LOCK_WAIT an operation that would block reports kal_err_again, + * which is the answer `fcntl' spells EAGAIN and the one a caller polls upon. + * + * A length of zero means "from `start' to the end, however far that comes to + * be", which is the whole-file convention every environment beneath spells the + * same way. + * + * ⭐⭐ A LOCK IS HELD BY THE FILE AND ENDS WITH IT, AND THAT IS REQUIRED RATHER + * THAN OBSERVED. It is released by `kal_fs_close_file' and by the end of the + * program that holds it, however that program ends. + * + * That requirement is the whole reason this belongs here rather than above the + * line: a caller CAN build exclusion for itself out of KAL_OPEN_EXCLUSIVE and a + * name, and nothing then releases that name when its holder dies --- so a + * program that ends abnormally while holding one locks itself out of its own + * file for ever. Release upon death is what only the environment can supply. + * + * ⚠️ AND IT IS THE FILE AND NOT THE PROGRAM, WHICH IS NARROWER THAN ONE + * ENVIRONMENT'S OLDEST FORM OF THIS. That form releases every lock a program + * holds upon a node as soon as the program closes ANY descriptor for it, so a + * library that opened the same file twice destroyed its own lock. An + * implementation shall not expose that: the holder is this `kal_file'. The + * environments that have the older form also have a newer one whose holder is + * exactly the open file, and that is the one to build upon. + * + * Two locks upon one file: an implementation may replace the earlier with the + * later where the ranges meet, which is what every environment beneath does. + * A caller that requires them distinct opens the file twice. */ +#define KAL_LOCK_SHARED ((kal_uintptr)1u << 0) +#define KAL_LOCK_EXCLUSIVE ((kal_uintptr)1u << 1) +#define KAL_LOCK_WAIT ((kal_uintptr)1u << 2) + +int kal_fs_lock(struct kal_file, kal_u64 start, kal_u64 len, kal_uintptr mode); +int kal_fs_unlock(struct kal_file, kal_u64 start, kal_u64 len); + +/* How much the volume a directory is on holds, and how much of that is free, + * in bytes. Version 0.10. + * + * An implementation that does not claim KAL_FS_PROP_CAPACITY for the volume + * reports kal_err_not_supported. One that claims it answers, and writes zero + * into a position it cannot distinguish --- which is the same convention + * `kal_task_parallelism' uses, and for the same reason: a caller must be able + * to tell "nothing available" from "not answered", and only one of the two is + * a number to act upon. + * + * Either pointer may be null, for a caller that wants one of the two. */ +int kal_fs_capacity(struct kal_dir, kal_u64* total, kal_u64* available); + /* Nodes whose content is another name. * * ⚠️ THESE ARE OPERATIONS OF THIS INTERFACE AND NOT AN INTERFACE OF THEIR OWN, diff --git a/include/openkal/process.h b/include/openkal/process.h index 3e396ae..ee39d43 100644 --- a/include/openkal/process.h +++ b/include/openkal/process.h @@ -72,6 +72,8 @@ struct kal_preopen { #define KAL_PROCESS_PROP_EXIT_STATUS ((kal_uintptr)1u << 2) #define KAL_PROCESS_PROP_CHANNEL ((kal_uintptr)1u << 3) #define KAL_PROCESS_PROP_GRANT_DIR ((kal_uintptr)1u << 4) +/* kal_process_spawn_bound is answered here. Version 0.10. */ +#define KAL_PROCESS_PROP_BOUND_LIFETIME ((kal_uintptr)1u << 5) #ifdef __cplusplus extern "C" { @@ -126,6 +128,52 @@ 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); +/* Starts a program whose lifetime is BOUND to the caller's: when the calling + * image ends, however it ends, the started program ends too. Version 0.10. + * + * ⚠️⚠️ ADDED BECAUSE A LIBRARY ABOVE THIS INTERFACE HAD TO COMPOSE SOMETHING IT + * COULD NOT THEN CONTROL, AND A CALLER WAS TOLD A FALSEHOOD ABOUT IT. + * + * Clause 7.1 declines to replace a running image, correctly. A C library asked + * for `execve' therefore composes it: start the program, wait for it, end with + * its status. That composition leaves THREE images where a system with the + * operation has two --- the caller, the copy that is waiting, and the program. + * + * ⭐ AND THE MIDDLE ONE IS THE ONE A SIGNAL REACHES. `kal_process_terminate' + * upon the identifier the caller holds terminates the WAITER. Measured, with a + * host as control: identical status words, opposite outcomes --- the caller is + * told the program died on the signal it sent, while the program runs to + * completion, unsupervised. openkal-linux#13. + * + * ⚠️ `kal_process_terminate' is not at fault: it was asked to terminate one + * started program and did. What was missing was a way to SAY the thing `execve' + * means --- this program stands in for me, so it does not outlive me. + * + * Otherwise identical to kal_process_spawn. An implementation that cannot bind a + * lifetime does not claim KAL_PROCESS_PROP_BOUND_LIFETIME and reports + * kal_err_not_supported here rather than starting a program it cannot bind: a + * caller that asked for the binding asked for it, and a program started without + * it is not the program the caller asked to start. + * + * A SECOND DECLARATION RATHER THAN A FLAG ON THE FIRST, because clause 8 forbids + * altering an existing one --- the same reason `kal_process_spawn_with' is a + * declaration and not an argument. + * + * ⚠️ WHICH MEANS THE TWO ADDITIONS DO NOT COMBINE, AND THAT IS DELIBERATE. There + * is no form that both grants directories and binds a lifetime. Declaring every + * combination is how an interface acquires four spawns and then eight, so the + * combination is declared when something needs it and not before; clause 8 + * permits adding it then. What a caller must not do meanwhile is take + * `kal_process_spawn_with' and assume the binding: it does not bind, and a + * program that outlives its caller is exactly the failure this exists to + * remove. */ +int kal_process_spawn_bound(struct kal_dir base, + const char* path, kal_uintptr path_len, + const char** argv, const kal_uintptr* argv_lens, kal_uintptr argc, + const char** envp, const kal_uintptr* envp_lens, kal_uintptr envc, + const struct kal_spawn_streams* streams, + struct kal_process* out); + kal_uintptr kal_process_props(void); #ifdef __cplusplus diff --git a/include/openkal/task.h b/include/openkal/task.h index e40b184..dc7ca6c 100644 --- a/include/openkal/task.h +++ b/include/openkal/task.h @@ -64,6 +64,24 @@ void kal_task_yield(void); * same moment; may be reused after one finishes. */ kal_uintptr kal_task_current(void); +/* How many contexts this environment can run at the same moment, or zero where + * it cannot say. Version 0.10. + * + * ⚠️⚠️ ADDED BECAUSE ITS ABSENCE WAS A WRONG ANSWER RATHER THAN A REFUSAL. + * KAL_TASK_PROP_PARALLEL says WHETHER contexts run at the same moment and not + * HOW MANY can, and a C library above has no other place to look --- so + * `sysconf(_SC_NPROCESSORS_ONLN)' fell back to 1 and + * `std::thread::hardware_concurrency()' answered 1, with no error. A program + * sizing a pool of workers got one worker and no way to know. Measured: 1 here + * against 32 on the same machine's own C library. + * + * ⭐ ZERO IS "CANNOT SAY" AND IS NOT ONE. A caller must be able to tell an + * environment that has one processor from one that will not answer, because the + * two call for different behaviour: the first is a fact to size against, and + * the second is a reason to ask the operator. An implementation that does not + * claim KAL_TASK_PROP_PARALLEL answers 1, which is the truth there. */ +kal_uintptr kal_task_parallelism(void); + /* Suspends the calling context while the word at the given address holds the * given value, until another context wakes it or the timeout elapses. The * comparison and the suspension occur without an intervening opportunity for diff --git a/include/openkal/version.h b/include/openkal/version.h index 5cefd9f..7f5ddca 100644 --- a/include/openkal/version.h +++ b/include/openkal/version.h @@ -33,7 +33,7 @@ * 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_MINOR 10u #define KAL_VERSION_PATCH 0u #define KAL_VERSION_MAKE(major, minor, patch) \ diff --git a/kit/mcpp.toml b/kit/mcpp.toml index ace8d92..432ca9c 100644 --- a/kit/mcpp.toml +++ b/kit/mcpp.toml @@ -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.9.0" +openkal = "0.10.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.9.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.7.1", features = ["standalone"] } +openkal-linux = { version = "0.9.0", features = ["standalone"] } diff --git a/mcpp.toml b/mcpp.toml index f9ff3c0..1ea4902 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,7 +1,7 @@ [package] namespace = "mcpplibs" name = "openkal" -version = "0.9.0" +version = "0.10.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/fs.cppm b/src/fs.cppm index c7422c7..68f34db 100644 --- a/src/fs.cppm +++ b/src/fs.cppm @@ -46,6 +46,10 @@ export using ::kal_fs_truncate; export using ::kal_fs_info; export using ::kal_fs_file_info; export using ::kal_fs_set_modified; +export using ::kal_fs_set_modified_at; +export using ::kal_fs_lock; +export using ::kal_fs_unlock; +export using ::kal_fs_capacity; export using ::kal_fs_mkdir; export using ::kal_fs_remove; export using ::kal_fs_rename; diff --git a/src/process.cppm b/src/process.cppm index bb76cf6..2ed0ba6 100644 --- a/src/process.cppm +++ b/src/process.cppm @@ -26,6 +26,7 @@ export using ::kal_preopen; export using ::kal_process_spawn; export using ::kal_process_spawn_with; +export using ::kal_process_spawn_bound; export using ::kal_process_channel; export using ::kal_process_channel_close; export using ::kal_process_wait; diff --git a/src/task.cppm b/src/task.cppm index a9ae903..82d6d0d 100644 --- a/src/task.cppm +++ b/src/task.cppm @@ -23,6 +23,7 @@ export using ::kal_task_start; export using ::kal_task_join; export using ::kal_task_yield; export using ::kal_task_current; +export using ::kal_task_parallelism; export using ::kal_task_wait; export using ::kal_task_wake; export using ::kal_task_props; From 46ab10e012e4f209713a3bac4e92085e9994df14 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Sun, 30 Aug 2026 16:48:56 +0800 Subject: [PATCH 2/5] The suite knows the five new positions, and observes the five new operations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ⚠️⚠️ CI CAUGHT WHAT THE ADDITION ITSELF DID NOT: a specification may add a capability position, and the suite that checks capability words carries its own list of which positions are assigned. Adding the position and not the list makes every implementation that claims it non-conforming: DID NOT HOLD [abi] the capability word contains no position the specification has not assigned Reported by openkal-macos and openkal-linux, on the release that assigned them. ⭐ The check is exactly right and the omission is mine --- it is the check that exists so an implementation cannot extend the interface rather than implement it, and it cannot tell an extension from a position the suite has not been told about. `kal::fs::locks`, `kal::fs::capacity` and `kal::process::bound_lifetime` are added to the modules, and to the three masks. --- and the operations are OBSERVED, not merely declared -------------------- ⚠️ Five declarations were added with no criterion between them. A specification that adds an operation and no way to tell whether an implementation performs it has added a name. ⭐⭐ THE LOCK OBSERVATION NEEDS NO SECOND PROGRAM, AND THAT IS THE WHOLE OF ITS DESIGN. openkal states that the holder is the open FILE. One environment's oldest form holds it by the PROCESS and releases every lock upon a node as soon as the program closes any descriptor for it --- so a library that opened one file twice destroyed its own lock. An implementation built on THAT form passes "a lock is taken" and "a lock is released" and fails on exactly one observation: a second open file of the same name is refused, not granted which this suite makes, in one program, with two `kal_file' handles. `kal_fs_capacity': what is available is no more than what is held, and a caller may ask for one of the two. `kal_fs_set_modified_at': a DIRECTORY, which is the reason the declaration exists --- and a file by the same operation, so that the new form is not a directory-only path. `kal_task_parallelism': the same answer each time, and one where contexts do not run at once. ⭐ Zero means "cannot say" and the suite does not require a number, because an implementation that will not say must be able to say so. `kal_process_spawn_bound': ⚠️ where the position is NOT claimed, the operation shall REFUSE rather than start something else. A caller that asked for a bound lifetime asked for it; a program started without the binding is not the program it asked to start, and quietly starting one is the failure the operation exists to remove. openkal-macos and openkal-windows both take this path, so it is the observation that governs two of the five implementations. --- conformance/mcpp.toml | 2 +- conformance/src/report.cpp | 2 +- conformance/src/sections/fs.cpp | 108 ++++++++++++++++++++++++++- conformance/src/sections/process.cpp | 22 +++++- conformance/src/sections/task.cpp | 11 +++ src/fs.cppm | 2 + src/process.cppm | 1 + 7 files changed, 144 insertions(+), 4 deletions(-) diff --git a/conformance/mcpp.toml b/conformance/mcpp.toml index 696400f..5fea0c9 100644 --- a/conformance/mcpp.toml +++ b/conformance/mcpp.toml @@ -1,7 +1,7 @@ [package] namespace = "mcpplibs" name = "openkal-conformance" -version = "0.7.0" +version = "0.8.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"] diff --git a/conformance/src/report.cpp b/conformance/src/report.cpp index 5b43d5a..a00d7a0 100644 --- a/conformance/src/report.cpp +++ b/conformance/src/report.cpp @@ -83,7 +83,7 @@ int failed_count() { return g_failed; } int unobserved_count() { return g_unobserved; } void write_inventory() { - line("openkal conformance suite, version 0.9.0"); + line("openkal conformance suite, version 0.10.0"); line(""); line("interface provision examined select with"); for (const auto& row : inventory) { diff --git a/conformance/src/sections/fs.cpp b/conformance/src/sections/fs.cpp index 30ffb44..8bdc39f 100644 --- a/conformance/src/sections/fs.cpp +++ b/conformance/src/sections/fs.cpp @@ -478,13 +478,119 @@ void run() { } } + // --- exclusion upon a range of a file, version 0.10 --------------------- + // + // ⭐⭐ THE OBSERVATION THAT TELLS THE TWO FORMS OF THIS APART NEEDS NO SECOND + // PROGRAM, AND THAT IS WHY IT IS WRITTEN THIS WAY. + // + // openkal states the holder as the open FILE. One environment's oldest form + // holds it by the PROCESS and releases every lock upon a node as soon as the + // program closes any descriptor for it --- so a library that opened one file + // twice destroyed its own lock. An implementation built on that form passes + // "a lock can be taken" and "it can be released" and fails only here: a + // SECOND OPEN FILE of one name, in this program, must be refused. + if (performs(kind::behaviour)) { + if ((kal_fs_props(here()) & kal::fs::locks.bits) != 0) { + put_file(kName, "xxxx"); + kal_file a{}, b{}; + const int oa = kal::fs::open_file(here(), kName, length(kName), + kal::fs::open::read | kal::fs::open::write, &a); + const int taken = oa == kal_ok + ? kal_fs_lock(a, 0, 0, KAL_LOCK_EXCLUSIVE) : oa; + observe(kind::behaviour, taken == kal_ok, + "an exclusive lock upon a whole file is taken"); + + const int ob = kal::fs::open_file(here(), kName, length(kName), + kal::fs::open::read | kal::fs::open::write, &b); + const int second = ob == kal_ok + ? kal_fs_lock(b, 0, 0, KAL_LOCK_EXCLUSIVE) : ob; + observe(kind::behaviour, second == kal_err_again, + "and a second open file of the same name is refused, not granted"); + + const int freed = taken == kal_ok ? kal_fs_unlock(a, 0, 0) : kal_err_invalid; + observe(kind::behaviour, freed == kal_ok, "the lock is released"); + const int again = ob == kal_ok + ? kal_fs_lock(b, 0, 0, KAL_LOCK_EXCLUSIVE) : ob; + observe(kind::behaviour, again == kal_ok, + "and once released, another open file may take it"); + if (again == kal_ok) kal_fs_unlock(b, 0, 0); + + // Neither kind and both kinds are the same mistake: a caller that + // asked for neither did not say what it wanted. + observe(kind::behaviour, + oa != kal_ok || kal_fs_lock(a, 0, 0, 0) == kal_err_invalid, + "asking for a lock that is neither shared nor exclusive is refused"); + + if (ob == kal_ok) kal_fs_close_file(b); + if (oa == kal_ok) kal_fs_close_file(a); + kal_fs_remove(here(), kName, length(kName)); + } else { + unobserved(kind::behaviour, + "an exclusive lock upon a whole file is taken", + "the implementation does not claim prop_locks for this volume"); + } + + // --- how much the volume holds -------------------------------------- + if ((kal_fs_props(here()) & kal::fs::capacity.bits) != 0) { + kal_u64 total = 0, available = 0; + const int e = kal_fs_capacity(here(), &total, &available); + observe(kind::behaviour, e == kal_ok && total > 0, + "the volume reports how much it holds"); + observe(kind::behaviour, e == kal_ok && available <= total, + "and what is available is no more than that"); + // Either pointer may be null, for a caller that wants one of the two. + kal_u64 one = 0; + observe(kind::behaviour, + kal_fs_capacity(here(), nullptr, &one) == kal_ok, + "and a caller may ask for one of the two"); + } else { + unobserved(kind::behaviour, "the volume reports how much it holds", + "the implementation does not claim prop_capacity for this volume"); + } + + // --- the modification time of a NAME, including a directory ---------- + // + // ⭐ THE DIRECTORY IS THE POINT. `kal_fs_set_modified' takes a `kal_file' + // and a directory is a `kal_dir', so before this declaration there was no + // route to a directory's time at all --- and an implementation reached one + // anyway, outside anything this specification stated. + if ((kal_fs_props(here()) & kal::fs::modified_time.bits) != 0) { + kal_fs_mkdir(here(), kDir, length(kDir)); + const kal_u64 chosen = 1600000000ull * 1000000000ull; + const int e = kal_fs_set_modified_at(here(), kDir, length(kDir), chosen); + kal_node_info after = fresh(); + const int read_back = + kal_fs_info(here(), kDir, length(kDir), 0, kal::fs::field::all, &after); + observe(kind::behaviour, + e == kal_ok && read_back == kal_ok + && after.modified_ns / 1000000000u == chosen / 1000000000u, + "the time a DIRECTORY reports as its last modification is set by name"); + kal_fs_remove(here(), kDir, length(kDir)); + + put_file(kName, "x"); + const int fe = kal_fs_set_modified_at(here(), kName, length(kName), chosen); + kal_node_info fa = fresh(); + const int fr = kal_fs_info(here(), kName, length(kName), 0, kal::fs::field::all, &fa); + observe(kind::behaviour, + fe == kal_ok && fr == kal_ok + && fa.modified_ns / 1000000000u == chosen / 1000000000u, + "and so is a file's, by the same operation"); + kal_fs_remove(here(), kName, length(kName)); + } else { + unobserved(kind::behaviour, + "the time a DIRECTORY reports as its last modification is set by name", + "the implementation does not claim prop_modified_time"); + } + } + 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 - | kal::fs::make_links).bits; + | kal::fs::make_links + | kal::fs::locks | kal::fs::capacity).bits; observe(kind::abi, (kal_fs_props(here()) & ~assigned) == 0, "the capability word contains no position the specification has not assigned"); diff --git a/conformance/src/sections/process.cpp b/conformance/src/sections/process.cpp index 14906ae..0f2c157 100644 --- a/conformance/src/sections/process.cpp +++ b/conformance/src/sections/process.cpp @@ -142,11 +142,31 @@ void run() { "the stream selection occupies three machine words"); const kal_uintptr assigned = (kal::process::terminate | kal::process::stream_passing | kal::process::exit_status | kal::process::channel - | kal::process::grant_dir).bits; + | kal::process::grant_dir + | kal::process::bound_lifetime).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, "the capability word contains no position the specification has not assigned"); + + // ⚠️ AN OPERATION THAT IS NOT CLAIMED SHALL REFUSE RATHER THAN PERFORM + // SOMETHING ELSE. A caller that asks for a bound lifetime asked for it; + // a program started WITHOUT the binding is not the program it asked to + // start, and an implementation that quietly starts one anyway is the + // failure the operation exists to remove. + if ((kal_process_props() & kal::process::bound_lifetime.bits) == 0) { + kal_process p{}; + const char* argv[1] = { "x" }; + const kal_uintptr lens[1] = { 1 }; + const int e = kal_process_spawn_bound(here(), "x", 1, argv, lens, 1, + nullptr, nullptr, 0, nullptr, &p); + observe(kind::behaviour, e == kal_err_not_supported, + "a lifetime this implementation cannot bind is refused, not ignored"); + } else { + unobserved(kind::behaviour, + "a lifetime this implementation cannot bind is refused, not ignored", + "the implementation claims prop_bound_lifetime"); + } } if (performs(kind::stability)) { diff --git a/conformance/src/sections/task.cpp b/conformance/src/sections/task.cpp index c21984f..143c1f1 100644 --- a/conformance/src/sections/task.cpp +++ b/conformance/src/sections/task.cpp @@ -220,6 +220,17 @@ void run() { | kal::task::thread_local_storage).bits; observe(kind::abi, (kal_task_props() & ~assigned) == 0, "the capability word contains no position the specification has not assigned"); + // ⭐ ZERO IS `CANNOT SAY' AND IS DISTINCT FROM ONE, which is the whole + // reason this enquiry exists: before it, a C library above answered 1 + // with no error and a program sizing a pool of workers got one worker. + // An implementation that will not say must say so, and not say "one". + const kal_uintptr n = kal_task_parallelism(); + observe(kind::behaviour, + (kal_task_props() & kal::task::parallel.bits) == 0 ? n == 1 : true, + "an implementation whose contexts do not run at once reports one"); + observe(kind::behaviour, kal_task_parallelism() == n, + "how many contexts can run at once is the same answer each time"); + observe(kind::abi, kal_task_current() == kal_task_current(), "the identity of the calling context is stable within it"); diff --git a/src/fs.cppm b/src/fs.cppm index 68f34db..38215b9 100644 --- a/src/fs.cppm +++ b/src/fs.cppm @@ -103,6 +103,8 @@ 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}; +inline constexpr props locks {KAL_FS_PROP_LOCKS}; +inline constexpr props capacity {KAL_FS_PROP_CAPACITY}; enum : int { seek_set = KAL_SEEK_SET, seek_current = KAL_SEEK_CURRENT, seek_end = KAL_SEEK_END }; diff --git a/src/process.cppm b/src/process.cppm index 2ed0ba6..2ed3666 100644 --- a/src/process.cppm +++ b/src/process.cppm @@ -51,6 +51,7 @@ inline constexpr props stream_passing{KAL_PROCESS_PROP_STREAM_PASSING}; 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 constexpr props bound_lifetime{KAL_PROCESS_PROP_BOUND_LIFETIME}; inline props properties() { return props{kal_process_props()}; } inline bool has(props p) { return properties().has(p); } From e8fab03233e1310caebf7930ed8d9cc1e7435733 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Sun, 30 Aug 2026 16:53:45 +0800 Subject: [PATCH 3/5] The bound-lifetime observation used a helper its own section does not have MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `here()' is defined in the fs section. The process section has its own way to name the working directory --- `kal::fs::working()' --- and the observation I added called the other one, so the suite did not compile at all. ⚠️ AND IT COMPILED HERE, which is why it reached CI. A local build of this package links against no implementation, so it stops at the link and never reports what the compile of one section said. The build that catches this is the one an implementation runs, and openkal-linux ran it on both compilers within minutes of the push. --- conformance/src/sections/process.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conformance/src/sections/process.cpp b/conformance/src/sections/process.cpp index 0f2c157..4694997 100644 --- a/conformance/src/sections/process.cpp +++ b/conformance/src/sections/process.cpp @@ -158,7 +158,7 @@ void run() { kal_process p{}; const char* argv[1] = { "x" }; const kal_uintptr lens[1] = { 1 }; - const int e = kal_process_spawn_bound(here(), "x", 1, argv, lens, 1, + const int e = kal_process_spawn_bound(kal::fs::working(), "x", 1, argv, lens, 1, nullptr, nullptr, 0, nullptr, &p); observe(kind::behaviour, e == kal_err_not_supported, "a lifetime this implementation cannot bind is refused, not ignored"); From bc9c625642b2289f4e821305e3c75d322efdd3b7 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Sun, 30 Aug 2026 17:03:49 +0800 Subject: [PATCH 4/5] A #define does not cross a module boundary, so the lock modes are exported MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ⚠️⚠️ THE SPECIFICATION HAS TWO SURFACES AND I ADDED TO ONE OF THEM. `KAL_LOCK_SHARED', `KAL_LOCK_EXCLUSIVE' and `KAL_LOCK_WAIT' are macros in `fs.h'. A consumer that writes `import openkal.fs' cannot see a macro --- so those three positions were nameable by half the consumers of this specification and not by the other half. error: use of undeclared identifier 'KAL_LOCK_EXCLUSIVE' ⭐ REPORTED BY THE SPECIFICATION'S OWN CONFORMANCE SUITE, WHICH IS ONE OF THE CONSUMERS THAT CANNOT SEE MACROS. The suite imports the modules, so it is exactly the reader that finds this --- and it found it on the release that added them. They join `open_flags' in a kind of their own, for the reason that one has: an intent and a capability word are different things, and a word that serves as both can be passed to the wrong operation. `kal::fs::lock_range' goes with them, as `kal::fs::open_file' goes with `open_flags'. ⚠️ The rule this leaves behind: a position added to the C header is not added until it is in the module too. The property words got both because they were added as a pair; these did not. --- conformance/src/sections/fs.cpp | 6 +++--- src/fs.cppm | 24 ++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/conformance/src/sections/fs.cpp b/conformance/src/sections/fs.cpp index 8bdc39f..fad1e21 100644 --- a/conformance/src/sections/fs.cpp +++ b/conformance/src/sections/fs.cpp @@ -496,21 +496,21 @@ void run() { const int oa = kal::fs::open_file(here(), kName, length(kName), kal::fs::open::read | kal::fs::open::write, &a); const int taken = oa == kal_ok - ? kal_fs_lock(a, 0, 0, KAL_LOCK_EXCLUSIVE) : oa; + ? kal::fs::lock_range(a, 0, 0, kal::fs::lock::exclusive) : oa; observe(kind::behaviour, taken == kal_ok, "an exclusive lock upon a whole file is taken"); const int ob = kal::fs::open_file(here(), kName, length(kName), kal::fs::open::read | kal::fs::open::write, &b); const int second = ob == kal_ok - ? kal_fs_lock(b, 0, 0, KAL_LOCK_EXCLUSIVE) : ob; + ? kal::fs::lock_range(b, 0, 0, kal::fs::lock::exclusive) : ob; observe(kind::behaviour, second == kal_err_again, "and a second open file of the same name is refused, not granted"); const int freed = taken == kal_ok ? kal_fs_unlock(a, 0, 0) : kal_err_invalid; observe(kind::behaviour, freed == kal_ok, "the lock is released"); const int again = ob == kal_ok - ? kal_fs_lock(b, 0, 0, KAL_LOCK_EXCLUSIVE) : ob; + ? kal::fs::lock_range(b, 0, 0, kal::fs::lock::exclusive) : ob; observe(kind::behaviour, again == kal_ok, "and once released, another open file may take it"); if (again == kal_ok) kal_fs_unlock(b, 0, 0); diff --git a/src/fs.cppm b/src/fs.cppm index 38215b9..ec0e914 100644 --- a/src/fs.cppm +++ b/src/fs.cppm @@ -123,6 +123,30 @@ inline constexpr open_flags truncate {KAL_OPEN_TRUNCATE}; inline constexpr open_flags append {KAL_OPEN_APPEND}; } +// The mode of kal_fs_lock, in its own kind for the reason `open_flags' is in +// one: an intent and a capability word are different things, and a word that +// serves as both can be passed to the wrong operation. +// +// ⚠️⚠️ AND IT IS HERE BECAUSE A `#define' DOES NOT CROSS A MODULE BOUNDARY. +// The header's macros are invisible to a consumer that writes `import +// openkal.fs', so a position added to the C header and not to this file is a +// position half the consumers cannot name. The specification's own conformance +// suite is such a consumer, and it is what reported the omission: +// +// error: use of undeclared identifier 'KAL_LOCK_EXCLUSIVE' +struct lock_tag; +using lock_mode = kal::props; + +namespace lock { +inline constexpr lock_mode shared {KAL_LOCK_SHARED}; +inline constexpr lock_mode exclusive{KAL_LOCK_EXCLUSIVE}; +inline constexpr lock_mode wait {KAL_LOCK_WAIT}; +} + +inline int lock_range(file f, kal_u64 start, kal_u64 len, lock_mode mode) { + return kal_fs_lock(f, start, len, mode.bits); +} + inline int open_file(dir base, const char* name, kal_uintptr len, open_flags flags, file* out) { return kal_fs_open(base, name, len, flags.bits, out); From 910e95ef4be444a24725e25e9296c1b0d9df6c59 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Sun, 30 Aug 2026 18:24:18 +0800 Subject: [PATCH 5/5] Give the second half of entry 13 the reason the first half already had The entry names `kal_fs_set_modified_at' and `kal_fs_capacity' and justified only the first. Found on a last read of the diff before merging: an entry that names two additions and argues for one is how an addition arrives unargued. Says why capacity belongs to `openkal.fs' and not `openkal.space' --- room on a volume is a property of the names a caller can already reach --- what its absence cost, which was a fixed number reported to a program checking for room, and why it answers bytes rather than the block count and block size every environment states in units of its own. --- SPEC.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/SPEC.md b/SPEC.md index 806ab67..4eee725 100644 --- a/SPEC.md +++ b/SPEC.md @@ -1086,3 +1086,19 @@ The following are recorded so that they are not mistaken for oversights. to open the directory for *reading* and set the time on that, outside anything stated here. A divergence caused by a missing declaration is a defect of the specification and is recorded as one. + + The second, `kal_fs_capacity`, exists for the reason that keeps it in this + interface rather than in `openkal.space`: how much room a volume has is a + property of the *names* a caller can already reach, not of the memory a + program runs in. A C library above answers `statvfs`, and with nothing here + to answer it with it reported a fixed number — ⚠️ **which is worse than + refusing, because a program that checks for room before writing was told + there was room.** + + ⭐ It answers bytes and not blocks, and that is the whole of the design + decision. Every environment this specification targets states a block count + and a block size, in units of its own choosing, and every one of them + differs; a caller wanting bytes multiplies two numbers whose meaning it must + first look up. Bytes are what the caller is deciding about, so bytes are + what this returns and the multiplication happens once, in the + implementation, where the units are known.