wasm-bindgen-cli-support 0.2.58

Shared support for the wasm-bindgen-cli package, an internal dependency
Documentation
//! Support for actually generating a JS function shim.
//!
//! This `Builder` type is used to generate JS function shims which sit between
//! exported functions, table elements, imports, etc. All function shims
//! generated by `wasm-bindgen` run through this type.

use crate::js::Context;
use crate::wit::InstructionData;
use crate::wit::{Adapter, AdapterId, AdapterKind, AdapterType, Instruction};
use anyhow::{anyhow, bail, Error};
use walrus::Module;

/// A one-size-fits-all builder for processing WebIDL bindings and generating
/// JS.
pub struct Builder<'a, 'b> {
    /// Parent context used to expose helper functions and such.
    pub cx: &'a mut Context<'b>,
    /// Whether or not this is building a constructor for a Rust class, and if
    /// so what class it's constructing.
    constructor: Option<String>,
    /// Whether or not this is building a method of a Rust class instance, and
    /// whether or not the method consumes `self` or not.
    method: Option<bool>,
    /// Whether or not we're catching exceptions from the main function
    /// invocation. Currently only used for imports.
    catch: bool,
    /// Whether or not we're logging the error coming out of this intrinsic
    log_error: bool,
}

/// Helper struct used to create JS to process all instructions in an adapter
/// function.
pub struct JsBuilder<'a, 'b> {
    /// General context for building JS, used to manage intrinsic names, exposed
    /// JS functions, etc.
    cx: &'a mut Context<'b>,

    /// The "prelude" of the function, or largely just the JS function we've
    /// built so far.
    prelude: String,

    /// JS code to execute in a `finally` block in case any exceptions happen.
    finally: String,

    /// An index used to manage allocation of temporary indices, used to name
    /// variables to ensure nothing clashes with anything else.
    tmp: usize,

    /// Names or expressions representing the arguments to the adapter. This is
    /// use to translate the `arg.get` instruction.
    args: Vec<String>,

    /// The wasm interface types "stack". The expressions pushed onto this stack
    /// are intended to be *pure*, and if they're not, they should be pushed
    /// into the `prelude`, assigned to a variable, and the variable should be
    /// pushed to the stack. We're not super principled about this though, so
    /// improvements will likely happen here over time.
    stack: Vec<String>,
}

pub struct JsFunction {
    pub code: String,
    pub ts_sig: String,
    pub js_doc: String,
    pub ts_arg_tys: Vec<String>,
    pub ts_ret_ty: Option<String>,
}

impl<'a, 'b> Builder<'a, 'b> {
    pub fn new(cx: &'a mut Context<'b>) -> Builder<'a, 'b> {
        Builder {
            log_error: false,
            cx,
            constructor: None,
            method: None,
            catch: false,
        }
    }

    pub fn method(&mut self, consumed: bool) {
        self.method = Some(consumed);
    }

    pub fn constructor(&mut self, class: &str) {
        self.constructor = Some(class.to_string());
    }

    pub fn catch(&mut self, catch: bool) {
        self.catch = catch;
    }

    pub fn log_error(&mut self, log: bool) {
        self.log_error = log;
    }

