Skip to content

feat(gust): one scheduler, one clock — spawn and timer import instead of embedding - #236

Merged
avrabe merged 3 commits into
mainfrom
feat/one-scheduler-one-clock
Jul 29, 2026
Merged

avrabe merged 3 commits into
mainfrom
feat/one-scheduler-one-clock

Conversation

@avrabe

@avrabe avrabe commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Fixes the defect #235 surfaced: the composite exported gust:os correctly while containing three schedulers and two clocks.

Principle: a provider must import every capability it does not own.

provider owns imports exports
exec the executor gust:os/taskdisp gust:os/exec + gust:sched/tasks
spawn nothing gust:sched/tasks gust:os/spawn
timer nothing gust:sched/tasks, gust:os/time gust:os/timer
time the clock gust:hal/mmio gust:os/time

plain/src/executor.rs is untouched — it stays in exec-provider with its proofs; spawn and timer lost their #[path] includes and all statics.

The composite is sealed

world root {
  import gust:os/taskdisp@0.1.0;
  import gust:hal/mmio@0.1.0;
  export gust:os/time@0.1.0;   export gust:os/log@0.1.0;
  export gust:os/spawn@0.1.0;  export gust:os/exec@0.1.0;
  export gust:os/timer@0.1.0;
}

No gust:sched anywhere. That was the reason for putting the scheduler seam in its own package rather than widening gust:os: a downstream tenant importing the published, signed world cannot reach set-deadline and mutate scheduling state. 20 741 B (was 16 905; the delta is exec's second exported interface and its WIT type section).

One scheduler, verified by routing

module0  7573 B  dispatch=1 spawn-route=0 timer-route=0  gust:os/taskdisp poll-task
module1  1219 B  dispatch=0 ...                          gust:hal/mmio read32
module2  1078 B  dispatch=0 ...                          gust:hal/mmio write32
module3  1452 B  dispatch=0 spawn-route=1 timer-route=0  gust:sched/tasks state poll-round admit wake
module4  1463 B  dispatch=0 spawn-route=0 timer-route=1  gust:os/time now deadline; gust:sched/tasks set-deadline slept-status

Exactly one module owns dispatch and imports no gust:sched; spawn and timer route their scheduler operations to it. A provider with a private table would do those locally and not import them.

A gate was written, disproved, and thrown away

The first ownership check searched each core module for plain/src/executor.rs (rustc bakes it into bounds-check panic locations). It is not a detector: rebuilding the pre-change spawn and timer providers showed neither module contains that string — both bounds-check before indexing, so the panic path is elided — while both held a full private Tasks. That gate would have printed "exactly one" over the triplicated composite. A Tasks lives in wasm .bss, which a module never declares, so there is no binary signal for "holds a table".

Replaced with two decidable checks: source-level (only one crate may #[path]-include the executor) and composite-level routing (above). Negative-tested against the pre-change providers — it fires on all three signals.

The residual-import negative test runs in-band on every invocation: the script composes the same five components with an unwired script and requires the same function to reject it.

wac plug now works — and still cannot build this

The edges are real now, and wac plug timer-provider --plug exec-provider --plug time-provider succeeds where every pre-change ordering failed. It still can't produce the deliverable: a plug result exports only the socket's exports, and plug never wires plug→plug, so gust:sched/tasks goes unsatisfied. So wac compose — but the script now names real edges instead of a bare { ... } union.

Not claimed

  • Not executed. "One scheduler" is verified structurally. Nobody has called spawn.start() and observed the handle in timer.sleep() through this composite. The two qemu probes exercise the executor natively and confirm no regression, not the new wiring.
  • Linear memories are still 5 (1/17/17/1/17 pages), unchanged. That is the Component Model being shared-nothing — not the defect, not a regression. No memory reduction is claimed because none was measured.
  • spawn-provider-cm3.o, timer-provider-cm3.o and os-ts-cm3.o are deliberately not regenerated; standalone re-measurements recorded in the RESULTS files (spawn text 1108→376, timer 844→552).
  • A stale claim was corrected rather than inherited: timer-provider/RESULTS.md said sleep was blocked on synth#518. Measured pre- and post-change at synth 0.49.0 — both compile 5/5. That text described an older ticks: u64 WIT; this change did not fix it, and the file now says so.

🤖 Generated with Claude Code

https://claude.ai/code/session_011QG86sovTbfnPNY9SfhSmo

avrabe and others added 3 commits July 29, 2026 15:04
…e (gale#224)

One wasm component exporting all five gust:os capabilities (time, log, spawn,
exec, timer) whose only residual imports are gust:hal/mmio and the app-supplied
gust:os/taskdisp. 16905 B, validates, gated by build-fused-gustos.sh.

wac plug does not apply: the five providers are a flat antichain (no provider
consumes another's export), so every plug ordering exits "the socket component
had no matching imports for the plugs that were provided". The operation needed
is a union merge with import forwarding — wac compose over a WAC script.

build-gustos-components.sh now builds all five providers, and its per-component
gate is widened from "imports only gust:hal/*" to "imports only gust:hal/* or
gust:os/*" — exec-provider and spawn-provider legitimately import the trusted
gust:os/taskdisp dispatch, which the APPLICATION supplies, so it is expected to
survive composition. Negative-tested: env, wasi:*, and heap imports still fail.

Not lowered to native, not run anywhere, and the three embedded executor copies
remain three separate Tasks tables in three separate linear memories — the
composite is structurally fused but semantically triplicated. FUSED-GUSTOS.md
records all of this explicitly.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011QG86sovTbfnPNY9SfhSmo
…mer stateless (gale#224)

exec/spawn/timer-provider each #[path]-included plain/src/executor.rs and each
kept its own `static mut TASKS`. Composed, that was three component instances
with three scheduler states, so a handle from spawn.start() was meaningless to
timer.sleep() and exec.state() — while wit-os/gust-os.wit's timer.sleep doc
already called its argument "the handle the app holds from spawn.start". The WIT
specified a shared handle space the implementation did not provide. Separately,
timer-provider and time-provider each hardcoded TIM2_CNT = 0x4000_0024 and read
it independently: one clock only because two constants matched.

A provider must import every capability it does not own:

  exec   owns the executor  imports taskdisp            exports exec + sched/tasks
  spawn  owns nothing       imports sched/tasks         exports spawn
  timer  owns nothing       imports sched/tasks + time  exports timer
  time   owns the clock     imports hal/mmio            exports time
  log    —                  imports hal/mmio            exports log

New WIT package gust:sched@0.1.0 (wit-os/deps/sched) holds the internal seam,
deliberately outside gust:os: composition satisfies it in place, so it never
appears in the fused component's world and a tenant importing gust:os has no
name to bind scheduler mutation to. spawn and timer are now stateless — no
static mut, no mapping table; a spawn handle IS the executor's handle. timer has
no gust:hal import at all. plain/src/executor.rs is untouched (Verus/Kani).

Gates:
- build-gustos-components.sh: allows gust:sched per-provider, and gates at source
  that exactly one crate #[path]-includes the executor.
- build-fused-gustos.sh: residual imports EXACTLY gust:hal/mmio + gust:os/taskdisp
  (gust:sched must not leak), with an in-band negative test that composes the same
  five components unwired and requires the identical check to reject it; plus a
  one-scheduler gate that unbundles the composite and asserts one dispatcher and
  that spawn's and timer's scheduler operations both route to it.
- Rejected signal, recorded in FUSED-GUSTOS.md: searching each core module for the
  executor's source path is NOT a detector — rebuilt from HEAD, neither pre-change
  provider module contains it (bounds checks elided) though both held a table.
- build-os-ts.sh: exec-provider joins the node and the composition moves from
  wac plug to wac compose (plug never wires plug->plug, so the spawn->exec edge
  has to be named). Verified end-to-end to a temp OSNODE: 7836 B / 8192.

Composite: 20741 B, 5 exports, residuals mmio + taskdisp. Still 5 linear memories
at 1/17/17/1/17 pages — unchanged, and not the defect: the CM is shared-nothing
and every instance gets its own. No memory-count reduction is claimed.

Checked-in *.o were deliberately not regenerated. gust_exec_probe and
gust_timer_probe still pass (both exercise the executor natively, so they confirm
no regression rather than exercising the new composition).

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011QG86sovTbfnPNY9SfhSmo
…e-clock

# Conflicts:
#	benches/gust/drivers/FUSED-GUSTOS.md
#	benches/gust/drivers/build-fused-gustos.sh
#	benches/gust/drivers/build-gustos-components.sh
@codecov

codecov Bot commented Jul 29, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@avrabe
avrabe merged commit 80a82ca into main Jul 29, 2026
64 checks passed
@avrabe
avrabe deleted the feat/one-scheduler-one-clock branch July 29, 2026 18:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant