diff --git a/crates/backend/src/ast.rs b/crates/backend/src/ast.rs
index 634e6e4e..d8eb5761 100644
--- a/crates/backend/src/ast.rs
+++ b/crates/backend/src/ast.rs
@@ -1,3 +1,4 @@
+use std::collections::BTreeMap;
 use proc_macro2::{Ident, Span};
 use shared;
 use syn;
@@ -19,6 +20,8 @@ pub struct Program {
     pub structs: Vec<Struct>,
     /// rust consts
     pub consts: Vec<Const>,
+    /// rust submodules
+    pub modules: BTreeMap<Ident, Module>,
 }
 
 /// A rust to js interface. Allows interaction with rust objects/functions
@@ -220,6 +223,16 @@ pub enum ConstValue {
     Null,
 }
 
+/// A rust module
+///
+/// This exists to give the ability to namespace js imports.
+#[cfg_attr(feature = "extra-traits", derive(Debug, PartialEq, Eq))]
+pub struct Module {
+    pub vis: syn::Visibility,
+    /// js -> rust interfaces
+    pub imports: Vec<Import>,
+}
+
 impl Program {
     pub(crate) fn shared(&self) -> Result<shared::Program, Diagnostic> {
         Ok(shared::Program {
@@ -227,6 +240,8 @@ impl Program {
             structs: self.structs.iter().map(|a| a.shared()).collect(),
             enums: self.enums.iter().map(|a| a.shared()).collect(),
             imports: self.imports.iter()
+                // add in imports from inside modules
+                .chain(self.modules.values().flat_map(|m| m.imports.iter()))
                 .map(|a| a.shared())
                 .collect::<Result<_, Diagnostic>>()?,
             version: shared::version(),
diff --git a/crates/backend/src/codegen.rs b/crates/backend/src/codegen.rs
index a40cbf3a..ac8f1096 100644
--- a/crates/backend/src/codegen.rs
+++ b/crates/backend/src/codegen.rs
@@ -43,6 +43,8 @@ impl TryToTokens for ast::Program {
         for i in self.imports.iter() {
             DescribeImport(&i.kind).to_tokens(tokens);
 
+            // If there is a js namespace, check that name isn't a type. If it is,
+            // this import might be a method on that type.
             if let Some(ns) = &i.js_namespace {
                 if types.contains(ns) && i.kind.fits_on_impl() {
                     let kind = match i.kind.try_to_token_stream() {
@@ -61,6 +63,11 @@ impl TryToTokens for ast::Program {
                 errors.push(e);
             }
         }
+        for m in self.modules.iter() {
+            if let Err(e) = ModuleInIter::from(m).try_to_tokens(tokens) {
+                errors.push(e);
+            }
+        }
         for e in self.enums.iter() {
             e.to_tokens(tokens);
         }
@@ -87,6 +94,7 @@ impl TryToTokens for ast::Program {
         // Each JSON blob is prepended with the length of the JSON blob so when
         // all these sections are concatenated in the final wasm file we know
         // how to extract all the JSON pieces, so insert the byte length here.
+        // The value is little-endian.
         let generated_static_length = description.len() + 4;
         let mut bytes = vec![
             (description.len() >> 0) as u8,
@@ -1103,6 +1111,43 @@ impl ToTokens for ast::Const {
     }
 }
 
+/// Struct to help implementing TryToTokens over the key/value pairs from the hashmap.
+struct ModuleInIter<'a> {
+    name: &'a Ident,
+    module: &'a ast::Module
+}
+
+impl<'a> From<(&'a Ident, &'a ast::Module)> for ModuleInIter<'a> {
+    fn from((name, module): (&'a Ident, &'a ast::Module)) -> ModuleInIter<'a> {
+        ModuleInIter { name, module }
+    }
+}
+
+impl<'a> TryToTokens for ModuleInIter<'a> {
+    fn try_to_tokens(&self, tokens: &mut TokenStream) -> Result<(), Diagnostic> {
+        let name = &self.name;
+        let imports = &self.module.imports;
+        let mut errors = Vec::new();
+        for i in imports.iter() {
+            DescribeImport(&i.kind).to_tokens(tokens);
+        }
+        let vis = &self.module.vis;
+        let mut body = TokenStream::new();
+        for i in imports.iter() {
+            if let Err(e) = i.kind.try_to_tokens(&mut body) {
+                errors.push(e);
+            }
+        }
+        Diagnostic::from_vec(errors)?;
+        (quote!{
+            #vis mod #name {
+                #body
+            }
+        }).to_tokens(tokens);
+        Ok(())
+    }
+}
+
 /// Emits the necessary glue tokens for "descriptor", generating an appropriate
 /// symbol name as well as attributes around the descriptor function itself.
 struct Descriptor<'a, T>(&'a Ident, T);
diff --git a/crates/web-sys/build.rs b/crates/web-sys/build.rs
index 8dbb232c..630975c9 100644
--- a/crates/web-sys/build.rs
+++ b/crates/web-sys/build.rs
@@ -13,6 +13,8 @@ use std::path;
 use std::process::{self, Command};
 
 fn main() {
+    env_logger::init();
+
     if let Err(e) = try_main() {
         eprintln!("Error: {}", e);
         for c in e.iter_causes() {
@@ -24,11 +26,9 @@ fn main() {
 
 fn try_main() -> Result<(), failure::Error> {
     println!("cargo:rerun-if-changed=build.rs");
-    env_logger::init();
-
     println!("cargo:rerun-if-changed=webidls/enabled");
-    let entries = fs::read_dir("webidls/enabled").context("reading webidls/enabled directory")?;
 
+    let entries = fs::read_dir("webidls/enabled").context("reading webidls/enabled directory")?;
     let mut source = SourceFile::default();
     for entry in entries {
         let entry = entry.context("getting webidls/enabled/*.webidl entry")?;
@@ -38,8 +38,7 @@ fn try_main() -> Result<(), failure::Error> {
         }
         println!("cargo:rerun-if-changed={}", path.display());
         source = source.add_file(&path)
-            .with_context(|_| format!("reading contents of file \"{}\"",
-                                      path.display()))?;
+            .with_context(|_| format!("reading contents of file \"{}\"", path.display()))?;
     }
 
     let bindings = match wasm_bindgen_webidl::compile(&source.contents) {
@@ -70,9 +69,9 @@ fn try_main() -> Result<(), failure::Error> {
         let status = Command::new("rustfmt")
             .arg(&out_file_path)
             .status()
-           .context("running rustfmt")?;
+            .context("running rustfmt")?;
         if !status.success() {
-           bail!("rustfmt failed: {}", status)
+            bail!("rustfmt failed: {}", status)
         }
     }
 
diff --git a/crates/web-sys/tests/wasm/console.rs b/crates/web-sys/tests/wasm/console.rs
new file mode 100644
index 00000000..61ce5c29
--- /dev/null
+++ b/crates/web-sys/tests/wasm/console.rs
@@ -0,0 +1,9 @@
+use wasm_bindgen_test::*;
+use wasm_bindgen::prelude::*;
+use web_sys::console;
+
+#[wasm_bindgen_test]
+fn test_console() {
+    console::time("test label");
+    console::time_end("test label");
+}
diff --git a/crates/web-sys/tests/wasm/main.rs b/crates/web-sys/tests/wasm/main.rs
index 279f9bde..69e4d6f0 100644
--- a/crates/web-sys/tests/wasm/main.rs
+++ b/crates/web-sys/tests/wasm/main.rs
@@ -14,6 +14,7 @@ pub mod anchor_element;
 pub mod body_element;
 pub mod br_element;
 pub mod button_element;
+pub mod console;
 pub mod div_element;
 pub mod element;
 pub mod event;
diff --git a/crates/webidl-tests/main.rs b/crates/webidl-tests/main.rs
index 2a31140b..12635f50 100644
--- a/crates/webidl-tests/main.rs
+++ b/crates/webidl-tests/main.rs
@@ -10,3 +10,4 @@ pub mod consts;
 pub mod enums;
 pub mod simple;
 pub mod throws;
+pub mod namespace;
diff --git a/crates/webidl-tests/namespace.js b/crates/webidl-tests/namespace.js
new file mode 100644
index 00000000..7c86dcd5
--- /dev/null
+++ b/crates/webidl-tests/namespace.js
@@ -0,0 +1,11 @@
+const strictEqual = require('assert').strictEqual;
+
+global.mathtest = {};
+
+global.mathtest.powf = function powf(base, exp) {
+    return Math.pow(base, exp);
+}
+
+global.mathtest.add_one = function add_one(val) {
+    return val + 1;
+}
diff --git a/crates/webidl-tests/namespace.rs b/crates/webidl-tests/namespace.rs
new file mode 100644
index 00000000..49afc623
--- /dev/null
+++ b/crates/webidl-tests/namespace.rs
@@ -0,0 +1,10 @@
+use wasm_bindgen_test::*;
+
+include!(concat!(env!("OUT_DIR"), "/namespace.rs"));
+
+#[wasm_bindgen_test]
+fn simple_namespace_test() {
+    assert_eq!(mathtest::add_one(1), 2);
+    assert_eq!(mathtest::powf(1.0, 100.0), 1.0);
+    assert_eq!(mathtest::powf(10.0, 2.0), 100.0);
+}
diff --git a/crates/webidl-tests/namespace.webidl b/crates/webidl-tests/namespace.webidl
new file mode 100644
index 00000000..1c3dbf3a
--- /dev/null
+++ b/crates/webidl-tests/namespace.webidl
@@ -0,0 +1,4 @@
+namespace mathtest {
+    long add_one(long val);
+    double powf(double base, double exponent);
+};
diff --git a/crates/webidl/src/first_pass.rs b/crates/webidl/src/first_pass.rs
index 4e58443a..f6c109fe 100644
--- a/crates/webidl/src/first_pass.rs
+++ b/crates/webidl/src/first_pass.rs
@@ -28,6 +28,7 @@ pub(crate) struct FirstPassRecord<'src> {
     /// The mixins, mapping their name to the webidl ast node for the mixin.
     pub(crate) mixins: BTreeMap<&'src str, Vec<&'src MixinMembers<'src>>>,
     pub(crate) typedefs: BTreeMap<&'src str, &'src weedle::types::Type<'src>>,
+    pub(crate) namespaces: BTreeMap<&'src str, NamespaceData<'src>>,
 }
 
 /// We need to collect interface data during the first pass, to be used later.
@@ -40,6 +41,30 @@ pub(crate) struct InterfaceData<'src> {
     pub(crate) superclass: Option<&'src str>,
 }
 
+/// We need to collect namespace data during the first pass, to be used later.
+pub(crate) struct NamespaceData<'src> {
+    /// Whether only partial namespaces were encountered
+    pub(crate) partial: bool,
+    pub(crate) operations: BTreeMap<Option<&'src str>, OperationData<'src>>,
+}
+
+impl<'src> NamespaceData<'src> {
+    /// Creates an empty node for a non-partial namespace.
+    pub(crate) fn empty_non_partial() -> Self {
+        Self {
+            partial: false,
+            operations: Default::default(),
+        }
+    }
+    /// Creates an empty node for a partial namespace.
+    pub(crate) fn empty_partial() -> Self {
+        Self {
+            partial: true,
+            operations: Default::default(),
+        }
+    }
+}
+
 #[derive(PartialEq, Eq, PartialOrd, Ord)]
 pub(crate) enum OperationId<'src> {
     Constructor,
@@ -83,6 +108,8 @@ impl<'src> FirstPass<'src, ()> for weedle::Definition<'src> {
             PartialInterface(interface) => interface.first_pass(record, ()),
             InterfaceMixin(mixin) => mixin.first_pass(record, ()),
             PartialInterfaceMixin(mixin) => mixin.first_pass(record, ()),
+            Namespace(namespace) => namespace.first_pass(record, ()),
+            PartialNamespace(namespace) => namespace.first_pass(record, ()),
             Typedef(typedef) => typedef.first_pass(record, ()),
             _ => {
                 // Other definitions aren't currently used in the first pass
@@ -111,7 +138,8 @@ impl<'src> FirstPass<'src, ()> for weedle::EnumDefinition<'src> {
     }
 }
 
-fn first_pass_operation<'src>(
+/// Helper function to add an operation to an interface.
+fn first_pass_interface_operation<'src>(
     record: &mut FirstPassRecord<'src>,
     self_name: &'src str,
     id: OperationId<'src>,
@@ -131,7 +159,7 @@ fn first_pass_operation<'src>(
         .operations
         .entry(id)
         .and_modify(|operation_data| operation_data.overloaded = true)
-        .or_insert_with(Default::default)
+        .or_default()
         .argument_names_same
         .entry(names)
         .and_modify(|same_argument_names| *same_argument_names = true)
@@ -146,7 +174,7 @@ impl<'src> FirstPass<'src, ()> for weedle::InterfaceDefinition<'src> {
             let interface = record
                 .interfaces
                 .entry(self.identifier.0)
-                .or_insert_with(Default::default);
+                .or_default();
             interface.partial = false;
             interface.superclass = self.inheritance.map(|s| s.identifier.0);
         }
@@ -199,7 +227,7 @@ impl<'src> FirstPass<'src, &'src str> for ExtendedAttribute<'src> {
     fn first_pass(&'src self, record: &mut FirstPassRecord<'src>, self_name: &'src str) -> Result<()> {
         match self {
             ExtendedAttribute::ArgList(list) if list.identifier.0 == "Constructor" => {
-                first_pass_operation(
+                first_pass_interface_operation(
                     record,
                     self_name,
                     OperationId::Constructor,
@@ -207,7 +235,7 @@ impl<'src> FirstPass<'src, &'src str> for ExtendedAttribute<'src> {
                 )
             }
             ExtendedAttribute::NoArgs(name) if (name.0).0 == "Constructor" => {
-                first_pass_operation(
+                first_pass_interface_operation(
                     record,
                     self_name,
                     OperationId::Constructor,
@@ -217,7 +245,7 @@ impl<'src> FirstPass<'src, &'src str> for ExtendedAttribute<'src> {
             ExtendedAttribute::NamedArgList(list)
                 if list.lhs_identifier.0 == "NamedConstructor" =>
             {
-                first_pass_operation(
+                first_pass_interface_operation(
                     record,
                     self_name,
                     OperationId::Constructor,
@@ -258,16 +286,20 @@ impl<'src> FirstPass<'src, &'src str> for weedle::interface::OperationInterfaceM
             warn!("Unsupported webidl operation {:?}", self);
             return Ok(())
         }
-        first_pass_operation(
+        first_pass_interface_operation(
             record,
             self_name,
             match self.identifier.map(|s| s.0) {
                 None => match self.specials.get(0) {
                     None => OperationId::Operation(None),
-                    Some(weedle::interface::Special::Getter(weedle::term::Getter)) => OperationId::IndexingGetter,
-                    Some(weedle::interface::Special::Setter(weedle::term::Setter)) => OperationId::IndexingSetter,
-                    Some(weedle::interface::Special::Deleter(weedle::term::Deleter)) => OperationId::IndexingDeleter,
-                    Some(weedle::interface::Special::LegacyCaller(weedle::term::LegacyCaller)) => return Ok(()),
+                    Some(weedle::interface::Special::Getter(weedle::term::Getter))
+                        => OperationId::IndexingGetter,
+                    Some(weedle::interface::Special::Setter(weedle::term::Setter))
+                        => OperationId::IndexingSetter,
+                    Some(weedle::interface::Special::Deleter(weedle::term::Deleter))
+                        => OperationId::IndexingDeleter,
+                    Some(weedle::interface::Special::LegacyCaller(weedle::term::LegacyCaller))
+                        => return Ok(()),
                 },
                 Some(ref name) => OperationId::Operation(Some(name.clone())),
             },
@@ -312,6 +344,91 @@ impl<'src> FirstPass<'src, ()> for weedle::TypedefDefinition<'src> {
     }
 }
 
+impl<'src> FirstPass<'src, ()> for weedle::NamespaceDefinition<'src> {
+    fn first_pass(&'src self, record: &mut FirstPassRecord<'src>, (): ()) -> Result<()> {
+        record
+            .namespaces
+            .entry(self.identifier.0)
+            .and_modify(|entry| entry.partial = false)
+            .or_insert_with(NamespaceData::empty_non_partial);
+
+        if util::is_chrome_only(&self.attributes) {
+            return Ok(())
+        }
+
+        // We ignore all attributes.
+
+        for member in &self.members.body {
+            member.first_pass(record, self.identifier.0)?;
+        }
+
+        Ok(())
+    }
+}
+
+impl<'src> FirstPass<'src, ()> for weedle::PartialNamespaceDefinition<'src> {
+    fn first_pass(&'src self, record: &mut FirstPassRecord<'src>, (): ()) -> Result<()> {
+        record
+            .namespaces
+            .entry(self.identifier.0)
+            .or_insert_with(NamespaceData::empty_partial);
+
+        if util::is_chrome_only(&self.attributes) {
+            return Ok(())
+        }
+
+        for member in &self.members.body {
+            member.first_pass(record, self.identifier.0)?;
+        }
+
+        Ok(())
+    }
+}
+
+impl<'src> FirstPass<'src, &'src str> for weedle::namespace::NamespaceMember<'src> {
+    fn first_pass(&'src self,
+                  record: &mut FirstPassRecord<'src>,
+                  namespace_name: &'src str) -> Result<()>
+    {
+        match self {
+            weedle::namespace::NamespaceMember::Operation(op) => {
+                op.first_pass(record, namespace_name)
+            }
+            _ => Ok(()),
+        }
+    }
+}
+
+impl<'src> FirstPass<'src, &'src str> for weedle::namespace::OperationNamespaceMember<'src> {
+    fn first_pass(&'src self,
+                  record: &mut FirstPassRecord<'src>,
+                  namespace_name: &'src str) -> Result<()>
+    {
+        let identifier = self.identifier.map(|s| s.0);
+        let arguments = &self.args.body.list;
+        let mut names = Vec::with_capacity(arguments.len());
+        for argument in arguments {
+            match argument {
+                Argument::Single(arg) => names.push(arg.identifier.0),
+                Argument::Variadic(_) => return Ok(()),
+            }
+        }
+        record
+            .namespaces
+            .get_mut(namespace_name)
+            .unwrap() // call this after creating the namespace
+            .operations
+            .entry(identifier)
+            .and_modify(|operation_data| operation_data.overloaded = true)
+            .or_insert_with(Default::default)
+            .argument_names_same
+            .entry(names)
+            .and_modify(|same_argument_names| *same_argument_names = true)
+            .or_insert(false);
+        Ok(())
+    }
+}
+
 impl<'a> FirstPassRecord<'a> {
     pub fn all_superclasses<'me>(&'me self, interface: &str)
         -> impl Iterator<Item = String> + 'me
diff --git a/crates/webidl/src/lib.rs b/crates/webidl/src/lib.rs
index 0fb33c90..4d139c0c 100644
--- a/crates/webidl/src/lib.rs
+++ b/crates/webidl/src/lib.rs
@@ -36,9 +36,9 @@ use std::path::Path;
 
 use backend::TryToTokens;
 use backend::defined::{ImportedTypeDefinitions, RemoveUndefinedImports};
-use backend::util::{ident_ty, rust_ident, wrap_import_function};
+use backend::util::{ident_ty, rust_ident, raw_ident, wrap_import_function};
 use failure::ResultExt;
-use heck::{ShoutySnakeCase};
+use heck::{ShoutySnakeCase, SnakeCase};
 use proc_macro2::{Ident, Span};
 use weedle::argument::Argument;
 use weedle::attribute::{ExtendedAttribute, ExtendedAttributeList};
@@ -185,13 +185,17 @@ impl<'src> WebidlParse<'src, ()> for weedle::Definition<'src> {
             weedle::Definition::Implements(..) => {
                 // nothing to do for this, ignore it
             }
+            weedle::Definition::Namespace(namespace) => {
+                namespace.webidl_parse(program, first_pass, ())?
+            }
+            weedle::Definition::PartialNamespace(namespace) => {
+                namespace.webidl_parse(program, first_pass, ())?
+            }
             // TODO
             weedle::Definition::Callback(..)
             | weedle::Definition::CallbackInterface(..)
             | weedle::Definition::Dictionary(..)
-            | weedle::Definition::PartialDictionary(..)
-            | weedle::Definition::Namespace(..)
-            | weedle::Definition::PartialNamespace(..) => {
+            | weedle::Definition::PartialDictionary(..) => {
                 warn!("Unsupported WebIDL definition: {:?}", self)
             }
         }
@@ -303,7 +307,7 @@ impl<'src> WebidlParse<'src, &'src weedle::InterfaceDefinition<'src>> for Extend
         interface: &'src weedle::InterfaceDefinition<'src>,
     ) -> Result<()> {
         let mut add_constructor = |arguments: &[Argument], class: &str| {
-            let (overloaded, same_argument_names) = first_pass.get_operation_overloading(
+            let (overloaded, same_argument_names) = first_pass.get_method_overloading(
                 arguments,
                 &::first_pass::OperationId::Constructor,
                 interface.identifier.0,
@@ -657,12 +661,17 @@ fn member_operation<'src>(
             match identifier.map(|s| s.0) {
                 None if specials.is_empty() => ::first_pass::OperationId::Operation(None),
                 None if specials.len() == 1 => match specials[0] {
-                    weedle::interface::Special::Getter(weedle::term::Getter) => ::first_pass::OperationId::IndexingGetter,
-                    weedle::interface::Special::Setter(weedle::term::Setter) => ::first_pass::OperationId::IndexingSetter,
-                    weedle::interface::Special::Deleter(weedle::term::Deleter) => ::first_pass::OperationId::IndexingDeleter,
-                    weedle::interface::Special::LegacyCaller(weedle::term::LegacyCaller) => return Ok(()),
+                    weedle::interface::Special::Getter(weedle::term::Getter) =>
+                        ::first_pass::OperationId::IndexingGetter,
+                    weedle::interface::Special::Setter(weedle::term::Setter) =>
+                        ::first_pass::OperationId::IndexingSetter,
+                    weedle::interface::Special::Deleter(weedle::term::Deleter) =>
+                        ::first_pass::OperationId::IndexingDeleter,
+                    weedle::interface::Special::LegacyCaller(weedle::term::LegacyCaller) =>
+                        return Ok(()),
                 },
-                Some(ref name) if specials.is_empty() => ::first_pass::OperationId::Operation(Some(name.clone())),
+                Some(ref name) if specials.is_empty() =>
+                    ::first_pass::OperationId::Operation(Some(name.clone())),
                 _ => {
                     warn!("Unsupported specials on type {:?}", (self_name, identifier));
                     return Ok(())
@@ -744,8 +753,8 @@ impl<'src> WebidlParse<'src, ()> for weedle::EnumDefinition<'src> {
                 variants: variants
                     .iter()
                     .map(|v| {
-                         if !v.0.is_empty() {
-                             rust_ident(camel_case_ident(&v.0).as_str())
+                        if !v.0.is_empty() {
+                            rust_ident(camel_case_ident(&v.0).as_str())
                         } else {
                             rust_ident("None")
                         }
@@ -764,10 +773,10 @@ impl<'src> WebidlParse<'src, &'src str> for weedle::interface::ConstMember<'src>
     fn webidl_parse(
         &'src self,
         program: &mut backend::ast::Program,
-        record: &FirstPassRecord<'src>,
+        first_pass: &FirstPassRecord<'src>,
         self_name: &'src str,
     ) -> Result<()> {
-        let ty = match self.const_type.to_syn_type(record, TypePosition::Return) {
+        let ty = match self.const_type.to_syn_type(first_pass, TypePosition::Return) {
             Some(s) => s,
             None => return Ok(()),
         };
@@ -783,3 +792,149 @@ impl<'src> WebidlParse<'src, &'src str> for weedle::interface::ConstMember<'src>
         Ok(())
     }
 }
+
+impl<'src> WebidlParse<'src, ()> for weedle::NamespaceDefinition<'src> {
+    fn webidl_parse(
+        &'src self,
+        program: &mut backend::ast::Program,
+        first_pass: &FirstPassRecord<'src>,
+        (): (),
+    ) -> Result<()> {
+        if util::is_chrome_only(&self.attributes) {
+            return Ok(());
+        }
+
+        let rust_name = rust_ident(self.identifier.0.to_snake_case().as_str());
+
+        program.modules.entry(rust_name.clone())
+            .and_modify(|_| warn!("Namespace with rust name {:?} added more than once", rust_name))
+            .or_insert_with(|| backend::ast::Module {
+                vis: public(),
+                imports: Default::default()
+            });
+
+        if let Some(attrs) = &self.attributes {
+            for attr in &attrs.body.list {
+                attr.webidl_parse(program, first_pass, self)?;
+            }
+        }
+
+        let namespace_names = NamespaceNames {
+            rust_name: &rust_name,
+            js_name: &self.identifier.0,
+        };
+        for member in &self.members.body {
+            member.webidl_parse(program, first_pass, namespace_names)?;
+        }
+
+        Ok(())
+    }
+}
+
+impl<'src> WebidlParse<'src, ()> for weedle::PartialNamespaceDefinition<'src> {
+    fn webidl_parse(
+        &'src self,
+        program: &mut backend::ast::Program,
+        first_pass: &FirstPassRecord<'src>,
+        (): (),
+    ) -> Result<()> {
+        if util::is_chrome_only(&self.attributes) {
+            return Ok(());
+        }
+
+        let rust_name = rust_ident(self.identifier.0.to_snake_case().as_str());
+
+        if !first_pass.namespaces.contains_key(self.identifier.0) {
+            warn!(
+                "Partial namespace {} missing non-partial namespace",
+                self.identifier.0
+            );
+        }
+
+        let namespace_names = NamespaceNames {
+            rust_name: &rust_name,
+            js_name: &self.identifier.0,
+        };
+        for member in &self.members.body {
+            member.webidl_parse(program, first_pass, namespace_names)?;
+        }
+
+        Ok(())
+    }
+}
+
+impl<'src> WebidlParse<'src, &'src weedle::NamespaceDefinition<'src>> for ExtendedAttribute<'src> {
+    fn webidl_parse(
+        &'src self,
+        _program: &mut backend::ast::Program,
+        _first_pass: &FirstPassRecord<'src>,
+        _namespace: &'src weedle::NamespaceDefinition<'src>,
+    ) -> Result<()> {
+        warn!("Unsupported WebIDL extended attribute: {:?}", self);
+        Ok(())
+    }
+}
+#[derive(Copy, Clone)]
+struct NamespaceNames<'a> {
+    rust_name: &'a Ident,
+    js_name: &'a str,
+}
+
+impl<'src> WebidlParse<'src, NamespaceNames<'src>> for weedle::namespace::NamespaceMember<'src> {
+    fn webidl_parse(
+        &'src self,
+        program: &mut backend::ast::Program,
+        first_pass: &FirstPassRecord<'src>,
+        ns_names: NamespaceNames<'src>
+    ) -> Result<()> {
+        match self {
+            weedle::namespace::NamespaceMember::Operation(op) => {
+                op.webidl_parse(program, first_pass, ns_names)?;
+            }
+            weedle::namespace::NamespaceMember::Attribute(_) => {
+                warn!("Attribute namespace members are not supported")
+            }
+        }
+        Ok(())
+    }
+}
+
+impl<'src> WebidlParse<'src, NamespaceNames<'src>> for weedle::namespace::OperationNamespaceMember<'src> {
+    fn webidl_parse(
+        &'src self,
+        program: &mut backend::ast::Program,
+        first_pass: &FirstPassRecord<'src>,
+        ns_names: NamespaceNames<'src>
+    ) -> Result<()> {
+        if util::is_chrome_only(&self.attributes) {
+            return Ok(());
+        }
+
+        let imported_fn = match first_pass.create_namespace_operation(
+            &self.args.body.list,
+            self.identifier.as_ref().map(|id| id.0),
+            &self.return_type,
+            ns_names.js_name,
+            util::throws(&self.attributes)
+        ) {
+            Some(f) => f,
+            None => { return Ok(()) }
+        };
+
+        let import = backend::ast::Import {
+            module: None,
+            js_namespace: Some(raw_ident(ns_names.js_name)),
+            kind: backend::ast::ImportKind::Function(imported_fn),
+        };
+
+        program
+            .modules
+            .get_mut(ns_names.rust_name)
+            .unwrap()
+            .imports
+            .push(import);
+
+        Ok(())
+    }
+}
+
diff --git a/crates/webidl/src/util.rs b/crates/webidl/src/util.rs
index 554f90f0..2754ea7a 100644
--- a/crates/webidl/src/util.rs
+++ b/crates/webidl/src/util.rs
@@ -12,7 +12,7 @@ use weedle::common::Identifier;
 use weedle::types::*;
 use weedle::literal::{ConstValue, FloatLit, IntegerLit};
 
-use first_pass::FirstPassRecord;
+use first_pass::{self, FirstPassRecord};
 
 /// Take a type and create an immutable shared reference to that type.
 fn shared_ref(ty: syn::Type) -> syn::Type {
@@ -830,6 +830,8 @@ impl<'src> FirstPassRecord<'src> {
     }
 
     /// Create a wasm-bindgen function, if possible.
+    ///
+    /// Currently fails on any variadic args.
     pub fn create_function(
         &self,
         name: &str,
@@ -910,31 +912,31 @@ impl<'src> FirstPassRecord<'src> {
     pub fn create_basic_method(
         &self,
         arguments: &[weedle::argument::Argument],
-        operation_id: ::first_pass::OperationId,
+        operation_id: first_pass::OperationId,
         return_type: &weedle::types::ReturnType,
         self_name: &str,
         is_static: bool,
         structural: bool,
         catch: bool,
     ) -> Option<backend::ast::ImportFunction> {
-        let (overloaded, same_argument_names) = self.get_operation_overloading(
+        let (overloaded, same_argument_names) = self.get_method_overloading(
             arguments,
             &operation_id,
             self_name,
         );
 
         let name = match &operation_id {
-            ::first_pass::OperationId::Constructor => panic!("constructors are unsupported"),
-            ::first_pass::OperationId::Operation(name) => match name {
+            first_pass::OperationId::Constructor => panic!("constructors are unsupported"),
+            first_pass::OperationId::Operation(name) => match name {
                 None => {
                     warn!("Operations without a name are unsupported");
                     return None;
                 }
                 Some(name) => name.to_string(),
             },
-            ::first_pass::OperationId::IndexingGetter => "get".to_string(),
-            ::first_pass::OperationId::IndexingSetter => "set".to_string(),
-            ::first_pass::OperationId::IndexingDeleter => "delete".to_string(),
+            first_pass::OperationId::IndexingGetter => "get".to_string(),
+            first_pass::OperationId::IndexingSetter => "set".to_string(),
+            first_pass::OperationId::IndexingDeleter => "delete".to_string(),
         };
 
         let kind = backend::ast::ImportFunctionKind::Method {
@@ -943,11 +945,11 @@ impl<'src> FirstPassRecord<'src> {
             kind: backend::ast::MethodKind::Operation(backend::ast::Operation {
                 is_static,
                 kind: match &operation_id {
-                    ::first_pass::OperationId::Constructor => panic!("constructors are unsupported"),
-                    ::first_pass::OperationId::Operation(_) => backend::ast::OperationKind::Regular,
-                    ::first_pass::OperationId::IndexingGetter => backend::ast::OperationKind::IndexingGetter,
-                    ::first_pass::OperationId::IndexingSetter => backend::ast::OperationKind::IndexingSetter,
-                    ::first_pass::OperationId::IndexingDeleter => backend::ast::OperationKind::IndexingDeleter,
+                    first_pass::OperationId::Constructor => panic!("constructors are unsupported"),
+                    first_pass::OperationId::Operation(_) => backend::ast::OperationKind::Regular,
+                    first_pass::OperationId::IndexingGetter => backend::ast::OperationKind::IndexingGetter,
+                    first_pass::OperationId::IndexingSetter => backend::ast::OperationKind::IndexingSetter,
+                    first_pass::OperationId::IndexingDeleter => backend::ast::OperationKind::IndexingDeleter,
                 },
             }),
         };
@@ -965,17 +967,17 @@ impl<'src> FirstPassRecord<'src> {
             }
         };
         let doc_comment = match &operation_id {
-            ::first_pass::OperationId::Constructor => panic!("constructors are unsupported"),
-            ::first_pass::OperationId::Operation(_) => Some(
+            first_pass::OperationId::Constructor => panic!("constructors are unsupported"),
+            first_pass::OperationId::Operation(_) => Some(
                 format!(
                     "The `{}()` method\n\n{}",
                     name,
                     mdn_doc(self_name, Some(&name))
                 )
             ),
-            ::first_pass::OperationId::IndexingGetter => Some("The indexing getter\n\n".to_string()),
-            ::first_pass::OperationId::IndexingSetter => Some("The indexing setter\n\n".to_string()),
-            ::first_pass::OperationId::IndexingDeleter => Some("The indexing deleter\n\n".to_string()),
+            first_pass::OperationId::IndexingGetter => Some("The indexing getter\n\n".to_string()),
+            first_pass::OperationId::IndexingSetter => Some("The indexing setter\n\n".to_string()),
+            first_pass::OperationId::IndexingDeleter => Some("The indexing deleter\n\n".to_string()),
         };
 
         self.create_function(
@@ -993,10 +995,10 @@ impl<'src> FirstPassRecord<'src> {
 
     /// Whether operation is overloaded and
     /// whether there overloads with same argument names for given argument types
-    pub fn get_operation_overloading(
+    pub fn get_method_overloading(
         &self,
         arguments: &[weedle::argument::Argument],
-        id: &::first_pass::OperationId,
+        id: &first_pass::OperationId,
         self_name: &str,
     ) -> (bool, bool) {
         let data = match self.interfaces.get(self_name) {
@@ -1023,6 +1025,91 @@ impl<'src> FirstPassRecord<'src> {
         )
     }
 
+    /// Create a wasm-bindgen operation (free function with no `self` type), if possible.
+    pub fn create_namespace_operation(
+        &self,
+        arguments: &[weedle::argument::Argument],
+        operation_name: Option<&str>,
+        return_type: &weedle::types::ReturnType,
+        namespace_name: &str,
+        catch: bool,
+    ) -> Option<backend::ast::ImportFunction> {
+        let (overloaded, same_argument_names) = self.get_namespaced_operation_overloading(
+            arguments,
+            operation_name,
+            namespace_name,
+        );
+
+        let name = match operation_name {
+            Some(name) => name.to_string(),
+            None => {
+                warn!("Operations without a name are unsupported");
+                return None;
+            }
+        };
+
+        let ret = match return_type {
+            weedle::types::ReturnType::Void(_) => None,
+            weedle::types::ReturnType::Type(ty) => {
+                match ty.to_syn_type(self, TypePosition::Return) {
+                    None => {
+                        warn!("Operation's return type is not yet supported: {:?}", ty);
+                        return None;
+                    }
+                    Some(ty) => Some(ty),
+                }
+            }
+        };
+        let doc_comment = format!("The `{}.{}()` function\n\n{}",
+                                  namespace_name,
+                                  name,
+                                  mdn_doc(namespace_name, Some(&name))); // checked link
+
+        self.create_function(
+            &name,
+            overloaded,
+            same_argument_names,
+            arguments,
+            ret,
+            backend::ast::ImportFunctionKind::Normal,
+            false,
+            catch,
+            Some(doc_comment),
+        )
+    }
+
+    /// Whether operation is overloaded and
+    /// whether there overloads with same argument names for given argument types
+    pub fn get_namespaced_operation_overloading(
+        &self,
+        arguments: &[weedle::argument::Argument],
+        operation_name: Option<&str>,
+        namespace_name: &str,
+    ) -> (bool, bool) {
+        let data = match self.namespaces.get(namespace_name) {
+            Some(data) => data,
+            None => return (false, false),
+        };
+        let data = match data.operations.get(&operation_name) {
+            Some(data) => data,
+            None => return (false, false),
+        };
+        let mut names = Vec::with_capacity(arguments.len());
+        for arg in arguments {
+            match arg {
+                Argument::Single(arg) => names.push(arg.identifier.0),
+                Argument::Variadic(_) => return (false, false),
+            }
+        }
+        (
+            data.overloaded,
+            *data
+                .argument_names_same
+                .get(&names)
+                .unwrap_or(&false)
+        )
+    }
+
     /// Create a wasm-bindgen getter method, if possible.
     pub fn create_getter(
         &self,
diff --git a/guide/src/testing.md b/guide/src/testing.md
index 8f4c2974..fe3bca62 100644
--- a/guide/src/testing.md
+++ b/guide/src/testing.md
@@ -26,7 +26,7 @@ cargo test
 ## The Web IDL Frontend's Tests
 
 ```
-cargo test -p wasm-bindgen-webidl
+cargo test -p webidl-tests --target wasm32-unknown-unknown
 ```
 
 ## The Macro UI Tests
diff --git a/src/lib.rs b/src/lib.rs
index 2f6a2be0..a4290155 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -790,7 +790,7 @@ pub mod __rt {
     /// above. That means if this function is called and referenced we'll pull
     /// in the object file and link the intrinsics.
     ///
-    /// Note that this is an #[inline] function to remove the function call
+    /// Note that this is an `#[inline]` function to remove the function call
     /// overhead we inject in functions, but right now it's unclear how to do
     /// this in a zero-cost fashion. The lowest cost seems to be generating a
     /// store that can't be optimized away (to a global), which is listed below.