Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -571,6 +571,73 @@ jobs:
fi
rm -f src/extra.cpp

- name: The interface list is derived from the artefact
working-directory: examples/substitution/impl-fd
run: |
set -euo pipefail
# `provides-interfaces` in a package manifest is a declaration, and a
# declaration is checked rather than trusted. The check available to
# it is clause 9's surface comparison stopped one step earlier: the
# walk that decides whether each group is exported whole already
# answers which interfaces an implementation provides.
#
# THE SUBJECT IS A SYNTHETIC OBJECT BUILT FROM SURFACE.txt, AND NOT
# THIS EXAMPLE. `impl-fd` exists to show substitution, not
# conformance: it defines six of openkal.stream's seven names and
# none of the other groups, so no interface is exported whole and
# every leg below would be measuring an implementation that claims
# nothing. The object here is constructed so that the answer is known
# in advance, which is what lets the check fail for the one reason it
# exists to catch.
python3 - ../../../SURFACE.txt > fake.c <<'PY'
import sys
groups, cur = {}, None
for line in open(sys.argv[1], encoding='utf-8'):
line = line.rstrip('\n')
if line.startswith('# openkal.'):
cur = line[2:].split(' ')[0]; groups[cur] = []
elif line.startswith('#') or not line.strip():
continue
elif cur:
groups[cur].append(line.strip())
# Three groups whole, and two names of a fourth: an interface is
# provided in whole or not at all, so the fourth must not be listed.
for g in ('openkal.version', 'openkal.abort', 'openkal.stream', 'openkal.memory'):
for n in groups.get(g, []):
print(f'int {n}(void);\nint {n}(void) {{ return 0; }}')
for n in groups.get('openkal.fs', [])[:2]:
print(f'int {n}(void);\nint {n}(void) {{ return 0; }}')
PY
cc -c fake.c -o fake.o
bash ../../../tools/check-surface.sh --interfaces \
../../../SURFACE.txt fake.o > provided.txt
cat provided.txt

for want in openkal.abort openkal.stream openkal.memory; do
grep -qx "$want" provided.txt || {
echo "::error::$want is exported whole and was not listed"; exit 1; }
done
# `openkal.version` is exported by every conforming implementation
# and is not an interface --- SURFACE.txt says so in its own header.
# Listing it would put a name that answers nothing in every package.
if grep -qx 'openkal.version' provided.txt; then
echo "::error::openkal.version is not an interface and must not be listed"; exit 1
fi
# HALF AN INTERFACE IS NOT A SMALLER CLAIM. Without this leg the
# checks above pass against a script that prints every group in
# SURFACE.txt and reads no artefact at all.
if grep -qx 'openkal.fs' provided.txt; then
echo "::error::a group exported in part was reported as provided"; exit 1
fi
# And the --toml spelling is the same answer in the shape a manifest
# carries, because a package's declaration is generated from it.
bash ../../../tools/check-surface.sh --toml \
../../../SURFACE.txt fake.o > provided.toml
grep -qx 'provides-interfaces = \[' provided.toml
grep -qx ' "openkal.stream",' provided.toml
rm -f fake.c fake.o provided.txt provided.toml
echo " ok the list is the artefact's own answer"

# ---------------------------------------------------------------------------
# The suite, against the implementation for each system.
#
Expand Down
36 changes: 36 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,42 @@ Clause 9 has two halves, and both are here.
names against `SURFACE.txt`. It detects the one freedom an implementation retains
after the language has removed the others: the addition of names.

**Which interfaces an implementation provides, derived rather than declared.**
`tools/check-surface.sh --interfaces` prints the interfaces the artefact exports
whole; `--toml` prints them as the array a package manifest carries:

```bash
bash tools/check-surface.sh --toml SURFACE.txt $(find target -name '*.o')
```

```toml
provides-interfaces = [
"openkal.abort",
"openkal.stream",
"openkal.memory",
]
```

It is a mode of the surface comparison rather than a second script because it is
that comparison stopped one step earlier: clause 9 already walks `SURFACE.txt`
group by group and decides, for each, whether the artefact exports it whole, in
part, or not at all. A second derivation of the same walk would go stale the
first time a group was added.

A package's `provides-interfaces` is therefore generated from the artefact and
not written by hand. A declaration derived from the thing it describes cannot
disagree with it, and an implementation that gains an interface cannot forget to
say so. A group exported in part is not listed: an interface is provided in whole
or not at all (clause 3), so half of one is not a smaller claim but a different
and false one.

