design-b Phase A: absorb libcontext's wasm backend (doc 22)

Bumps wxwidgets (fiber lane in sched_context.h) and kicad (libcontext's wasm
backend becomes an adapter over it), so there is now ONE party performing
every emscripten_fiber_swap and recording who is on the CPU. Behaviour is
preserved throughout - libcontext still decides, the registry observes and
beacons any disagreement - which is the de-risking step doc 20's D2 never had.

Harness + spec gain three fiber-lane scenarios: a fresh fiber enters at its
entry and a swap suspends the swapper (fiber_roundtrip), releasing a suspended
fiber is legal while a stale id refuses instead of use-after-free
(fiber_release_suspended), and symmetric swaps leave a parked star context
undisturbed (fiber_and_star_coexist). The spec asserts the lane's counters,
including the tripwire that must stay zero: fiberNonEnterableSwaps.

Doc 22 gains the Phase A work log: the gate (kicad suite 139/1, the 1 being the
pre-existing occ-probe glb matrix; wx 346/1/3; batteries 48/48; zero tripwires
anywhere), the four bugs the tripwires caught and the single rule behind three
of them, the grace ring and the 33-coroutine measurement Phase B needs from it,
and the process traps this run paid for.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EarUW9DS1c1sSW4ZNrkGQS
This commit is contained in:
Gergő Törcsvári 2026-08-06 20:19:42 +02:00
commit 586fbadfe9
No known key found for this signature in database
GPG key ID: 8E75F2CDE64E5322
5 changed files with 322 additions and 3 deletions

View file

