Wado

WEP: WebIDL Binding Generator (wado-from-idl)

Context

Wado targets WebAssembly and WASI, but the web browser remains the most important deployment platform for Wasm. Browser APIs (DOM, Fetch, Canvas, WebSocket, etc.) are specified in WebIDL — a language-neutral interface description format maintained by W3C. To use these APIs from Wado, we need a tool that generates type-safe Wado bindings from WebIDL definitions.

Prior Art: wasm-bindgen / web-sys

Rust's wasm-bindgen ecosystem has proven that WebIDL-to-language binding generation works. However, it has well-documented ergonomic problems:

Type casting boilerplate — JavaScript's prototype inheritance maps to Rust's Deref chain, requiring explicit dyn_into() / dyn_ref() casts:

// web-sys: get an input element and read its value
let el = document.get_element_by_id("search").unwrap();
let input: HtmlInputElement = el.dyn_into().unwrap();  // can panic
let value = input.value();

Untyped promises — All promises return JsValue, losing type information:

// web-sys: fetch returns untyped JsValue
let resp_value = JsFuture::from(window.fetch_with_str(&url)).await?;
let resp: Response = resp_value.dyn_into()?;  // manual cast
let text = JsFuture::from(resp.text()?).await?;
let s: String = text.as_string().unwrap();    // another manual cast

Ugly overload naming — WebIDL method overloads produce suffixed names:

// web-sys: fetch has 4 generated variants
window.fetch_with_str(&url);
window.fetch_with_str_and_init(&url, &opts);
window.fetch_with_request(&req);
window.fetch_with_request_and_init(&req, &opts);

Closure boilerplate — Event handlers require Closure::wrap + .forget():

// web-sys: adding an event listener
let closure = Closure::wrap(Box::new(move |event: Event| {
    // ...
}) as Box<dyn FnMut(Event)>);
element.add_event_listener_with_callback("click", closure.as_ref().unchecked_ref())?;
closure.forget();  // leak memory to prevent drop

Wado's Advantages

Wado eliminates several root causes of these pain points:

Decision

Source Data

wado-from-idl uses the @webref/idl npm package as its source of truth. This package provides curated, validated WebIDL definitions scraped from W3C and WHATWG specifications, updated weekly. All definitions are guaranteed to parse and be internally consistent.

npm install @webref/idl

The package's parseAll() API (powered by webidl2) produces parsed AST nodes with structured default fields for optional parameters and dictionary members. Default value types: boolean, number, string, null, dictionary (= {}), sequence (= []). When no default is specified, the default field is null.

Tool Implementation

wado-from-idl (renamed from wado-from-wit) already provides a type-safe IR (WadoModule, WadoResource, WadoStruct, WadoEffect, etc.) and a WadoCodeGenerator that emits clean Wado source. The concepts map directly: WIT interfaces and WebIDL interfaces both produce resources, structs, enums, and effects.

The crate gains a WebIDL parser alongside the WIT parser, sharing the IR and codegen layers:

wado-from-idl --webidl-dir ./webidl-json/ --output-dir ./web/

Pipeline:

