//! 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::descriptor::VectorKind;
use crate::js::Context;
use crate::wit::InstructionData;
use crate::wit::{
    Adapter, AdapterId, AdapterKind, AdapterType, AuxFunctionArgumentData, Instruction,
};
use anyhow::{bail, Error};
use std::collections::HashSet;
use std::fmt::Write;
use walrus::{Module, ValType};

/// 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 this is a classless this function (receives JS `this` as first param)
    classless_this: 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>,

    /// A debug name for the function being generated, used for error messages
    debug_name: &'a str,

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

    /// Code which should go before the `try {` in a try-finally block.
    pre_try: 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_doc: String,
    pub ts_arg_tys: Vec<String>,
    pub ts_ret_ty: Option<String>,
    pub ts_refs: HashSet<TsReference>,
    /// Whether this function has a single optional argument.
    ///
    /// If the function is a setter, that means that the field it sets is optional.
    pub might_be_optional_field: bool,
    pub catch: bool,
    pub log_error: bool,
}

/// A references to an (likely) exported symbol used in TS type expression.
///
/// Right now, only string enum require this type of analysis.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum TsReference {
    StringEnum(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,
            classless_this: false,
            catch: false,
        }
    }

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

    pub fn classless_this(&mut self) {
        self.classless_this = true;
    }

    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],
        args_data: &Option<Vec<AuxFunctionArgumentData>>,
        asyncness: bool,
        variadic: bool,
        generate_jsdoc: bool,
        debug_name: &str,
        ret_ty_override: &Option<String>,
        ret_desc: &Option<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, debug_name);
        if let Some(consumes_self) = self.method {
            let _ = params.next();
            if js.cx.config.generate_reset_state {
                js.prelude(
                    "
                    if (this.__wbg_inst !== undefined && this.__wbg_inst !== __wbg_instance_id) {
                        throw new Error('Invalid stale object from previous Wasm instance');
                    }
                    ",
                );
            }
            if js.cx.config.debug {
                js.prelude(
                    "if (this.__wbg_ptr == 0) throw new Error('Attempt to use a moved value');",
                );
            }
            if consumes_self {
                js.prelude("const ptr = this.__destroy_into_raw();");
                js.args.push("ptr".into());
            } else {
                js.args.push("this.__wbg_ptr".into());
            }
        } else if self.classless_this {
            let _ = params.next();
            js.args.push("this".into());
        }
        for (i, param) in params.enumerate() {
            let arg = match args_data {
                Some(list) => list[i].clone(),
                None => AuxFunctionArgumentData {
                    name: format!("arg{i}"),
                    ty_override: None,
                    desc: None,
                },
            };
            js.args.push(arg.name.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,
                &self.constructor,
            )?;
        }

        assert_eq!(
            js.stack.len(),
            adapter.results.len(),
            "stack size mismatch for {debug_name}"
        );
        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('(');
        for (i, v) in function_args.iter().enumerate() {
            if i != 0 {
                code.push_str(", ");
            }

            if variadic && i == function_args.len() - 1 {
                code.push_str("...");
            }

            code.push_str(&v.name);
        }
        code.push_str(") {\n");

        let call = if !js.finally.is_empty() {
            format!(
                "{}try {{\n{}}} finally {{\n{}}}\n",
                js.pre_try, js.prelude, js.finally
            )
        } else {
            js.pre_try + &js.prelude
        };

        if self.catch {
            js.cx.expose_handle_error()?;
        }

        // 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();
        }

        code.push_str(&call);
        code.push('}');

        // Rust Structs' fields converted into Getter and Setter functions before
        // we decode them from webassembly, finding if a function is a field
        // should start from here. Struct fields(Getter) only have one arg, and
        // this is the clue we can infer if a function might be a field.
        let mut might_be_optional_field = false;
        let (ts_sig, ts_arg_tys, ts_ret_ty, ts_refs) = self.typescript_signature(
            &function_args,
            &arg_tys,
            &adapter.inner_results,
            &mut might_be_optional_field,
            asyncness,
            variadic,
            ret_ty_override,
        );
        let js_doc = if generate_jsdoc {
            self.js_doc_comments(
                &function_args,
                &arg_tys,
                &ts_ret_ty,
                variadic,
                ret_ty_override,
                ret_desc,
            )
        } else {
            String::new()
        };

        // generate ts_doc
        // ts doc is slightly different than js doc, where there is no
        // arguments types followed after @param tag, as well as no special
        // casings for arguments names such as "@param {string} [arg]" that
        // tags the argument as optional, for ts doc we only need arg names
        // and rest are just derived from function ts signature
        let ts_doc = self.ts_doc_comments(&function_args, ret_desc);

        Ok(JsFunction {
            code,
            ts_sig,
            js_doc,
            ts_doc,
            ts_arg_tys,
            ts_ret_ty,
            ts_refs,
            might_be_optional_field,
            catch: self.catch,
            log_error: self.log_error,
        })
    }

    /// 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,
        args_data: &[AuxFunctionArgumentData],
        arg_tys: &[&AdapterType],
        result_tys: &[AdapterType],
        might_be_optional_field: &mut bool,
        asyncness: bool,
        variadic: bool,
        ret_ty_override: &Option<String>,
    ) -> (String, Vec<String>, Option<String>, HashSet<TsReference>) {
        // Build up the typescript signature as well
        let mut omittable = true;
        let mut ts_args = Vec::new();
        let mut ts_arg_tys = Vec::new();
        let mut ts_refs = HashSet::new();
        for (
            AuxFunctionArgumentData {
                name, ty_override, ..
            },
            ty,
        ) in args_data.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();
            if let Some(v) = ty_override {
                omittable = false;
                arg.push_str(": ");
                ts.push_str(v);
            } else {
                match ty {
                    AdapterType::Option(ty) if omittable => {
                        // e.g. `foo?: string | null`
                        arg.push_str("?: ");
                        adapter2ts(ty, TypePosition::Argument, &mut ts, Some(&mut ts_refs));
                        ts.push_str(" | null");
                    }
                    ty => {
                        adapter2ts(ty, TypePosition::Argument, &mut ts, Some(&mut ts_refs));
                        omittable = false;
                        arg.push_str(": ");
                    }
                }
            }
            arg.push_str(&ts);
            ts_arg_tys.push(ts);
            ts_args.push(arg);
        }
        ts_args.reverse();
        ts_arg_tys.reverse();
        let mut ts = String::from("(");
        if variadic {
            if let Some((last, non_variadic_args)) = ts_args.split_last() {
                ts.push_str(&non_variadic_args.join(", "));
                if !non_variadic_args.is_empty() {
                    ts.push_str(", ");
                }
                ts.push_str((String::from("...") + last).as_str())
            }
        } else {
            ts.push_str(&ts_args.join(", "));
        };
        ts.push(')');

        // If this function is an optional field's setter, it should have only
        // one arg, and omittable should be `true`.
        if ts_args.len() == 1 && omittable {
            *might_be_optional_field = true;
        }

        // 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();
            if let Some(v) = &ret_ty_override {
                ret.push_str(v);
            } else {
                match result_tys.len() {
                    0 => ret.push_str("void"),
                    1 => adapter2ts(
                        &result_tys[0],
                        TypePosition::Return,
                        &mut ret,
                        Some(&mut ts_refs),
                    ),
                    _ => ret.push_str("[any]"),
                }
            }
            if asyncness {
                ret = format!("Promise<{ret}>");
            }
            ts.push_str(&ret);
            ts_ret = Some(ret);
        }
        (ts, ts_arg_tys, ts_ret, ts_refs)
    }

    /// Returns a helpful JS doc comment which lists types for all parameters
    /// and the return value.
    fn js_doc_comments(
        &self,
        args_data: &[AuxFunctionArgumentData],
        arg_tys: &[&AdapterType],
        ts_ret: &Option<String>,
        variadic: bool,
        ret_ty_override: &Option<String>,
        ret_desc: &Option<String>,
    ) -> String {
        let (variadic_arg, fn_arg_names) = match args_data.split_last() {
            Some((last, args)) if variadic => (Some(last), args),
            _ => (None, args_data),
        };

        let mut omittable = true;
        let mut js_doc_args = Vec::new();

        for (
            AuxFunctionArgumentData {
                name,
                ty_override,
                desc,
            },
            ty,
        ) in fn_arg_names.iter().zip(arg_tys).rev()
        {
            let mut arg = "@param {".to_string();

            if let Some(v) = ty_override {
                omittable = false;
                arg.push_str(v);
                arg.push_str("} ");
                arg.push_str(name);
            } else {
                match ty {
                    AdapterType::Option(ty) if omittable => {
                        adapter2ts(ty, TypePosition::Argument, &mut arg, None);
                        arg.push_str(" | null} ");
                        arg.push('[');
                        arg.push_str(name);
                        arg.push(']');
                    }
                    _ => {
                        omittable = false;
                        adapter2ts(ty, TypePosition::Argument, &mut arg, None);
                        arg.push_str("} ");
                        arg.push_str(name);
                    }
                }
            }
            // append description
            if let Some(v) = desc {
                arg.push_str(" - ");
                arg.push_str(v);
            }
            arg.push('\n');
            js_doc_args.push(arg);
        }

        let mut ret: String = js_doc_args.into_iter().rev().collect();

        if let (
            Some(AuxFunctionArgumentData {
                name,
                ty_override,
                desc,
            }),
            Some(ty),
        ) = (variadic_arg, arg_tys.last())
        {
            ret.push_str("@param {...");
            if let Some(v) = ty_override {
                ret.push_str(v);
            } else {
                adapter2ts(ty, TypePosition::Argument, &mut ret, None);
            }
            ret.push_str("} ");
            ret.push_str(name);

            // append desc
            if let Some(v) = desc {
                ret.push_str(" - ");
                ret.push_str(v);
            }
            ret.push('\n');
        }
        if let Some(ts) = ret_ty_override.as_ref().or(ts_ret.as_ref()) {
            // skip if type is void and there is no description
            if ts != "void" || ret_desc.is_some() {
                ret.push_str(&format!("@returns {{{ts}}}"));
            }
            // append return description
            if let Some(v) = ret_desc {
                ret.push(' ');
                ret.push_str(v);
            }
        }
        ret
    }

    /// Returns a helpful TS doc comment which lists all parameters and
    /// the return value descriptions.
    fn ts_doc_comments(
        &self,
        args_data: &[AuxFunctionArgumentData],
        ret_desc: &Option<String>,
    ) -> String {
        let mut ts_doc = String::new();
        // ofc we dont need arg type for ts doc, only arg name
        for AuxFunctionArgumentData { name, desc, .. } in args_data.iter() {
            ts_doc.push_str("@param ");
            ts_doc.push_str(name);

            // append desc
            if let Some(v) = desc {
                ts_doc.push_str(" - ");
                ts_doc.push_str(v);
            }
            ts_doc.push('\n');
        }

        // only if there is return description, as we dont want empty @return tag
        if let Some(ret_desc) = ret_desc {
            ts_doc.push_str("@returns ");
            ts_doc.push_str(ret_desc);
        }
        ts_doc
    }
}

