From 0af54f80fde1ee500ed9a75afd4ecdb217e5af92 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Sun, 25 Oct 2026 05:29:16 +0900 Subject: [PATCH 1/4] 0.16.0 (draft) --- a least stack size, whether input is altered, and what closing a directory ends kal_task_start_sized starts a context whose stack is at least the size asked for; a request beyond what the environment can give is kal_err_no_memory, the outcome kal_alloc reports when it is exhausted (clause 3.1). KAL_TERM_PASS_INPUT states that what is typed reaches the program as the keystrokes produced it. KAL_TERM_PASS_CONTROL keeps its meaning, and its paragraph no longer claims the bytes arrive unaltered. fs.h states what closing a directory does to its enumerations, and that kal_fs_list_next refuses a closed directory. Clause 11 entries 22 to 24, and a conformance observation of the least size. --- README.md | 2 +- SPEC.md | 73 ++++++++++++++++++++++++++++++- SURFACE.txt | 3 +- conformance/src/declarations.c | 1 + conformance/src/sections/task.cpp | 64 +++++++++++++++++++++++++++ include/openkal/fs.h | 8 ++++ include/openkal/task.h | 40 +++++++++++++++-- include/openkal/terminal.h | 51 +++++++++++++++++++-- include/openkal/version.h | 2 +- mcpp.toml | 2 +- src/macros.cppm | 1 + src/task.cppm | 1 + src/terminal.cppm | 4 ++ 13 files changed, 240 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 43b927c..0ee574f 100644 --- a/README.md +++ b/README.md @@ -77,7 +77,7 @@ conditional on the target. ```toml [dependencies] -openkal = "0.15.0" +openkal = "0.16.0" [target.'cfg(os = "linux")'.dependencies] openkal-linux = "0.15.1" diff --git a/SPEC.md b/SPEC.md index f224dd9..1f8221e 100644 --- a/SPEC.md +++ b/SPEC.md @@ -1,4 +1,4 @@ -# openkal Specification, version 0.15 +# openkal Specification, version 0.16 ## 1. Scope @@ -1410,3 +1410,74 @@ The following are recorded so that they are not mistaken for oversights. composed from a record the implementation kept about contexts it does not control — is the shape clause 7.1 excludes, and the defect this entry follows was a wrong range rather than a missing one. +22. **A least size for a started context's stack.** **Settled in 0.16.** + `kal_task_start_sized` starts a context whose stack is at least the size + asked for. Version 0.15 said the size was "a property rather than a + parameter" and offered no enquiry that reported it, so it was neither: a + C library above accepted `pthread_attr_setstacksize`, ran every thread on + the implementation's own stack whatever was asked, and could not say how + large that was (`mcpplibs/openkal-musl#48`). The reason given — that an + environment which does not allocate stacks separately cannot honour a + request — named no environment, and every environment `openkal.task` is + implemented on takes a size natively: two of them as an argument of the + call that starts a thread, and the third because the implementation maps + the region itself. + + **A least size, and a request beyond what the environment can give is + `kal_err_no_memory`.** Clause 3.1 is why this is a supply: a stack is + memory, and an environment that bounds its size bounds what is available, + which is the outcome `kal_alloc` reports when it is exhausted. A position + in `kal_task_props` saying whether requests are honoured was considered + and not adopted: it would have admitted an operation that a provided + interface answers with a refusal, which clause 6.1 treats as a defect, and + there is no environment it would have described. An exact size was + considered and not adopted, because the C library above asks for its own + default on every thread and that default is smaller than what the + implementations beneath it provide. + + **There is no form taking a region**, for the reason the declaration + gives, and the C library above refuses a thread whose attributes name a + region rather than run it somewhere else and report success. + +23. **Whether input is altered on its way.** **Settled in 0.16.** + `KAL_TERM_PASS_INPUT` states that what is typed reaches the program as the + keystrokes produced it. Without it the return key reached a program in raw + mode as a line feed in two of the three environments, the same byte + another key produces, and a program that inserts a line break on one key + and submits on the other could not be written (`mcpplibs/openkal#48`, + measured against a host C library on one terminal). + + **It is a position of its own and not a widening of + `KAL_TERM_PASS_CONTROL`.** That position was assigned in 0.14 and retains + its meaning (clause 6.2): implementations released before this one report + it set whenever no keystroke is reserved, and a reading that included the + alteration of input would have had each of them claim something about it + that it had never established. The two questions are also independent — + whether a keystroke is kept, and whether one that is delivered is + rewritten — and a program performing line editing of its own wants the + second without the first. + + **It is not entry 20's declined finer position.** That entry declined a + position for each class of *reserved* keystroke, because one environment + governs the classes with a single switch. This position is a different + dimension, and it is spelled at the same granularity entry 20 chose: one + position covers every mechanism by which the environment alters input. + + **An environment that alters input only while it assembles lines** has no + separate switch, and reports the position as it stands rather than + composing one. The program asking is told by `kal_terminal_get_mode`, and + the C library above, which knows what it asked for, performs the + alteration itself where the environment would not have. + +24. **What closing a directory does to its enumerations.** **Stated in + 0.16.** `openkal.fs` said from its first version that closing a directory + releases the iterators it produced, and no observation reached the case. + One implementation released them only at the end, so a C library above + that stopped reading a directory early kept a descriptor for every + directory it closed, until a program that polled a directory ran out of + them (`mcpplibs/openkal-linux#34`). The declaration now states the + consequence for a caller as well: the directory is a released handle, and + `kal_fs_list_next` given it refuses rather than using an iterator the close + has released. Each implementation observes the release in its own tests, + because a count of open handles is not portable — clause 7.13 observes its + first paragraph the same way, for the same reason. diff --git a/SURFACE.txt b/SURFACE.txt index add07f4..2fb6e16 100644 --- a/SURFACE.txt +++ b/SURFACE.txt @@ -1,4 +1,4 @@ -# The C surface of openkal 0.15, one name per line. +# The C surface of openkal 0.16, 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 @@ -95,6 +95,7 @@ kal_task_parallelism kal_task_props kal_task_stack kal_task_start +kal_task_start_sized kal_task_wait kal_task_wake kal_task_yield diff --git a/conformance/src/declarations.c b/conformance/src/declarations.c index f7ce12e..305aae3 100644 --- a/conformance/src/declarations.c +++ b/conformance/src/declarations.c @@ -113,6 +113,7 @@ void okc_declarations_c(void) (void)sizeof(&kal_task_props); (void)sizeof(&kal_task_stack); (void)sizeof(&kal_task_start); + (void)sizeof(&kal_task_start_sized); (void)sizeof(&kal_task_wait); (void)sizeof(&kal_task_wake); (void)sizeof(&kal_task_yield); diff --git a/conformance/src/sections/task.cpp b/conformance/src/sections/task.cpp index a474a2f..8557741 100644 --- a/conformance/src/sections/task.cpp +++ b/conformance/src/sections/task.cpp @@ -129,6 +129,42 @@ void asks_about_itself(void*) { e_again == kal_ok && base_again == base && size_again == size); } +volatile int g_sized_holds = -1; +volatile int g_sized_room = -1; +volatile kal_uintptr g_sized_size = 0; + +// A recursion whose frames are touched one after another, so that an +// environment which grows a stack a page at a time is given every page in +// order. It is not a tail call, and it is reached through a pointer the compiler +// cannot see through, so neither inlining nor the elimination of the frames +// removes what it measures. +long recurse(long n); +long (*volatile g_recurse)(long) = recurse; +long recurse(long n) { + volatile char pad[1000]; + pad[0] = static_cast(n); + if (n == 0) return 0; + return g_recurse(n - 1) + pad[0]; +} + +constexpr kal_uintptr kSizedAsk = 4u * 1024u * 1024u; + +// THE PROPERTY A CALLER RELIES ON IS ROOM, AND THE SIZE REPORTED IS HOW IT IS +// NAMED. So both are observed: the region the context is told of is at least +// what was asked for and contains the context, and a recursion of three +// quarters of the request completes. +void asks_for_room(void*) { + char here = 0; + const kal_uintptr at = reinterpret_cast(&here); + void* base = nullptr; + kal_uintptr size = 0; + const int e = kal_task_stack(&base, &size); + g_sized_size = size; + store_release(&g_sized_holds, holds(at, e, base, size) && size >= kSizedAsk); + g_recurse(static_cast(kSizedAsk / 4 * 3 / 1024)); + store_release(&g_sized_room, 1); +} + #endif } // namespace @@ -218,6 +254,34 @@ void run() { unobserved(kind::behaviour, "a started context's stack bounds contain that context", "a context could not be started"); } + + // A LEAST SIZE, version 0.16. A request the environment cannot meet is + // reported as exhaustion, which is an answer and not a defect, so it is + // recorded as not observed rather than as a failure. + kal_task sized{}; + const int es = kal_task_start_sized(asks_for_room, nullptr, kSizedAsk, &sized); + if (es == kal_ok) { + kal_task_join(sized); + observe(kind::behaviour, load_acquire(&g_sized_holds) == 1, + "a context started with a least stack size is told at least that size"); + observe(kind::behaviour, load_acquire(&g_sized_room) == 1, + "a context started with a least stack size has that room"); + } else if (es == kal_err_no_memory) { + unobserved(kind::behaviour, + "a context started with a least stack size is told at least that size", + "the environment cannot provide a stack of four mebibytes"); + } else { + observe(kind::behaviour, false, + "a least stack size is met or reported as exhaustion"); + } + kal_task plain{}; + if (kal_task_start_sized(does_nothing, nullptr, 0, &plain) == kal_ok) { + observe(kind::behaviour, kal_task_join(plain) == kal_ok, + "a least stack size of zero starts a context as kal_task_start does"); + } else { + observe(kind::behaviour, false, + "a least stack size of zero starts a context as kal_task_start does"); + } } // The construction the interface exists to support. The count is exact: diff --git a/include/openkal/fs.h b/include/openkal/fs.h index 038c123..e8ef932 100644 --- a/include/openkal/fs.h +++ b/include/openkal/fs.h @@ -498,6 +498,14 @@ kal_intptr kal_fs_link_read (struct kal_dir base, const char* name, kal_uintptr * by closing the directory that produced it. The end is reported by the * iterator becoming zero. * + * AN ITERATOR IS PASSED WITH THE DIRECTORY THAT PRODUCED IT, and a closed + * directory is a released handle (clause 6.7): kal_fs_list_next given one + * reports kal_err_invalid and does not use the iterator, which closing the + * directory has released. An enumeration abandoned before its end is therefore + * ended by the close, and holds nothing afterwards --- measured before version + * 0.16 stated it, one implementation kept a descriptor for every directory a C + * library above it had stopped reading early, until the program ran out. + * * 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 diff --git a/include/openkal/task.h b/include/openkal/task.h index f1ea5f6..133caa4 100644 --- a/include/openkal/task.h +++ b/include/openkal/task.h @@ -46,13 +46,45 @@ extern "C" { * 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 + * The stack is provided by the implementation, and here its size is the + * implementation's too; `kal_task_start_sized' states a least size. Where the + * stack is remains the implementation's in both forms. 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); +/* Starts a context whose stack is at least `stack_size' bytes. Version 0.16. + * + * Everything kal_task_start states holds, and in addition the started context, + * asking kal_task_stack, is told a size of at least `stack_size'. An + * implementation may provide more --- a size rounded to its granularity, or a + * least size of its own below which it does not go --- and zero asks for + * nothing beyond what kal_task_start provides. + * + * A LEAST SIZE AND NOT A SIZE, because a program that states one states what it + * needs: a recursion that must not fault, a run-time that keeps frames of its + * own. An implementation that rounded down would fault the program that asked, + * and one that is required to honour a smaller request than its own default has + * been required to do something no caller depends upon. The C library this + * specification is implemented beneath asks for its own default on every + * thread, and that default is smaller than what the implementations beneath it + * provide; under an exact size every thread would have lost room by asking. + * + * A REQUEST THE ENVIRONMENT CANNOT MEET IS kal_err_no_memory. A stack is + * memory, and an environment that bounds how large one may be bounds what is + * available, which clause 3.1 calls a supply: the outcome is the one kal_alloc + * reports when it is exhausted. It is not kal_err_not_supported. Every + * environment that starts a context gives it a stack, so the operation is not + * one some environments lack (clause 6.1), and an environment whose stacks are + * of one size meets every request up to that size. + * + * THERE IS NO FORM THAT TAKES A REGION. An environment that allocates a + * context's stack itself cannot run the context on a region it did not + * allocate, and the interface would then contain an operation some + * environments can never perform (clause 6.4). */ +int kal_task_start_sized(void (*entry)(void*), void* arg, kal_uintptr stack_size, + struct kal_task* out); + /* Waits for a context to finish. A context is waited for at most once. */ int kal_task_join(struct kal_task); diff --git a/include/openkal/terminal.h b/include/openkal/terminal.h index bc24baf..e3513cb 100644 --- a/include/openkal/terminal.h +++ b/include/openkal/terminal.h @@ -35,11 +35,16 @@ #define KAL_TERM_LINE_EDIT ((kal_uintptr)1u << 0) /* the environment assembles lines */ #define KAL_TERM_ECHO ((kal_uintptr)1u << 1) /* the environment shows what is typed */ #define KAL_TERM_PASS_CONTROL ((kal_uintptr)1u << 2) /* the environment reserves no keystroke */ +#define KAL_TERM_PASS_INPUT ((kal_uintptr)1u << 3) /* the environment alters nothing typed */ /* KAL_TERM_PASS_CONTROL IS A GUARANTEE IN ONE DIRECTION AND A PERMISSION IN THE - * OTHER. Set, every keystroke reaches the program as the bytes it produces, - * including the ones an environment ordinarily keeps for itself. Clear, the - * environment may reserve an agreed set of keystrokes for actions of its own. + * OTHER. Set, every keystroke reaches the program, including the ones an + * environment ordinarily keeps for itself. Clear, the environment may reserve + * an agreed set of keystrokes for actions of its own. Whether the bytes a + * keystroke produces are altered on their way is a different question, and + * KAL_TERM_PASS_INPUT answers it: up to version 0.15 this paragraph said that a + * keystroke arrived "as the bytes it produces", which no environment that + * rewrites the return key had done. * Which keystrokes those are belongs to the environment: an interface that * required every environment to reserve the same ones would require of all of * them what one of them happens to spell, which is the shape clause 7.1 @@ -68,6 +73,46 @@ * require of an environment whose single switch governs them together a * distinction it does not have --- the defect clause 6.4 describes. */ +/* KAL_TERM_PASS_INPUT, VERSION 0.16. Set, what is typed reaches the program as + * the bytes the keystrokes produced: the return key is a carriage return + * (0x0d), and is not rewritten into the line feed (0x0a) another key produces. + * Clear, the environment may alter input on its way, and which alterations it + * makes are its own. + * + * A PROGRAM THAT TELLS KEYS APART CANNOT BE WRITTEN WITHOUT IT. An environment + * that rewrites the return key into a line feed delivers the return key and the + * key that produces a line feed as the same byte, and nothing above this + * interface can separate them again once the rewrite has happened. A program + * that inserts a line break on one and submits on the other is the ordinary + * case, not a contrived one. + * + * IT IS NOT KAL_TERM_PASS_CONTROL. That position says whether the environment + * KEEPS a keystroke; this one says whether it REWRITES one it delivers. The two + * are independent in every environment that has both, a program that reads + * keystrokes for line editing of its own wants the second without the first, + * and folding them into one position would have changed what the first had + * already said about every implementation released before this one (clause 8). + * + * THE POSITION COVERS EVERY MECHANISM BY WHICH THE ENVIRONMENT ALTERS INPUT, for + * the reason given for KAL_TERM_PASS_CONTROL: rewriting a carriage return or a + * line feed into the other, discarding a carriage return, and clearing the high + * bit of every byte, where the environment has them. It is read as set only + * where none is in effect. Clearing it restores the alteration the environment + * ordinarily applies. + * + * IT IS INDEPENDENT OF LINE ASSEMBLY, AND THE CONSEQUENCE IS STATED HERE. An + * environment that assembles lines ends one at a line feed, so a program that + * sets this position and keeps KAL_TERM_LINE_EDIT may find that the return key + * no longer ends a line. That is what the same request does in the environments + * that offer it natively, and it is what the C library above them maps to. + * + * AN ENVIRONMENT THAT CANNOT SEPARATE IT FROM ANOTHER POSITION reports it as it + * stands and leaves it there. One environment alters input only while it + * assembles lines and has no switch of its own for the alteration: it reports + * this position set whenever line assembly is off, and a request it cannot + * establish independently leaves the position as it is. kal_terminal_get_mode is + * how a caller learns which happened, which is what the pair exists for. */ + /* Positions in the result of kal_terminal_props. */ #define KAL_TERM_PROP_MODE ((kal_uintptr)1u << 0) /* get_mode/set_mode are answered */ #define KAL_TERM_PROP_SIZE ((kal_uintptr)1u << 1) /* the display size is known */ diff --git a/include/openkal/version.h b/include/openkal/version.h index 54f5b22..03cf123 100644 --- a/include/openkal/version.h +++ b/include/openkal/version.h @@ -42,7 +42,7 @@ * for the reason tools/check-readme-versions.sh exists --- the one fact a * consumer actually asks for was checked by nobody. */ #define KAL_VERSION_MAJOR 0u -#define KAL_VERSION_MINOR 15u +#define KAL_VERSION_MINOR 16u #define KAL_VERSION_PATCH 0u #define KAL_VERSION_MAKE(major, minor, patch) \ diff --git a/mcpp.toml b/mcpp.toml index 0269b96..0b700e4 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,7 +1,7 @@ [package] namespace = "mcpplibs" name = "openkal" -version = "0.15.0" +version = "0.16.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/macros.cppm b/src/macros.cppm index e8338ca..a967ff1 100644 --- a/src/macros.cppm +++ b/src/macros.cppm @@ -105,6 +105,7 @@ inline constexpr kal_uintptr KAL_TASK_PROP_WAIT_TIMEOUT_M = (kal_uintptr)( inline constexpr kal_uintptr KAL_TERM_ECHO_M = (kal_uintptr)(KAL_TERM_ECHO); inline constexpr kal_uintptr KAL_TERM_LINE_EDIT_M = (kal_uintptr)(KAL_TERM_LINE_EDIT); inline constexpr kal_uintptr KAL_TERM_PASS_CONTROL_M = (kal_uintptr)(KAL_TERM_PASS_CONTROL); +inline constexpr kal_uintptr KAL_TERM_PASS_INPUT_M = (kal_uintptr)(KAL_TERM_PASS_INPUT); inline constexpr kal_uintptr KAL_TERM_PROP_MODE_M = (kal_uintptr)(KAL_TERM_PROP_MODE); inline constexpr kal_uintptr KAL_TERM_PROP_SIZE_M = (kal_uintptr)(KAL_TERM_PROP_SIZE); inline constexpr kal_uintptr KAL_TIME_PROP_MONOTONIC_SUSPENDS_M = (kal_uintptr)(KAL_TIME_PROP_MONOTONIC_SUSPENDS); diff --git a/src/task.cppm b/src/task.cppm index 55049c5..73df667 100644 --- a/src/task.cppm +++ b/src/task.cppm @@ -20,6 +20,7 @@ export import openkal.types; export using ::kal_task; export using ::kal_task_start; +export using ::kal_task_start_sized; export using ::kal_task_join; export using ::kal_task_yield; export using ::kal_task_current; diff --git a/src/terminal.cppm b/src/terminal.cppm index 6ab6991..a50ed2f 100644 --- a/src/terminal.cppm +++ b/src/terminal.cppm @@ -39,6 +39,10 @@ inline constexpr mode echo {KAL_TERM_ECHO}; // its own choosing. The position is spelled so that zero is the weaker claim, // which is what lets an implementation that predates it answer honestly. inline constexpr mode pass_control{KAL_TERM_PASS_CONTROL}; +// Set, what is typed arrives as the keystrokes produced it: the return key is a +// carriage return and is not rewritten into a line feed. Independent of +// pass_control, which says whether a keystroke is kept rather than altered. +inline constexpr mode pass_input {KAL_TERM_PASS_INPUT}; struct props_tag; using props = kal::props; From 570854120a90a27b67bc075724a844f9692a8e14 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Sun, 25 Oct 2026 06:39:57 +0900 Subject: [PATCH 2/4] The position's text states what was measured on the third environment Its console consumes Ctrl+J in raw mode unless its own switch for delivering what a key produces is on, and its line assembly alters input whatever it is asked; with the position set, all three deliver 0x0d and 0x0a. --- SPEC.md | 22 ++++++++++++++-------- include/openkal/terminal.h | 15 +++++++++------ 2 files changed, 23 insertions(+), 14 deletions(-) diff --git a/SPEC.md b/SPEC.md index 1f8221e..97a06a0 100644 --- a/SPEC.md +++ b/SPEC.md @@ -1443,9 +1443,12 @@ The following are recorded so that they are not mistaken for oversights. `KAL_TERM_PASS_INPUT` states that what is typed reaches the program as the keystrokes produced it. Without it the return key reached a program in raw mode as a line feed in two of the three environments, the same byte - another key produces, and a program that inserts a line break on one key - and submits on the other could not be written (`mcpplibs/openkal#48`, - measured against a host C library on one terminal). + another key produces, and in the third Ctrl+J produced no byte at all; a + program that inserts a line break on one key and submits on the other + could not be written (`mcpplibs/openkal#48`, measured against a host C + library on one terminal, and on the third environment's console with the + keys written into it as key events). With the position set, all three + deliver the return key as 0x0d and Ctrl+J as 0x0a. **It is a position of its own and not a widening of `KAL_TERM_PASS_CONTROL`.** That position was assigned in 0.14 and retains @@ -1463,11 +1466,14 @@ The following are recorded so that they are not mistaken for oversights. dimension, and it is spelled at the same granularity entry 20 chose: one position covers every mechanism by which the environment alters input. - **An environment that alters input only while it assembles lines** has no - separate switch, and reports the position as it stands rather than - composing one. The program asking is told by `kal_terminal_get_mode`, and - the C library above, which knows what it asked for, performs the - alteration itself where the environment would not have. + **An environment whose line assembly alters input whatever it is asked** + reports the position clear while it assembles lines, and a request leaves + it so; the program asking is told by `kal_terminal_get_mode`. The + alterations an environment applies when the position is clear are its own + and differ between the three, so the C library above, which knows what its + caller asked for, asks for unaltered input in raw mode and performs the + rewriting its caller asked for itself. That is how a caller of the C + library reads the same bytes on every environment. 24. **What closing a directory does to its enumerations.** **Stated in 0.16.** `openkal.fs` said from its first version that closing a directory diff --git a/include/openkal/terminal.h b/include/openkal/terminal.h index e3513cb..2e8d73e 100644 --- a/include/openkal/terminal.h +++ b/include/openkal/terminal.h @@ -106,12 +106,15 @@ * no longer ends a line. That is what the same request does in the environments * that offer it natively, and it is what the C library above them maps to. * - * AN ENVIRONMENT THAT CANNOT SEPARATE IT FROM ANOTHER POSITION reports it as it - * stands and leaves it there. One environment alters input only while it - * assembles lines and has no switch of its own for the alteration: it reports - * this position set whenever line assembly is off, and a request it cannot - * establish independently leaves the position as it is. kal_terminal_get_mode is - * how a caller learns which happened, which is what the pair exists for. */ + * AN ENVIRONMENT WHOSE LINE ASSEMBLY ALTERS INPUT WHATEVER IT IS ASKED reports + * the position clear while it assembles lines, and a request for it there + * leaves it clear. One environment does: assembling lines, it ends one at the + * return key as a carriage return and a line feed, and no switch it has changes + * that. With line assembly off its own switch for delivering what a key + * produces establishes the position, and without that switch it consumes + * Ctrl+J and delivers no byte --- measured, and the reason the position reads + * clear there. kal_terminal_get_mode is how a caller learns which happened, + * which is what the pair exists for. */ /* Positions in the result of kal_terminal_props. */ #define KAL_TERM_PROP_MODE ((kal_uintptr)1u << 0) /* get_mode/set_mode are answered */ From 425f209b6008ea13d76579abbca75d085b9f092e Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Sun, 25 Oct 2026 06:46:16 +0900 Subject: [PATCH 3/4] The design of the round, second edition, as measured; and PASS_CONTROL's paragraph reads in its original order --- ...10-25-three-reports-optimisation-design.md | 179 ++++++++++++++++++ include/openkal/terminal.h | 9 +- 2 files changed, 183 insertions(+), 5 deletions(-) create mode 100644 .agents/docs/2026-10-25-three-reports-optimisation-design.md diff --git a/.agents/docs/2026-10-25-three-reports-optimisation-design.md b/.agents/docs/2026-10-25-three-reports-optimisation-design.md new file mode 100644 index 0000000..03811b7 --- /dev/null +++ b/.agents/docs/2026-10-25-three-reports-optimisation-design.md @@ -0,0 +1,179 @@ +# 三份报告的综合方案:枚举的释放、线程栈的大小、输入的改写 + +第二版,2026-10-25。状态:**实施所依据的方案**。第一版的结论经实测修正三处(§4), +本版取代第一版。规范对照基于 `SPEC.md` 0.15,结果为 0.16。 + +| 报告 | 性质 | 落点 | +| --- | --- | --- | +| openkal-linux#34(及 PR #35) | 关闭目录不结束其未读完的枚举,描述符泄漏 | 规范条文 + 五个实现 | +| openkal-musl#48 | 线程栈大小被忽略;未用的栈被全部提交 | 规范新增操作 + 三个实现 + C 库 | +| openkal#48 | raw 模式下回车与 Ctrl+J 不可区分 | 规范新增位置 + 四个实现 + C 库 | + +--- + +## 1. 结论 + +1. **`kal_task_start_sized`**:以下限表达栈大小;环境给不出时报告 `kal_err_no_memory`。 +2. **`KAL_TERM_PASS_INPUT`(bit 3)**:环境不改写所输入的字节。它与 `KAL_TERM_PASS_CONTROL` 相互独立。 +3. **fs.h 补述**:关闭目录即释放其迭代器,`kal_fs_list_next` 拒绝已关闭的目录。 +4. 实现层另修三处报告未覆盖的缺陷: + - Linux 线程栈下方无保护页; + - musl 为不使用的栈映射并逐字节清零; + - 系统调用分派函数在 -O0 下栈帧为 69,008 字节。 + +规范只新增声明,不改既有声明(条款 8);旧实现对新位置读作零,所言为真(条款 6.2)。 + +--- + +## 2. 仓库、版本与依赖 + +| 仓库 | 版本 | 内容 | +| --- | --- | --- | +| openkal | 0.15.0 → 0.16.0 | 两项声明、fs.h 补述、条款 11 第 22–24 条、conformance 观测 | +| openkal-linux | 0.16.1 → 0.17.0 | PR #35(保留原作者)、枚举安全释放、保护页、8 MiB、sized、PASS_INPUT | +| openkal-macos | 0.13.0 → 0.14.0 | 枚举、8 MiB 与 256 KiB 保护页、sized、PASS_INPUT | +| openkal-windows | 0.11.0 → 0.12.0 | 枚举、8 MiB 预留、sized、PASS_INPUT(虚拟终端输入) | +| openkal-emscripten | 0.4.0 → 0.5.0 | 枚举、sized、PASS_INPUT | +| openkal-opensbi | 0.8.2 → 0.8.3 | 仅随规范移动版本钉 | +| openkal-musl | 0.20.1 → 0.21.0 | termios 映射、`pthread_create` 五行、`__clone` 传尺寸、分派帧 | +| openkal-llvm-runtime | 0.15.4 → 0.15.5 | 仅移动 openkal-musl 版本钉 | +| mcpp-index | — | 上述各版本的索引行(GLOBAL 与 CN) | + +openkal-uefi 不提供受影响的接口,也不在 musl 的依赖图中,不动。 + +**任务依赖**(箭头表示"须先完成"): + +``` +S 规范 0.16 ──┬─> B 五个实现(并行) ──> P openkal-musl ──> R openkal-llvm-runtime + └─> B' openkal-opensbi(版本钉)─┘ +R ──> T 打标签与发布(GitHub 发布 + gtc 镜像到 gitcode) ──> I mcpp-index ──> V xlings 沙箱验证 +``` + +所有仓库使用同名分支,每个仓库的 CI 因此能取到其余仓库的同名分支,联合测试未发布的整体(沿用上一轮的约定)。每个仓库一个 PR,提交作为检查点。 + +--- + +## 3. 设计 + +### 3.1 目录枚举(fs) + +- listing 记入一张表,记录其目录的描述符或句柄。`kal_fs_close_dir` 在关闭前结束该目录的全部 listing:先结束,因为关闭后这个编号可能已指向另一个目录。 +- `kal_fs_list_next` 先校验目录句柄,已释放即 `kal_err_invalid`(条款 6.7);再在表中**查找**迭代器,而不是直接沿它读内存。因此陈旧的迭代器,或与其目录不匹配的迭代器,都被拒绝,且不会被读穿。 +- 关闭与另一上下文的 `kal_fs_list_next` 并发时,被读取中的 listing 只做标记,由读取方退出时释放(条款 6.6)。两个上下文读同一枚举时轮流进行。 +- 锁:Linux、macOS、emscripten 用自旋锁(与分配器同理,不依赖 openkal.task);Windows 用 SRW 锁。 +- 观测放在各实现自己的测试里,依据条款 7.13 的先例:句柄计数不可移植。 + +### 3.2 栈大小(task) + +- `kal_task_start_sized(entry, arg, stack_size, out)`:上下文经 `kal_task_stack` 得知的大小不小于 `stack_size`。实现可以多给,零表示不要求。 +- 以下限而非确切值表达:musl 每个线程都请求其默认的 128 KiB,按确切值实现会使所有线程变小。 +- 给不出即 `kal_err_no_memory`,依据条款 3.1(限定可用量是供给)。不设 props 位,也不允许 not_supported(条款 6.1)。 +- 不提供以区域为参数的形式(条款 6.4)。 + +各实现: + +| 实现 | 默认 | 保护页 | sized | +| --- | --- | --- | --- | +| Linux standalone | 8 MiB `mmap` | 256 KiB `PROT_NONE` | 自映射,`max(请求, 默认)` | +| Linux hosted | 运行时自定 | 运行时的 | `pthread_attr_setstacksize`(加余量) | +| macOS | 8 MiB | 256 KiB | standalone 自行组成系统的线程属性(名字被 musl 占用),hosted 调用系统函数;每次 CI 逐字节比对两者 | +| Windows | 8 MiB 预留 | 环境自有,且编译器逐页探测 | `CreateThread` + `STACK_SIZE_PARAM_IS_A_RESERVATION` | +| emscripten | 平台默认 | 平台的 | `pthread_attr_setstacksize`;不提高默认值,因为线性内存即时占用 | + +保护页取 256 KiB,因为 musl 分派函数在 -O0 下实测有 69,008 字节的帧,一页的保护会被一帧越过。 + +musl: +- `pthread_create` 改五行:只映射 TLS、TSD 和启动参数,不映射栈;把请求的大小记入 `stack_size`;拒绝调用者提供的栈(`EINVAL`)。 +- `__clone` 由线程指针取得 `struct pthread`,把大小传给 `kal_task_start_sized`(弱引用,缺失时退回 `kal_task_start`)。 +- `stack_size` 在 musl 中别无读者;`stack` 被 `h_errno.c` 用来判断是否主线程,故保持非空。 + +### 3.3 输入改写(terminal) + +- `KAL_TERM_PASS_INPUT`:置位表示所输入的字节原样到达,回车为 0x0d,不被改写为 0x0a。 +- 它覆盖环境改写输入的全部机制,沿用 PASS_CONTROL 的规则:termios 上是 `ISTRIP|INLCR|IGNCR|ICRNL`,四者皆清才读作置位;清除该位置时只恢复 `ICRNL`;请求值与当前值相同则不写。 +- 与行编辑相互独立;后果(行编辑下回车不再结束一行)写入条文。 + +| 实现 | 映射 | +| --- | --- | +| Linux、macOS、emscripten | 上述四个 termios 标志 | +| Windows | 行输入关闭 **且** `ENABLE_VIRTUAL_TERMINAL_INPUT` 打开时置位;请求置位时打开后者;行输入打开时读作清除,请求不改变它 | + +musl: +- `tcsetattr` 在四标志皆清时请求该位置(`cfmakeraw` 即如此)。 +- 在无回显的 raw 模式下,若程序保留 `ICRNL` 或 `ISTRIP`,也请求该位置,由本层改写。这一状态属于终端而非描述符,作用于交互式标准流的读取。 +- `tcgetattr` 报告的是实际生效的状态。 + +--- + +## 4. 实测修正了第一版的什么 + +| 第一版 | 实测 | 第二版 | +| --- | --- | --- | +| Windows:行输入关闭即"不改写",PASS_INPUT 读作置位 | 控制台关闭行输入、未开虚拟终端输入时,回车为 0x0d,Ctrl+J(三种写法的键事件)**不产生任何字节**;打开虚拟终端输入后,回车 0x0d、Ctrl+J 0x0a;行输入下为 `61 0d 0a` | PASS_INPUT = 行输入关闭 ∧ 虚拟终端输入;由后者建立 | +| musl 按描述符记录"由本层改写" | 程序常经 fd 1 设置、经 fd 0 读取;且在新的 Windows 映射下,原条件在三个实现上都不会成立 | 改为终端级状态;条件改为"无回显的 raw 模式下保留 ICRNL/ISTRIP" | +| 规范 conformance 观测描述符泄漏 | CI 的 RLIMIT_NOFILE 可达 1048576,泄漏不可见 | 由各实现以自身的句柄表观测 | + +--- + +## 5. 测量 + +| 断言 | 手段 | 结果 | +| --- | --- | --- | +| PR #35 的测试能检出缺陷 | Linux:新测试配旧 `fs.cpp` | 失败;配新 `fs.cpp` 通过 | +| 提前关闭不再泄漏 | 各实现自身测试:Linux `/proc/self/fd`、macOS `fcntl`、Windows `GetProcessHandleCount` | Windows 104/104,其余相等 | +| 关闭与读取竞争 200 次 | 各实现自身测试 | 每次读取方都得到报告,进程存活 | +| 至少给出所请求的栈 | 规范 conformance(4 MiB 请求,递归 3 MiB)+ 各实现测试(1/16/64 MiB) | Linux、macOS、Windows(三个工具链)均成立;Windows 1 MiB → 8.25 MiB,64 MiB → 64.25 MiB | +| musl 上 `setstacksize(4 MiB)` 有效 | `examples/stack-size`,main 对照 | main:约 256 KiB 处 SIGSEGV;本轮:成立 | +| 256 MiB 请求不再提交内存 | 同上(Linux 读 VmRSS) | 未增长 | +| 调用者提供的栈被拒绝 | 同上 | `EINVAL` | +| 栈下一字节读即故障 | 同上,在 `posix_spawn` 启动的程序中;macOS hosted 测试在 fork 子进程中 | Linux、macOS 均故障 | +| 分派函数帧 | clang 22.1.8 -O0 反汇编 | `sub rsp, 0x10d90` → `0x2e10` | +| raw 下回车与 Ctrl+J | musl `examples/terminal` 经 pty 与系统 C 库逐字节对照(`cfmakeraw` 与保留 ICRNL 两种) | 两种模式均一致:`0d 0a` 与 `0a 0a` | +| PASS_INPUT 的往返与"不重写" | Linux、macOS:自建 pty;Windows:自建控制台并写入键事件 | 成立 | +| 规范 conformance | 三平台五行 + emscripten | Linux 200 held / 0 / 3 not observed;Windows 182 / 0 / 6 | + +另见:macOS 上 musl 经 `fork` 复制的程序中,`pthread_create` 返回 `EAGAIN`;未修改的 main 同样如此(§7)。 + +--- + +## 6. 多角度审查 + +**架构。** 两项声明都是资源的性质,由资源本身回答:栈大小以上下文为资源,输入改写以 stream 为资源。没有新增结构、错误值或注册表(条款 5.3、5.2、7.1)。规范之外的不对称由 C 库承担:它知道调用者要什么,所以在 raw 模式下自己完成改写。这一分工与上一轮 `pthread_getattr_np` 相同。 + +**稳定性。** +- 迭代器被查找而非信任,因此陈旧的迭代器、不匹配的目录、并发关闭都不会造成释放后使用。 +- 栈有保护页,溢出即故障,而不是写入相邻映射。 +- 代价:`kal_space_start` 时若表锁或 busy 标记被别的上下文持有,子进程会永远等待。这与分配器锁是同一类已记录的限制。 + +**简洁。** 一个操作、一个位置、一段条文补述。termios 的四个输入标志并为一个位置,不按机制拆分(第 20 条的粒度)。musl 的改动限定为五行并记入 PATCHES.md。 + +**用户体验。** +- 报告者的三个场景各自独立修复:轮询 `/proc` 不再耗尽描述符;`setstacksize` 生效;`cfmakeraw` 下回车为 0x0d、Ctrl+J 为 0x0a。 +- 不设置栈大小的程序(`std::thread` 无法设置)在三个原生平台上都得到 8 MiB。 +- Windows 上 raw 模式的方向键、功能键开始以 Unix 终端的转义序列到达。 + +**兼容性。** +- 只新增声明。旧实现对 PASS_INPUT 读作零,musl 据此报告 `ICRNL` 置位,为真。 +- 旧实现缺 `kal_task_start_sized` 时,musl 经弱引用退回 `kal_task_start`。 +- 行为变化: + - 已关闭目录的迭代器从"继续可用"变为被拒绝(fs.h 原本就称其已释放); + - `pthread_attr_setstack` 从"成功但不使用"变为 `EINVAL`; + - macOS、Windows 的默认线程栈从 512 KiB / 1 MiB 变为 8 MiB(地址空间,不是内存)。 + +**跨平台。** 置位时三平台字节相同,已测。默认栈三平台相同,已测。清除时的改写方式各平台不同,这是"零为较弱主张"的含义;C 库在 raw 模式下消除这种差异。 + +**一致性。** 五个实现按同一模式修复枚举(表、查找、busy/doomed),按同一下限语义实现 sized,PASS_INPUT 在 termios 上的四标志映射在三个实现中逐字一致。 + +**无感升级。** 旧程序在新实现上照常编译、照常运行。新程序在旧实现上链接 `kal_task_start_sized` 会在链接期失败(条款 6.2 表中 link 一栏);musl 用弱引用避免把这一点传递给 C 程序。 + +**测试覆盖。** 每项断言都有观测,且观测的是调用者依赖的性质:可用空间而非数值,字节而非标志,句柄计数而非返回值。三平台都经 CI 实测。未覆盖: +- emscripten 的终端(node 下无 pty); +- 真实键盘(以键事件代替;键事件即键盘驱动产生的同一记录)。 + +--- + +## 7. 待定与已知限制 + +1. **macOS(standalone)上 fork 后的线程启动**:复制出的程序中 `pthread_create` 返回 `EAGAIN`。在 CI 上以未修改的 main(openkal-musl 0.20.1、openkal-macos 0.13.0)复现:`parent rc=0`,`copy rc=11`。这是既有缺陷,不由本轮引入,另行报告。 +2. **Windows 行输入模式**下回车以 `0d 0a` 结束一行,与 Unix 的 `0a` 不同。这是行编辑而非 raw 输入的差异,不在本轮范围内。 +3. **`do_mmap` 对一切映射逐字节清零**(不止线程栈),根治需要 openkal.memory 声明零页属性。另行处理。 diff --git a/include/openkal/terminal.h b/include/openkal/terminal.h index 2e8d73e..5f86f70 100644 --- a/include/openkal/terminal.h +++ b/include/openkal/terminal.h @@ -40,15 +40,14 @@ /* KAL_TERM_PASS_CONTROL IS A GUARANTEE IN ONE DIRECTION AND A PERMISSION IN THE * OTHER. Set, every keystroke reaches the program, including the ones an * environment ordinarily keeps for itself. Clear, the environment may reserve - * an agreed set of keystrokes for actions of its own. Whether the bytes a + * an agreed set of keystrokes for actions of its own. Which keystrokes those + * are belongs to the environment: an interface that required every environment + * to reserve the same ones would require of all of them what one of them + * happens to spell, which is the shape clause 7.1 excludes. Whether the bytes a * keystroke produces are altered on their way is a different question, and * KAL_TERM_PASS_INPUT answers it: up to version 0.15 this paragraph said that a * keystroke arrived "as the bytes it produces", which no environment that * rewrites the return key had done. - * Which keystrokes those are belongs to the environment: an interface that - * required every environment to reserve the same ones would require of all of - * them what one of them happens to spell, which is the shape clause 7.1 - * excludes. * * A PROGRAM THAT READS KEYSTROKES CANNOT BE WRITTEN WITHOUT IT. openkal has no * signals, so a program that has turned line assembly off and reads what is From e7ebab0f7aee38f4c31147a40ccad37d6315e604 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Sun, 25 Oct 2026 06:46:52 +0900 Subject: [PATCH 4/4] The suite observes the input position as it observes the control position: not an error, distinguished or zero, and put back --- conformance/src/sections/task.cpp | 2 -- conformance/src/sections/terminal.cpp | 24 ++++++++++++++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/conformance/src/sections/task.cpp b/conformance/src/sections/task.cpp index 8557741..780a9e8 100644 --- a/conformance/src/sections/task.cpp +++ b/conformance/src/sections/task.cpp @@ -131,7 +131,6 @@ void asks_about_itself(void*) { volatile int g_sized_holds = -1; volatile int g_sized_room = -1; -volatile kal_uintptr g_sized_size = 0; // A recursion whose frames are touched one after another, so that an // environment which grows a stack a page at a time is given every page in @@ -159,7 +158,6 @@ void asks_for_room(void*) { void* base = nullptr; kal_uintptr size = 0; const int e = kal_task_stack(&base, &size); - g_sized_size = size; store_release(&g_sized_holds, holds(at, e, base, size) && size >= kSizedAsk); g_recurse(static_cast(kSizedAsk / 4 * 3 / 1024)); store_release(&g_sized_room, 1); diff --git a/conformance/src/sections/terminal.cpp b/conformance/src/sections/terminal.cpp index 81ee749..2aa46bb 100644 --- a/conformance/src/sections/terminal.cpp +++ b/conformance/src/sections/terminal.cpp @@ -117,6 +117,30 @@ void run() { "the mode the section found is the mode it leaves"); } + // THE POSITION VERSION 0.16 ADDED, IN THE SAME TERMS. Asking that input + // arrive unaltered is not an error, and what is read back is the position + // or zero: zero from an implementation that predates it, and from an + // environment whose line assembly alters input whatever it is asked, which + // terminal.h states. The terminal is put back. + { + const kal_uintptr pass = kal::terminal::pass_input.bits; + const int rc = kal_terminal_set_mode(out, original | pass); + observe(kind::behaviour, rc == kal_ok, + "asking that input arrive unaltered is not an error"); + + kal_uintptr after = 0; + const int re = kal_terminal_get_mode(out, &after); + const kal_uintptr got = after & pass; + observe(kind::behaviour, re == kal_ok && (got == pass || got == 0), + "the unaltered-input position is either distinguished or reads as zero"); + + kal_terminal_set_mode(out, original); + kal_uintptr back = 0; + observe(kind::behaviour, + kal_terminal_get_mode(out, &back) == kal_ok && back == original, + "the mode the section found is the mode it leaves, after the input position"); + } + // The display size, where it is known. An environment that cannot ask // reports not_supported and leaves both outputs untouched, so the outputs // are pre-set to a value the operation would not produce.