@webref/idl (JSON) → Parse → IR (WadoModule: resources, types, effects) → WadoCodeGenerator → web/*.wado

The WIT path remains unchanged (--wit-dir). Both frontends produce the same IR, so codegen improvements benefit both WASI and Web API bindings.

Unified Attribute: #[cm("...")]

The #[cm("...")] attribute provides Component Model ABI mappings for all namespaces, with the namespace encoded in the value:

// Web API binding
#[cm("web:dom#Element")]
pub resource Element extends Node { ... }

#[cm("web:dom#Element.getAttribute")]
fn get_attribute(&self, name: String) -> Option<String>;

// WASI binding
#[cm("wasi:http/types@0.3.0#request")]
pub resource Request { ... }

Sub-attributes for method kinds:

#[cm("web:dom#Element.id", getter)]     // property getter
#[cm("web:dom#Element.id", setter)]     // property setter
#[cm("web:dom#[constructor]Document")]   // constructor
#[cm("web:url#[static]URL.createObjectURL")]  // static method

Parameter name mapping:

#[cm_params("self", "qualified-name")]
fn get_attribute(&self, qualified_name: String) -> Option<String>;

Language Extension: resource extends

WebIDL interfaces form single-inheritance hierarchies (e.g., EventTarget → Node → Element → HTMLElement → HTMLInputElement). Wasm-bindgen handles this with #[wasm_bindgen(extends = Node, extends = EventTarget)] attributes that generate Deref chains — a well-known Rust anti-pattern.

wado-from-idl introduces resource extends as a first-class Wado language feature:

#[cm("web:dom#EventTarget")]
pub resource EventTarget { ... }

#[cm("web:dom#Node")]
pub resource Node extends EventTarget { ... }

#[cm("web:dom#Element")]
pub resource Element extends Node { ... }

#[cm("web:dom#HTMLElement")]
pub resource HtmlElement extends Element { ... }

#[cm("web:dom#HTMLInputElement")]
pub resource HtmlInputElement extends HtmlElement { ... }

Semantics:

This applies only to resource (opaque handle types), not to struct (value types). Value types remain non-inheritable — inheritance is about handle polymorphism, not data layout.

WebIDL → Wado Type Mapping

Primitive Types

WebIDL Wado
boolean bool
byte / octet i8 / u8
short / unsigned short i16 / u16
long / unsigned long i32 / u32
long long / unsigned long long i64 / u64
float / unrestricted float f32
double / unrestricted double f64
DOMString / USVString / ByteString String
undefined ()

Compound Types

WebIDL Wado Example
T? (nullable) Option<T> Element?Option<Element>
sequence<T> List<T> sequence<Node>List<Node>
FrozenArray<T> List<T> Immutable in JS, same Wado type
Promise<T> Future<T> Promise<Response>Future<Response>
record<K, V> TreeMap<K, V>
(A or B) (union) variant See union section
any JsValue Opaque escape hatch

Structural Types

WebIDL Wado Pattern
interface resource Opaque handle with methods
interface X : Y resource X extends Y Single inheritance
interface mixin trait Shared behavior across resources
dictionary struct Data with field defaults
enum enum String enum → Wado enum
callback fn(...) Closure type
typedef type Newtype alias

Effect Design

Web API calls are side effects. wado-from-idl groups them into per-specification effects, following the same pattern as WASI effects (Stdout, FileSystem, etc.):

// web:dom — DOM access
pub interface Dom {
    fn window() -> Window;
    fn document() -> Document;
}

// web:fetch — network requests
pub interface Fetch {
    fn fetch(input: String, init: Option<RequestInit>) -> Future<Response>;
}

// web:canvas — canvas rendering
pub interface Canvas2D { ... }

// web:websocket — WebSocket connections
pub interface WebSocket { ... }

// web:storage — localStorage / sessionStorage
pub interface Storage { ... }

User code declares which effects it needs:

fn app() with Dom, Fetch {
    let doc = Dom::document();
    let data = Fetch::fetch("/api/data", null);
}

Method Overload Strategy

WebIDL allows method overloading (same name, different signatures) and optional parameters with default values. wado-from-idl resolves these using default arguments:

// WebIDL: optional parameter with default
Node cloneNode(optional boolean deep = false);
// WebIDL: optional parameter without default
undefined fillText(DOMString text, double x, double y, optional double maxWidth);
// WebIDL: nullable callback with optional third argument
undefined addEventListener(DOMString type, EventListener? callback,
                           optional (AddEventListenerOptions or boolean) options);
// Generated Wado: optional params with known defaults use the actual type
fn clone_node(&self, deep: bool = false) -> Node;
// Optional params without a WebIDL default use Option<T> with = null
fn fill_text(&self, text: String, x: f64, y: f64, max_width: Option<f64> = null);
// Nullable params remain Option<T>; optional params get defaults
fn add_event_listener(&self, type: String, callback: Option<fn(Event)>,
                      options: Option<AddEventListenerOptions> = null);

Default arguments are desugared at the call site with zero overhead — see WEP: Default Arguments.

For true type overloads (different types at the same position), wado-from-idl generates a variant:

variant CanvasImageSourceOrPath {
    Source(CanvasImageSource),
    Path(Path2d),
}

Concrete Examples

EventTarget → Node → Element (Inheritance Chain)

WebIDL:

interface EventTarget {
  constructor();
  undefined addEventListener(DOMString type, EventListener? callback,
                             optional (AddEventListenerOptions or boolean) options);
  undefined removeEventListener(DOMString type, EventListener? callback,
                                optional (EventListenerOptions or boolean) options);
  boolean dispatchEvent(Event event);
};

interface Node : EventTarget {
  readonly attribute unsigned short nodeType;
  readonly attribute DOMString nodeName;
  readonly attribute Node? parentNode;
  readonly attribute NodeList childNodes;
  attribute DOMString? textContent;
  Node appendChild(Node node);
  Node removeChild(Node child);
  Node cloneNode(optional boolean deep = false);
};

interface Element : Node {
  readonly attribute DOMString tagName;
  attribute DOMString id;
  attribute DOMString className;
  DOMString? getAttribute(DOMString qualifiedName);
  undefined setAttribute(DOMString qualifiedName, DOMString value);
  Element? querySelector(DOMString selectors);
  NodeList querySelectorAll(DOMString selectors);
};

Generated Wado:

#![generated]

pub struct EventListenerOptions {
    pub capture: bool = false,
}

pub struct AddEventListenerOptions {
    pub capture: bool = false,
    pub passive: Option<bool> = null,
    pub once: bool = false,
    pub signal: Option<AbortSignal> = null,
}

#[cm("web:dom#EventTarget")]
pub resource EventTarget {
    #[cm("web:dom#[constructor]EventTarget")]
    fn new() -> EventTarget;

    #[cm("web:dom#EventTarget.addEventListener")]
    fn add_event_listener(&self, type: String, callback: Option<fn(Event)>,
                          options: AddEventListenerOptions = AddEventListenerOptions {});

    #[cm("web:dom#EventTarget.removeEventListener")]
    fn remove_event_listener(&self, type: String, callback: Option<fn(Event)>,
                             options: EventListenerOptions = EventListenerOptions {});

    #[cm("web:dom#EventTarget.dispatchEvent")]
    fn dispatch_event(&self, event: Event) -> bool;
}

#[cm("web:dom#Node")]
pub resource Node extends EventTarget {
    #[cm("web:dom#Node.nodeType", getter)]
    fn node_type(&self) -> u16;

    #[cm("web:dom#Node.nodeName", getter)]
    fn node_name(&self) -> String;

    #[cm("web:dom#Node.parentNode", getter)]
    fn parent_node(&self) -> Option<Node>;

    #[cm("web:dom#Node.childNodes", getter)]
    fn child_nodes(&self) -> NodeList;

    #[cm("web:dom#Node.textContent", getter)]
    fn text_content(&self) -> Option<String>;

    #[cm("web:dom#Node.textContent", setter)]
    fn set_text_content(&self, value: Option<String>);

    #[cm("web:dom#Node.appendChild")]
    fn append_child(&self, node: &Node) -> Node;

    #[cm("web:dom#Node.removeChild")]
    fn remove_child(&self, child: &Node) -> Node;

    #[cm("web:dom#Node.cloneNode")]
    fn clone_node(&self, deep: bool = false) -> Node;
}

#[cm("web:dom#Element")]
pub resource Element extends Node {
    #[cm("web:dom#Element.tagName", getter)]
    fn tag_name(&self) -> String;

    #[cm("web:dom#Element.id", getter)]
    fn id(&self) -> String;

    #[cm("web:dom#Element.id", setter)]
    fn set_id(&self, value: String);

    #[cm("web:dom#Element.className", getter)]
    fn class_name(&self) -> String;

    #[cm("web:dom#Element.className", setter)]
    fn set_class_name(&self, value: String);

    #[cm("web:dom#Element.getAttribute")]
    fn get_attribute(&self, qualified_name: String) -> Option<String>;

    #[cm("web:dom#Element.setAttribute")]
    fn set_attribute(&self, qualified_name: String, value: String);

    #[cm("web:dom#Element.querySelector")]
    fn query_selector(&self, selectors: String) -> Option<Element>;

    #[cm("web:dom#Element.querySelectorAll")]
    fn query_selector_all(&self, selectors: String) -> NodeList;
}

Key design points:

Document and the Dom Effect

#[cm("web:dom")]
pub interface Dom {
    fn window() -> Window;
    fn document() -> Document;
}

#[cm("web:dom#Document")]
pub resource Document extends Node {
    #[cm("web:dom#[constructor]Document")]
    fn new() -> Document;

    #[cm("web:dom#Document.getElementById")]
    fn get_element_by_id(&self, element_id: String) -> Option<Element>;

    #[cm("web:dom#Document.createElement")]
    fn create_element(&self, local_name: String) -> Element;

    #[cm("web:dom#Document.createTextNode")]
    fn create_text_node(&self, data: String) -> Text;

    #[cm("web:dom#Document.querySelector")]
    fn query_selector(&self, selectors: String) -> Option<Element>;

    #[cm("web:dom#Document.querySelectorAll")]
    fn query_selector_all(&self, selectors: String) -> NodeList;

    #[cm("web:dom#Document.body", getter)]
    fn body(&self) -> Option<HtmlElement>;

    #[cm("web:dom#Document.head", getter)]
    fn head(&self) -> Option<HtmlHeadElement>;
}

User code:

use { Dom, Document, Element } from "web:dom";

fn setup_page() with Dom {
    let doc = Dom::document();
    let el = doc.create_element("div");
    el.set_id("app");
    el.set_text_content(Option::Some("Hello, Wado!"));
    if let Some(body) = doc.body() {
        body.append_child(&el);  // append_child from Node, available via extends
    }
}

Compared to web-sys:

// web-sys: 6 unwrap/expect calls, 2 type casts
let window = web_sys::window().expect("no window");
let document = window.document().expect("no document");
let body = document.body().expect("no body");
let el = document.create_element("div").expect("create_element failed");
el.set_id("app");
el.set_text_content(Some("Hello, Wado!"));
body.append_child(&el).expect("append_child failed");

HTMLInputElement (Deep Inheritance)

Five-level chain: EventTarget → Node → Element → HTMLElement → HTMLInputElement.

#[cm("web:html#HTMLElement")]
pub resource HtmlElement extends Element {
    #[cm("web:html#HTMLElement.title", getter)]
    fn title(&self) -> String;

    #[cm("web:html#HTMLElement.title", setter)]
    fn set_title(&self, value: String);

    #[cm("web:html#HTMLElement.hidden", getter)]
    fn hidden(&self) -> Option<String>;

    #[cm("web:html#HTMLElement.hidden", setter)]
    fn set_hidden(&self, value: Option<String>);

    #[cm("web:html#HTMLElement.click")]
    fn click(&self);

    #[cm("web:html#HTMLElement.innerText", getter)]
    fn inner_text(&self) -> String;
}

#[cm("web:html#HTMLInputElement")]
pub resource HtmlInputElement extends HtmlElement {
    #[cm("web:html#HTMLInputElement.value", getter)]
    fn value(&self) -> String;

    #[cm("web:html#HTMLInputElement.value", setter)]
    fn set_value(&self, value: String);

    #[cm("web:html#HTMLInputElement.type", getter)]
    fn type_(&self) -> String;

    #[cm("web:html#HTMLInputElement.type", setter)]
    fn set_type(&self, value: String);

    #[cm("web:html#HTMLInputElement.checked", getter)]
    fn checked(&self) -> bool;

    #[cm("web:html#HTMLInputElement.checked", setter)]
    fn set_checked(&self, value: bool);

    #[cm("web:html#HTMLInputElement.disabled", getter)]
    fn disabled(&self) -> bool;

    #[cm("web:html#HTMLInputElement.disabled", setter)]
    fn set_disabled(&self, value: bool);

    #[cm("web:html#HTMLInputElement.focus")]
    fn focus(&self);
}

User code — thanks to resource extends, all ancestor methods are directly accessible:

use { Dom, Document, HtmlInputElement } from "web:dom";

fn setup_form() with Dom {
    let doc = Dom::document();
    if let Some(el) = doc.query_selector("#username") {
        if let Some(input) = el.downcast::<HtmlInputElement>() {
            // HtmlInputElement's own method
            let val = input.value();

            // Element's method (2 levels up) — no cast needed
            input.set_attribute("placeholder", "Enter username");

            // Node's method (3 levels up) — no cast needed
            let parent = input.parent_node();

            // EventTarget's method (4 levels up) — no cast needed
            input.add_event_listener("input", Option::Some(|e: Event| {
                // Closures just work — no Closure::wrap, no .forget()
                log_stdout(`input changed`);
            }));  // options defaults to null
        }
    }
}

Compared to web-sys:

// web-sys: requires explicit dyn_into at every level crossing
let el = document.query_selector("#username")?.unwrap();
let input: HtmlInputElement = el.dyn_into()?;  // explicit cast
input.set_attribute("placeholder", "Enter username")?;
let parent = input.parent_node();  // works via Deref to Node
// Event listener requires Closure::wrap boilerplate...

Events (Typed Event Handling)

#[cm("web:dom#Event")]
pub resource Event {
    #[cm("web:dom#Event.type", getter)]
    fn type_(&self) -> String;

    #[cm("web:dom#Event.target", getter)]
    fn target(&self) -> Option<EventTarget>;

    #[cm("web:dom#Event.preventDefault")]
    fn prevent_default(&self);

    #[cm("web:dom#Event.stopPropagation")]
    fn stop_propagation(&self);
}

#[cm("web:uievents#MouseEvent")]
pub resource MouseEvent extends Event {
    #[cm("web:uievents#MouseEvent.clientX", getter)]
    fn client_x(&self) -> i32;

    #[cm("web:uievents#MouseEvent.clientY", getter)]
    fn client_y(&self) -> i32;

    #[cm("web:uievents#MouseEvent.button", getter)]
    fn button(&self) -> i16;
}

#[cm("web:uievents#KeyboardEvent")]
pub resource KeyboardEvent extends Event {
    #[cm("web:uievents#KeyboardEvent.key", getter)]
    fn key(&self) -> String;

    #[cm("web:uievents#KeyboardEvent.code", getter)]
    fn code(&self) -> String;

    #[cm("web:uievents#KeyboardEvent.ctrlKey", getter)]
    fn ctrl_key(&self) -> bool;

    #[cm("web:uievents#KeyboardEvent.shiftKey", getter)]
    fn shift_key(&self) -> bool;
}

User code — typed event handler with downcast:

use { Dom, KeyboardEvent } from "web:dom";

fn setup_search() with Dom {
    let doc = Dom::document();
    if let Some(input) = doc.query_selector("#search") {
        input.add_event_listener("keydown", Option::Some(|e: Event| {
            if let Some(ke) = e.downcast::<KeyboardEvent>() {
                if ke.key() == "Enter" {
                    e.prevent_default();
                    // ... perform search
                }
            }
        }));  // options defaults to null
    }
}

Fetch (Typed Async)

WebIDL:

dictionary RequestInit {
  ByteString method;
  HeadersInit headers;
  BodyInit? body;
  RequestMode mode;
  RequestCredentials credentials;
};

interface Response {
  readonly attribute boolean ok;
  readonly attribute unsigned short status;
  readonly attribute ByteString statusText;
  readonly attribute Headers headers;
  Promise<USVString> text();
  Promise<any> json();
  Response clone();
};

Generated Wado:

pub struct RequestInit {
    pub method: Option<String> = null,
    pub headers: Option<Headers> = null,
    pub body: Option<String> = null,
    pub mode: Option<RequestMode> = null,
    pub credentials: Option<RequestCredentials> = null,
}

pub enum RequestMode {
    Cors,
    NoCors,
    SameOrigin,
    Navigate,
}

pub enum RequestCredentials {
    Omit,
    SameOrigin,
    Include,
}

#[cm("web:fetch#Response")]
pub resource Response {
    #[cm("web:fetch#Response.ok", getter)]
    fn ok(&self) -> bool;

    #[cm("web:fetch#Response.status", getter)]
    fn status(&self) -> u16;

    #[cm("web:fetch#Response.statusText", getter)]
    fn status_text(&self) -> String;

    #[cm("web:fetch#Response.headers", getter)]
    fn headers(&self) -> Headers;

    #[cm("web:fetch#Response.text")]
    fn text(&self) -> Future<String>;

    #[cm("web:fetch#Response.json")]
    fn json(&self) -> Future<JsValue>;

    #[cm("web:fetch#Response.clone")]
    fn clone(&self) -> Response;
}

#[cm("web:fetch")]
pub interface Fetch {
    #[cm("web:fetch#fetch")]
    fn fetch(input: String, init: RequestInit = RequestInit {}) -> Future<Response>;
}

User code:

use { Fetch, Response, RequestInit, RequestMode } from "web:fetch";

fn load_data(url: String) with Fetch {
    let resp = Fetch::fetch(url).read();  // init defaults to RequestInit {}
    if let Some(r) = resp {
        if r.ok() {
            let body = r.text().read();
            if let Some(text) = body {
                log_stdout(`Got: {text}`);
            }
        }
    }
}

fn post_json(url: String, body: String) with Fetch {
    let init: RequestInit = {
        method: Option::Some("POST"),
        body: Option::Some(body),
        mode: Option::Some(RequestMode::Cors),
    };
    let resp = Fetch::fetch(url, init).read();
}

Compared to web-sys:

// web-sys: 12+ lines, untyped JsValue everywhere
let mut opts = RequestInit::new();
opts.method("POST");
opts.body(Some(&JsValue::from_str(&body)));
let request = Request::new_with_str_and_init(&url, &opts)?;
let window = web_sys::window().unwrap();
let resp_value = JsFuture::from(window.fetch_with_request(&request)).await?;
let resp: Response = resp_value.dyn_into()?;
let text_value = JsFuture::from(resp.text()?).await?;
let text: String = text_value.as_string().unwrap();

Canvas2D (Drawing API)

#[cm("web:html#HTMLCanvasElement")]
pub resource HtmlCanvasElement extends HtmlElement {
    #[cm("web:html#HTMLCanvasElement.width", getter)]
    fn width(&self) -> u32;

    #[cm("web:html#HTMLCanvasElement.width", setter)]
    fn set_width(&self, value: u32);

    #[cm("web:html#HTMLCanvasElement.height", getter)]
    fn height(&self) -> u32;

    #[cm("web:html#HTMLCanvasElement.height", setter)]
    fn set_height(&self, value: u32);

    #[cm("web:html#HTMLCanvasElement.getContext")]
    fn get_context(&self, context_id: String) -> Option<CanvasRenderingContext2d>;
}

#[cm("web:canvas#CanvasRenderingContext2D")]
pub resource CanvasRenderingContext2d {
    fn save(&self);
    fn restore(&self);

    fn fill_rect(&self, x: f64, y: f64, w: f64, h: f64);
    fn stroke_rect(&self, x: f64, y: f64, w: f64, h: f64);
    fn clear_rect(&self, x: f64, y: f64, w: f64, h: f64);

    fn begin_path(&self);
    fn close_path(&self);
    fn move_to(&self, x: f64, y: f64);
    fn line_to(&self, x: f64, y: f64);
    fn arc(&self, x: f64, y: f64, radius: f64,
           start_angle: f64, end_angle: f64, anticlockwise: bool = false);
    fn fill(&self);
    fn stroke(&self);

    #[cm("web:canvas#CanvasRenderingContext2D.fillStyle", getter)]
    fn fill_style(&self) -> String;
    #[cm("web:canvas#CanvasRenderingContext2D.fillStyle", setter)]
    fn set_fill_style(&self, value: String);

    #[cm("web:canvas#CanvasRenderingContext2D.strokeStyle", getter)]
    fn stroke_style(&self) -> String;
    #[cm("web:canvas#CanvasRenderingContext2D.strokeStyle", setter)]
    fn set_stroke_style(&self, value: String);

    #[cm("web:canvas#CanvasRenderingContext2D.lineWidth", getter)]
    fn line_width(&self) -> f64;
    #[cm("web:canvas#CanvasRenderingContext2D.lineWidth", setter)]
    fn set_line_width(&self, value: f64);

    fn fill_text(&self, text: String, x: f64, y: f64, max_width: Option<f64> = null);

    #[cm("web:canvas#CanvasRenderingContext2D.font", getter)]
    fn font(&self) -> String;
    #[cm("web:canvas#CanvasRenderingContext2D.font", setter)]
    fn set_font(&self, value: String);
}

User code:

use { Dom } from "web:dom";
use { HtmlCanvasElement, CanvasRenderingContext2d } from "web:canvas";

fn draw() with Dom {
    let doc = Dom::document();
    if let Some(canvas) = doc.query_selector("#myCanvas") {
        if let Some(c) = canvas.downcast::<HtmlCanvasElement>() {
            if let Some(ctx) = c.get_context("2d") {
                ctx.set_fill_style("red");
                ctx.fill_rect(10.0, 10.0, 100.0, 50.0);

                ctx.begin_path();
                ctx.arc(75.0, 75.0, 50.0, 0.0, f64::PI * 2.0);  // anticlockwise defaults to false
                ctx.set_fill_style("blue");
                ctx.fill();

                ctx.set_font("24px serif");
                ctx.fill_text("Hello Wado!", 50.0, 150.0);  // max_width defaults to null
            }
        }
    }
}

Mixins (Trait Composition)

WebIDL:

interface mixin ParentNode {
  readonly attribute HTMLCollection children;
  Element? querySelector(DOMString selectors);
  NodeList querySelectorAll(DOMString selectors);
};

Document includes ParentNode;
Element includes ParentNode;
DocumentFragment includes ParentNode;

Generated Wado:

pub trait ParentNode {
    fn children(&self) -> HtmlCollection;
    fn query_selector(&self, selectors: String) -> Option<Element>;
    fn query_selector_all(&self, selectors: String) -> NodeList;
}

impl ParentNode for Document { /* generated delegation to host */ }
impl ParentNode for Element { /* generated delegation to host */ }
impl ParentNode for DocumentFragment { /* generated delegation to host */ }