@ -523,6 +523,176 @@ void Scenario_ForeignStackRefused()
report( "foreign_stack_refused", true );
}
// ---------------------------------------------------------------------------
// FIBER LANE scenarios (doc 22 Phase A) — the symmetric semantics libcontext's
// wasm backend adapts onto. These pin what the adapter relies on: a fresh
// fiber enters at its entry, a swap suspends the swapper, enterability is
// registry truth, releasing a suspended fiber is legal, a stale id refuses
// instead of use-after-free, and the star lane is undisturbed throughout.
// ---------------------------------------------------------------------------
ContextId g_fiber_root = 0;
ContextId fiber_root()
{
// Adopted once for the whole battery, like libcontext's main context.
if( !g_fiber_root )
g_fiber_root = fiber_adopt_current( 64 * 1024, "test-root" );
return g_fiber_root;
}
alignas( 16 ) char g_fiber_a_stack[64 * 1024];
ContextId g_fiber_a = 0;
int g_fiber_a_runs = 0;
int g_fiber_a_saw_current = -1;
int g_fiber_a_saw_root_enterable = -1;
int g_fiber_a_saw_self_enterable = -1;
void fiber_a_entry( void* )
{
// Never returns, like libcontext's trampoline: run, swap back, repeat.
for( ;; )
{
++g_fiber_a_runs;
g_fiber_a_saw_current = ( fiber_current() == g_fiber_a ) ? 1 : 0;
g_fiber_a_saw_root_enterable = fiber_enterable( g_fiber_root ) ? 1 : 0;
g_fiber_a_saw_self_enterable = fiber_enterable( g_fiber_a ) ? 1 : 0;
fiber_swap( g_fiber_a, g_fiber_root );
}
}
void Scenario_FiberRoundtrip()
{
if( !fiber_root() )
return report( "fiber_roundtrip", false, "could not adopt the root" );
g_fiber_a_runs = 0;
g_fiber_a = fiber_create( fiber_a_entry, nullptr, g_fiber_a_stack,
sizeof( g_fiber_a_stack ), 32 * 1024, "fiber-a" );
if( !g_fiber_a )
return report( "fiber_roundtrip", false, "fiber_create failed" );
if( status_of( g_fiber_a ) != Status::Fresh || !fiber_enterable( g_fiber_a ) )
return report( "fiber_roundtrip", false, "a fresh fiber should be enterable" );
if( g_fiber_a_runs != 0 )
return report( "fiber_roundtrip", false, "body ran before the first swap" );
if( !fiber_swap( g_fiber_root, g_fiber_a ) )
return report( "fiber_roundtrip", false, "first swap refused" );
// Back here: the fiber ran once and swapped back to the root.
if( g_fiber_a_runs != 1 )
return report( "fiber_roundtrip", false,
"runs=" + std::to_string( g_fiber_a_runs ) + " after first swap" );
if( fiber_current() != g_fiber_root )
return report( "fiber_roundtrip", false, "root is not fiber_current() again" );
if( status_of( g_fiber_a ) != Status::Suspended || !fiber_enterable( g_fiber_a ) )
return report( "fiber_roundtrip", false,
std::string( "suspended fiber status: " )
+ status_name( status_of( g_fiber_a ) ) );
// What the fiber observed mid-run: it was current, the suspended root was
// enterable, and it itself (Running) was not — registry truth, no guessing.
if( g_fiber_a_saw_current != 1 )
return report( "fiber_roundtrip", false, "fiber did not see itself as current" );
if( g_fiber_a_saw_root_enterable != 1 )
return report( "fiber_roundtrip", false, "suspended root was not enterable" );
if( g_fiber_a_saw_self_enterable != 0 )
return report( "fiber_roundtrip", false, "a RUNNING fiber claimed to be enterable" );
// Re-entry takes the loop again — the second swap resumes, not restarts.
if( !fiber_swap( g_fiber_root, g_fiber_a ) || g_fiber_a_runs != 2 )
return report( "fiber_roundtrip", false, "second swap did not re-run the body" );
report( "fiber_roundtrip", true );
}
void Scenario_FiberReleaseSuspended()
{
// Fiber A is Suspended after the roundtrip. Releasing it mid-suspend is
// LEGAL — libcontext refcounts drop never-finished coroutines — and the
// stale id must then refuse instead of resuming freed state.
if( !g_fiber_a || status_of( g_fiber_a ) != Status::Suspended )
return report( "fiber_release_suspended", false, "precondition: fiber A suspended" );
if( !fiber_release( g_fiber_a ) )
return report( "fiber_release_suspended", false, "release refused" );
if( fiber_enterable( g_fiber_a ) )
return report( "fiber_release_suspended", false, "released fiber still enterable" );
if( fiber_swap( g_fiber_root, g_fiber_a ) )
return report( "fiber_release_suspended", false,
"swap into a released fiber was allowed (use-after-free)" );
report( "fiber_release_suspended", true );
}
alignas( 16 ) char g_fiber_b_stack[64 * 1024];
ContextId g_fiber_b = 0;
int g_fiber_b_runs = 0;
int g_coexist_stage = 0;
int g_coexist_result = 0;
void fiber_b_entry( void* )
{
for( ;; )
{
++g_fiber_b_runs;
fiber_swap( g_fiber_b, g_fiber_root );
}
}
void body_coexist_park( void* )
{
g_coexist_stage = 1;
g_coexist_result = yield_park( "coexist-park" );
g_coexist_stage = 2;
}
void Scenario_FiberAndStarCoexist()
{
// One registry, two lanes: a PARKED star context must sit undisturbed
// while symmetric fiber swaps happen, and resume cleanly afterwards —
// production Phase A runs tool fibers while D1's star layer idles.
g_coexist_stage = 0;
g_fiber_b_runs = 0;
const ContextId star = create( body_coexist_park, nullptr, "coexist-star" );
drain(); // parks
if( g_coexist_stage != 1 || status_of( star ) != Status::Parked )
return report( "fiber_and_star_coexist", false, "star context did not park" );
g_fiber_b = fiber_create( fiber_b_entry, nullptr, g_fiber_b_stack,
sizeof( g_fiber_b_stack ), 32 * 1024, "fiber-b" );
if( !g_fiber_b || !fiber_swap( g_fiber_root, g_fiber_b ) || g_fiber_b_runs != 1 )
return report( "fiber_and_star_coexist", false, "fiber roundtrip failed" );
if( status_of( star ) != Status::Parked )
return report( "fiber_and_star_coexist", false,
"fiber swaps disturbed the parked star context" );
if( !mark_ready( star, 7 ) )
return report( "fiber_and_star_coexist", false, "mark_ready failed" );
drain();
if( g_coexist_stage != 2 || g_coexist_result != 7 )
return report( "fiber_and_star_coexist", false, "star context did not resume" );
destroy( star );
fiber_release( g_fiber_b );
report( "fiber_and_star_coexist", true );
}
// ---------------------------------------------------------------------------
// scenario 10: a REAL async wake — JS timeout resolves the park
// The park's wake crosses a JS turn, which is the shape every production
@ -609,6 +779,9 @@ int main()
Scenario_DeepPark();
Scenario_FiberNestsInContext();
Scenario_ForeignStackRefused();
Scenario_FiberRoundtrip();
Scenario_FiberReleaseSuspended();
Scenario_FiberAndStarCoexist();
// Async last: it emits STATS + SUMMARY when its wake lands.
Scenario_AsyncWake();