A build tool reads that array at dependency resolution, against the
`requires-interfaces` a consumer states in its own package, and refuses a
combination before anything is compiled. That is the first of the three times
clause 6.2 tabulates, and the earliest at which the question can be answered —
source that asks the same question with `#ifdef` asks it during preprocessing,
which is earlier than any answer exists.

**The behaviour.** [`conformance/`](conformance/) is a program an implementation
runs against itself — 193 observations across fifteen interfaces, in four kinds:
behaviour, ABI, stability and cost.
Expand Down
82 changes: 80 additions & 2 deletions tools/check-surface.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,43 @@
# incomplete coverage, and it detects the one freedom an implementation retains
# after the language has removed the others: the addition of names.
#
# check-surface.sh [--complete] <surface-list> <object-or-archive>...
# check-surface.sh [--complete] [--interfaces|--toml] <surface-list> <object>...
#
# Without --complete, an absent name denotes an interface the implementation
# does not provide, which clause 3 permits. With --complete, every name in the
# list is required, which is what an implementation claiming the whole
# specification asserts.
#
# --interfaces WRITES THE ANSWER THE COMPARISON ALREADY REACHES, AND THAT IS
# WHY IT IS A MODE OF THIS SCRIPT RATHER THAN A SECOND ONE. Clause 9's surface
# comparison walks SURFACE.txt group by group and decides, for each, whether
# the artefact exports it whole, in part, or not at all. "Which interfaces does
# this implementation provide" is that same walk, stopped one step earlier.
# Deriving it anywhere else would be the same decision written twice, and the
# copy would go stale the first time a group was added.
#
# --interfaces prints one interface name per line. --toml prints the array a
# manifest carries, so that a package's `[kernel-abi] provides-interfaces` is
# GENERATED FROM THE ARTEFACT rather than written by hand: a declaration
# derived from the thing it describes cannot disagree with it, and a package
# that gains an interface cannot forget to say so.
#
# A group exported in PART is a defect under --complete and is reported as
# such; under --interfaces it is simply not listed, because an interface is
# provided in whole or not at all (clause 3) and half of one is not a smaller
# claim but a different, false one.
set -euo pipefail

complete=0
if [ "${1:-}" = "--complete" ]; then complete=1; shift; fi
emit=''
while :; do
case "${1:-}" in
--complete) complete=1; shift ;;
--interfaces) emit=lines; shift ;;
--toml) emit=toml; shift ;;
*) break ;;
esac
done

list="${1:?usage: check-surface.sh [--complete] <surface-list> <object>...}"
shift
Expand Down Expand Up @@ -93,6 +120,57 @@ done <<< "$found"
# implementation does not provide and is not a deviation". A group none of whose
# names is exported is an interface not provided. A group SOME of whose names
# are exported is the thing this check exists to catch: half an interface.
# --interfaces / --toml: the same group walk, reporting which interfaces the
# artefact provides WHOLE. Written before the --complete block so that a caller
# asking for both gets the list and the verdict from one reading of the list.
if [ -n "$emit" ]; then
provided=''
group=''; want=''
emit_group() {
[ -n "$group" ] && [ -n "$want" ] || return 0
# `openkal.version` is not an interface (SURFACE.txt says so in its own
# header: it provides no resource). Every conforming implementation
# exports it, so listing it would put a name in every package's
# declaration that answers nothing.
[ "$group" != "openkal.version" ] || return 0
local present=0 absent=0
while read -r name; do
[ -n "$name" ] || continue
if grep -qxF -- "$name" <<< "$found"; then present=$((present+1))
else absent=$((absent+1)); fi
done <<< "$want"
if [ "$present" -gt 0 ] && [ "$absent" -eq 0 ]; then
provided="$provided$group
"
fi
}
while IFS= read -r line; do
case "$line" in
# The heading may carry prose after the name --- SURFACE.txt's
# `openkal.version` row explains there why it is spelled as a group
# and is not an interface --- so the name is the first word of it
# and not the rest of the line.
'# openkal.'*) emit_group; group="${line#\# }"; group="${group%% *}"; want='' ;;
'#'*|'') ;;
*) want="$want$line
" ;;
esac
done < "$list"
emit_group
if [ -z "$provided" ]; then
echo "no interface is exported whole; refusing to write an empty declaration" >&2
exit 1
fi
if [ "$emit" = toml ]; then
echo "provides-interfaces = ["
while read -r g; do [ -n "$g" ] && printf ' "%s",\n' "$g"; done <<< "$provided"
echo "]"
else
printf '%s' "$provided"
fi
exit 0
fi

if [ "$complete" -eq 1 ]; then
group=''; want=''
check_group() {
Expand Down
Loading