impl<'a, 'b> JsBuilder<'a, 'b> {
    pub fn new(cx: &'a mut Context<'b>, debug_name: &'a str) -> JsBuilder<'a, 'b> {
        JsBuilder {
            cx,
            debug_name,
            args: Vec::new(),
            tmp: 0,
            pre_try: String::new(),
            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('\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('\n');
            }
        }
    }

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

    fn pop(&mut self) -> String {
        match self.stack.pop() {
            Some(s) => s,
            None => panic!("popping an empty stack in {}", self.debug_name),
        }
    }

    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_bigint(&mut self, arg: &str) {
        if !self.cx.config.debug {
            return;
        }
        self.cx.expose_assert_bigint();
        self.prelude(&format!("_assertBigInt({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_non_null(&mut self, arg: &str) {
        self.cx.expose_assert_non_null();
        self.prelude(&format!("_assertNonNull({arg});"));
    }

    fn assert_char(&mut self, arg: &str) {
        self.cx.expose_assert_char();
        self.prelude(&format!("_assertChar({arg});"));
    }

    fn assert_optional_bigint(&mut self, arg: &str) {
        if !self.cx.config.debug {
            return;
        }
        self.cx.expose_is_like_none();
        self.prelude(&format!("if (!isLikeNone({arg})) {{"));
        self.assert_bigint(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.generate_reset_state {
            // Under reset state, we need comprehensive validation
            self.prelude(&format!(
                "\
                if (({arg}).__wbg_inst !== undefined && ({arg}).__wbg_inst !== __wbg_instance_id) {{
                    throw new Error('Invalid stale object from previous Wasm instance');
                }}
                "
            ));
        }
        if self.cx.config.debug {
            // Debug mode only checks for moved values
            self.prelude(&format!(
                "\
                if ({arg}.__wbg_ptr === 0) {{
                    throw new Error('Attempt to use a moved value');
                }}
                ",
            ));
        }
    }

    fn string_to_memory(
        &mut self,
        mem: walrus::MemoryId,
        malloc: walrus::FunctionId,
        realloc: Option<walrus::FunctionId>,
    ) {
        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!(
            "const ptr{i} = {pass}({val}, wasm.{malloc}{realloc});",
        ));
        self.prelude(&format!("const len{i} = WASM_VECTOR_LEN;"));
        self.push(format!("ptr{i}"));
        self.push(format!("len{i}"));
    }
}

