diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 23b4261..ecfb5d7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,33 +1,75 @@ name: CI +# What this workflow asserts about the specification. +# +# A specification is a claim about programs, so the claims are tested by +# building and running programs rather than by reading the text. There are four: +# +# declarations the two forms compile, under three compiler families and on +# three systems, and declare the same entities +# substitution a program's source is invariant under a change of +# implementation, and the check that says so fails when it should +# conformance the suite in this repository runs against the implementation +# for each system and every observation holds +# composability an implementation that provides three interfaces is examined +# for three, rather than failing to link +# +# The three compiler families are covered because the specification is a +# contract and a contract that holds only under the compiler its author used is +# a description of that compiler. + on: push: branches: [main] pull_request: workflow_dispatch: +env: + # A version verified to build these packages, not a measured minimum. The pin + # exists for reproducibility rather than because an older mcpp is known to + # fail. + MCPP_VERSION: 2026.8.19.4 + XLINGS_VERSION: v2026.8.17.2 + XLINGS_NON_INTERACTIVE: '1' + jobs: - build: - name: declarations compile, and substitution holds - runs-on: ubuntu-24.04 - timeout-minutes: 30 - env: - # A version verified to build this package, not a measured minimum. The - # package uses modules, exported extern "C" declarations and ordinary - # dependencies, none of which is recent; the pin exists for reproducibility - # rather than because an older mcpp is known to fail. - MCPP_VERSION: 2026.8.19.3 - XLINGS_VERSION: v2026.8.17.2 - XLINGS_NON_INTERACTIVE: '1' + # --------------------------------------------------------------------------- + # The declarations compile, everywhere, in both forms. + declarations: + name: declarations (${{ matrix.os }}, ${{ matrix.toolchain }}) + runs-on: ${{ matrix.os }} + timeout-minutes: 45 + strategy: + fail-fast: false + matrix: + include: + - { os: ubuntu-24.04, toolchain: 'gcc@16.1.0' } + - { os: ubuntu-24.04, toolchain: 'llvm@22.1.8' } + - { os: macos-14, toolchain: 'llvm@20.1.7' } + - { os: windows-2022, toolchain: 'llvm@20.1.7' } + - { os: windows-2022, toolchain: 'msvc@system' } + defaults: + run: + shell: bash steps: - uses: actions/checkout@v4 - - name: Install xlings + - name: Install xlings and mcpp (Unix) + if: runner.os != 'Windows' run: | curl -fsSL https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.sh \ | bash -s "$XLINGS_VERSION" echo "$HOME/.xlings/subos/current/bin" >> "$GITHUB_PATH" + - name: Install xlings and mcpp (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: | + irm https://d2learn.org/xlings-install.ps1.txt | iex + # The installer amends the user's environment; a later step in this + # job reads none of it, so the directory is named here. + "$env:USERPROFILE\.xlings\subos\current\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + - name: Install mcpp run: | xlings update @@ -35,9 +77,76 @@ jobs: mcpp --version mcpp self config --mirror GLOBAL - - name: The declarations compile + # The compiler family and version for this row. mcpp keeps its toolchains + # in a sandbox of its own, so this selects rather than installs into the + # system, and `mcpp test' and `mcpp run' have no flag for it --- which is + # why it is set once here rather than passed to each command. + - name: Select the toolchain + run: | + spec='${{ matrix.toolchain }}' + case "$spec" in + msvc*) mcpp toolchain default msvc ;; + *) mcpp toolchain install "${spec%@*}" "${spec#*@}" + mcpp toolchain default "$spec" ;; + esac + mcpp toolchain list + + # The C++ form. The modules are the artefact a C++ consumer imports, and + # building them is what proves the compiler accepts them. + - name: The module form compiles run: mcpp build + # The C form, with the environment's own headers excluded --- because the + # consumer this form exists for, a C library being ported onto openkal, is + # compiled that way. The tool needs a driver it can pass -nostdinc to; the + # toolchain that has no such spelling compiles the same declarations in + # the translation unit the conformance suite carries, which every row of + # the conformance job below builds. + - name: The C form compiles without the environment's headers + if: runner.os != 'Windows' + run: | + CC=cc bash tools/check-declarations.sh + command -v clang >/dev/null && CC=clang bash tools/check-declarations.sh || true + + # --------------------------------------------------------------------------- + # The property the specification exists for. + substitution: + name: substitution holds (${{ matrix.toolchain }}) + runs-on: ubuntu-24.04 + timeout-minutes: 40 + strategy: + fail-fast: false + matrix: + toolchain: ['gcc@16.1.0', 'llvm@22.1.8'] + steps: + - uses: actions/checkout@v4 + + - name: Install xlings + run: | + curl -fsSL https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.sh \ + | bash -s "$XLINGS_VERSION" + echo "$HOME/.xlings/subos/current/bin" >> "$GITHUB_PATH" + + - name: Install mcpp + run: | + xlings update + xlings install "mcpp@$MCPP_VERSION" -y -g + mcpp self config --mirror GLOBAL + + # The compiler family and version for this row. mcpp keeps its toolchains + # in a sandbox of its own, so this selects rather than installs into the + # system, and `mcpp test' and `mcpp run' have no flag for it --- which is + # why it is set once here rather than passed to each command. + - name: Select the toolchain + run: | + spec='${{ matrix.toolchain }}' + case "$spec" in + msvc*) mcpp toolchain default msvc ;; + *) mcpp toolchain install "${spec%@*}" "${spec#*@}" + mcpp toolchain default "$spec" ;; + esac + mcpp toolchain list + - name: Substitution holds working-directory: examples/substitution/app run: | @@ -46,12 +155,14 @@ jobs: # second, so that a change made by either build would be detected. before="$(sha256sum src/main.cpp | cut -d' ' -f1)" - mcpp run > with-fd.log 2>&1 + mcpp build > /dev/null + ./target/*/*/bin/app > with-fd.log 2>&1 grep -q 'the application produced this line' with-fd.log sed -i 's|openkal-fd = { path = "../impl-fd" }|openkal-discard = { path = "../impl-discard" }|' mcpp.toml rm -rf target - mcpp run > with-discard.log 2>&1 + mcpp build > /dev/null + ./target/*/*/bin/app > with-discard.log 2>&1 if grep -q 'the application produced this line' with-discard.log; then echo "the discarding implementation produced output"; exit 1 fi @@ -77,35 +188,104 @@ jobs: fi rm -f src/extra.cpp - # The specification is a claim about programs, and a claim about programs - # is tested by running one. The implementation is checked out at its own - # head rather than named by version, so that this step asserts what it is - # for: that the specification as written here and an implementation as - # written there agree today. - # - # Two rules in clause 7 were added because this program was written and - # run. Neither was visible in the specification text. + # --------------------------------------------------------------------------- + # The suite, against the implementation for each system. + # + # The implementations are checked out at the branch under test where they have + # one and at their default branch otherwise, so that this job asserts what it + # is for: that the specification as written here and the implementations as + # written there agree today. + conformance: + name: conformance (${{ matrix.implementation }}, ${{ matrix.toolchain }}) + runs-on: ${{ matrix.os }} + timeout-minutes: 60 + strategy: + fail-fast: false + matrix: + include: + - { os: ubuntu-24.04, toolchain: 'gcc@16.1.0', implementation: openkal-linux } + - { os: ubuntu-24.04, toolchain: 'llvm@22.1.8', implementation: openkal-linux } + - { os: macos-14, toolchain: 'llvm@20.1.7', implementation: openkal-macos } + - { os: windows-2022, toolchain: 'llvm@20.1.7', implementation: openkal-windows } + - { os: windows-2022, toolchain: 'msvc@system', implementation: openkal-windows } + defaults: + run: + shell: bash + steps: - uses: actions/checkout@v4 - with: - repository: mcpplibs/openkal-linux - path: .impl - - - name: The portable program runs above an implementation - run: | - cat > examples/portable/mcpp.toml <<'TOML' - [package] - name = "portable" - version = "0.1.0" - - [dependencies] - openkal = { path = "../.." } - openkal-linux = { path = "../../.impl" } - TOML - # The implementation names the specification by version; here it is - # the working tree that is under test, so the path is substituted. - sed -i 's|^openkal = ".*"$|openkal = { path = ".." }|' .impl/mcpp.toml || true - cd examples/portable - mcpp run 2>&1 | tee run.log - grep -q 'openkal: the portable program, above eight interfaces' run.log - grep -q 'openkal: observations that did not hold: 0' run.log - ! grep -q 'NOT HELD' run.log + + - name: The implementation + run: | + git clone --quiet https://github.com/mcpplibs/${{ matrix.implementation }}.git .impl + branch='${{ github.head_ref || github.ref_name }}' + if git -C .impl rev-parse --verify --quiet "origin/$branch" > /dev/null; then + git -C .impl checkout --quiet "origin/$branch" + echo "the implementation is at $branch" + else + echo "the implementation has no $branch; its default branch is used" + fi + + - name: Install xlings and mcpp (Unix) + if: runner.os != 'Windows' + run: | + curl -fsSL https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.sh \ + | bash -s "$XLINGS_VERSION" + echo "$HOME/.xlings/subos/current/bin" >> "$GITHUB_PATH" + + - name: Install xlings and mcpp (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: | + irm https://d2learn.org/xlings-install.ps1.txt | iex + "$env:USERPROFILE\.xlings\subos\current\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + + - name: Install mcpp + run: | + xlings update + xlings install "mcpp@$MCPP_VERSION" -y -g + mcpp self config --mirror GLOBAL + + # The compiler family and version for this row. mcpp keeps its toolchains + # in a sandbox of its own, so this selects rather than installs into the + # system, and `mcpp test' and `mcpp run' have no flag for it --- which is + # why it is set once here rather than passed to each command. + - name: Select the toolchain + run: | + spec='${{ matrix.toolchain }}' + case "$spec" in + msvc*) mcpp toolchain default msvc ;; + *) mcpp toolchain install "${spec%@*}" "${spec#*@}" + mcpp toolchain default "$spec" ;; + esac + mcpp toolchain list + + # Every interface and every kind of examination. The exit status is the + # verdict: 0 when every observation held, 1 when one did not, and 2 when + # nothing was observed --- the last being the outcome a run that selected + # no interface would otherwise pass silently. + - name: Every interface, every kind of examination + run: | + bash tools/run-conformance.sh '${{ matrix.implementation }}' .impl full + + # The suite is composable because openkal is: an implementation provides + # an interface in whole or not at all, and a suite that examined all eight + # unconditionally would fail to link against a conforming implementation + # of three. Selecting three is therefore asserted to produce a report + # rather than a link failure. + - name: A selection of three interfaces is examined, not refused + run: | + rm -rf conformance/target + bash tools/run-conformance.sh '${{ matrix.implementation }}' .impl core,fs,task \ + | tee selected.log + + # A report was produced, and nothing in it failed. + grep -qE 'observations: [0-9]+ held, 0 did not hold' selected.log + + # An interface that was not selected is reported as not examined and + # carries the reason, rather than being absent --- a report that + # omitted it could not be distinguished from a report on an + # implementation that provides it. + grep -q 'openkal.process --- the interface was not selected' selected.log + + # And an interface that was selected was examined. + grep -qE 'held +\[behaviour\].*' selected.log diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..a87f539 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "clangd.path": "/home/speak/.xlings/data/xpkgs/xim-x-llvm-tools/22.1.8/bin/clangd", + "clangd.arguments": [] +} \ No newline at end of file diff --git a/README.md b/README.md index 716427f..c76b655 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,8 @@ # openkal openkal is a portable kernel application binary interface. This repository -contains the normative specification and the C++ modules that declare it. +contains the normative specification, the declarations in both of the forms it +distributes them in, and the suite an implementation runs against itself. The specification is [`SPEC.md`](SPEC.md). @@ -11,17 +12,42 @@ Declarations, and no definitions. Every function declared here is supplied by an implementation package; building this package alone produces a library with undefined references, which is the intended outcome. -| Module | Interface | Class | -| --- | --- | --- | -| `openkal.types` | machine word, error values, transfer result | — | -| `openkal.abort` | termination | core | -| `openkal.stream` | byte streams | core | -| `openkal.memory` | allocation | core | -| `openkal.env` | the parameters a program receives at inception | standard | -| `openkal.time` | monotonic and wall time sources | standard | -| `openkal.fs` | directories and open files, relative throughout | standard | -| `openkal.process` | starting a program and waiting for it | standard | -| `openkal.task` | execution contexts, and the primitive they are built upon | standard | +| Module | Header | Interface | Class | +| --- | --- | --- | --- | +| `openkal.types` | `openkal/types.h` | machine word, error values, transfer result | — | +| `openkal.abort` | `openkal/abort.h` | termination | core | +| `openkal.stream` | `openkal/stream.h` | byte streams | core | +| `openkal.memory` | `openkal/memory.h` | allocation | core | +| `openkal.env` | `openkal/env.h` | the parameters a program receives at inception | standard | +| `openkal.time` | `openkal/time.h` | monotonic and wall time sources | standard | +| `openkal.fs` | `openkal/fs.h` | directories and open files, relative throughout | standard | +| `openkal.process` | `openkal/process.h` | starting a program and waiting for it | standard | +| `openkal.task` | `openkal/task.h` | execution contexts, and the primitive they are built upon | standard | + +### One statement of the declarations, two ways to reach it + +The contract is a C application binary interface, and a C translation unit has +no `import`. The canonical consumer of this contract — a C library ported onto +openkal — *is* a C translation unit, so the declarations are distributed in both +forms. + +The header is the statement. The module includes it in its global module +fragment and exports the names, so a consumer that imports and a consumer that +includes obtain the same entities rather than two declarations that agree today. +What the module adds is not a second declaration: it is what C++ can check and C +cannot — the layouts clause 5.3 freezes are asserted there, and the capability +words become types that cannot be mixed. + +`SURFACE.txt` is normative, and both forms are compared against it: + +| | | +| --- | --- | +| `tools/check-declarations.sh` | compiles a translation unit naming every entity, with `-nostdinc` — because the consumer the header exists for is compiled that way | +| a test in each implementation | the same list, reached through `import openkal.*` | +| `conformance/src/declarations.c` | the same list again, compiled by every toolchain that builds the suite | + +The header includes nothing. openkal must be usable on a freestanding target, +and a consumer compiled with `-nostdinc` has nothing to include. ## How a program uses openkal @@ -31,10 +57,16 @@ conditional on the target. ```toml [dependencies] -openkal = "0.3.0" +openkal = "0.5.0" [target.'cfg(os = "linux")'.dependencies] -openkal-linux = "0.3.0" +openkal-linux = "0.5.0" + +[target.'cfg(os = "macos")'.dependencies] +openkal-macos = "0.3.0" + +[target.'cfg(windows)'.dependencies] +openkal-windows = "0.1.0" ``` The program imports the interface and names no implementation. @@ -49,21 +81,64 @@ int main() { } ``` -Changing the implementation is a change to the second dependency. The source +Changing the implementation is a change to one line of the manifest. The source does not change, and this property is the reason the specification exists. ## How an implementation is written -An implementation provides definitions and no modules. It imports the same -interface a consumer imports, because it needs the declarations it is defining, -and it exports nothing: the interface belongs to this package. The reference -implementation is -[`openkal-linux`](https://github.com/mcpplibs/openkal-linux), which is -maintained as a worked example in addition to being usable. +An implementation provides definitions and no modules. It reaches the +declarations it is defining and exports nothing: the interface belongs to this +package. + +| | | +| --- | --- | +| [`openkal-linux`](https://github.com/mcpplibs/openkal-linux) | on the kernel's own system-call interface | +| [`openkal-macos`](https://github.com/mcpplibs/openkal-macos) | on the kernel's own calls, and two names no C library defines | +| [`openkal-windows`](https://github.com/mcpplibs/openkal-windows) | on Win32 and the object manager beneath it, using no C runtime symbol | + +Clause 4 states how the declarations are organised, clause 6 states how the +absence of an interface is expressed, and clause 7 states the requirements an +implementation must satisfy. + +## Conformance + +Clause 9 has two halves, and both are here. + +**The artefact.** `tools/check-surface.sh` compares an implementation's exported +names against `SURFACE.txt`. It detects the one freedom an implementation retains +after the language has removed the others: the addition of names. + +**The behaviour.** [`conformance/`](conformance/) is a program an implementation +runs against itself — 97 observations across eight interfaces, in four kinds: +behaviour, ABI, stability and cost. + +```bash +# from an implementation's working tree +bash /path/to/openkal/tools/run-conformance.sh openkal-linux . full +``` + +It is composable, because openkal is: an implementation provides an interface in +whole or not at all, so each interface is a feature and a run reports on what was +selected. It reports three counts, and the third is the one to read — +`97 held, 0 did not hold, 0 not observed` — because a suite that reported only +the first two cannot distinguish an interface that behaved from one it never +examined. + +It depends on openkal and the language. There is no `import std`: the suite must +run against an implementation in a program that carries no other runtime, and a +suite resting on facilities that implementation may be the only supplier of would +be reporting on itself. + +## What is built, and by what + +Three compiler families across three systems, because a contract that holds only +under the compiler its author used is a description of that compiler. -Clause 4 of the specification states how the modules are organised, clause 6 -states how the absence of an interface is expressed, and clause 7 states the -requirements an implementation must satisfy. +| | Linux | macOS | Windows | +| --- | --- | --- | --- | +| gcc | ✓ | — | ✓ (PE, GNU CRT) | +| llvm | ✓ | ✓ | ✓ (MSVC ABI) | +| msvc | — | — | ✓ | ## License diff --git a/SPEC.md b/SPEC.md index 529bb41..4e9e806 100644 --- a/SPEC.md +++ b/SPEC.md @@ -1,10 +1,13 @@ -# openkal Specification, version 0.4 +# openkal Specification, version 0.5 ## 1. Scope openkal defines an interface between a program and the environment that -executes it. The interface is stated as a C application binary interface and is -distributed as a set of C++ modules that declare it. +executes it. The interface is stated as a C application binary interface. It is +distributed as a set of C headers that declare it and a set of C++ modules that +export those declarations, so that a consumer written in either language +reaches the same entities. Clause 4.2 records why the second way of reaching +them is not a convenience. The specification has two audiences. An *implementation* supplies the functions declared here and is judged by whether it can do so without constructing a @@ -40,7 +43,7 @@ provides an interface in whole or not at all. | `openkal.task` | an execution context, and a suspension primitive | standard | | `openkal.event` | readiness of a set of resources | reserved | -Version 0.3 specifies the core and standard interfaces. The optional rows are +Version 0.5 specifies the core and standard interfaces. The optional rows are specified; the reserved row is not, and its name shall not be used for other purposes. @@ -89,17 +92,25 @@ all yield a byte stream. That decomposition was withdrawn for three reasons. The stream is therefore the shared currency of the specification and not its common entrance. -## 4. Module organisation +## 4. Organisation of the declarations -The specification package provides one module per interface. An implementation -provides no module at all. +The specification package provides one header per interface, which is where the +declarations are, and one module per interface, which exports them. An +implementation provides neither. -| Module | Provided by | Imported by | +| Interface | Module | Header | | --- | --- | --- | -| `openkal.types` | the specification package | the other interface modules | -| `openkal.abort` | the specification package | consumers, implementations | -| `openkal.stream` | the specification package | consumers, implementations | -| `openkal.memory` | the specification package | consumers, implementations | +| shared definitions | `openkal.types` | `openkal/types.h` | +| `openkal.abort` | `openkal.abort` | `openkal/abort.h` | +| `openkal.stream` | `openkal.stream` | `openkal/stream.h` | +| `openkal.memory` | `openkal.memory` | `openkal/memory.h` | +| `openkal.env` | `openkal.env` | `openkal/env.h` | +| `openkal.time` | `openkal.time` | `openkal/time.h` | +| `openkal.fs` | `openkal.fs` | `openkal/fs.h` | +| `openkal.process` | `openkal.process` | `openkal/process.h` | +| `openkal.task` | `openkal.task` | `openkal/task.h` | + +`openkal.h` includes every header, for a consumer that uses several. A consumer imports the interface. An implementation imports the same interface, because it needs the declarations it is defining, and it exports nothing. @@ -121,7 +132,81 @@ not a restriction that must be enforced; it follows from the arrangement. An implementation may of course publish additional facilities, and it does so in its own modules, which a consumer that uses them must import by name. -### 4.2 Absence of an implementation +### 4.2 One statement of the declarations, two ways to reach it + +The contract is a C application binary interface. The specification package +states it once, as a C header per interface, and provides a C++ module per +interface that includes that header in its global module fragment and exports +the names it declares. + +The second way of reaching the declarations is not a convenience. A C++ +consumer imports the module for the interface it uses; a consumer written in C +cannot, because a C translation unit has no import. The canonical consumer +written in C is a C library being ported onto openkal, which is the case clause +1 names first, so a specification reachable only by import would be unusable by +the consumer it exists for. Version 0.4 was reachable only by import, and the +omission was found by attempting that port. + +The arrangement is deliberately not two statements that agree. A module that +re-declared what the header declares would be a second declaration of the same +contract, and two declarations drift; here there is one declaration, and the +module exports it. A consumer that imports and a consumer that includes +therefore obtain the same entities, and no procedure is required to keep them +equal because they are not two things. + +The headers include no header of their own, name no library, and obtain the +width of a machine word from the compiler. The second way of reaching the +declarations therefore adds nothing to what the specification package requires, +which remains nothing. + +#### What the module adds + +The module is not a translation of the header. It carries what C++ can check +and C cannot, and both are checks rather than facilities. + +**The frozen layouts are asserted.** Clause 5.3 declares the layout of every +structure immutable. A declaration that something shall not change is not a +mechanism; `static_assert` is. `kal_io_result` being two machine words is the +difference between a result returned in registers and one returned through a +hidden pointer, which is a change of calling convention that no declaration +would report and that a consumer built against the earlier layout would not +survive. + +**The capability words become types that cannot be mixed.** Clause 6.2 gives +each interface a word and positions within it, and every such word is a +`kal_uintptr`. A program that tests a file-system position against the task +word therefore compiles, runs, and answers a question nobody asked; the +position numbers are small and several interfaces have assigned the same ones, +so the answer is frequently the plausible one. In the module each interface's +positions carry the interface in their type, the operations that compose them +are defined only within one interface, and the mistake becomes a diagnostic. +Nothing is stored beyond the word and every operation is constant-evaluated. + +The same reasoning applies to the flags of `kal_fs_open`, which are an intent +rather than a property and are a distinct type from either. + +#### Arrangements considered and not adopted + +**Two declarations, compared by a procedure.** Not adopted: it is the +arrangement this clause exists to avoid, and a procedure that compares them is +a thing that can be omitted, skipped or made to pass. + +**Generating the header from the modules.** Not adopted: it makes the module +form normative, which contradicts clause 1, and it introduces a generator, +which is a build-time dependency the specification package is written to avoid +having. + +**Leaving the C form to each implementation.** Not adopted for the reason +clause 4.1 gives: a name the consumer relies upon would be under the control of +a party the specification does not govern, and a C library ported onto openkal +would take its declarations from whichever implementation it happened to be +built against. + +**A second package carrying the C form.** Not adopted: one contract in two +packages can be resolved at two versions, and the property a contract has is +that there is one of it. + +### 4.3 Absence of an implementation A consumer that depends upon the specification and upon no implementation compiles and fails to link, and the diagnostic names the undefined functions: @@ -155,6 +240,21 @@ environment's own error value was considered and rejected: such a channel is global mutable state with the defects of `errno`, and control flow that consults it is not portable. +Version 0.5 adds five values, and records why, because the set being closed +makes an addition to it the kind of change a reader is entitled to see argued. + +| Value | Why the set could not express it | +| --- | --- | +| `kal_err_not_found` | Clause 7.7 already required it. The value did not exist, so both implementations reported a missing name as `kal_err_invalid`, and a caller could not distinguish a name that is absent from a handle that is wrong. The defect was in the enumeration rather than in the implementations. | +| `kal_err_exists` | `kal_fs_open` with `exclusive`, and `kal_fs_mkdir`, fail because the name is already there. It is an expected outcome, and mapping it to `kal_err_io` would report a medium failure for one. | +| `kal_err_not_empty` | Removing a directory that is not empty. Same reasoning. | +| `kal_err_is_directory` | A file operation applied to a directory. | +| `kal_err_not_directory` | A directory operation applied to a file, and a component of a name that is not a directory. | + +The addition is governed by clause 8, which admits new declarations. A value +already assigned retains its meaning, so a program compiled against version 0.4 +observes the same values for the same conditions as before. + ### 5.3 Structure layouts The layout of every structure declared by this specification is frozen at @@ -194,6 +294,14 @@ position that has not been assigned reads as zero, so that a program compiled against a later specification behaves correctly against an earlier implementation. +A property that varies between the *resources* of an interface rather than +between implementations cannot be a word, because there is no one answer to +record in it. Such a property is reported by an enquiry taking the resource. +`kal_stream_props` is the example: the same implementation answers differently +for a terminal and for a file. The enquiry is not the defect clause 6.4 +describes, because it is not an operation upon the resource --- nothing is +transferred, and no resource can fail to answer. + Information therefore becomes available at three times, each being the earliest at which it exists. @@ -364,19 +472,115 @@ implementation that reports absence as a failure obliges every caller to distinguish that failure from the ones that denote a broken enquiry — a directory it may not read, a name it may not resolve. -`kal_fs_open_file` and `kal_fs_open_dir` are access rather than enquiry, and -shall report a name that does not exist as `kal_err_not_found`. +`kal_fs_open_file`, `kal_fs_open` and `kal_fs_open_dir` are access rather than +enquiry, and shall report a name that does not exist as `kal_err_not_found`. The distinction is the same one `openkal.env` draws between a variable that is absent and one whose value is empty, and it is drawn for the same reason: a caller that cannot tell them apart cannot act correctly upon either. -### 7.8 Termination +### 7.8 Stating the whole of an intent when opening + +`kal_fs_open` takes a flags word. The two-flag form of version 0.3 remains +specified and remains available, and an implementation ordinarily defines it in +terms of the flags form. + +The reason for the addition is that three of the conditions a C library must +express cannot be reconstructed above the two-flag form without making the +caller silently wrong, which clause 3.1 identifies as the boundary between a +supply and a simulation. + +| Condition | What reconstruction above the two-flag form produces | +| --- | --- | +| truncate | Opening and then setting the length is two operations. A program that stops between them leaves the tail of a longer previous contents behind, and the file is neither the old one nor the new one. | +| exclusive | Enquiring and then opening is not exclusion. Between the two, another context creates the name, and the caller that asked to be the creator is not. | +| append | Seeking to the end and then writing is not appending. A second writer between the two produces an overwrite, and neither writer observes that it happened. | + +`kal_fs_truncate` and `kal_fs_file_info` are added for the same reason and are +recorded here rather than in a list of conveniences. Setting the length of an +open file is not expressible through any other operation. Enquiring about an +open file is not expressible through `kal_fs_info`: the name a file was opened +by may since have been removed or given to another file, so a C library +answering the enquiry from the name would answer about a different file than +the one the caller holds. + +### 7.9 Termination `kal_exit` shall terminate immediately. Registered exit handlers and static destructors shall not run. An implementation that runs them prevents a caller from reasoning about what executes after the call. +### 7.10 Thread-local storage in a started context + +`openkal.task` reports, through `kal_task_props`, whether a context started by +`kal_task_start` observes the thread-local storage of the toolchain that +compiled the program. + +It is reported rather than provided. The register convention that delivers +thread-local storage belongs to openarch (clause 10), and an operation here +that installed a thread pointer would place a processor's calling convention in +an interface that is meant to be independent of it. + +The property is reported because a C library ported onto openkal keeps its +per-context state --- its error value, its locale, its cancellation state --- in +one such variable, and therefore cannot be ported onto an implementation whose +contexts lack it. Reporting the property allows the library to state that +requirement. An implementation whose contexts are the host's threads has the +property without doing anything; an implementation upon a scheduler of its own +has it only if it establishes the convention, and one that does not shall report +the position as zero rather than leave the question to be discovered. + +### 7.11 The inverse of an enquiry + +An interface that reports a property of a resource and offers no way to set it +is incomplete wherever the property is one the environment records rather than +derives. + +`kal_fs_file_info` reports the time at which a file was last modified. +`kal_fs_set_modified` sets it. The operation was added in version 0.5 because +three ordinary programs — one that copies a file and preserves its dates, one +that extracts an archive, and one whose whole purpose is to mark a file as +current — could not be written above the interface without it, and each of them +is a program a C library above openkal is expected to host. The absence was not +visible in the specification text; it became visible when such programs were +compiled above an implementation. + +It takes the open file rather than the name, for the reason given at +`kal_fs_file_info`: the name may since refer to something else, and setting the +time of the wrong file is a worse outcome than not setting it. + +The three environments openkal has been implemented on record the time to a +nanosecond, to a microsecond and to a hundred nanoseconds respectively. The +interface states the value in nanoseconds and does not require that it be +returned unchanged; the conformance procedure compares whole seconds, which is +the resolution every environment that records the time at all agrees upon. An +interface that required more would be requiring of every environment what one of +them happens to provide. + +An implementation whose environment does not record the time at all does not +claim `KAL_FS_PROP_MODIFIED_TIME` and reports `kal_err_not_supported`. An +implementation that claims the position is required to perform the operation: +clause 6.2 exists so that a claim is a claim about what can be done. + +### 7.12 One reserved name + +`openkal.fs` names things relative to a directory the program holds, and every +operation that answers a question about a thing takes a name. A program holding +a directory therefore had no way to ask a question about *that* directory: what +it is, when it last changed, whether it can be written. The handle is not a +name, and no operation took a handle alone. + +The remedy is one reserved word rather than five more operations. `"."` denotes +the directory itself, wherever a name is accepted. It does not introduce a way +to ascend, so the confinement clause 7.1 depends upon is unaffected; and every +environment openkal has been implemented on can express it, two of them because +their own naming already reserves the same word and the third because its +object manager expresses the same thing as an empty name beside the directory's +handle. + +`".."` remains invalid. The asymmetry is the point: the first names the thing +the program already holds, and the second names something it does not. + ## 8. Evolution Each interface is versioned independently. A revision may add declarations and @@ -392,7 +596,8 @@ source. ## 9. Conformance procedure -A conformance suite shall verify both halves of an implementation's claim. +A conformance suite shall verify both halves of an implementation's claim, and +the specification package shall verify the third. 1. **Behaviour.** Every operation the implementation declares shall behave as this specification requires. @@ -403,6 +608,15 @@ A conformance suite shall verify both halves of an implementation's claim. coverage, and it is the half that detects an implementation which has extended the interface rather than implemented it. +3. **Declarations.** The declarations, clause 4.2, shall be compared against + `SURFACE.txt` by compiling a translation unit that names every entity the + list contains. This half belongs to the specification package rather than to + an implementation. It does not compare the two ways of reaching the + declarations with each other, because there is one declaration and no + comparison to make; what it detects is a name the list requires and the + header does not declare. The translation unit is compiled without the + environment's headers, because the consumer the header exists for is. + Behavioural conformance cannot be established exhaustively, and this specification does not claim otherwise. An implementation may export the correct names, satisfy every test, and fail on an untested input. The residue is the diff --git a/SURFACE.txt b/SURFACE.txt index 3d84978..481e893 100644 --- a/SURFACE.txt +++ b/SURFACE.txt @@ -1,4 +1,4 @@ -# The C surface of openkal 0.3, one name per line. +# The C surface of openkal 0.5, 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 @@ -15,6 +15,7 @@ kal_stderr kal_stdin kal_stdout kal_stream_flush +kal_stream_props kal_stream_read kal_stream_write # openkal.memory @@ -35,10 +36,12 @@ kal_time_wall # openkal.fs kal_fs_close_dir kal_fs_close_file +kal_fs_file_info kal_fs_info kal_fs_list_begin kal_fs_list_next kal_fs_mkdir +kal_fs_open kal_fs_open_dir kal_fs_open_file kal_fs_props @@ -47,7 +50,9 @@ kal_fs_rename kal_fs_preopen kal_fs_preopen_count kal_fs_seek +kal_fs_set_modified kal_fs_stream +kal_fs_truncate # openkal.process kal_process_close kal_process_props diff --git a/conformance/.gitignore b/conformance/.gitignore new file mode 100644 index 0000000..ed77b72 --- /dev/null +++ b/conformance/.gitignore @@ -0,0 +1,4 @@ +target/ +mcpp.lock +*.tmp +okc-conformance* diff --git a/conformance/README.md b/conformance/README.md new file mode 100644 index 0000000..2e85234 --- /dev/null +++ b/conformance/README.md @@ -0,0 +1,152 @@ +# openkal-conformance + +The behavioural half of clause 9, as a program an implementation runs against +itself. + +```bash +# from an implementation's working tree +mcpp build --features full +./target/*/*/bin/openkal-conformance +``` + +The implementation under examination is not named in this package's manifest. +It is supplied by whoever runs the suite, exactly as any consumer supplies one — +and `tools/run-conformance.sh` in the parent directory is where that is done, so +that a change to how it is done is one change: + +```bash +git clone https://github.com/mcpplibs/openkal .spec +bash .spec/tools/run-conformance.sh openkal-linux . full +# ^ the package ^ where it is +``` + +This package is not published to the index separately. It is a program rather +than a library — a dependency that contributed a binary target to its dependant +would be a surprising thing for `[dependencies]` to name — and it is reached by +checking out the specification, which is the package it is the conformance +procedure for. + +## It is composable, because openkal is + +An interface is the unit of provision. An implementation provides one in whole +or not at all, and one it does not provide is absent as a link-time definition — +so a suite that examined all eight unconditionally would fail to link against a +conforming implementation of five, and would report nothing at all rather than +reporting five. + +Each interface is therefore a feature. + +| | | +| --- | --- | +| `mcpp build` | the core set: `abort`, `stream`, `memory` | +| `mcpp build --features standard` | every interface version 0.5 defines | +| `mcpp build --features fs,task` | the core set, and these two | +| `mcpp build --features full` | every interface and every kind of examination | + +The core set is the default because clause 3 says every implementation provides +it, and there is no arrangement that examines nothing: a run that observed +nothing exits 2 rather than 0. + +`process` implies `fs`, because a program is started relative to a directory. +`cost` implies `time`, because a cost is a duration. + +## Four kinds of examination + +| kind | what it asks | selected by | +| --- | --- | --- | +| behaviour | does the operation do what the specification requires | always | +| abi | are the shapes clause 5.3 freezes the shapes the linked implementation has, and does each capability word contain only positions the specification has assigned | `--features abi` | +| stability | does the operation still work after twenty thousand of it | `--features stability` | +| cost | how long does one take | `--features cost` | + +A cost is reported and never asserted. A number that varies with the machine +cannot be a verdict, and a suite that made it one would fail on a loaded runner +and be discarded rather than consulted. + +## Three counts, and the third is the one to read + +``` +observations: 97 held, 0 did not hold, 0 not observed +``` + +A suite that reported only what held and what did not cannot distinguish an +interface that behaved from an interface it never examined, and a run in which +nothing was examined then reports success. Every unmade observation therefore +carries the reason it was not made: + +``` + not observed [behaviour] a wait honours a timeout --- the implementation + does not claim prop_wait_timeout +``` + +## What the observations are written to catch + +The suite is written on the assumption that an implementation which is nearly +right passes a suite which is nearly written. Several observations exist because +of a specific way of being nearly right. + +**A return value is not an effect.** The three conditions `kal_fs_open` exists +to express are observed by reading the file afterwards. A truncation that did +not happen leaves a longer file, an exclusion that did not happen succeeds, and +an append that did not happen overwrites — and in all three cases every call +returns `kal_ok`. + +**A control that can distinguish.** Clause 7.6 requires the argument vector to be +passed unaltered. The suite starts a copy of itself with the first element the +copy expects, and the copy reports through its status that it saw it. It then +starts a second copy with a *different* first element and requires the copy to +report *that*. Without the second, an implementation that ignored the vector +entirely and passed a fixed name would satisfy the first. + +**An identity is compared with its peers, not with the observer.** The suite +requires the identities of four contexts that ran at the same time to be distinct +*from each other*. Comparing each with the starting context's identity is +satisfied by an implementation that answers one wrong value for all of them — and +one did: it answered zero for every context it started, which differs from the +starting context's identity and is useless to a consumer, because zero is what a +table keyed on the identity reads as "no entry". + +**A claim is checked.** An implementation that claims `prop_wait_timeout` is +required to have a wait that times out; one that claims `prop_thread_local` is +required to give a started context its own instance of a `thread_local` +variable; one that claims `prop_exit_status` is required to report the value the +started program returned. A claim nobody checks is a comment. + +**A count, not an outcome.** Four contexts each increment a counter twenty +thousand times under a mutex the suite builds from the suspension primitive. The +observation is that the sum is eighty thousand. A mutex that is unsound loses +increments and produces no other symptom. + +**Repetition.** An implementation that packs a generation into a handle and +never reclaims the index satisfies every observation that opens one file. It +stops at twenty thousand. + +## What it depends upon + +openkal, and the language. + +The report is written through `openkal.stream`, the durations come from +`openkal.time`, and there is no `import std`. That is not austerity: the suite +must run against an implementation in either arrangement, including one in a +program that carries no other runtime, where a standard library would not link — +and where it did link, the suite would be reporting on an implementation while +resting on facilities that implementation may be the only supplier of. + +The price is the formatting in `okc.report`, and it is sixty lines. + +## Layout + +One module per concern, interface and implementation separated, as the mcpp +benchmark harness is arranged. + +| | | +| --- | --- | +| `src/spec.cppm` | what is examined, expressed as data; the one place that reads the selection macros | +| `src/report.cppm` `.cpp` | the three states, the counts, and the formatting | +| `src/suite.cppm` `.cpp` | the driver: what runs and in what order | +| `src/sections/*.cppm` `.cpp` | one section per interface | +| `src/sections/child.cppm` `.cpp` | the copy the suite starts, for the two requirements that end the program that satisfies them | +| `src/main.cpp` | the entry | + +Adding an interface to the specification is a section and a row in +`src/spec.cppm`. It is not an edit to the driver or to the report. diff --git a/conformance/mcpp.toml b/conformance/mcpp.toml new file mode 100644 index 0000000..320792b --- /dev/null +++ b/conformance/mcpp.toml @@ -0,0 +1,83 @@ +[package] +namespace = "mcpplibs" +name = "openkal-conformance" +version = "0.5.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"] +repo = "https://github.com/mcpplibs/openkal" + +# The specification, and nothing else. +# +# The suite does not import std. It is written to run against an implementation +# in either arrangement, including one in a program that carries no other +# runtime, and a suite that reached for a standard library would not link there +# --- and where it did link, it would be reporting on an implementation while +# resting on facilities that implementation may be the only supplier of. +# +# 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.5.0" + +# The implementation under examination is not named here. +# +# It is supplied by whoever runs the suite, as a second dependency, exactly as +# any consumer supplies one. An implementation's own continuous integration adds +# a path to itself; a reader examining a published implementation adds its +# version. Naming one here would make the suite an implementation's dependant +# rather than an implementation's instrument. + +[targets.openkal-conformance] +kind = "bin" +main = "src/main.cpp" + +# What is examined, selected the way openkal is provided. +# +# An interface is the unit of provision: an implementation provides one in whole +# or not at all, and one it does not provide is absent as a link-time +# definition. A suite that examined every interface unconditionally would +# therefore fail to link against a conforming implementation of five of them, +# and would report nothing at all rather than reporting five. +# +# So each interface is a feature. A section whose feature is not active is +# compiled with its body removed and reports itself as not examined, which keeps +# the count of what was skipped in the report rather than in the reader's head. +# +# mcpp run the core set +# mcpp run --features standard every interface version 0.5 defines +# mcpp run --features fs,task the core set, and these two +# +# The core set is the default because clause 3 says every implementation +# provides it. There is no arrangement that examines nothing. +[features] +default = ["core"] + +core = [] +env = [] +time = [] +fs = [] +process = ["fs"] # a program is started relative to a directory +task = [] + +standard = ["core", "env", "time", "fs", "process", "task"] + +# The kinds of examination. Behaviour is always performed; the other three are +# selected, because each costs time that a reader running the suite to answer +# "does this implementation work" does not want to spend. +# +# abi the shapes the specification freezes, checked against the +# implementation that was linked rather than against the headers +# that were compiled +# stability the same operation many times, which is where a handle scheme +# that leaks and an allocator that fragments become visible +# cost reported, never asserted: a number that varies with the machine +# cannot be a verdict, and a suite that made it one would fail on a +# loaded runner +abi = [] +stability = [] +# A cost is a duration, and a duration is openkal.time. The dependency is +# declared rather than assumed, so that requesting cost without time selects +# time instead of failing to link. +cost = ["time"] +full = ["standard", "abi", "stability", "cost"] diff --git a/conformance/src/atomic.cppm b/conformance/src/atomic.cppm new file mode 100644 index 0000000..7b03f12 --- /dev/null +++ b/conformance/src/atomic.cppm @@ -0,0 +1,105 @@ +// okc.atomic --- the four operations the suite needs, under three compilers. +// +// The suite builds a mutex out of openkal's suspension primitive, and a mutex +// needs an atomic exchange and an atomic compare-and-exchange. Two of the three +// compilers this package is built with publish those as builtins of the same +// name; the third publishes them under different names, in a different form, +// and only for a signed thirty-two-bit word. +// +// They are stated here rather than at each use, and they are stated rather than +// imported: the suite depends on openkal and the language and does not import +// std, because it must run against an implementation in a program that carries +// no other runtime. `` is a header of the standard library even where a +// freestanding implementation is required to provide it, and a suite reporting +// on an implementation while resting on facilities that implementation may be +// the only supplier of would be reporting on itself. +// +// The ordering is release on a store and acquire on a load, which is what the +// two builtins are asked for. The third compiler's operations are full barriers +// on the two architectures this package is built for, which is stronger and +// therefore also correct; there is no weaker spelling of them, and a suite is +// not the place to want one. +export module okc.atomic; + +import openkal.types; + +#if defined(_MSC_VER) && !defined(__clang__) +extern "C" { +long _InterlockedExchange(long volatile*, long); +long _InterlockedCompareExchange(long volatile*, long, long); +long _InterlockedOr(long volatile*, long); +} +#pragma intrinsic(_InterlockedExchange, _InterlockedCompareExchange, _InterlockedOr) +#endif + +export namespace okc { + +// A word, loaded so that everything the writer did before storing it is visible +// to a reader that sees it. +inline kal_u32 load_acquire(const volatile kal_u32* p) { +#if defined(_MSC_VER) && !defined(__clang__) + // An `or' of zero reads the word and writes back what was there, which is + // the shortest read this compiler offers that is atomic and ordered. + return static_cast( + _InterlockedOr(reinterpret_cast(const_cast(p)), 0)); +#else + return __atomic_load_n(p, __ATOMIC_ACQUIRE); +#endif +} + +inline int load_acquire(const volatile int* p) { +#if defined(_MSC_VER) && !defined(__clang__) + return static_cast( + _InterlockedOr(reinterpret_cast(const_cast(p)), 0)); +#else + return __atomic_load_n(p, __ATOMIC_ACQUIRE); +#endif +} + +// A word, stored so that everything done before the store is visible to a +// reader that sees it. +inline void store_release(volatile kal_u32* p, kal_u32 v) { +#if defined(_MSC_VER) && !defined(__clang__) + _InterlockedExchange(reinterpret_cast(p), static_cast(v)); +#else + __atomic_store_n(p, v, __ATOMIC_RELEASE); +#endif +} + +inline void store_release(volatile int* p, int v) { +#if defined(_MSC_VER) && !defined(__clang__) + _InterlockedExchange(reinterpret_cast(p), static_cast(v)); +#else + __atomic_store_n(p, v, __ATOMIC_RELEASE); +#endif +} + +// The old value, replaced by the new one in one indivisible step. +inline kal_u32 exchange(volatile kal_u32* p, kal_u32 v) { +#if defined(_MSC_VER) && !defined(__clang__) + return static_cast( + _InterlockedExchange(reinterpret_cast(p), static_cast(v))); +#else + return __atomic_exchange_n(p, v, __ATOMIC_ACQ_REL); +#endif +} + +// Replaces the word with `desired' if it holds `expected'. Reports whether it +// did, and leaves what it actually held in `expected' either way --- which is +// the form a mutex's acquisition loop wants and the form both compilers offer. +inline bool compare_exchange(volatile kal_u32* p, kal_u32& expected, kal_u32 desired) { +#if defined(_MSC_VER) && !defined(__clang__) + const long was = _InterlockedCompareExchange(reinterpret_cast(p), + static_cast(desired), + static_cast(expected)); + const kal_u32 seen = static_cast(was); + if (seen == expected) return true; + expected = seen; + return false; +#else + return __atomic_compare_exchange_n(p, &expected, desired, false, + __ATOMIC_ACQUIRE, __ATOMIC_RELAXED); +#endif +} + +} // namespace okc diff --git a/conformance/src/declarations.c b/conformance/src/declarations.c new file mode 100644 index 0000000..bf39848 --- /dev/null +++ b/conformance/src/declarations.c @@ -0,0 +1,82 @@ +/* The C declarations, named once, so that every toolchain that builds this + * suite compiles them. + * + * Clause 4.3 requires the two forms of the declarations to declare the same + * entities, and tools/check-declarations.sh performs that comparison against + * SURFACE.txt --- which is normative --- with the environment's own headers + * excluded. That tool needs a compiler driver it can pass -nostdinc to, and one + * of the three toolchains this suite is built with has no such spelling. So the + * comparison is performed in one place and the compiling is performed here, in + * a translation unit the suite carries: a header that fails to compile as C + * under a toolchain then fails that toolchain's build rather than going + * unnoticed until a C consumer meets it. + * + * No name is referenced in an evaluated context. The suite is composable --- an + * implementation provides an interface in whole or not at all --- so a file that + * required all fifty-one definitions would fail to link against a conforming + * implementation of three interfaces, which is the arrangement the suite exists + * to accommodate. sizeof is unevaluated, so this file declares a dependency on + * nothing while still rejecting a name the header fails to declare. + * + * Regenerated by tools/check-declarations.sh --write. It is committed rather + * than generated during the build so that the suite compiles in a checkout that + * has nothing but a compiler. + */ +#include + +void okc_declarations_c(void); +void okc_declarations_c(void) +{ + (void)sizeof(&kal_abort); + (void)sizeof(&kal_alloc); + (void)sizeof(&kal_env_arg); + (void)sizeof(&kal_env_arg_count); + (void)sizeof(&kal_env_var); + (void)sizeof(&kal_env_var_at); + (void)sizeof(&kal_env_var_count); + (void)sizeof(&kal_exit); + (void)sizeof(&kal_free); + (void)sizeof(&kal_fs_close_dir); + (void)sizeof(&kal_fs_close_file); + (void)sizeof(&kal_fs_file_info); + (void)sizeof(&kal_fs_info); + (void)sizeof(&kal_fs_list_begin); + (void)sizeof(&kal_fs_list_next); + (void)sizeof(&kal_fs_mkdir); + (void)sizeof(&kal_fs_open); + (void)sizeof(&kal_fs_open_dir); + (void)sizeof(&kal_fs_open_file); + (void)sizeof(&kal_fs_preopen); + (void)sizeof(&kal_fs_preopen_count); + (void)sizeof(&kal_fs_props); + (void)sizeof(&kal_fs_remove); + (void)sizeof(&kal_fs_rename); + (void)sizeof(&kal_fs_seek); + (void)sizeof(&kal_fs_set_modified); + (void)sizeof(&kal_fs_stream); + (void)sizeof(&kal_fs_truncate); + (void)sizeof(&kal_process_close); + (void)sizeof(&kal_process_props); + (void)sizeof(&kal_process_spawn); + (void)sizeof(&kal_process_terminate); + (void)sizeof(&kal_process_wait); + (void)sizeof(&kal_stderr); + (void)sizeof(&kal_stdin); + (void)sizeof(&kal_stdout); + (void)sizeof(&kal_stream_flush); + (void)sizeof(&kal_stream_props); + (void)sizeof(&kal_stream_read); + (void)sizeof(&kal_stream_write); + (void)sizeof(&kal_task_current); + (void)sizeof(&kal_task_join); + (void)sizeof(&kal_task_props); + (void)sizeof(&kal_task_start); + (void)sizeof(&kal_task_wait); + (void)sizeof(&kal_task_wake); + (void)sizeof(&kal_task_yield); + (void)sizeof(&kal_time_monotonic); + (void)sizeof(&kal_time_monotonic_granularity); + (void)sizeof(&kal_time_props); + (void)sizeof(&kal_time_sleep); + (void)sizeof(&kal_time_wall); +} diff --git a/conformance/src/main.cpp b/conformance/src/main.cpp new file mode 100644 index 0000000..da92e44 --- /dev/null +++ b/conformance/src/main.cpp @@ -0,0 +1,35 @@ +// The openkal conformance suite. +// +// Clause 9 requires that both halves of an implementation's claim be verified. +// The surface half is a static examination of the artefact and is performed by +// tools/check-surface.sh; the declarations half is performed by +// tools/check-declarations.sh. This program is the behavioural half, and it +// carries three further kinds of examination that clause 9 does not require and +// that an implementer wants: the shapes the specification freezes, the same +// operation many times, and what an operation costs. +// +// It is composable in the same way openkal is. An interface is the unit of +// provision, an implementation provides one in whole or not at all, and one it +// does not provide is absent as a link-time definition --- so a suite that +// examined all eight unconditionally would fail to link against a conforming +// implementation of five, and would report nothing rather than reporting five. +// +// mcpp run the core set +// mcpp run --features standard every interface version 0.5 defines +// mcpp run --features fs,task the core set, and these two +// mcpp run --features full every interface and every kind +// +// The implementation under examination is named by whoever runs the suite, as +// a second dependency, exactly as any consumer names one. +import okc.suite; +import okc.child; + +int main() { + // Two of the specification's requirements end the program that satisfies + // them, so they are observed in a copy this program starts. A copy is told + // what to do through its argument vector and answers through its status. + const okc::errand e = okc::child_errand(); + if (e != okc::errand::none) okc::perform(e); + + return okc::run_all(); +} diff --git a/conformance/src/report.cpp b/conformance/src/report.cpp new file mode 100644 index 0000000..e591738 --- /dev/null +++ b/conformance/src/report.cpp @@ -0,0 +1,132 @@ +module okc.report; + +import openkal.types; +import openkal.stream; +import okc.spec; + +namespace okc { +namespace { + +int g_held = 0; +int g_failed = 0; +int g_unobserved = 0; + +kal_uintptr length(const char* s) { + kal_uintptr n = 0; while (s && s[n]) ++n; return n; +} + +void write(const char* s) { + if (s && *s) kal_stream_write(kal_stdout(), s, length(s)); +} + +} // namespace + +void put(const char* s) { write(s); } + +void put_signed(long long v) { + char buf[24]; + int i = static_cast(sizeof buf); + const bool negative = v < 0; + unsigned long long u = negative ? 0ull - static_cast(v) + : static_cast(v); + if (u == 0) buf[--i] = '0'; + while (u) { buf[--i] = static_cast('0' + u % 10); u /= 10; } + if (negative) buf[--i] = '-'; + kal_stream_write(kal_stdout(), buf + i, static_cast(static_cast(sizeof buf) - i)); +} + +void put_hex(kal_uintptr v) { + char buf[18]; + int i = static_cast(sizeof buf); + if (v == 0) buf[--i] = '0'; + while (v) { + const int d = static_cast(v & 15u); + buf[--i] = static_cast(d < 10 ? '0' + d : 'a' + d - 10); + v >>= 4; + } + write("0x"); + kal_stream_write(kal_stdout(), buf + i, static_cast(static_cast(sizeof buf) - i)); +} + +void observe(kind k, bool held, const char* what) { + if (held) { ++g_held; write(" held "); } + else { ++g_failed; write(" DID NOT HOLD "); } + write("["); write(name_of(k)); write("] "); + write(what); write("\n"); +} + +void unobserved(kind k, const char* what, const char* because) { + ++g_unobserved; + write(" not observed ["); write(name_of(k)); write("] "); + write(what); write(" --- "); write(because); write("\n"); +} + +void measure(const char* what, kal_u64 total_ns, int iterations) { + write(" measured [cost] "); + write(what); + write(": "); + put_signed(iterations ? static_cast(total_ns / static_cast(iterations)) : 0); + write(" ns per operation, over "); + put_signed(iterations); + write("\n"); +} + +void claim(const char* what, kal_uintptr word) { + write(" claimed "); write(what); write(" = "); put_hex(word); write("\n"); +} + +void heading(const char* text) { write("\n"); write(text); write("\n"); } +void line(const char* text) { write(text); write("\n"); } + +int held_count() { return g_held; } +int failed_count() { return g_failed; } +int unobserved_count() { return g_unobserved; } + +void write_inventory() { + line("openkal conformance suite, version 0.5.0"); + line(""); + line("interface provision examined select with"); + for (const auto& row : inventory) { + write(" "); + write(row.name); + for (kal_uintptr i = length(row.name); i < 17; ++i) write(" "); + write(row.core ? "core " : "standard "); + write(row.selected ? "yes " : "no "); + write("--features "); + write(row.feature); + write("\n"); + } + line(""); + write("kinds performed: behaviour"); + if (performs(kind::abi)) write(" abi"); + if (performs(kind::stability)) write(" stability"); + if (performs(kind::cost)) write(" cost"); + write("\n"); + for (const auto& row : inventory) { + if (row.selected) continue; + write(" openkal is composable, and this run did not examine "); + write(row.name); + write("\n"); + } +} + +int summarise() { + write("\nobservations: "); + put_signed(g_held); write(" held, "); + put_signed(g_failed); write(" did not hold, "); + put_signed(g_unobserved); write(" not observed\n"); + + // An empty run is not a passing run. Every arrangement of features selects + // at least the core set, so a run that observed nothing means the sections + // were not built, and reporting success for it would conceal exactly the + // defect this program exists to find. + if (g_held == 0 && g_failed == 0) { + line("nothing was examined; the suite was built with no section"); + return 2; + } + line(g_failed == 0 ? "the implementation conforms in every observation made" + : "the implementation does not conform"); + return g_failed == 0 ? 0 : 1; +} + +} // namespace okc diff --git a/conformance/src/report.cppm b/conformance/src/report.cppm new file mode 100644 index 0000000..67f4c40 --- /dev/null +++ b/conformance/src/report.cppm @@ -0,0 +1,47 @@ +// okc.report --- how an observation is recorded and how the run is summarised. +// +// Three states, and the third is the one that matters. A suite reporting only +// "held" and "did not hold" cannot distinguish an interface that behaved from +// an interface it never examined, and a run in which nothing was examined then +// reports success. Every unmade observation therefore carries the reason it was +// not made, and the count of them is part of the verdict a reader reads. +export module okc.report; + +import openkal.types; +import okc.spec; + +export namespace okc { + +// An observation that held, or did not. +void observe(kind k, bool held, const char* what); + +// An observation that was not made. The reason is a parameter rather than an +// option: an unexplained omission is indistinguishable from an oversight. +void unobserved(kind k, const char* what, const char* because); + +// A measurement. It is never a verdict; see okc.spec. +void measure(const char* what, kal_u64 total_ns, int iterations); + +// A note attached to the report: what the implementation claims about itself, +// written out so that a reader can compare two implementations without running +// either. +void claim(const char* what, kal_uintptr word); + +void heading(const char* text); +void line(const char* text); + +// The report's own three counts. +int held_count(); +int failed_count(); +int unobserved_count(); + +// Writes the inventory, then returns the status the program exits with. +void write_inventory(); +int summarise(); + +// Formatting, exported because the sections report values as well as verdicts. +void put(const char* s); +void put_signed(long long v); +void put_hex(kal_uintptr v); + +} // namespace okc diff --git a/conformance/src/sections/abort.cpp b/conformance/src/sections/abort.cpp new file mode 100644 index 0000000..9c4df28 --- /dev/null +++ b/conformance/src/sections/abort.cpp @@ -0,0 +1,73 @@ +module okc.abort; + +import openkal.types; +import openkal.abort; +import openkal.process; +import openkal.fs; +import openkal.env; +import okc.report; +import okc.spec; +import okc.child; + +// Termination cannot be observed by the program that performs it, so it is +// observed in a started copy. That requires three interfaces beyond the one +// under examination --- one to start the copy, one to name the program, and one +// to tell the copy what it is --- and an arrangement that selected none of them +// reports that rather than reporting nothing. +namespace okc::termination { + +void run() { + heading("openkal.abort"); +#ifndef MCPP_FEATURE_CORE + unobserved(kind::behaviour, "openkal.abort", "the core set was not selected"); + return; +#elif !defined(MCPP_FEATURE_PROCESS) || !defined(MCPP_FEATURE_ENV) || !defined(MCPP_FEATURE_FS) + unobserved(kind::behaviour, "kal_exit terminates with the status it was given", + "termination ends the observing program, so it is observed in a started copy; " + "that needs openkal.process to start one, openkal.fs to name it, and " + "openkal.env to tell it what to do"); + unobserved(kind::behaviour, "kal_exit runs no static destructor", + "the same"); + unobserved(kind::behaviour, "kal_abort does not return", + "the same"); +#else + int status = 0, terminated = 0; + + if (start_copy("openkal-conformance-child", argument_for(errand::exit_with_33), status, terminated)) { + observe(kind::behaviour, terminated == 0 && status == 33, + "kal_exit terminates with the status it was given"); + } else { + unobserved(kind::behaviour, "kal_exit terminates with the status it was given", + "a copy of this program could not be started; openkal.process reported a failure " + "or the program's own name did not resolve against a supplied directory"); + } + + // Clause 7.8: registered exit handlers and static destructors shall not + // run. The copy arms a destructor that would announce itself, and the + // observation is that the copy still reported the status: an + // implementation that ended through a C library's exit would run the + // destructor first, and the destructor writes to the copy's output. + if (start_copy("openkal-conformance-child", argument_for(errand::exit_after_writing), status, terminated)) { + observe(kind::behaviour, terminated == 0 && status == 34, + "kal_exit terminates immediately, and the status survives"); + line(" (the copy's output above shows BEFORE EXIT and shall not show DESTRUCTOR RAN)"); + } else { + unobserved(kind::behaviour, "kal_exit runs no static destructor", + "a copy of this program could not be started"); + } + + // kal_abort does not return, and what it does instead is the environment's + // to decide: the specification requires only that the program stop after + // the message. A status of 33 or 34 would mean it returned into the copy's + // ordinary path, which is the thing being excluded. + if (start_copy("openkal-conformance-child", argument_for(errand::abort_with_message), status, terminated)) { + observe(kind::behaviour, terminated != 0 || (status != 33 && status != 34 && status != 0), + "kal_abort ends the program rather than returning"); + } else { + unobserved(kind::behaviour, "kal_abort does not return", + "a copy of this program could not be started"); + } +#endif +} + +} // namespace okc::termination diff --git a/conformance/src/sections/abort.cppm b/conformance/src/sections/abort.cppm new file mode 100644 index 0000000..2f2c7f0 --- /dev/null +++ b/conformance/src/sections/abort.cppm @@ -0,0 +1,11 @@ +// okc.abort --- the section that examines openkal.abort. +// +// The namespace is `termination' rather than `abort': a namespace of that name +// would shadow nothing the suite uses today and would shadow something the day +// a section reached for it, and a name that is safe only until someone writes +// the obvious line is not safe. +export module okc.abort; + +export namespace okc::termination { +void run(); +} diff --git a/conformance/src/sections/child.cpp b/conformance/src/sections/child.cpp new file mode 100644 index 0000000..e0a4df3 --- /dev/null +++ b/conformance/src/sections/child.cpp @@ -0,0 +1,207 @@ +module okc.child; + +// Every openkal module is imported unconditionally, in this and in every other +// section. An import creates no link-time dependency of its own --- what +// creates one is a call --- so the conditionals that select what is examined +// guard the calls and never the imports. They could not guard the imports in +// any case: an import inside a conditional block is refused by the build, and +// refused for a good reason, since a module graph that depends on the +// preprocessor is a module graph that cannot be scanned. +import openkal.types; +import openkal.abort; +import openkal.env; +import openkal.stream; +import openkal.time; +import openkal.fs; +import openkal.process; + +namespace okc { +namespace { + +bool same(const char* a, kal_uintptr alen, const char* b) { + kal_uintptr n = 0; while (b[n]) ++n; + if (n != alen) return false; + for (kal_uintptr i = 0; i < n; ++i) if (a[i] != b[i]) return false; + return true; +} + +} // namespace + +const char* argument_for(errand e) { + switch (e) { + case errand::exit_with_33: return "--child-exit"; + case errand::exit_after_writing: return "--child-exit-after-writing"; + case errand::abort_with_message: return "--child-abort"; + case errand::wait_to_be_terminated: return "--child-wait"; + case errand::none: break; + } + return ""; +} + +errand child_errand() { +#ifdef MCPP_FEATURE_ENV + for (kal_uintptr i = 1; i < kal_env_arg_count(); ++i) { + kal_uintptr len = 0; + const char* a = kal_env_arg(i, &len); + if (!a) continue; + if (same(a, len, argument_for(errand::exit_with_33))) return errand::exit_with_33; + if (same(a, len, argument_for(errand::exit_after_writing))) return errand::exit_after_writing; + if (same(a, len, argument_for(errand::abort_with_message))) return errand::abort_with_message; + if (same(a, len, argument_for(errand::wait_to_be_terminated))) return errand::wait_to_be_terminated; + } +#endif + return errand::none; +} + +namespace { +// Something the specification requires shall NOT run after kal_exit. A static +// object whose destructor writes is the shortest thing that would betray an +// implementation calling a C library's exit instead of terminating. +struct after { + bool armed = false; + ~after() { +#ifdef MCPP_FEATURE_CORE + if (armed) kal_stream_write(kal_stdout(), "DESTRUCTOR RAN\n", 15); +#endif + } +}; +after g_after; +} // namespace + +[[noreturn]] void perform(errand e) { + switch (e) { + case errand::exit_with_33: + // Clause 7.6: the first element of the vector is the name the + // started program observes as its own, passed unaltered. The copy + // reports which one it saw through its status, because that is the + // only channel openkal.process defines --- and reporting it is what + // makes the parent's observation an observation rather than an + // assumption. +#ifdef MCPP_FEATURE_ENV + { + kal_uintptr len = 0; + const char* self = kal_env_arg(0, &len); + if (!self || !same(self, len, "openkal-conformance-child")) kal_exit(36); + } +#endif + kal_exit(33); + case errand::exit_after_writing: + // The status is 34, and the parent additionally requires that this + // program's output contain the written bytes and not the + // destructor's: an implementation that terminated by way of a C + // library's exit would run the destructor, and clause 7.8 says it + // shall not. + g_after.armed = true; +#ifdef MCPP_FEATURE_CORE + kal_stream_write(kal_stdout(), "BEFORE EXIT\n", 12); +#endif + kal_exit(34); + case errand::abort_with_message: + kal_abort("openkal-conformance: the message kal_abort was given\n", 52); + case errand::wait_to_be_terminated: +#ifdef MCPP_FEATURE_TIME + for (;;) kal_time_sleep(5u * 1000u * 1000u); +#else + for (;;) { } +#endif + case errand::none: + break; + } + kal_exit(35); +} + +// --- where this program is --------------------------------------------------- + +#if defined(MCPP_FEATURE_ENV) && defined(MCPP_FEATURE_FS) + +namespace { + +bool is_separator(char c) { return c == '/' || c == '\\'; } + +// Whether `name' is a prefix of `whole' that ends at a component boundary. +// A supplied directory whose own name ends in a separator --- a volume, on an +// environment that names volumes --- matches without requiring another. +bool prefix_at_boundary(const char* name, kal_uintptr nlen, + const char* whole, kal_uintptr wlen) { + if (nlen == 0 || nlen > wlen) return false; + for (kal_uintptr i = 0; i < nlen; ++i) if (name[i] != whole[i]) return false; + if (is_separator(name[nlen - 1])) return true; + return nlen == wlen || is_separator(whole[nlen]); +} + +} // namespace + +bool locate_self(kal_dir& base, const char*& relative, kal_uintptr& relative_len) { + kal_uintptr len = 0; + const char* argv0 = kal_env_arg(0, &len); + if (!argv0 || len == 0) return false; + + kal_uintptr best = 0, best_len = 0; + bool found = false; + for (kal_uintptr i = 0; i < kal_fs_preopen_count(); ++i) { + kal_dir d{}; const char* name = nullptr; kal_uintptr nlen = 0; + if (kal_fs_preopen(i, &d, &name, &nlen) != kal_ok) continue; + if (!prefix_at_boundary(name, nlen, argv0, len)) continue; + if (!found || nlen > best_len) { found = true; best = i; best_len = nlen; } + } + + if (!found) { + // No supplied directory names it, so the name is already relative to + // the directory the program was started in --- which is the first + // directory the environment supplied. + base = kal::fs::working(); + relative = argv0; relative_len = len; + return relative_len > 0; + } + + kal_dir d{}; const char* name = nullptr; kal_uintptr nlen = 0; + kal_fs_preopen(best, &d, &name, &nlen); + base = d; + kal_uintptr at = best_len; + while (at < len && is_separator(argv0[at])) ++at; + relative = argv0 + at; + relative_len = len - at; + return relative_len > 0; +} + +#else + +bool locate_self(kal_dir&, const char*&, kal_uintptr&) { return false; } + +#endif + +// --- starting a copy --------------------------------------------------------- + +#if defined(MCPP_FEATURE_ENV) && defined(MCPP_FEATURE_FS) && defined(MCPP_FEATURE_PROCESS) + +bool start_copy_running(const char* first_element, const char* errand_argument, + kal_process& out) { + kal_dir base{}; const char* rel = nullptr; kal_uintptr rel_len = 0; + if (!locate_self(base, rel, rel_len)) return false; + + const char* argv[2] = { first_element, errand_argument }; + kal_uintptr lens[2]; + for (int i = 0; i < 2; ++i) { kal_uintptr n = 0; while (argv[i][n]) ++n; lens[i] = n; } + + const kal_spawn_streams streams{ 0, 0, 0 }; + return kal_process_spawn(base, rel, rel_len, argv, lens, 2, nullptr, nullptr, 0, + &streams, &out) == kal_ok; +} + +bool start_copy(const char* first_element, const char* errand_argument, + int& status, int& terminated) { + kal_process child{}; + if (!start_copy_running(first_element, errand_argument, child)) return false; + const int e = kal_process_wait(child, &status, &terminated); + kal_process_close(child); + return e == kal_ok; +} + +#else + +bool start_copy(const char*, const char*, int&, int&) { return false; } +bool start_copy_running(const char*, const char*, kal_process&) { return false; } + +#endif + +} // namespace okc diff --git a/conformance/src/sections/child.cppm b/conformance/src/sections/child.cppm new file mode 100644 index 0000000..3dd1183 --- /dev/null +++ b/conformance/src/sections/child.cppm @@ -0,0 +1,59 @@ +// okc.child --- the copy of itself the suite starts. +// +// Two of the specification's requirements cannot be observed from within the +// observing program, because satisfying them ends it: kal_exit terminates +// immediately, and kal_abort does not return. They are therefore observed in a +// started copy, whose status is the only channel openkal.process defines. +// +// The copy must be able to tell that it is one, and the only thing that +// distinguishes it is the argument vector it was started with --- which is +// openkal.env. An arrangement that did not select openkal.env therefore cannot +// observe termination at all, and says so rather than passing. +export module okc.child; + +import openkal.types; +import openkal.fs; +import openkal.process; + +export namespace okc { + +enum class errand { + none = 0, + exit_with_33, // kal_exit shall terminate immediately with the status + exit_after_writing, // ... and shall not run what a C library would run after + abort_with_message, // kal_abort shall report the message and not return + wait_to_be_terminated, // so that a request to terminate has something to reach +}; + +// What this program was started to do, or `none' if it was started by a person. +errand child_errand(); + +// Performs it. Does not return. +[[noreturn]] void perform(errand e); + +// The name of the argument that selects each errand, for the parent to pass. +const char* argument_for(errand e); + +// Where this program itself is, expressed the way openkal names things: a +// directory the environment supplied and a remainder relative to it. +// +// openkal gives a program no operation that resolves a global name --- that +// work belongs to a C library and is performed once against the supplied +// directories --- so a program without one performs it, and this is it. It asks +// the supplied directories which of them is a prefix of the name the program +// was started by, and makes no assumption about how an absolute name is +// spelled: one environment writes a leading separator and another writes a +// volume first, and a suite that knew which would be a suite for one of them. +bool locate_self(kal_dir& base, const char*& relative, kal_uintptr& relative_len); + +// Starts a copy with the given first element and errand, and reports how it +// ended. False means no copy could be started. +bool start_copy(const char* first_element, const char* errand_argument, + int& status, int& terminated); + +// The same, without awaiting it, for the observation that needs a copy still +// running when it is made. +bool start_copy_running(const char* first_element, const char* errand_argument, + kal_process& out); + +} // namespace okc diff --git a/conformance/src/sections/env.cpp b/conformance/src/sections/env.cpp new file mode 100644 index 0000000..1778cf1 --- /dev/null +++ b/conformance/src/sections/env.cpp @@ -0,0 +1,138 @@ +module okc.env; + +import openkal.types; +import openkal.env; +import okc.report; +import okc.spec; + +namespace okc::env { +namespace { + +kal_uintptr length(const char* s) { kal_uintptr n = 0; while (s && s[n]) ++n; return n; } + +bool same(const char* a, const char* b) { + kal_uintptr i = 0; + while (a[i] && a[i] == b[i]) ++i; + return a[i] == b[i]; +} + +} // namespace + +void run() { + heading("openkal.env"); +#ifndef MCPP_FEATURE_ENV + unobserved(kind::behaviour, "openkal.env", "the interface was not selected"); + return; +#else + const kal_uintptr count = kal_env_arg_count(); + + // Position zero is the name by which the program was started, and an + // environment that has no such name reports an empty string rather than + // omitting it --- so the count is at least one on every environment. + observe(kind::behaviour, count >= 1, "the argument vector has at least one element"); + + { + kal_uintptr len = 999; + const char* a0 = kal_env_arg(0, &len); + observe(kind::behaviour, a0 != nullptr && len == length(a0), + "the first argument is reported with its own length"); + } + + // Reading past the end is answered, not undefined: a program that walks the + // vector must be able to stop. + { + kal_uintptr len = 999; + observe(kind::behaviour, kal_env_arg(count, &len) == nullptr && len == 0, + "reading past the last argument reports nothing"); + } + + // The distinction the interface exists to preserve. A caller that cannot + // tell an absent variable from one whose value is empty cannot act + // correctly upon either. + { + const char* name = "OPENKAL_CONFORMANCE_NO_SUCH_VARIABLE_EXISTS"; + kal_uintptr len = 999; + observe(kind::behaviour, + kal_env_var(name, length(name), &len) == nullptr, + "a variable that is absent is reported as absent"); + } + { + const char* name = "OPENKAL_CONFORMANCE_EMPTY"; + kal_uintptr len = 999; + const char* v = kal_env_var(name, length(name), &len); + if (v == nullptr) { + unobserved(kind::behaviour, + "a variable whose value is empty is distinguished from an absent one", + "openkal.env has no operation that sets a variable, so the runner must " + "set OPENKAL_CONFORMANCE_EMPTY to the empty string for this to be examined"); + } else { + observe(kind::behaviour, len == 0, + "a variable whose value is empty is reported as present and empty"); + } + } + + // Enumeration reaches the same values as the enquiry. The order is + // unspecified, so the observation is that a name found by enumeration is + // found again by name --- not that the two agree position by position. + { + const kal_uintptr n = kal_env_var_count(); + bool consistent = true; + bool examined = false; + for (kal_uintptr i = 0; i < n && i < 64; ++i) { + kal_uintptr nlen = 0, vlen = 0; + const char* value = nullptr; + const char* entry = kal_env_var_at(i, &nlen, &value, &vlen); + if (!entry) { consistent = false; break; } + kal_uintptr again = 0; + const char* found = kal_env_var(entry, nlen, &again); + if (!found || again != vlen) { consistent = false; break; } + examined = true; + } + if (examined) + observe(kind::behaviour, consistent, + "every enumerated variable is found again by its own name"); + else + unobserved(kind::behaviour, "enumeration reaches the same values as the enquiry", + "the environment supplied no variables to enumerate"); + + kal_uintptr nlen = 0, vlen = 0; + const char* value = nullptr; + observe(kind::behaviour, kal_env_var_at(n, &nlen, &value, &vlen) == nullptr, + "reading past the last variable reports nothing"); + } + + if (performs(kind::abi)) { + // The strings are counted, and the count is what a caller uses. An + // implementation that reported a length excluding or including a + // terminator inconsistently would be caught by comparing the two. + kal_uintptr len = 0; + const char* a0 = kal_env_arg(0, &len); + bool consistent = a0 != nullptr; + if (consistent) for (kal_uintptr i = 0; i < len; ++i) if (a0[i] == '\0') consistent = false; + observe(kind::abi, consistent, + "a counted string contains no terminator within its own length"); + } + + if (performs(kind::stability)) { + bool all = true; + for (int i = 0; i < repetitions && all; ++i) { + kal_uintptr len = 0; + const char* a = kal_env_arg(0, &len); + if (!a) all = false; + } + observe(kind::stability, all, "the parameters remain readable after many enquiries"); + + // The pointers a program receives are the environment's and remain + // valid: a program that kept one across other work would otherwise + // read freed memory, and openkal offers no operation that would have + // told it not to. + kal_uintptr l1 = 0, l2 = 0; + const char* first = kal_env_arg(0, &l1); + const char* second = kal_env_arg(0, &l2); + observe(kind::stability, first && second && l1 == l2 && same(first, second), + "an argument reads the same on a later enquiry"); + } +#endif +} + +} // namespace okc::env diff --git a/conformance/src/sections/env.cppm b/conformance/src/sections/env.cppm new file mode 100644 index 0000000..3ecb188 --- /dev/null +++ b/conformance/src/sections/env.cppm @@ -0,0 +1,10 @@ +// okc.env --- the section that examines openkal.env. +// +// The interface is one function. A section records observations through +// okc.report and returns; whether it examined anything is decided inside, by +// the same feature that decided whether the interface is provided at all. +export module okc.env; + +export namespace okc::env { +void run(); +} diff --git a/conformance/src/sections/fs.cpp b/conformance/src/sections/fs.cpp new file mode 100644 index 0000000..54f0dae --- /dev/null +++ b/conformance/src/sections/fs.cpp @@ -0,0 +1,369 @@ +module okc.fs; + +import openkal.types; +import openkal.fs; +import openkal.stream; +import openkal.time; +import okc.report; +import okc.spec; + +namespace okc::fs { +namespace { + +kal_uintptr length(const char* s) { kal_uintptr n = 0; while (s && s[n]) ++n; return n; } + +constexpr const char* kName = "okc-conformance.tmp"; +constexpr const char* kDir = "okc-conformance.dir"; +constexpr const char* kOther = "okc-conformance-2.tmp"; + +#ifdef MCPP_FEATURE_FS +kal_dir here() { return kal::fs::working(); } + +bool write_bytes(kal_file f, const char* s, kal_uintptr n) { + kal_stream st{ kal_fs_stream(f) }; + const kal_io_result r = kal_stream_write(st, s, n); + return r.e == kal_ok && r.n == n; +} + +// Opens, writes and releases in one step, so that the observations that follow +// read as what they are about rather than as file handling. +bool put_file(const char* name, const char* text) { + kal_file f{}; + const auto flags = kal::fs::open::write | kal::fs::open::create | kal::fs::open::truncate; + if (kal::fs::open_file(here(), name, length(name), flags, &f) != kal_ok) return false; + const bool ok = write_bytes(f, text, length(text)); + kal_fs_close_file(f); + return ok; +} + +long read_file(const char* name, char* buf, kal_uintptr cap) { + kal_file f{}; + if (kal::fs::open_file(here(), name, length(name), kal::fs::open::read, &f) != kal_ok) + return -1; + kal_stream st{ kal_fs_stream(f) }; + const kal_io_result r = kal_stream_read(st, buf, cap); + kal_fs_close_file(f); + return r.e == kal_ok ? static_cast(r.n) : -1; +} +#endif + +} // namespace + +void run() { + heading("openkal.fs"); +#ifndef MCPP_FEATURE_FS + unobserved(kind::behaviour, "openkal.fs", "the interface was not selected"); + return; +#else + claim("kal_fs_props", kal_fs_props); + + // Every operation is relative to a directory the environment supplied, so + // the first observation is that it supplied one. + { + const kal_uintptr n = kal_fs_preopen_count(); + observe(kind::behaviour, n >= 1, "the environment supplied at least one directory"); + kal_dir d{}; const char* name = nullptr; kal_uintptr len = 0; + const int e = kal_fs_preopen(0, &d, &name, &len); + observe(kind::behaviour, e == kal_ok && name != nullptr && len > 0, + "the first supplied directory has a name"); + if (name) { put(" the program was started in: "); + kal_stream_write(kal_stdout(), name, len); put("\n"); } + + // Names are the environment's and this specification requires only that + // they be distinct. A caller that resolves a global name against them + // cannot do so if two are the same. + bool distinct = true; + for (kal_uintptr i = 0; i < n && distinct; ++i) + for (kal_uintptr j = i + 1; j < n && distinct; ++j) { + kal_dir a{}, b{}; const char* an = nullptr; const char* bn = nullptr; + kal_uintptr al = 0, bl = 0; + kal_fs_preopen(i, &a, &an, &al); + kal_fs_preopen(j, &b, &bn, &bl); + if (al != bl) continue; + bool same = true; + for (kal_uintptr k = 0; k < al; ++k) if (an[k] != bn[k]) { same = false; break; } + if (same) distinct = false; + } + observe(kind::behaviour, distinct, "the supplied directories have distinct names"); + } + + // A name that ascends, and a name that begins with a separator, are + // refused. A program able to ascend from the directory it was given would + // not be confined by having been given it, and confinement is a property of + // what the environment supplied rather than of the program's cooperation. + { + kal_file f{}; + const char* up = "../okc-conformance-escape"; + observe(kind::behaviour, + kal::fs::open_file(here(), up, length(up), kal::fs::open::read, &f) != kal_ok, + "a name that ascends is refused"); + const char* rooted = "/etc/passwd"; + observe(kind::behaviour, + kal::fs::open_file(here(), rooted, length(rooted), kal::fs::open::read, &f) != kal_ok, + "a name that begins with a separator is refused"); + } + + // Clause 7.12: the one reserved name. It is observed in both of the ways a + // program uses it --- asking a question about the directory it holds, and + // obtaining a second reference to it --- because an implementation may + // accept it in one operation and not in the other, and one of the three + // accepted it in neither until this was written. + { + const char* self = "."; + kal_node_info info{}; + observe(kind::behaviour, + kal_fs_info(here(), self, length(self), &info) == kal_ok + && info.kind == kal_node_directory, + "the reserved name denotes the directory itself"); + + kal_dir again{}; + const int e = kal_fs_open_dir(here(), self, length(self), &again); + observe(kind::behaviour, e == kal_ok, + "the directory itself can be opened through the reserved name"); + if (e == kal_ok) { + // The second reference is a directory in its own right: a name + // created through the original is found through it. + const char* probe = "okc-self.tmp"; + put_file(probe, "x"); + kal_node_info seen{}; + observe(kind::behaviour, + kal_fs_info(again, probe, length(probe), &seen) == kal_ok + && seen.kind == kal_node_file, + "the second reference reaches what the first reaches"); + kal_fs_remove(here(), probe, length(probe)); + kal_fs_close_dir(again); + } else { + unobserved(kind::behaviour, "the second reference reaches what the first reaches", + "the directory itself could not be opened"); + } + } + + // Clause 7.7: enquiry about a name that does not exist is answered, and + // access to it is refused with the value that says which condition held. + { + kal_fs_remove(here(), kName, length(kName)); + kal_node_info info{}; + observe(kind::behaviour, + kal_fs_info(here(), kName, length(kName), &info) == kal_ok + && info.kind == kal_node_absent, + "enquiry about a name that does not exist succeeds and reports absence"); + kal_file f{}; + observe(kind::behaviour, + kal::fs::open_file(here(), kName, length(kName), kal::fs::open::read, &f) + == kal_err_not_found, + "opening a name that does not exist reports that it does not exist"); + } + + // Creation, transfer, positioning, length and enquiry about the handle. + { + observe(kind::behaviour, put_file(kName, "0123456789"), "a file is created and written"); + + kal_file f{}; + const int e = kal::fs::open_file(here(), kName, length(kName), + kal::fs::open::read | kal::fs::open::write, &f); + observe(kind::behaviour, e == kal_ok, "the file is opened again"); + if (e == kal_ok) { + kal_u64 at = 0; + observe(kind::behaviour, kal_fs_seek(f, 3, kal::fs::seek_set, &at) == kal_ok && at == 3, + "positioning reports where it arrived"); + char buf[8] = {}; + kal_stream st{ kal_fs_stream(f) }; + const kal_io_result r = kal_stream_read(st, buf, 4); + observe(kind::behaviour, + r.e == kal_ok && r.n == 4 && buf[0] == '3' && buf[3] == '6', + "a transfer after positioning reads from where it was positioned"); + + kal_node_info info{}; + observe(kind::behaviour, + kal_fs_file_info(f, &info) == kal_ok && info.size == 10 + && info.kind == kal_node_file, + "enquiry about the open file reports its length and what it is"); + + observe(kind::behaviour, kal_fs_truncate(f, 4) == kal_ok + && kal_fs_file_info(f, &info) == kal_ok && info.size == 4, + "the length of an open file is set"); + + // The inverse of the enquiry above, and it is checked by reading + // back rather than by the value returned: an implementation that + // reported success and set nothing would satisfy the return value. + // + // Whole seconds, not nanoseconds. The three environments openkal is + // implemented on record a modification time to a nanosecond, to a + // microsecond and to a hundred nanoseconds respectively, so an + // observation that required the value to come back unchanged would + // be requiring a resolution the interface does not claim. A second + // is the resolution every environment that records the time at all + // agrees upon. + if (kal::fs::has(kal::fs::modified_time)) { + // Opened again for writing, because that is what the operation + // requires: one environment decides at the point of opening + // what may afterwards be done with a file. + const kal_u64 chosen = 1600000000ull * 1000000000ull; // 2020-09-13 + kal_file w{}; + const int opened = kal::fs::open_file(here(), kName, length(kName), + kal::fs::open::read | kal::fs::open::write, + &w); + const int e = opened == kal_ok ? kal_fs_set_modified(w, chosen) : opened; + kal_node_info after{}; + const int read_back = opened == kal_ok ? kal_fs_file_info(w, &after) : opened; + if (opened == kal_ok) kal_fs_close_file(w); + observe(kind::behaviour, + e == kal_ok && read_back == kal_ok + && after.modified_ns / 1000000000u == chosen / 1000000000u, + "the time an open file reports as its last modification is set"); + } else { + unobserved(kind::behaviour, + "the time an open file reports as its last modification is set", + "the implementation does not claim prop_modified_time"); + } + kal_fs_close_file(f); + } + } + + // The three conditions clause 7.8 records, each observed by its effect. A + // return value alone would accept an implementation that reported success + // and did nothing, which is exactly the outcome those flags exist to + // exclude. + { + char buf[64]; + observe(kind::behaviour, put_file(kName, "0123456789") && put_file(kName, "abc") + && read_file(kName, buf, sizeof buf) == 3, + "opening with truncate discards what lay beyond"); + + kal_file f{}; + const auto excl = kal::fs::open::write | kal::fs::open::create | kal::fs::open::exclusive; + observe(kind::behaviour, + kal::fs::open_file(here(), kName, length(kName), excl, &f) == kal_err_exists, + "creating a name that exists, exclusively, reports that it exists"); + + put_file(kName, "one"); + const auto app = kal::fs::open::write | kal::fs::open::append; + if (kal::fs::open_file(here(), kName, length(kName), app, &f) == kal_ok) { + kal_u64 at = 0; + kal_fs_seek(f, 0, kal::fs::seek_set, &at); // and it shall still append + write_bytes(f, "two", 3); + kal_fs_close_file(f); + } + const long n = read_file(kName, buf, sizeof buf); + observe(kind::behaviour, n == 6 && buf[0] == 'o' && buf[3] == 't', + "a transfer to a file opened for appending goes to the end"); + } + + // Directories: creation, enumeration, removal. + { + kal_fs_remove(here(), kDir, length(kDir)); + observe(kind::behaviour, kal_fs_mkdir(here(), kDir, length(kDir)) == kal_ok, + "a directory is created"); + kal_dir d{}; + const int e = kal_fs_open_dir(here(), kDir, length(kDir), &d); + observe(kind::behaviour, e == kal_ok, "the directory is opened"); + if (e == kal_ok) { + kal_file f{}; + const auto flags = kal::fs::open::write | kal::fs::open::create; + if (kal::fs::open_file(d, "a", 1, flags, &f) == kal_ok) kal_fs_close_file(f); + if (kal::fs::open_file(d, "b", 1, flags, &f) == kal_ok) kal_fs_close_file(f); + + // What was reported is kept, not only how much of it. A count that + // does not match tells a reader that something is wrong and not + // what, and the difference between "one entry too many" and "the + // wrong two entries" is the whole of the diagnosis. + int seen = 0; + char reported[256]; kal_uintptr at = 0; + kal_uintptr iter = 0; + if (kal_fs_list_begin(d, &iter) == kal_ok) { + for (;;) { + const char* name = nullptr; kal_uintptr len = 0; int knd = 0; + if (kal_fs_list_next(d, &iter, &name, &len, &knd) != kal_ok) break; + if (!name) break; + ++seen; + if (at + len + 2 < sizeof reported) { + if (at) reported[at++] = ' '; + for (kal_uintptr k = 0; k < len; ++k) reported[at++] = name[k]; + reported[at] = '\0'; + } + } + } + reported[at < sizeof reported ? at : sizeof reported - 1] = '\0'; + observe(kind::behaviour, seen == 2, + "enumeration reports the entries that were created and nothing else"); + if (seen != 2) { line(" what it reported: "); line(reported); line("\n"); } + + kal_fs_remove(d, "a", 1); + kal_fs_remove(d, "b", 1); + kal_fs_close_dir(d); + } + observe(kind::behaviour, kal_fs_remove(here(), kDir, length(kDir)) == kal_ok, + "the directory is removed"); + } + + // Renaming, and that the old name is then absent. + { + put_file(kName, "x"); + kal_fs_remove(here(), kOther, length(kOther)); + observe(kind::behaviour, + kal_fs_rename(here(), kName, length(kName), here(), kOther, length(kOther)) == kal_ok, + "a name is renamed"); + kal_node_info info{}; + observe(kind::behaviour, + kal_fs_info(here(), kName, length(kName), &info) == kal_ok + && info.kind == kal_node_absent, + "the name it was renamed from is then absent"); + kal_fs_remove(here(), kOther, length(kOther)); + } + + 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).bits; + observe(kind::abi, (kal_fs_props & ~assigned) == 0, + "the capability word contains no position the specification has not assigned"); + + // Clause 6.6: an implementation shall not treat a released handle as + // valid. The recommended construction divides the word into an index + // and a generation; whatever the construction, the property is the + // same and it is what this observes. + put_file(kName, "x"); + kal_file f{}; + if (kal::fs::open_file(here(), kName, length(kName), kal::fs::open::read, &f) == kal_ok) { + const kal_file released = f; + kal_fs_close_file(f); + kal_node_info info{}; + observe(kind::abi, kal_fs_file_info(released, &info) != kal_ok, + "a released handle is not treated as valid"); + } + kal_fs_remove(here(), kName, length(kName)); + } + + if (performs(kind::stability)) { + // Where a handle scheme that never reuses a slot stops. An + // implementation that packed a generation into the word and never + // reclaimed the index would satisfy every observation above. + bool all = true; + put_file(kName, "x"); + for (int i = 0; i < repetitions && all; ++i) { + kal_file f{}; + if (kal::fs::open_file(here(), kName, length(kName), kal::fs::open::read, &f) != kal_ok) + all = false; + else kal_fs_close_file(f); + } + observe(kind::stability, all, "a file opened and released many times is openable again"); + kal_fs_remove(here(), kName, length(kName)); + } + + if (performs(kind::cost)) { + put_file(kName, "x"); + const kal_duration t0 = kal_time_monotonic(); + for (int i = 0; i < cost_iterations; ++i) { + kal_file f{}; + if (kal::fs::open_file(here(), kName, length(kName), kal::fs::open::read, &f) == kal_ok) + kal_fs_close_file(f); + } + measure("opening and releasing a file", kal_time_monotonic() - t0, cost_iterations); + kal_fs_remove(here(), kName, length(kName)); + } +#endif +} + +} // namespace okc::fs diff --git a/conformance/src/sections/fs.cppm b/conformance/src/sections/fs.cppm new file mode 100644 index 0000000..9dea8d2 --- /dev/null +++ b/conformance/src/sections/fs.cppm @@ -0,0 +1,10 @@ +// okc.fs --- the section that examines openkal.fs. +// +// The interface is one function. A section records observations through +// okc.report and returns; whether it examined anything is decided inside, by +// the same feature that decided whether the interface is provided at all. +export module okc.fs; + +export namespace okc::fs { +void run(); +} diff --git a/conformance/src/sections/memory.cpp b/conformance/src/sections/memory.cpp new file mode 100644 index 0000000..c17998c --- /dev/null +++ b/conformance/src/sections/memory.cpp @@ -0,0 +1,119 @@ +module okc.memory; + +import openkal.types; +import openkal.memory; +import openkal.time; +import okc.report; +import okc.spec; + +namespace okc::memory { +namespace { + +bool aligned(const void* p, kal_uintptr a) { + return (reinterpret_cast(p) & (a - 1)) == 0; +} + +// Writes a pattern across the whole extent and reads it back. An allocator that +// returned a region shorter than it promised would satisfy every other +// observation and fail this one, and it would fail it at the end rather than at +// the start, which is why the whole extent is written rather than the first +// word. +bool writable_throughout(unsigned char* p, kal_uintptr n) { + for (kal_uintptr i = 0; i < n; ++i) p[i] = static_cast(i * 31u + 7u); + for (kal_uintptr i = 0; i < n; ++i) + if (p[i] != static_cast(i * 31u + 7u)) return false; + return true; +} + +} // namespace + +void run() { + heading("openkal.memory"); +#ifndef MCPP_FEATURE_CORE + unobserved(kind::behaviour, "openkal.memory", "the core set was not selected"); + return; +#else + { + auto* p = static_cast(kal_alloc(1024, 16)); + observe(kind::behaviour, p != nullptr, "a region is obtained"); + if (p) { + observe(kind::behaviour, writable_throughout(p, 1024), + "the whole of the region is writable and reads back"); + kal_free(p, 1024, 16); + } + } + + // The alignment is part of the request, and an environment that could not + // honour it would have to report failure rather than return a region that + // is nearly right. + const kal_uintptr alignments[] = { 16, 64, 256, 4096 }; + for (kal_uintptr a : alignments) { + void* p = kal_alloc(a * 2, a); + observe(kind::behaviour, p != nullptr && aligned(p, a), + a == 16 ? "a region aligned to sixteen bytes" + : a == 64 ? "a region aligned to sixty-four bytes" + : a == 256 ? "a region aligned to two hundred and fifty-six bytes" + : "a region aligned to a page"); + if (p) kal_free(p, a * 2, a); + } + + // Two live regions do not overlap. An allocator that returned the same + // region twice would pass every observation that examined one region. + { + auto* a = static_cast(kal_alloc(512, 16)); + auto* b = static_cast(kal_alloc(512, 16)); + bool distinct = a && b; + if (distinct) { + for (int i = 0; i < 512; ++i) { a[i] = 0xA5; b[i] = 0x5A; } + for (int i = 0; i < 512 && distinct; ++i) + if (a[i] != 0xA5 || b[i] != 0x5A) distinct = false; + } + observe(kind::behaviour, distinct, "two live regions do not overlap"); + kal_free(a, 512, 16); + kal_free(b, 512, 16); + } + + if (performs(kind::abi)) { + // The size and alignment are carried by the interface so that an + // implementation need keep no record of its own. An implementation that + // ignored them and kept a header would still be conforming; one that + // required them to be wrong would not. + void* p = kal_alloc(4096, 4096); + observe(kind::abi, p != nullptr && aligned(p, 4096), + "the alignment the caller stated is the alignment obtained"); + kal_free(p, 4096, 4096); + } + + if (performs(kind::stability)) { + // Exhaustion is a defined outcome and a leak is not. An allocator that + // never reused a region would satisfy every observation above and would + // stop here. + bool all = true; + for (int i = 0; i < repetitions && all; ++i) { + const kal_uintptr n = 16u + static_cast(i % 512) * 8u; + void* p = kal_alloc(n, 16); + if (!p) { all = false; break; } + static_cast(p)[n - 1] = 1; + kal_free(p, n, 16); + } + observe(kind::stability, all, "a region obtained and released many times is obtainable again"); + + void* big = kal_alloc(1u << 20, 4096); + observe(kind::stability, big != nullptr, + "a large region is still obtainable after many small ones"); + kal_free(big, 1u << 20, 4096); + } + + if (performs(kind::cost)) { + const kal_duration t0 = kal_time_monotonic(); + for (int i = 0; i < cost_iterations; ++i) { + void* p = kal_alloc(64, 16); + kal_free(p, 64, 16); + } + measure("obtaining and releasing sixty-four bytes", + kal_time_monotonic() - t0, cost_iterations); + } +#endif +} + +} // namespace okc::memory diff --git a/conformance/src/sections/memory.cppm b/conformance/src/sections/memory.cppm new file mode 100644 index 0000000..b42ade0 --- /dev/null +++ b/conformance/src/sections/memory.cppm @@ -0,0 +1,10 @@ +// okc.memory --- the section that examines openkal.memory. +// +// The interface is one function. A section records observations through +// okc.report and returns; whether it examined anything is decided inside, by +// the same feature that decided whether the interface is provided at all. +export module okc.memory; + +export namespace okc::memory { +void run(); +} diff --git a/conformance/src/sections/process.cpp b/conformance/src/sections/process.cpp new file mode 100644 index 0000000..7f55965 --- /dev/null +++ b/conformance/src/sections/process.cpp @@ -0,0 +1,132 @@ +module okc.process; + +import openkal.types; +import openkal.process; +import openkal.fs; +import openkal.env; +import openkal.time; +import okc.report; +import okc.spec; +import okc.child; + +namespace okc::process { + +void run() { + heading("openkal.process"); +#if !defined(MCPP_FEATURE_PROCESS) + unobserved(kind::behaviour, "openkal.process", "the interface was not selected"); + return; +#elif !defined(MCPP_FEATURE_ENV) || !defined(MCPP_FEATURE_FS) + unobserved(kind::behaviour, "a program is started, awaited, and reports its status", + "a started copy is told what to do through its argument vector, which is " + "openkal.env, and that interface was not selected"); + return; +#else + claim("kal_process_props", kal_process_props); + + // Starting, awaiting, and the status a program reports. + { + int status = 0, terminated = 0; + if (start_copy("openkal-conformance-child", argument_for(errand::exit_with_33), + status, terminated)) { + observe(kind::behaviour, true, "a started program is awaited"); + if (kal::process::has(kal::process::exit_status)) + observe(kind::behaviour, terminated == 0 && status == 33, + "the status the program reported is the status it returned"); + else + unobserved(kind::behaviour, "the status is the value the program returned", + "the implementation does not claim prop_exit_status"); + } else { + unobserved(kind::behaviour, "a program is started", + "a copy of this program could not be started; the program's own name did " + "not resolve against a supplied directory, or the operation failed"); + } + } + + // Clause 7.6, which exists because the two sides must agree. The copy is + // started with a first element the specification says shall be passed + // unaltered, and the copy exits 33 only if it observed exactly that; an + // implementation that derived the first element from the path, or prepended + // to the vector, produces 36 instead. + { + int status = 0, terminated = 0; + if (start_copy("openkal-conformance-child", argument_for(errand::exit_with_33), + status, terminated)) + observe(kind::behaviour, terminated == 0 && status == 33, + "the argument vector is passed unaltered, and the copy read its own name"); + if (start_copy("a-name-the-copy-does-not-expect", argument_for(errand::exit_with_33), + status, terminated)) + observe(kind::behaviour, terminated == 0 && status == 36, + "a different first element reaches the copy, which is how the first " + "observation is known to be observing something"); + } + + // Termination, if the implementation claims it can request one. + if (kal::process::has(kal::process::terminate)) { +#ifdef MCPP_FEATURE_TIME + kal_process child{}; + if (start_copy_running("openkal-conformance-child", + argument_for(errand::wait_to_be_terminated), child)) { + kal_time_sleep(50u * 1000u * 1000u); + const int e = kal_process_terminate(child); + int status = 0, terminated = 0; + kal_process_wait(child, &status, &terminated); + observe(kind::behaviour, e == kal_ok, + "termination of a started program is requested"); + observe(kind::behaviour, terminated != 0 || status != 0, + "the terminated program did not report an ordinary success"); + kal_process_close(child); + } else { + unobserved(kind::behaviour, "termination is requested", + "a copy of this program could not be started"); + } +#else + unobserved(kind::behaviour, "termination is requested", + "the copy waits to be terminated, which needs openkal.time"); +#endif + } else { + unobserved(kind::behaviour, "termination is requested", + "the implementation does not claim prop_terminate"); + } + + if (performs(kind::abi)) { + observe(kind::abi, sizeof(kal_process) == sizeof(kal_uintptr), + "a process handle occupies one machine word"); + observe(kind::abi, sizeof(kal_spawn_streams) == 3 * sizeof(kal_uintptr), + "the stream selection occupies three machine words"); + const kal_uintptr assigned = (kal::process::terminate | kal::process::stream_passing + | kal::process::exit_status).bits; + observe(kind::abi, (kal_process_props & ~assigned) == 0, + "the capability word contains no position the specification has not assigned"); + } + + if (performs(kind::stability)) { + // Fewer repetitions than elsewhere, and the number is stated: starting + // a program is the most expensive operation the specification has, and + // a suite that took a minute here would be run less often than one that + // takes a second. + constexpr int rounds = 50; + bool all = true; + for (int i = 0; i < rounds && all; ++i) { + int status = 0, terminated = 0; + if (!start_copy("openkal-conformance-child", argument_for(errand::exit_with_33), + status, terminated) || status != 33) all = false; + } + observe(kind::stability, all, "a program started and awaited many times keeps starting"); + put(" programs started and awaited: "); put_signed(rounds); put("\n"); + } + + if (performs(kind::cost)) { + constexpr int rounds = 20; + const kal_duration t0 = kal_time_monotonic(); + for (int i = 0; i < rounds; ++i) { + int status = 0, terminated = 0; + if (!start_copy("openkal-conformance-child", argument_for(errand::exit_with_33), + status, terminated)) break; + } + measure("starting a program and awaiting it", kal_time_monotonic() - t0, rounds); + } +#endif +} + +} // namespace okc::process diff --git a/conformance/src/sections/process.cppm b/conformance/src/sections/process.cppm new file mode 100644 index 0000000..d5cbc79 --- /dev/null +++ b/conformance/src/sections/process.cppm @@ -0,0 +1,10 @@ +// okc.process --- the section that examines openkal.process. +// +// The interface is one function. A section records observations through +// okc.report and returns; whether it examined anything is decided inside, by +// the same feature that decided whether the interface is provided at all. +export module okc.process; + +export namespace okc::process { +void run(); +} diff --git a/conformance/src/sections/stream.cpp b/conformance/src/sections/stream.cpp new file mode 100644 index 0000000..1f156a6 --- /dev/null +++ b/conformance/src/sections/stream.cpp @@ -0,0 +1,89 @@ +module okc.stream; + +import openkal.types; +import openkal.stream; +import openkal.time; +import okc.report; +import okc.spec; + +namespace okc::stream { + +void run() { + heading("openkal.stream"); +#ifndef MCPP_FEATURE_CORE + unobserved(kind::behaviour, "openkal.stream", "the core set was not selected"); + return; +#else + const kal_stream out = kal_stdout(); + + // Clause 7.4: the whole buffer is transferred or the condition that + // prevented it is reported. A partial transfer is not a successful + // outcome, so the count and the error are examined together --- a suite + // that looked only at the error would accept an implementation that wrote + // half the bytes and said nothing. + { + const char msg[] = " (openkal.stream: this line was written by the suite)\n"; + const kal_uintptr n = sizeof msg - 1; + const kal_io_result r = kal_stream_write(out, msg, n); + observe(kind::behaviour, r.e == kal_ok && r.n == n, + "a write transfers the whole buffer and reports it"); + } + + // A transfer of nothing is a transfer. An implementation that refused it + // would break every caller that writes a computed length. + { + const kal_io_result r = kal_stream_write(out, "", 0); + observe(kind::behaviour, r.e == kal_ok && r.n == 0, + "a write of no bytes succeeds and reports no bytes"); + } + + // Clause 6.6: the standard streams are borrowed, so they remain usable + // after any number of operations and there is nothing to release. + observe(kind::behaviour, + kal_stdin().h != kal_stdout().h || kal_stdout().h == kal_stderr().h, + "the three standard streams are obtainable"); + + // Committing what is not buffered is not a failure. An implementation that + // reported one would oblige every caller to distinguish it from a real + // failure to reach the medium. + observe(kind::behaviour, kal_stream_flush(out) == kal_ok, + "committing an unbuffered stream reports success"); + + // The property a consumer must have before it has transferred anything. + claim("kal_stream_props(stdout)", kal::properties(out).bits); + observe(kind::behaviour, + (kal::properties(out).bits & ~kal::stream_prop::interactive.bits) == 0, + "a stream reports no position the specification has not assigned"); + + if (performs(kind::abi)) { + // The width of a handle is what allows an implementation to store a + // descriptor, an operating-system handle or a capability index in it. + observe(kind::abi, sizeof(kal_stream) == sizeof(kal_uintptr), + "a stream handle occupies one machine word"); + observe(kind::abi, sizeof(kal_io_result) == 2 * sizeof(kal_uintptr), + "a transfer result occupies two machine words"); + // The same handle answers the same way twice. An implementation that + // packed a generation into the word and incremented it on use would + // pass every behavioural observation above and fail here. + observe(kind::abi, kal_stdout().h == kal_stdout().h, + "the standard stream handle is stable between enquiries"); + } + + if (performs(kind::stability)) { + bool all = true; + for (int i = 0; i < repetitions && all; ++i) { + const kal_io_result r = kal_stream_write(out, "", 0); + if (r.e != kal_ok) all = false; + } + observe(kind::stability, all, "a stream serves the operation repeated many times"); + } + + if (performs(kind::cost)) { + const kal_duration t0 = kal_time_monotonic(); + for (int i = 0; i < cost_iterations; ++i) kal_stream_write(out, "", 0); + measure("a transfer of no bytes", kal_time_monotonic() - t0, cost_iterations); + } +#endif +} + +} // namespace okc::stream diff --git a/conformance/src/sections/stream.cppm b/conformance/src/sections/stream.cppm new file mode 100644 index 0000000..9e4bc86 --- /dev/null +++ b/conformance/src/sections/stream.cppm @@ -0,0 +1,10 @@ +// okc.stream --- the section that examines openkal.stream. +// +// The interface is one function. A section records observations through +// okc.report and returns; whether it examined anything is decided inside, by +// the same feature that decided whether the interface is provided at all. +export module okc.stream; + +export namespace okc::stream { +void run(); +} diff --git a/conformance/src/sections/task.cpp b/conformance/src/sections/task.cpp new file mode 100644 index 0000000..4a1e321 --- /dev/null +++ b/conformance/src/sections/task.cpp @@ -0,0 +1,270 @@ +module okc.task; + +import openkal.types; +import openkal.task; +import openkal.time; +import okc.atomic; +import okc.report; +import okc.spec; + +namespace okc::task { +namespace { + +#ifdef MCPP_FEATURE_TASK + +// A mutex, built here from the suspension primitive. +// +// openkal has no mutex, deliberately: a mutex is a construction above the +// primitive rather than a facility of the boundary, and an interface offering +// one would oblige an implementation whose environment has no kernel mutex to +// construct one. This is the construction, and its being twenty lines is the +// evidence for the decomposition. +// +// Three states rather than two. A mutex that recorded only "held" and "free" +// would have to wake a waiter on every release, because it could not know +// whether one existed; the third state records that one does. +struct mutex { + volatile kal_u32 state = 0; // 0 free, 1 held, 2 held and wanted + + void lock() { + kal_u32 expected = 0; + if (compare_exchange(&state, expected, 1u)) return; + for (;;) { + const kal_u32 previous = exchange(&state, 2u); + if (previous == 0) return; + kal_task_wait(const_cast(&state), 2u, 0); + } + } + + void unlock() { + if (exchange(&state, 0u) == 2u) { + kal_uintptr woken = 0; + kal_task_wake(const_cast(&state), 1, &woken); + } + } +}; + +mutex g_mutex; +volatile long long g_counter = 0; +constexpr int kContexts = 4; +constexpr int kIncrements = 20000; + +void increment(void*) { + for (int i = 0; i < kIncrements; ++i) { + g_mutex.lock(); + g_counter = g_counter + 1; // deliberately not atomic: the mutex is under test + g_mutex.unlock(); + } +} + +volatile kal_u32 g_word = 0; +volatile int g_ran = 0; +kal_uintptr g_identity = 0; + +void sets_the_word(void*) { + g_identity = kal_task_current(); + store_release(&g_ran, 1); + store_release(&g_word, 1u); + kal_uintptr woken = 0; + kal_task_wake(const_cast(&g_word), 1, &woken); +} + +thread_local int g_per_context = 0; +volatile int g_saw = -1; + +void records_its_own(void*) { + g_per_context = 7; + store_release(&g_saw, g_per_context); +} + +void does_nothing(void*) { } + +// Every context records the identity openkal gave it, so that the identities can +// be compared with each other rather than each with the starting one. +constexpr int kIdentities = 4; +kal_uintptr g_identities[kIdentities]; +volatile kal_u32 g_next_identity = 0; + +void records_its_identity(void*) { + kal_u32 slot = 0; + do { slot = load_acquire(&g_next_identity); } + while (slot < kIdentities && !compare_exchange(&g_next_identity, slot, slot + 1)); + if (slot < kIdentities) g_identities[slot] = kal_task_current(); +} + +#endif + +} // namespace + +void run() { + heading("openkal.task"); +#ifndef MCPP_FEATURE_TASK + unobserved(kind::behaviour, "openkal.task", "the interface was not selected"); + return; +#else + claim("kal_task_props", kal_task_props); + + // A context runs, and the context that started it can tell that it did. + { + kal_task t{}; + const int e = kal_task_start(sets_the_word, nullptr, &t); + observe(kind::behaviour, e == kal_ok, "an execution context starts"); + if (e == kal_ok) { + // The suspension primitive. The comparison and the suspension occur + // without an intervening opportunity for the value to change + // unobserved, which is what a caller cannot construct for itself + // and is the whole reason the operation exists. The loop + // re-examines the condition after waking, because waking is + // permitted to be spurious. + while (load_acquire(&g_word) == 0) + kal_task_wait(const_cast(&g_word), 0u, 0); + observe(kind::behaviour, load_acquire(&g_ran) == 1, + "the started context ran"); + observe(kind::behaviour, kal_task_join(t) == kal_ok, "the context is awaited"); + observe(kind::behaviour, g_identity != kal_task_current(), + "a started context has an identity distinct from the starting one"); + // Zero is the value an implementation reaches by not answering, and + // a consumer keyed on the identity reads it as "no entry" however + // many times it is written. An implementation that returned zero for + // every started context satisfied the observation above --- zero + // differs from the starting context's identity --- and satisfied + // nothing a consumer needs. + observe(kind::behaviour, g_identity != 0, + "the identity of a started context is not zero"); + } + } + + // The construction the interface exists to support. The count is exact: + // an increment lost to a race is what an unsound mutex produces, and the + // sum is the only thing that shows it. + { + kal_task t[kContexts]{}; + int started = 0; + g_counter = 0; + for (int i = 0; i < kContexts; ++i) + if (kal_task_start(increment, nullptr, &t[i]) == kal_ok) ++started; + for (int i = 0; i < started; ++i) kal_task_join(t[i]); + observe(kind::behaviour, started == kContexts, + "four execution contexts start"); + observe(kind::behaviour, g_counter == static_cast(started) * kIncrements, + "a mutex built from the suspension primitive loses no increment"); + put(" increments: "); put_signed(g_counter); + put(" of "); put_signed(static_cast(started) * kIncrements); put("\n"); + } + + // The identities of contexts that ran at the same time, compared with each + // other. Comparing each with the starting context's is satisfied by an + // implementation that answers one wrong value for all of them. + { + kal_task t[kIdentities]{}; + int started = 0; + g_next_identity = 0; + for (int i = 0; i < kIdentities; ++i) g_identities[i] = 0; + for (int i = 0; i < kIdentities; ++i) + if (kal_task_start(records_its_identity, nullptr, &t[i]) == kal_ok) ++started; + for (int i = 0; i < started; ++i) kal_task_join(t[i]); + + bool distinct = started == kIdentities; + for (int i = 0; i < started; ++i) { + if (g_identities[i] == 0) distinct = false; + for (int k = i + 1; k < started; ++k) + if (g_identities[i] == g_identities[k]) distinct = false; + } + observe(kind::behaviour, distinct, + "contexts that ran at the same time have identities distinct from each other"); + } + + // Relinquishing the processor is permitted to do nothing, and an + // implementation without a scheduler returns immediately --- which is a + // supply and not a simulation, so the observation is that it returns. + { kal_task_yield(); observe(kind::behaviour, true, "relinquishing the processor returns"); } + + // A property that is claimed is a property that can be checked. + if (kal::task::has(kal::task::wait_timeout)) { + volatile kal_u32 never = 0; + const kal_duration t0 = kal_time_monotonic(); + const int e = kal_task_wait(const_cast(&never), 0u, + 30u * 1000u * 1000u); + const kal_duration elapsed = kal_time_monotonic() - t0; + observe(kind::behaviour, e == kal_err_again, + "a wait with a timeout reports that it elapsed when nothing woke it"); + observe(kind::behaviour, elapsed >= 20u * 1000u * 1000u, + "the wait lasted approximately the timeout it was given"); + } else { + unobserved(kind::behaviour, "a wait honours a timeout", + "the implementation does not claim prop_wait_timeout"); + } + + if (kal::task::has(kal::task::thread_local_storage)) { + g_per_context = 3; + kal_task t{}; + if (kal_task_start(records_its_own, nullptr, &t) == kal_ok) { + kal_task_join(t); + observe(kind::behaviour, + load_acquire(&g_saw) == 7 && g_per_context == 3, + "a started context observes its own thread-local storage"); + } else { + unobserved(kind::behaviour, "a started context observes its own thread-local storage", + "a context could not be started"); + } + } else { + unobserved(kind::behaviour, "a started context observes its own thread-local storage", + "the implementation does not claim prop_thread_local"); + } + + if (performs(kind::abi)) { + observe(kind::abi, sizeof(kal_task) == sizeof(kal_uintptr), + "a task handle occupies one machine word"); + const kal_uintptr assigned = (kal::task::preemptive | kal::task::parallel + | kal::task::wait_timeout + | 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"); + observe(kind::abi, kal_task_current() == kal_task_current(), + "the identity of the calling context is stable within it"); + + // A wake of no contexts is permitted and shall report that it woke + // none, which is what a caller uses to decide whether to try again. + volatile kal_u32 nobody = 0; + kal_uintptr woken = 99; + observe(kind::abi, + kal_task_wake(const_cast(&nobody), 0, &woken) == kal_ok + && woken == 0, + "waking no contexts succeeds and reports that none were woken"); + } + + if (performs(kind::stability)) { + // Starting and awaiting many contexts. An implementation that leaked a + // stack or a handle per context stops here and nowhere earlier. + bool all = true; + const int rounds = repetitions / 10; + for (int i = 0; i < rounds && all; ++i) { + kal_task t{}; + if (kal_task_start(does_nothing, nullptr, &t) != kal_ok) all = false; + else if (kal_task_join(t) != kal_ok) all = false; + } + observe(kind::stability, all, "contexts started and awaited many times keep starting"); + put(" contexts started and awaited: "); put_signed(rounds); put("\n"); + } + + if (performs(kind::cost)) { + const int rounds = cost_iterations / 10; + const kal_duration t0 = kal_time_monotonic(); + for (int i = 0; i < rounds; ++i) { + kal_task t{}; + if (kal_task_start(does_nothing, nullptr, &t) == kal_ok) kal_task_join(t); + } + measure("starting and awaiting an execution context", kal_time_monotonic() - t0, rounds); + + volatile kal_u32 w = 0; + const kal_duration t1 = kal_time_monotonic(); + for (int i = 0; i < cost_iterations; ++i) { + kal_uintptr woken = 0; + kal_task_wake(const_cast(&w), 1, &woken); + } + measure("waking an address nothing waits upon", kal_time_monotonic() - t1, cost_iterations); + } +#endif +} + +} // namespace okc::task diff --git a/conformance/src/sections/task.cppm b/conformance/src/sections/task.cppm new file mode 100644 index 0000000..b5de872 --- /dev/null +++ b/conformance/src/sections/task.cppm @@ -0,0 +1,10 @@ +// okc.task --- the section that examines openkal.task. +// +// The interface is one function. A section records observations through +// okc.report and returns; whether it examined anything is decided inside, by +// the same feature that decided whether the interface is provided at all. +export module okc.task; + +export namespace okc::task { +void run(); +} diff --git a/conformance/src/sections/time.cpp b/conformance/src/sections/time.cpp new file mode 100644 index 0000000..4f9d772 --- /dev/null +++ b/conformance/src/sections/time.cpp @@ -0,0 +1,108 @@ +module okc.time; + +import openkal.types; +import openkal.time; +import okc.report; +import okc.spec; + +namespace okc::time { + +void run() { + heading("openkal.time"); +#ifndef MCPP_FEATURE_TIME + unobserved(kind::behaviour, "openkal.time", "the interface was not selected"); + return; +#else + claim("kal_time_props", kal_time_props); + + // The monotonic source measures elapsed time and never decreases. The + // observation is made over many reads rather than two, because a source + // that decreased occasionally --- one assembled from two counters, or read + // without a barrier --- would satisfy a comparison of two. + { + kal_duration previous = kal_time_monotonic(); + bool never_decreased = true; + for (int i = 0; i < 4096; ++i) { + const kal_duration now = kal_time_monotonic(); + if (now < previous) { never_decreased = false; break; } + previous = now; + } + observe(kind::behaviour, never_decreased, "the monotonic source never decreases"); + } + + // A granularity of zero would tell a program that measures short intervals + // nothing, and the specification requires the source to report what it has. + { + const kal_duration g = kal_time_monotonic_granularity(); + observe(kind::behaviour, g >= 1, "the monotonic source reports a granularity"); + put(" reported granularity: "); put_signed(static_cast(g)); + put(" nanoseconds\n"); + } + + // Suspension is for at least the duration asked for and never for less. + // The comparison allows for the granularity the source reported, because a + // source that cannot resolve the interval cannot be asked to prove it. + { + const kal_duration requested = 20u * 1000u * 1000u; + const kal_duration slack = kal_time_monotonic_granularity(); + const kal_duration t0 = kal_time_monotonic(); + kal_time_sleep(requested); + const kal_duration elapsed = kal_time_monotonic() - t0; + observe(kind::behaviour, elapsed + slack >= requested, + "suspension lasts at least the duration requested"); + put(" requested 20000000 ns, observed "); put_signed(static_cast(elapsed)); + put(" ns\n"); + } + + // The wall source is claimed or it is not, and the claim is checked rather + // than assumed. An implementation whose environment has no such notion + // reports zero, which is why a value alone cannot answer the question. + if (kal::time::has(kal::time::wall_available)) { + const kal_duration w = kal_time_wall(); + // 1 January 2020, as nanoseconds since the epoch. A clock that reported + // an earlier time would be reporting something other than the time + // agreed with the rest of the world. + observe(kind::behaviour, w > 1577836800ull * 1000000000ull, + "the wall source reports a time later than 2020 when it claims to be available"); + } else { + unobserved(kind::behaviour, "the wall source reports a real time", + "the implementation does not claim prop_wall_available"); + } + + if (performs(kind::abi)) { + // Clause 6.2: a position that has not been assigned reads as zero, so + // that a program compiled against a later specification behaves + // correctly against an earlier implementation. An implementation that + // set an unassigned position would break that program silently. + const kal_uintptr assigned = (kal::time::wall_available + | kal::time::monotonic_suspends + | kal::time::sleep_precise).bits; + observe(kind::abi, (kal_time_props & ~assigned) == 0, + "the capability word contains no position the specification has not assigned"); + observe(kind::abi, sizeof(kal_duration) == 8, + "a duration is sixty-four bits, as the interface fixes it"); + } + + if (performs(kind::stability)) { + kal_duration previous = kal_time_monotonic(); + bool ordered = true; + for (int i = 0; i < repetitions && ordered; ++i) { + const kal_duration now = kal_time_monotonic(); + if (now < previous) ordered = false; + previous = now; + } + observe(kind::stability, ordered, "the monotonic source is ordered across many reads"); + } + + if (performs(kind::cost)) { + const kal_duration t0 = kal_time_monotonic(); + kal_duration sink = 0; + for (int i = 0; i < cost_iterations; ++i) sink += kal_time_monotonic(); + const kal_duration total = kal_time_monotonic() - t0; + observe(kind::cost, sink != 0, "the measured reads produced values"); + measure("reading the monotonic source", total, cost_iterations); + } +#endif +} + +} // namespace okc::time diff --git a/conformance/src/sections/time.cppm b/conformance/src/sections/time.cppm new file mode 100644 index 0000000..84ed9bb --- /dev/null +++ b/conformance/src/sections/time.cppm @@ -0,0 +1,10 @@ +// okc.time --- the section that examines openkal.time. +// +// The interface is one function. A section records observations through +// okc.report and returns; whether it examined anything is decided inside, by +// the same feature that decided whether the interface is provided at all. +export module okc.time; + +export namespace okc::time { +void run(); +} diff --git a/conformance/src/spec.cppm b/conformance/src/spec.cppm new file mode 100644 index 0000000..98348e8 --- /dev/null +++ b/conformance/src/spec.cppm @@ -0,0 +1,150 @@ +// okc.spec --- WHAT is examined, expressed as data. +// +// The inventory lives here so that adding an interface to the specification +// means adding a row and a section, and means editing neither the driver nor +// the report. The driver knows how to run a section; this module knows which +// sections exist, which of them the build selected, and what each one is for. +export module okc.spec; + +import openkal.types; + +export namespace okc { + +// The four kinds of examination. +// +// They are distinguished because they answer different questions and because a +// reader must be able to tell which question a line in the report belongs to. +// The fourth is the reason the distinction is not decoration: a cost is a +// number that varies with the machine, so it can be reported and cannot be a +// verdict, and a suite that failed on a loaded runner would be discarded rather +// than consulted. +enum class kind { + behaviour, // what the specification requires of the operation + abi, // the shapes clause 5.3 freezes, and the words clause 6.2 defines + stability, // the same operation many times + cost, // reported, never asserted +}; + +constexpr const char* name_of(kind k) { + switch (k) { + case kind::behaviour: return "behaviour"; + case kind::abi: return "abi"; + case kind::stability: return "stability"; + case kind::cost: return "cost"; + } + return "?"; +} + +// Which interface a section examines. The order is the order clause 3 lists +// them in, and the report follows it, so two runs of different implementations +// can be read side by side. +enum class interface_id { + abort, stream, memory, env, time, fs, process, task, count +}; + +struct interface_row { + const char* name; + bool core; // clause 3: every implementation provides it + bool selected; // this build examined it + const char* feature; // what to pass to examine it +}; + +// Selection is decided once, here, by the macros the build defines for the +// features it activated. Every other translation unit reads the answer rather +// than asking the question, so a section added later cannot forget to be +// listed and a feature renamed later cannot leave a stale conditional behind. +inline constexpr interface_row inventory[] = { + { "openkal.abort", true, +#ifdef MCPP_FEATURE_CORE + true, +#else + false, +#endif + "core" }, + { "openkal.stream", true, +#ifdef MCPP_FEATURE_CORE + true, +#else + false, +#endif + "core" }, + { "openkal.memory", true, +#ifdef MCPP_FEATURE_CORE + true, +#else + false, +#endif + "core" }, + { "openkal.env", false, +#ifdef MCPP_FEATURE_ENV + true, +#else + false, +#endif + "env" }, + { "openkal.time", false, +#ifdef MCPP_FEATURE_TIME + true, +#else + false, +#endif + "time" }, + { "openkal.fs", false, +#ifdef MCPP_FEATURE_FS + true, +#else + false, +#endif + "fs" }, + { "openkal.process", false, +#ifdef MCPP_FEATURE_PROCESS + true, +#else + false, +#endif + "process" }, + { "openkal.task", false, +#ifdef MCPP_FEATURE_TASK + true, +#else + false, +#endif + "task" }, +}; + +// Which kinds of examination this build performs. Behaviour is unconditional: +// a run that examined no behaviour would be a run that answered nothing. +inline constexpr bool performs(kind k) { + switch (k) { + case kind::behaviour: return true; + case kind::abi: +#ifdef MCPP_FEATURE_ABI + return true; +#else + return false; +#endif + case kind::stability: +#ifdef MCPP_FEATURE_STABILITY + return true; +#else + return false; +#endif + case kind::cost: +#ifdef MCPP_FEATURE_COST + return true; +#else + return false; +#endif + } + return false; +} + +// How many times a stability section repeats an operation. Large enough that a +// handle scheme which never reuses a slot exhausts, and small enough that the +// suite remains a thing one runs rather than schedules. +inline constexpr int repetitions = 20000; + +// How many times a cost section repeats an operation before dividing. +inline constexpr int cost_iterations = 2000; + +} // namespace okc diff --git a/conformance/src/suite.cpp b/conformance/src/suite.cpp new file mode 100644 index 0000000..c0f9e2a --- /dev/null +++ b/conformance/src/suite.cpp @@ -0,0 +1,36 @@ +module okc.suite; + +import okc.spec; +import okc.report; +import okc.abort; +import okc.stream; +import okc.memory; +import okc.env; +import okc.time; +import okc.fs; +import okc.process; +import okc.task; + +namespace okc { + +int run_all() { + write_inventory(); + + // The order is clause 3's, so that two runs against different + // implementations can be read side by side without either being sorted. + // openkal.abort is last among the core interfaces rather than first, + // because observing it requires starting a copy and the copy's own report + // would otherwise appear before this one's heading. + stream::run(); + memory::run(); + env::run(); + time::run(); + fs::run(); + process::run(); + task::run(); + termination::run(); + + return summarise(); +} + +} // namespace okc diff --git a/conformance/src/suite.cppm b/conformance/src/suite.cppm new file mode 100644 index 0000000..a7a747f --- /dev/null +++ b/conformance/src/suite.cppm @@ -0,0 +1,15 @@ +// okc.suite --- the driver. +// +// It knows how to run a section and in what order. Which sections exist is +// okc.spec's business, and what each one examines is the section's, so adding +// an interface to the specification means adding a section and a row and +// touching neither this module nor the report. +export module okc.suite; + +export namespace okc { + +// Runs every section, in the order clause 3 lists the interfaces, and returns +// the status the program exits with. +int run_all(); + +} // namespace okc diff --git a/examples/portable/mcpp.toml b/examples/portable/mcpp.toml index e45843a..bd7fe77 100644 --- a/examples/portable/mcpp.toml +++ b/examples/portable/mcpp.toml @@ -9,10 +9,13 @@ name = "portable" version = "0.1.0" [dependencies] -openkal = "0.4.0" +openkal = "0.5.0" [target.'cfg(os = "linux")'.dependencies] -openkal-linux = "0.4.0" +openkal-linux = "0.5.0" [target.'cfg(os = "macos")'.dependencies] -openkal-macos = "0.2.0" +openkal-macos = "0.3.0" + +[target.'cfg(windows)'.dependencies] +openkal-windows = "0.1.0" diff --git a/examples/portable/mcpp.toml.bak b/examples/portable/mcpp.toml.bak deleted file mode 100644 index 318c7ea..0000000 --- a/examples/portable/mcpp.toml.bak +++ /dev/null @@ -1,11 +0,0 @@ -# The program declares the contract and names no implementation. Whoever builds -# it appends the implementation: an implementation's continuous integration -# appends itself as a path dependency, and the specification's own appends a -# published one. That the implementation is named here and not in the source is -# the arrangement this example exists to show. -[package] -name = "portable" -version = "0.1.0" - -[dependencies] -openkal = "0.3.0" diff --git a/include/openkal.h b/include/openkal.h new file mode 100644 index 0000000..d0fddd3 --- /dev/null +++ b/include/openkal.h @@ -0,0 +1,24 @@ +/* openkal --- the whole interface, for a consumer that cannot import a module. + * + * The specification states its contract as a C application binary interface + * and distributes C++ modules that declare it. A C++ consumer imports the + * module for the interface it uses. A consumer written in C cannot: a C + * translation unit has no import. The canonical such consumer is a C library + * being ported onto openkal, which is the case the specification exists to + * support, so the declarations are distributed in both forms. Clause 4.3 + * requires the two to declare the same entities and states how that is + * verified. */ +#ifndef OPENKAL_H +#define OPENKAL_H + +#include "openkal/types.h" +#include "openkal/abort.h" +#include "openkal/stream.h" +#include "openkal/memory.h" +#include "openkal/env.h" +#include "openkal/time.h" +#include "openkal/fs.h" +#include "openkal/process.h" +#include "openkal/task.h" + +#endif /* OPENKAL_H */ diff --git a/include/openkal/abort.h b/include/openkal/abort.h new file mode 100644 index 0000000..f22dffd --- /dev/null +++ b/include/openkal/abort.h @@ -0,0 +1,35 @@ +/* openkal.abort --- termination. Core: every environment can terminate. */ +#ifndef OPENKAL_ABORT_H +#define OPENKAL_ABORT_H +#include "types.h" + +/* The attribute is spelled differently in the two languages and is written out + * here rather than omitted. A caller that does not know the function cannot + * return is a caller the compiler will warn about at the point where control + * appears to continue, which is the point the function exists to remove. */ +#if defined(__cplusplus) +# define KAL_NORETURN [[noreturn]] +#elif defined(_MSC_VER) +# define KAL_NORETURN __declspec(noreturn) +#elif defined(__STDC_VERSION__) && __STDC_VERSION__ >= 201112L +# define KAL_NORETURN _Noreturn +#else +# define KAL_NORETURN +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +/* Terminates the program after reporting the given message. The length is + * explicit so that the interface does not depend on a string function. */ +KAL_NORETURN void kal_abort(const char* msg, kal_uintptr len); + +/* Terminates the program with the given status. Termination is immediate: + * registered exit handlers and static destructors do not run. */ +KAL_NORETURN void kal_exit(int code); + +#ifdef __cplusplus +} +#endif +#endif /* OPENKAL_ABORT_H */ diff --git a/include/openkal/env.h b/include/openkal/env.h new file mode 100644 index 0000000..04e1553 --- /dev/null +++ b/include/openkal/env.h @@ -0,0 +1,28 @@ +/* openkal.env --- the parameters a program receives at inception. */ +#ifndef OPENKAL_ENV_H +#define OPENKAL_ENV_H +#include "types.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/* The number of arguments, and the argument at a given position. Position + * zero is the name by which the program was started; an environment that has + * no such name reports an empty string rather than omitting it. */ +kal_uintptr kal_env_arg_count(void); +const char* kal_env_arg(kal_uintptr index, kal_uintptr* len); + +/* The value of a named variable, or a null pointer. */ +const char* kal_env_var(const char* name, kal_uintptr name_len, kal_uintptr* value_len); + +/* Enumeration, for a program that must copy the whole set. The order is + * unspecified and is not required to be stable between calls. */ +kal_uintptr kal_env_var_count(void); +const char* kal_env_var_at(kal_uintptr index, kal_uintptr* name_len, + const char** value, kal_uintptr* value_len); + +#ifdef __cplusplus +} +#endif +#endif /* OPENKAL_ENV_H */ diff --git a/include/openkal/fs.h b/include/openkal/fs.h new file mode 100644 index 0000000..2ed4609 --- /dev/null +++ b/include/openkal/fs.h @@ -0,0 +1,167 @@ +/* openkal.fs --- directories and open files. + * + * Every operation is relative to a directory the program holds. There is no + * global namespace of paths: a global namespace is unavailable in a + * capability-based kernel and an implementation upon one would have to + * construct it, which clause 7.1 excludes. Resolving an absolute path is + * therefore work a C library performs against a directory the environment + * supplied, once, rather than work each program performs. */ +#ifndef OPENKAL_FS_H +#define OPENKAL_FS_H +#include "types.h" + +/* A directory, or an open file. Both are owned: the program obtained them and + * releases them. */ +struct kal_dir { kal_uintptr h; }; +struct kal_file { kal_uintptr h; }; + +/* What a name refers to. */ +enum kal_node_kind { + kal_node_absent = 0, + kal_node_file = 1, + kal_node_directory = 2, + kal_node_link = 3, + kal_node_other = 4 +}; + +struct kal_node_info { + kal_uintptr size; + kal_u64 modified_ns; /* wall time, as openkal.time defines it */ + int kind; + int writable; +}; + +/* Positions in kal_fs_props. */ +#define KAL_FS_PROP_CASE_SENSITIVE ((kal_uintptr)1u << 0) +#define KAL_FS_PROP_LINKS ((kal_uintptr)1u << 1) +#define KAL_FS_PROP_MODIFIED_TIME ((kal_uintptr)1u << 2) +#define KAL_FS_PROP_ATOMIC_RENAME ((kal_uintptr)1u << 3) + +/* Positions in the flags word of kal_fs_open. */ +#define KAL_OPEN_READ ((kal_uintptr)1u << 0) +#define KAL_OPEN_WRITE ((kal_uintptr)1u << 1) +#define KAL_OPEN_CREATE ((kal_uintptr)1u << 2) +#define KAL_OPEN_EXCLUSIVE ((kal_uintptr)1u << 3) /* fail if the name exists */ +#define KAL_OPEN_TRUNCATE ((kal_uintptr)1u << 4) +#define KAL_OPEN_APPEND ((kal_uintptr)1u << 5) + +/* Positions in the whence argument of kal_fs_seek. */ +#define KAL_SEEK_SET 0 +#define KAL_SEEK_CURRENT 1 +#define KAL_SEEK_END 2 + +#ifdef __cplusplus +extern "C" { +#endif + +/* The directories the environment supplied at inception, and the only ones a + * program can reach. A set rather than a single directory: a program that + * starts another must reach the program it starts, and the two are commonly + * not beneath one root. + * + * Each has a name, which is how the environment identifies it and how a C + * library above openkal decides which one an absolute path belongs to. The + * names are the environment's; this specification requires only that they be + * distinct. An implementation whose environment has a global path namespace + * reports names drawn from it, so that a C library can both resolve an + * absolute path and report one. */ +kal_uintptr kal_fs_preopen_count(void); +int kal_fs_preopen(kal_uintptr index, struct kal_dir* out, + const char** name, kal_uintptr* len); + +/* Opening. A name is a single component or a sequence separated by a forward + * slash; it shall not begin with a separator and shall not contain a + * component that ascends. + * + * One name is reserved: "." denotes the directory itself. Without it a program + * holding a directory has no way to ask an operation about that directory --- + * what it is, when it changed, whether it can be written --- and the operations + * that answer those questions all take a name. It is one reserved word rather + * than five more operations, every environment can express it, and it does not + * introduce a way to ascend. Clause 7.12. */ +int kal_fs_open_dir (struct kal_dir base, const char* name, kal_uintptr len, + struct kal_dir* out); +int kal_fs_open_file(struct kal_dir base, const char* name, kal_uintptr len, + int write, int create, struct kal_file* out); + +/* Opening, stating the whole of the intent in one word. + * + * The two-flag form above cannot express three conditions a C library must + * express, and each of them, emulated, leaves the caller silently wrong rather + * than merely bounded: truncation performed after opening leaves the tail of + * a shorter rewrite behind if the program stops in between; exclusion tested + * before opening is not exclusion; and appending performed by seeking is not + * appending when a second writer exists. Clause 3.1 classifies each of those + * as a simulation, so the specification states the intent instead. */ +int kal_fs_open(struct kal_dir base, const char* name, kal_uintptr len, + kal_uintptr flags, struct kal_file* out); + +/* Release. An implementation shall not treat a released handle as valid. */ +void kal_fs_close_dir (struct kal_dir); +void kal_fs_close_file(struct kal_file); + +/* A file is read and written through openkal.stream. The stream remains valid + * while the file is open and is not separately released; the file owns it. */ +kal_uintptr kal_fs_stream(struct kal_file); + +/* Positioning. It appears here and not in openkal.stream because on a hosted + * system whether a stream can be repositioned is a property of the individual + * stream, and an interface offering it on every stream would contain an + * operation some of its resources can never satisfy. */ +int kal_fs_seek(struct kal_file, kal_i64 offset, int whence, + kal_u64* result); + +/* Sets the length of an open file, extending it with zero bytes or discarding + * what lies beyond. */ +int kal_fs_truncate(struct kal_file, kal_u64 size); + +/* Enquiry, creation and removal, all relative to a directory. Enquiry about a + * name that does not exist succeeds and reports kal_node_absent rather than + * failing: a caller that asks what a name refers to has been answered when + * told that it refers to nothing. Clause 7.7. */ +int kal_fs_info (struct kal_dir base, const char* name, kal_uintptr len, + struct kal_node_info* out); +int kal_fs_mkdir (struct kal_dir base, const char* name, kal_uintptr len); +int kal_fs_remove(struct kal_dir base, const char* name, kal_uintptr len); +int kal_fs_rename(struct kal_dir from, const char* a, kal_uintptr alen, + struct kal_dir to, const char* b, kal_uintptr blen); + +/* Enquiry about an open file. It is not expressible through kal_fs_info: the + * name a file was opened by may since have been removed or reused, and a C + * library answering fstat from the name would answer about a different file. */ +int kal_fs_file_info(struct kal_file, struct kal_node_info* out); + +/* Sets the time kal_fs_file_info reports for an open file. + * + * The inverse of an enquiry that already exists, and the interface is + * incomplete without it: a program that copies a file and preserves its dates, + * or that extracts an archive, or that marks a file as current, has no way to + * say so. Each of those is a program a C library above openkal is expected to + * host, and none of them can be written from the operations above. + * + * The file rather than the name, for the reason stated at kal_fs_file_info: the + * name may since refer to something else, and setting the time of the wrong + * file is worse than not setting it. + * + * The file shall have been opened with KAL_OPEN_WRITE. One environment decides + * at the point of opening what may afterwards be done with the file, and cannot + * be asked later; requiring the intent to be stated when the file is opened is + * the same rule clause 7.8 states for the other three conditions. + * + * An implementation whose environment does not record a modification time does + * not claim KAL_FS_PROP_MODIFIED_TIME and reports kal_err_not_supported here. + * An implementation that claims the position shall be able to perform this. */ +int kal_fs_set_modified(struct kal_file, kal_u64 modified_ns); + +/* Enumeration. The iterator is owned and is released by reading past the end + * or by closing the directory that produced it. */ +int kal_fs_list_begin(struct kal_dir, kal_uintptr* iter); +int kal_fs_list_next (struct kal_dir, kal_uintptr* iter, const char** name, + kal_uintptr* len, int* kind); + +extern const kal_uintptr kal_fs_props; + +#ifdef __cplusplus +} +#endif +#endif /* OPENKAL_FS_H */ diff --git a/include/openkal/memory.h b/include/openkal/memory.h new file mode 100644 index 0000000..168bbdc --- /dev/null +++ b/include/openkal/memory.h @@ -0,0 +1,26 @@ +/* openkal.memory --- allocation. Core: any environment with writable memory + * can supply an allocator, and one that can be exhausted bounds what is + * available rather than making its callers silently wrong. */ +#ifndef OPENKAL_MEMORY_H +#define OPENKAL_MEMORY_H +#include "types.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/* Returns a region of at least size bytes aligned to align, or a null + * pointer. align shall be a power of two. Where the environment already + * provides an allocator, an implementation is built upon it and not beside + * it: two allocators drawing on one region of memory is a defect that appears + * only under load. */ +void* kal_alloc(kal_uintptr size, kal_uintptr align); + +/* Releases a region obtained from kal_alloc. The size and alignment are those + * passed to the allocation. */ +void kal_free(void* p, kal_uintptr size, kal_uintptr align); + +#ifdef __cplusplus +} +#endif +#endif /* OPENKAL_MEMORY_H */ diff --git a/include/openkal/process.h b/include/openkal/process.h new file mode 100644 index 0000000..6cbd28c --- /dev/null +++ b/include/openkal/process.h @@ -0,0 +1,52 @@ +/* openkal.process --- a program image that has been started. The operations + * are to start one, to wait for it, and to request its termination. + * Duplication of the calling image is not among them: duplicating an address + * space and its execution state cannot be performed faithfully on every + * environment this specification targets. */ +#ifndef OPENKAL_PROCESS_H +#define OPENKAL_PROCESS_H +#include "types.h" +#include "fs.h" + +struct kal_process { kal_uintptr h; }; + +/* How a started program's standard streams are supplied. A stream handle of + * zero denotes that the program inherits the corresponding stream of its + * parent, which is what an environment without a general mechanism for + * passing handles can always provide. */ +struct kal_spawn_streams { + kal_uintptr in; + kal_uintptr out; + kal_uintptr err; +}; + +/* Positions in kal_process_props. */ +#define KAL_PROCESS_PROP_TERMINATE ((kal_uintptr)1u << 0) +#define KAL_PROCESS_PROP_STREAM_PASSING ((kal_uintptr)1u << 1) +#define KAL_PROCESS_PROP_EXIT_STATUS ((kal_uintptr)1u << 2) + +#ifdef __cplusplus +extern "C" { +#endif + +/* Starts the program named relative to a directory. The argument vector is + * complete and is passed unaltered: argv[0] is the name the started program + * observes as its own, and an implementation neither derives it from path nor + * prepends anything to the vector. Clause 7.6. */ +int kal_process_spawn(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); + +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); + +extern const kal_uintptr kal_process_props; + +#ifdef __cplusplus +} +#endif +#endif /* OPENKAL_PROCESS_H */ diff --git a/include/openkal/stream.h b/include/openkal/stream.h new file mode 100644 index 0000000..142d5fa --- /dev/null +++ b/include/openkal/stream.h @@ -0,0 +1,51 @@ +/* openkal.stream --- byte streams. Core. */ +#ifndef OPENKAL_STREAM_H +#define OPENKAL_STREAM_H +#include "types.h" + +/* An opaque handle occupying one machine word. The width is fixed and the + * interpretation is not, which is what allows an implementation to be placed + * above a C library, beneath one, or without one. */ +struct kal_stream { kal_uintptr h; }; + +#ifdef __cplusplus +extern "C" { +#endif + +/* The program's standard streams. These handles are borrowed. */ +struct kal_stream kal_stdin (void); +struct kal_stream kal_stdout(void); +struct kal_stream kal_stderr(void); + +/* Transfers the whole buffer, or reports the condition that prevented it. On + * failure, n reports how many bytes were transferred before the failure. */ +struct kal_io_result kal_stream_write(struct kal_stream s, const void* buf, kal_uintptr len); + +/* Transfers at most len bytes and reports how many were transferred. Zero + * bytes with kal_ok denotes end of input. */ +struct kal_io_result kal_stream_read(struct kal_stream s, void* buf, kal_uintptr len); + +/* Commits any buffering the implementation performs. */ +int kal_stream_flush(struct kal_stream s); + +/* Properties of one stream. + * + * An enquiry rather than a word, because the property varies between the + * resources of the interface and not between implementations: the same + * implementation answers differently for a terminal and for a file. It is not + * an operation upon the stream and therefore is not the defect clause 6.4 + * describes; nothing is transferred and no resource can fail to answer. + * + * A C library requires the answer before it has transferred anything: it must + * choose a buffering discipline, and one that defers a prompt until a buffer + * fills leaves an interactive program appearing not to respond. A library that + * had to assume would be silently wrong for one of the two cases. */ +kal_uintptr kal_stream_props(struct kal_stream s); + +/* Positions in the result of kal_stream_props. */ +#define KAL_STREAM_PROP_INTERACTIVE ((kal_uintptr)1u << 0) + +#ifdef __cplusplus +} +#endif +#endif /* OPENKAL_STREAM_H */ diff --git a/include/openkal/task.h b/include/openkal/task.h new file mode 100644 index 0000000..ac3a92d --- /dev/null +++ b/include/openkal/task.h @@ -0,0 +1,63 @@ +/* openkal.task --- execution contexts, and the primitive they are built upon. + * + * Mutexes and condition variables are absent: they are constructions above the + * primitive rather than facilities of the boundary, which is observable in any + * C library that implements them. */ +#ifndef OPENKAL_TASK_H +#define OPENKAL_TASK_H +#include "types.h" + +struct kal_task { kal_uintptr h; }; + +/* Positions in kal_task_props. */ +#define KAL_TASK_PROP_PREEMPTIVE ((kal_uintptr)1u << 0) +#define KAL_TASK_PROP_PARALLEL ((kal_uintptr)1u << 1) +#define KAL_TASK_PROP_WAIT_TIMEOUT ((kal_uintptr)1u << 2) +/* A context started by kal_task_start observes the thread-local storage of the + * toolchain that compiled the program: a variable declared thread_local has a + * distinct instance in it, correctly initialised. The register convention that + * delivers this belongs to openarch rather than to openkal, so the + * specification reports whether the implementation's contexts have it rather + * than providing an operation that establishes it. A C library ported onto + * openkal keeps its per-context state in one such variable, and cannot be + * ported onto an implementation that lacks the property. */ +#define KAL_TASK_PROP_THREAD_LOCAL ((kal_uintptr)1u << 3) + +#ifdef __cplusplus +extern "C" { +#endif + +/* Starts a context executing the given function with the given argument. 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. */ +int kal_task_start(void (*entry)(void*), void* arg, 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); + +/* Relinquishes the processor. An implementation without a scheduler returns + * immediately, which is a supply and not a simulation. */ +void kal_task_yield(void); + +/* The identity of the calling context. Unique among contexts running at the + * same moment; may be reused after one finishes. */ +kal_uintptr kal_task_current(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 + * the value to change unobserved. A timeout of zero denotes no timeout. */ +int kal_task_wait(const kal_u32* word, kal_u32 expected, + kal_u64 timeout_ns); + +/* Wakes at most count contexts suspended upon the given address and reports + * how many were woken. A count of zero wakes none and is permitted. */ +int kal_task_wake(const kal_u32* word, kal_uintptr count, kal_uintptr* woken); + +extern const kal_uintptr kal_task_props; + +#ifdef __cplusplus +} +#endif +#endif /* OPENKAL_TASK_H */ diff --git a/include/openkal/time.h b/include/openkal/time.h new file mode 100644 index 0000000..50389b1 --- /dev/null +++ b/include/openkal/time.h @@ -0,0 +1,31 @@ +/* openkal.time --- time sources. Two are distinguished because they answer + * different questions and an environment may provide one and not the other. */ +#ifndef OPENKAL_TIME_H +#define OPENKAL_TIME_H +#include "types.h" + +/* Nanoseconds. The unit is fixed rather than reported, because a program that + * must consult a unit before performing arithmetic acquires a branch that no + * environment needs. */ +typedef kal_u64 kal_duration; + +/* Positions in kal_time_props. */ +#define KAL_TIME_PROP_WALL_AVAILABLE ((kal_uintptr)1u << 0) +#define KAL_TIME_PROP_MONOTONIC_SUSPENDS ((kal_uintptr)1u << 1) +#define KAL_TIME_PROP_SLEEP_PRECISE ((kal_uintptr)1u << 2) + +#ifdef __cplusplus +extern "C" { +#endif + +kal_duration kal_time_monotonic(void); +kal_duration kal_time_wall(void); +kal_duration kal_time_monotonic_granularity(void); +void kal_time_sleep(kal_duration ns); + +extern const kal_uintptr kal_time_props; + +#ifdef __cplusplus +} +#endif +#endif /* OPENKAL_TIME_H */ diff --git a/include/openkal/types.h b/include/openkal/types.h new file mode 100644 index 0000000..ad88e8b --- /dev/null +++ b/include/openkal/types.h @@ -0,0 +1,96 @@ +/* openkal.types --- definitions shared by every openkal interface. + * + * The C form of the declarations the module of the same name carries. Clause + * 4.3 requires the two to declare the same entities; SURFACE.txt is the list + * both are compared against, so neither is the other's source and a divergence + * is reported by the conformance procedure rather than discovered by a + * consumer. + * + * The file includes no header. openkal is required to be usable on a + * freestanding target, and a consumer compiled with -nostdinc --- a C library + * being ported onto openkal is exactly that consumer --- has none to include. + */ +#ifndef OPENKAL_TYPES_H +#define OPENKAL_TYPES_H + +/* The width of a machine word, obtained from the compiler rather than from a + * header, for the reason stated above. + * + * Two compilers state it and one does not. Where the compiler publishes its own + * spelling of the type, that spelling is used and the definition is exact. The + * remaining compiler publishes the property the type is defined by --- the width + * of a pointer --- and not the type, so the type is written from the property. + * Taking it from that compiler's own header instead would give this file an + * include, and the consumer this file exists for has none. */ +#if defined(__UINTPTR_TYPE__) +typedef __UINTPTR_TYPE__ kal_uintptr; +#elif defined(_MSC_VER) +# if defined(_WIN64) +typedef unsigned __int64 kal_uintptr; +# else +typedef unsigned int kal_uintptr; +# endif +#else +# error "openkal requires a compiler that states the width of a pointer" +#endif + +/* The three fixed widths openkal's operations use, stated once here rather than + * at each use. + * + * They are openkal's names for openkal's types. Reaching for the compiler's + * spelling at each use had two defects: it reached for a spelling one of the + * three compilers this specification is built with does not have, and it stated + * in eight places a decision that belongs in one. A reader asking what width an + * offset has now has one place to look. */ +#if defined(__UINT64_TYPE__) +typedef __UINT32_TYPE__ kal_u32; +typedef __UINT64_TYPE__ kal_u64; +typedef __INT64_TYPE__ kal_i64; +#elif defined(_MSC_VER) +typedef unsigned int kal_u32; +typedef unsigned __int64 kal_u64; +typedef signed __int64 kal_i64; +#else +# error "openkal requires a compiler that states a sixty-four bit type" +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +/* The complete set of error conditions openkal defines. The set is closed: an + * implementation maps its environment's error values onto these and does not + * extend them. */ +enum kal_error { + kal_ok = 0, + kal_err_invalid = 1, /* the handle or an argument is not valid */ + kal_err_again = 2, /* the operation would block */ + kal_err_io = 3, /* the device or medium reported a failure */ + kal_err_no_memory = 4, + kal_err_no_space = 5, + kal_err_permission = 6, + kal_err_not_supported = 7, + kal_err_closed = 8, /* the peer closed the connection */ + kal_err_not_found = 9, /* the name does not exist */ + kal_err_exists = 10, /* the name exists and the caller required + * that it not */ + kal_err_not_empty = 11, /* a directory that is required to be empty + * is not */ + kal_err_is_directory = 12, /* the name refers to a directory and the + * operation applies to a file */ + kal_err_not_directory = 13 /* the reverse of the preceding condition */ +}; + +/* The result of an operation that transfers a count. The layout is frozen: + * two machine words are returned in registers on the architectures openkal + * targets, and a wider result would be returned through a hidden pointer. */ +struct kal_io_result { + kal_uintptr n; + int e; +}; + +#ifdef __cplusplus +} +#endif + +#endif /* OPENKAL_TYPES_H */ diff --git a/mcpp.toml b/mcpp.toml index 0710c94..4e750bf 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,7 +1,7 @@ [package] namespace = "mcpplibs" name = "openkal" -version = "0.4.0" +version = "0.5.0" description = "openkal: a portable kernel ABI specification. This package carries the normative declarations; implementations are separate packages." license = "Apache-2.0" authors = ["mcpplibs"] @@ -13,3 +13,20 @@ repo = "https://github.com/mcpplibs/openkal" # produces a library with undefined references, which is the intended result. [targets.openkal] kind = "lib" + +# The C form of the declarations, clause 4.2, placed where a consumer will find +# it. The key states where the declarations are; it does not add a dependency +# and does not change what this package requires, which remains nothing. The +# headers include no header of their own, name no library, and obtain the width +# of a machine word from the compiler, so they remain usable on a target that +# has no C library and under any toolchain. A build system that is not this one +# reaches the same declarations by adding the directory itself; this key is how +# this distribution channel says so, as `kind = "lib"` above is. +# +# The declarations are not left to an implementation to supply. Clause 4.1 +# records why: a name the consumer relies upon would then be under the control +# of a party the specification does not govern, and a C library ported onto +# openkal would take its declarations from whichever implementation it was +# built against. +[build] +include_dirs = ["include"] diff --git a/src/abort.cppm b/src/abort.cppm index c1006c3..6fa073c 100644 --- a/src/abort.cppm +++ b/src/abort.cppm @@ -4,26 +4,23 @@ // because every environment can terminate: an implementation with nothing else // available may halt the processor, and halting is an implementation rather // than a simulation. +module; +#include + export module openkal.abort; export import openkal.types; -export extern "C" { - // Terminates the program after reporting the given message. The message is // passed with an explicit length so that the interface does not depend on a -// string function being available. -// -// The function does not return. Returning would continue into the state the -// caller has just declared impossible. -[[noreturn]] void kal_abort(const char* msg, kal_uintptr len); +// string function being available. The function does not return: returning +// would continue into the state the caller has just declared impossible. +export using ::kal_abort; // Terminates the program with the given status. Termination is immediate: // registered exit handlers and static destructors do not run. An // implementation that runs them is not conforming, because a caller cannot // then reason about what executes after the call. -[[noreturn]] void kal_exit(int code); - -} +export using ::kal_exit; export namespace kal { diff --git a/src/env.cppm b/src/env.cppm index 9d17110..2b34e38 100644 --- a/src/env.cppm +++ b/src/env.cppm @@ -11,28 +11,17 @@ // cannot be altered without a synchronisation rule this specification declines // to impose. The values a started program receives are supplied at the point of // starting it, which is openkal.process. +module; +#include + export module openkal.env; export import openkal.types; -export extern "C" { - -// The number of arguments, and the argument at a given position. Position zero -// is the name by which the program was started, which an environment that has -// no such name reports as an empty string rather than omitting. -kal_uintptr kal_env_arg_count(void); -const char* kal_env_arg(kal_uintptr index, kal_uintptr* len); - -// The value of a named variable, or a null pointer. The name is passed with an -// explicit length so that the interface does not require a string function. -const char* kal_env_var(const char* name, kal_uintptr name_len, kal_uintptr* value_len); - -// Enumeration, for a program that must copy the whole set. The order is -// unspecified and is not required to be stable between calls. -kal_uintptr kal_env_var_count(void); -const char* kal_env_var_at(kal_uintptr index, kal_uintptr* name_len, - const char** value, kal_uintptr* value_len); - -} +export using ::kal_env_arg_count; +export using ::kal_env_arg; +export using ::kal_env_var; +export using ::kal_env_var_count; +export using ::kal_env_var_at; export namespace kal::env { diff --git a/src/fs.cppm b/src/fs.cppm index 15aa2c2..d89c205 100644 --- a/src/fs.cppm +++ b/src/fs.cppm @@ -14,94 +14,53 @@ // library against a root directory the environment supplies, once, rather than // by each program or by this specification. It is also what allows a program to // be confined without its cooperation. +module; +#include + export module openkal.fs; export import openkal.types; // A directory, or an open file. Both are owned: the program obtained them and // releases them. -export struct kal_dir { kal_uintptr h; }; -export struct kal_file { kal_uintptr h; }; +export using ::kal_dir; +export using ::kal_file; // What a name refers to, and what is known about it. -export enum kal_node_kind : int { - kal_node_absent = 0, - kal_node_file = 1, - kal_node_directory = 2, - kal_node_link = 3, - kal_node_other = 4, // a device, a socket, or anything the environment does not classify -}; - -export struct kal_node_info { - kal_uintptr size; - __UINT64_TYPE__ modified_ns; // wall time, as openkal.time defines it - int kind; - int writable; -}; - -export extern "C" { - -// The directories the environment supplied at inception, and the only ones a -// program can reach: every other directory is opened relative to one of these. -// -// A set rather than a single directory. One directory is insufficient, and the -// insufficiency is not hypothetical: a program that starts another must reach -// the program it starts, and the two are commonly not beneath one root. An -// interface offering a single directory would oblige such a program to receive -// the whole file system as its root, which would defeat the confinement the -// arrangement exists to provide. -// -// Each has a name, which is how the environment identifies it and how a C -// library above openkal decides which one an absolute path belongs to. The -// names are the environment's, and this specification does not constrain them -// beyond requiring that they be distinct. -kal_uintptr kal_fs_preopen_count(void); -int kal_fs_preopen(kal_uintptr index, kal_dir* out, - const char** name, kal_uintptr* len); - -// Opening. A name is a single component or a sequence separated by a forward -// slash; it shall not begin with a separator and shall not contain a component -// that ascends. An implementation shall reject a name that does, because a -// program that could ascend from the directory it was given would not be -// confined by having been given it. -int kal_fs_open_dir (kal_dir base, const char* name, kal_uintptr len, kal_dir* out); -int kal_fs_open_file(kal_dir base, const char* name, kal_uintptr len, int write, int create, kal_file* out); - -// Release. An implementation shall not treat a released handle as valid. -void kal_fs_close_dir (kal_dir); -void kal_fs_close_file(kal_file); - -// A file is read and written through openkal.stream. This function obtains the -// stream, which remains valid while the file is open and is not separately -// released; the file owns it. -kal_uintptr kal_fs_stream(kal_file); - -// Positioning. It appears here and not in openkal.stream because whether a -// stream can be repositioned is a property of the individual stream on a hosted -// system, and an interface that offered it on every stream would contain an -// operation some of its resources can never satisfy. -int kal_fs_seek(kal_file, __INT64_TYPE__ offset, int whence, __UINT64_TYPE__* result); - -// Enquiry, creation and removal, all relative to a directory. -// -// Enquiry about a name that does not exist succeeds and reports kal_node_absent -// rather than failing: a caller that asks what a name refers to has been -// answered when told that it refers to nothing. Clause 7.7. Opening is access -// rather than enquiry and reports kal_err_not_found instead. -int kal_fs_info (kal_dir base, const char* name, kal_uintptr len, kal_node_info* out); -int kal_fs_mkdir (kal_dir base, const char* name, kal_uintptr len); -int kal_fs_remove(kal_dir base, const char* name, kal_uintptr len); -int kal_fs_rename(kal_dir from, const char* a, kal_uintptr alen, - kal_dir to, const char* b, kal_uintptr blen); - -// Enumeration. The iterator is owned and is released by reading past the end or -// by closing the directory that produced it. -int kal_fs_list_begin(kal_dir, kal_uintptr* iter); -int kal_fs_list_next (kal_dir, kal_uintptr* iter, const char** name, kal_uintptr* len, int* kind); - -// Properties of this implementation's file system. -extern const kal_uintptr kal_fs_props; - -} +export using ::kal_node_kind; +export using ::kal_node_absent; +export using ::kal_node_file; +export using ::kal_node_directory; +export using ::kal_node_link; +export using ::kal_node_other; +export using ::kal_node_info; + +export using ::kal_fs_preopen_count; +export using ::kal_fs_preopen; +export using ::kal_fs_open_dir; +export using ::kal_fs_open_file; +export using ::kal_fs_open; +export using ::kal_fs_close_dir; +export using ::kal_fs_close_file; +export using ::kal_fs_stream; +export using ::kal_fs_seek; +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_mkdir; +export using ::kal_fs_remove; +export using ::kal_fs_rename; +export using ::kal_fs_list_begin; +export using ::kal_fs_list_next; +export using ::kal_fs_props; + +static_assert(sizeof(kal_dir) == sizeof(kal_uintptr), "clause 7.2"); +static_assert(sizeof(kal_file) == sizeof(kal_uintptr), "clause 7.2"); +// Clause 5.3: the layout is frozen. An implementation and a consumer built at +// different times must agree on where each field is, and nothing else reports +// a disagreement. +static_assert(__builtin_offsetof(kal_node_info, size) == 0); +static_assert(__builtin_offsetof(kal_node_info, modified_ns) == sizeof(kal_uintptr)); export namespace kal::fs { @@ -109,14 +68,35 @@ using dir = kal_dir; using file = kal_file; using info = kal_node_info; -enum : kal_uintptr { - prop_case_sensitive = 1u << 0, - prop_links = 1u << 1, // openkal.fs.links is provided - prop_modified_time = 1u << 2, // kal_node_info::modified_ns is meaningful - prop_atomic_rename = 1u << 3, -}; +struct props_tag; +using props = kal::props; + +inline constexpr props case_sensitive{KAL_FS_PROP_CASE_SENSITIVE}; +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}; + +enum : int { seek_set = KAL_SEEK_SET, seek_current = KAL_SEEK_CURRENT, seek_end = KAL_SEEK_END }; + +// The flags of kal_fs_open, composable and distinguishable from a capability +// word: an intent and a property are different kinds of thing, and a word that +// serves as both is a word a program can pass to the wrong operation. +struct open_tag; +using open_flags = kal::props; + +namespace open { +inline constexpr open_flags read {KAL_OPEN_READ}; +inline constexpr open_flags write {KAL_OPEN_WRITE}; +inline constexpr open_flags create {KAL_OPEN_CREATE}; +inline constexpr open_flags exclusive{KAL_OPEN_EXCLUSIVE}; +inline constexpr open_flags truncate {KAL_OPEN_TRUNCATE}; +inline constexpr open_flags append {KAL_OPEN_APPEND}; +} -enum : int { seek_set = 0, seek_current = 1, seek_end = 2 }; +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); +} inline kal_uintptr preopen_count() { return kal_fs_preopen_count(); } @@ -127,6 +107,8 @@ inline dir working() { kal_fs_preopen(0, &d, &n, &l); return d; } -inline bool has(kal_uintptr p) { return (kal_fs_props & p) != 0; } + +inline props properties() { return props{kal_fs_props}; } +inline bool has(props p) { return properties().has(p); } } diff --git a/src/memory.cppm b/src/memory.cppm index 04488e5..bab90f3 100644 --- a/src/memory.cppm +++ b/src/memory.cppm @@ -10,28 +10,14 @@ // // Whether an environment "has a heap" is therefore not a property of the // hardware. Any environment with writable memory can supply one. +module; +#include + export module openkal.memory; export import openkal.types; -export extern "C" { - -// Returns a region of at least `size` bytes aligned to `align`, or a null -// pointer. `align` shall be a power of two. -// -// Where the environment already provides an allocator, an implementation of -// this function is required to be built upon it rather than beside it. Two -// allocators drawing on one region of memory is a defect that appears only -// under load: a C library's formatted output is commonly coupled to its own -// allocator, so an implementation that introduces a second one places two -// independent claimants on the same memory. -void* kal_alloc(kal_uintptr size, kal_uintptr align); - -// Releases a region obtained from kal_alloc. The size and alignment are those -// passed to the allocation, and an implementation that does not need them -// discards them at no cost. -void kal_free(void* p, kal_uintptr size, kal_uintptr align); - -} +export using ::kal_alloc; +export using ::kal_free; export namespace kal { diff --git a/src/process.cppm b/src/process.cppm index 553a77b..b86b2a9 100644 --- a/src/process.cppm +++ b/src/process.cppm @@ -10,74 +10,38 @@ // behaviour of a large portable program corroborates the choice: it starts its // subordinate programs by spawning them and calls neither of the duplicating // operations. +module; +#include + export module openkal.process; export import openkal.types; export import openkal.fs; -export struct kal_process { kal_uintptr h; }; - -// How a started program's standard streams are supplied. A stream handle of -// zero denotes that the program inherits the corresponding stream of its -// parent, which is what an environment without a general mechanism for passing -// handles can always provide. -export struct kal_spawn_streams { - kal_uintptr in; - kal_uintptr out; - kal_uintptr err; -}; - -export extern "C" { - -// Starts the program named relative to a directory, with the given arguments -// and named values. Both are passed as counted arrays of counted strings, so -// that the interface requires neither a terminator convention nor a string -// function. -// -// The started program's working directory is the directory supplied here, and -// there is no operation that changes it afterwards: a working directory that -// can be changed is shared mutable state between execution contexts, and the -// specification declines to impose a synchronisation rule upon it. -// The argument vector is complete and is passed unaltered: argv[0] is the name -// the started program observes as its own, and an implementation neither -// derives it from `path` nor prepends anything to the vector. Clause 7.6. The -// two sides must agree, because the started program reads argv[0] through -// kal_env_arg(0). -int kal_process_spawn(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 kal_spawn_streams* streams, - kal_process* out); - -// Waits for the program to finish and reports the status it finished with. A -// program terminated by the environment reports a status this specification does -// not interpret beyond its being distinguishable from an ordinary status. -int kal_process_wait(kal_process, int* status, int* terminated); - -// Requests termination. An implementation that cannot terminate a program -// reports that the operation is unsupported rather than reporting success. -int kal_process_terminate(kal_process); +export using ::kal_process; +export using ::kal_spawn_streams; -// Release. Releasing a handle does not affect the program it refers to; a -// program that has not been waited for continues, and an implementation that -// must collect it does so. -void kal_process_close(kal_process); +export using ::kal_process_spawn; +export using ::kal_process_wait; +export using ::kal_process_terminate; +export using ::kal_process_close; +export using ::kal_process_props; -extern const kal_uintptr kal_process_props; - -} +static_assert(sizeof(kal_process) == sizeof(kal_uintptr), "clause 7.2"); +static_assert(sizeof(kal_spawn_streams) == 3 * sizeof(kal_uintptr), "clause 5.3"); export namespace kal::process { using process = kal_process; using streams = kal_spawn_streams; -enum : kal_uintptr { - prop_terminate = 1u << 0, // termination can be requested - prop_stream_passing = 1u << 1, // streams other than the parent's can be supplied - prop_exit_status = 1u << 2, // the status is the value the program returned -}; +struct props_tag; +using props = kal::props; + +inline constexpr props terminate {KAL_PROCESS_PROP_TERMINATE}; +inline constexpr props stream_passing{KAL_PROCESS_PROP_STREAM_PASSING}; +inline constexpr props exit_status {KAL_PROCESS_PROP_EXIT_STATUS}; -inline bool has(kal_uintptr p) { return (kal_process_props & p) != 0; } +inline props properties() { return props{kal_process_props}; } +inline bool has(props p) { return properties().has(p); } } diff --git a/src/stream.cppm b/src/stream.cppm index 4442a1b..02a516f 100644 --- a/src/stream.cppm +++ b/src/stream.cppm @@ -12,72 +12,56 @@ // failure for a pipe. An interface that offered positioning on every stream // would therefore contain an operation that some streams can never satisfy, // which is the defect this specification's decomposition exists to avoid. +module; +#include + export module openkal.stream; export import openkal.types; -// An opaque handle occupying one machine word. -// -// The width is fixed and the interpretation is not. An implementation stores -// whatever it natively uses: a descriptor, an operating-system handle, a -// pointer to a driver structure, or a capability index. No implementation is -// required to maintain a translation table, and the absence of that requirement -// is what allows openkal to be implemented above a C library, below one, or -// without one. -export struct kal_stream { kal_uintptr h; }; +// An opaque handle occupying one machine word. The width is fixed and the +// interpretation is not: an implementation stores whatever it natively uses --- +// a descriptor, an operating-system handle, a pointer to a driver structure, a +// capability index. No implementation is required to maintain a translation +// table, and the absence of that requirement is what allows openkal to be +// implemented above a C library, below one, or without one. +export using ::kal_stream; -export extern "C" { +export using ::kal_stdin; +export using ::kal_stdout; +export using ::kal_stderr; +export using ::kal_stream_write; +export using ::kal_stream_read; +export using ::kal_stream_flush; +export using ::kal_stream_props; -// The program's standard streams. These handles are borrowed: the caller does -// not own them and does not close them. -kal_stream kal_stdin (void); -kal_stream kal_stdout(void); -kal_stream kal_stderr(void); +static_assert(sizeof(kal_stream) == sizeof(kal_uintptr), + "a handle occupies one machine word: clause 7.2"); -// Transfers the whole buffer, or reports the condition that prevented it. -// -// A partial transfer is not a successful outcome. The alternative convention, -// in which the caller inspects the count and repeats the call, places the same -// loop in every caller and has been a recurring source of defects in interfaces -// that adopted it. The loop belongs in the implementation, which is written -// once. -// -// On failure, `n` reports how many bytes were transferred before the failure. -kal_io_result kal_stream_write(kal_stream s, const void* buf, kal_uintptr len); +export namespace kal { -// Transfers at most `len` bytes and reports how many were transferred. A -// result of zero bytes with `kal_ok` indicates end of input; unlike a partial -// write, a partial read carries information the caller needs. -kal_io_result kal_stream_read(kal_stream s, void* buf, kal_uintptr len); +using stream = kal_stream; -// Commits any buffering the implementation performs. An implementation that -// does not buffer returns `kal_ok`. -int kal_stream_flush(kal_stream s); +// The tag exists only to distinguish this interface's capability word from +// another's; it is never defined and never instantiated. +struct stream_props_tag; +using stream_props = props; +namespace stream_prop { +// Positions in the result of kal_stream_props. +inline constexpr stream_props interactive{KAL_STREAM_PROP_INTERACTIVE}; } -export namespace kal { - -using stream = kal_stream; - inline stream in () { return kal_stdin(); } inline stream out() { return kal_stdout(); } inline stream err() { return kal_stderr(); } inline kal_io_result write(stream s, const void* p, kal_uintptr n) { return kal_stream_write(s, p, n); } inline kal_io_result read (stream s, void* p, kal_uintptr n) { return kal_stream_read(s, p, n); } -inline int flush(stream s) { return kal_stream_flush(s); } +inline int flush(stream s) { return kal_stream_flush(s); } -// --- Optional capabilities ------------------------------------------------- -// -// Version 0.2 defines none. The mechanism by which an optional operation would -// be expressed is deferred until one exists, and clause 6 of the specification -// records the candidates together with the constraint each carries. -// -// An earlier draft placed a fallback overload here, to be displaced by an -// implementation declaring its own. That arrangement requires the -// implementation's declarations to be visible to the consumer, which requires -// the implementation to own the module the consumer imports, which contradicts -// the layering this specification adopts. The fallback was removed with the -// mechanism it belonged to. +// The properties of one stream, as a set that cannot be confused with another +// interface's. A caller asks whether a stream is interactive before it has +// transferred anything, because that is when the answer changes what it does. +inline stream_props properties(stream s) { return stream_props{kal_stream_props(s)}; } } diff --git a/src/task.cppm b/src/task.cppm index 55e8416..8c1ce96 100644 --- a/src/task.cppm +++ b/src/task.cppm @@ -11,62 +11,43 @@ // and the boundary provides the wait. An interface offering mutexes would place // a library at the kernel boundary, and would oblige an implementation whose // environment has no kernel mutex to construct one. +module; +#include + export module openkal.task; export import openkal.types; -export struct kal_task { kal_uintptr h; }; - -export extern "C" { - -// Starts a context executing the given function with the given argument. 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. -int kal_task_start(void (*entry)(void*), void* arg, kal_task* out); - -// Waits for a context to finish. A context is waited for at most once. -int kal_task_join(kal_task); +export using ::kal_task; -// Relinquishes the processor. An implementation without a scheduler returns -// immediately, which is a supply and not a simulation: the operation promises -// only that the caller may be descheduled, not that it will be. -void kal_task_yield(void); +export using ::kal_task_start; +export using ::kal_task_join; +export using ::kal_task_yield; +export using ::kal_task_current; +export using ::kal_task_wait; +export using ::kal_task_wake; +export using ::kal_task_props; -// The identity of the calling context, for a program that must distinguish -// them. The value is unique among contexts running at the same moment and may -// be reused after one finishes. -kal_uintptr kal_task_current(void); - -// --- The suspension primitive --------------------------------------------- -// -// 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 -// the value to change unobserved, which is the property that makes the -// primitive usable and which a caller cannot construct for itself. -// -// A timeout of zero denotes no timeout. -int kal_task_wait(const __UINT32_TYPE__* word, __UINT32_TYPE__ expected, - __UINT64_TYPE__ timeout_ns); - -// Wakes at most `count` contexts suspended upon the given address, and reports -// how many were woken. A count of zero wakes none and is permitted. -int kal_task_wake(const __UINT32_TYPE__* word, kal_uintptr count, kal_uintptr* woken); - -extern const kal_uintptr kal_task_props; - -} +static_assert(sizeof(kal_task) == sizeof(kal_uintptr), "clause 7.2"); export namespace kal::task { using task = kal_task; -enum : kal_uintptr { - prop_preemptive = 1u << 0, // a context may be descheduled without yielding - prop_parallel = 1u << 1, // contexts may execute simultaneously - prop_wait_timeout = 1u << 2, // kal_task_wait honours a timeout -}; +struct props_tag; +using props = kal::props; + +inline constexpr props preemptive {KAL_TASK_PROP_PREEMPTIVE}; +inline constexpr props parallel {KAL_TASK_PROP_PARALLEL}; +inline constexpr props wait_timeout{KAL_TASK_PROP_WAIT_TIMEOUT}; + +// A context started by kal_task_start observes the thread-local storage of the +// toolchain that compiled the program. Clause 7.10 states why the property is +// reported rather than provided: the register convention belongs to openarch, +// and a C library ported onto openkal keeps its per-context state in one such +// variable and therefore cannot be ported onto an implementation without it. +inline constexpr props thread_local_storage{KAL_TASK_PROP_THREAD_LOCAL}; -inline bool has(kal_uintptr p) { return (kal_task_props & p) != 0; } +inline props properties() { return props{kal_task_props}; } +inline bool has(props p) { return properties().has(p); } } diff --git a/src/time.cppm b/src/time.cppm index 8bed678..1b51be4 100644 --- a/src/time.cppm +++ b/src/time.cppm @@ -9,53 +9,41 @@ // records when something happened requires the second. Merging them would // oblige an implementation whose clock cannot be adjusted to claim that it can, // or the reverse. +module; +#include + export module openkal.time; export import openkal.types; // Nanoseconds. The unit is fixed rather than reported, because a program that // must consult a unit before performing arithmetic acquires a branch that no // environment needs. -export using kal_duration = __UINT64_TYPE__; - -export extern "C" { - -// Elapsed time from an unspecified origin. The origin is fixed for the lifetime -// of the program, so differences are meaningful and absolute values are not. -kal_duration kal_time_monotonic(void); - -// Time since 1970-01-01T00:00:00Z, disregarding leap seconds. An implementation -// whose environment has no such notion reports zero, which the capability word -// distinguishes from an environment whose clock happens to read zero. -kal_duration kal_time_wall(void); - -// The granularity of the monotonic source, in nanoseconds. A program that -// measures short intervals requires it in order to know what it has measured. -kal_duration kal_time_monotonic_granularity(void); +export using ::kal_duration; -// Suspends the calling context for at least the given duration. An -// implementation may suspend for longer and shall not suspend for less. -void kal_time_sleep(kal_duration ns); - -// Properties of this implementation's time sources. -extern const kal_uintptr kal_time_props; - -} +export using ::kal_time_monotonic; +export using ::kal_time_wall; +export using ::kal_time_monotonic_granularity; +export using ::kal_time_sleep; +export using ::kal_time_props; export namespace kal::time { +struct props_tag; +using props = kal::props; + // Positions in kal_time_props. A position, once assigned, retains its meaning; // an unassigned position reads as zero, so that a program compiled against a // later specification behaves correctly against an earlier implementation. -enum : kal_uintptr { - prop_wall_available = 1u << 0, // the wall source reports a real time - prop_monotonic_suspends = 1u << 1, // the monotonic source stops while the machine is suspended - prop_sleep_precise = 1u << 2, // suspension granularity matches the monotonic granularity -}; +inline constexpr props wall_available {KAL_TIME_PROP_WALL_AVAILABLE}; +inline constexpr props monotonic_suspends{KAL_TIME_PROP_MONOTONIC_SUSPENDS}; +inline constexpr props sleep_precise {KAL_TIME_PROP_SLEEP_PRECISE}; inline kal_duration monotonic() { return kal_time_monotonic(); } inline kal_duration wall() { return kal_time_wall(); } inline kal_duration granularity() { return kal_time_monotonic_granularity(); } inline void sleep(kal_duration ns) { kal_time_sleep(ns); } -inline bool has(kal_uintptr p) { return (kal_time_props & p) != 0; } + +inline props properties() { return props{kal_time_props}; } +inline bool has(props p) { return properties().has(p); } } diff --git a/src/types.cppm b/src/types.cppm index e6e7363..87946bb 100644 --- a/src/types.cppm +++ b/src/types.cppm @@ -1,48 +1,99 @@ // openkal.types --- definitions shared by every openkal interface. // -// The declarations in this module are normative. An implementation package -// re-exports them unchanged; it may not redefine them. That prohibition is -// enforced by the language rather than by convention: a module that redeclares -// an imported type is rejected by the compiler. +// There is one statement of the declarations and two ways to reach it. The +// statement is the C header, because the contract is a C application binary +// interface; this module includes it in its global module fragment and exports +// the names, so a consumer that imports and a consumer that includes obtain +// the same entities rather than two declarations that agree today. +// +// What the module adds is not a second declaration. It is what C++ can check +// and C cannot: the layouts clause 5.3 freezes are asserted here, and the +// capability words become types that cannot be mixed. +module; +#include + export module openkal.types; -// The width of a machine word, obtained from the compiler rather than from a -// header. A freestanding target may have no , and openkal is required -// to be usable on such a target. -export using kal_uintptr = __UINTPTR_TYPE__; +// The width of a machine word, and the three fixed widths the operations use. +export using ::kal_uintptr; +export using ::kal_u32; +export using ::kal_u64; +export using ::kal_i64; -// The complete set of error conditions openkal defines. -// -// The set is closed. An implementation maps its platform's error values onto -// these; it does not extend the set. Mapping is not emulation: a table lookup -// preserves the property that every backend implements the interface naturally, -// whereas reproducing a foreign error namespace does not. -// -// Detail beyond these values is deliberately unavailable. A per-thread channel -// carrying the platform's own error value was considered and rejected, because -// such a channel is global mutable state with the same defects as errno, and -// because control flow that depends on it is not portable by construction. -export enum kal_error : int { - kal_ok = 0, - kal_err_invalid = 1, // the handle or an argument is not valid - kal_err_again = 2, // the operation would block and blocking was not requested - kal_err_io = 3, // the device or medium reported a failure - kal_err_no_memory = 4, - kal_err_no_space = 5, - kal_err_permission = 6, - kal_err_not_supported = 7, - kal_err_closed = 8, // the peer closed the connection -}; +// The complete set of error conditions openkal defines. The set is closed: an +// implementation maps its environment's error values onto these and does not +// extend them. +export using ::kal_error; +export using ::kal_ok; +export using ::kal_err_invalid; +export using ::kal_err_again; +export using ::kal_err_io; +export using ::kal_err_no_memory; +export using ::kal_err_no_space; +export using ::kal_err_permission; +export using ::kal_err_not_supported; +export using ::kal_err_closed; +export using ::kal_err_not_found; +export using ::kal_err_exists; +export using ::kal_err_not_empty; +export using ::kal_err_is_directory; +export using ::kal_err_not_directory; // The result of an operation that transfers a count. +export using ::kal_io_result; + +// Clause 5.3 declares the layout of every structure immutable. A declaration +// that something shall not change is not a mechanism; this is the mechanism. +// Two machine words are returned in registers on the architectures openkal +// targets, and a wider result would be returned through a hidden pointer +// instead --- a change of calling convention that no declaration would report +// and that a consumer built against the earlier layout would not survive. +static_assert(sizeof(kal_io_result) == 2 * sizeof(kal_uintptr), + "kal_io_result must remain two machine words: clause 5.3"); +static_assert(alignof(kal_io_result) == alignof(kal_uintptr)); +static_assert(__builtin_offsetof(kal_io_result, n) == 0); +static_assert(__builtin_offsetof(kal_io_result, e) == sizeof(kal_uintptr)); +static_assert(sizeof(kal_uintptr) == sizeof(void*), + "kal_uintptr must hold a pointer: clause 5.1"); + +export namespace kal { + +// A capability word, carrying the interface it belongs to in its type. // -// The layout of this structure is frozen. openkal's evolution rule permits new -// declarations and forbids changes to existing ones; a structure layout is not -// protected by that rule unless the layout itself is declared immutable, which -// it is here. Two machine words are returned in registers on the architectures -// openkal targets, and a wider result would be returned through a hidden -// pointer instead. -export struct kal_io_result { - kal_uintptr n; - int e; +// Clause 6.2 gives each interface a word named kal__props and +// positions within it. Every such word is a kal_uintptr, so a program that +// tests a file-system position against the task word compiles, runs, and +// answers a question nobody asked. The position numbers are small and several +// interfaces have assigned the same ones, so the answer is frequently the +// plausible one. +// +// The tag makes the two words different types. The operations that compose +// positions are defined only between positions of one interface, so the +// mistake is a diagnostic rather than a result. Nothing is stored beyond the +// word itself and every operation is constant-evaluated, so the type occupies +// the same register the plain word did. +template +struct props { + kal_uintptr bits; + + constexpr props() : bits(0) {} + constexpr explicit props(kal_uintptr b) : bits(b) {} + + friend constexpr props operator|(props a, props b) { return props{a.bits | b.bits}; } + friend constexpr props operator&(props a, props b) { return props{a.bits & b.bits}; } + friend constexpr bool operator==(props a, props b) { return a.bits == b.bits; } + + // Whether every position in `p` is present. The question a program asks is + // "may I do this", and a program that requires two properties asks it once + // rather than twice, so the test is written for a set and not for a bit. + constexpr bool has(props p) const { return (bits & p.bits) == p.bits; } + + constexpr explicit operator bool() const { return bits != 0; } }; + +// Reads a capability word an implementation defined, as the type of the +// interface it belongs to. +template +constexpr props read_props(const kal_uintptr& word) { return props{word}; } + +} // namespace kal diff --git a/tools/check-declarations.sh b/tools/check-declarations.sh new file mode 100755 index 0000000..e25a3db --- /dev/null +++ b/tools/check-declarations.sh @@ -0,0 +1,94 @@ +#!/usr/bin/env bash +# Clause 4.3. The specification distributes its declarations in two forms, and +# a reader is entitled to assume that the two declare the same entities. +# +# This tool examines the C form. It does not compare the two forms with each +# other: it compiles a translation unit that names every entity in +# SURFACE.txt, which is normative, so that a name the header does not declare +# fails to compile and the diagnostic names it. The C++ form is examined the +# same way, against the same list, by a conformance test in each implementation +# package, where a build of the modules already exists. Neither form is +# therefore the other's source. +# +# check-declarations.sh [--write] [] [] +# +# The translation unit is compiled with -nostdinc, because the consumer this +# header exists for --- a C library being ported onto openkal --- is compiled +# that way, and a header that required one of the environment's own would be +# unusable by it. A check that permitted the environment's headers would pass +# for a header that cannot be used. +set -euo pipefail + +write=0 +if [ "${1:-}" = "--write" ]; then write=1; shift; fi + +here="$(cd "$(dirname "$0")/.." && pwd)" +list="${1:-$here/SURFACE.txt}" +incdir="${2:-$here/include}" +work="$(mktemp -d)" +trap 'rm -rf "$work"' EXIT + +names="$(grep -vE '^[[:space:]]*(#|$)' "$list" | sort -u)" +count="$(printf '%s\n' "$names" | grep -c .)" +[ "$count" -gt 0 ] || { echo "the surface list is empty" >&2; exit 1; } + +{ + echo '#include ' + echo '/* Naming each entity is what makes an absent declaration a compile' + echo ' error. Taking the address additionally rejects a name introduced as' + echo ' a macro, which would satisfy a textual search and satisfy nothing' + echo ' else. */' + echo 'const void *const okc_surface[] = {' + while read -r n; do [ -n "$n" ] && echo " (const void *)&$n,"; done <<< "$names" + echo '};' +} > "$work/surface.c" + +cc="${CC:-cc}" +builtin_inc="$("$cc" -print-file-name=include)" +"$cc" -std=c11 -ffreestanding -nostdinc -isystem "$builtin_inc" \ + -Wall -Wextra -Werror -Wno-unused-parameter \ + -I"$incdir" -c "$work/surface.c" -o "$work/surface.o" + +echo "the C declarations are complete and compile without the environment's headers: $count name(s)" + +# The same list, as a translation unit the conformance suite carries. +# +# This tool needs a driver it can pass -nostdinc to, and one of the three +# toolchains the suite is built with has no such spelling; a header that failed +# to compile as C under that toolchain would go unnoticed here. So the suite +# carries a file naming the same entities, every toolchain compiles it, and this +# is where the file is written --- and where a file that has fallen behind the +# list is detected, since a stale copy would be compiled happily by all three. +carried="$here/conformance/src/declarations.c" + +if [ "$write" -eq 1 ]; then + { + sed -n '1,/^ \*\/$/p' "$carried" + echo '#include ' + echo + echo 'void okc_declarations_c(void);' + echo 'void okc_declarations_c(void)' + echo '{' + while read -r n; do + [ -n "$n" ] && printf ' (void)sizeof(&%s);\n' "$n" + done <<< "$names" + echo '}' + } > "$carried.next" + mv "$carried.next" "$carried" + echo "wrote $carried: $count name(s)" + exit 0 +fi + +[ -f "$carried" ] || exit 0 + +carried_names="$(grep -oE '&kal_[a-z_0-9]+' "$carried" | cut -c2- | sort -u)" +if [ "$carried_names" != "$names" ]; then + echo "conformance/src/declarations.c does not name the surface list." >&2 + echo "only in SURFACE.txt:" >&2 + comm -23 <(printf '%s\n' "$names") <(printf '%s\n' "$carried_names") >&2 + echo "only in declarations.c:" >&2 + comm -13 <(printf '%s\n' "$names") <(printf '%s\n' "$carried_names") >&2 + echo "regenerate it: tools/check-declarations.sh --write" >&2 + exit 1 +fi +echo "the translation unit the suite carries names the same $count entities" diff --git a/tools/run-conformance.sh b/tools/run-conformance.sh new file mode 100755 index 0000000..3a06812 --- /dev/null +++ b/tools/run-conformance.sh @@ -0,0 +1,98 @@ +#!/usr/bin/env bash +# Runs the conformance suite against one implementation. +# +# run-conformance.sh [features] [extra mcpp arguments] +# +# The suite's manifest names openkal and does not name an implementation: the +# implementation is supplied by whoever runs the suite, which is what makes the +# suite an instrument rather than an implementation's dependant. Supplying it is +# a few lines of manifest, and those lines are here rather than repeated in +# every implementation's continuous integration, so that a change to how it is +# done is one change. +# +# Both working trees are modified in place. That is intended: this runs in +# checkouts that exist for the length of one job. +set -euo pipefail + +package="${1:?usage: run-conformance.sh [features] [mcpp arguments...]}" +implementation="${2:?usage: run-conformance.sh [features] [mcpp arguments...]}" +features="${3:-full}" +shift 3 2>/dev/null || shift $# + +# A path this script hands to the build tool rather than to the shell. +# +# On one of the three systems the shell and the build tool disagree about what +# a path is: the shell reports /d/a/openkal, and the tool --- which is a program +# of the system rather than of the shell --- reads that as a directory named `d' +# at the root of the current volume. The translation exists on that system and +# is a no-op everywhere else. +native() { + if command -v cygpath > /dev/null 2>&1; then cygpath -m "$1"; else printf '%s\n' "$1"; fi +} + +here="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +suite="$here/conformance" +[ -f "$suite/mcpp.toml" ] || { echo "no suite at $suite" >&2; exit 2; } + +implementation="$(cd "$implementation" && pwd)" +[ -f "$implementation/mcpp.toml" ] || { + echo "$implementation is not a package" >&2; exit 2; } + +here_native="$(native "$here")" +implementation_native="$(native "$implementation")" + +# The specification is taken from this working tree rather than from the version +# the manifests name, so that a run asserts what it is for: that the +# specification as written here and the implementation as written there agree +# today. +# +# It is rewritten in both manifests and not in one. A package may be reached by +# a path or by a version and not by both at once, and the two manifests reach +# openkal independently; rewriting one of them produces a resolution failure +# rather than a build. +# +# Not `sed -i`: BSD sed reads the next word as a backup suffix and so the GNU +# form fails on macOS. This form is the same on every system. +point_at_the_specification() { + sed "s|^openkal = .*$|openkal = { path = \"$here_native\" }|" "$1" > "$1.next" + mv "$1.next" "$1" +} +point_at_the_specification "$suite/mcpp.toml" +point_at_the_specification "$implementation/mcpp.toml" + +if ! grep -q "^$package = " "$suite/mcpp.toml"; then + # Appended immediately after openkal, which is inside [dependencies]. A + # plain append would land under [features]. + awk -v line="$package = { path = \"$implementation_native\" }" ' + { print } + /^openkal = / && !done { print line; done = 1 } + ' "$suite/mcpp.toml" > "$suite/mcpp.toml.next" + mv "$suite/mcpp.toml.next" "$suite/mcpp.toml" +fi + +echo "--- the suite's dependencies ---" +sed -n '/^\[dependencies\]/,/^$/p' "$suite/mcpp.toml" + +cd "$suite" +mcpp build --features "$features" "$@" + +binary="$(find target -type f \( -name 'openkal-conformance' -o -name 'openkal-conformance.exe' \) | head -1)" +[ -n "$binary" ] || { echo "the suite was not produced" >&2; exit 2; } + +# openkal.env reads variables and does not set them, so the one observation that +# requires a variable whose value is empty requires the runner to supply it. +# Supplying it here rather than leaving it to each caller is the difference +# between ninety-one observations and ninety. +export OPENKAL_CONFORMANCE_EMPTY="" + +# A suite built for a system other than the one building it is run through +# whatever runs it, named in the environment. The suite starts a copy of itself +# for the three observations that end the program that makes them, and it does +# so through openkal.process rather than through anything this script arranges, +# so the copy is started by the same mechanism as the original. +runner="${OPENKAL_CONFORMANCE_RUNNER:-}" + +# The suite's own exit status is the verdict: 0 when every observation held, 1 +# when one did not, 2 when nothing was observed. The last is the one a run that +# selected no interface would otherwise pass silently. +$runner "./$binary"