diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cd46f0e..9e4dba4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -159,6 +159,54 @@ jobs: echo "under review: $("$built" --version) (from $MCPP_SOURCE_REF)" fi + + # ⚠️⚠️ A JOB THAT RESOLVES THE PUBLISHED SPECIFICATION CANNOT REVIEW A + # CHANGE TO ONE. + # + # Another step in this file already clones the specification and + # substitutes it, and that made the arrangement look complete. It was not: + # the steps below reached `openkal' BY VERSION, so a version under review + # --- which by definition is not published --- failed them with + # + # E_NOT_FOUND: package 'compat.openkal@0.9.0' not found in the + # synced index ... the index is current, so this name is either + # wrong or not published yet + # + # ⭐⭐ THE UNIT IS THE JOB, NOT THE REPOSITORY. Measured 2026-08-28 across + # the eight repositories of this ecosystem while one change spanned all of + # them: seven jobs in three repositories had this shape, and every one of + # those repositories ALSO had a job that substituted correctly --- which is + # what made the gap invisible to a check done a repository at a time. + # + # These jobs are green on `main` and can only be green there, because + # there the published version is the one under test. That is the property + # that makes the defect silent: it is not a check that fails, it is a + # check that cannot run at the only time it would have something to say. + - name: Point at the specification's working tree + run: | + set -euo pipefail + git clone --quiet https://github.com/mcpplibs/openkal .spec + if git -C .spec rev-parse --verify --quiet "origin/$OPENKAL_BRANCH" > /dev/null; then + git -C .spec checkout --quiet "origin/$OPENKAL_BRANCH" + echo "the specification is at $OPENKAL_BRANCH" + else + echo "the specification has no $OPENKAL_BRANCH; its default branch is used" + fi + # ⚠️ EVERY MANIFEST THAT NAMES IT, AND BY THE SAME FORM. mcpp refuses + # a graph in which one package reaches a dependency by version and + # another by path, so substituting only the root leaves the build + # refusing for a second reason instead of the first. + # ⚠️ NOT `sed -i'. This step runs on macOS and on Windows too, and + # BSD sed requires an argument to -i that GNU sed refuses. A temporary + # file is the spelling that holds on all three. + subst() { # subst + sed "s|^openkal = .*$|openkal = { path = \"$2\" }|" "$1" > "$1.next" + mv "$1.next" "$1" + grep -q "path = \"$2\"" "$1" \ + || { echo "::error::$1 was not substituted"; exit 1; } + } + subst mcpp.toml .spec + subst examples/hello/mcpp.toml ../../.spec - name: Install the emulator run: | # Both homes: the shim on PATH dispatches against whichever home owns @@ -274,6 +322,9 @@ jobs: run: shell: bash env: + # The specification is taken from the branch of the same name where one + # exists, so a change spanning both repositories is reviewed as a whole. + OPENKAL_BRANCH: ${{ github.head_ref || github.ref_name }} MCPP_VERSION: 2026.8.27.1 XLINGS_VERSION: v2026.8.17.2 XLINGS_NON_INTERACTIVE: '1' @@ -416,6 +467,53 @@ jobs: echo "under review: $("$built" --version) (from $MCPP_SOURCE_REF)" fi + + # ⚠️⚠️ A JOB THAT RESOLVES THE PUBLISHED SPECIFICATION CANNOT REVIEW A + # CHANGE TO ONE. + # + # Another step in this file already clones the specification and + # substitutes it, and that made the arrangement look complete. It was not: + # the steps below reached `openkal' BY VERSION, so a version under review + # --- which by definition is not published --- failed them with + # + # E_NOT_FOUND: package 'compat.openkal@0.9.0' not found in the + # synced index ... the index is current, so this name is either + # wrong or not published yet + # + # ⭐⭐ THE UNIT IS THE JOB, NOT THE REPOSITORY. Measured 2026-08-28 across + # the eight repositories of this ecosystem while one change spanned all of + # them: seven jobs in three repositories had this shape, and every one of + # those repositories ALSO had a job that substituted correctly --- which is + # what made the gap invisible to a check done a repository at a time. + # + # These jobs are green on `main` and can only be green there, because + # there the published version is the one under test. That is the property + # that makes the defect silent: it is not a check that fails, it is a + # check that cannot run at the only time it would have something to say. + - name: Point at the specification's working tree + run: | + set -euo pipefail + git clone --quiet https://github.com/mcpplibs/openkal .spec + if git -C .spec rev-parse --verify --quiet "origin/$OPENKAL_BRANCH" > /dev/null; then + git -C .spec checkout --quiet "origin/$OPENKAL_BRANCH" + echo "the specification is at $OPENKAL_BRANCH" + else + echo "the specification has no $OPENKAL_BRANCH; its default branch is used" + fi + # ⚠️ EVERY MANIFEST THAT NAMES IT, AND BY THE SAME FORM. mcpp refuses + # a graph in which one package reaches a dependency by version and + # another by path, so substituting only the root leaves the build + # refusing for a second reason instead of the first. + # ⚠️ NOT `sed -i'. This step runs on macOS and on Windows too, and + # BSD sed requires an argument to -i that GNU sed refuses. A temporary + # file is the spelling that holds on all three. + subst() { # subst + sed "s|^openkal = .*$|openkal = { path = \"$2\" }|" "$1" > "$1.next" + mv "$1.next" "$1" + grep -q "path = \"$2\"" "$1" \ + || { echo "::error::$1 was not substituted"; exit 1; } + } + subst mcpp.toml .spec - name: The backend cross-builds run: | # ⚠️ TWICE, AND THE FIRST IS ALLOWED TO FAIL — every row of this diff --git a/.gitignore b/.gitignore index 7435482..73453ed 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,7 @@ target/ .mcpp/ compile_commands.json mcpp.lock + +# A working tree of the specification placed beside the sources. No trailing +# slash: the pattern must match a symbolic link to one as well as a directory. +.spec diff --git a/README.md b/README.md index 2ac7d54..cba817a 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,8 @@ An implementation of [openkal][kal] on the RISC-V Supervisor Binary Interface. ```toml [dependencies] -openkal = "0.5.1" -openkal-opensbi = "0.1.0" +openkal = "0.9.0" +openkal-opensbi = "0.3.0" ``` ## ⭐ The portable RISC-V backend, as distinct from a board's own diff --git a/examples/hello/mcpp.toml b/examples/hello/mcpp.toml index dd57044..ed13128 100644 --- a/examples/hello/mcpp.toml +++ b/examples/hello/mcpp.toml @@ -23,5 +23,5 @@ runner = ["qemu-system-riscv64", "-machine", "virt", "-nographic", # That is the right refusal --- two forms can name two different trees --- and # it means an example inside a repository follows that repository's own # declaration while a change is in flight. -openkal = "0.8.0" +openkal = "0.9.0" openkal-opensbi = { path = "../.." } diff --git a/mcpp.toml b/mcpp.toml index 948476f..a22a2e0 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -14,7 +14,7 @@ [package] namespace = "mcpplibs" name = "openkal-opensbi" -version = "0.2.0" +version = "0.3.0" description = "An implementation of openkal on the RISC-V Supervisor Binary Interface, portable across every machine whose firmware provides one" license = "Apache-2.0" @@ -33,7 +33,7 @@ repo = "https://github.com/mcpplibs/openkal-opensbi" # The contract, not an implementation of it. Declaring it turns a version # mismatch into a resolution-time message rather than a link-time one. [dependencies] -openkal = "0.8.0" +openkal = "0.9.0" # ⭐ WHAT RECEIVES CONTROL, WHICH IS A STATEMENT ABOUT THE PROGRAM. # diff --git a/src/env.cpp b/src/env.cpp index 3ef1595..4765c17 100644 --- a/src/env.cpp +++ b/src/env.cpp @@ -35,26 +35,28 @@ extern "C" { +// A machine started by firmware receives no arguments and no named values, and +// the interface is provided rather than withheld: the answer "there are none" +// is an answer, and a program that asks is answered rather than failing to link. +// +// Each of these copies into the caller's buffer and reports the length the value +// has. Here there is no value, so each reports the condition --- which is what +// distinguishes "there is no such thing" from "there is one and it is empty", +// and is the distinction the interface exists to preserve. kal_uintptr kal_env_arg_count(void) { return 0; } -const char* kal_env_arg(kal_uintptr, kal_uintptr* len) { - if (len) *len = 0; - return nullptr; +kal_intptr kal_env_arg(kal_uintptr, char*, kal_uintptr) { + return -kal_err_not_found; } kal_uintptr kal_env_var_count(void) { return 0; } -const char* kal_env_var(const char*, kal_uintptr, kal_uintptr* value_len) { - if (value_len) *value_len = 0; - return nullptr; +kal_intptr kal_env_var(const char*, kal_uintptr, char*, kal_uintptr) { + return -kal_err_not_found; } -const char* kal_env_var_at(kal_uintptr, kal_uintptr* name_len, - const char** value, kal_uintptr* value_len) { - if (name_len) *name_len = 0; - if (value) *value = nullptr; - if (value_len) *value_len = 0; - return nullptr; +kal_intptr kal_env_var_at(kal_uintptr, char*, kal_uintptr) { + return -kal_err_not_found; } } // extern "C" diff --git a/src/kal.cpp b/src/kal.cpp index 5ba8860..2175caf 100644 --- a/src/kal.cpp +++ b/src/kal.cpp @@ -65,8 +65,8 @@ bool dbcn_available() { return g_dbcn == 1; } -kal_io_result write_all(const unsigned char* p, kal_uintptr n) { - if (n == 0) return kal_io_result{0, kal_ok}; +kal_intptr write_all(const unsigned char* p, kal_uintptr n) { + if (n == 0) return 0; if (dbcn_available()) { // The buffer is passed by physical address split into low and high @@ -76,21 +76,24 @@ kal_io_result write_all(const unsigned char* p, kal_uintptr n) { while (done < n) { auto r = sbi_call(SBI_EXT_DBCN, SBI_DBCN_WRITE, n - done, reinterpret_cast(p + done), 0); - if (r.error != SBI_SUCCESS) return kal_io_result{done, kal_err_io}; + if (r.error != SBI_SUCCESS) + return done ? static_cast(done) : -kal_err_io; // A short write is legal here and is why the loop exists; openkal // requires the whole buffer or a report, so the retry is the // implementation's job rather than every caller's. - if (r.value <= 0) return kal_io_result{done, kal_err_io}; + if (r.value <= 0) + return done ? static_cast(done) : -kal_err_io; done += static_cast(r.value); } - return kal_io_result{done, kal_ok}; + return static_cast(done); } for (kal_uintptr i = 0; i < n; ++i) { auto r = sbi_call(SBI_EXT_LEGACY_PUTCHAR, 0, p[i], 0, 0); - if (r.error != SBI_SUCCESS) return kal_io_result{i, kal_err_io}; + if (r.error != SBI_SUCCESS) + return i ? static_cast(i) : -kal_err_io; } - return kal_io_result{n, kal_ok}; + return static_cast(n); } // ── The heap ──────────────────────────────────────────────────────────────── @@ -178,30 +181,29 @@ kal_stream kal_stdin (void) { return kal_stream{kStdin}; } kal_stream kal_stdout(void) { return kal_stream{kStdout}; } kal_stream kal_stderr(void) { return kal_stream{kStderr}; } -kal_io_result kal_stream_write(kal_stream s, const void* buf, kal_uintptr n) { - if (s.h != kStdout && s.h != kStderr) - return kal_io_result{0, kal_err_invalid}; +kal_intptr kal_stream_write(kal_stream s, const void* buf, kal_uintptr n) { + if (s.h != kStdout && s.h != kStderr) return -kal_err_invalid; // ⚠️ Both streams reach the same console. SBI has one, and reporting two // that are secretly one would be a claim the firmware cannot honour. return write_all(static_cast(buf), n); } -kal_io_result kal_stream_read(kal_stream s, void* buf, kal_uintptr n) { - if (s.h != kStdin) return kal_io_result{0, kal_err_invalid}; - if (n == 0) return kal_io_result{0, kal_ok}; +kal_intptr kal_stream_read(kal_stream s, void* buf, kal_uintptr n) { + if (s.h != kStdin) return -kal_err_invalid; + if (n == 0) return 0; auto* out = static_cast(buf); if (dbcn_available()) { auto r = sbi_call(SBI_EXT_DBCN, SBI_DBCN_READ, n, reinterpret_cast(out), 0); - if (r.error != SBI_SUCCESS) return kal_io_result{0, kal_err_io}; - return kal_io_result{static_cast(r.value), kal_ok}; + if (r.error != SBI_SUCCESS) return -kal_err_io; + return static_cast(r.value); } auto r = sbi_call(SBI_EXT_LEGACY_GETCHAR, 0, 0, 0, 0); // The legacy extension reports "nothing available" as a negative value, // which is end of input as far as a reader is concerned. - if (r.error < 0) return kal_io_result{0, kal_ok}; + if (r.error < 0) return 0; out[0] = static_cast(r.error); - return kal_io_result{1, kal_ok}; + return 1; } // The firmware console is not buffered by this implementation, so there is @@ -220,6 +222,16 @@ kal_uintptr kal_stream_props(kal_stream s) { } // ── openkal.memory ────────────────────────────────────────────────────────── +// The quantum this environment allocates and protects memory in. +// +// ONE, AND THAT IS AN ANSWER RATHER THAN AN ABSENCE. This machine has no +// memory management unit in this arrangement: memory is a fixed region and the +// allocator hands out any alignment a caller asks for, so every address and +// every length is acceptable. A caller that rounds to one is correct, which is +// what the operation promises. Reporting a page size the firmware does not +// enforce would be reporting a fact about some other machine. +kal_uintptr kal_memory_granularity(void) { return 1; } + void* kal_alloc(kal_uintptr size, kal_uintptr align) { if (size == 0) size = 1; if (align == 0) align = 1; diff --git a/src/time.cpp b/src/time.cpp index 5388822..50bc519 100644 --- a/src/time.cpp +++ b/src/time.cpp @@ -124,6 +124,6 @@ void kal_time_sleep(kal_duration ns) { while (ticks() < deadline) {} } -const kal_uintptr kal_time_props = KAL_TIME_PROP_SLEEP_PRECISE; +kal_uintptr kal_time_props(void) { return KAL_TIME_PROP_SLEEP_PRECISE; } } // extern "C" diff --git a/src/version.cpp b/src/version.cpp new file mode 100644 index 0000000..eb214a0 --- /dev/null +++ b/src/version.cpp @@ -0,0 +1,24 @@ +#include + +// What this implementation says about itself before it is used. +// +// NOT AN INTERFACE, SO NOT CONDITIONAL ON ONE, AND NOT A BURDEN ON A MACHINE +// WITH NO OPERATING SYSTEM EITHER: both answers are constants. Clause 3.2 closes +// the set of core INTERFACES and these provide no resource; what they let a +// consumer do is ask which interfaces are here before it calls into one, which a +// consumer that is linked learns from the linker and one bound otherwise cannot. +extern "C" { + +kal_u64 kal_version(void) { return KAL_VERSION; } + +// ⚠️ THE WORD AGREES WITH WHAT IS EXPORTED, WHICH IS THE WHOLE OF ITS VALUE. +// This machine has no storage, no second image and no scheduler, so +// `openkal.fs', `openkal.process' and `openkal.task' are absent as definitions +// --- and the word says so rather than leaving a consumer to discover it by +// failing to link, which is the only report the earlier arrangement had. +kal_u64 kal_interfaces(void) { + return KAL_IFACE_ABORT | KAL_IFACE_STREAM | KAL_IFACE_MEMORY + | KAL_IFACE_ENV | KAL_IFACE_TIME; +} + +}