    pub fn process(
        &mut self,
        adapter: &Adapter,
        instructions: &[InstructionData],
        explicit_arg_names: &Option<Vec<String>>,
    ) -> Result<JsFunction, Error> {
        if self
            .cx
            .aux
            .imports_with_assert_no_shim
            .contains(&adapter.id)
        {
            bail!("generating a shim for something asserted to have no shim");
        }

        let mut params = adapter.params.iter();
        let mut function_args = Vec::new();
        let mut arg_tys = Vec::new();

        // If this is a method then we're generating this as part of a class
        // method, so the leading parameter is the this pointer stored on
        // the JS object, so synthesize that here.
        let mut js = JsBuilder::new(self.cx);
        match self.method {
            Some(consumes_self) => {
                drop(params.next());
                if js.cx.config.debug {
                    js.prelude(
                        "if (this.ptr == 0) throw new Error('Attempt to use a moved value');\n",
                    );
                }
                if consumes_self {
                    js.prelude("var ptr = this.ptr;");
                    js.prelude("this.ptr = 0;");
                    js.args.push("ptr".to_string());
                } else {
                    js.args.push("this.ptr".to_string());
                }
            }
            None => {}
        }
        for (i, param) in params.enumerate() {
            let arg = match explicit_arg_names {
                Some(list) => list[i].clone(),
                None => format!("arg{}", i),
            };
            js.args.push(arg.clone());
            function_args.push(arg);
            arg_tys.push(param);
        }

        // Translate all instructions, the fun loop!
        //
        // This loop will process all instructions for this adapter function.
        // Each instruction will push/pop from the `js.stack` variable, and will
        // eventually build up the entire `js.prelude` variable with all the JS
        // code that we're going to be adding. Note that the stack at the end
        // represents all returned values.
        //
        // We don't actually manage a literal stack at runtime, but instead we
        // act as more of a compiler to generate straight-line code to make it
        // more JIT-friendly. The generated code should be equivalent to the
        // wasm interface types stack machine, however.
        for instr in instructions {
            instruction(&mut js, &instr.instr, &mut self.log_error)?;
        }

        assert_eq!(js.stack.len(), adapter.results.len());
        match js.stack.len() {
            0 => {}
            1 => {
                let val = js.pop();
                js.prelude(&format!("return {};", val));
            }

            // TODO: this should be pretty trivial to support (commented out
            // code below), but we should be sure to have a test for this
            // somewhere. Currently I don't think it's possible to actually
            // exercise this with just Rust code, so let's wait until we get
            // some tests to enable this path.
            _ => bail!("multi-value returns from adapters not supported yet"),
            // _ => {
            //     let expr = js.stack.join(", ");
            //     js.stack.truncate(0);
            //     js.prelude(&format!("return [{}];", expr));
            // }
        }
        assert!(js.stack.is_empty());

        // // Remove extraneous typescript args which were synthesized and aren't
        // // part of our function shim.
        // while self.ts_args.len() > function_args.len() {
        //     self.ts_args.remove(0);
        // }

        let mut code = String::new();
        code.push_str("(");
        code.push_str(&function_args.join(", "));
        code.push_str(") {\n");

        let mut call = js.prelude;
        if js.finally.len() != 0 {
            call = format!("try {{\n{}}} finally {{\n{}}}\n", call, js.finally);
        }

        if self.catch {
            js.cx.expose_handle_error()?;
            call = format!("try {{\n{}}} catch (e) {{\n handleError(e)\n}}\n", call);
        }

        // Generate a try/catch block in debug mode which handles unexpected and
        // unhandled exceptions, typically used on imports. This currently just
        // logs what happened, but keeps the exception being thrown to propagate
        // elsewhere.
        if self.log_error {
            js.cx.expose_log_error();
            call = format!("try {{\n{}}} catch (e) {{\n logError(e)\n}}\n", call);
        }

        code.push_str(&call);
        code.push_str("}");

        let (ts_sig, ts_arg_tys, ts_ret_ty) =
            self.typescript_signature(&function_args, &arg_tys, &adapter.results);
        let js_doc = self.js_doc_comments(&function_args, &arg_tys, &ts_ret_ty);

        Ok(JsFunction {
            code,
            ts_sig,
            js_doc,
            ts_arg_tys,
            ts_ret_ty,
        })
    }

    /// Returns the typescript signature of the binding that this has described.
    /// This is used to generate all the TypeScript definitions later on.
    ///
    /// Note that the TypeScript returned here is just the argument list and the
    /// return value, it doesn't include the function name in any way.
    fn typescript_signature(
        &self,
        arg_names: &[String],
        arg_tys: &[&AdapterType],
        result_tys: &[AdapterType],
    ) -> (String, Vec<String>, Option<String>) {
        // Build up the typescript signature as well
        let mut omittable = true;
        let mut ts_args = Vec::new();
        let mut ts_arg_tys = Vec::new();
        for (name, ty) in arg_names.iter().zip(arg_tys).rev() {
            // In TypeScript, we can mark optional parameters as omittable
            // using the `?` suffix, but only if they're not followed by
            // non-omittable parameters. Therefore iterate the parameter list
            // in reverse and stop using the `?` suffix for optional params as
            // soon as a non-optional parameter is encountered.
            let mut arg = name.to_string();
            let mut ts = String::new();
            match ty {
                AdapterType::Option(ty) if omittable => {
                    arg.push_str("?: ");
                    adapter2ts(ty, &mut ts);
                }
                ty => {
                    omittable = false;
                    arg.push_str(": ");
                    adapter2ts(ty, &mut ts);
                }
            }
            arg.push_str(&ts);
            ts_arg_tys.push(ts);
            ts_args.push(arg);
        }
        ts_args.reverse();
        ts_arg_tys.reverse();
        let mut ts = format!("({})", ts_args.join(", "));

        // Constructors have no listed return type in typescript
        let mut ts_ret = None;
        if self.constructor.is_none() {
            ts.push_str(": ");
            let mut ret = String::new();
            match result_tys.len() {
                0 => ret.push_str("void"),
                1 => adapter2ts(&result_tys[0], &mut ret),
                _ => ret.push_str("[any]"),
            }
            ts.push_str(&ret);
            ts_ret = Some(ret);
        }
        return (ts, ts_arg_tys, ts_ret);
    }

