Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .config/hakari.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,10 @@ platforms = [
[final-excludes]
# `nexus` is the no_std kernel — it must not depend on the (std) workspace-hack.
# hakari removes the edge; `hakari verify` enforces its absence. (#279)
workspace-members = ["nexus"]
#
# `nexus-nostd-smoketest` is the no_std derive-macro compile-gate (#304): it is
# built for thumbv7em/wasm32, so a std workspace-hack edge would break it.
workspace-members = ["nexus", "nexus-nostd-smoketest"]
third-party = [
{ name = "futures-core" },
]
8 changes: 8 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ members = [
"crates/nexus-fjall",
"crates/nexus-macros",
"crates/nexus-macros/tests/cross_crate_test",
"crates/nexus-nostd-smoketest",
"crates/nexus-postgres",
"crates/nexus-store",
"crates/nexus-store-testing",
Expand Down
24 changes: 24 additions & 0 deletions crates/nexus-nostd-smoketest/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
[package]
name = "nexus-nostd-smoketest"
version = "0.0.0"
edition = "2024"
publish = false

# Compile-gate crate for #304: proves the `#[nexus::aggregate]` /
# `#[derive(DomainEvent)]` macro OUTPUT (not just its source) is `core`-clean by
# building it for `thumbv7em-none-eabihf` + `wasm32-unknown-unknown` under
# `nix flake check`. Deliberately excluded from `workspace-hack` (see
# .config/hakari.toml) — a std workspace-hack edge would defeat the bare-metal
# build. No `[lints] workspace = true` for the same reason it needs no hack edge:
# it is a target-compile probe, exercised only through the two flake gates.

[dependencies]
nexus = { path = "../nexus", default-features = false, optional = true }
thiserror = { workspace = true, optional = true }

[features]
default = []
# The gate builds `--no-default-features --features derive`: with `derive` off
# this is an empty `#![no_std]` lib; with it on, the macro output is compiled for
# the target. `nexus/derive` is additive and does NOT pull `nexus/std`.
derive = ["dep:nexus", "nexus/derive", "dep:thiserror"]
24 changes: 24 additions & 0 deletions crates/nexus-nostd-smoketest/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
//! `no_std` compile smoke-test for the nexus derive macros (#304).
//!
//! #279 (PR #303) made the `nexus` kernel `no_std` and added flake gates that
//! build the kernel **bare** (`--no-default-features`). Those gates prove the
//! *core* compiles `no_std`, but they never compile the **output** of
//! `#[nexus::aggregate]` / `#[derive(DomainEvent)]` for a `no_std` target — the
//! macro source was only grepped for `std::` paths, which is weak.
//!
//! This crate closes that gap: it defines a real aggregate using BOTH macros
//! plus a [`Handle`] impl, entirely in `core` (no allocator — `Events<E, 0>` is
//! the single-event `ArrayVec` path). The two flake gates build it for
//! `thumbv7em-none-eabihf` and `wasm32-unknown-unknown`; if a macro ever emits a
//! `std::` path, the generated code fails to compile for `thumbv7em` and the
//! gate goes red. Logic correctness is already covered on the host by the
//! `nexus-cross-crate-test` nextest suite — this crate is a *compile* probe.
#![no_std]

// The whole probe lives behind `derive`: with the feature off this is an empty
// `#![no_std]` lib (trivially portable); with it on, the macro output is what
// gets compiled for the target. `pub` so the probe's items are reachable API —
// otherwise every generated/hand-written item reads as dead code (a hard error
// under the flake's `clippy --deny warnings`).
#[cfg(feature = "derive")]
pub mod smoke;
115 changes: 115 additions & 0 deletions crates/nexus-nostd-smoketest/src/smoke.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
//! The `no_std` aggregate whose macro output the flake gates compile.
//!
//! Mirrors `nexus-cross-crate-test` (the std cross-crate probe) but stays in
//! `core`: `core::fmt` for `Display`, a fixed-size `[u8; 8]` id (no `String`),
//! and `Events<TaskEvent>` at the default `N = 0` (single event, no allocator).

use core::fmt;
use nexus::{AggregateRoot, AggregateState, DomainEvent, Events, Handle, Version, events};

// --- Id (core-only: fixed array, `core::fmt` Display) ---
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
pub struct TaskId([u8; 8]);

impl TaskId {
pub fn new(id: u64) -> Self {
Self(id.to_be_bytes())
}
}

impl fmt::Display for TaskId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "task-{}", u64::from_be_bytes(self.0))
}
}

