ADR 0001 — Foreign-architecture support policy (“draw the line by axis”)

Status: Accepted (2026-07-28). Context: Q1–Q3 extracted the arch interface from the nine tree ports — all of which are GCC + 8-bit-byte + downward-stack, i.e. the ARM/RISC-V “convergence.” The user flagged that this silently baked those three assumptions into the “common denominator,” and named a family of platforms that break them and are wanted eventually: TI C2000 (C28x), TI MSP430X, NXP MC56F8xxx (56800E), NXP S12/S12X, Analog Devices SHARC, Motorola DSP56000.

Decision

Draw the transparency line by axis of foreignness, not by architecture. Absorb the cheap, universally-honest axes into the common core; quarantine the viral, rare axes behind an explicit opt-in the core is built without. (Chosen over “quarantine every foreign arch wholesale” and “build full transparency for everything up front.”)

The axes and where each lands

Axis

Targets that hit it

Cost to the scheduler core

Verdict

CHAR_BIT ≠ 8 (word-addressed)

C2000 (16), 56800E (16), DSP56000 (24), SHARC (32)

Low — only the stack-size unit + no uint8_t in the contract; the scheduler is structs/indices/an opaque SP token

Absorb

Stack grows up

C2000

Low — arch_context_init is arch-owned; only the overflow-guard / high-water scan cares about “which end”

Absorb (arch-declared property)

Non-GCC vendor compiler

all of them

Required regardless — compiler-abstraction + arch-owned .asm switch is the porting layer

Neither (it’s the plan)

Harvard / multiple memory spaces

DSP56000 (X/Y/P), SHARC (PM/DM), 56800E (milder)

High + viral — fn/arg/stack can live in different spaces; a uniform void* breaks

Quarantine

Banked / wide pointers

MSP430X (20-bit), S12X (paged)

Medium — context saves bank regs (arch-owned) but cross-bank calls tax the toolchain

Mostly quarantine / toolchain config

Big-endian

S12X, others

Low for the scheduler; matters for serialization (HAL/crypto, not the core)

Absorb (core is endian-neutral)

What “absorb the cheap axes” means concretely (applied now)

  1. CHAR_BIT-neutral stack contract. The kernel↔arch contract must not use uint8_t (optional in C, absent on C2000) or say “bytes = 8 bits.” Stacks are a void* region + a size_t count of addressable cells (C sizeof units: 8-bit on ARM, 16-bit on C2000, 24 on a 56000, 32 on SHARC). This is correct C by the standard — ARM merely let us ignore it. arch_context_init(ctx, void* stack, size_t cells, fn, arg). ARCH_MIN_STACK_SIZE is a cell count. The no-op spinlock default type stops being uint8_t (→ unsigned char, the addressable cell, valid everywhere).

  2. Stack direction as an arch property. #define ARCH_STACK_GROWS_UP {0|1} (default 0 via arch_defaults.h). Read only by the stack-guard / high-water scan; arch_context_init builds the frame in its own direction. C2000 sets it to 1.

What “quarantine the viral axes” means (deferred, by design)

The core carries no multiple-address-space or banking model. When a real Harvard / multi-space target (DSP56000, SHARC) is actually on the bench, it is added as an explicit opt-in “exotic memory model” extension — the core must be buildable, and ship, without it. No speculative machinery for hardware we don’t have.

Consequences

  • The core gets more honest, not more cluttered. The two absorbed axes cost almost nothing on ordinary targets (a byte is a cell on ARM; direction default is down) and remove two false assumptions — this improves the “clean, refined” goal, doesn’t dilute it.

  • C2000 becomes the first integrated foreign precedent. It exercises only the absorb-axes (CHAR_BIT 16, stack-up, cl2000) and is a flat, single-address-space machine — no Harvard, no banking. So it validates the “absorb” half cleanly and defers the “quarantine” question honestly to a future DSP56000/SHARC. The precedent generalizes to the whole word-size/stack-direction family (C2000 → 56800E → MSP430X).

  • A concrete leak was already caught by including C2000: uint8_t in the shared defaults — proof the outlier belongs in the analysis, not beside it.

Follow-through (this change set)

  • arch/arch_defaults.h: spinlock default uint8_t→unsigned char; add ARCH_STACK_GROWS_UP default (0).

  • arch/arm_v7m, arch/arm_v6m: arch_context_init signature → void* + size_t cells (behaviourally identical on ARM).

  • New arch/c2000/{arch_irq.h,kernel_arch.h}: cl2000 DINT/EINT Tier 0; ARCH_STACK_GROWS_UP 1; cell-based stack; no SMP/MPU/unpriv → all defaulted.

  • Validated under cl2000 25.11.1 (/c/ti/.../ti-cgt-c2000_25.11.1.LTS) — the vendor compiler R1.1 promised but only GCC had exercised.


Addendum (2026-08-10) — completing CHAR_BIT absorption into the CORE + the type vocabulary

Status: Accepted (extends the original; does not revise it). See Addendum 2 below — the concrete tk_u8_t typedef this addendum introduced was itself later superseded; the four-kinds framework it established still stands.