    /// Returns a helpful JS doc comment which lists types for all parameters
    /// and the return value.
    fn js_doc_comments(
        &self,
        arg_names: &[String],
        arg_tys: &[&AdapterType],
        ts_ret: &Option<String>,
    ) -> String {
        let mut ret = String::new();
        for (name, ty) in arg_names.iter().zip(arg_tys) {
            ret.push_str("@param {");
            adapter2ts(ty, &mut ret);
            ret.push_str("} ");
            ret.push_str(name);
            ret.push_str("\n");
        }
        if let Some(ts) = ts_ret {
            if ts != "void" {
                ret.push_str(&format!("@returns {{{}}}", ts));
            }
        }
        ret
    }
}

impl<'a, 'b> JsBuilder<'a, 'b> {
    pub fn new(cx: &'a mut Context<'b>) -> JsBuilder<'a, 'b> {
        JsBuilder {
            cx,
            args: Vec::new(),
            tmp: 0,
            finally: String::new(),
            prelude: String::new(),
            stack: Vec::new(),
        }
    }

    pub fn arg(&self, idx: u32) -> &str {
        &self.args[idx as usize]
    }

    pub fn prelude(&mut self, prelude: &str) {
        for line in prelude.trim().lines().map(|l| l.trim()) {
            if !line.is_empty() {
                self.prelude.push_str(line);
                self.prelude.push_str("\n");
            }
        }
    }

    pub fn finally(&mut self, finally: &str) {
        for line in finally.trim().lines().map(|l| l.trim()) {
            if !line.is_empty() {
                self.finally.push_str(line);
                self.finally.push_str("\n");
            }
        }
    }

    pub fn tmp(&mut self) -> usize {
        let ret = self.tmp;
        self.tmp += 1;
        return ret;
    }

    fn pop(&mut self) -> String {
        self.stack.pop().unwrap()
    }

    fn push(&mut self, arg: String) {
        self.stack.push(arg);
    }

    fn assert_class(&mut self, arg: &str, class: &str) {
        self.cx.expose_assert_class();
        self.prelude(&format!("_assertClass({}, {});", arg, class));
    }

    fn assert_number(&mut self, arg: &str) {
        if !self.cx.config.debug {
            return;
        }
        self.cx.expose_assert_num();
        self.prelude(&format!("_assertNum({});", arg));
    }

    fn assert_bool(&mut self, arg: &str) {
        if !self.cx.config.debug {
            return;
        }
        self.cx.expose_assert_bool();
        self.prelude(&format!("_assertBoolean({});", arg));
    }

    fn assert_optional_number(&mut self, arg: &str) {
        if !self.cx.config.debug {
            return;
        }
        self.cx.expose_is_like_none();
        self.prelude(&format!("if (!isLikeNone({})) {{", arg));
        self.assert_number(arg);
        self.prelude("}");
    }

    fn assert_optional_bool(&mut self, arg: &str) {
        if !self.cx.config.debug {
            return;
        }
        self.cx.expose_is_like_none();
        self.prelude(&format!("if (!isLikeNone({})) {{", arg));
        self.assert_bool(arg);
        self.prelude("}");
    }

    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,
        ));
    }

    fn string_to_memory(
        &mut self,
        mem: walrus::MemoryId,
        malloc: walrus::FunctionId,
        realloc: Option<walrus::FunctionId>,
    ) -> Result<(), Error> {
        let pass = self.cx.expose_pass_string_to_wasm(mem)?;
        let val = self.pop();
        let malloc = self.cx.export_name_of(malloc);
        let i = self.tmp();
        let realloc = match realloc {
            Some(f) => format!(", wasm.{}", self.cx.export_name_of(f)),
            None => String::new(),
        };
        self.prelude(&format!(
            "var ptr{i} = {f}({0}, wasm.{malloc}{realloc});",
            val,
            i = i,
            f = pass,
            malloc = malloc,
            realloc = realloc,
        ));
        self.prelude(&format!("var len{} = WASM_VECTOR_LEN;", i));
        self.push(format!("ptr{}", i));
        self.push(format!("len{}", i));
        Ok(())
    }
}