impl AsRef<[u8]> for TaskId {
fn as_ref(&self) -> &[u8] {
&self.0
}
}

// --- Events (derive macro output compiled for the target) ---
#[derive(Debug, Clone, DomainEvent)]
pub enum TaskEvent {
Started,
Finished,
}

// --- State ---
#[derive(Default, Debug, Clone)]
pub struct TaskState {
started: bool,
done: bool,
}

impl AggregateState for TaskState {
type Event = TaskEvent;
fn initial() -> Self {
Self::default()
}
fn apply(mut self, event: &TaskEvent) -> Self {
match event {
TaskEvent::Started => self.started = true,
TaskEvent::Finished => self.done = true,
}
self
}
}

// --- Error (thiserror, no_std — no `std::error::Error` bridge on target) ---
#[derive(Debug, thiserror::Error)]
pub enum TaskError {
#[error("already started")]
AlreadyStarted,
#[error("not started")]
NotStarted,
#[error("already done")]
AlreadyDone,
}

// --- Aggregate (attribute macro output compiled for the target) ---
#[nexus::aggregate(state = TaskState, error = TaskError, id = TaskId)]
pub struct TaskAggregate;

// --- Commands + decide (single-event `Events<_, 0>`, no allocator) ---
pub struct StartTask;
pub struct FinishTask;

impl Handle<StartTask> for TaskAggregate {
fn handle(state: &TaskState, _cmd: StartTask) -> Result<Events<TaskEvent>, TaskError> {
if state.started {
return Err(TaskError::AlreadyStarted);
}
Ok(events![TaskEvent::Started])
}
}

impl Handle<FinishTask> for TaskAggregate {
fn handle(state: &TaskState, _cmd: FinishTask) -> Result<Events<TaskEvent>, TaskError> {
if !state.started {
return Err(TaskError::NotStarted);
}
if state.done {
return Err(TaskError::AlreadyDone);
}
Ok(events![TaskEvent::Finished])
}
}

// Exercise the generic `AggregateRoot` surface so the monomorphised driver
// methods (`new`/`handle`/`commit_persisted`/`version`) are instantiated for a
// `no_std` aggregate — a std leak in any of them would surface here at compile
// time on the target.
pub fn drive(id: u64) -> Option<Version> {
let mut root = AggregateRoot::<TaskAggregate>::new(TaskId::new(id));
if let Ok(events) = root.handle(StartTask) {
root.commit_persisted(Version::INITIAL, &events);
}
root.version()
}

// Prove the `DomainEvent::name` codegen is reachable in `core`.
pub fn event_name(event: &TaskEvent) -> &'static str {
event.name()
}
6 changes: 6 additions & 0 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -131,11 +131,16 @@
# `nexus-nostd` (thumbv7em-none-eabihf) is the STRONG gate: a fully
# std-free bare-metal target. `wasm32-unknown-unknown` still ships std,
# so it alone would not catch a std leak. Both build --no-default-features.
# Each gate also builds `nexus-nostd-smoketest` (#304): a crate that
# uses `#[nexus::aggregate]` + `#[derive(DomainEvent)]`, so the macro
# OUTPUT — not just its source — is compiled for the target. A macro
# emitting a `std::` path fails the thumbv7em build here.
nexus-wasm = craneLib.mkCargoDerivation (commonArgs // {
inherit cargoArtifacts;
pname = "nexus-wasm";
buildPhaseCargoCommand = ''
cargo build -p nexus --target wasm32-unknown-unknown --no-default-features
cargo build -p nexus-nostd-smoketest --target wasm32-unknown-unknown --no-default-features --features derive
'';
});

Expand All @@ -144,6 +149,7 @@
pname = "nexus-nostd";
buildPhaseCargoCommand = ''
cargo build -p nexus --target thumbv7em-none-eabihf --no-default-features
cargo build -p nexus-nostd-smoketest --target thumbv7em-none-eabihf --no-default-features --features derive
'';
});
};
Expand Down
Loading