Context. The original change set neutralized the kernel↔arch contract (the arch_defaults.h spinlock, arch_context_init’s cells, the C2000 arch headers) and “validated under cl2000” — but that validation only ever compiled the arch headers. The first time the reshaped kernel core (tk_kernel.*, tk_sync.*, tk_mutex.h, the TCB) actually met cl2000 (starting the C2000 reshape port, R1.3), it did not compile: ~40 uint8_t uses remained in the core internals + public API — kind-1 values (priority, flags, core masks, enum tags) mis-spelled with a type that is absent on C2000. So the “absorb CHAR_BIT” decision had been applied to the contract but not carried into the core.

A full-core byte-width sweep confirmed uint8_t is the only leak: the core has no byte-pointer arithmetic, no serialization / bit-packing, no memcpy. ADR 0001’s “the scheduler is structs/indices/an opaque SP token” analysis holds; only the type spelling leaked.

The type vocabulary (four kinds). Byte-width portability stays honest by never conflating four distinct quantities — each has its own type; mixing them is the bug:

  1. Small integer value (priority, flags, masks, enum tags) → a value in 0..255, never a byte of memory. (Addendum 2: spelled unsigned char directly at root — see below.) uint8_t is optional in C and absent on C2000 — it must never appear in the core.

  2. Addressable memory cell (stacks, RAM buffers) → tk_stack_cell_t = unsigned char + a size_t count of cells. sizeof/memcpy/indexing/alignof are all cell-correct (C guarantees sizeof(char)==1) on every target.

  3. Machine word / register / stack pointer → opaque: struct arch_context (the arch picks the width) + uintptr_t for the in-transit SP token. The core never interprets it; registers live only in arch/.

  4. Wire octet (data leaving the CPU) → an explicit octet type + explicit (de)serialization, owned by the serializing feature/HAL, never the core, never by struct-aliasing a wire buffer (a “byte” buffer holds 16-bit cells on C2000). No such surface exists in the core today; this rule attaches to trace/introspect export when they are ported.

The core lives entirely in kinds 1–3, all portable by construction.

Decision (as made 2026-08-10; superseded in part — see Addendum 2). Complete the CHAR_BIT absorption into the core: add tk_u8_t = uint_least8_t (in kernel/tk_types.h) and use it for the kind-1 fields; keep kind-2 as tk_stack_cell_t. Chosen over (a) a C2000-only typedef … uint8_t shim (makes uint8_t a 16-bit lie; leaves the core non-neutral so the next word-addressed arch inherits the shim) and (b) a single unifying tk_byte_t for both kinds 1 & 2 (conceptual economy, but types integer values as memory bytes and erases the kind-1/2 seam that governs aliasing and the kind-4 wire boundary). Behaviorally identical on the 8 GCC/host ports (uint_least8_t == uint8_t there); a one-time core change that makes R4.9 (“a new arch adds no core edits”) genuinely true for the whole word-addressed family (C2000 → 56800E(16) → MSP430X → DSP56000(24) → SHARC(32)).

Verified (evidence, not assertion). The reshaped kernel core compiles under cl2000 25.11.1 for the first time (tk_kernel.c + tk_sync.c + tk_arch_impl.c→arch/c2000, --c99), and still compiles clean under RISC-V GCC with -Wall -Wextra (no regression). Two build/portability notes captured here so they are not re-discovered:

  • The C2000 build requires --c99 — the core uses C99 inline for (int i …) declarations; cl2000 defaults to C89. A build flag (R9), not a code change; the C2000 board/CCS project must set it.

  • int is 16-bit on C2000 (32 on ARM). The core compiled clean (no 32-bit-int declarations), but runtime int-range correctness is a standing check the full-core cl2000 build now guards; watch it as new core code lands.


Addendum 2 (2026-08-19) — tk_u8_t dropped; kind-1 is spelled unsigned char directly

Status: Accepted (supersedes Addendum’s concrete kind-1 typedef only; the four-kinds framework is unchanged). Recorded when this ADR was migrated into the root docs/.

What changed. Addendum (2026-08-10) resolved kind-1 (small integer values) to a named typedef tk_u8_t = uint_least8_t. The root tree took the simpler road it had left open: kind-1 is spelled unsigned char directly, with no project typedef at all. kernel/tk_types.h now defines only tk_stack_cell_t (kind 2) and documents kind 1 as: “Spell it unsigned char directly — it is the smallest addressable unsigned type, holds 0..255 on every target, and (unlike uint8_t, which is optional in C and absent on C2000) is guaranteed to exist. No project typedef needed.”

Why. unsigned char already satisfies every constraint tk_u8_t was introduced to meet (exists everywhere, holds 0..255, never uint8_t), so the typedef bought a name without buying a guarantee — a layer to learn for no portability gain. Dropping it keeps the vocabulary to exactly one named type (tk_stack_cell_t, whose cell-counting rule is the load-bearing one) and leaves kind 1 as ordinary C. The four-kinds analysis stands verbatim — only kind 1’s spelling changed (typedef → direct unsigned char).

Residual leak (open, tracked as roadmap, not fixed here). kernel/tk_kernel.h’s public introspection snapshot struct tk_thread_info still declares uint8_t base_priority / uint8_t state — the exact kind-1-as-uint8_t anti-pattern this ADR forbids. It is a public-header leak (not core internals) and predates this migration; fix it to unsigned char when the introspection surface is next touched.