fn instruction(js: &mut JsBuilder, instr: &Instruction, log_error: &mut bool) -> Result<(), Error> {
    // Here first properly aligned nonzero address is chosen to be the
    // out-pointer. We use the address for a BigInt64Array sometimes which
    // means it needs to be 8-byte aligned. Otherwise valid code is
    // unlikely to ever be working around address 8, so this should be a
    // safe address to use for returning data through.
    let retptr_val = 8;

    match instr {
        Instruction::Standard(wit_walrus::Instruction::ArgGet(n)) => {
            let arg = js.arg(*n).to_string();
            js.push(arg);
        }

        Instruction::Standard(wit_walrus::Instruction::CallAdapter(_)) => {
            panic!("standard call adapter functions should be mapped to our adapters");
        }

        Instruction::Standard(wit_walrus::Instruction::CallCore(_))
        | Instruction::CallExport(_)
        | Instruction::CallAdapter(_)
        | Instruction::CallTableElement(_)
        | Instruction::Standard(wit_walrus::Instruction::DeferCallCore(_)) => {
            let invoc = Invocation::from(instr, js.cx.module)?;
            let (params, results) = invoc.params_results(js.cx);

            // Pop off the number of parameters for the function we're calling
            let mut args = Vec::new();
            for _ in 0..params {
                args.push(js.pop());
            }
            args.reverse();

            // Call the function through an export of the underlying module.
            let call = invoc.invoke(js.cx, &args, &mut js.prelude, log_error)?;

            // And then figure out how to actually handle where the call
            // happens. This is pretty conditional depending on the number of
            // return values of the function.
            match (invoc.defer(), results) {
                (true, 0) => {
                    js.finally(&format!("{};", call));
                    js.stack.extend(args);
                }
                (true, _) => panic!("deferred calls must have no results"),
                (false, 0) => js.prelude(&format!("{};", call)),
                (false, n) => {
                    js.prelude(&format!("var ret = {};", call));
                    if n == 1 {
                        js.push("ret".to_string());
                    } else {
                        for i in 0..n {
                            js.push(format!("ret[{}]", i));
                        }
                    }
                }
            }
        }

        Instruction::Standard(wit_walrus::Instruction::IntToWasm { trap: false, .. }) => {
            let val = js.pop();
            js.assert_number(&val);
            js.push(val);
        }

        // When converting to a JS number we need to specially handle the `u32`
        // case because if the high bit is set then it comes out as a negative
        // number, but we want to switch that to an unsigned representation.
        Instruction::Standard(wit_walrus::Instruction::WasmToInt {
            trap: false,
            output,
            ..
        }) => {
            let val = js.pop();
            match output {
                wit_walrus::ValType::U32 => js.push(format!("{} >>> 0", val)),
                _ => js.push(val),
            }
        }

        Instruction::Standard(wit_walrus::Instruction::WasmToInt { trap: true, .. })
        | Instruction::Standard(wit_walrus::Instruction::IntToWasm { trap: true, .. }) => {
            bail!("trapping wasm-to-int and int-to-wasm instructions not supported")
        }

        Instruction::Standard(wit_walrus::Instruction::MemoryToString(mem)) => {
            let len = js.pop();
            let ptr = js.pop();
            let get = js.cx.expose_get_string_from_wasm(*mem)?;
            js.push(format!("{}({}, {})", get, ptr, len));
        }

        Instruction::Standard(wit_walrus::Instruction::StringToMemory { mem, malloc }) => {
            js.string_to_memory(*mem, *malloc, None)?;
        }

        Instruction::StringToMemory {
            mem,
            malloc,
            realloc,
        } => {
            js.string_to_memory(*mem, *malloc, *realloc)?;
        }

        Instruction::Retptr => js.stack.push(retptr_val.to_string()),

        Instruction::StoreRetptr { ty, offset, mem } => {
            let (mem, size) = match ty {
                AdapterType::I32 => (js.cx.expose_int32_memory(*mem), 4),
                AdapterType::F32 => (js.cx.expose_f32_memory(*mem), 4),
                AdapterType::F64 => (js.cx.expose_f64_memory(*mem), 8),
                other => bail!("invalid aggregate return type {:?}", other),
            };
            // Note that we always assume the return pointer is argument 0,
            // which is currently the case for LLVM.
            let val = js.pop();
            let expr = format!(
                "{}()[{} / {} + {}] = {};",
                mem,
                js.arg(0),
                size,
                offset,
                val,
            );
            js.prelude(&expr);
        }

        Instruction::LoadRetptr { ty, offset, mem } => {
            let (mem, size) = match ty {
                AdapterType::I32 => (js.cx.expose_int32_memory(*mem), 4),
                AdapterType::F32 => (js.cx.expose_f32_memory(*mem), 4),
                AdapterType::F64 => (js.cx.expose_f64_memory(*mem), 8),
                other => bail!("invalid aggregate return type {:?}", other),
            };
            // If we're loading from the return pointer then we must have pushed
            // it earlier, and we always push the same value, so load that value
            // here
            let expr = format!("{}()[{} / {} + {}]", mem, retptr_val, size, offset);
            js.prelude(&format!("var r{} = {};", offset, expr));
            js.push(format!("r{}", offset));
        }

        Instruction::I32FromBool => {
            let val = js.pop();
            js.assert_bool(&val);
            // JS will already coerce booleans into numbers for us
            js.push(val);
        }

        Instruction::I32FromStringFirstChar => {
            let val = js.pop();
            js.push(format!("{}.codePointAt(0)", val));
        }

        Instruction::I32FromAnyrefOwned => {
            js.cx.expose_add_heap_object();
            let val = js.pop();
            js.push(format!("addHeapObject({})", val));
        }

        Instruction::I32FromAnyrefBorrow => {
            js.cx.expose_borrowed_objects();
            js.cx.expose_global_stack_pointer();
            let val = js.pop();
            js.push(format!("addBorrowedObject({})", val));
            js.finally("heap[stack_pointer++] = undefined;");
        }

        Instruction::I32FromAnyrefRustOwned { class } => {
            let val = js.pop();
            js.assert_class(&val, &class);
            js.assert_not_moved(&val);
            let i = js.tmp();
            js.prelude(&format!("var ptr{} = {}.ptr;", i, val));
            js.prelude(&format!("{}.ptr = 0;", val));
            js.push(format!("ptr{}", i));
        }

        Instruction::I32FromAnyrefRustBorrow { class } => {
            let val = js.pop();
            js.assert_class(&val, &class);
            js.assert_not_moved(&val);
            js.push(format!("{}.ptr", val));
        }

        Instruction::I32FromOptionRust { class } => {
            let val = js.pop();
            js.cx.expose_is_like_none();
            let i = js.tmp();
            js.prelude(&format!("let ptr{} = 0;", i));
            js.prelude(&format!("if (!isLikeNone({0})) {{", val));
            js.assert_class(&val, class);
            js.assert_not_moved(&val);
            js.prelude(&format!("ptr{} = {}.ptr;", i, val));
            js.prelude(&format!("{}.ptr = 0;", val));
            js.prelude("}");
            js.push(format!("ptr{}", i));
        }

        Instruction::I32Split64 { signed } => {
            let val = js.pop();
            let f = if *signed {
                js.cx.expose_int64_cvt_shim()
            } else {
                js.cx.expose_uint64_cvt_shim()
            };
            let i = js.tmp();
            js.prelude(&format!(
                "
                 {f}[0] = {val};
                 const low{i} = u32CvtShim[0];
                 const high{i} = u32CvtShim[1];
                 ",
                i = i,
                f = f,
                val = val,
            ));
            js.push(format!("low{}", i));
            js.push(format!("high{}", i));
        }

        Instruction::I32SplitOption64 { signed } => {
            let val = js.pop();
            js.cx.expose_is_like_none();
            let f = if *signed {
                js.cx.expose_int64_cvt_shim()
            } else {
                js.cx.expose_uint64_cvt_shim()
            };
            let i = js.tmp();
            js.prelude(&format!(
                "\
                    {f}[0] = isLikeNone({val}) ? BigInt(0) : {val};
                    const low{i} = u32CvtShim[0];
                    const high{i} = u32CvtShim[1];
                ",
                i = i,
                f = f,
                val = val,
            ));
            js.push(format!("!isLikeNone({0})", val));
            js.push(format!("low{}", i));
            js.push(format!("high{}", i));
        }

        Instruction::I32FromOptionAnyref { table_and_alloc } => {
            let val = js.pop();
            js.cx.expose_is_like_none();
            match table_and_alloc {
                Some((table, alloc)) => {
                    let alloc = js.cx.expose_add_to_anyref_table(*table, *alloc)?;
                    js.push(format!("isLikeNone({0}) ? 0 : {1}({0})", val, alloc));
                }
                None => {
                    js.cx.expose_add_heap_object();
                    js.push(format!("isLikeNone({0}) ? 0 : addHeapObject({0})", val));
                }
            }
        }

        Instruction::I32FromOptionU32Sentinel => {
            let val = js.pop();
            js.cx.expose_is_like_none();
            js.assert_optional_number(&val);
            js.push(format!("isLikeNone({0}) ? 0xFFFFFF : {0}", val));
        }

        Instruction::I32FromOptionBool => {
            let val = js.pop();
            js.cx.expose_is_like_none();
            js.assert_optional_bool(&val);
            js.push(format!("isLikeNone({0}) ? 0xFFFFFF : {0} ? 1 : 0", val));
        }

        Instruction::I32FromOptionChar => {
            let val = js.pop();
            js.cx.expose_is_like_none();
            js.push(format!(
                "isLikeNone({0}) ? 0xFFFFFF : {0}.codePointAt(0)",
                val
            ));
        }

        Instruction::I32FromOptionEnum { hole } => {
            let val = js.pop();
            js.cx.expose_is_like_none();
            js.assert_optional_number(&val);
            js.push(format!("isLikeNone({0}) ? {1} : {0}", val, hole));
        }

        Instruction::FromOptionNative { .. } => {
            let val = js.pop();
            js.cx.expose_is_like_none();
            js.assert_optional_number(&val);
            js.push(format!("!isLikeNone({0})", val));
            js.push(format!("isLikeNone({0}) ? 0 : {0}", val));
        }

        Instruction::VectorToMemory { kind, malloc, mem } => {
            let val = js.pop();
            let func = js.cx.pass_to_wasm_function(*kind, *mem)?;
            let malloc = js.cx.export_name_of(*malloc);
            let i = js.tmp();
            js.prelude(&format!(
                "var ptr{i} = {f}({0}, wasm.{malloc});",
                val,
                i = i,
                f = func,
                malloc = malloc,
            ));
            js.prelude(&format!("var len{} = WASM_VECTOR_LEN;", i));
            js.push(format!("ptr{}", i));
            js.push(format!("len{}", i));
        }

        Instruction::OptionString {
            mem,
            malloc,
            realloc,
        } => {
            let func = js.cx.expose_pass_string_to_wasm(*mem)?;
            js.cx.expose_is_like_none();
            let i = js.tmp();
            let malloc = js.cx.export_name_of(*malloc);
            let val = js.pop();
            let realloc = match realloc {
                Some(f) => format!(", wasm.{}", js.cx.export_name_of(*f)),
                None => String::new(),
            };
            js.prelude(&format!(
                "var ptr{i} = isLikeNone({0}) ? 0 : {f}({0}, wasm.{malloc}{realloc});",
                val,
                i = i,
                f = func,
                malloc = malloc,
                realloc = realloc,
            ));
            js.prelude(&format!("var len{} = WASM_VECTOR_LEN;", i));
            js.push(format!("ptr{}", i));
            js.push(format!("len{}", i));
        }

        Instruction::OptionVector { kind, mem, malloc } => {
            let func = js.cx.pass_to_wasm_function(*kind, *mem)?;
            js.cx.expose_is_like_none();
            let i = js.tmp();
            let malloc = js.cx.export_name_of(*malloc);
            let val = js.pop();
            js.prelude(&format!(
                "var ptr{i} = isLikeNone({0}) ? 0 : {f}({0}, wasm.{malloc});",
                val,
                i = i,
                f = func,
                malloc = malloc,
            ));
            js.prelude(&format!("var len{} = WASM_VECTOR_LEN;", i));
            js.push(format!("ptr{}", i));
            js.push(format!("len{}", i));
        }

        Instruction::MutableSliceToMemory {
            kind,
            malloc,
            mem,
            free,
        } => {
            // First up, pass the JS value into wasm, getting out a pointer and
            // a length. These two pointer/length values get pushed onto the
            // value stack.
            let val = js.pop();
            let func = js.cx.pass_to_wasm_function(*kind, *mem)?;
            let malloc = js.cx.export_name_of(*malloc);
            let i = js.tmp();
            js.prelude(&format!(
                "var ptr{i} = {f}({val}, wasm.{malloc});",
                val = val,
                i = i,
                f = func,
                malloc = malloc,
            ));
            js.prelude(&format!("var len{} = WASM_VECTOR_LEN;", i));
            js.push(format!("ptr{}", i));
            js.push(format!("len{}", i));

            // Next we set up a `finally` clause which will both update the
            // original mutable slice with any modifications, and then free the
            // Rust-backed memory.
            let free = js.cx.export_name_of(*free);
            let get = js.cx.memview_function(*kind, *mem);
            js.finally(&format!(
                "
                    {val}.set({get}().subarray(ptr{i} / {size}, ptr{i} / {size} + len{i}));
                    wasm.{free}(ptr{i}, len{i} * {size});
                ",
                val = val,
                get = get,
                free = free,
                size = kind.size(),
                i = i,
            ));
        }

        Instruction::BoolFromI32 => {
            let val = js.pop();
            js.push(format!("{} !== 0", val));
        }

        Instruction::AnyrefLoadOwned => {
            js.cx.expose_take_object();
            let val = js.pop();
            js.push(format!("takeObject({})", val));
        }

        Instruction::StringFromChar => {
            let val = js.pop();
            js.push(format!("String.fromCodePoint({})", val));
        }

        Instruction::I64FromLoHi { signed } => {
            let f = if *signed {
                js.cx.expose_int64_cvt_shim()
            } else {
                js.cx.expose_uint64_cvt_shim()
            };
            let i = js.tmp();
            let high = js.pop();
            let low = js.pop();
            js.prelude(&format!(
                "\
                     u32CvtShim[0] = {low};
                     u32CvtShim[1] = {high};
                     const n{i} = {f}[0];
                 ",
                low = low,
                high = high,
                f = f,
                i = i,
            ));
            js.push(format!("n{}", i))
        }

        Instruction::RustFromI32 { class } => {
            js.cx.require_class_wrap(class);
            let val = js.pop();
            js.push(format!("{}.__wrap({})", class, val));
        }

        Instruction::OptionRustFromI32 { class } => {
            js.cx.require_class_wrap(class);
            let val = js.pop();
            js.push(format!(
                "{0} === 0 ? undefined : {1}.__wrap({0})",
                val, class,
            ))
        }

        Instruction::CachedStringLoad {
            owned,
            optional: _,
            mem,
            free,
        } => {
            let len = js.pop();
            let ptr = js.pop();
            let tmp = js.tmp();

            let get = js.cx.expose_get_cached_string_from_wasm(*mem)?;

            js.prelude(&format!("var v{} = {}({}, {});", tmp, get, ptr, len));

            if *owned {
                let free = js.cx.export_name_of(*free);
                js.prelude(&format!(
                    "if ({ptr} !== 0) {{ wasm.{}({ptr}, {len}); }}",
                    free,
                    ptr = ptr,
                    len = len,
                ));
            }

            js.push(format!("v{}", tmp));
        }

        Instruction::TableGet => {
            let val = js.pop();
            js.cx.expose_get_object();
            js.push(format!("getObject({})", val));
        }

        Instruction::StackClosure {
            adapter,
            nargs,
            mutable,
        } => {
            let i = js.tmp();
            let b = js.pop();
            let a = js.pop();
            js.prelude(&format!("var state{} = {{a: {}, b: {}}};", i, a, b));
            let args = (0..*nargs)
                .map(|i| format!("arg{}", i))
                .collect::<Vec<_>>()
                .join(", ");
            let wrapper = js.cx.adapter_name(*adapter);
            if *mutable {
                // Mutable closures need protection against being called
                // recursively, so ensure that we clear out one of the
                // internal pointers while it's being invoked.
                js.prelude(&format!(
                    "var cb{i} = ({args}) => {{
                        const a = state{i}.a;
                        state{i}.a = 0;
                        try {{
                            return {name}(a, state{i}.b, {args});
                        }} finally {{
                            state{i}.a = a;
                        }}
                    }};",
                    i = i,
                    args = args,
                    name = wrapper,
                ));
            } else {
                js.prelude(&format!(
                    "var cb{i} = ({args}) => {wrapper}(state{i}.a, state{i}.b, {args});",
                    i = i,
                    args = args,
                    wrapper = wrapper,
                ));
            }

            // Make sure to null out our internal pointers when we return
            // back to Rust to ensure that any lingering references to the
            // closure will fail immediately due to null pointers passed in
            // to Rust.
            js.finally(&format!("state{}.a = state{0}.b = 0;", i));
            js.push(format!("cb{}", i));
        }

        Instruction::VectorLoad { kind, mem, free } => {
            let len = js.pop();
            let ptr = js.pop();
            let f = js.cx.expose_get_vector_from_wasm(*kind, *mem)?;
            let i = js.tmp();
            let free = js.cx.export_name_of(*free);
            js.prelude(&format!("var v{} = {}({}, {}).slice();", i, f, ptr, len));
            js.prelude(&format!(
                "wasm.{}({}, {} * {});",
                free,
                ptr,
                len,
                kind.size()
            ));
            js.push(format!("v{}", i))
        }

        Instruction::OptionVectorLoad { kind, mem, free } => {
            let len = js.pop();
            let ptr = js.pop();
            let f = js.cx.expose_get_vector_from_wasm(*kind, *mem)?;
            let i = js.tmp();
            let free = js.cx.export_name_of(*free);
            js.prelude(&format!("let v{};", i));
            js.prelude(&format!("if ({} !== 0) {{", ptr));
            js.prelude(&format!("v{} = {}({}, {}).slice();", i, f, ptr, len));
            js.prelude(&format!(
                "wasm.{}({}, {} * {});",
                free,
                ptr,
                len,
                kind.size()
            ));
            js.prelude("}");
            js.push(format!("v{}", i));
        }

        Instruction::View { kind, mem } => {
            let len = js.pop();
            let ptr = js.pop();
            let f = js.cx.expose_get_vector_from_wasm(*kind, *mem)?;
            js.push(format!("{f}({ptr}, {len})", ptr = ptr, len = len, f = f));
        }

        Instruction::OptionView { kind, mem } => {
            let len = js.pop();
            let ptr = js.pop();
            let f = js.cx.expose_get_vector_from_wasm(*kind, *mem)?;
            js.push(format!(
                "{ptr} === 0 ? undefined : {f}({ptr}, {len})",
                ptr = ptr,
                len = len,
                f = f
            ));
        }

        Instruction::OptionU32Sentinel => {
            let val = js.pop();
            js.push(format!("{0} === 0xFFFFFF ? undefined : {0}", val));
        }

        Instruction::ToOptionNative { ty: _, signed } => {
            let val = js.pop();
            let present = js.pop();
            js.push(format!(
                "{} === 0 ? undefined : {}{}",
                present,
                val,
                if *signed { "" } else { " >>> 0" },
            ));
        }

        Instruction::OptionBoolFromI32 => {
            let val = js.pop();
            js.push(format!("{0} === 0xFFFFFF ? undefined : {0} !== 0", val));
        }

        Instruction::OptionCharFromI32 => {
            let val = js.pop();
            js.push(format!(
                "{0} === 0xFFFFFF ? undefined : String.fromCodePoint({0})",
                val,
            ));
        }

        Instruction::OptionEnumFromI32 { hole } => {
            let val = js.pop();
            js.push(format!("{0} === {1} ? undefined : {0}", val, hole));
        }

        Instruction::Option64FromI32 { signed } => {
            let f = if *signed {
                js.cx.expose_int64_cvt_shim()
            } else {
                js.cx.expose_uint64_cvt_shim()
            };
            let i = js.tmp();
            let high = js.pop();
            let low = js.pop();
            let present = js.pop();
            js.prelude(&format!(
                "
                    u32CvtShim[0] = {low};
                    u32CvtShim[1] = {high};
                    const n{i} = {present} === 0 ? undefined : {f}[0];
                ",
                present = present,
                low = low,
                high = high,
                f = f,
                i = i,
            ));
            js.push(format!("n{}", i));
        }
    }
    Ok(())
}