fn instruction(
    js: &mut JsBuilder,
    instr: &Instruction,
    log_error: &mut bool,
    constructor: &Option<String>,
) -> Result<(), Error> {
    fn wasm_to_string_enum(name: &str, index: &str) -> String {
        // e.g. ["a","b","c"][someIndex]
        format!("__wbindgen_enum_{name}[{index}]")
    }
    fn string_enum_to_wasm(name: &str, invalid: u32, enum_val: &str) -> String {
        // e.g. (["a","b","c"].indexOf(someEnumVal) + 1 || 4) - 1
        //                                                 |
        //                                           invalid + 1
        //
        // The idea is that `indexOf` returns -1 if someEnumVal is invalid,
        // and with +1 we get 0 which is falsey, so we can use || to
        // substitute invalid+1. Finally, we just do -1 to get the correct
        // values for everything.
        format!(
            "(__wbindgen_enum_{name}.indexOf({enum_val}) + 1 || {invalid}) - 1",
            invalid = invalid + 1
        )
    }

    fn int128_to_int64x2(val: &str) -> (String, String) {
        // we don't need to perform any conversion here, because the JS
        // WebAssembly API will automatically convert the bigints to 64 bits
        // for us. This even allows us to ignore signedness.
        let low = val.to_owned();
        let high = format!("{val} >> BigInt(64)");
        (low, high)
    }
    fn int64x2_to_int128(low: String, high: String, signed: bool) -> String {
        let low = format!("BigInt.asUintN(64, {low})");
        if signed {
            format!("({low} | ({high} << BigInt(64)))")
        } else {
            format!("({low} | (BigInt.asUintN(64, {high}) << BigInt(64)))")
        }
    }

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

        Instruction::CallExport(_)
        | Instruction::CallAdapter(_)
        | Instruction::DeferFree { .. } => {
            let invoc = Invocation::from(instr, js.cx.module);
            let (mut params, results) = invoc.params_results(js.cx);

            let mut args = Vec::new();
            let tmp = js.tmp();
            if invoc.defer() {
                if let Instruction::DeferFree { .. } = instr {
                    // Ignore `free`'s final `align` argument, since that's manually inserted later.
                    params -= 1;
                }
                // If the call is deferred, the arguments to the function still need to be
                // accessible in the `finally` block, so we declare variables to hold the args
                // outside of the try-finally block and then set those to the args.
                for (i, arg) in js.stack[js.stack.len() - params..].iter().enumerate() {
                    let name = format!("deferred{tmp}_{i}");
                    writeln!(js.pre_try, "let {name};").unwrap();
                    writeln!(js.prelude, "{name} = {arg};").unwrap();
                    args.push(name);
                }
                if let Instruction::DeferFree { align, .. } = instr {
                    // add alignment
                    args.push(align.to_string());
                }
            } else {
                // Otherwise, pop off the number of parameters for the function we're calling.
                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};"));
                }
                (true, _) => panic!("deferred calls must have no results"),
                (false, 0) => js.prelude(&format!("{call};")),
                (false, n) => {
                    js.prelude(&format!("const ret = {call};"));
                    if n == 1 {
                        js.push("ret".to_string());
                    } else {
                        for i in 0..n {
                            js.push(format!("ret[{i}]"));
                        }
                    }
                }
            }
        }

        Instruction::Int32ToWasm => {
            let val = js.pop();
            js.assert_number(&val);
            js.push(val);
        }
        Instruction::WasmToInt32 { unsigned_32 } => {
            let val = js.pop();
            if *unsigned_32 {
                // 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.
                js.push(format!("{val} >>> 0"))
            } else {
                js.push(val)
            }
        }

        Instruction::Int64ToWasm => {
            let val = js.pop();
            js.assert_bigint(&val);
            js.push(val);
        }
        Instruction::WasmToInt64 { unsigned } => {
            let val = js.pop();
            if *unsigned {
                js.push(format!("BigInt.asUintN(64, {val})"))
            } else {
                js.push(val)
            }
        }

        Instruction::Int128ToWasm => {
            let val = js.pop();
            js.assert_bigint(&val);
            let (low, high) = int128_to_int64x2(&val);
            js.push(low);
            js.push(high);
        }
        Instruction::WasmToInt128 { signed } => {
            let high = js.pop();
            let low = js.pop();
            js.push(int64x2_to_int128(low, high, *signed));
        }

        Instruction::OptionInt128ToWasm => {
            let val = js.pop();
            js.cx.expose_is_like_none();
            js.assert_optional_bigint(&val);
            let (low, high) = int128_to_int64x2(&val);
            js.push(format!("!isLikeNone({val})"));
            js.push(format!("isLikeNone({val}) ? BigInt(0) : {low}"));
            js.push(format!("isLikeNone({val}) ? BigInt(0) : {high}"));
        }
        Instruction::OptionWasmToInt128 { signed } => {
            let high = js.pop();
            let low = js.pop();
            let present = js.pop();
            let val = int64x2_to_int128(low, high, *signed);
            js.push(format!("{present} === 0 ? undefined : {val}"));
        }

        Instruction::WasmToStringEnum { name } => {
            let index = js.pop();
            js.cx.expose_string_enum(name);
            js.push(wasm_to_string_enum(name, &index))
        }

        Instruction::OptionWasmToStringEnum { name } => {
            // Since hole is currently variant_count+1 and the lookup is
            // ["a","b","c"][index], the lookup will implicitly return map
            // the hole to undefined, because OOB indexes will return undefined.
            let index = js.pop();
            js.cx.expose_string_enum(name);
            js.push(wasm_to_string_enum(name, &index))
        }

        Instruction::StringEnumToWasm { name, invalid } => {
            let enum_val = js.pop();
            js.cx.expose_string_enum(name);
            js.push(string_enum_to_wasm(name, *invalid, &enum_val))
        }

        Instruction::OptionStringEnumToWasm {
            name,
            invalid,
            hole,
        } => {
            let enum_val = js.pop();
            js.cx.expose_string_enum(name);
            let enum_val_expr = string_enum_to_wasm(name, *invalid, &enum_val);
            js.cx.expose_is_like_none();

            // e.g. isLikeNone(someEnumVal) ? 4 : (string_enum_to_wasm(someEnumVal))
            js.push(format!(
                "isLikeNone({enum_val}) ? {hole} : ({enum_val_expr})"
            ))
        }

        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::StringToMemory {
            mem,
            malloc,
            realloc,
        } => {
            js.string_to_memory(*mem, *malloc, *realloc);
        }

        Instruction::Retptr { size } => {
            js.cx.inject_stack_pointer_shim()?;
            js.prelude(&format!(
                "const retptr = wasm.__wbindgen_add_to_stack_pointer(-{size});"
            ));
            js.finally(&format!("wasm.__wbindgen_add_to_stack_pointer({size});"));
            js.stack.push("retptr".to_string());
        }

        Instruction::StoreRetptr { ty, offset, mem } => {
            let mem = js.cx.expose_dataview_memory(*mem);
            let (method, size) = match ty {
                AdapterType::I32 => ("setInt32", 4),
                AdapterType::I64 => ("setBigInt64", 8),
                AdapterType::F32 => ("setFloat32", 4),
                AdapterType::F64 => ("setFloat64", 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}().{method}({} + {size} * {offset}, {val}, true);",
                js.arg(0),
            );
            js.prelude(&expr);
        }

        Instruction::LoadRetptr { ty, offset, mem } => {
            let mem = js.cx.expose_dataview_memory(*mem);
            let (method, quads) = match ty {
                AdapterType::I32 => ("getInt32", 1),
                AdapterType::I64 => ("getBigInt64", 2),
                AdapterType::F32 => ("getFloat32", 1),
                AdapterType::F64 => ("getFloat64", 2),
                other => bail!("invalid aggregate return type {other:?}"),
            };
            let size = quads * 4;
            // Separate the offset and the scaled offset, because otherwise you don't guarantee
            // that the variable names will be unique.
            let scaled_offset = offset / quads;
            // 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}().{method}(retptr + {size} * {scaled_offset}, true)");
            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();
            let i = js.tmp();
            js.prelude(&format!("const char{i} = {val}.codePointAt(0);"));
            let val = format!("char{i}");
            js.assert_char(&val);
            js.push(val);
        }

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

        Instruction::I32FromExternrefBorrow => {
            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::I32FromExternrefRustOwned { class } => {
            let val = js.pop();
            js.assert_class(&val, class);
            js.assert_not_moved(&val);
            let i = js.tmp();
            js.prelude(&format!("var ptr{i} = {val}.__destroy_into_raw();"));
            js.push(format!("ptr{i}"));
        }

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

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

        Instruction::I32FromOptionExternref { 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_externref_table(*table, *alloc);
                    js.push(format!("isLikeNone({val}) ? 0 : {alloc}({val})"));
                }
                None => {
                    js.cx.expose_add_heap_object();
                    js.push(format!("isLikeNone({val}) ? 0 : addHeapObject({val})"));
                }
            }
        }

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

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

        Instruction::I32FromOptionChar => {
            let val = js.pop();
            let i = js.tmp();
            js.cx.expose_is_like_none();
            js.prelude(&format!(
                "const char{i} = isLikeNone({val}) ? 0xFFFFFF : {val}.codePointAt(0);"
            ));
            let val = format!("char{i}");
            js.cx.expose_assert_char();
            js.prelude(&format!(
                "if ({val} !== 0xFFFFFF) {{ _assertChar({val}); }}"
            ));
            js.push(val);
        }

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

        Instruction::F64FromOptionSentinelInt { signed } => {
            let val = js.pop();
            js.cx.expose_is_like_none();
            js.assert_optional_number(&val);

            // We need to convert the given number to a 32-bit integer before
            // passing it to the ABI for 2 reasons:
            // 1. Rust's behavior for `value_f64 as i32/u32` is different from
            //    the WebAssembly behavior for values outside the 32-bit range.
            //    We could implement this behavior in Rust too, but it's easier
            //    to do it in JS.
            // 2. If we allowed values outside the 32-bit range, the sentinel
            //    value itself would be allowed. This would make it impossible
            //    to distinguish between the sentinel value and a valid value.
            //
            // To perform the actual conversion, we use JS bit shifts. Handily,
            // >> and >>> perform a conversion to i32 and u32 respectively
            // to apply the bit shift, so we can use e.g. x >>> 0 to convert to
            // u32.

            let op = if *signed { ">>" } else { ">>>" };
            js.push(format!("isLikeNone({val}) ? 0x100000001 : ({val}) {op} 0"));
        }
        Instruction::F64FromOptionSentinelF32 => {
            let val = js.pop();
            js.cx.expose_is_like_none();
            js.assert_optional_number(&val);

            // Similar to the above 32-bit integer variant, we convert the
            // number to a 32-bit *float* before passing it to the ABI. This
            // ensures consistent behavior with WebAssembly and makes it
            // possible to use a sentinel value.

            js.push(format!(
                "isLikeNone({val}) ? 0x100000001 : Math.fround({val})"
            ));
        }

        Instruction::FromOptionNative { ty } => {
            let val = js.pop();
            js.cx.expose_is_like_none();
            if *ty == ValType::I64 {
                js.assert_optional_bigint(&val);
            } else {
                js.assert_optional_number(&val);
            }
            js.push(format!("!isLikeNone({val})"));
            js.push(format!(
                "isLikeNone({val}) ? {zero} : {val}",
                zero = if *ty == ValType::I64 {
                    // We can't use bigint literals for now. See:
                    // https://github.com/wasm-bindgen/wasm-bindgen/issues/4246
                    "BigInt(0)"
                } else {
                    "0"
                }
            ));
        }

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

        Instruction::UnwrapResult { table_and_drop } => {
            let take_object = if let Some((table, drop)) = *table_and_drop {
                js.cx
                    .expose_take_from_externref_table(table, drop)
                    .to_string()
            } else {
                js.cx.expose_take_object();
                "takeObject".to_string()
            };
            // is_err is popped first. The original layout was: ResultAbi {
            //    abi: ResultAbiUnion<T>,
            //    err: u32,
            //    is_err: u32,
            // }
            // So is_err is last to be added to the stack.
            let is_err = js.pop();
            let err = js.pop();
            js.prelude(&format!(
                "
                if ({is_err}) {{
                    throw {take_object}({err});
                }}
                ",
            ));
        }

        Instruction::UnwrapResultString { table_and_drop } => {
            let take_object = if let Some((table, drop)) = *table_and_drop {
                js.cx
                    .expose_take_from_externref_table(table, drop)
                    .to_string()
            } else {
                js.cx.expose_take_object();
                "takeObject".to_string()
            };
            let is_err = js.pop();
            let err = js.pop();
            let len = js.pop();
            let ptr = js.pop();
            let i = js.tmp();
            js.prelude(&format!(
                "
                var ptr{i} = {ptr};
                var len{i} = {len};
                if ({is_err}) {{
                    ptr{i} = 0; len{i} = 0;
                    throw {take_object}({err});
                }}
                ",
            ));
            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({val}) ? 0 : {func}({val}, wasm.{malloc}{realloc});",
            ));
            js.prelude(&format!("var len{i} = WASM_VECTOR_LEN;"));
            js.push(format!("ptr{i}"));
            js.push(format!("len{i}"));
        }

        Instruction::OptionVector { kind, mem, malloc } => {
            let func = js.cx.pass_to_wasm_function(kind.clone(), *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({val}) ? 0 : {func}({val}, wasm.{malloc});",
            ));
            js.prelude(&format!("var len{i} = WASM_VECTOR_LEN;"));
            js.push(format!("ptr{i}"));
            js.push(format!("len{i}"));
        }

        Instruction::MutableSliceToMemory { kind, malloc, mem } => {
            // Copy the contents of the typed array into wasm.
            let val = js.pop();
            let func = js.cx.pass_to_wasm_function(kind.clone(), *mem);
            let malloc = js.cx.export_name_of(*malloc);
            let i = js.tmp();
            js.prelude(&format!("var ptr{i} = {func}({val}, wasm.{malloc});",));
            js.prelude(&format!("var len{i} = WASM_VECTOR_LEN;"));
            // Then pass it the pointer and the length of where we copied it.
            js.push(format!("ptr{i}"));
            js.push(format!("len{i}"));
            // Then we give Wasm a reference to the original typed array, so that it can
            // update it with modifications made on the Wasm side before returning.
            js.push(val);
        }

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

        Instruction::ExternrefLoadOwned { table_and_drop } => {
            let take_object = if let Some((table, drop)) = *table_and_drop {
                js.cx
                    .expose_take_from_externref_table(table, drop)
                    .to_string()
            } else {
                js.cx.expose_take_object();
                "takeObject".to_string()
            };
            let val = js.pop();
            js.push(format!("{take_object}({val})"));
        }

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

        Instruction::RustFromI32 { class } => {
            let val = js.pop();
            match constructor {
                Some(name) if name == class => {
                    let (ptr_assignment, register_data) = if js.cx.config.generate_reset_state {
                        (
                            format!(
                                "\
                                this.__wbg_ptr = {val} >>> 0;
                                this.__wbg_inst = __wbg_instance_id;
                                "
                            ),
                            format!("{{ ptr: {val} >>> 0, instance: __wbg_instance_id }}"),
                        )
                    } else {
                        (
                            format!("this.__wbg_ptr = {val} >>> 0;"),
                            "this.__wbg_ptr".to_string(),
                        )
                    };

                    js.prelude(&format!(
                        "
                        {ptr_assignment}
                        {name}Finalization.register(this, {register_data}, this);
                        "
                    ));
                    js.push(String::from("this"));
                }
                Some(_) | None => {
                    let identifier = js.cx.require_class_wrap(class);
                    js.push(format!("{identifier}.__wrap({val})"));
                }
            }
        }

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

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

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

            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.{free}({ptr}, {len}, 1); }}",
                ));
            }

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

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

        Instruction::Closure {
            adapter,
            nargs,
            mutable,
            dtor_if_persistent,
        } => {
            let b = js.pop();
            let a = js.pop();
            let wrapper = js.cx.export_adapter_name(*adapter);

            // TODO: further merge the heap and stack closure handling as
            // they're almost identical (by nature) except for ownership
            // integration.
            if let Some(dtor) = dtor_if_persistent {
                let make_closure = if *mutable {
                    js.cx.expose_make_mut_closure();
                    "makeMutClosure"
                } else {
                    js.cx.expose_make_closure();
                    "makeClosure"
                };

                let dtor = &js.cx.module.exports.get(*dtor).name;

                js.push(format!("{make_closure}({a}, {b}, wasm.{dtor}, {wrapper})"));
            } else {
                let i = js.tmp();
                js.prelude(&format!("var state{i} = {{a: {a}, b: {b}}};"));
                let args = (0..*nargs)
                    .map(|i| format!("arg{i}"))
                    .collect::<Vec<_>>()
                    .join(", ");
                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 {wrapper}(a, state{i}.b, {args});
                            }} finally {{
                                state{i}.a = a;
                            }}
                        }};",
                    ));
                } else {
                    js.prelude(&format!(
                        "var cb{i} = ({args}) => {wrapper}(state{i}.a, state{i}.b, {args});",
                    ));
                }

                // 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{i}.a = state{i}.b = 0;"));
                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.clone(), *mem);
            let i = js.tmp();
            let free = js.cx.export_name_of(*free);
            js.prelude(&format!("var v{i} = {f}({ptr}, {len}).slice();"));
            js.prelude(&format!(
                "wasm.{free}({ptr}, {len} * {size}, {size});",
                size = 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.clone(), *mem);
            let i = js.tmp();
            let free = js.cx.export_name_of(*free);
            js.prelude(&format!("let v{i};"));
            js.prelude(&format!("if ({ptr} !== 0) {{"));
            js.prelude(&format!("v{i} = {f}({ptr}, {len}).slice();"));
            js.prelude(&format!(
                "wasm.{free}({ptr}, {len} * {size}, {size});",
                size = kind.size()
            ));
            js.prelude("}");
            js.push(format!("v{i}"));
        }

        Instruction::View { kind, mem } => {
            let len = js.pop();
            let ptr = js.pop();
            // For externref types, use the view version that doesn't take ownership
            let f = match kind {
                VectorKind::Externref | VectorKind::NamedExternref(_) => {
                    js.cx.expose_get_array_js_value_view_from_wasm(*mem)
                }
                _ => js.cx.expose_get_vector_from_wasm(kind.clone(), *mem),
            };
            js.push(format!("{f}({ptr}, {len})"));
        }

        Instruction::OptionView { kind, mem } => {
            let len = js.pop();
            let ptr = js.pop();
            // For externref types, use the view version that doesn't take ownership
            let f = match kind {
                VectorKind::Externref | VectorKind::NamedExternref(_) => {
                    js.cx.expose_get_array_js_value_view_from_wasm(*mem)
                }
                _ => js.cx.expose_get_vector_from_wasm(kind.clone(), *mem),
            };
            js.push(format!("{ptr} === 0 ? undefined : {f}({ptr}, {len})"));
        }

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

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

        Instruction::ToOptionNative { ty, signed } => {
            let val = js.pop();
            let present = js.pop();
            js.push(format!(
                "{present} === 0 ? undefined : {}",
                if *signed {
                    val
                } else {
                    match ty {
                        ValType::I32 => format!("{val} >>> 0"),
                        ValType::I64 => format!("BigInt.asUintN(64, {val})"),
                        _ => unreachable!("unsigned non-integer"),
                    }
                },
            ));
        }

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

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

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

        Instruction::I32FromNonNull => {
            let val = js.pop();
            js.assert_non_null(&val);
            js.push(val);
        }

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

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

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

