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
128 changes: 68 additions & 60 deletions crates/nexum-sdk/src/wit_bindgen_macro.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,9 @@
//!
//! Before this macro existed, each module hand-rolled ~80 lines of
//! mechanical glue: the `struct WitBindgenHost;` plus the core trait
//! impls (`ChainHost`, `LocalStoreHost`, `LoggingHost`) plus
//! `convert_chain_err` / `convert_fault` / `sdk_fault_into_wit` /
//! `convert_level`. The code differed across modules in zero places
//! that were not bugs.
//! impls (`ChainHost`, `LocalStoreHost`, `LoggingHost`) plus the fault,
//! chain-error, and level conversions. The code differed across modules
//! in zero places that were not bugs.
//!
//! The adapter is capability-selected: the `caps: [...]` form emits
//! only the pieces backed by the module's declared capabilities
Expand All @@ -32,10 +31,10 @@
//! // or, capability-selected:
//! // nexum_sdk::bind_host_via_wit_bindgen!(caps: [chain, logging]);
//!
//! // `WitBindgenHost`, `convert_fault`, and `sdk_fault_into_wit` are
//! // now in scope, plus per selected capability: `convert_chain_err`
//! // (chain), the `LocalStoreHost` impl (local_store), and
//! // `convert_level`, `HostLogSink`, and `install_tracing`
//! // `WitBindgenHost` and the `Fault` `From` impls (both directions)
//! // are now in scope, plus per selected capability: `convert_chain_err`
//! // (chain), the `LocalStoreHost` impl (local_store), and the
//! // `Level` `From` impl, `HostLogSink`, and `install_tracing`
//! // (logging), with the wit-bindgen and SDK types tied together
//! // through identifier resolution. Call `install_tracing()` once at
//! // the top of `Guest::init` to route `tracing::info!(...)` to the
Expand All @@ -45,12 +44,15 @@
//! ```

/// Generate `WitBindgenHost` + the `*Host` trait impls + the error /
/// level converters for the selected capabilities. See module docs.
/// level `From` impls for the selected capabilities. See module docs.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worth flagging before the code review: PR #358 (still open, unmerged) already closes the same issue (#264) with a very similar approach — converting the same shim functions in the same two files (crates/nexum-sdk/src/wit_bindgen_macro.rs, crates/shepherd-sdk/src/wit_bindgen_macro.rs) to From impls. Worth closing one of the two to avoid duplicate work and a merge conflict once either lands — probably whichever hasn't been rebased onto the current Videre train, since this one is stacked on feat/m3-intent-body-no-std and #358 predates the train entirely.

On the code itself: this is clean. Both From impls are a faithful 1:1 copy of the old functions' logic (including the #[non_exhaustive] wildcard arm and the five-tier Level if/else chain — no branch dropped), the orphan-rule reasoning holds since wit_bindgen::generate! makes the wit Fault type crate-local at each macro-expansion site, and all 6 changed module call sites resolve ?/.map_err(Into::into) to the same target type the old explicit calls produced. One tiny doc nit: docs/05-sdk-design.md's updated snippet relies on ?'s implicit From conversion without naming the From impl in scope, unlike tutorial-first-module.md's equivalent update which does call it out explicitly — a one-line note there would keep the two docs consistent.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks - two threads here.

Duplicate: closed #358, the develop-based PR that predates the train, as superseded by this one. #264 will close when this lands. Kept this PR since it is the version rebased onto the Videre train, as you suggested.

Doc nit: the two docs differ by audience on purpose. tutorial-first-module.md names the From impls because it teaches the reader the conversion; docs/05-sdk-design.md's snippet stays terser and lets ? speak for itself, in keeping with the terse-docs convention for the design docs. Leaving 05-sdk-design.md as-is, but happy to add the one-line note if you would rather they match verbatim.