enum Invocation {
    Core { id: walrus::FunctionId, defer: bool },
    Adapter(AdapterId),
}

impl Invocation {
    fn from(instr: &Instruction, module: &Module) -> Result<Invocation, Error> {
        use Instruction::*;
        Ok(match instr {
            Standard(wit_walrus::Instruction::CallCore(f)) => Invocation::Core {
                id: *f,
                defer: false,
            },

            Standard(wit_walrus::Instruction::DeferCallCore(f)) => Invocation::Core {
                id: *f,
                defer: true,
            },

            CallExport(e) => match module.exports.get(*e).item {
                walrus::ExportItem::Function(id) => Invocation::Core { id, defer: false },
                _ => panic!("can only call exported function"),
            },

            // The function table never changes right now, so we can statically
            // look up the desired function.
            CallTableElement(idx) => {
                let table = module
                    .tables
                    .main_function_table()?
                    .ok_or_else(|| anyhow!("no function table found"))?;
                let functions = match &module.tables.get(table).kind {
                    walrus::TableKind::Function(f) => f,
                    _ => bail!("should have found a function table"),
                };
                let id = functions
                    .elements
                    .get(*idx as usize)
                    .and_then(|id| *id)
                    .ok_or_else(|| anyhow!("function table wasn't filled in a {}", idx))?;
                Invocation::Core { id, defer: false }
            }

            CallAdapter(id) => Invocation::Adapter(*id),

            // this function is only called for the above instructions
            _ => unreachable!(),
        })
    }

