kernel: start independent harts and exchange bounded mailbox requests

0b52e1efc00f9a65822213dfef2a998086e7988b3a69f7cc889f02d9995164e1
Verified: make -C kernel check; native checks and mailbox execution on 1, 2, and 8 harts pass.
Alexis Sellier committed ago 1 parent 1279b225
kernel/Makefile +9 -3
2 2
EMU ?= $(or $(RAD_EMULATOR),emulator)
3 3
HOST_EMU ?= $(EMU)
4 4
COMPILER := ../bin/radiance.rv64.dev
5 5
COMPILE := $(HOST_EMU) -memory-size=385024 -data-size=348160 -stack-size=512 -run $(COMPILER)
6 6
MODULES := $(wildcard core/*.rad)
7 -
CORE_ASM := arch/atomic.ras arch/context.ras arch/clock.ras arch/mmio.ras arch/physical.ras
7 +
CORE_ASM := arch/atomic.ras arch/context.ras arch/clock.ras arch/mmio.ras arch/physical.ras arch/smp.ras
8 8
CORE := -pkg core -mod core.rad $(addprefix -mod ,$(MODULES) $(CORE_ASM))
9 9
CHECK_MODULES := $(wildcard check/*.rad)
10 10
USER_INPUTS := user.rad $(wildcard user/*.rad user/*/*.rad) core/abi.rad
11 11
USER_BASE := -pkg abi -mod core/abi.rad -pkg user -mod user.rad -mod user/sys.rad -pkg probe -mod user/probe.rad
12 12
BASE_IMAGES := sample_root sample_control sample_scalars sample_memory sample_overflow sample_events sample_alias sample_instance
47 47
	$(COMPILE) $(BASE_CATALOG) $(CORE) -pkg interrupt -start arch/entry.ras -mod interrupt.rad -mod check/interrupt.ras -entry interrupt -o $@
48 48
49 49
native.rv64: native.rad native/instances.rad arch/entry.ras core.rad $(MODULES) $(CORE_ASM) build/baseline/images.rad user/sys.ras $(COMPILER)
50 50
	$(COMPILE) $(BASE_CATALOG) $(CORE) -pkg native -start arch/entry.ras -mod native.rad -mod native/instances.rad -entry native -o $@
51 51
52 -
check: all check.rv64 context.rv64 interrupt.rv64 native.rv64
52 +
parallel.rv64: parallel.rad parallel/mailboxes.rad arch/start.ras core.rad $(MODULES) $(CORE_ASM) build/baseline/images.rad user/sys.ras $(COMPILER)
53 +
	$(COMPILE) $(BASE_CATALOG) $(CORE) -pkg parallel -start arch/start.ras -mod parallel.rad -mod parallel/mailboxes.rad -entry parallel -o $@
54 +
55 +
check: all check.rv64 context.rv64 interrupt.rv64 native.rv64 parallel.rv64
53 56
	$(HOST_EMU) -run check.rv64
54 57
	$(MACHINE) -run kernel.rv64
55 58
	$(MACHINE) -max-steps=1000000 -run context.rv64
56 59
	$(MACHINE) -harts=2 -irq=3 -irq-at=4000000 -uart-rx=52 -uart-rx-at=8000000 -run interrupt.rv64
57 60
	$(MACHINE) -run native.rv64
61 +
	$(MACHINE) -harts=1 -run parallel.rv64
62 +
	$(MACHINE) -harts=2 -run parallel.rv64
63 +
	$(MACHINE) -harts=8 -run parallel.rv64
58 64
59 65
clean:
60 66
	rm -rf build
61 -
	rm -f kernel.rv64 check.rv64 context.rv64 interrupt.rv64 native.rv64
67 +
	rm -f kernel.rv64 check.rv64 context.rv64 interrupt.rv64 native.rv64 parallel.rv64
