pub const ASYNC_MODULE: &str = "pulseengine:async";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AsyncFamily {
ErrorContext,
Stream,
Future,
WaitableTask,
}
pub const LOWERED_FIELDS: &[&str] = &["error-context.drop"];
pub fn is_lowered_field(field: &str) -> bool {
LOWERED_FIELDS.contains(&field)
}
impl AsyncFamily {
pub fn from_field(field: &str) -> Option<Self> {
let resource = field.split('.').next().unwrap_or(field);
match resource {
"error-context" => Some(AsyncFamily::ErrorContext),
"stream" => Some(AsyncFamily::Stream),
"future" => Some(AsyncFamily::Future),
"waitable-set" | "waitable" | "task" => Some(AsyncFamily::WaitableTask),
_ => None,
}
}
pub const fn tag(self) -> &'static str {
match self {
AsyncFamily::ErrorContext => "error-context",
AsyncFamily::Stream => "stream",
AsyncFamily::Future => "future",
AsyncFamily::WaitableTask => "waitable-task",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AsyncDecline {
pub field: String,
pub family: Option<AsyncFamily>,
pub reason: String,
}
impl core::fmt::Display for AsyncDecline {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "#80 async-intrinsic decline: {}", self.reason)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AsyncClassification {
NotAsync,
Lowered {
family: AsyncFamily,
field: String,
},
Declined(AsyncDecline),
}
pub fn classify(module: &str, field: &str) -> AsyncClassification {
if module != ASYNC_MODULE {
return AsyncClassification::NotAsync;
}
if is_lowered_field(field) {
let family = AsyncFamily::from_field(field)
.expect("a LOWERED_FIELDS entry must classify into a family");
return AsyncClassification::Lowered {
family,
field: field.to_string(),
};
}
match AsyncFamily::from_field(field) {
Some(fam) => AsyncClassification::Declined(AsyncDecline {
field: field.to_string(),
family: Some(fam),
reason: decline_reason(fam, field),
}),
None => AsyncClassification::Declined(AsyncDecline {
field: field.to_string(),
family: None,
reason: format!(
"unknown P3-async intrinsic '{ASYNC_MODULE}::{field}' — synth \
will not emit a call against an unspecified async contract \
(RFC #46); recognized families: error-context (lowered), \
stream / future / waitable-set / task (declined)"
),
}),
}
}
fn decline_reason(family: AsyncFamily, field: &str) -> String {
let ns = ASYNC_MODULE;
match family {
AsyncFamily::ErrorContext => format!(
"'{ns}::{field}' (error-context family) not compiled: only \
error-context.drop (a scalar handle op) is lowered. This op \
transfers a message string through linear memory (canonical ABI), \
which needs the same bounds-checked linmem-base-relative buffer \
lowering as the stream family that synth does not yet generate \
(#80). Use error-context.drop, or link this op against a host that \
owns the buffer protocol."
),
AsyncFamily::Stream => format!(
"'{ns}::{field}' (stream family) not compiled: synth does not yet \
generate the bounds-checked linear-memory buffer layout the stream \
read/write intrinsics require (#80 §3). Lower only error-context.drop \
for now, or link the stream intrinsic against a host that owns the \
buffer protocol."
),
AsyncFamily::Future => format!(
"'{ns}::{field}' (future family) not compiled: the readable/writable-\
end buffer transfer protocol is unimplemented (#80 §3). Only \
error-context.drop is lowered."
),
AsyncFamily::WaitableTask => format!(
"'{ns}::{field}' (waitable/task family) not compiled: synth does not \
yet save/restore register state across the scheduler yield at \
waitable-set.wait (#80 §4). Only error-context.drop is lowered."
),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn error_context_drop_is_lowered() {
assert_eq!(
classify(ASYNC_MODULE, "error-context.drop"),
AsyncClassification::Lowered {
family: AsyncFamily::ErrorContext,
field: "error-context.drop".to_string(),
}
);
assert!(is_lowered_field("error-context.drop"));
}
#[test]
fn error_context_buffer_ops_are_declined() {
for field in ["error-context.new", "error-context.debug-message"] {
match classify(ASYNC_MODULE, field) {
AsyncClassification::Declined(d) => {
assert_eq!(d.family, Some(AsyncFamily::ErrorContext));
assert!(d.reason.contains("buffer"), "{field}: {}", d.reason);
assert!(d.reason.contains("linear memory"), "{field}: {}", d.reason);
}
other => panic!("{field} must be declined (linmem buffer), got {other:?}"),
}
assert!(!is_lowered_field(field));
}
}
#[test]
fn stream_is_declined_by_name() {
let c = classify(ASYNC_MODULE, "stream.read");
match c {
AsyncClassification::Declined(d) => {
assert_eq!(d.family, Some(AsyncFamily::Stream));
assert!(d.reason.contains("stream"));
assert!(d.reason.contains("buffer"));
assert!(d.to_string().contains("stream.read"));
}
other => panic!("stream.read should be declined, got {other:?}"),
}
}
#[test]
fn future_and_waitable_are_declined() {
assert!(matches!(
classify(ASYNC_MODULE, "future.read"),
AsyncClassification::Declined(d) if d.family == Some(AsyncFamily::Future)
));
assert!(matches!(
classify(ASYNC_MODULE, "waitable-set.wait"),
AsyncClassification::Declined(d) if d.family == Some(AsyncFamily::WaitableTask)
));
assert!(matches!(
classify(ASYNC_MODULE, "task.return"),
AsyncClassification::Declined(d) if d.family == Some(AsyncFamily::WaitableTask)
));
}
#[test]
fn unknown_async_intrinsic_is_declined() {
match classify(ASYNC_MODULE, "quantum.entangle") {
AsyncClassification::Declined(d) => {
assert_eq!(d.family, None);
assert!(d.reason.contains("unknown"));
}
other => panic!("unknown intrinsic should decline, got {other:?}"),
}
}
#[test]
fn non_async_import_is_untouched() {
assert_eq!(classify("env", "print_i32"), AsyncClassification::NotAsync);
assert_eq!(
classify("wasi:cli/stdout", "write"),
AsyncClassification::NotAsync
);
assert_eq!(
classify("env", "stream.read"),
AsyncClassification::NotAsync
);
}
#[test]
fn lowered_field_set_is_exact() {
assert_eq!(LOWERED_FIELDS, &["error-context.drop"]);
assert!(is_lowered_field("error-context.drop"));
for f in [
"error-context.new",
"error-context.debug-message",
"stream.read",
"future.read",
"waitable-set.wait",
"task.return",
] {
assert!(!is_lowered_field(f), "{f} must not be lowered");
}
}
}