    fn params_results(&self, cx: &Context) -> (usize, usize) {
        match self {
            Invocation::Core { id, .. } => {
                let ty = cx.module.funcs.get(*id).ty();
                let ty = cx.module.types.get(ty);
                (ty.params().len(), ty.results().len())
            }
            Invocation::Adapter(id) => {
                let adapter = &cx.wit.adapters[id];
                (adapter.params.len(), adapter.results.len())
            }
        }
    }

    fn invoke(
        &self,
        cx: &mut Context,
        args: &[String],
        prelude: &mut String,
        log_error: &mut bool,
    ) -> Result<String, Error> {
        match self {
            Invocation::Core { id, .. } => {
                let name = cx.export_name_of(*id);
                Ok(format!("wasm.{}({})", name, args.join(", ")))
            }
            Invocation::Adapter(id) => {
                let adapter = &cx.wit.adapters[id];
                let kind = match adapter.kind {
                    AdapterKind::Import { kind, .. } => kind,
                    AdapterKind::Local { .. } => {
                        bail!("adapter-to-adapter calls not supported yet");
                    }
                };
                let import = &cx.aux.import_map[id];
                let variadic = cx.aux.imports_with_variadic.contains(id);
                if cx.import_never_log_error(import) {
                    *log_error = false;
                }
                cx.invoke_import(import, kind, args, variadic, prelude)
            }
        }
    }

    fn defer(&self) -> bool {
        match self {
            Invocation::Core { defer, .. } => *defer,
            _ => false,
        }
    }
}

fn adapter2ts(ty: &AdapterType, dst: &mut String) {
    match ty {
        AdapterType::I32
        | AdapterType::S8
        | AdapterType::S16
        | AdapterType::S32
        | AdapterType::U8
        | AdapterType::U16
        | AdapterType::U32
        | AdapterType::F32
        | AdapterType::F64 => dst.push_str("number"),
        AdapterType::I64 | AdapterType::S64 | AdapterType::U64 => dst.push_str("BigInt"),
        AdapterType::String => dst.push_str("string"),
        AdapterType::Anyref => dst.push_str("any"),
        AdapterType::Bool => dst.push_str("boolean"),
        AdapterType::Vector(kind) => dst.push_str(kind.js_ty()),
        AdapterType::Option(ty) => {
            adapter2ts(ty, dst);
            dst.push_str(" | undefined");
        }
        AdapterType::Struct(name) => dst.push_str(name),
        AdapterType::Function => dst.push_str("any"),
    }
}