Skip to content

feat(gust): gust_stack — dissolved CM composition driven ON the kiln stack (north-star end-to-end) - #105

Merged
avrabe merged 7 commits into
mainfrom
feat/gust-stack-north-star
Jun 24, 2026
Merged

feat(gust): gust_stack — dissolved CM composition driven ON the kiln stack (north-star end-to-end)#105
avrabe merged 7 commits into
mainfrom
feat/gust-stack-north-star

Conversation

@avrabe

@avrabe avrabe commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

The BYO-OS north star — drive the real components via the Component Model → meld-fuse → dissolve → run on the kiln/gust stack — now executes end to end, bare-metal.

gust_fused (#94) proved the dissolve: CM → fuse → synth → run-demo()=53, called once.
gust_stack proves the "on our stack" half: the kiln-async Scheduler (gust's executor) drives the same dissolved composition (gale-app-demo + gale-kiln, fused → native) as the body of a kiln task, re-polled every scheduler round.

cargo run --release --bin gust_stack      # qemu lm3s6965evb / Cortex-M3
→ gust-stack: 5000 poll rounds; dissolved run-demo() = 53 each round; mismatches = 0
→ the verified gale CM composition ran on the kiln/gust stack (dissolved to native, no runtime)

So the whole path runs with no wasm runtime: components on top (CM) → meld fuse (one merged-memory module) → synth dissolve (native) → driven by the kiln-async scheduler on gust. The verified gale components execute as scheduled work on the executor, not a one-shot call.

  • src/bin/gust_stack.rs — kiln Scheduler + the dissolved run-demo as the task body.
  • build.rs — links fused.o into gust_stack too (-bin= scoped; native bins unaffected).
  • DEMONSTRATOR.md — the on-stack section.
  • rivet FIND-BYOOS-008 verifies REQ-BYOOS-MULTIRT-001, related-to SAC-BYOOS-EXEC (the kiln executor). rivet validate: PASS.

Next rungs: a richer driven example (engine_control control-loop as the task body) and the same on real silicon (G474RE/F100).

🤖 Generated with Claude Code

avrabe and others added 2 commits June 24, 2026 20:52
…iln stack (north-star end-to-end)

gust_fused proved the dissolve (CM → meld fuse → synth → run-demo()=53, one-shot).
gust_stack proves the other half — "running on our stack": the kiln-async
Scheduler (gust's executor) drives the SAME dissolved composition (gale-app-demo +
gale-kiln, fused → native) as the body of a kiln task, re-polled every scheduler
round. Bare-metal qemu Cortex-M3: 5000 poll rounds, dissolved run-demo()=53 each
round, 0 mismatches, no wasm runtime.

So the whole BYO-OS north-star executes end-to-end: components on top (Component
Model) → meld fuse (one merged-memory module) → synth dissolve (native) → driven
by the kiln-async scheduler on the gust stack.

- src/bin/gust_stack.rs — kiln Scheduler + the dissolved run-demo as the task body.
- build.rs — links fused.o into gust_stack too (scoped -bin=).
- DEMONSTRATOR.md — the on-stack section.
- rivet FIND-BYOOS-008 (verifies REQ-BYOOS-MULTIRT-001, related-to SAC-BYOOS-EXEC);
  rivet validate PASS.

Next rungs: a richer driven example (engine_control control-loop as the task body)
and the same on real silicon (G474RE/F100).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ic sensors→actuators loop)

gust_control: the kiln-async scheduler drives the dissolved engine_control
algorithm (synth-dissolved control_step, same as control.c / the WIT component)
as its task body — one control tick per scheduler round: synthesize a crank
sample → control_step → spark/fuel actuators. Where gust_stack drives the
fixed-result run-demo, this drives a real control loop, the workload an engine
node runs.

Bare-metal qemu Cortex-M3: gate control_step(3000,50,80,0) = spark 33° fuel 2300µs
(== C/wasmtime); 5000 ticks; last sample control_step(4700,75,80,0) = spark 38°
fuel 2440µs (== wasmtime). Dissolved, no runtime, on the gust/kiln stack.

Two TCB-integration findings (documented):
- control_step needs synth --native-pointer-abi (it reads ignition/fuel TABLES
  from wasm linmem; the plain --relocatable .o emits no data section → reads 0)
  + --shadow-stack-size 8192 (else it reserves the full 128KB linmem as .bss and
  overflows RAM — synth#383 class). And an r11=0 TCB trampoline: native-pointer-abi
  pins the linmem base to r11==0, which the kiln scheduler clobbers, so in-loop
  calls need r11 re-zeroed (same shim the dissolved gust kernel uses).
- Built with SYNTH_NO_LOCAL_PROMOTE=1: v0.14.0's default-on local promotion
  register-exhausts on this denser function (filing to synth — the promotion
  cost-gate needs register-pressure awareness; the non-promoted lowering is fine).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@codecov

codecov Bot commented Jun 24, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

avrabe and others added 5 commits June 24, 2026 21:33
…rtex-M4 silicon (G474RE)

gust_control flashed on the physical NUCLEO-G474RE (probe-rs): the kiln-async
scheduler drives the dissolved engine_control control loop on real hardware —
control_step(3000,50,80,0)=spark33/fuel2300 (== C/wasmtime), 5000 ticks, last
(4700,75,80,0)=spark38/fuel2440 (== wasmtime). The full north-star executes on
silicon: components (CM) → meld fuse / synth dissolve → driven by kiln on gust,
Cortex-M4, no runtime.

- wasm-kernel/control_step-cm4.o — cortex-m4 dissolve (native-pointer-abi +
  shadow-stack-size 8192, SYNTH_NO_LOCAL_PROMOTE per synth#474); build.rs picks
  cm4 for thumbv7em / cm3 for qemu+F100 (the cortex-m3 .o won't link into M4).
- silicon/RESULTS-g474re.md — the on-silicon rung-2 record.
F100/M3 silicon pending the board.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…8K + control 64K + real STM32F100)

The durable answer to "why not renode": a one-off `renode --console` on a dev
box isn't reproducible — a Bazel `renode_test` target is. Extend the existing
self-contained renode-test module (own MODULE.bazel + git_override on
pulseengine/renode-bazel-rules) from one target to three M3 device classes,
each with a checked-in hermetic .repl (no dependency on the pinned Renode's
bundled platform set):

  - gust-renode         generic M3 8K  — dissolved gust kernel (existing)
  - gust-control-renode M3 64K (F103RE)— north-star rung 1: kiln driving the
                                          dissolved engine_control control_step;
                                          deterministic cycle count
  - gust-f100-renode    STM32F100RB    — the real STM32VLDISCOVERY part (8K/128K),
                                          dissolved kernel pinned to its memory class

gust_control's 9408 B .bss needs the 64K class — it does NOT fit the F100's 8K
SRAM, so the F100 target runs the kernel and the 64K F103RE-class runs the
control loop (honest device-fit constraint).

Verified locally on the same Renode engine (1.16.1): gust_control = 0x162CB6 =
1,453,238 instr over RunFor 2s, no fault, SP at the 64K top; the kernel on F100
= 200,000,000 instr, bss fits (ends 0x200012B4 under the 8K top 0x20002000), no
fault. M3 is cacheless so instr ≈ cycles — this is the CI-reproducible cycle-
class seed, complementary to the qemu correctness gate and the G474RE DWT bench.

Honest finding (documented in README): the SemihostingUart heartbeat is NOT
capturable headless on the macOS portable (CreateFileBackend on cpu.uartSemihosting
captures nothing), and the repo's proven `Wait For Line` tests assert on a real
USART, not a SemihostingUart — so a content assertion is deferred to CI rather
than added blind (would risk a 120s timeout -> CI-red). Correctness stays gated
by the qemu exit-code run + gale_decider_diff; Renode adds the cycle dimension.

Also adds a targeted .gitignore rule for .claude/pulseengine/ agent working
state (per the issue-hunt skill) so per-package working-context.md can't be
committed by accident.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ad break

No workflow ran the gust renode-test bazel targets — so the module's "first
green is in CI" was aspirational, and a latent break went unnoticed: under the
repo's pinned Bazel 9.1.0, rules_renode (pinned 5fc76ad) fails to LOAD because
its renode/defs.bzl uses PyInfo / py_* as builtins, which Bazel 8+ removed
("name 'PyInfo' is not defined", defs.bzl:10 and :179). The existing
gust-renode target was equally broken; nothing exercised it.

Fixes:
- benches/gust/renode-test/.bazelrc: re-inject the legacy Python globals via
  --incompatible_autoload_externally so the pinned rule loads unmodified, and
  --check_direct_dependencies=off for the rules_python 0.36.0->1.7.0 transitive
  skew. Verified locally: `bazel query //:*` now resolves all three targets on
  Bazel 9.1.1 (was: load error).
- .github/workflows/gust-renode.yml: a dedicated Linux job running
  `bazel test //:gust-renode //:gust-control-renode //:gust-f100-renode`. This
  is the actual CI runner that makes the dissolved-artifact, real-M3-model,
  deterministic-cycle gate reproducible (no board), complementing renode-tests.yml
  (Zephyr sem robots) and zephyr-tests.yml (qemu M3 functional).

Upstream friction to file on pulseengine/renode-bazel-rules: defs.bzl should
`load("@rules_python//python:defs.bzl", "PyInfo", ...)` rather than rely on the
removed builtins (breaks on Bazel 8/9 without the autoload shim).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…incompatible)

First CI run failed at analysis on Bazel 9.1 with a second, deeper break beyond
PyInfo: "@@rules_shell+//shell/runfiles:runfiles_impl must produce a single
file". Root cause: the pinned rules_renode (5fc76ad == upstream HEAD, nothing
newer to bump to) is authored against Bazel 8 — it relies on the PyInfo/py_*/sh_*
globals AND the old rules_shell runfiles_impl single-file behavior, all of which
Bazel 8 provides via the DEFAULT --incompatible_autoload_externally migration
set. Bazel 9 emptied that set (migration window closed), so gale's root-pinned
9.1.0 breaks the module at both load and analysis. Upstream's own CI uses
bazelisk-latest with no .bazelversion (i.e. Bazel 8), so 8.x is the known-good
config.

Fix: a module-local .bazelversion (8.7.0) — bazelisk picks the nearest one from
cwd, so it overrides root 9.1.0 only for this self-contained module (own
MODULE.bazel; the rest of gale stays on 9.1.0). Dropped the explicit
--incompatible_autoload_externally override from .bazelrc: on Bazel 8 its
default set already covers both py_* and sh_*; replacing it with only the py
symbols would have broken the sh_* autoload the runfiles path needs. Verified:
`bazel query //:*` resolves all three targets on 8.7.0 with no autoload flag.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ves its toolchain via execroot path

The Bazel 8 pin got the module loading, analyzing, building (16 actions) and the
three tests EXECUTING — they then died at runtime, all three:
  ...renode_1.15.3+...-dotnet_portable/renode-test: No such file or directory

Root cause: rules_renode's renode_test wrapper (defs.bzl:104) invokes the
toolchain binary by its EXECROOT-relative path
"external/rules_renode++renode+renode_toolchain/.../renode-test". Inside the
test's runfiles tree (CWD = ...runfiles/_main) that path only resolves if Bazel
materialises the legacy "_main/external/<repo>" symlinks — i.e.
--legacy_external_runfiles. Its default flipped to false in recent Bazel, so the
wrapper can't find renode-test. The rule was authored when it defaulted true
(matching upstream's bazelisk-latest CI at the time). Setting it true in the
module .bazelrc restores resolution.

Verified in 8.7.0 that the flag exists and defaults false (bazel help test).
This is the same upstream-friction item (task #32): the rule should rlocation
the toolchain binary instead of relying on the execroot path + legacy symlinks.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@avrabe
avrabe merged commit 4c483d6 into main Jun 24, 2026
100 of 102 checks passed
@avrabe
avrabe deleted the feat/gust-stack-north-star branch June 24, 2026 21:28
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