kernel/NOTES.md +19 -2
1 1
# Kernel implementation decisions
2 2
3 3
The specification at https://radiant.computer/system/kernel takes precedence
4 4
for fixed call numbers, handle layout, rights, and object behavior. These notes
5 -
record the contracts established through step 20 of the 22-step plan.
5 +
record the contracts established through step 21 of the 22-step plan.
6 6
7 7
## Source and trust boundary
8 8
9 9
- Kernel mechanisms use freestanding Radiance; RAS owns machine entry, register
10 10
  state, atomics, and MMIO. Hosted checks exercise the same mechanism modules.
294 294
  scheduling of these effects is separate from their validation.
295 295
- Public Page counts, Interrupt numbers, and Events capacities use u32. Public
296 296
  domain identifiers use u16, with Pending, Active, and Dead lifecycle values.
297 297
  The register transport uses u64 words.
298 298
299 +
## Hart startup and remote transport
300 +
301 +
- Each online hart enters M-mode on a private 64 KiB stack in reserved image
302 +
  memory. Hart zero release-publishes shared state; secondary harts acquire it
303 +
  before use. Each hart synchronizes local instruction fetch.
304 +
- One FIFO ticket lock serializes shared kernel mechanisms. Its Once guard must
305 +
  be released exactly once. Machine interrupts remain disabled while it is held.
306 +
  Wrapping u32 tickets stay distinct with at most eight callers.
307 +
- Each hart has eight ordinary FIFO request slots. Exact kind, domain incarnation,
308 +
  and context identity form the coalescing key. A full queue of distinct requests
309 +
  returns Busy without disturbing order. Transport itself grants no authority.
310 +
- CLINT publication uses firmware-selected banks and full memory/I/O fences.
311 +
  Queue drain and software-interrupt acknowledgement share the publication lock.
312 +
- Machine probes cover ticket wraparound, mailbox capacity, coherent increments,
313 +
  and software-interrupt wakeup after payload publication on 1, 2, and 8 harts.
314 +
  RV64A checks run in machine mode, not the hosted emulator.
315 +
299 316
## Validation
300 317
301 318
Use the current machine-capable sibling emulator. Set `RAD_EMULATOR`, pass
302 319
`EMU` to the kernel Make invocation, or put `emulator` on PATH. The kernel build
303 320
checks compiler dependencies. From the repository root, run:
304 321
305 322
```sh
306 323
make -C kernel check
307 324
```
308 325
309 -
Run hosted direct-call coverage for argument and authority errors, metadata, query errors, and execution/lifecycle effects. Keep the existing native, context, IRQ, activation, and lifecycle checks.
326 +
Run machine mailbox/startup probes on 1, 2, and 8 harts, including wraparound, full queues, coherent shared increments, and CLINT wakeup after payload publication.
310 327
311 328
Run the context reservation probe with an emulator that retains LR/SC
312 329
reservations across traps. This checks the kernel's reservation invalidation.
kernel/arch/smp.ras added +43 -0
1 +
// RV64A FIFO tickets and machine-mode CLINT software interrupts.
2 +
.text;
3 +
.export @"core::smp::ticket";
4 +
@"core::smp::ticket"
5 +
	li     %t0    1;
6 +
	amoadd.w.aqrl %a0 %t0 0(%a0);
7 +
	// AMO word results are sign-extended by RV64; the Radiance u32 ABI is not.
8 +
	slli   %a0    %a0    32;
9 +
	srli   %a0    %a0    32;
10 +
	ret;
11 +
12 +
.export @"core::smp::hart";
13 +
@"core::smp::hart"
14 +
	csrr   %a0    mhartid;
15 +
	slli   %a0    %a0    32;
16 +
	srli   %a0    %a0    32;
17 +
	ret;
18 +
19 +
.export @"core::smp::kick";
20 +
@"core::smp::kick"
21 +
	slli   %a1    %a1    2;
22 +
	add    %a0    %a0    %a1;
23 +
	li     %t0    1;
24 +
	// Full iorw,iorw ordering publishes RAM before asserting MSIP.
25 +
	fence;
26 +
	sw     %t0    0(%a0);
27 +
	fence;
28 +
	ret;