///
/// The fault and level conversions are `From` impls: orphan-legal here
/// because the wit-bindgen types are local to the expanding cdylib.
///
/// Macro hygiene note: `macro_rules!` is not hygienic for type names
/// or function items, so the names `WitBindgenHost`, `convert_chain_err`,
/// `convert_fault`, `sdk_fault_into_wit`, `convert_level`, `HostLogSink`,
/// and `install_tracing` are intentionally visible in the caller's scope.
/// `HostLogSink`, and `install_tracing` are intentionally visible in the
/// caller's scope.
#[macro_export]
macro_rules! bind_host_via_wit_bindgen {
// Blanket-world form: every core interface is in scope, emit the
Expand All @@ -71,46 +73,51 @@ macro_rules! bind_host_via_wit_bindgen {
/// Lift the wit-bindgen `types.fault` (per-cdylib) into the
/// SDK's `Fault`. Exhaustive on the seven-case vocabulary; the
/// `rate-limited` backoff record maps field for field.
fn convert_fault(f: nexum::host::types::Fault) -> $crate::host::Fault {
match f {
nexum::host::types::Fault::Unsupported(s) => $crate::host::Fault::Unsupported(s),
nexum::host::types::Fault::Unavailable(s) => $crate::host::Fault::Unavailable(s),
nexum::host::types::Fault::Denied(s) => $crate::host::Fault::Denied(s),
nexum::host::types::Fault::RateLimited(rl) => {
$crate::host::Fault::RateLimited($crate::host::RateLimit {
retry_after_ms: rl.retry_after_ms,
})
impl ::core::convert::From<nexum::host::types::Fault> for $crate::host::Fault {
fn from(f: nexum::host::types::Fault) -> Self {
match f {
nexum::host::types::Fault::Unsupported(s) => Self::Unsupported(s),
nexum::host::types::Fault::Unavailable(s) => Self::Unavailable(s),
nexum::host::types::Fault::Denied(s) => Self::Denied(s),
nexum::host::types::Fault::RateLimited(rl) => {
Self::RateLimited($crate::host::RateLimit {
retry_after_ms: rl.retry_after_ms,
})
}
nexum::host::types::Fault::Timeout => Self::Timeout,
nexum::host::types::Fault::InvalidInput(s) => Self::InvalidInput(s),
nexum::host::types::Fault::Internal(s) => Self::Internal(s),
}
nexum::host::types::Fault::Timeout => $crate::host::Fault::Timeout,
nexum::host::types::Fault::InvalidInput(s) => $crate::host::Fault::InvalidInput(s),
nexum::host::types::Fault::Internal(s) => $crate::host::Fault::Internal(s),
}
}

/// Reverse direction: lower the SDK [`Fault`](
/// $crate::host::Fault) back into the per-cdylib wit-bindgen
/// `Fault` so `Guest::init` / `Guest::on_event` can return what
/// the export signature expects.
/// Reverse direction: lower the SDK `Fault` back into the
/// per-cdylib wit-bindgen `Fault` so `Guest::init` /
/// `Guest::on_event` can return what the export signature
/// expects (`?` applies it).
///
/// Carries a wildcard arm because `$crate::host::Fault` is
/// `#[non_exhaustive]`: a future SDK-side case must compile in
/// module crates without source changes. Falls back to
/// `internal` carrying the `Display` detail.
fn sdk_fault_into_wit(f: $crate::host::Fault) -> nexum::host::types::Fault {
use nexum::host::types::{Fault as WitFault, RateLimit as WitRateLimit};
match f {
$crate::host::Fault::Unsupported(s) => WitFault::Unsupported(s),
$crate::host::Fault::Unavailable(s) => WitFault::Unavailable(s),
$crate::host::Fault::Denied(s) => WitFault::Denied(s),
$crate::host::Fault::RateLimited(rl) => WitFault::RateLimited(WitRateLimit {
retry_after_ms: rl.retry_after_ms,
}),
$crate::host::Fault::Timeout => WitFault::Timeout,
$crate::host::Fault::InvalidInput(s) => WitFault::InvalidInput(s),
$crate::host::Fault::Internal(s) => WitFault::Internal(s),
// `$crate::host::Fault` is `#[non_exhaustive]`; a future
// SDK case lands here as `internal`.
other => WitFault::Internal(::std::string::ToString::to_string(&other)),
impl ::core::convert::From<$crate::host::Fault> for nexum::host::types::Fault {
fn from(f: $crate::host::Fault) -> Self {
match f {
$crate::host::Fault::Unsupported(s) => Self::Unsupported(s),
$crate::host::Fault::Unavailable(s) => Self::Unavailable(s),
$crate::host::Fault::Denied(s) => Self::Denied(s),
$crate::host::Fault::RateLimited(rl) => {
Self::RateLimited(nexum::host::types::RateLimit {
retry_after_ms: rl.retry_after_ms,
})
}
$crate::host::Fault::Timeout => Self::Timeout,
$crate::host::Fault::InvalidInput(s) => Self::InvalidInput(s),
$crate::host::Fault::Internal(s) => Self::Internal(s),
// `$crate::host::Fault` is `#[non_exhaustive]`; a
// future SDK case lands here as `internal`.
other => Self::Internal(::std::string::ToString::to_string(&other)),
}
}
}

Expand Down Expand Up @@ -163,7 +170,7 @@ macro_rules! __bind_host_cap_via_wit_bindgen {
fn convert_chain_err(e: nexum::host::chain::ChainError) -> $crate::host::ChainError {
match e {
nexum::host::chain::ChainError::Fault(f) => {
$crate::host::ChainError::Fault(convert_fault(f))
$crate::host::ChainError::Fault(::core::convert::Into::into(f))
}
nexum::host::chain::ChainError::Rpc(r) => {
$crate::host::ChainError::Rpc($crate::host::RpcError {
Expand All @@ -184,50 +191,51 @@ macro_rules! __bind_host_cap_via_wit_bindgen {
::core::option::Option<::std::vec::Vec<u8>>,
$crate::host::Fault,
> {
nexum::host::local_store::get(key).map_err(convert_fault)
nexum::host::local_store::get(key).map_err($crate::host::Fault::from)
}
fn set(
&self,
key: &str,
value: &[u8],
) -> ::core::result::Result<(), $crate::host::Fault> {
nexum::host::local_store::set(key, value).map_err(convert_fault)
nexum::host::local_store::set(key, value).map_err($crate::host::Fault::from)
}
fn delete(&self, key: &str) -> ::core::result::Result<(), $crate::host::Fault> {
nexum::host::local_store::delete(key).map_err(convert_fault)
nexum::host::local_store::delete(key).map_err($crate::host::Fault::from)
}
fn list_keys(
&self,
prefix: &str,
) -> ::core::result::Result<::std::vec::Vec<::std::string::String>, $crate::host::Fault>
{
nexum::host::local_store::list_keys(prefix).map_err(convert_fault)
nexum::host::local_store::list_keys(prefix).map_err($crate::host::Fault::from)
}
}
};
(logging) => {
impl $crate::host::LoggingHost for WitBindgenHost {
fn log(&self, level: $crate::Level, message: &str) {
nexum::host::logging::log(convert_level(level), message);
nexum::host::logging::log(nexum::host::logging::Level::from(level), message);
}
}

/// Translate a `tracing_core::Level` into the wit-bindgen
/// `logging::Level` wire enum. `Level` is a set of associated
/// consts, not a matchable enum, so compare rather than match;
/// the five tiers are total, so the final arm is `Trace`.
fn convert_level(level: $crate::Level) -> nexum::host::logging::Level {
use $crate::Level;
if level == Level::ERROR {
nexum::host::logging::Level::Error
} else if level == Level::WARN {
nexum::host::logging::Level::Warn
} else if level == Level::INFO {
nexum::host::logging::Level::Info
} else if level == Level::DEBUG {
nexum::host::logging::Level::Debug
} else {
nexum::host::logging::Level::Trace
impl ::core::convert::From<$crate::Level> for nexum::host::logging::Level {
fn from(level: $crate::Level) -> Self {
if level == $crate::Level::ERROR {
Self::Error
} else if level == $crate::Level::WARN {
Self::Warn
} else if level == $crate::Level::INFO {
Self::Info
} else if level == $crate::Level::DEBUG {
Self::Debug
} else {
Self::Trace
}
}
}

Expand Down
4 changes: 2 additions & 2 deletions crates/shepherd-sdk/src/wit_bindgen_macro.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
//!
//! Layers on `nexum_sdk::bind_host_via_wit_bindgen!`, which emits the
//! core adapter (`WitBindgenHost`, the `ChainHost` / `LocalStoreHost`
//! / `LoggingHost` impls, the error and level converters, and the
//! / `LoggingHost` impls, the fault and level `From` impls, and the
//! tracing wiring). This macro invokes it and adds the
//! [`CowApiHost`](crate::cow::CowApiHost) impl over the
//! `shepherd:cow/cow-api` import shims.
Expand Down Expand Up @@ -37,7 +37,7 @@ macro_rules! bind_cow_host_via_wit_bindgen {
fn convert_cow_err(e: shepherd::cow::cow_api::CowApiError) -> $crate::cow::CowApiError {
match e {
shepherd::cow::cow_api::CowApiError::Fault(f) => {
$crate::cow::CowApiError::Fault(convert_fault(f))
$crate::cow::CowApiError::Fault(::core::convert::Into::into(f))
}
shepherd::cow::cow_api::CowApiError::Http(h) => {
$crate::cow::CowApiError::Http($crate::cow::HttpFailure {
Expand Down
4 changes: 2 additions & 2 deletions docs/05-sdk-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,14 +176,14 @@ struct HttpProbe;
impl HttpProbe {
fn init(config: Vec<(String, String)>) -> Result<(), Fault> {
install_tracing();
let cfg = strategy::parse_config(&config).map_err(sdk_fault_into_wit)?;
let cfg = strategy::parse_config(&config)?;
// ...
Ok(())
}

fn on_block(block: types::Block) -> Result<(), Fault> {
strategy::on_block(&nexum_sdk::http::WasiFetch, /* ... */ block.number)
.map_err(sdk_fault_into_wit)
.map_err(Into::into)
}
}
```
Expand Down
10 changes: 5 additions & 5 deletions docs/tutorial-first-module.md
Original file line number Diff line number Diff line change
Expand Up @@ -304,8 +304,8 @@ use std::sync::OnceLock;

use nexum::host::types;

// `WitBindgenHost`, `convert_fault`, `sdk_fault_into_wit`, `convert_level`,
// `HostLogSink`, `install_tracing` are generated below. Single source
// `WitBindgenHost`, the fault and level `From` impls, `HostLogSink`,
// and `install_tracing` are generated below. Single source
// of truth in `nexum-sdk` + `shepherd-sdk`.
shepherd_sdk::bind_cow_host_via_wit_bindgen!();

Expand All @@ -316,7 +316,7 @@ struct StopLoss;
impl Guest for StopLoss {
fn init(config: Vec<(String, String)>) -> Result<(), Fault> {
install_tracing();
let cfg = strategy::parse_config(&config).map_err(sdk_fault_into_wit)?;
let cfg = strategy::parse_config(&config)?;
tracing::info!(
"stop-loss init: owner={:#x} trigger={} sell={:#x} buy={:#x}",
cfg.owner,
Expand All @@ -333,7 +333,7 @@ impl Guest for StopLoss {
return Ok(());
};
if let types::Event::Block(block) = event {
strategy::on_block(&WitBindgenHost, block.chain_id, cfg).map_err(sdk_fault_into_wit)?;
strategy::on_block(&WitBindgenHost, block.chain_id, cfg)?;
}
Ok(())
}
Expand All @@ -344,7 +344,7 @@ export!(StopLoss);

The macro generates `WitBindgenHost`, the `ChainHost` /
`LocalStoreHost` / `LoggingHost` / `CowApiHost` impls, the
`Fault` conversions (`convert_fault`, `sdk_fault_into_wit`), and
`Fault` `From` impls (both directions), and
`install_tracing`, which installs the guest `tracing` facade so
the `tracing::info!`, `warn!`, and `error!` macros reach the host
log call with no `Host` value to thread through. Call it once at
Expand Down
5 changes: 2 additions & 3 deletions modules/ethflow-watcher/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ wit_bindgen::generate!({

pub mod strategy;

// `WitBindgenHost`, `sdk_fault_into_wit`, `convert_level`
// `WitBindgenHost` and the fault and level `From` impls
// are generated below. Single source of truth in `nexum-sdk` + `shepherd-sdk`.
// Gated on `wasm32` so the strategy can be reused in native targets
// (e.g. the backtest replay harness in `crates/shepherd-backtest`).
Expand All @@ -72,8 +72,7 @@ impl Guest for EthFlowWatcher {
if let types::Event::ChainLogs(batch) = event {
let logs: Vec<nexum_sdk::events::Log> =
batch.logs.into_iter().map(Into::into).collect();
strategy::on_chain_logs(&WitBindgenHost, batch.chain_id, &logs)
.map_err(sdk_fault_into_wit)?;
strategy::on_chain_logs(&WitBindgenHost, batch.chain_id, &logs)?;
}
// Block / Tick / Message are not used by this module.
Ok(())
Expand Down
6 changes: 3 additions & 3 deletions modules/examples/balance-tracker/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ use nexum::host::types;

static SETTINGS: OnceLock<strategy::Settings> = OnceLock::new();

// `WitBindgenHost`, `sdk_fault_into_wit`, and `install_tracing` (used
// `WitBindgenHost`, the fault `From` impls, and `install_tracing` (used
// by the handlers) are generated by the attribute alongside the
// wit-bindgen call and the `Guest`/`export!` glue.
struct BalanceTracker;
Expand All @@ -44,7 +44,7 @@ struct BalanceTracker;
impl BalanceTracker {
fn init(config: Vec<(String, String)>) -> Result<(), Fault> {
install_tracing();
let cfg = strategy::parse_config(&config).map_err(sdk_fault_into_wit)?;
let cfg = strategy::parse_config(&config)?;
tracing::info!(
"balance-tracker init: {} addresses, threshold={} wei",
cfg.addresses.len(),
Expand All @@ -58,6 +58,6 @@ impl BalanceTracker {
let Some(cfg) = SETTINGS.get() else {
return Ok(());
};
strategy::on_block(&WitBindgenHost, block.chain_id, cfg).map_err(sdk_fault_into_wit)
strategy::on_block(&WitBindgenHost, block.chain_id, cfg).map_err(Into::into)
}
}
7 changes: 3 additions & 4 deletions modules/examples/http-probe/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ use nexum::host::types;

static SETTINGS: OnceLock<strategy::Settings> = OnceLock::new();

// `sdk_fault_into_wit` and `install_tracing` (used by the handlers) are
// The fault `From` impls and `install_tracing` (used by the handlers) are
// generated by the attribute alongside the wit-bindgen call and the
// `Guest`/`export!` glue.
struct HttpProbe;
Expand All @@ -52,7 +52,7 @@ struct HttpProbe;
impl HttpProbe {
fn init(config: Vec<(String, String)>) -> Result<(), Fault> {
install_tracing();
let cfg = strategy::parse_config(&config).map_err(sdk_fault_into_wit)?;
let cfg = strategy::parse_config(&config)?;
tracing::info!(
"http-probe init: probe_url={} denied_url={} every_n_blocks={}",
cfg.probe_url,
Expand All @@ -67,7 +67,6 @@ impl HttpProbe {
let Some(cfg) = SETTINGS.get() else {
return Ok(());
};
strategy::on_block(&nexum_sdk::http::WasiFetch, cfg, block.number)
.map_err(sdk_fault_into_wit)
strategy::on_block(&nexum_sdk::http::WasiFetch, cfg, block.number).map_err(Into::into)
}
}
7 changes: 3 additions & 4 deletions modules/examples/price-alert/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ use nexum::host::types;

static SETTINGS: OnceLock<strategy::Settings> = OnceLock::new();

// `WitBindgenHost`, `sdk_fault_into_wit`, and `install_tracing` (used
// `WitBindgenHost`, the fault `From` impls, and `install_tracing` (used
// by the handlers) are generated by the attribute alongside the
// wit-bindgen call and the `Guest`/`export!` glue.
struct PriceAlert;
Expand All @@ -59,7 +59,7 @@ struct PriceAlert;
impl PriceAlert {
fn init(config: Vec<(String, String)>) -> Result<(), Fault> {
install_tracing();
let cfg = strategy::parse_config(&config).map_err(sdk_fault_into_wit)?;
let cfg = strategy::parse_config(&config)?;
tracing::info!(
"price-alert init: oracle={:#x} threshold={} direction={:?} every_n_blocks={}",
cfg.oracle_address,
Expand All @@ -75,7 +75,6 @@ impl PriceAlert {
let Some(cfg) = SETTINGS.get() else {
return Ok(());
};
strategy::on_block(&WitBindgenHost, block.chain_id, cfg, block.number)
.map_err(sdk_fault_into_wit)
strategy::on_block(&WitBindgenHost, block.chain_id, cfg, block.number).map_err(Into::into)
}
}
Loading
Loading