Mixins are the one place where Wado traits shine over resource extends: a mixin can be applied to unrelated types that don't share an inheritance chain.

Union Types (Variant Generation)

WebIDL unions become Wado variants:

// WebIDL
typedef (Node or DOMString) NodeOrString;
// Generated Wado
pub variant NodeOrString {
    AsNode(Node),
    AsString(String),
}

Common unions get readable names from their WebIDL typedef. Unnamed unions get auto-generated names from constituent types.

Output File Structure

web/
├── dom/
│   ├── types.wado          # EventTarget, Node, Element, Document, Window
│   ├── events.wado         # Event, MouseEvent, KeyboardEvent, UIEvent
│   ├── html_elements.wado  # HTMLElement, HTMLInputElement, HTMLDivElement, ...
│   └── effect.wado         # pub effect Dom { ... }
├── dom.wado                # flat re-export: pub use { ... } from "web:dom/types.wado";
├── fetch/
│   ├── types.wado          # Request, Response, Headers, RequestInit
│   └── effect.wado         # pub effect Fetch { ... }
├── fetch.wado
├── canvas/
│   ├── types.wado          # CanvasRenderingContext2D, Path2D, ImageData
│   └── effect.wado
├── canvas.wado
├── websocket/
│   ├── types.wado          # WebSocket, CloseEvent, MessageEvent
│   └── effect.wado
├── websocket.wado
├── url/
│   └── types.wado          # URL, URLSearchParams
├── url.wado
└── ...

Organized by W3C/WHATWG specification, mirroring how wasi/ is organized by WASI interface.

Comparison Summary

Pain Point web-sys (Rust) Wado (wado-from-idl)
Type casting el.dyn_into::<HtmlInputElement>()? el.downcast::<HtmlInputElement>()Option
Closure boilerplate Closure::wrap(Box::new(...)).forget() \|e: Event\| { ... } — native GC closures
Untyped promises JsFuture → JsValue (untyped) Future<Response> (typed)
Overload naming fetch_with_str_and_init() fetch(url) or fetch(url, init)
Feature flags features = ["Element", "Node", ...] None needed
Inheritance Deref chain (anti-pattern) resource extends (first-class)
Effect tracking None (side effects implicit) with Dom, with Fetch (explicit)

Consequences

Language Changes Required

Dependencies

Not In Scope