29 +
30 +
.export @"core::smp::acknowledge";
31 +
@"core::smp::acknowledge"
32 +
	slli   %a1    %a1    2;
33 +
	add    %a0    %a0    %a1;
34 +
	// Full iorw,iorw ordering completes the MSIP clear before queue handling.
35 +
	fence;
36 +
	sw     %zero  0(%a0);
37 +
	fence;
38 +
	ret;
39 +
40 +
.export @"core::smp::syncInstructions";
41 +
@"core::smp::syncInstructions"
42 +
	fence.i;
43 +
	ret;
kernel/arch/start.ras added +37 -0
1 +
// M-mode entry on each hart with private 64 KiB stacks in reserved image RAM.
2 +
.text;
3 +
    csrw mie %zero;
4 +
    csrr %t0 mstatus;
5 +
    li %t1 -9;
6 +
    and %t0 %t0 %t1;
7 +
    csrw mstatus %t0;
8 +
    la %t0 @bootFault;
9 +
    csrw mtvec %t0;
10 +
    csrr %a0 mhartid;
11 +
    li %t0 8;
12 +
    bgeu %a0 %t0 @bootFault;
13 +
    la %sp @hartStacks;
14 +
    addi %t0 %a0 1;
15 +
    slli %t0 %t0 16;
16 +
    add %sp %sp %t0;
17 +
    call @"::default";
18 +
    csrr %t0 mhartid;
19 +
    bnez %t0 @bootIdle;
20 +
    li %t1 0x5555;
21 +
    j @bootFinish;
22 +
@bootFault
23 +
    csrr %t1 mcause;
24 +
    slli %t1 %t1 16;
25 +
    li %t0 0x3333;
26 +
    or %t1 %t1 %t0;
27 +
@bootFinish
28 +
    li %t0 0x10001000;
29 +
    sw %t1 0(%t0);
30 +
@bootIdle
31 +
    wfi;
32 +
    j @bootIdle;
33 +
34 +
.data;
35 +
.align 16;
36 +
@hartStacks
37 +
.space 524288;
kernel/core.rad +1 -0
25 25
export mod mmio;
26 26
export mod physical;
27 27
export mod activation;
28 28
export mod lifecycle;
29 29
export mod calls;
30 +
export mod smp;
kernel/core/smp.rad added +162 -0
1 +
//! FIFO kernel serialization and bounded, identity-bearing remote messages.
2 +
//! One global Lock protects all kernel state and all Mailbox operations. User
3 +
//! architectural frames remain owned by their Running hart outside that lock.
4 +
5 +
use core::abi;
6 +
use core::atomic;
7 +
8 +
/// Maximum distinct requests retained by one hart's mailbox.
9 +
export constant CAPACITY: u32 = 8;
10 +
11 +
/// Naturally aligned FIFO ticket counters; never move or copy a published lock.
12 +
/// At most platform::MAX_HARTS callers may be outstanding, one per online hart.
13 +
export record Lock {
14 +
    /// Next ticket to allocate, modulo 2^32; accessed atomically after publication.
15 +
    next: u32,
16 +
    /// Ticket currently admitted, modulo 2^32; accessed atomically after publication.
17 +
    serving: u32,
18 +
}
19 +
20 +
/// Exact-use ownership of a held lock; consume only through release.
21 +
export union Guard: Once {
22 +
    /// Lock address and acquired ticket; the lock must outlive this obligation.
23 +
    Held {
24 +
        /// Stable address of the acquired kernel lock.
25 +
        lock: *mut Lock,
26 +
        /// Ticket whose turn was observed with acquire ordering.
27 +
        ticket: u32,
28 +
    },
29 +
}
30 +
31 +
/// Atomically allocate one wrapping ticket with acquire/release ordering.
32 +
/// The native RV64A implementation zero-extends the old word to the u32 ABI.
33 +
fn ticket(pointer: *mut u32) -> u32;
34 +
35 +
/// Initialize unshared storage once, before publishing its stable address.
36 +
/// Reinitializing a published lock or one with outstanding callers is forbidden.
37 +
export fn init(lock: *mut Lock) {
38 +
    set lock.next = 0;
39 +
    set lock.serving = 0;
40 +
}
41 +
42 +
/// Wait in ticket order and return the obligation to release this lock once.
43 +
/// The caller must keep machine interrupts disabled until release, must not
44 +
/// acquire recursively, and must keep the lock at a stable address. There may
45 +
/// be at most platform::MAX_HARTS outstanding callers, so u32 wrap is unambiguous.
46 +
export fn acquire(lock: *mut Lock) -> Guard {
47 +
    let acquired = ticket(&mut lock.next);
48 +
    while atomic::load(&lock.serving) <> acquired {}
49 +
    return Guard::Held { lock, ticket: acquired };
50 +
}
51 +
52 +
/// Consume a held lock and release-publish all kernel writes to the next ticket.
53 +
export fn release(guard: Guard) {
54 +
    match guard {
55 +
        case Guard::Held { lock, ticket: acquired } => {
56 +
            atomic::store(&mut lock.serving, ((acquired as u64 + 1) & 0xffffffff) as u32);
57 +
        },
58 +
    }
59 +
}
60 +
61 +
/// Remote mechanisms only; a request does not convey capability authority.
62 +
export union RequestKind: Copy {
63 +
    /// Reconsider a waiting context after authorized event publication.
64 +
    Wake,
65 +
    /// Dispatch an explicitly authorized context with its assigned budget.
66 +
    Run,
67 +
    /// Stop the named context before lifecycle reclamation can proceed.
68 +
    Stop,
69 +
}
70 +
71 +
/// A remote message, not authority: the receiver must recheck current domain
72 +
/// incarnation and context identity under the global lock before any mutation.
73 +
export record Request: Copy {
74 +
    /// Mechanism requested of the receiving hart.
75 +
    kind: RequestKind,
76 +
    /// Complete target domain identity, including its incarnation.
77 +
    domain: abi::Object,
78 +
    /// Complete target context identity, including its incarnation.
79 +
    context: u64,
80 +
}
81 +
82 +
/// Caller-owned FIFO storage; every access after initialization requires the
83 +
/// global kernel lock, including canPost. Keep admission and posting in the
84 +
/// same critical section when another state change depends on admission.
85 +
export record Mailbox {
86 +
    /// Circular request storage; only count entries starting at head are live.
87 +
    entries: [Request; CAPACITY],
88 +
    /// Index of the oldest live request, always less than CAPACITY.
89 +
    head: u32,
90 +
    /// Number of live distinct requests, never greater than CAPACITY.
91 +
    count: u32,
92 +
}
93 +
94 +
/// Initialize private mailbox storage before publication to any receiving hart.
95 +
export fn initMailbox(mailbox: *mut Mailbox) {
96 +
    let empty = Request {
97 +
        kind: RequestKind::Wake,
98 +
        domain: abi::Object { kind: abi::Kind::Empty, index: 0, epoch: 0 },
99 +
        context: 0,
100 +
    };
101 +
    set mailbox.entries = [empty; CAPACITY];
102 +
    set mailbox.head = 0;
103 +
    set mailbox.count = 0;
104 +
}
105 +
106 +
/// Compare the entire message key, including both object and context incarnations.
107 +
fn identical(left: Request, right: Request) -> bool {
108 +
    return left.kind == right.kind and left.domain.kind == right.domain.kind
109 +
        and left.domain.index == right.domain.index and left.domain.epoch == right.domain.epoch
110 +
        and left.context == right.context;
111 +
}
112 +
113 +
/// Search only live entries; the caller holds the global kernel lock.
114 +
fn contains(mailbox: *Mailbox, request: Request) -> bool {
115 +
    for offset in 0..mailbox.count {
116 +
        if identical(mailbox.entries[(mailbox.head + offset) % CAPACITY], request) { return true; }
117 +
    }
118 +
    return false;
119 +
}
120 +
121 +
/// Check admission without mutation while holding the global kernel lock.
122 +
/// An identical outstanding request is admissible even when the mailbox is full.
123 +
export fn canPost(mailbox: *Mailbox, request: Request) -> bool {
124 +
    return mailbox.count < CAPACITY or contains(mailbox, request);
125 +
}
126 +
127 +
/// Append or coalesce under the global kernel lock. Identical requests retain
128 +
/// their original FIFO position. Busy means a full distinct request, and leaves
129 +
/// all mailbox state unchanged. The sender authorizes the operation separately.
130 +
export fn post(mailbox: *mut Mailbox, request: Request) throws (abi::Error) {
131 +
    if contains(mailbox, request) { return; }
132 +
    if mailbox.count == CAPACITY { throw abi::Error::Busy; }
133 +
    set mailbox.entries[(mailbox.head + mailbox.count) % CAPACITY] = request;
134 +
    set mailbox.count = mailbox.count + 1;
135 +
}
136 +
137 +
/// Remove the oldest request under the global kernel lock, or return nil.
138 +
/// Revalidate the message's current identities before performing its operation.
139 +
export fn take(mailbox: *mut Mailbox) -> ?Request {
140 +
    if mailbox.count == 0 { return nil; }
141 +
    let request = mailbox.entries[mailbox.head];
142 +
    set mailbox.head = (mailbox.head + 1) % CAPACITY;
143 +
    set mailbox.count = mailbox.count - 1;
144 +
    return request;
145 +
}
146 +
147 +
/// Read mhartid in machine mode; boot must validate it fits platform::MAX_HARTS.
148 +
export unsafe fn hart() -> u32;
149 +
150 +
/// Set a validated CLINT MSIP bank with full memory and I/O ordering.
151 +
/// clint is the physical controller base; bank is its FDT-selected bank index,
152 +
/// not necessarily a hardware hart ID. Publish the request before calling.
153 +
export unsafe fn kick(clint: u64, bank: u32);
154 +
155 +
/// Clear a validated CLINT MSIP bank with full memory and I/O ordering.
156 +
/// Use the same lock as posting to serialize acknowledgement with queue drain;
157 +
/// clearing after an unlocked empty observation can lose a concurrent kick.
158 +
export unsafe fn acknowledge(clint: u64, bank: u32);
159 +
160 +
/// Synchronize this hart's instruction fetch after observing published code.
161 +
/// Each executing hart must call this before entering newly published code.
162 +
export unsafe fn syncInstructions();
kernel/parallel.rad added +121 -0
1 +
//! Machine proof of per-hart stacks, coherent ticket locking, and CLINT requests.
2 +
3 +
use core::abi;
4 +
use core::atomic;
5 +
use core::clock;
6 +
use core::cpu;
7 +
use core::fdt;
8 +
use core::platform;
9 +
use core::smp;
10 +
11 +
mod mailboxes;
12 +
13 +
/// Release-published platform initialization.
14 +
static READY: u32 = 0;
15 +
/// Single FIFO lock for all protected counters and request mailboxes.
16 +
static LOCK: smp::Lock = undefined;
17 +
/// Firmware-selected hardware routes, immutable after publication.
18 +
static PLATFORM: platform::Platform = undefined;
19 +
/// Protected counter incremented by every actual hardware hart.
20 +
static COUNTER: u32 = 0;
21 +
/// Per-hart completion publication for each phase.
22 +
static DONE: [u32; platform::MAX_HARTS] = undefined;
23 +
/// Ordinary coherent data published before each software interrupt.
24 +
static PAYLOAD: [u32; platform::MAX_HARTS] = undefined;
25 +
/// Values observed by the receiving harts.
26 +
static OBSERVED: [u32; platform::MAX_HARTS] = undefined;
27 +
/// Bounded remote queues with the same lock as acknowledgement.
28 +
static MAILBOXES: [smp::Mailbox; platform::MAX_HARTS] = undefined;
29 +
30 +
/// Resolve a hardware ID to its validated firmware routing record.
31 +
fn route(hart: u32) -> platform::Hart {
32 +
    for i in 0..PLATFORM.hartCount {
33 +
        if PLATFORM.harts[i].id == hart { return PLATFORM.harts[i]; }
34 +
    }
35 +
    panic "route: executing hart absent from firmware";
36 +
}
37 +
38 +
/// Wait for all discovered harts, not absent array slots.
39 +
fn barrier(phase: u32) {
40 +
    for i in 0..PLATFORM.hartCount {
41 +
        let hart = PLATFORM.harts[i].id;
42 +
        while atomic::load(&DONE[hart]) < phase {}
43 +
    }
44 +
}
45 +
46 +
/// Execute the same kernel function independently on each online hart.
47 +
@default unsafe fn main(hardware: u64, description: *u8) -> u32 {
48 +
    assert hardware < platform::MAX_HARTS as u64;
49 +
    let hart = hardware as u32;
50 +
    if hart == 0 {
51 +
        let size = try! fdt::word(@sliceOf(description, 40), 4);
52 +
        assert size >= 40 and size <= fdt::MAX_BYTES;
53 +
        let mut tree: fdt::Tree = undefined;
54 +
        try! fdt::decode(@sliceOf(description, size), &mut tree);
55 +
        try! platform::discover(&tree, &mut PLATFORM);
56 +
        smp::init(&mut LOCK);
57 +
        set LOCK.next = 0xfffffffc;
58 +
        set LOCK.serving = 0xfffffffc;
59 +
        for i in 0..platform::MAX_HARTS {
60 +
            atomic::store(&mut DONE[i], 0);
61 +
            set PAYLOAD[i] = 0;
62 +
            set OBSERVED[i] = 0;
63 +
            smp::initMailbox(&mut MAILBOXES[i]);
64 +
        }
65 +
        mailboxes::run();
66 +
        atomic::store(&mut READY, 1);
67 +
    } else {
68 +
        while atomic::load(&READY) == 0 {}
69 +
    }
70 +
    let routing = route(hart);
71 +
    assert smp::hart() == hart;
72 +
    smp::syncInstructions();
73 +
    clock::arm(PLATFORM.clint.base, routing.clintIndex, 0xffffffffffffffff);
74 +
    smp::acknowledge(PLATFORM.clint.base, routing.clintIndex);
75 +
    for i in 0..256 {
76 +
        let guard = smp::acquire(&mut LOCK);
77 +
        set COUNTER += 1;
78 +
        smp::release(guard);
79 +
    }
80 +
    atomic::store(&mut DONE[hart], 1);
81 +
    if hart == 0 {
82 +
        barrier(1);
83 +
        assert COUNTER == PLATFORM.hartCount * 256;
84 +
        for i in 0..PLATFORM.hartCount {
85 +
            let recipient = PLATFORM.harts[i];
86 +
            if recipient.id == 0 { continue; }
87 +
            let guard = smp::acquire(&mut LOCK);
88 +
            set PAYLOAD[recipient.id] = 100 + recipient.id;
89 +
            try! smp::post(&mut MAILBOXES[recipient.id], smp::Request {
90 +
                kind: smp::RequestKind::Wake,
91 +
                domain: abi::Object { kind: abi::Kind::Domain, index: 7, epoch: 11 },
92 +
                context: 0x100000000 | recipient.id as u64,
93 +
            });
94 +
            smp::kick(PLATFORM.clint.base, recipient.clintIndex);
95 +
            smp::release(guard);
96 +
        }
97 +
        atomic::store(&mut DONE[hart], 2);
98 +
        barrier(2);
99 +
        for i in 0..PLATFORM.hartCount {
100 +
            let recipient = PLATFORM.harts[i].id;
101 +
            if recipient <> 0 { assert OBSERVED[recipient] == 100 + recipient; }
102 +
        }
103 +
    } else {
104 +
        let mut frame: cpu::Frame = undefined;
105 +
        cpu::init(&mut frame, 0, 0, 0, 0);
106 +
        clock::interrupts(8);
107 +
        cpu::idle(&mut frame);
108 +
        clock::interrupts(0);
109 +
        assert frame.cause == 0x8000000000000003;
110 +
        let guard = smp::acquire(&mut LOCK);
111 +
        let request = smp::take(&mut MAILBOXES[hart]) else { panic "main: missing published request"; };
112 +
        assert request.kind == smp::RequestKind::Wake;
113 +
        assert request.domain.index == 7 and request.domain.epoch == 11;
114 +
        assert request.context == (0x100000000 | hart as u64);
115 +
        set OBSERVED[hart] = PAYLOAD[hart];
116 +
        smp::acknowledge(PLATFORM.clint.base, routing.clintIndex);
117 +
        smp::release(guard);
118 +
        atomic::store(&mut DONE[hart], 2);
119 +
    }
120 +
    return 0;
121 +
}
kernel/parallel/mailboxes.rad added +125 -0
1 +
//! Real RV64A ticket wraparound and bounded remote-request queue contracts.
2 +
//! Machine checks use the kernel's atomic object code on a real emulated hart.
3 +
//! Queue checks hold a ticket guard for admission, publication, and consumption.
4 +
5 +
use core::abi;
6 +
use core::atomic;
7 +
use core::smp;
8 +
9 +
/// Make one valid domain message with an explicit full context identity.
10 +
fn request(kind: smp::RequestKind, epoch: u32, context: u64) -> smp::Request {
11 +
    return smp::Request {
12 +
        kind, domain: abi::Object { kind: abi::Kind::Domain, index: 7, epoch }, context,
13 +
    };
14 +
}
15 +
16 +
/// Reject a full distinct request without disturbing subsequent FIFO delivery.
17 +
fn busy(mailbox: *mut smp::Mailbox, message: smp::Request) {
18 +
    assert not smp::canPost(mailbox, message);
19 +
    try smp::post(mailbox, message) catch error {
20 +
        assert error == abi::Error::Busy;
21 +
        return;
22 +
    };
23 +
    panic "busy: full mailbox accepted a distinct request";
24 +
}
25 +
26 +
/// Consume and compare one whole message, including both incarnation fields.
27 +
fn expect(mailbox: *mut smp::Mailbox, expected: smp::Request) {
28 +
    let actual = smp::take(mailbox) else { panic "expect: mailbox lost a request"; };
29 +
    assert actual.kind == expected.kind;
30 +
    assert actual.domain.kind == expected.domain.kind;
31 +
    assert actual.domain.index == expected.domain.index;
32 +
    assert actual.domain.epoch == expected.domain.epoch;
33 +
    assert actual.context == expected.context;
34 +
}
35 +
36 +
/// Exercise full-ring coalescing, failed admission, FIFO order, and slot reuse.
37 +
/// The caller holds the same kernel lock for admission, publication, and drain.
38 +
fn capacity(mailbox: *mut smp::Mailbox) {
39 +
    for round in 0..2 {
40 +
        let base = 0x100000000 + round as u64 * 32;
41 +
        for i in 0..smp::CAPACITY {
42 +
            let message = request(smp::RequestKind::Wake, 3, base + i as u64);
43 +
            assert smp::canPost(mailbox, message);
44 +
            try! smp::post(mailbox, message);
45 +
        }
46 +
        let duplicate = request(smp::RequestKind::Wake, 3, base + 3);
47 +
        assert smp::canPost(mailbox, duplicate);
48 +
        try! smp::post(mailbox, duplicate);
49 +
        busy(mailbox, request(smp::RequestKind::Wake, 3, base + 99));
50 +
        for i in 0..4 {
51 +
            expect(mailbox, request(smp::RequestKind::Wake, 3, base + i as u64));
52 +
        }
53 +
        for i in smp::CAPACITY..smp::CAPACITY + 4 {
54 +
            let message = request(smp::RequestKind::Wake, 3, base + i as u64);
55 +
            assert smp::canPost(mailbox, message);
56 +
            try! smp::post(mailbox, message);
57 +
        }
58 +
        let retained = request(smp::RequestKind::Wake, 3, base + 6);
59 +
        assert smp::canPost(mailbox, retained);
60 +
        try! smp::post(mailbox, retained);
61 +
        busy(mailbox, request(smp::RequestKind::Stop, 3, base + 6));
62 +
        for i in 4..smp::CAPACITY + 4 {
63 +
            expect(mailbox, request(smp::RequestKind::Wake, 3, base + i as u64));
64 +
        }
65 +
        assert smp::take(mailbox) == nil;
66 +
    }
67 +
}
68 +
69 +
/// Distinct mechanism, domain slot, domain epoch, and context epoch all survive.
70 +
/// Reposting each exact key must neither add entries nor reorder the first post.
71 +
fn identities(mailbox: *mut smp::Mailbox) {
72 +
    let wake = request(smp::RequestKind::Wake, 3, 0x100000007);
73 +
    let run = request(smp::RequestKind::Run, 3, 0x100000007);
74 +
    let stop = request(smp::RequestKind::Stop, 3, 0x100000007);
75 +
    let domainEpoch = request(smp::RequestKind::Wake, 4, 0x100000007);
76 +
    let contextEpoch = request(smp::RequestKind::Wake, 3, 0x200000007);
77 +
    let mut domainSlot = wake;
78 +
    set domainSlot.domain.index = 8;
79 +
    let mut objectKind = wake;
80 +
    set objectKind.domain.kind = abi::Kind::Empty;
81 +
    // Mailbox keys are transport data, not authorization or a live-identity test.
82 +
    let messages = [wake, run, stop, domainEpoch, contextEpoch, domainSlot, objectKind];
83 +
    for i in 0..messages.len { try! smp::post(mailbox, messages[i]); }
84 +
    for i in 0..messages.len {
85 +
        assert smp::canPost(mailbox, messages[i]);
86 +
        try! smp::post(mailbox, messages[i]);
87 +
    }
88 +
    for i in 0..messages.len { expect(mailbox, messages[i]); }
89 +
    assert smp::take(mailbox) == nil;
90 +
    // A consumed key is a new request, not a permanently retained coalescing bit.
91 +
    try! smp::post(mailbox, wake);
92 +
    expect(mailbox, wake);
93 +
    assert smp::take(mailbox) == nil;
94 +
}
95 +
96 +
/// Acquire and release real tickets across 0xffffffff, including sign-bit values.
97 +
/// A missing AMO zero-extension would leave acquire spinning at the first ticket.
98 +
fn wraparound(lock: *mut smp::Lock) {
99 +
    // The lock is unshared and has no callers when its starting counters are seeded.
100 +
    atomic::store(&mut lock.next, 0xfffffffe);
101 +
    atomic::store(&mut lock.serving, 0xfffffffe);
102 +
    for i in 0..4 {
103 +
        let current = ((0xfffffffe + i as u64) & 0xffffffff) as u32;
104 +
        let next = ((current as u64 + 1) & 0xffffffff) as u32;
105 +
        let guard = smp::acquire(lock);
106 +
        assert atomic::load(&lock.serving) == current;
107 +
        assert atomic::load(&lock.next) == next;
108 +
        smp::release(guard);
109 +
        assert atomic::load(&lock.serving) == next;
110 +
    }
111 +
}
112 +
113 +
/// Exercise queue behavior under a real ticket guard and wrapping exact-use release.
114 +
export fn run() {
115 +
    let mut lock: smp::Lock = undefined;
116 +
    let mut mailbox: smp::Mailbox = undefined;
117 +
    smp::init(&mut lock);
118 +
    smp::initMailbox(&mut mailbox);
119 +
    wraparound(&mut lock);
120 +
    let guard = smp::acquire(&mut lock);
121 +
    assert smp::take(&mut mailbox) == nil;
122 +
    capacity(&mut mailbox);
123 +
    identities(&mut mailbox);
124 +
    smp::release(guard);
125 +
}