Skip to content

Commit 2de1beb

Browse files
0.3.0 — adopt openkal 0.9 (#12)
* 0.3.0 --- adopt openkal 0.9 Transfers return one signed word. The parameters a program receives are copied into the caller's buffer and the length reported is the value's own --- here there are none, so each reports the condition, which is what distinguishes "no such thing" from "one, and it is empty". `kal_memory_granularity' is answered with a constant, which is what the specification says the cheap answer should be, and `kal_version' and `kal_interfaces' likewise. The second is worth having on this row in particular: a machine with no storage, no second image and no scheduler has those interfaces absent as definitions, and the word now says so rather than leaving a consumer to discover it by failing to link. * README: the versions it names are the versions that exist Every README here opens by showing what a program writes in its manifest, which is the first thing a reader copies and the last thing anyone edits. These lines had drifted --- the specification's own README asked for a version four minor releases old --- and nothing checked them. `openkal/tools/check-readme-versions.sh` now does. * ci: build against the specification under review, not the published one A job that resolves the published specification cannot review a change to one. Another job in this file already cloned the specification and substituted 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 across the eight repositories of this ecosystem while one change spanned all of them: seven jobs in three of them had this shape, and each of those repositories also had a job doing it correctly -- which is what made the gap invisible to a check done a repository at a time. These steps are green on main and can only be green there, because there the published version is the one under test. It is not a check that fails, it is a check that cannot run at the only time it would have something to say. The README also asked a reader for openkal 0.5.1 against a specification now at 0.9.0, and examples/hello asked for 0.8.0. Both are lines a reader copies. * ci: substitute the specification per step, not per job Measured 2026-08-28 across the eight repositories of this ecosystem while one change spanned all of them: eight jobs in four of them called `mcpp build' at a point where the manifest still named openkal BY VERSION, so a version under review -- which by definition is not published -- failed them with E_NOT_FOUND. The mechanism is not a missing substitution. run-conformance.sh substitutes the manifest and RESTORES IT ON EXIT, correctly; every step after it is back to naming a version. So an audit asking "does this job substitute?" passes the job and misses the steps, which is how the first pass at this found three repositories and not four. These steps are green on main and can only be green there, because there the published version is the one under test. It is not a check that fails, it is a check that cannot run at the only time it would have something to say. The substitution is also portable now: the opensbi and uefi portability jobs run on macOS and Windows, where BSD sed requires an argument to -i that GNU sed refuses. --------- Co-authored-by: speak-agent <x.d2learn.org@gmail.com>
1 parent 42eb89e commit 2de1beb

9 files changed

Lines changed: 175 additions & 35 deletions

File tree

.github/workflows/ci.yml

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,54 @@ jobs:
159159
echo "under review: $("$built" --version) (from $MCPP_SOURCE_REF)"
160160
fi
161161
162+
163+
# ⚠️⚠️ A JOB THAT RESOLVES THE PUBLISHED SPECIFICATION CANNOT REVIEW A
164+
# CHANGE TO ONE.
165+
#
166+
# Another step in this file already clones the specification and
167+
# substitutes it, and that made the arrangement look complete. It was not:
168+
# the steps below reached `openkal' BY VERSION, so a version under review
169+
# --- which by definition is not published --- failed them with
170+
#
171+
# E_NOT_FOUND: package 'compat.openkal@0.9.0' not found in the
172+
# synced index ... the index is current, so this name is either
173+
# wrong or not published yet
174+
#
175+
# ⭐⭐ THE UNIT IS THE JOB, NOT THE REPOSITORY. Measured 2026-08-28 across
176+
# the eight repositories of this ecosystem while one change spanned all of
177+
# them: seven jobs in three repositories had this shape, and every one of
178+
# those repositories ALSO had a job that substituted correctly --- which is
179+
# what made the gap invisible to a check done a repository at a time.
180+
#
181+
# These jobs are green on `main` and can only be green there, because
182+
# there the published version is the one under test. That is the property
183+
# that makes the defect silent: it is not a check that fails, it is a
184+
# check that cannot run at the only time it would have something to say.
185+
- name: Point at the specification's working tree
186+
run: |
187+
set -euo pipefail
188+
git clone --quiet https://github.com/mcpplibs/openkal .spec
189+
if git -C .spec rev-parse --verify --quiet "origin/$OPENKAL_BRANCH" > /dev/null; then
190+
git -C .spec checkout --quiet "origin/$OPENKAL_BRANCH"
191+
echo "the specification is at $OPENKAL_BRANCH"
192+
else
193+
echo "the specification has no $OPENKAL_BRANCH; its default branch is used"
194+
fi
195+
# ⚠️ EVERY MANIFEST THAT NAMES IT, AND BY THE SAME FORM. mcpp refuses
196+
# a graph in which one package reaches a dependency by version and
197+
# another by path, so substituting only the root leaves the build
198+
# refusing for a second reason instead of the first.
199+
# ⚠️ NOT `sed -i'. This step runs on macOS and on Windows too, and
200+
# BSD sed requires an argument to -i that GNU sed refuses. A temporary
201+
# file is the spelling that holds on all three.
202+
subst() { # subst <file> <relative-path-to-the-specification>
203+
sed "s|^openkal = .*$|openkal = { path = \"$2\" }|" "$1" > "$1.next"
204+
mv "$1.next" "$1"
205+
grep -q "path = \"$2\"" "$1" \
206+
|| { echo "::error::$1 was not substituted"; exit 1; }
207+
}
208+
subst mcpp.toml .spec
209+
subst examples/hello/mcpp.toml ../../.spec
162210
- name: Install the emulator
163211
run: |
164212
# Both homes: the shim on PATH dispatches against whichever home owns
@@ -274,6 +322,9 @@ jobs:
274322
run:
275323
shell: bash
276324
env:
325+
# The specification is taken from the branch of the same name where one
326+
# exists, so a change spanning both repositories is reviewed as a whole.
327+
OPENKAL_BRANCH: ${{ github.head_ref || github.ref_name }}
277328
MCPP_VERSION: 2026.8.27.1
278329
XLINGS_VERSION: v2026.8.17.2
279330
XLINGS_NON_INTERACTIVE: '1'
@@ -416,6 +467,53 @@ jobs:
416467
echo "under review: $("$built" --version) (from $MCPP_SOURCE_REF)"
417468
fi
418469
470+
471+
# ⚠️⚠️ A JOB THAT RESOLVES THE PUBLISHED SPECIFICATION CANNOT REVIEW A
472+
# CHANGE TO ONE.
473+
#
474+
# Another step in this file already clones the specification and
475+
# substitutes it, and that made the arrangement look complete. It was not:
476+
# the steps below reached `openkal' BY VERSION, so a version under review
477+
# --- which by definition is not published --- failed them with
478+
#
479+
# E_NOT_FOUND: package 'compat.openkal@0.9.0' not found in the
480+
# synced index ... the index is current, so this name is either
481+
# wrong or not published yet
482+
#
483+
# ⭐⭐ THE UNIT IS THE JOB, NOT THE REPOSITORY. Measured 2026-08-28 across
484+
# the eight repositories of this ecosystem while one change spanned all of
485+
# them: seven jobs in three repositories had this shape, and every one of
486+
# those repositories ALSO had a job that substituted correctly --- which is
487+
# what made the gap invisible to a check done a repository at a time.
488+
#
489+
# These jobs are green on `main` and can only be green there, because
490+
# there the published version is the one under test. That is the property
491+
# that makes the defect silent: it is not a check that fails, it is a
492+
# check that cannot run at the only time it would have something to say.
493+
- name: Point at the specification's working tree
494+
run: |
495+
set -euo pipefail
496+
git clone --quiet https://github.com/mcpplibs/openkal .spec
497+
if git -C .spec rev-parse --verify --quiet "origin/$OPENKAL_BRANCH" > /dev/null; then
498+
git -C .spec checkout --quiet "origin/$OPENKAL_BRANCH"
499+
echo "the specification is at $OPENKAL_BRANCH"
500+
else
501+
echo "the specification has no $OPENKAL_BRANCH; its default branch is used"
502+
fi
503+
# ⚠️ EVERY MANIFEST THAT NAMES IT, AND BY THE SAME FORM. mcpp refuses
504+
# a graph in which one package reaches a dependency by version and
505+
# another by path, so substituting only the root leaves the build
506+
# refusing for a second reason instead of the first.
507+
# ⚠️ NOT `sed -i'. This step runs on macOS and on Windows too, and
508+
# BSD sed requires an argument to -i that GNU sed refuses. A temporary
509+
# file is the spelling that holds on all three.
510+
subst() { # subst <file> <relative-path-to-the-specification>
511+
sed "s|^openkal = .*$|openkal = { path = \"$2\" }|" "$1" > "$1.next"
512+
mv "$1.next" "$1"
513+
grep -q "path = \"$2\"" "$1" \
514+
|| { echo "::error::$1 was not substituted"; exit 1; }
515+
}
516+
subst mcpp.toml .spec
419517
- name: The backend cross-builds
420518
run: |
421519
# ⚠️ TWICE, AND THE FIRST IS ALLOWED TO FAIL — every row of this

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,7 @@ target/
22
.mcpp/
33
compile_commands.json
44
mcpp.lock
5+
6+
# A working tree of the specification placed beside the sources. No trailing
7+
# slash: the pattern must match a symbolic link to one as well as a directory.
8+
.spec

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ An implementation of [openkal][kal] on the RISC-V Supervisor Binary Interface.
44

55
```toml
66
[dependencies]
7-
openkal = "0.5.1"
8-
openkal-opensbi = "0.1.0"
7+
openkal = "0.9.0"
8+
openkal-opensbi = "0.3.0"
99
```
1010

1111
## ⭐ The portable RISC-V backend, as distinct from a board's own

examples/hello/mcpp.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,5 +23,5 @@ runner = ["qemu-system-riscv64", "-machine", "virt", "-nographic",
2323
# That is the right refusal --- two forms can name two different trees --- and
2424
# it means an example inside a repository follows that repository's own
2525
# declaration while a change is in flight.
26-
openkal = "0.8.0"
26+
openkal = "0.9.0"
2727
openkal-opensbi = { path = "../.." }

mcpp.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
[package]
1515
namespace = "mcpplibs"
1616
name = "openkal-opensbi"
17-
version = "0.2.0"
17+
version = "0.3.0"
1818
description = "An implementation of openkal on the RISC-V Supervisor Binary Interface, portable across every machine whose firmware provides one"
1919
license = "Apache-2.0"
2020

@@ -33,7 +33,7 @@ repo = "https://github.com/mcpplibs/openkal-opensbi"
3333
# The contract, not an implementation of it. Declaring it turns a version
3434
# mismatch into a resolution-time message rather than a link-time one.
3535
[dependencies]
36-
openkal = "0.8.0"
36+
openkal = "0.9.0"
3737

3838
# ⭐ WHAT RECEIVES CONTROL, WHICH IS A STATEMENT ABOUT THE PROGRAM.
3939
#

src/env.cpp

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -35,26 +35,28 @@
3535

3636
extern "C" {
3737

38+
// A machine started by firmware receives no arguments and no named values, and
39+
// the interface is provided rather than withheld: the answer "there are none"
40+
// is an answer, and a program that asks is answered rather than failing to link.
41+
//
42+
// Each of these copies into the caller's buffer and reports the length the value
43+
// has. Here there is no value, so each reports the condition --- which is what
44+
// distinguishes "there is no such thing" from "there is one and it is empty",
45+
// and is the distinction the interface exists to preserve.
3846
kal_uintptr kal_env_arg_count(void) { return 0; }
3947

40-
const char* kal_env_arg(kal_uintptr, kal_uintptr* len) {
41-
if (len) *len = 0;
42-
return nullptr;
48+
kal_intptr kal_env_arg(kal_uintptr, char*, kal_uintptr) {
49+
return -kal_err_not_found;
4350
}
4451

4552
kal_uintptr kal_env_var_count(void) { return 0; }
4653

47-
const char* kal_env_var(const char*, kal_uintptr, kal_uintptr* value_len) {
48-
if (value_len) *value_len = 0;
49-
return nullptr;
54+
kal_intptr kal_env_var(const char*, kal_uintptr, char*, kal_uintptr) {
55+
return -kal_err_not_found;
5056
}
5157

52-
const char* kal_env_var_at(kal_uintptr, kal_uintptr* name_len,
53-
const char** value, kal_uintptr* value_len) {
54-
if (name_len) *name_len = 0;
55-
if (value) *value = nullptr;
56-
if (value_len) *value_len = 0;
57-
return nullptr;
58+
kal_intptr kal_env_var_at(kal_uintptr, char*, kal_uintptr) {
59+
return -kal_err_not_found;
5860
}
5961

6062
} // extern "C"

src/kal.cpp

Lines changed: 29 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,8 @@ bool dbcn_available() {
6565
return g_dbcn == 1;
6666
}
6767

68-
kal_io_result write_all(const unsigned char* p, kal_uintptr n) {
69-
if (n == 0) return kal_io_result{0, kal_ok};
68+
kal_intptr write_all(const unsigned char* p, kal_uintptr n) {
69+
if (n == 0) return 0;
7070

7171
if (dbcn_available()) {
7272
// 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) {
7676
while (done < n) {
7777
auto r = sbi_call(SBI_EXT_DBCN, SBI_DBCN_WRITE,
7878
n - done, reinterpret_cast<kal_uintptr>(p + done), 0);
79-
if (r.error != SBI_SUCCESS) return kal_io_result{done, kal_err_io};
79+
if (r.error != SBI_SUCCESS)
80+
return done ? static_cast<kal_intptr>(done) : -kal_err_io;
8081
// A short write is legal here and is why the loop exists; openkal
8182
// requires the whole buffer or a report, so the retry is the
8283
// implementation's job rather than every caller's.
83-
if (r.value <= 0) return kal_io_result{done, kal_err_io};
84+
if (r.value <= 0)
85+
return done ? static_cast<kal_intptr>(done) : -kal_err_io;
8486
done += static_cast<kal_uintptr>(r.value);
8587
}
86-
return kal_io_result{done, kal_ok};
88+
return static_cast<kal_intptr>(done);
8789
}
8890

8991
for (kal_uintptr i = 0; i < n; ++i) {
9092
auto r = sbi_call(SBI_EXT_LEGACY_PUTCHAR, 0, p[i], 0, 0);
91-
if (r.error != SBI_SUCCESS) return kal_io_result{i, kal_err_io};
93+
if (r.error != SBI_SUCCESS)
94+
return i ? static_cast<kal_intptr>(i) : -kal_err_io;
9295
}
93-
return kal_io_result{n, kal_ok};
96+
return static_cast<kal_intptr>(n);
9497
}
9598

9699
// ── The heap ────────────────────────────────────────────────────────────────
@@ -178,30 +181,29 @@ kal_stream kal_stdin (void) { return kal_stream{kStdin}; }
178181
kal_stream kal_stdout(void) { return kal_stream{kStdout}; }
179182
kal_stream kal_stderr(void) { return kal_stream{kStderr}; }
180183

181-
kal_io_result kal_stream_write(kal_stream s, const void* buf, kal_uintptr n) {
182-
if (s.h != kStdout && s.h != kStderr)
183-
return kal_io_result{0, kal_err_invalid};
184+
kal_intptr kal_stream_write(kal_stream s, const void* buf, kal_uintptr n) {
185+
if (s.h != kStdout && s.h != kStderr) return -kal_err_invalid;
184186
// ⚠️ Both streams reach the same console. SBI has one, and reporting two
185187
// that are secretly one would be a claim the firmware cannot honour.
186188
return write_all(static_cast<const unsigned char*>(buf), n);
187189
}
188190

189-
kal_io_result kal_stream_read(kal_stream s, void* buf, kal_uintptr n) {
190-
if (s.h != kStdin) return kal_io_result{0, kal_err_invalid};
191-
if (n == 0) return kal_io_result{0, kal_ok};
191+
kal_intptr kal_stream_read(kal_stream s, void* buf, kal_uintptr n) {
192+
if (s.h != kStdin) return -kal_err_invalid;
193+
if (n == 0) return 0;
192194
auto* out = static_cast<unsigned char*>(buf);
193195
if (dbcn_available()) {
194196
auto r = sbi_call(SBI_EXT_DBCN, SBI_DBCN_READ,
195197
n, reinterpret_cast<kal_uintptr>(out), 0);
196-
if (r.error != SBI_SUCCESS) return kal_io_result{0, kal_err_io};
197-
return kal_io_result{static_cast<kal_uintptr>(r.value), kal_ok};
198+
if (r.error != SBI_SUCCESS) return -kal_err_io;
199+
return static_cast<kal_intptr>(r.value);
198200
}
199201
auto r = sbi_call(SBI_EXT_LEGACY_GETCHAR, 0, 0, 0, 0);
200202
// The legacy extension reports "nothing available" as a negative value,
201203
// which is end of input as far as a reader is concerned.
202-
if (r.error < 0) return kal_io_result{0, kal_ok};
204+
if (r.error < 0) return 0;
203205
out[0] = static_cast<unsigned char>(r.error);
204-
return kal_io_result{1, kal_ok};
206+
return 1;
205207
}
206208

207209
// The firmware console is not buffered by this implementation, so there is
@@ -220,6 +222,16 @@ kal_uintptr kal_stream_props(kal_stream s) {
220222
}
221223

222224
// ── openkal.memory ──────────────────────────────────────────────────────────
225+
// The quantum this environment allocates and protects memory in.
226+
//
227+
// ONE, AND THAT IS AN ANSWER RATHER THAN AN ABSENCE. This machine has no
228+
// memory management unit in this arrangement: memory is a fixed region and the
229+
// allocator hands out any alignment a caller asks for, so every address and
230+
// every length is acceptable. A caller that rounds to one is correct, which is
231+
// what the operation promises. Reporting a page size the firmware does not
232+
// enforce would be reporting a fact about some other machine.
233+
kal_uintptr kal_memory_granularity(void) { return 1; }
234+
223235
void* kal_alloc(kal_uintptr size, kal_uintptr align) {
224236
if (size == 0) size = 1;
225237
if (align == 0) align = 1;

src/time.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,6 @@ void kal_time_sleep(kal_duration ns) {
124124
while (ticks() < deadline) {}
125125
}
126126

127-
const kal_uintptr kal_time_props = KAL_TIME_PROP_SLEEP_PRECISE;
127+
kal_uintptr kal_time_props(void) { return KAL_TIME_PROP_SLEEP_PRECISE; }
128128

129129
} // extern "C"

src/version.cpp

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
#include <openkal/version.h>
2+
3+
// What this implementation says about itself before it is used.
4+
//
5+
// NOT AN INTERFACE, SO NOT CONDITIONAL ON ONE, AND NOT A BURDEN ON A MACHINE
6+
// WITH NO OPERATING SYSTEM EITHER: both answers are constants. Clause 3.2 closes
7+
// the set of core INTERFACES and these provide no resource; what they let a
8+
// consumer do is ask which interfaces are here before it calls into one, which a
9+
// consumer that is linked learns from the linker and one bound otherwise cannot.
10+
extern "C" {
11+
12+
kal_u64 kal_version(void) { return KAL_VERSION; }
13+
14+
// ⚠️ THE WORD AGREES WITH WHAT IS EXPORTED, WHICH IS THE WHOLE OF ITS VALUE.
15+
// This machine has no storage, no second image and no scheduler, so
16+
// `openkal.fs', `openkal.process' and `openkal.task' are absent as definitions
17+
// --- and the word says so rather than leaving a consumer to discover it by
18+
// failing to link, which is the only report the earlier arrangement had.
19+
kal_u64 kal_interfaces(void) {
20+
return KAL_IFACE_ABORT | KAL_IFACE_STREAM | KAL_IFACE_MEMORY
21+
| KAL_IFACE_ENV | KAL_IFACE_TIME;
22+
}
23+
24+
}

0 commit comments

Comments
 (0)