use crate::ast::TopLevel;
pub struct UnsupportedEffect {
pub effect: String,
pub fn_name: String,
pub line: usize,
pub reason: UnsupportedReason,
}
#[derive(Debug)]
pub enum UnsupportedReason {
Permanent { hint: &'static str },
OutOfRelease { phase: &'static str },
PendingPhase { phase: &'static str },
}
pub fn check_supported_effects(items: &[TopLevel]) -> Result<(), Vec<UnsupportedEffect>> {
let mut errors = Vec::new();
for item in items {
if let TopLevel::FnDef(fd) = item {
for effect in &fd.effects {
if let Some(reason) = classify(&effect.node) {
errors.push(UnsupportedEffect {
effect: effect.node.clone(),
fn_name: fd.name.clone(),
line: effect.line,
reason,
});
}
}
}
}
if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
}
pub fn render_errors(errors: &[UnsupportedEffect]) -> String {
let mut out = String::new();
for (i, e) in errors.iter().enumerate() {
if i > 0 {
out.push('\n');
}
match &e.reason {
UnsupportedReason::Permanent { hint } => {
out.push_str(&format!(
" ! [{}] in fn `{}` (line {}): {hint}.\n \
This effect cannot ever be supported on `--target wasip2`. \
Use `--target wasm-gc` or `aver run` (VM) for programs that \
need this capability.",
e.effect, e.fn_name, e.line
));
}
UnsupportedReason::OutOfRelease { phase } => {
out.push_str(&format!(
" ! [{}] in fn `{}` (line {}): out of 0.18 scope ({phase}).\n \
Direct WIT lowering for this effect is planned but not in \
this release. Use `--target wasm-gc` for the time being, \
or wait for {phase}.",
e.effect, e.fn_name, e.line
));
}
UnsupportedReason::PendingPhase { phase } => {
out.push_str(&format!(
" ! [{}] in fn `{}` (line {}): not wired yet — pending {phase}.\n \
The wasip2 effect map for this call lands in {phase}; until \
then, only zero-effect programs compile on `--target wasip2`. \
Use `--target wasm-gc` or `aver run` (VM) in the meantime.",
e.effect, e.fn_name, e.line
));
}
}
}
out
}
fn classify(effect: &str) -> Option<UnsupportedReason> {
if effect.starts_with("Terminal.") {
return Some(UnsupportedReason::Permanent {
hint: "WASI 0.2 has no raw/cooked-mode terminal operations \
(set-raw-mode, set-echo, get-window-size); the capability \
is structurally absent",
});
}
if effect == "Env.set" {
return Some(UnsupportedReason::Permanent {
hint: "WASI 0.2 environment is read-only by design; no host \
implementation can ever satisfy a write",
});
}
if effect.starts_with("Http.") {
return Some(UnsupportedReason::OutOfRelease {
phase: "Phase 2 / 0.19",
});
}
if effect.starts_with("Tcp.") {
return Some(UnsupportedReason::OutOfRelease {
phase: "Phase 2 / 0.19",
});
}
if effect.starts_with("HttpServer.") {
return Some(UnsupportedReason::OutOfRelease {
phase: "Phase 3 / 0.19+",
});
}
if matches!(
effect,
"Disk.exists"
| "Disk.readText"
| "Disk.writeText"
| "Disk.appendText"
| "Disk.delete"
| "Disk.deleteDir"
| "Disk.makeDir"
| "Disk.listDir"
) {
return None;
}
if effect.starts_with("Disk.") {
return Some(UnsupportedReason::PendingPhase { phase: "Phase 1.5" });
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn classifies_permanent_rejects() {
assert!(matches!(
classify("Terminal.readKey"),
Some(UnsupportedReason::Permanent { .. })
));
assert!(matches!(
classify("Terminal.setColor"),
Some(UnsupportedReason::Permanent { .. })
));
assert!(matches!(
classify("Env.set"),
Some(UnsupportedReason::Permanent { .. })
));
assert!(classify("Time.sleep").is_none());
}
#[test]
fn classifies_out_of_release_rejects() {
assert!(matches!(
classify("Http.get"),
Some(UnsupportedReason::OutOfRelease { .. })
));
assert!(matches!(
classify("Tcp.connect"),
Some(UnsupportedReason::OutOfRelease { .. })
));
assert!(matches!(
classify("HttpServer.listen"),
Some(UnsupportedReason::OutOfRelease { .. })
));
}
#[test]
fn classifies_pending_phase_rejects() {
assert!(classify("Console.print").is_none());
assert!(classify("Console.error").is_none());
assert!(classify("Console.warn").is_none());
assert!(classify("Time.unixMs").is_none());
assert!(classify("Time.now").is_none());
assert!(classify("Time.sleep").is_none());
assert!(classify("Random.int").is_none());
assert!(classify("Random.float").is_none());
assert!(classify("Args.get").is_none());
assert!(classify("Env.get").is_none());
assert!(classify("Console.readLine").is_none());
assert!(classify("Disk.exists").is_none());
assert!(classify("Disk.readText").is_none());
assert!(classify("Disk.writeText").is_none());
assert!(classify("Disk.appendText").is_none());
assert!(classify("Disk.delete").is_none());
assert!(classify("Disk.deleteDir").is_none());
assert!(classify("Disk.makeDir").is_none());
assert!(classify("Disk.listDir").is_none());
}
#[test]
fn unknown_effects_are_passthrough() {
assert!(classify("MyDomain.doThing").is_none());
assert!(classify("Custom.action").is_none());
}
}