impl Invocation {
    fn from(instr: &Instruction, module: &Module) -> Invocation {
        use Instruction::*;
        match instr {
            DeferFree { free, .. } => Invocation::Core {
                id: *free,
                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"),
            },

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

#[derive(Debug, Clone, Copy)]
enum TypePosition {
    Argument,
    Return,
}

fn adapter2ts(
    ty: &AdapterType,
    position: TypePosition,
    dst: &mut String,
    refs: Option<&mut HashSet<TsReference>>,
) {
    match ty {
        AdapterType::I32
        | AdapterType::S8
        | AdapterType::S16
        | AdapterType::S32
        | AdapterType::U8
        | AdapterType::U16
        | AdapterType::U32
        | AdapterType::F32
        | AdapterType::F64
        | AdapterType::NonNull => dst.push_str("number"),
        AdapterType::I64
        | AdapterType::S64
        | AdapterType::U64
        | AdapterType::S128
        | AdapterType::U128 => dst.push_str("bigint"),
        AdapterType::String => dst.push_str("string"),
        AdapterType::Externref => dst.push_str("any"),
        AdapterType::Bool => dst.push_str("boolean"),
        AdapterType::Vector(kind) => dst.push_str(&kind.js_ty()),
        AdapterType::Option(ty) => {
            adapter2ts(ty, position, dst, refs);
            dst.push_str(match position {
                TypePosition::Argument => " | null | undefined",
                TypePosition::Return => " | undefined",
            });
        }
        AdapterType::NamedExternref(name) => dst.push_str(name),
        AdapterType::Struct(name) => dst.push_str(name),
        AdapterType::Enum(name) => dst.push_str(name),
        AdapterType::StringEnum(name) => {
            if let Some(refs) = refs {
                refs.insert(TsReference::StringEnum(name.clone()));
            }

            dst.push_str(name);
        }
        AdapterType::Function => dst.push_str("any"),
    }
}
