use crate::ast::{Field, FnDecl, Item, Module, Stmt, UdFile, Value};
#[derive(Debug, thiserror::Error)]
pub enum WasmLowerError {
#[error("`@module.format` is not `\"wasm\"` (got {got:?})")]
NotWasm { got: String },
#[error("`@module.format` field is missing")]
UnknownFormat,
#[error("missing field `{field}` in `@module`")]
MissingField { field: String },
#[error("field `{field}` has wrong shape: expected {expected}, got something else")]
WrongShape { field: String, expected: String },
#[error("gap in coverage: cursor at 0x{cursor:x}, next block at 0x{addr:x}")]
Gap { cursor: u64, addr: u64 },
#[error(
"overlap: cursor was at 0x{cursor:x}, next block starts at 0x{addr:x} (still inside the previous block)"
)]
Overlap { cursor: u64, addr: u64 },
#[error("coverage mismatch: walked 0x{covered:x} bytes but file_size declares 0x{file_size:x}")]
SizeMismatch { covered: u64, file_size: u64 },
#[error("WASM lower only knows how to handle `@raw` and `fn` items; got {kind}")]
UnsupportedItem { kind: &'static str },
#[error("`fn {name}` is missing an `@addr(0x…)` directive")]
FnMissingAddr { name: String },
#[error("`fn {name}` body contains a statement that lower-to-wasm can't handle: {kind}")]
UnsupportedStmt { name: String, kind: &'static str },
}
pub fn lower_to_wasm(file: &UdFile) -> Result<Vec<u8>, WasmLowerError> {
let format = read_string(&file.module, "format").ok_or(WasmLowerError::UnknownFormat)?;
if format != "wasm" {
return Err(WasmLowerError::NotWasm { got: format });
}
let build = build_block(&file.module)?;
let file_size = read_int(build, "file_size")?;
let mut blocks: Vec<(u64, Vec<u8>)> = Vec::new();
for item in &file.items {
match item {
Item::Raw { addr, bytes } => blocks.push((*addr, bytes.clone())),
Item::Function(fd) => {
let addr = fd.addr.ok_or_else(|| WasmLowerError::FnMissingAddr {
name: fd.name.clone(),
})?;
let bytes = collect_fn_bytes(fd)?;
blocks.push((addr, bytes));
}
Item::Comment(_) => {}
Item::Section { .. } => {
return Err(WasmLowerError::UnsupportedItem { kind: "section" });
}
Item::Strings { .. } => {
return Err(WasmLowerError::UnsupportedItem { kind: "strings" });
}
Item::Notes { .. } => return Err(WasmLowerError::UnsupportedItem { kind: "notes" }),
Item::JumpTable { .. } => {
return Err(WasmLowerError::UnsupportedItem { kind: "jump_table" });
}
}
}
blocks.sort_by_key(|(addr, _)| *addr);
let mut out = Vec::with_capacity(file_size as usize);
let mut cursor: u64 = 0;
for (addr, bytes) in &blocks {
let len = bytes.len() as u64;
if *addr < cursor {
return Err(WasmLowerError::Overlap {
cursor,
addr: *addr,
});
}
if *addr > cursor {
return Err(WasmLowerError::Gap {
cursor,
addr: *addr,
});
}
out.extend_from_slice(bytes);
cursor = addr.saturating_add(len);
}
if cursor != file_size {
return Err(WasmLowerError::SizeMismatch {
covered: cursor,
file_size,
});
}
Ok(out)
}
fn collect_fn_bytes(fd: &FnDecl) -> Result<Vec<u8>, WasmLowerError> {
let mut out: Vec<u8> = Vec::new();
for stmt in &fd.body {
match stmt {
Stmt::Asm { bytes, .. } => out.extend_from_slice(bytes),
Stmt::Comment(_) | Stmt::Label { .. } => {}
_ => {
return Err(WasmLowerError::UnsupportedStmt {
name: fd.name.clone(),
kind: "non-asm statement",
});
}
}
}
Ok(out)
}
fn build_block(module: &Module) -> Result<&[Field], WasmLowerError> {
for f in &module.fields {
if f.name == "build" {
if let Value::Block(fields) = &f.value {
return Ok(fields);
}
return Err(WasmLowerError::WrongShape {
field: "build".into(),
expected: "block".into(),
});
}
}
Err(WasmLowerError::MissingField {
field: "build".into(),
})
}
fn read_int(fields: &[Field], name: &str) -> Result<u64, WasmLowerError> {
for f in fields {
if f.name == name {
if let Value::Int(n) = &f.value {
return Ok(*n);
}
return Err(WasmLowerError::WrongShape {
field: name.into(),
expected: "integer".into(),
});
}
}
Err(WasmLowerError::MissingField { field: name.into() })
}
fn read_string(module: &Module, name: &str) -> Option<String> {
module.fields.iter().find_map(|f| match &f.value {
Value::String(s) if f.name == name => Some(s.clone()),
_ => None,
})
}