From f831711f5d2d9f4e56d691a7e5d3aa37066fb164 Mon Sep 17 00:00:00 2001
From: Alex Crichton <alex@alexcrichton.com>
Date: Tue, 19 Feb 2019 09:08:37 -0800
Subject: [PATCH] Support `Option<RustStruct>` in arguments/returns

Add all the necessary support in a few locations and we should be good
to go!

Closes #1252
---
 crates/backend/src/codegen.rs        | 11 ++++
 crates/cli-support/src/js/js2rust.rs | 84 ++++++++++++++++------------
 crates/cli-support/src/js/rust2js.rs | 25 +++++++++
 tests/wasm/classes.js                | 19 +++++++
 tests/wasm/classes.rs                | 41 ++++++++++++++
 5 files changed, 145 insertions(+), 35 deletions(-)

diff --git a/crates/backend/src/codegen.rs b/crates/backend/src/codegen.rs
index 92d8cad4..7b0eef94 100644
--- a/crates/backend/src/codegen.rs
+++ b/crates/backend/src/codegen.rs
@@ -248,6 +248,17 @@ impl ToTokens for ast::Struct {
                     (*js).borrow_mut()
                 }
             }
+
+            impl ::wasm_bindgen::convert::OptionIntoWasmAbi for #name {
+                #[inline]
+                fn none() -> Self::Abi { 0 }
+            }
+
+            impl ::wasm_bindgen::convert::OptionFromWasmAbi for #name {
+                #[inline]
+                fn is_none(abi: &Self::Abi) -> bool { *abi == 0 }
+            }
+
         })
         .to_tokens(tokens);
 
diff --git a/crates/cli-support/src/js/js2rust.rs b/crates/cli-support/src/js/js2rust.rs
index af964b12..4dc4e8b2 100644
--- a/crates/cli-support/src/js/js2rust.rs
+++ b/crates/cli-support/src/js/js2rust.rs
@@ -313,6 +313,18 @@ impl<'a, 'b> Js2Rust<'a, 'b> {
                         .push(format!("isLikeNone({0}) ? {1} : {0}", name, hole));
                     return Ok(self);
                 }
+                Descriptor::RustStruct(ref s) => {
+                    self.js_arguments.push((name.clone(), format!("{} | undefined", s)));
+                    self.prelude(&format!("let ptr{} = 0;", i));
+                    self.prelude(&format!("if ({0} !== null && {0} !== undefined) {{", name));
+                    self.assert_class(&name, s);
+                    self.assert_not_moved(&name);
+                    self.prelude(&format!("ptr{} = {}.ptr;", i, name));
+                    self.prelude(&format!("{}.ptr = 0;", name));
+                    self.prelude("}");
+                    self.rust_arguments.push(format!("ptr{}", i));
+                    return Ok(self);
+                }
                 _ => bail!(
                     "unsupported optional argument type for calling Rust function from JS: {:?}",
                     arg
@@ -322,44 +334,13 @@ impl<'a, 'b> Js2Rust<'a, 'b> {
 
         if let Some(s) = arg.rust_struct() {
             self.js_arguments.push((name.clone(), s.to_string()));
-
-            if self.cx.config.debug {
-                self.cx.expose_assert_class();
-                self.prelude(&format!(
-                    "\
-                     _assertClass({arg}, {struct_});\n\
-                     ",
-                    arg = name,
-                    struct_ = s
-                ));
-            }
-
+            self.assert_class(&name, s);
+            self.assert_not_moved(&name);
             if arg.is_by_ref() {
                 self.rust_arguments.push(format!("{}.ptr", name));
             } else {
-                self.prelude(&format!(
-                    "\
-                     const ptr{i} = {arg}.ptr;\n\
-                     ",
-                    i = i,
-                    arg = name
-                ));
-                if self.cx.config.debug {
-                    self.prelude(&format!(
-                        "\
-                        if (ptr{i} === 0) {{
-                            throw new Error('Attempt to use a moved value');
-                        }}
-                        ",
-                        i = i,
-                    ));
-                }
-                self.prelude(&format!(
-                    "\
-                     {arg}.ptr = 0;\n\
-                     ",
-                    arg = name
-                ));
+                self.prelude(&format!("const ptr{} = {}.ptr;", i, name));
+                self.prelude(&format!("{}.ptr = 0;", name));
                 self.rust_arguments.push(format!("ptr{}", i));
             }
             return Ok(self);
@@ -627,6 +608,17 @@ impl<'a, 'b> Js2Rust<'a, 'b> {
                     );
                     return Ok(self);
                 }
+                Descriptor::RustStruct(ref name) => {
+                    self.ret_ty = format!("{} | undefined", name);
+                    self.cx.require_class_wrap(name);
+                    self.ret_expr = format!("
+                        const ptr = RET;
+                        return ptr === 0 ? undefined : {}.__wrap(ptr);
+                    ",
+                        name,
+                    );
+                    return Ok(self);
+                }
                 _ => bail!(
                     "unsupported optional return type for calling Rust function from JS: {:?}",
                     ty
@@ -764,4 +756,26 @@ impl<'a, 'b> Js2Rust<'a, 'b> {
         ts.push(';');
         (js, ts, self.js_doc_comments())
     }
+
+    fn assert_class(&mut self, arg: &str, class: &str) {
+        if !self.cx.config.debug {
+            return
+        }
+        self.cx.expose_assert_class();
+        self.prelude(&format!("_assertClass({}, {});", arg, class));
+    }
+
+    fn assert_not_moved(&mut self, arg: &str) {
+        if !self.cx.config.debug {
+            return
+        }
+        self.prelude(&format!(
+            "\
+                if ({0}.ptr === 0) {{
+                    throw new Error('Attempt to use a moved value');
+                }}
+            ",
+            arg,
+        ));
+    }
 }
diff --git a/crates/cli-support/src/js/rust2js.rs b/crates/cli-support/src/js/rust2js.rs
index 284ce5bc..a9a84d02 100644
--- a/crates/cli-support/src/js/rust2js.rs
+++ b/crates/cli-support/src/js/rust2js.rs
@@ -213,6 +213,13 @@ impl<'a, 'b> Rust2Js<'a, 'b> {
                     ));
                     return Ok(());
                 }
+                Descriptor::RustStruct(ref class) => {
+                    self.cx.require_class_wrap(class);
+                    let assign = format!("let c{0} = {0} === 0 ? undefined : {1}.__wrap({0});", abi, class);
+                    self.prelude(&assign);
+                    self.js_arguments.push(format!("c{}", abi));
+                    return Ok(());
+                }
                 _ => bail!(
                     "unsupported optional argument type for calling JS function from Rust: {:?}",
                     arg
@@ -456,6 +463,24 @@ impl<'a, 'b> Rust2Js<'a, 'b> {
                     );
                     return Ok(());
                 }
+                Descriptor::RustStruct(ref class) => {
+                    // Like below, assert the type
+                    self.ret_expr = format!(
+                        "\
+                        const val = JS;
+                        if (val === undefined || val === null)
+                            return 0;
+                        if (!(val instanceof {0})) {{
+                            throw new Error('expected value of type {0}');
+                        }}
+                        const ret = val.ptr;
+                        val.ptr = 0;
+                        return ret;\
+                    ",
+                        class
+                    );
+                    return Ok(());
+                }
                 _ => bail!(
                     "unsupported optional return type for calling JS function from Rust: {:?}",
                     ty
diff --git a/tests/wasm/classes.js b/tests/wasm/classes.js
index 1c3ef4cc..2e9512c8 100644
--- a/tests/wasm/classes.js
+++ b/tests/wasm/classes.js
@@ -148,3 +148,22 @@ exports.js_conditional_bindings = () => {
     const x = new wasm.ConditionalBindings();
     x.free();
 };
+
+exports.js_assert_none = x => {
+  assert.strictEqual(x, undefined);
+};
+exports.js_assert_some = x => {
+  assert.ok(x instanceof wasm.OptionClass);
+};
+exports.js_return_none1 = () => null;
+exports.js_return_none2 = () => undefined;
+exports.js_return_some = x => x;
+
+exports.js_test_option_classes = () => {
+  assert.strictEqual(wasm.option_class_none(), undefined);
+  wasm.option_class_assert_none(undefined);
+  wasm.option_class_assert_none(null);
+  const c = wasm.option_class_some();
+  assert.ok(c instanceof wasm.OptionClass);
+  wasm.option_class_assert_some(c);
+};
diff --git a/tests/wasm/classes.rs b/tests/wasm/classes.rs
index 4d199992..024848b0 100644
--- a/tests/wasm/classes.rs
+++ b/tests/wasm/classes.rs
@@ -23,6 +23,13 @@ extern "C" {
     fn js_access_fields();
     fn js_renamed_export();
     fn js_conditional_bindings();
+
+    fn js_assert_none(a: Option<OptionClass>);
+    fn js_assert_some(a: Option<OptionClass>);
+    fn js_return_none1() -> Option<OptionClass>;
+    fn js_return_none2() -> Option<OptionClass>;
+    fn js_return_some(a: OptionClass) -> Option<OptionClass>;
+    fn js_test_option_classes();
 }
 
 #[wasm_bindgen_test]
@@ -414,7 +421,41 @@ impl ConditionalBindings {
         ConditionalBindings {}
     }
 }
+
 #[wasm_bindgen_test]
 fn conditional_bindings() {
     js_conditional_bindings();
 }
+
+#[wasm_bindgen]
+pub struct OptionClass(u32);
+
+#[wasm_bindgen_test]
+fn option_class() {
+    js_assert_none(None);
+    js_assert_some(Some(OptionClass(1)));
+    assert!(js_return_none1().is_none());
+    assert!(js_return_none2().is_none());
+    assert_eq!(js_return_some(OptionClass(2)).unwrap().0, 2);
+    js_test_option_classes();
+}
+
+#[wasm_bindgen]
+pub fn option_class_none() -> Option<OptionClass> {
+    None
+}
+
+#[wasm_bindgen]
+pub fn option_class_some() -> Option<OptionClass> {
+    Some(OptionClass(3))
+}
+
+#[wasm_bindgen]
+pub fn option_class_assert_none(x: Option<OptionClass>) {
+    assert!(x.is_none());
+}
+
+#[wasm_bindgen]
+pub fn option_class_assert_some(x: Option<OptionClass>) {
+    assert_eq!(x.unwrap().0, 3);
+}