/*
 * Copyright 2017 WebAssembly Community Group participants
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include "wabt/c-writer.h"

#include <cctype>
#include <cinttypes>
#include <iterator>
#include <limits>
#include <map>
#include <set>
#include <string_view>
#include <vector>

#include "wabt/cast.h"
#include "wabt/common.h"
#include "wabt/ir.h"
#include "wabt/literal.h"
#include "wabt/sha256.h"
#include "wabt/stream.h"
#include "wabt/string-util.h"

#define INDENT_SIZE 2

#define UNIMPLEMENTED(x) printf("unimplemented: %s\n", (x)), abort()

// code to be inserted into the generated output
extern const char* s_header_top;
extern const char* s_header_bottom;
extern const char* s_source_includes;
extern const char* s_source_declarations;
extern const char* s_simd_source_declarations;
extern const char* s_atomicops_source_declarations;

namespace wabt {

namespace {

struct Label {
  Label(LabelType label_type,
        const std::string& name,
        const TypeVector& sig,
        size_t type_stack_size,
        size_t try_catch_stack_size,
        bool used = false)
      : label_type(label_type),
        name(name),
        sig(sig),
        type_stack_size(type_stack_size),
        try_catch_stack_size(try_catch_stack_size),
        used(used) {}

  bool HasValue() const { return !sig.empty(); }

  LabelType label_type;
  const std::string& name;
  const TypeVector& sig;
  size_t type_stack_size;
  size_t try_catch_stack_size;
  bool used = false;
};

struct LocalName {
  explicit LocalName(const std::string& name) : name(name) {}
  const std::string& name;
};

struct ParamName : LocalName {
  using LocalName::LocalName;
  ParamName(const Var& var) : LocalName(var.name()) {}
};

struct LabelName : LocalName {
  using LocalName::LocalName;
};

struct GlobalName {
  GlobalName(ModuleFieldType type, const std::string& name)
      : type(type), name(name) {}
  ModuleFieldType type;
  const std::string& name;
};

struct TagSymbol : GlobalName {
  explicit TagSymbol(const std::string& name)
      : GlobalName(ModuleFieldType::Tag, name) {}
};

struct ExternalRef : GlobalName {
  using GlobalName::GlobalName;
};

struct TailCallRef : GlobalName {
  explicit TailCallRef(const std::string& name)
      : GlobalName(ModuleFieldType::Func, name) {}
};

struct ExternalInstancePtr : GlobalName {
  using GlobalName::GlobalName;
};

struct ExternalInstanceRef : GlobalName {
  using GlobalName::GlobalName;
};

struct GotoLabel {
  explicit GotoLabel(const Var& var) : var(var) {}
  const Var& var;
};

struct LabelDecl {
  explicit LabelDecl(const std::string& name) : name(name) {}
  std::string name;
};

struct GlobalInstanceVar {
  explicit GlobalInstanceVar(const Var& var) : var(var) {}
  const Var& var;
};

struct StackVar {
  explicit StackVar(Index index, Type type = Type::Any)
      : index(index), type(type) {}
  Index index;
  Type type;
};

struct TypeEnum {
  explicit TypeEnum(Type type) : type(type) {}
  Type type;
};

struct SignedType {
  explicit SignedType(Type type) : type(type) {}
  Type type;
};

struct TryCatchLabel {
  TryCatchLabel(const std::string& name, size_t try_catch_stack_size)
      : name(name), try_catch_stack_size(try_catch_stack_size), used(false) {}
  std::string name;
  size_t try_catch_stack_size;
  bool used;
};

struct FuncTypeExpr {
  const FuncType* func_type;
  FuncTypeExpr(const FuncType* f) : func_type(f) {}
};

struct Newline {};
struct OpenBrace {};
struct CloseBrace {};

int GetShiftMask(Type type) {
  // clang-format off
  switch (type) {
    case Type::I32: return 31;
    case Type::I64: return 63;
    default: WABT_UNREACHABLE; return 0;
  }
  // clang-format on
}

/*
 * This function is the default behavior for name_to_output_file_index_. For
 * single .c output, this function returns a vector filled with 0. For multiple
 * .c outputs, this function sorts all non-imported functions in the module by
 * their names, and then divides all non-imported functions into equal-sized
 * buckets (# of non-imported functions / # of .c outputs) based on the sorting.
 */
static std::vector<size_t> default_name_to_output_file_index(
    std::vector<Func*>::const_iterator func_begin,
    std::vector<Func*>::const_iterator func_end,
    size_t num_imports,
    size_t num_streams) {
  std::vector<size_t> result;
  result.resize(std::distance(func_begin, func_end));
  if (num_streams == 1) {
    return result;
  }

  std::map<std::string, Index> sorted_functions;
  size_t non_imported_funcs = result.size() - num_imports;
  size_t bucket_size = non_imported_funcs / num_streams +
                       (non_imported_funcs % num_streams ? 1 : 0);
  Index func_index = 0;
  for (auto func = func_begin; func != func_end; func++) {
    sorted_functions.insert({(*func)->name, func_index});
    ++func_index;
  }
  Index sorted_func_index = 0;
  for (const auto& [func_name, index] : sorted_functions) {
    bool is_import = index < num_imports;
    if (!is_import) {
      result.at(index) = sorted_func_index / bucket_size;
      ++sorted_func_index;
    }
  }
  return result;
}

class CWriter {
 public:
  CWriter(std::vector<Stream*>&& c_streams,
          Stream* h_stream,
          Stream* h_impl_stream,
          const char* header_name,
          const char* header_impl_name,
          const WriteCOptions& options)
      : options_(options),
        c_streams_(std::move(c_streams)),
        h_stream_(h_stream),
        h_impl_stream_(h_impl_stream),
        header_name_(header_name),
        header_impl_name_(header_impl_name) {
    module_prefix_ = MangleModuleName(options_.module_name);
    if (c_streams_.size() != 1 && options.name_to_output_file_index) {
      name_to_output_file_index_ = options.name_to_output_file_index;
    } else {
      name_to_output_file_index_ = default_name_to_output_file_index;
    }
  }

  Result WriteModule(const Module&);

 private:
  using SymbolSet = std::set<std::string>;
  using SymbolMap = std::map<std::string, std::string>;
  using StackTypePair = std::pair<Index, Type>;
  using StackVarSymbolMap = std::map<StackTypePair, std::string>;

  void WriteCHeader();
  void WriteCSource();

  size_t MarkTypeStack() const;
  void ResetTypeStack(size_t mark);
  Type StackType(Index) const;
  void PushType(Type);
  void PushTypes(const TypeVector&);
  void DropTypes(size_t count);

  void PushLabel(LabelType,
                 const std::string& name,
                 const FuncSignature&,
                 bool used = false);
  const Label* FindLabel(const Var& var, bool mark_used = true);
  bool IsTopLabelUsed() const;
  void PopLabel();

  static constexpr char MangleType(Type);
  static constexpr char MangleField(ModuleFieldType);
  static std::string MangleTypes(const TypeVector&);
  static std::string Mangle(std::string_view name, bool double_underscores);
  static std::string MangleName(std::string_view);
  static std::string MangleModuleName(std::string_view);
  static std::string ExportName(std::string_view module_name,
                                std::string_view export_name);
  std::string ExportName(std::string_view export_name) const;
  static std::string TailCallExportName(std::string_view module_name,
                                        std::string_view export_name);
  std::string TailCallExportName(std::string_view export_name) const;
  std::string ModuleInstanceTypeName() const;
  static std::string ModuleInstanceTypeName(std::string_view module_name);
  void ClaimName(SymbolSet& set,
                 SymbolMap& map,
                 char type_suffix,
                 std::string_view wasm_name,
                 const std::string& c_name);
  std::string FindUniqueName(SymbolSet& set,
                             std::string_view proposed_name) const;
  std::string ClaimUniqueName(SymbolSet& set,
                              SymbolMap& map,
                              char type_suffix,
                              std::string_view wasm_name,
                              const std::string& proposed_c_name);
  void DefineImportName(const Import* import,
                        std::string_view module_name,
                        std::string_view field_name);
  void ReserveExportNames();
  void ReserveExportName(std::string_view);
  std::string DefineImportedModuleInstanceName(std::string_view name);
  std::string DefineInstanceMemberName(ModuleFieldType, std::string_view);
  std::string DefineGlobalScopeName(ModuleFieldType, std::string_view);
  std::string DefineLocalScopeName(std::string_view name, bool is_label);
  std::string DefineParamName(std::string_view);
  std::string DefineLabelName(std::string_view);
  std::string DefineStackVarName(Index, Type, std::string_view);

  static void SerializeFuncType(const FuncType&, std::string&);

  std::string GetGlobalName(ModuleFieldType, const std::string&) const;
  std::string GetLocalName(const std::string&, bool is_label) const;
  std::string GetTailCallRef(const std::string&) const;

  void Indent(int size = INDENT_SIZE);
  void Dedent(int size = INDENT_SIZE);
  void WriteIndent();
  void WriteData(const char* src, size_t size);
  void Writef(const char* format, ...);

  template <typename T, typename U, typename... Args>
  void Write(T&& t, U&& u, Args&&... args) {
    Write(std::forward<T>(t));
    Write(std::forward<U>(u));
    Write(std::forward<Args>(args)...);
  }

  static const char* GetReferenceTypeName(const Type& type);
  static const char* GetReferenceNullValue(const Type& type);
  static const char* GetCTypeName(const Type& type);

  const char* InternalSymbolScope() const;

  enum class CWriterPhase {
    Declarations,
    Definitions,
  };

  void Write() {}
  void Write(Newline);
  void Write(OpenBrace);
  void Write(CloseBrace);
  void Write(uint64_t);
  void Write(std::string_view);
  void Write(const ParamName&);
  void Write(const LabelName&);
  void Write(const GlobalName&);
  void Write(const TagSymbol&);
  void Write(const ExternalRef&);
  void Write(const TailCallRef&);
  void Write(const ExternalInstancePtr&);
  void Write(const ExternalInstanceRef&);
  void Write(Type);
  void Write(SignedType);
  void Write(TypeEnum);
  void Write(const GotoLabel&);
  void Write(const LabelDecl&);
  void Write(const GlobalInstanceVar&);
  void Write(const StackVar&);
  void Write(const TypeVector&);
  void Write(const Const&);
  void WriteInitExpr(const ExprList&);
  void WriteInitExprTerminal(const Expr*);
  std::string GenerateHeaderGuard() const;
  void WriteSourceTop();
  void WriteMultiCTop();
  void WriteMultiCTopEmpty();
  void DeclareStruct(const TypeVector&);
  void WriteTailcalleeParamTypes();
  void WriteMultivalueResultTypes();
  void WriteTagTypes();
  void WriteFuncTypeDecls();
  void WriteFuncTypes();
  void Write(const FuncTypeExpr&);
  void WriteTagDecls();
  void WriteTags();
  void ComputeUniqueImports();
  void BeginInstance();
  void WriteImports();
  void WriteTailCallWeakImports();
  void WriteFuncDeclarations();
  void WriteFuncDeclaration(const FuncDeclaration&, const std::string&);
  void WriteTailCallFuncDeclaration(const std::string&);
  void WriteImportFuncDeclaration(const FuncDeclaration&,
                                  const std::string& module_name,
                                  const std::string&);
  void WriteCallIndirectFuncDeclaration(const FuncDeclaration&,
                                        const std::string&);
  void ComputeSimdScope();
  void WriteHeaderIncludes();
  void WriteV128Decl();
  void WriteModuleInstance();
  void WriteGlobals();
  void WriteGlobal(const Global&, const std::string&);
  void WriteGlobalPtr(const Global&, const std::string&);
  void WriteMemories();
  void WriteMemory(const std::string&);
  void WriteMemoryPtr(const std::string&);
  void WriteTables();
  void WriteTable(const std::string&, const wabt::Type&);
  void WriteTablePtr(const std::string&, const Table&);
  void WriteTableType(const wabt::Type&);
  void WriteDataInstances();
  void WriteElemInstances();
  void WriteGlobalInitializers();
  void WriteDataInitializerDecls();
  void WriteDataInitializers();
  void WriteElemInitializerDecls();
  void WriteElemInitializers();
  void WriteElemTableInit(bool, const ElemSegment*, const Table*);
  void WriteExports(CWriterPhase);
  void WriteTailCallExports(CWriterPhase);
  void WriteInitDecl();
  void WriteFreeDecl();
  void WriteGetFuncTypeDecl();
  void WriteInit();
  void WriteFree();
  void WriteGetFuncType();
  void WriteInitInstanceImport();
  void WriteImportProperties(CWriterPhase);
  void WriteFuncs();
  void BeginFunction(const Func&);
  void FinishFunction();
  void Write(const Func&);
  void WriteTailCallee(const Func&);
  void WriteParamsAndLocals();
  void WriteParams(const std::vector<std::string>& index_to_name);
  void WriteParamSymbols(const std::vector<std::string>& index_to_name);
  void WriteParamTypes(const FuncDeclaration& decl);
  void WriteLocals(const std::vector<std::string>& index_to_name);
  void WriteStackVarDeclarations();
  void Write(const ExprList&);
  void WriteTailCallAsserts(const FuncSignature&);
  void WriteTailCallStack();
  void WriteUnwindTryCatchStack(const Label*);
  void FinishReturnCall();
  void Spill(const TypeVector&);
  void Unspill(const TypeVector&);

  template <typename Vars, typename TypeOf, typename ToDo>
  void WriteVarsByType(const Vars&, const TypeOf&, const ToDo&);

  template <typename sources>
  void Spill(const TypeVector&, const sources& src);
  template <typename targets>
  void Unspill(const TypeVector&, const targets& tgt);

  enum class AssignOp {
    Disallowed,
    Allowed,
  };

  void WriteSimpleUnaryExpr(Opcode, const char* op);
  void WriteInfixBinaryExpr(Opcode,
                            const char* op,
                            AssignOp = AssignOp::Allowed);
  void WritePrefixBinaryExpr(Opcode, const char* op);
  void WriteSignedBinaryExpr(Opcode, const char* op);
  void Write(const BinaryExpr&);
  void Write(const CompareExpr&);
  void Write(const ConvertExpr&);
  void Write(const LoadExpr&);
  void Write(const StoreExpr&);
  void Write(const UnaryExpr&);
  void Write(const TernaryExpr&);
  void Write(const SimdLaneOpExpr&);
  void Write(const SimdLoadLaneExpr&);
  void Write(const SimdStoreLaneExpr&);
  void Write(const SimdShuffleOpExpr&);
  void Write(const LoadSplatExpr&);
  void Write(const LoadZeroExpr&);
  void Write(const Block&);

  void Write(const AtomicLoadExpr& expr);
  void Write(const AtomicStoreExpr& expr);
  void Write(const AtomicRmwExpr& expr);
  void Write(const AtomicRmwCmpxchgExpr& expr);

  size_t BeginTry(const TryExpr& tryexpr);
  void WriteTryCatch(const TryExpr& tryexpr);
  void WriteTryDelegate(const TryExpr& tryexpr);
  void Write(const Catch& c);
  void WriteThrow();

  void PushTryCatch(const std::string& name);
  void PopTryCatch();

  void PushFuncSection(std::string_view include_condition = "");

  bool IsImport(const std::string& name) const;

  const WriteCOptions& options_;
  const Module* module_ = nullptr;
  const Func* func_ = nullptr;
  Stream* stream_ = nullptr;
  std::vector<Stream*> c_streams_;
  Stream* h_stream_ = nullptr;
  Stream* h_impl_stream_ = nullptr;
  std::string header_name_;
  std::string header_impl_name_;
  Result result_ = Result::Ok;
  int indent_ = 0;
  bool should_write_indent_next_ = false;
  int consecutive_newline_count_ = 0;

  SymbolMap global_sym_map_;
  SymbolMap local_sym_map_;
  SymbolMap import_module_sym_map_;
  StackVarSymbolMap stack_var_sym_map_;
  SymbolSet global_syms_;
  SymbolSet local_syms_;
  TypeVector type_stack_;
  std::vector<Label> label_stack_;
  std::vector<TryCatchLabel> try_catch_stack_;
  std::string module_prefix_;
  SymbolSet typevector_structs_;

  std::vector<const Import*> unique_imports_;
  SymbolSet import_module_set_;       // modules that are imported from
  SymbolSet import_func_module_set_;  // modules that funcs are imported from

  std::vector<std::pair<std::string, MemoryStream>> func_sections_;
  SymbolSet func_includes_;

  std::vector<std::string> unique_func_type_names_;

  std::function<std::vector<size_t>(std::vector<Func*>::const_iterator,
                                    std::vector<Func*>::const_iterator,
                                    size_t,
                                    size_t)>
      name_to_output_file_index_;

  bool simd_used_in_header_;

  bool in_tail_callee_;
};

// TODO: if WABT begins supporting debug names for labels,
// will need to avoid conflict between a label named "$Bfunc" and
// the implicit func label
static constexpr char kImplicitFuncLabel[] = "$Bfunc";

// These should be greater than any ModuleFieldType (used for MangleField).
static constexpr char kParamSuffix =
    'a' + static_cast<char>(ModuleFieldType::Tag) + 1;
static constexpr char kLabelSuffix = kParamSuffix + 1;

static constexpr char kGlobalSymbolPrefix[] = "w2c_";
static constexpr char kLocalSymbolPrefix[] = "var_";
static constexpr char kAdminSymbolPrefix[] = "wasm2c_";
static constexpr char kTailCallSymbolPrefix[] = "wasm_tailcall_";
static constexpr char kTailCallFallbackPrefix[] = "wasm_fallback_";
static constexpr unsigned int kTailCallStackSize = 1024;

size_t CWriter::MarkTypeStack() const {
  return type_stack_.size();
}

void CWriter::ResetTypeStack(size_t mark) {
  assert(mark <= type_stack_.size());
  type_stack_.erase(type_stack_.begin() + mark, type_stack_.end());
}

Type CWriter::StackType(Index index) const {
  assert(index < type_stack_.size());
  return *(type_stack_.rbegin() + index);
}

void CWriter::PushType(Type type) {
  type_stack_.push_back(type);
}

void CWriter::PushTypes(const TypeVector& types) {
  type_stack_.insert(type_stack_.end(), types.begin(), types.end());
}

void CWriter::DropTypes(size_t count) {
  assert(count <= type_stack_.size());
  type_stack_.erase(type_stack_.end() - count, type_stack_.end());
}

void CWriter::PushLabel(LabelType label_type,
                        const std::string& name,
                        const FuncSignature& sig,
                        bool used) {
  if (label_type == LabelType::Loop)
    label_stack_.emplace_back(label_type, name, sig.param_types,
                              type_stack_.size(), try_catch_stack_.size(),
                              used);
  else
    label_stack_.emplace_back(label_type, name, sig.result_types,
                              type_stack_.size(), try_catch_stack_.size(),
                              used);
}

const Label* CWriter::FindLabel(const Var& var, bool mark_used) {
  Label* label = nullptr;

  if (var.is_index()) {
    // We've generated names for all labels, so we should only be using an
    // index when branching to the implicit function label, which can't be
    // named.
    assert(var.index() + 1 == label_stack_.size());
    label = &label_stack_[0];
  } else {
    assert(var.is_name());
    for (Index i = label_stack_.size(); i > 0; --i) {
      label = &label_stack_[i - 1];
      if (label->name == var.name())
        break;
    }
  }

  assert(label);
  if (mark_used) {
    label->used = true;
  }
  return label;
}

bool CWriter::IsTopLabelUsed() const {
  assert(!label_stack_.empty());
  return label_stack_.back().used;
}

void CWriter::PopLabel() {
  label_stack_.pop_back();
}

// static
constexpr char CWriter::MangleType(Type type) {
  // clang-format off
  switch (type) {
    case Type::I32: return 'i';
    case Type::I64: return 'j';
    case Type::F32: return 'f';
    case Type::F64: return 'd';
    case Type::V128: return 'o';
    case Type::FuncRef: return 'r';
    case Type::ExternRef: return 'e';
    default:
      WABT_UNREACHABLE;
  }
  // clang-format on
}

// static
constexpr char CWriter::MangleField(ModuleFieldType type) {
  assert(static_cast<std::underlying_type<ModuleFieldType>::type>(type) <
         std::numeric_limits<char>::max());
  return 'a' + static_cast<char>(type);
}

// remove risky characters for pasting into a C-style comment
static std::string SanitizeForComment(std::string_view str) {
  std::string result;

  for (const uint8_t ch : str) {
    // escape control chars, DEL, >7-bit chars, trigraphs, and end of comment
    if (ch < ' ' || ch > '~' || ch == '?' || ch == '/') {
      result += "\\" + StringPrintf("%02X", ch);
    } else {
      result += ch;
    }
  }

  return result;
}

// static
std::string CWriter::MangleTypes(const TypeVector& types) {
  assert(types.size() >= 2);
  std::string result = "wasm_multi_";
  for (auto type : types) {
    result += MangleType(type);
  }
  return result;
}

/* The C symbol for an export from this module. */
std::string CWriter::ExportName(std::string_view export_name) const {
  return kGlobalSymbolPrefix + module_prefix_ + '_' + MangleName(export_name);
}

/* The C symbol for an export from an arbitrary module. */
// static
std::string CWriter::ExportName(std::string_view module_name,
                                std::string_view export_name) {
  return kGlobalSymbolPrefix + MangleModuleName(module_name) + '_' +
         MangleName(export_name);
}

/* The C symbol for a tail-callee export from this module. */
std::string CWriter::TailCallExportName(std::string_view export_name) const {
  return kTailCallSymbolPrefix + ExportName(export_name);
}

/* The C symbol for a tail-callee export from an arbitrary module. */
// static
std::string CWriter::TailCallExportName(std::string_view module_name,
                                        std::string_view export_name) {
  return kTailCallSymbolPrefix + ExportName(module_name, export_name);
}

/* The type name of an instance of this module. */
std::string CWriter::ModuleInstanceTypeName() const {
  return kGlobalSymbolPrefix + module_prefix_;
}

/* The type name of an instance of an arbitrary module. */
// static
std::string CWriter::ModuleInstanceTypeName(std::string_view module_name) {
  return kGlobalSymbolPrefix + MangleModuleName(module_name);
}

/*
 * Hardcoded "C"-locale versions of isalpha/isdigit/isalnum/isxdigit for use
 * in CWriter::Mangle(). We don't use the standard isalpha/isdigit/isalnum
 * because the caller might have changed the current locale.
 */
static bool internal_isalpha(uint8_t ch) {
  return (ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z');
}

static bool internal_isdigit(uint8_t ch) {
  return (ch >= '0' && ch <= '9');
}

static bool internal_isalnum(uint8_t ch) {
  return internal_isalpha(ch) || internal_isdigit(ch);
}

static bool internal_ishexdigit(uint8_t ch) {
  return internal_isdigit(ch) || (ch >= 'A' && ch <= 'F');  // capitals only
}

// static
std::string CWriter::Mangle(std::string_view name, bool double_underscores) {
  /*
   * Name mangling transforms arbitrary Wasm names into "safe" C names
   * in a deterministic way. To avoid collisions, distinct Wasm names must be
   * transformed into distinct C names.
   *
   * The rules implemented here are:
   * 1) any hex digit ('A' through 'F') that follows the sequence "0x"
   *    is escaped
   * 2) any underscore at the beginning, at the end, or following another
   *    underscore, is escaped
   * 3) if double_underscores is set, underscores are replaced with
   *    two underscores.
   * 4) otherwise, any alphanumeric character is kept as-is,
   *    and any other character is escaped
   *
   * "Escaped" means the character is represented with the sequence "0xAB",
   * where A B are hex digits ('0'-'9' or 'A'-'F') representing the character's
   * numeric value.
   *
   * Module names are mangled with double_underscores=true to prevent
   * collisions between, e.g., a module "alfa" with export
   * "bravo_charlie" vs. a module "alfa_bravo" with export "charlie".
   */

  enum State { Any, Zero, ZeroX, ZeroXHexDigit } state{Any};
  bool last_was_underscore = false;

  std::string result;
  auto append_escaped = [&](const uint8_t ch) {
    result += "0x" + StringPrintf("%02X", ch);
    last_was_underscore = false;
    state = Any;
  };

  auto append_verbatim = [&](const uint8_t ch) {
    result += ch;
    last_was_underscore = (ch == '_');
  };

  for (auto it = name.begin(); it != name.end(); ++it) {
    const uint8_t ch = *it;
    switch (state) {
      case Any:
        state = (ch == '0') ? Zero : Any;
        break;
      case Zero:
        state = (ch == 'x') ? ZeroX : Any;
        break;
      case ZeroX:
        state = internal_ishexdigit(ch) ? ZeroXHexDigit : Any;
        break;
      case ZeroXHexDigit:
        WABT_UNREACHABLE;
        break;
    }

    /* rule 1 */
    if (state == ZeroXHexDigit) {
      append_escaped(ch);
      continue;
    }

    /* rule 2 */
    if ((ch == '_') && ((it == name.begin()) || (std::next(it) == name.end()) ||
                        last_was_underscore)) {
      append_escaped(ch);
      continue;
    }

    /* rule 3 */
    if (double_underscores && ch == '_') {
      append_verbatim(ch);
      append_verbatim(ch);
      continue;
    }

    /* rule 4 */
    if (internal_isalnum(ch) || (ch == '_')) {
      append_verbatim(ch);
    } else {
      append_escaped(ch);
    }
  }

  return result;
}

// static
std::string CWriter::MangleName(std::string_view name) {
  return Mangle(name, false);
}

// static
std::string CWriter::MangleModuleName(std::string_view name) {
  return Mangle(name, true);
}

/*
 * Allocate a C symbol (must be unused) in the SymbolSet,
 * and a mapping from the Wasm name (tagged with
 * the index space of the name) to that C symbol.
 */
void CWriter::ClaimName(SymbolSet& set,
                        SymbolMap& map,
                        char type_suffix,
                        std::string_view wasm_name,
                        const std::string& c_name) {
  const std::string type_tagged_wasm_name =
      std::string(wasm_name) + type_suffix;

  [[maybe_unused]] bool success;
  success = set.insert(c_name).second;
  assert(success);

  success = map.emplace(type_tagged_wasm_name, c_name).second;
  assert(success);
}

/*
 * Make a proposed C symbol unique in a given symbol set by appending
 * an integer to the symbol if necessary.
 */
std::string CWriter::FindUniqueName(SymbolSet& set,
                                    std::string_view proposed_name) const {
  std::string unique{proposed_name};
  if (set.find(unique) != set.end()) {
    std::string base = unique + "_";
    size_t count = 0;
    do {
      unique = base + std::to_string(count++);
    } while (set.find(unique) != set.end());
  }
  return unique;
}

/*
 * Find a unique C symbol in the symbol set and claim it (mapping the
 * type-tagged Wasm name to it).
 */
std::string CWriter::ClaimUniqueName(SymbolSet& set,
                                     SymbolMap& map,
                                     char type_suffix,
                                     std::string_view wasm_name,
                                     const std::string& proposed_c_name) {
  const std::string unique = FindUniqueName(set, proposed_c_name);
  ClaimName(set, map, type_suffix, wasm_name, unique);
  return unique;
}

std::string_view StripLeadingDollar(std::string_view name) {
  assert(!name.empty());
  assert(name.front() == '$');
  name.remove_prefix(1);
  return name;
}

void CWriter::DefineImportName(const Import* import,
                               std::string_view module,
                               std::string_view field_name) {
  std::string name;
  ModuleFieldType type{};

  switch (import->kind()) {
    case ExternalKind::Func:
      type = ModuleFieldType::Func;
      name = cast<FuncImport>(import)->func.name;
      break;
    case ExternalKind::Tag:
      type = ModuleFieldType::Tag;
      name = cast<TagImport>(import)->tag.name;
      break;
    case ExternalKind::Global:
      type = ModuleFieldType::Global;
      name = cast<GlobalImport>(import)->global.name;
      break;
    case ExternalKind::Memory:
      type = ModuleFieldType::Memory;
      name = cast<MemoryImport>(import)->memory.name;
      break;
    case ExternalKind::Table:
      type = ModuleFieldType::Table;
      name = cast<TableImport>(import)->table.name;
      break;
  }

  import_module_sym_map_.emplace(name, import->module_name);

  const std::string mangled = ExportName(module, field_name);
  global_syms_.erase(mangled);  // duplicate imports are allowed
  ClaimName(global_syms_, global_sym_map_, MangleField(type), name, mangled);
}

/*
 * Reserve a C symbol for the public name of a module's export.  The
 * format of these is "w2c_" + the module prefix + "_" + the mangled
 * export name. Reserving the symbol prevents internal functions and
 * other names from shadowing/overlapping the exports.
 */
void CWriter::ReserveExportName(std::string_view name) {
  ClaimName(global_syms_, global_sym_map_, MangleField(ModuleFieldType::Export),
            name, ExportName(name));
}

/*
 * Names for functions, function types, tags, and segments are globally unique
 * across modules (formatted the same as an export, as "w2c_" + module prefix +
 * "_" + the name, made unique if necessary).
 */
std::string CWriter::DefineGlobalScopeName(ModuleFieldType type,
                                           std::string_view name) {
  return ClaimUniqueName(global_syms_, global_sym_map_, MangleField(type), name,
                         ExportName(StripLeadingDollar(name)));
}

std::string CWriter::GetGlobalName(ModuleFieldType type,
                                   const std::string& name) const {
  std::string mangled = name + MangleField(type);
  assert(global_sym_map_.count(mangled) == 1);
  return global_sym_map_.at(mangled);
}

/* Names for params, locals, and stack vars are formatted as "var_" + name. */
std::string CWriter::DefineLocalScopeName(std::string_view name,
                                          bool is_label) {
  return ClaimUniqueName(
      local_syms_, local_sym_map_, is_label ? kLabelSuffix : kParamSuffix, name,
      kLocalSymbolPrefix + MangleName(StripLeadingDollar(name)));
}

std::string CWriter::GetLocalName(const std::string& name,
                                  bool is_label) const {
  std::string mangled = name + (is_label ? kLabelSuffix : kParamSuffix);
  assert(local_sym_map_.count(mangled) == 1);
  return local_sym_map_.at(mangled);
}

std::string CWriter::GetTailCallRef(const std::string& name) const {
  return kTailCallSymbolPrefix + GetGlobalName(ModuleFieldType::Func, name);
}

std::string CWriter::DefineParamName(std::string_view name) {
  return DefineLocalScopeName(name, false);
}

std::string CWriter::DefineLabelName(std::string_view name) {
  return DefineLocalScopeName(name, true);
}

std::string CWriter::DefineStackVarName(Index index,
                                        Type type,
                                        std::string_view name) {
  std::string unique =
      FindUniqueName(local_syms_, kLocalSymbolPrefix + MangleName(name));
  StackTypePair stp = {index, type};
  [[maybe_unused]] bool success =
      stack_var_sym_map_.emplace(stp, unique).second;
  assert(success);
  return unique;
}

/*
 * Members of the module instance (globals, tables, and memories) are formatted
 * as "w2c_" + the mangled name of the element (made unique if necessary).
 */
std::string CWriter::DefineInstanceMemberName(ModuleFieldType type,
                                              std::string_view name) {
  return ClaimUniqueName(
      global_syms_, global_sym_map_, MangleField(type), name,
      kGlobalSymbolPrefix + MangleName(StripLeadingDollar(name)));
}

/*
 * The name of a module-instance member that points to the originating
 * instance of an imported function is formatted as "w2c_" + originating
 * module prefix + "_instance".
 */
std::string CWriter::DefineImportedModuleInstanceName(std::string_view name) {
  return ClaimUniqueName(global_syms_, global_sym_map_,
                         MangleField(ModuleFieldType::Import), name,
                         ExportName(name, "instance"));
}

void CWriter::Indent(int size) {
  indent_ += size;
}

void CWriter::Dedent(int size) {
  indent_ -= size;
  assert(indent_ >= 0);
}

void CWriter::WriteIndent() {
  static char s_indent[] =
      "                                                                       "
      "                                                                       ";
  static size_t s_indent_len = sizeof(s_indent) - 1;
  size_t to_write = indent_;
  while (to_write >= s_indent_len) {
    stream_->WriteData(s_indent, s_indent_len);
    to_write -= s_indent_len;
  }
  if (to_write > 0) {
    stream_->WriteData(s_indent, to_write);
  }
}

void CWriter::WriteData(const char* src, size_t size) {
  if (should_write_indent_next_) {
    WriteIndent();
    should_write_indent_next_ = false;
  }
  if (size > 0 && src[0] != '\n') {
    consecutive_newline_count_ = 0;
  }
  stream_->WriteData(src, size);
}

void WABT_PRINTF_FORMAT(2, 3) CWriter::Writef(const char* format, ...) {
  WABT_SNPRINTF_ALLOCA(buffer, length, format);
  WriteData(buffer, length);
}

void CWriter::Write(Newline) {
  // Allow maximum one blank line between sections
  if (consecutive_newline_count_ < 2) {
    Write("\n");
    consecutive_newline_count_++;
  }
  should_write_indent_next_ = true;
}

void CWriter::Write(OpenBrace) {
  Write("{");
  Indent();
  Write(Newline());
}

void CWriter::Write(CloseBrace) {
  Dedent();
  Write("}");
}

void CWriter::Write(uint64_t val) {
  Writef("%" PRIu64, val);
}

void CWriter::Write(std::string_view s) {
  WriteData(s.data(), s.size());
}

void CWriter::Write(const ParamName& name) {
  Write(GetLocalName(name.name, false));
}

void CWriter::Write(const LabelName& name) {
  Write(GetLocalName(name.name, true));
}

void CWriter::Write(const GlobalName& name) {
  Write(GetGlobalName(name.type, name.name));
}

void CWriter::Write(const TagSymbol& name) {
  if (!IsImport(name.name)) {
    Write("&");
  }
  Write(GlobalName(name));
}

void CWriter::Write(const ExternalInstancePtr& name) {
  if (!IsImport(name.name)) {
    Write("&");
  }
  Write("instance->", GlobalName(name));
}

void CWriter::Write(const ExternalRef& name) {
  if (name.type == ModuleFieldType::Func || !IsImport(name.name)) {
    Write(GlobalName(name));
  } else {
    Write("(*", GlobalName(name), ")");
  }
}

void CWriter::Write(const TailCallRef& name) {
  Write(GetTailCallRef(name.name));
}

void CWriter::Write(const ExternalInstanceRef& name) {
  if (IsImport(name.name)) {
    Write("(*instance->", GlobalName(name), ")");
  } else {
    Write("instance->", GlobalName(name));
  }
}

void CWriter::Write(const GotoLabel& goto_label) {
  const Label* label = FindLabel(goto_label.var);
  if (label->HasValue()) {
    size_t amount = label->sig.size();
    assert(type_stack_.size() >= label->type_stack_size);
    assert(type_stack_.size() >= amount);
    assert(type_stack_.size() - amount >= label->type_stack_size);
    Index offset = type_stack_.size() - label->type_stack_size - amount;
    if (offset != 0) {
      for (Index i = 0; i < amount; ++i) {
        Write(StackVar(amount - i - 1 + offset, label->sig[i]), " = ",
              StackVar(amount - i - 1), "; ");
      }
    }
  }

  WriteUnwindTryCatchStack(label);

  if (goto_label.var.is_name()) {
    Write("goto ", LabelName(goto_label.var.name()), ";");
  } else {
    // We've generated names for all labels, so we should only be using an
    // index when branching to the implicit function label, which can't be
    // named.
    Write("goto ", LabelName(kImplicitFuncLabel), ";");
  }
}

void CWriter::Write(const LabelDecl& label) {
  if (IsTopLabelUsed())
    Write(label.name, ":;", Newline());
}

void CWriter::Write(const GlobalInstanceVar& var) {
  assert(var.var.is_name());
  Write(ExternalInstanceRef(ModuleFieldType::Global, var.var.name()));
}

void CWriter::Write(const StackVar& sv) {
  Index index = type_stack_.size() - 1 - sv.index;
  Type type = sv.type;
  if (type == Type::Any) {
    assert(index < type_stack_.size());
    type = type_stack_[index];
  }

  StackTypePair stp = {index, type};
  auto iter = stack_var_sym_map_.find(stp);
  if (iter == stack_var_sym_map_.end()) {
    std::string name = MangleType(type) + std::to_string(index);
    Write(DefineStackVarName(index, type, name));
  } else {
    Write(iter->second);
  }
}

// static
const char* CWriter::GetCTypeName(const Type& type) {
  // clang-format off
  switch (type) {
  case Type::I32: return "u32";
  case Type::I64: return "u64";
  case Type::F32: return "f32";
  case Type::F64: return "f64";
  case Type::V128: return "v128";
  case Type::FuncRef: return "wasm_rt_funcref_t";
  case Type::ExternRef: return "wasm_rt_externref_t";
    default:
      WABT_UNREACHABLE;
  }
  // clang-format on
}

void CWriter::Write(Type type) {
  Write(GetCTypeName(type));
}

void CWriter::Write(TypeEnum type) {
  // clang-format off
  switch (type.type) {
    case Type::I32: Write("WASM_RT_I32"); break;
    case Type::I64: Write("WASM_RT_I64"); break;
    case Type::F32: Write("WASM_RT_F32"); break;
    case Type::F64: Write("WASM_RT_F64"); break;
    case Type::V128: Write("WASM_RT_V128"); break;
    case Type::FuncRef: Write("WASM_RT_FUNCREF"); break;
    case Type::ExternRef: Write("WASM_RT_EXTERNREF"); break;
    default:
      WABT_UNREACHABLE;
  }
  // clang-format on
}

void CWriter::Write(SignedType type) {
  // clang-format off
  switch (type.type) {
    case Type::I32: Write("s32"); break;
    case Type::I64: Write("s64"); break;
    default:
      WABT_UNREACHABLE;
  }
  // clang-format on
}

void CWriter::Write(const TypeVector& types) {
  if (types.empty()) {
    Write("void");
  } else if (types.size() == 1) {
    Write(types[0]);
  } else {
    Write("struct ", MangleTypes(types));
  }
}

void CWriter::Write(const Const& const_) {
  switch (const_.type()) {
    case Type::I32:
      Writef("%uu", static_cast<int32_t>(const_.u32()));
      break;

    case Type::I64:
      Writef("%" PRIu64 "ull", static_cast<int64_t>(const_.u64()));
      break;

    case Type::F32: {
      uint32_t f32_bits = const_.f32_bits();
      // TODO(binji): Share with similar float info in interp.cc and literal.cc
      if ((f32_bits & 0x7f800000u) == 0x7f800000u) {
        const char* sign = (f32_bits & 0x80000000) ? "-" : "";
        uint32_t significand = f32_bits & 0x7fffffu;
        if (significand == 0) {
          // Infinity.
          Writef("%sINFINITY", sign);
        } else {
          // Nan.
          Writef("f32_reinterpret_i32(0x%08x) /* %snan:0x%06x */", f32_bits,
                 sign, significand);
        }
      } else if (f32_bits == 0x80000000) {
        // Negative zero. Special-cased so it isn't written as -0 below.
        Writef("-0.f");
      } else {
        Writef("%.9g", Bitcast<float>(f32_bits));
      }
      break;
    }

    case Type::F64: {
      uint64_t f64_bits = const_.f64_bits();
      // TODO(binji): Share with similar float info in interp.cc and literal.cc
      if ((f64_bits & 0x7ff0000000000000ull) == 0x7ff0000000000000ull) {
        const char* sign = (f64_bits & 0x8000000000000000ull) ? "-" : "";
        uint64_t significand = f64_bits & 0xfffffffffffffull;
        if (significand == 0) {
          // Infinity.
          Writef("%sINFINITY", sign);
        } else {
          // Nan.
          Writef("f64_reinterpret_i64(0x%016" PRIx64 ") /* %snan:0x%013" PRIx64
                 " */",
                 f64_bits, sign, significand);
        }
      } else if (f64_bits == 0x8000000000000000ull) {
        // Negative zero. Special-cased so it isn't written as -0 below.
        Writef("-0.0");
      } else {
        Writef("%.17g", Bitcast<double>(f64_bits));
      }
      break;
    }
    case Type::V128: {
      Writef("simde_wasm_i32x4_const(0x%08x, 0x%08x, 0x%08x, 0x%08x)",
             const_.vec128().u32(0), const_.vec128().u32(1),
             const_.vec128().u32(2), const_.vec128().u32(3));
      break;
    }

    default:
      WABT_UNREACHABLE;
  }
}

void CWriter::WriteInitDecl() {
  Write("void ", kAdminSymbolPrefix, module_prefix_, "_instantiate(",
        ModuleInstanceTypeName(), "*");
  for (const auto& import_module_name : import_module_set_) {
    Write(", struct ", ModuleInstanceTypeName(import_module_name), "*");
  }
  Write(");", Newline());
}

void CWriter::WriteFreeDecl() {
  Write("void ", kAdminSymbolPrefix, module_prefix_, "_free(",
        ModuleInstanceTypeName(), "*);", Newline());
}

void CWriter::WriteGetFuncTypeDecl() {
  Write("wasm_rt_func_type_t ", kAdminSymbolPrefix, module_prefix_,
        "_get_func_type(uint32_t param_count, uint32_t result_count, ...);",
        Newline());
}

void CWriter::WriteInitExpr(const ExprList& expr_list) {
  if (expr_list.empty()) {
    WABT_UNREACHABLE;
  }

  std::vector<std::string> mini_stack;

  for (const auto& expr : expr_list) {
    if (expr.type() == ExprType::Binary) {
      // Extended const expressions include at least one binary op.
      // This builds a C expression from the operands.
      if (mini_stack.size() < 2) {
        WABT_UNREACHABLE;
      }

      const auto binexpr = cast<BinaryExpr>(&expr);
      char op;
      switch (binexpr->opcode) {
        case Opcode::I32Add:
        case Opcode::I64Add:
        case Opcode::F32Add:
        case Opcode::F64Add:
          op = '+';
          break;

        case Opcode::I32Sub:
        case Opcode::I64Sub:
        case Opcode::F32Sub:
        case Opcode::F64Sub:
          op = '-';
          break;

        case Opcode::I32Mul:
        case Opcode::I64Mul:
        case Opcode::F32Mul:
        case Opcode::F64Mul:
          op = '*';
          break;

        default:
          WABT_UNREACHABLE;
      }

      std::string combination =
          "((" + std::string(GetCTypeName(binexpr->opcode.GetParamType1())) +
          ")" + mini_stack.at(mini_stack.size() - 2) + ")" + op + "((" +
          std::string(GetCTypeName(binexpr->opcode.GetParamType2())) + ")" +
          mini_stack.at(mini_stack.size() - 1) + ")";
      mini_stack.resize(mini_stack.size() - 2);
      mini_stack.push_back(std::move(combination));
    } else {
      // Leaf node (nullary const expression)
      Stream* existing_stream = stream_;
      MemoryStream terminal_stream;
      stream_ = &terminal_stream;
      WriteInitExprTerminal(&expr);
      const auto& buf = terminal_stream.output_buffer();
      mini_stack.emplace_back(reinterpret_cast<const char*>(buf.data.data()),
                              buf.data.size());
      stream_ = existing_stream;
    }
  }

  if (mini_stack.size() != 1) {
    WABT_UNREACHABLE;
  }

  Write(mini_stack.front());
}

void CWriter::WriteInitExprTerminal(const Expr* expr) {
  switch (expr->type()) {
    case ExprType::Const:
      Write(cast<ConstExpr>(expr)->const_);
      break;

    case ExprType::GlobalGet:
      Write(GlobalInstanceVar(cast<GlobalGetExpr>(expr)->var));
      break;

    case ExprType::RefFunc: {
      const Func* func = module_->GetFunc(cast<RefFuncExpr>(expr)->var);
      const FuncDeclaration& decl = func->decl;

      assert(decl.has_func_type);
      const FuncType* func_type = module_->GetFuncType(decl.type_var);

      Write("(wasm_rt_funcref_t){", FuncTypeExpr(func_type), ", ",
            "(wasm_rt_function_ptr_t)",
            ExternalRef(ModuleFieldType::Func, func->name), ", {");
      if (options_.features.tail_call_enabled() &&
          (IsImport(func->name) || func->features_used.tailcall)) {
        Write(TailCallRef(func->name));
      } else {
        Write("NULL");
      }
      Write("}, ");

      if (IsImport(func->name)) {
        Write("instance->", GlobalName(ModuleFieldType::Import,
                                       import_module_sym_map_[func->name]));
      } else {
        Write("instance");
      }
      Write("}");
    } break;

    case ExprType::RefNull:
      Write(GetReferenceNullValue(cast<RefNullExpr>(expr)->type));
      break;

    default:
      WABT_UNREACHABLE;
  }
}

std::string CWriter::GenerateHeaderGuard() const {
  std::string result;
  for (char c : header_name_) {
    if (isalnum(c) || c == '_') {
      result += toupper(c);
    } else {
      result += '_';
    }
  }
  result += "_GENERATED_";
  return result;
}

void CWriter::WriteSourceTop() {
  Write(s_source_includes);
  Write(Newline(), "#include \"", header_name_, "\"", Newline());
  Write(s_source_declarations, Newline());

  if (module_->features_used.simd) {
    if (!simd_used_in_header_) {
      WriteV128Decl();
    }
    Write(s_simd_source_declarations);
  }

  if (module_->features_used.threads) {
    Write(s_atomicops_source_declarations);
  }
}

void CWriter::WriteMultiCTop() {
  if (c_streams_.size() > 1) {
    assert(header_impl_name_.size() > 0);
    Write("/* Automatically generated by wasm2c */", Newline());
    Write("#include \"", header_impl_name_, "\"", Newline());
  }
}

void CWriter::WriteMultiCTopEmpty() {
  for (auto& stream : c_streams_) {
    if (stream->offset() == 0) {
      stream_ = stream;
      Write("/* Empty wasm2c generated file */\n");
      Write("typedef int dummy_def;");
    }
  }
}

void CWriter::DeclareStruct(const TypeVector& types) {
  const std::string name = MangleTypes(types);
  if (!typevector_structs_.insert(name).second) {
    return;
  }

  Write(Newline(), "#ifndef ", name, Newline());
  Write("#define ", name, " ", name, Newline());
  Write("struct ", name, " ", OpenBrace());
  for (Index i = 0; i < types.size(); ++i) {
    const Type type = types[i];
    Write(type);
    Writef(" %c%d;", MangleType(type), i);
    Write(Newline());
  }
  Write(CloseBrace(), ";", Newline(), "#endif  /* ", name, " */", Newline());
}

void CWriter::WriteTailcalleeParamTypes() {
  // A structure for the spilled parameters of a tail-callee is needed in
  // three cases: for a function that makes a tail-call (and therefore
  // will have a tail-callee version generated), for any imported function
  // (for which wasm2c generates a weak import that presents the tail-callee
  // interface, in case the exporting module doesn't generate one itself), and
  // for any type entry referenced in a return_call_indirect instruction.

  for (const Func* func : module_->funcs) {
    if (IsImport(func->name) || func->features_used.tailcall) {
      if (func->decl.sig.GetNumParams() > 1) {
        DeclareStruct(func->decl.sig.param_types);
      }
    }
  }

  for (TypeEntry* type : module_->types) {
    FuncType* func_type = cast<FuncType>(type);
    if (func_type->GetNumParams() > 1 && func_type->features_used.tailcall) {
      DeclareStruct(func_type->sig.param_types);
    }
  }
}

void CWriter::WriteMultivalueResultTypes() {
  for (TypeEntry* type : module_->types) {
    FuncType* func_type = cast<FuncType>(type);
    if (func_type->GetNumResults() > 1) {
      DeclareStruct(func_type->sig.result_types);
    }
  }
}

void CWriter::WriteTagTypes() {
  for (const Tag* tag : module_->tags) {
    const FuncDeclaration& tag_type = tag->decl;
    Index num_params = tag_type.GetNumParams();
    if (num_params <= 1) {
      continue;
    }
    DeclareStruct(tag_type.sig.param_types);
  }
}

void CWriter::WriteFuncTypeDecls() {
  if (module_->types.empty()) {
    return;
  }

  Write(Newline());

  std::string serialized_type;
  for (const TypeEntry* type : module_->types) {
    const std::string name =
        DefineGlobalScopeName(ModuleFieldType::Type, type->name);

    if (c_streams_.size() > 1) {
      Write("FUNC_TYPE_DECL_EXTERN_T(", name, ");", Newline());
    }
  }
}

void CWriter::WriteFuncTypes() {
  if (module_->types.empty()) {
    return;
  }

  Write(Newline());

  std::unordered_map<std::string, std::string> type_hash;

  std::string serialized_type;
  for (const TypeEntry* type : module_->types) {
    const std::string name = GetGlobalName(ModuleFieldType::Type, type->name);
    SerializeFuncType(*cast<FuncType>(type), serialized_type);

    auto prior_type = type_hash.find(serialized_type);
    if (prior_type != type_hash.end()) {
      /* duplicate function type */
      unique_func_type_names_.push_back(prior_type->second);
    } else {
      unique_func_type_names_.push_back(name);
      type_hash.emplace(serialized_type, name);
      if (c_streams_.size() > 1) {
        Write("FUNC_TYPE_EXTERN_T(");
      } else {
        Write("FUNC_TYPE_T(");
      }
      Write(name, ") = \"");
      for (uint8_t x : serialized_type) {
        Writef("\\x%02x", x);
      }
      Write("\";", Newline());
    }
  }
}

void CWriter::Write(const FuncTypeExpr& expr) {
  Index func_type_index = module_->GetFuncTypeIndex(expr.func_type->sig);
  Write(unique_func_type_names_.at(func_type_index));
}

// static
void CWriter::SerializeFuncType(const FuncType& func_type,
                                std::string& serialized_type) {
  unsigned int len = func_type.GetNumParams() + func_type.GetNumResults() + 1;

  char* const mangled_signature = static_cast<char*>(alloca(len));
  char* next_byte = mangled_signature;

  // step 1: serialize each param type
  for (Index i = 0; i < func_type.GetNumParams(); ++i) {
    *next_byte++ = MangleType(func_type.GetParamType(i));
  }

  // step 2: separate params and results with a space
  *next_byte++ = ' ';

  // step 3: serialize each result type
  for (Index i = 0; i < func_type.GetNumResults(); ++i) {
    *next_byte++ = MangleType(func_type.GetResultType(i));
  }

  assert(next_byte - mangled_signature == len);

  // step 4: SHA-256 the whole string
  sha256({mangled_signature, len}, serialized_type);
}

void CWriter::WriteTagDecls() {
  Index tag_index = 0;
  for (const Tag* tag : module_->tags) {
    bool is_import = tag_index < module_->num_tag_imports;
    if (!is_import) {
      // Tags are identified and compared solely by their (unique) address.
      // The data stored in this variable is never read.
      if (tag_index == module_->num_tag_imports) {
        Write(Newline());
        Write("typedef char wasm_tag_placeholder_t;", Newline());
      }
      DefineGlobalScopeName(ModuleFieldType::Tag, tag->name);
      if (c_streams_.size() > 1) {
        Write("extern const wasm_tag_placeholder_t ",
              GlobalName(ModuleFieldType::Tag, tag->name), ";", Newline());
      }
    }
    tag_index++;
  }
}

void CWriter::WriteTags() {
  Write(Newline());
  Index tag_index = 0;
  for (const Tag* tag : module_->tags) {
    bool is_import = tag_index < module_->num_tag_imports;
    if (!is_import) {
      Write(InternalSymbolScope(), "const wasm_tag_placeholder_t ",
            GlobalName(ModuleFieldType::Tag, tag->name), ";", Newline());
    }
    tag_index++;
  }
}

void CWriter::ComputeUniqueImports() {
  using modname_name_pair = std::pair<std::string, std::string>;
  std::map<modname_name_pair, const Import*> import_map;
  for (const Import* import : module_->imports) {
    // After emplacing, the returned bool says whether the insert happened;
    // i.e., was there already an import with the same modname and name?
    // If there was, make sure it was at least the same kind of import.
    const auto iterator_and_insertion_bool = import_map.emplace(
        modname_name_pair(import->module_name, import->field_name), import);
    if (!iterator_and_insertion_bool.second) {
      if (iterator_and_insertion_bool.first->second->kind() != import->kind()) {
        UNIMPLEMENTED("contradictory import declaration");
      } else {
        fprintf(stderr, "warning: duplicate import declaration \"%s\" \"%s\"\n",
                import->module_name.c_str(), import->field_name.c_str());
      }
    }
    import_module_set_.insert(import->module_name);
    if (import->kind() == ExternalKind::Func) {
      import_func_module_set_.insert(import->module_name);
    }
  }

  for (const auto& node : import_map) {
    unique_imports_.push_back(node.second);
  }
}

void CWriter::BeginInstance() {
  if (module_->imports.empty()) {
    Write("typedef struct ", ModuleInstanceTypeName(), " ", OpenBrace());
    return;
  }

  ComputeUniqueImports();

  // define names of per-instance imports
  for (const Import* import : module_->imports) {
    DefineImportName(import, import->module_name, import->field_name);
  }

  // Forward declaring module instance types
  for (const auto& import_module : import_module_set_) {
    DefineImportedModuleInstanceName(import_module);
    Write("struct ", ModuleInstanceTypeName(import_module), ";", Newline());
  }

  // Forward declaring module imports
  for (const Import* import : unique_imports_) {
    if ((import->kind() == ExternalKind::Func) ||
        (import->kind() == ExternalKind::Tag)) {
      continue;
    }

    Write("extern ");
    switch (import->kind()) {
      case ExternalKind::Global: {
        const Global& global = cast<GlobalImport>(import)->global;
        Write(global.type);
        break;
      }

      case ExternalKind::Memory: {
        Write("wasm_rt_memory_t");
        break;
      }

      case ExternalKind::Table:
        WriteTableType(cast<TableImport>(import)->table.elem_type);
        break;

      default:
        WABT_UNREACHABLE;
    }
    Write("* ", ExportName(import->module_name, import->field_name), "(struct ",
          ModuleInstanceTypeName(import->module_name), "*);", Newline());
  }
  Write(Newline());

  // Add pointers to module instances that any func is imported from,
  // so that imported functions can be given their own module instances
  // when invoked
  Write("typedef struct ", ModuleInstanceTypeName(), " ", OpenBrace());
  for (const auto& import_module : import_func_module_set_) {
    Write("struct ", ModuleInstanceTypeName(import_module), "* ",
          GlobalName(ModuleFieldType::Import, import_module), ";", Newline());
  }

  for (const Import* import : unique_imports_) {
    if ((import->kind() == ExternalKind::Func) ||
        (import->kind() == ExternalKind::Tag)) {
      continue;
    }

    Write("/* import: '", SanitizeForComment(import->module_name), "' '",
          SanitizeForComment(import->field_name), "' */", Newline());

    switch (import->kind()) {
      case ExternalKind::Global:
        WriteGlobal(cast<GlobalImport>(import)->global,
                    std::string("*") +
                        ExportName(import->module_name, import->field_name));
        break;

      case ExternalKind::Memory:
        WriteMemory(std::string("*") +
                    ExportName(import->module_name, import->field_name));
        break;

      case ExternalKind::Table: {
        const Table& table = cast<TableImport>(import)->table;
        WriteTable(std::string("*") +
                       ExportName(import->module_name, import->field_name),
                   table.elem_type);
      } break;

      default:
        WABT_UNREACHABLE;
    }

    Write(Newline());
  }
}

// Write module-wide imports (funcs & tags), which aren't tied to an instance.
void CWriter::WriteImports() {
  if (unique_imports_.empty())
    return;

  Write(Newline());

  for (const Import* import : unique_imports_) {
    if (import->kind() == ExternalKind::Func) {
      Write(Newline(), "/* import: '", SanitizeForComment(import->module_name),
            "' '", SanitizeForComment(import->field_name), "' */", Newline());
      const Func& func = cast<FuncImport>(import)->func;
      WriteImportFuncDeclaration(
          func.decl, import->module_name,
          ExportName(import->module_name, import->field_name));
      Write(";", Newline());
      if (options_.features.tail_call_enabled()) {
        WriteTailCallFuncDeclaration(GetTailCallRef(func.name));
        Write(";", Newline());
      }
    } else if (import->kind() == ExternalKind::Tag) {
      Write(Newline(), "/* import: '", SanitizeForComment(import->module_name),
            "' '", SanitizeForComment(import->field_name), "' */", Newline());
      Write("extern const wasm_rt_tag_t ",
            ExportName(import->module_name, import->field_name), ";",
            Newline());
    }
  }
}

void CWriter::WriteTailCallWeakImports() {
  for (const Import* import : unique_imports_) {
    if (import->kind() != ExternalKind::Func) {
      continue;
    }
    const Func& func = cast<FuncImport>(import)->func;
    Write(Newline(), "/* handler for missing tail-call on import: '",
          SanitizeForComment(import->module_name), "' '",
          SanitizeForComment(import->field_name), "' */", Newline());
    Write("WEAK_FUNC_DECL(",
          TailCallExportName(import->module_name, import->field_name), ", ",
          kTailCallFallbackPrefix, module_prefix_, "_",
          ExportName(import->module_name, import->field_name), ")", Newline());
    Write(OpenBrace(), "next->fn = NULL;", Newline());

    Index num_params = func.GetNumParams();
    Index num_results = func.GetNumResults();
    if (num_params >= 1) {
      Write(func.decl.sig.param_types, " params;", Newline());
      Write("wasm_rt_memcpy(params, tail_call_stack, sizeof(params);",
            Newline());
    }

    if (num_results >= 1) {
      Write(func.decl.sig.result_types, " result = ");
    }

    Write(ExportName(import->module_name, import->field_name),
          "(*instance_ptr");

    if (num_params == 1) {
      Write(", params");
    } else {
      for (Index i = 0; i < num_params; ++i) {
        Writef(", params.%c%d", MangleType(func.GetParamType(i)), i);
      }
    }

    Write(");", Newline());

    if (num_results >= 1) {
      Write("wasm_rt_memcpy(tail_call_stack, &result, sizeof(result));",
            Newline());
    }

    Write(CloseBrace(), Newline());
  }
}

void CWriter::WriteFuncDeclarations() {
  if (module_->funcs.size() == module_->num_func_imports)
    return;

  Write(Newline());

  Index func_index = 0;
  for (const Func* func : module_->funcs) {
    bool is_import = func_index < module_->num_func_imports;
    if (!is_import) {
      Write(InternalSymbolScope());
      WriteFuncDeclaration(
          func->decl, DefineGlobalScopeName(ModuleFieldType::Func, func->name));
      Write(";", Newline());

      if (func->features_used.tailcall) {
        Write(InternalSymbolScope());
        WriteTailCallFuncDeclaration(GetTailCallRef(func->name));
        Write(";", Newline());
      }
    }
    ++func_index;
  }
}

void CWriter::WriteFuncDeclaration(const FuncDeclaration& decl,
                                   const std::string& name) {
  Write(decl.sig.result_types, " ", name, "(");
  Write(ModuleInstanceTypeName(), "*");
  WriteParamTypes(decl);
  Write(")");
}

void CWriter::WriteTailCallFuncDeclaration(const std::string& mangled_name) {
  Write("void ", mangled_name,
        "(void **instance_ptr, void *tail_call_stack, wasm_rt_tailcallee_t "
        "*next)");
}

void CWriter::WriteImportFuncDeclaration(const FuncDeclaration& decl,
                                         const std::string& module_name,
                                         const std::string& name) {
  Write(decl.sig.result_types, " ", name, "(");
  Write("struct ", ModuleInstanceTypeName(module_name), "*");
  WriteParamTypes(decl);
  Write(")");
}

void CWriter::WriteCallIndirectFuncDeclaration(const FuncDeclaration& decl,
                                               const std::string& name) {
  Write(decl.sig.result_types, " ", name, "(void*");
  WriteParamTypes(decl);
  Write(")");
}

static bool func_uses_simd(const FuncSignature& sig) {
  return std::any_of(sig.param_types.begin(), sig.param_types.end(),
                     [](auto x) { return x == Type::V128; }) ||
         std::any_of(sig.result_types.begin(), sig.result_types.end(),
                     [](auto x) { return x == Type::V128; });
}

void CWriter::ComputeSimdScope() {
  simd_used_in_header_ =
      module_->features_used.simd &&
      (std::any_of(module_->globals.begin(), module_->globals.end(),
                   [](const auto& x) { return x->type == Type::V128; }) ||
       std::any_of(module_->imports.begin(), module_->imports.end(),
                   [](const auto& x) {
                     return x->kind() == ExternalKind::Func &&
                            func_uses_simd(cast<FuncImport>(x)->func.decl.sig);
                   }) ||
       std::any_of(module_->exports.begin(), module_->exports.end(),
                   [&](const auto& x) {
                     return x->kind == ExternalKind::Func &&
                            func_uses_simd(module_->GetFunc(x->var)->decl.sig);
                   }));
}

void CWriter::WriteHeaderIncludes() {
  Write("#include \"wasm-rt.h\"", Newline());

  if (module_->features_used.exceptions) {
    Write("#include \"wasm-rt-exceptions.h\"", Newline(), Newline());
  }

  if (simd_used_in_header_) {
    WriteV128Decl();
  }

  Write(Newline());
}

void CWriter::WriteV128Decl() {
  Write("#include <simde/wasm/simd128.h>", Newline(), Newline());
  Write("#ifndef WASM_RT_SIMD_TYPE_DEFINED", Newline(),
        "#define WASM_RT_SIMD_TYPE_DEFINED", Newline(),
        "typedef simde_v128_t v128;", Newline(), "#endif", Newline(),
        Newline());
}

void CWriter::WriteModuleInstance() {
  BeginInstance();
  WriteGlobals();
  WriteMemories();
  WriteTables();
  WriteDataInstances();
  WriteElemInstances();

  // C forbids an empty struct
  if (module_->globals.empty() && module_->memories.empty() &&
      module_->tables.empty() && import_func_module_set_.empty()) {
    Write("char dummy_member;", Newline());
  }

  Write(CloseBrace(), " ", ModuleInstanceTypeName(), ";", Newline());
  Write(Newline());
}

void CWriter::WriteGlobals() {
  Index global_index = 0;
  if (module_->globals.size() != module_->num_global_imports) {
    for (const Global* global : module_->globals) {
      bool is_import = global_index < module_->num_global_imports;
      if (!is_import) {
        WriteGlobal(*global, DefineInstanceMemberName(ModuleFieldType::Global,
                                                      global->name));
        Write(Newline());
      }
      ++global_index;
    }
  }
}

void CWriter::WriteGlobal(const Global& global, const std::string& name) {
  Write(global.type, " ", name, ";");
}

void CWriter::WriteGlobalPtr(const Global& global, const std::string& name) {
  Write(global.type, "* ", name, "(", ModuleInstanceTypeName(), "* instance)");
}

void CWriter::WriteMemories() {
  if (module_->memories.size() == module_->num_memory_imports)
    return;

  Index memory_index = 0;
  for (const Memory* memory : module_->memories) {
    bool is_import = memory_index < module_->num_memory_imports;
    if (!is_import) {
      WriteMemory(
          DefineInstanceMemberName(ModuleFieldType::Memory, memory->name));
      Write(Newline());
    }
    ++memory_index;
  }
}

void CWriter::WriteMemory(const std::string& name) {
  Write("wasm_rt_memory_t ", name, ";");
}

void CWriter::WriteMemoryPtr(const std::string& name) {
  Write("wasm_rt_memory_t* ", name, "(", ModuleInstanceTypeName(),
        "* instance)");
}

void CWriter::WriteTables() {
  if (module_->tables.size() == module_->num_table_imports) {
    return;
  }

  Index table_index = 0;
  for (const Table* table : module_->tables) {
    bool is_import = table_index < module_->num_table_imports;
    if (!is_import) {
      WriteTable(DefineInstanceMemberName(ModuleFieldType::Table, table->name),
                 table->elem_type);
      Write(Newline());
    }
    ++table_index;
  }
}

void CWriter::WriteTable(const std::string& name, const wabt::Type& type) {
  WriteTableType(type);
  Write(" ", name, ";");
}

void CWriter::WriteTablePtr(const std::string& name, const Table& table) {
  WriteTableType(table.elem_type);
  Write("* ", name, "(", ModuleInstanceTypeName(), "* instance)");
}

void CWriter::WriteTableType(const wabt::Type& type) {
  Write("wasm_rt_", GetReferenceTypeName(type), "_table_t");
}

void CWriter::WriteGlobalInitializers() {
  if (module_->globals.empty())
    return;

  Write(Newline(), "static void init_globals(", ModuleInstanceTypeName(),
        "* instance) ", OpenBrace());
  Index global_index = 0;
  for (const Global* global : module_->globals) {
    bool is_import = global_index < module_->num_global_imports;
    if (!is_import) {
      assert(!global->init_expr.empty());
      Write(ExternalInstanceRef(ModuleFieldType::Global, global->name), " = ");
      WriteInitExpr(global->init_expr);
      Write(";", Newline());
    }
    ++global_index;
  }
  Write(CloseBrace(), Newline());
}

static inline bool is_droppable(const DataSegment* data_segment) {
  return (data_segment->kind == SegmentKind::Passive) &&
         (!data_segment->data.empty());
}

static inline bool is_droppable(const ElemSegment* elem_segment) {
  return (elem_segment->kind == SegmentKind::Passive) &&
         (!elem_segment->elem_exprs.empty());
}

void CWriter::WriteDataInstances() {
  for (const DataSegment* data_segment : module_->data_segments) {
    std::string name =
        DefineGlobalScopeName(ModuleFieldType::DataSegment, data_segment->name);
    if (is_droppable(data_segment)) {
      Write("bool ", "data_segment_dropped_", name, " : 1;", Newline());
    }
  }
}

void CWriter::WriteDataInitializerDecls() {
  if (module_->memories.empty()) {
    return;
  }

  for (const DataSegment* data_segment : module_->data_segments) {
    if (data_segment->data.empty()) {
      continue;
    }

    if (c_streams_.size() > 1) {
      Write(Newline(), "extern const u8 data_segment_data_",
            GlobalName(ModuleFieldType::DataSegment, data_segment->name), "[];",
            Newline());
    }
  }
}

void CWriter::WriteDataInitializers() {
  if (module_->memories.empty()) {
    return;
  }

  for (const DataSegment* data_segment : module_->data_segments) {
    if (data_segment->data.empty()) {
      continue;
    }

    Write(Newline(), InternalSymbolScope(), "const u8 data_segment_data_",
          GlobalName(ModuleFieldType::DataSegment, data_segment->name),
          "[] = ", OpenBrace());
    size_t i = 0;
    for (uint8_t x : data_segment->data) {
      Writef("0x%02x, ", x);
      if ((++i % 12) == 0)
        Write(Newline());
    }
    if (i > 0)
      Write(Newline());
    Write(CloseBrace(), ";", Newline());
  }

  Write(Newline(), "static void init_memories(", ModuleInstanceTypeName(),
        "* instance) ", OpenBrace());
  if (module_->memories.size() > module_->num_memory_imports) {
    Index memory_idx = module_->num_memory_imports;
    for (Index i = memory_idx; i < module_->memories.size(); i++) {
      const Memory* memory = module_->memories[i];
      uint64_t max;
      if (memory->page_limits.has_max) {
        max = memory->page_limits.max;
      } else {
        max = memory->page_limits.is_64 ? (static_cast<uint64_t>(1) << 48)
                                        : 65536;
      }
      Write("wasm_rt_allocate_memory(",
            ExternalInstancePtr(ModuleFieldType::Memory, memory->name), ", ",
            memory->page_limits.initial, ", ", max, ", ",
            memory->page_limits.is_64, ");", Newline());
    }
  }

  for (const DataSegment* data_segment : module_->data_segments) {
    if (data_segment->kind != SegmentKind::Active) {
      continue;
    }
    const Memory* memory =
        module_->memories[module_->GetMemoryIndex(data_segment->memory_var)];
    Write("LOAD_DATA(",
          ExternalInstanceRef(ModuleFieldType::Memory, memory->name), ", ");
    WriteInitExpr(data_segment->offset);
    if (data_segment->data.empty()) {
      Write(", NULL, 0");
    } else {
      Write(", data_segment_data_",
            GlobalName(ModuleFieldType::DataSegment, data_segment->name), ", ",
            data_segment->data.size());
    }
    Write(");", Newline());
  }

  Write(CloseBrace(), Newline());

  if (!module_->data_segments.empty()) {
    Write(Newline(), "static void init_data_instances(",
          ModuleInstanceTypeName(), " *instance) ", OpenBrace());

    for (const DataSegment* data_segment : module_->data_segments) {
      if (is_droppable(data_segment)) {
        Write("instance->data_segment_dropped_",
              GlobalName(ModuleFieldType::DataSegment, data_segment->name),
              " = false;", Newline());
      }
    }

    Write(CloseBrace(), Newline());
  }
}

void CWriter::WriteElemInstances() {
  for (const ElemSegment* elem_segment : module_->elem_segments) {
    std::string name =
        DefineGlobalScopeName(ModuleFieldType::ElemSegment, elem_segment->name);
    if (is_droppable(elem_segment)) {
      Write("bool ", "elem_segment_dropped_", name, " : 1;", Newline());
    }
  }
}

void CWriter::WriteElemInitializerDecls() {
  if (module_->tables.empty()) {
    return;
  }

  for (const ElemSegment* elem_segment : module_->elem_segments) {
    if (elem_segment->elem_exprs.empty()) {
      continue;
    }

    if (elem_segment->elem_type == Type::ExternRef) {
      // no need to store externref elem initializers because only
      // ref.null is possible
      continue;
    }

    if (c_streams_.size() > 1) {
      Write(Newline(),
            "extern const wasm_elem_segment_expr_t elem_segment_exprs_",
            GlobalName(ModuleFieldType::ElemSegment, elem_segment->name), "[];",
            Newline());
    }
  }
}

void CWriter::WriteElemInitializers() {
  if (module_->tables.empty()) {
    return;
  }

  for (const ElemSegment* elem_segment : module_->elem_segments) {
    if (elem_segment->elem_exprs.empty()) {
      continue;
    }

    if (elem_segment->elem_type == Type::ExternRef) {
      // no need to store externref elem initializers because only
      // ref.null is possible
      continue;
    }

    Write(Newline(), InternalSymbolScope(),
          "const wasm_elem_segment_expr_t elem_segment_exprs_",
          GlobalName(ModuleFieldType::ElemSegment, elem_segment->name),
          "[] = ", OpenBrace());

    for (const ExprList& elem_expr : elem_segment->elem_exprs) {
      assert(elem_expr.size() == 1);
      const Expr& expr = elem_expr.front();
      switch (expr.type()) {
        case ExprType::RefFunc: {
          const Func* func = module_->GetFunc(cast<RefFuncExpr>(&expr)->var);
          const FuncType* func_type = module_->GetFuncType(func->decl.type_var);
          Write("{RefFunc, ", FuncTypeExpr(func_type),
                ", (wasm_rt_function_ptr_t)",
                ExternalRef(ModuleFieldType::Func, func->name), ", {");
          if (options_.features.tail_call_enabled() &&
              (IsImport(func->name) || func->features_used.tailcall)) {
            Write(TailCallRef(func->name));
          } else {
            Write("NULL");
          }
          Write("}, ");

          if (IsImport(func->name)) {
            Write("offsetof(", ModuleInstanceTypeName(), ", ",
                  GlobalName(ModuleFieldType::Import,
                             import_module_sym_map_[func->name]),
                  ")");
          } else {
            Write("0");
          }
          Write("},", Newline());
        } break;
        case ExprType::RefNull:
          Write("{RefNull, NULL, NULL, {NULL}, 0},", Newline());
          break;
        case ExprType::GlobalGet: {
          const Global* global =
              module_->GetGlobal(cast<GlobalGetExpr>(&expr)->var);
          assert(IsImport(global->name));
          Write("{GlobalGet, NULL, NULL, {NULL}, offsetof(",
                ModuleInstanceTypeName(), ", ",
                GlobalName(ModuleFieldType::Global, global->name), ")},",
                Newline());
        } break;
        default:
          WABT_UNREACHABLE;
      }
    }
    Write(CloseBrace(), ";", Newline());
  }

  Write(Newline(), "static void init_tables(", ModuleInstanceTypeName(),
        "* instance) ", OpenBrace());

  if (module_->tables.size() > module_->num_table_imports) {
    Index table_idx = module_->num_table_imports;
    for (Index i = table_idx; i < module_->tables.size(); i++) {
      const Table* table = module_->tables[i];
      uint32_t max =
          table->elem_limits.has_max ? table->elem_limits.max : UINT32_MAX;
      Write("wasm_rt_allocate_", GetReferenceTypeName(table->elem_type),
            "_table(", ExternalInstancePtr(ModuleFieldType::Table, table->name),
            ", ", table->elem_limits.initial, ", ", max, ");", Newline());
    }
  }

  for (const ElemSegment* elem_segment : module_->elem_segments) {
    if (elem_segment->kind != SegmentKind::Active) {
      continue;
    }

    const Table* table = module_->GetTable(elem_segment->table_var);

    WriteElemTableInit(true, elem_segment, table);
  }

  Write(CloseBrace(), Newline());

  if (!module_->elem_segments.empty()) {
    Write(Newline(), "static void init_elem_instances(",
          ModuleInstanceTypeName(), " *instance) ", OpenBrace());

    for (const ElemSegment* elem_segment : module_->elem_segments) {
      if (is_droppable(elem_segment)) {
        Write("instance->elem_segment_dropped_",
              GlobalName(ModuleFieldType::ElemSegment, elem_segment->name),
              " = false;", Newline());
      }
    }

    Write(CloseBrace(), Newline());
  }
}

void CWriter::WriteElemTableInit(bool active_initialization,
                                 const ElemSegment* src_segment,
                                 const Table* dst_table) {
  assert(dst_table->elem_type == Type::FuncRef ||
         dst_table->elem_type == Type::ExternRef);
  assert(dst_table->elem_type == src_segment->elem_type);

  Write(GetReferenceTypeName(dst_table->elem_type), "_table_init(",
        ExternalInstancePtr(ModuleFieldType::Table, dst_table->name), ", ");

  // elem segment exprs needed only for funcref tables
  // because externref tables can only be initialized with ref.null
  if (dst_table->elem_type == Type::FuncRef) {
    if (src_segment->elem_exprs.empty()) {
      Write("NULL, ");
    } else {
      Write("elem_segment_exprs_",
            GlobalName(ModuleFieldType::ElemSegment, src_segment->name), ", ");
    }
  }

  // src_size, dest_addr, src_addr, N
  if (active_initialization) {
    Write(src_segment->elem_exprs.size(), ", ");
    WriteInitExpr(src_segment->offset);
    Write(", 0, ", src_segment->elem_exprs.size());
  } else {
    if (is_droppable(src_segment)) {
      Write("(instance->elem_segment_dropped_",
            GlobalName(ModuleFieldType::ElemSegment, src_segment->name),
            " ? 0 : ", src_segment->elem_exprs.size(), "), ");
    } else {
      Write("0, ");
    }
    Write(StackVar(2), ", ", StackVar(1), ", ", StackVar(0));
  }

  if (dst_table->elem_type == Type::FuncRef) {
    Write(", instance");
  }

  Write(");", Newline());
}

void CWriter::WriteExports(CWriterPhase kind) {
  if (module_->exports.empty())
    return;

  for (const Export* export_ : module_->exports) {
    Write(Newline(), "/* export: '", SanitizeForComment(export_->name), "' */",
          Newline());

    const std::string mangled_name = ExportName(export_->name);
    std::string internal_name;
    std::vector<std::string> index_to_name;

    switch (export_->kind) {
      case ExternalKind::Func: {
        const Func* func = module_->GetFunc(export_->var);
        internal_name = func->name;
        if (kind == CWriterPhase::Declarations) {
          WriteFuncDeclaration(func->decl, mangled_name);
        } else {
          func_ = func;
          local_syms_ = global_syms_;
          local_sym_map_.clear();
          stack_var_sym_map_.clear();
          Write(func_->decl.sig.result_types, " ", mangled_name, "(");
          MakeTypeBindingReverseMapping(func_->GetNumParamsAndLocals(),
                                        func_->bindings, &index_to_name);
          WriteParams(index_to_name);
        }
        break;
      }

      case ExternalKind::Global: {
        const Global* global = module_->GetGlobal(export_->var);
        internal_name = global->name;
        WriteGlobalPtr(*global, mangled_name);
        break;
      }

      case ExternalKind::Memory: {
        const Memory* memory = module_->GetMemory(export_->var);
        internal_name = memory->name;
        WriteMemoryPtr(mangled_name);
        break;
      }

      case ExternalKind::Table: {
        const Table* table = module_->GetTable(export_->var);
        internal_name = table->name;
        WriteTablePtr(mangled_name, *table);
        break;
      }

      case ExternalKind::Tag: {
        const Tag* tag = module_->GetTag(export_->var);
        internal_name = tag->name;
        if (kind == CWriterPhase::Declarations) {
          Write("extern ");
        }
        Write("const wasm_rt_tag_t ", mangled_name);
        break;
      }

      default:
        WABT_UNREACHABLE;
    }

    if (kind == CWriterPhase::Declarations) {
      Write(";", Newline());
      continue;
    }

    Write(" ");
    switch (export_->kind) {
      case ExternalKind::Func: {
        Write(OpenBrace());
        if (func_->GetNumResults() > 0) {
          Write("return ");
        }
        Write(ExternalRef(ModuleFieldType::Func, internal_name), "(");

        if (IsImport(internal_name)) {
          Write("instance->",
                GlobalName(ModuleFieldType::Import,
                           import_module_sym_map_[internal_name]));
        } else {
          Write("instance");
        }
        WriteParamSymbols(index_to_name);
        Write(CloseBrace(), Newline());

        local_sym_map_.clear();
        stack_var_sym_map_.clear();
        func_ = nullptr;
        break;
      }

      case ExternalKind::Global:
        Write(OpenBrace());
        Write("return ",
              ExternalInstancePtr(ModuleFieldType::Global, internal_name), ";",
              Newline());
        Write(CloseBrace(), Newline());
        break;

      case ExternalKind::Memory:
        Write(OpenBrace());
        Write("return ",
              ExternalInstancePtr(ModuleFieldType::Memory, internal_name), ";",
              Newline());
        Write(CloseBrace(), Newline());
        break;

      case ExternalKind::Table:
        Write(OpenBrace());
        Write("return ",
              ExternalInstancePtr(ModuleFieldType::Table, internal_name), ";",
              Newline());
        Write(CloseBrace(), Newline());
        break;

      case ExternalKind::Tag:
        Write("= ", TagSymbol(internal_name), ";", Newline());
        break;

      default:
        WABT_UNREACHABLE;
    }
  }
}

void CWriter::WriteTailCallExports(CWriterPhase kind) {
  for (const Export* export_ : module_->exports) {
    if (export_->kind != ExternalKind::Func) {
      continue;
    }

    const Func* func = module_->GetFunc(export_->var);

    if (!IsImport(func->name) && !func->features_used.tailcall) {
      continue;
    }

    const std::string mangled_name = TailCallExportName(export_->name);

    Write(Newline(), "/* export for tail-call of '",
          SanitizeForComment(export_->name), "' */", Newline());
    if (kind == CWriterPhase::Declarations) {
      WriteTailCallFuncDeclaration(mangled_name);
      Write(";", Newline());
    } else {
      WriteTailCallFuncDeclaration(mangled_name);
      Write(" ", OpenBrace());
      const Func* func = module_->GetFunc(export_->var);
      Write(TailCallRef(func->name), "(instance_ptr, tail_call_stack, next);",
            Newline(), CloseBrace(), Newline());
    }
  }
}

void CWriter::WriteInit() {
  Write(Newline(), "void ", kAdminSymbolPrefix, module_prefix_, "_instantiate(",
        ModuleInstanceTypeName(), "* instance");
  for (const auto& import_module_name : import_module_set_) {
    Write(", struct ", ModuleInstanceTypeName(import_module_name), "* ",
          GlobalName(ModuleFieldType::Import, import_module_name));
  }
  Write(") ", OpenBrace());

  Write("assert(wasm_rt_is_initialized());", Newline());

  if (!import_module_set_.empty()) {
    Write("init_instance_import(instance");
    for (const auto& import_module_name : import_module_set_) {
      Write(", ", GlobalName(ModuleFieldType::Import, import_module_name));
    }
    Write(");", Newline());
  }

  if (!module_->globals.empty()) {
    Write("init_globals(instance);", Newline());
  }
  if (!module_->tables.empty()) {
    Write("init_tables(instance);", Newline());
  }
  if (!module_->memories.empty()) {
    Write("init_memories(instance);", Newline());
  }
  if (!module_->tables.empty() && !module_->elem_segments.empty()) {
    Write("init_elem_instances(instance);", Newline());
  }
  if (!module_->memories.empty() && !module_->data_segments.empty()) {
    Write("init_data_instances(instance);", Newline());
  }

  for (Var* var : module_->starts) {
    Write(ExternalRef(ModuleFieldType::Func, module_->GetFunc(*var)->name));
    if (IsImport(module_->GetFunc(*var)->name)) {
      Write("(instance->",
            GlobalName(ModuleFieldType::Import,
                       import_module_sym_map_[module_->GetFunc(*var)->name]),
            ");");
    } else {
      Write("(instance);");
    }
    Write(Newline());
  }
  Write(CloseBrace(), Newline());
}

void CWriter::WriteGetFuncType() {
  Write(Newline(), "wasm_rt_func_type_t ", kAdminSymbolPrefix, module_prefix_,
        "_get_func_type(uint32_t param_count, uint32_t result_count, "
        "...) ",
        OpenBrace());

  Write("va_list args;", Newline());

  for (const TypeEntry* type : module_->types) {
    const FuncType* func_type = cast<FuncType>(type);
    const FuncSignature& signature = func_type->sig;

    Write(Newline(), "if (param_count == ", signature.GetNumParams(),
          " && result_count == ", signature.GetNumResults(), ") ", OpenBrace());
    Write("va_start(args, result_count);", Newline());
    Write("if (true");
    for (const auto& t : signature.param_types) {
      Write(" && va_arg(args, wasm_rt_type_t) == ", TypeEnum(t));
    }
    for (const auto& t : signature.result_types) {
      Write(" && va_arg(args, wasm_rt_type_t) == ", TypeEnum(t));
    }
    Write(") ", OpenBrace());
    Write("va_end(args);", Newline());
    Write("return ", FuncTypeExpr(func_type), ";", Newline());
    Write(CloseBrace(), Newline());
    Write("va_end(args);", Newline());
    Write(CloseBrace(), Newline());
  }

  Write(Newline(), "return NULL;", Newline());
  Write(CloseBrace(), Newline());
}

void CWriter::WriteInitInstanceImport() {
  if (import_module_set_.empty())
    return;

  Write(Newline(), "static void init_instance_import(",
        ModuleInstanceTypeName(), "* instance");
  for (const auto& import_module_name : import_module_set_) {
    Write(", struct ", ModuleInstanceTypeName(import_module_name), "* ",
          GlobalName(ModuleFieldType::Import, import_module_name));
  }
  Write(") ", OpenBrace());

  for (const auto& import_module : import_func_module_set_) {
    Write("instance->", GlobalName(ModuleFieldType::Import, import_module),
          " = ", GlobalName(ModuleFieldType::Import, import_module), ";",
          Newline());
  }

  for (const Import* import : unique_imports_) {
    switch (import->kind()) {
      case ExternalKind::Func:
      case ExternalKind::Tag:
        break;

      case ExternalKind::Global:
      case ExternalKind::Memory:
      case ExternalKind::Table: {
        Write("instance->", ExportName(import->module_name, import->field_name),
              " = ", ExportName(import->module_name, import->field_name), "(",
              GlobalName(ModuleFieldType::Import, import->module_name), ");",
              Newline());
        break;
      }

      default:
        WABT_UNREACHABLE;
    }
  }
  Write(CloseBrace(), Newline());
}

void CWriter::WriteImportProperties(CWriterPhase kind) {
  if (import_module_set_.empty())
    return;

  Write(Newline());

  auto write_import_prop = [&](const Import* import, std::string prop,
                               std::string type, uint64_t value) {
    if (kind == CWriterPhase::Declarations) {
      Write("extern ");
    }
    Write("const ", type, " ", kAdminSymbolPrefix, module_prefix_, "_", prop,
          "_", MangleModuleName(import->module_name), "_",
          MangleName(import->field_name));
    if (kind == CWriterPhase::Definitions) {
      Write(" = ", value);
    }

    Write(";", Newline());
  };

  for (const Import* import : unique_imports_) {
    if (import->kind() == ExternalKind::Memory) {
      const Limits* limits = &(cast<MemoryImport>(import)->memory.page_limits);
      // We use u64 so we can handle both 32-bit and 64-bit memories
      const uint64_t default_max = limits->is_64
                                       ? (static_cast<uint64_t>(1) << 48)
                                       : (static_cast<uint64_t>(1) << 16);
      write_import_prop(import, "min", "u64", limits->initial);
      write_import_prop(import, "max", "u64",
                        limits->has_max ? limits->max : default_max);
      write_import_prop(import, "is64", "u8", limits->is_64);
    } else if (import->kind() == ExternalKind::Table) {
      const Limits* limits = &(cast<TableImport>(import)->table.elem_limits);
      const uint64_t default_max = std::numeric_limits<uint32_t>::max();
      write_import_prop(import, "min", "u32", limits->initial);
      write_import_prop(import, "max", "u32",
                        limits->has_max ? limits->max : default_max);
    } else {
      continue;
    }
  }
}

void CWriter::WriteFree() {
  Write(Newline(), "void ", kAdminSymbolPrefix, module_prefix_, "_free(",
        ModuleInstanceTypeName(), "* instance) ", OpenBrace());

  {
    Index table_index = 0;
    for (const Table* table : module_->tables) {
      bool is_import = table_index < module_->num_table_imports;
      if (!is_import) {
        Write("wasm_rt_free_", GetReferenceTypeName(table->elem_type),
              "_table(",
              ExternalInstancePtr(ModuleFieldType::Table, table->name), ");",
              Newline());
      }
      ++table_index;
    }
  }

  {
    Index memory_index = 0;
    for (const Memory* memory : module_->memories) {
      bool is_import = memory_index < module_->num_memory_imports;
      if (!is_import) {
        Write("wasm_rt_free_memory(",
              ExternalInstancePtr(ModuleFieldType::Memory, memory->name), ");",
              Newline());
      }
      ++memory_index;
    }
  }

  Write(CloseBrace(), Newline());
}

void CWriter::WriteFuncs() {
  std::vector<size_t> c_stream_assignment =
      name_to_output_file_index_(module_->funcs.begin(), module_->funcs.end(),
                                 module_->num_func_imports, c_streams_.size());
  Index func_index = 0;
  for (const Func* func : module_->funcs) {
    bool is_import = func_index < module_->num_func_imports;
    if (!is_import) {
      stream_ = c_streams_.at(c_stream_assignment.at(func_index));
      Write(*func);
      if (func->features_used.tailcall) {
        WriteTailCallee(*func);
      }
    }
    ++func_index;
  }
}

void CWriter::PushFuncSection(std::string_view include_condition) {
  func_sections_.emplace_back(include_condition, MemoryStream{});
  stream_ = &func_sections_.back().second;
}

bool CWriter::IsImport(const std::string& name) const {
  return import_module_sym_map_.count(name);
}

template <typename sources>
void CWriter::Spill(const TypeVector& types, const sources& src) {
  assert(types.size() > 0);

  if (types.size() == 1) {
    Write("tmp = ", src(0), ";", Newline());
    return;
  }

  for (Index i = 0; i < types.size(); ++i) {
    Writef("tmp.%c%d = ", MangleType(types.at(i)), i);
    Write(src(i), ";", Newline());
  }
}

void CWriter::Spill(const TypeVector& types) {
  Spill(types, [&](auto i) { return StackVar(types.size() - i - 1); });
}

template <typename targets>
void CWriter::Unspill(const TypeVector& types, const targets& tgt) {
  assert(types.size() > 0);

  if (types.size() == 1) {
    Write(tgt(0), " = tmp;", Newline());
    return;
  }

  for (Index i = 0; i < types.size(); ++i) {
    Write(tgt(i));
    Writef(" = tmp.%c%d;", MangleType(types.at(i)), i);
    Write(Newline());
  }
}

void CWriter::Unspill(const TypeVector& types) {
  Unspill(types, [&](auto i) { return StackVar(types.size() - i - 1); });
}

void CWriter::WriteTailCallAsserts(const FuncSignature& sig) {
  if (sig.param_types.size()) {
    Write("static_assert(sizeof(", sig.param_types, ") <= ", kTailCallStackSize,
          ");", Newline());
  }
  if (sig.result_types.size() && sig.result_types != sig.param_types) {
    Write("static_assert(sizeof(", sig.result_types,
          ") <= ", kTailCallStackSize, ");", Newline());
  }
}

void CWriter::WriteTailCallStack() {
  Write("void *instance_ptr_storage;", Newline());
  Write("void **instance_ptr = &instance_ptr_storage;", Newline());
  Write("char tail_call_stack[", std::to_string(kTailCallStackSize), "];",
        Newline());
  Write("wasm_rt_tailcallee_t next_storage;", Newline());
  Write("wasm_rt_tailcallee_t *next = &next_storage;", Newline());
}

void CWriter::WriteUnwindTryCatchStack(const Label* label) {
  assert(try_catch_stack_.size() >= label->try_catch_stack_size);

  if (try_catch_stack_.size() != label->try_catch_stack_size) {
    const std::string& name =
        try_catch_stack_.at(label->try_catch_stack_size).name;

    Write("wasm_rt_set_unwind_target(", name, "_outer_target);", Newline());
  }
}

void CWriter::FinishReturnCall() {
  if (in_tail_callee_) {
    Write(CloseBrace(), Newline(), "return;", Newline());
    return;
  }

  Write("while (next->fn) { next->fn(instance_ptr, tail_call_stack, next); }",
        Newline());
  PushTypes(func_->decl.sig.result_types);
  if (!func_->decl.sig.result_types.empty()) {
    Write(OpenBrace(), func_->decl.sig.result_types, " tmp;", Newline(),
          "wasm_rt_memcpy(&tmp, tail_call_stack, sizeof(tmp));", Newline());
    Unspill(func_->decl.sig.result_types);
    Write(CloseBrace(), Newline());
  }

  Write(CloseBrace(), Newline(), "goto ", LabelName(kImplicitFuncLabel), ";",
        Newline());
}

void CWriter::BeginFunction(const Func& func) {
  func_ = &func;
  in_tail_callee_ = false;
  local_syms_.clear();
  local_sym_map_.clear();
  stack_var_sym_map_.clear();
  func_sections_.clear();
  func_includes_.clear();

  /*
   * If offset of stream_ is 0, this is the first time some function is written
   * to this stream, then write multi c top.
   */
  if (stream_->offset() == 0) {
    WriteMultiCTop();
  }
  Write(Newline());
}

void CWriter::FinishFunction() {
  for (size_t i = 0; i < func_sections_.size(); ++i) {
    auto& [condition, stream] = func_sections_.at(i);
    std::unique_ptr<OutputBuffer> buf = stream.ReleaseOutputBuffer();
    if (condition.empty() || func_includes_.count(condition)) {
      stream_->WriteData(buf->data.data(), buf->data.size());
    }

    if (i == 0) {
      WriteStackVarDeclarations();  // these come immediately after section #0
                                    // (return type/name/params/locals)
    }
  }

  Write(CloseBrace(), Newline());

  func_ = nullptr;
}

void CWriter::Write(const Func& func) {
  Stream* prev_stream = stream_;
  BeginFunction(func);
  PushFuncSection();
  Write(func.decl.sig.result_types, " ",
        GlobalName(ModuleFieldType::Func, func.name), "(");
  WriteParamsAndLocals();
  Write("FUNC_PROLOGUE;", Newline());

  PushFuncSection();

  std::string label = DefineLabelName(kImplicitFuncLabel);
  ResetTypeStack(0);
  std::string empty;  // Must not be temporary, since address is taken by Label.
  PushLabel(LabelType::Func, empty, func.decl.sig);
  Write(func.exprs, LabelDecl(label));
  PopLabel();
  ResetTypeStack(0);
  PushTypes(func.decl.sig.result_types);
  Write("FUNC_EPILOGUE;", Newline());

  // Return the top of the stack implicitly.
  Index num_results = func.GetNumResults();
  if (num_results == 1) {
    Write("return ", StackVar(0), ";", Newline());
  } else if (num_results >= 2) {
    Write(OpenBrace(), func.decl.sig.result_types, " tmp;", Newline());
    Spill(func.decl.sig.result_types);
    Write("return tmp;", Newline(), CloseBrace(), Newline());
  }

  stream_ = prev_stream;
  FinishFunction();
}

template <typename Vars, typename TypeOf, typename ToDo>
void CWriter::WriteVarsByType(const Vars& vars,
                              const TypeOf& typeof,
                              const ToDo& todo) {
  for (Type type : {Type::I32, Type::I64, Type::F32, Type::F64, Type::V128,
                    Type::FuncRef, Type::ExternRef}) {
    Index var_index = 0;
    size_t count = 0;
    for (const auto& var : vars) {
      if (typeof(var) == type) {
        if (count == 0) {
          Write(type, " ");
          Indent(4);
        } else {
          Write(", ");
          if ((count % 8) == 0)
            Write(Newline());
        }

        todo(var_index, var);
        ++count;
      }
      ++var_index;
    }
    if (count != 0) {
      Dedent(4);
      Write(";", Newline());
    }
  }
}

void CWriter::WriteTailCallee(const Func& func) {
  Stream* prev_stream = stream_;
  BeginFunction(func);
  in_tail_callee_ = true;
  PushFuncSection();
  WriteTailCallFuncDeclaration(GetTailCallRef(func.name));
  Write(" ", OpenBrace());
  WriteTailCallAsserts(func.decl.sig);
  Write(ModuleInstanceTypeName(), "* instance = *instance_ptr;", Newline());

  std::vector<std::string> index_to_name;
  MakeTypeBindingReverseMapping(func.GetNumParamsAndLocals(), func.bindings,
                                &index_to_name);
  if (func.GetNumParams()) {
    WriteVarsByType(
        func.decl.sig.param_types, [](auto x) { return x; },
        [&](Index i, Type) { Write(DefineParamName(index_to_name[i])); });
    Write(OpenBrace(), func.decl.sig.param_types, " tmp;", Newline(),
          "wasm_rt_memcpy(&tmp, tail_call_stack, sizeof(tmp));", Newline());
    Unspill(func.decl.sig.param_types,
            [&](auto i) { return ParamName(index_to_name[i]); });
    Write(CloseBrace(), Newline());
  }

  WriteLocals(index_to_name);

  PushFuncSection();

  std::string label = DefineLabelName(kImplicitFuncLabel);
  ResetTypeStack(0);
  std::string empty;  // Must not be temporary, since address is taken by Label.
  PushLabel(LabelType::Func, empty, func.decl.sig);
  Write(func.exprs, LabelDecl(label));
  PopLabel();
  ResetTypeStack(0);
  PushTypes(func.decl.sig.result_types);

  // Return the top of the stack implicitly.
  if (func.GetNumResults()) {
    Write(OpenBrace(), func.decl.sig.result_types, " tmp;", Newline());
    Spill(func.decl.sig.result_types);
    Write("wasm_rt_memcpy(tail_call_stack, &tmp, sizeof(tmp));", Newline());
    Write(CloseBrace(), Newline());
  }
  Write("next->fn = NULL;", Newline());

  stream_ = prev_stream;
  FinishFunction();
}

void CWriter::WriteParamsAndLocals() {
  std::vector<std::string> index_to_name;
  MakeTypeBindingReverseMapping(func_->GetNumParamsAndLocals(), func_->bindings,
                                &index_to_name);
  WriteParams(index_to_name);
  Write(" ", OpenBrace());
  WriteLocals(index_to_name);
}

void CWriter::WriteParams(const std::vector<std::string>& index_to_name) {
  Write(ModuleInstanceTypeName(), "* instance");
  if (func_->GetNumParams() != 0) {
    Indent(4);
    for (Index i = 0; i < func_->GetNumParams(); ++i) {
      Write(", ");
      if (i != 0 && (i % 8) == 0) {
        Write(Newline());
      }
      Write(func_->GetParamType(i), " ", DefineParamName(index_to_name[i]));
    }
    Dedent(4);
  }
  Write(")");
}

void CWriter::WriteParamSymbols(const std::vector<std::string>& index_to_name) {
  if (func_->GetNumParams() != 0) {
    Indent(4);
    for (Index i = 0; i < func_->GetNumParams(); ++i) {
      Write(", ");
      if (i != 0 && (i % 8) == 0) {
        Write(Newline());
      }
      Write(ParamName(index_to_name[i]));
    }
    Dedent(4);
  }
  Write(");", Newline());
}

void CWriter::WriteParamTypes(const FuncDeclaration& decl) {
  if (decl.GetNumParams() != 0) {
    for (Index i = 0; i < decl.GetNumParams(); ++i) {
      Write(", ");
      Write(decl.GetParamType(i));
    }
  }
}

void CWriter::WriteLocals(const std::vector<std::string>& index_to_name) {
  Index num_params = func_->GetNumParams();
  WriteVarsByType(
      func_->local_types, [](auto x) { return x; },
      [&](Index local_index, Type local_type) {
        Write(DefineParamName(index_to_name[num_params + local_index]), " = ");
        if (local_type == Type::FuncRef || local_type == Type::ExternRef) {
          Write(GetReferenceNullValue(local_type));
        } else if (local_type == Type::V128) {
          Write("simde_wasm_i64x2_make(0, 0)");
        } else {
          Write("0");
        }
      });
}

void CWriter::WriteStackVarDeclarations() {
  WriteVarsByType(
      stack_var_sym_map_, [](auto stp_name) { return stp_name.first.second; },
      [&](Index, auto& stp_name) { Write(stp_name.second); });
}

void CWriter::Write(const Block& block) {
  std::string label = DefineLabelName(block.label);
  DropTypes(block.decl.GetNumParams());
  size_t mark = MarkTypeStack();
  PushLabel(LabelType::Block, block.label, block.decl.sig);
  PushTypes(block.decl.sig.param_types);
  Write(block.exprs, LabelDecl(label));
  ResetTypeStack(mark);
  PopLabel();
  PushTypes(block.decl.sig.result_types);
}

size_t CWriter::BeginTry(const TryExpr& tryexpr) {
  Write(OpenBrace()); /* beginning of try-catch */
  const std::string tlabel = DefineLabelName(tryexpr.block.label);
  Write("WASM_RT_UNWIND_TARGET *", tlabel,
        "_outer_target = wasm_rt_get_unwind_target();", Newline());
  Write("WASM_RT_UNWIND_TARGET ", tlabel, "_unwind_target;", Newline());
  Write("if (!wasm_rt_try(", tlabel, "_unwind_target)) ");
  Write(OpenBrace()); /* beginning of try block */
  DropTypes(tryexpr.block.decl.GetNumParams());
  const size_t mark = MarkTypeStack();
  PushLabel(LabelType::Try, tryexpr.block.label, tryexpr.block.decl.sig);
  PushTypes(tryexpr.block.decl.sig.param_types);
  Write("wasm_rt_set_unwind_target(&", tlabel, "_unwind_target);", Newline());
  PushTryCatch(tlabel);
  Write(tryexpr.block.exprs);
  ResetTypeStack(mark);
  Write("wasm_rt_set_unwind_target(", tlabel, "_outer_target);", Newline());
  Write(CloseBrace());          /* end of try block */
  Write(" else ", OpenBrace()); /* beginning of catch blocks or delegate */
  assert(label_stack_.back().name == tryexpr.block.label);
  assert(label_stack_.back().label_type == LabelType::Try);
  label_stack_.back().label_type = LabelType::Catch;
  if (try_catch_stack_.back().used) {
    Write(tlabel, "_catch:;", Newline());
  }

  return mark;
}

void CWriter::WriteTryCatch(const TryExpr& tryexpr) {
  const size_t mark = BeginTry(tryexpr);

  /* exception has been thrown -- do we catch it? */

  const LabelName tlabel = LabelName(tryexpr.block.label);

  Write("wasm_rt_set_unwind_target(", tlabel, "_outer_target);", Newline());
  PopTryCatch();

  /* save the thrown exception to the stack if it might be rethrown later */
  PushFuncSection(tryexpr.block.label);
  Write("/* save exception ", tlabel, " for rethrow */", Newline());
  Write("const wasm_rt_tag_t ", tlabel, "_tag = wasm_rt_exception_tag();",
        Newline());
  Write("uint32_t ", tlabel, "_size = wasm_rt_exception_size();", Newline());
  Write("void *", tlabel, " = alloca(", tlabel, "_size);", Newline());
  Write("wasm_rt_memcpy(", tlabel, ", wasm_rt_exception(), ", tlabel, "_size);",
        Newline());
  PushFuncSection();

  assert(!tryexpr.catches.empty());
  bool has_catch_all{};
  for (auto it = tryexpr.catches.cbegin(); it != tryexpr.catches.cend(); ++it) {
    if (it == tryexpr.catches.cbegin()) {
      Write(Newline());
    } else {
      Write(" else ");
    }
    ResetTypeStack(mark);
    Write(*it);
    if (it->IsCatchAll()) {
      has_catch_all = true;
      break;
    }
  }
  if (!has_catch_all) {
    /* if not caught, rethrow */
    Write(" else ", OpenBrace());
    WriteThrow();
    Write(CloseBrace(), Newline());
  }
  Write(CloseBrace(), Newline()); /* end of catch blocks */
  Write(CloseBrace(), Newline()); /* end of try-catch */

  ResetTypeStack(mark);
  assert(!label_stack_.empty());
  assert(label_stack_.back().name == tryexpr.block.label);
  Write(LabelDecl(GetLocalName(tryexpr.block.label, true)));
  PopLabel();
  PushTypes(tryexpr.block.decl.sig.result_types);
}

void CWriter::Write(const Catch& c) {
  if (c.IsCatchAll()) {
    Write(c.exprs);
    return;
  }

  Write("if (wasm_rt_exception_tag() == ",
        TagSymbol(module_->GetTag(c.var)->name), ") ", OpenBrace());

  const Tag* tag = module_->GetTag(c.var);
  const FuncDeclaration& tag_type = tag->decl;
  const Index num_params = tag_type.GetNumParams();
  PushTypes(tag_type.sig.param_types);
  if (num_params == 1) {
    Write("wasm_rt_memcpy(&", StackVar(0), ", wasm_rt_exception(), sizeof(",
          tag_type.GetParamType(0), "));", Newline());
  } else if (num_params > 1) {
    Write(OpenBrace(), tag_type.sig.param_types, " tmp;", Newline());
    Write("wasm_rt_memcpy(&tmp, wasm_rt_exception(), sizeof(tmp));", Newline());
    Unspill(tag_type.sig.param_types);
    Write(CloseBrace(), Newline());
  }

  Write(c.exprs);
  Write(CloseBrace());
}

void CWriter::WriteThrow() {
  if (try_catch_stack_.empty()) {
    Write("wasm_rt_throw();", Newline());
  } else {
    Write("goto ", try_catch_stack_.back().name, "_catch;", Newline());
    try_catch_stack_.back().used = true;
  }
}

void CWriter::PushTryCatch(const std::string& name) {
  try_catch_stack_.emplace_back(name, try_catch_stack_.size());
}

void CWriter::PopTryCatch() {
  assert(!try_catch_stack_.empty());
  try_catch_stack_.pop_back();
}

void CWriter::WriteTryDelegate(const TryExpr& tryexpr) {
  const size_t mark = BeginTry(tryexpr);

  /* exception has been thrown -- where do we delegate it? */

  if (tryexpr.delegate_target.is_index()) {
    /* must be the implicit function label */
    assert(!try_catch_stack_.empty());
    const std::string& unwind_name = try_catch_stack_.at(0).name;
    Write("wasm_rt_set_unwind_target(", unwind_name, "_outer_target);",
          Newline());

    Write("wasm_rt_throw();", Newline());
  } else {
    const Label* label = FindLabel(tryexpr.delegate_target, false);

    assert(try_catch_stack_.size() >= label->try_catch_stack_size);

    if (label->label_type == LabelType::Try) {
      Write("goto ", LabelName(label->name), "_catch;", Newline());
      try_catch_stack_.at(label->try_catch_stack_size).used = true;
    } else if (label->try_catch_stack_size == 0) {
      assert(!try_catch_stack_.empty());
      const std::string& unwind_name = try_catch_stack_.at(0).name;
      Write("wasm_rt_set_unwind_target(", unwind_name, "_outer_target);",
            Newline());

      Write("wasm_rt_throw();", Newline());
    } else {
      const std::string label_target =
          try_catch_stack_.at(label->try_catch_stack_size - 1).name + "_catch";
      Write("goto ", label_target, ";", Newline());
      try_catch_stack_.at(label->try_catch_stack_size - 1).used = true;
    }
  }

  Write(CloseBrace(), Newline());
  Write(CloseBrace(), Newline());

  PopTryCatch();
  ResetTypeStack(mark);
  assert(!label_stack_.empty());
  assert(label_stack_.back().name == tryexpr.block.label);
  Write(LabelDecl(GetLocalName(tryexpr.block.label, true)));
  PopLabel();
  PushTypes(tryexpr.block.decl.sig.result_types);
}

void CWriter::Write(const ExprList& exprs) {
  for (const Expr& expr : exprs) {
    switch (expr.type()) {
      case ExprType::Binary:
        Write(*cast<BinaryExpr>(&expr));
        break;

      case ExprType::Block:
        Write(cast<BlockExpr>(&expr)->block);
        break;

      case ExprType::Br:
        Write(GotoLabel(cast<BrExpr>(&expr)->var), Newline());
        // Stop processing this ExprList, since the following are unreachable.
        return;

      case ExprType::BrIf:
        Write("if (", StackVar(0), ") {");
        DropTypes(1);
        Write(GotoLabel(cast<BrIfExpr>(&expr)->var), "}", Newline());
        break;

      case ExprType::BrTable: {
        const auto* bt_expr = cast<BrTableExpr>(&expr);
        Write("switch (", StackVar(0), ") ", OpenBrace());
        DropTypes(1);
        Index i = 0;
        for (const Var& var : bt_expr->targets) {
          Write("case ", i++, ": ", GotoLabel(var), Newline());
        }
        Write("default: ");
        Write(GotoLabel(bt_expr->default_target), Newline(), CloseBrace(),
              Newline());
        // Stop processing this ExprList, since the following are unreachable.
        return;
      }

      case ExprType::Call: {
        const Var& var = cast<CallExpr>(&expr)->var;
        const Func& func = *module_->GetFunc(var);
        Index num_params = func.GetNumParams();
        Index num_results = func.GetNumResults();
        assert(type_stack_.size() >= num_params);
        if (num_results > 1) {
          Write(OpenBrace(), func.decl.sig.result_types, " tmp = ");
        } else if (num_results == 1) {
          Write(StackVar(num_params - 1, func.GetResultType(0)), " = ");
        }

        assert(var.is_name());
        Write(ExternalRef(ModuleFieldType::Func, var.name()), "(");
        if (IsImport(func.name)) {
          Write("instance->", GlobalName(ModuleFieldType::Import,
                                         import_module_sym_map_[func.name]));
        } else {
          Write("instance");
        }
        for (Index i = 0; i < num_params; ++i) {
          Write(", ");
          Write(StackVar(num_params - i - 1));
        }
        Write(");", Newline());
        DropTypes(num_params);
        PushTypes(func.decl.sig.result_types);
        if (num_results > 1) {
          Unspill(func.decl.sig.result_types);
          Write(CloseBrace(), Newline());
        }
        break;
      }

      case ExprType::CallIndirect: {
        const FuncDeclaration& decl = cast<CallIndirectExpr>(&expr)->decl;
        Index num_params = decl.GetNumParams();
        Index num_results = decl.GetNumResults();
        assert(type_stack_.size() > num_params);
        if (num_results > 1) {
          Write(OpenBrace(), decl.sig.result_types, " tmp = ");
        } else if (num_results == 1) {
          Write(StackVar(num_params, decl.GetResultType(0)), " = ");
        }

        const Table* table =
            module_->GetTable(cast<CallIndirectExpr>(&expr)->table);

        assert(decl.has_func_type);
        const FuncType* func_type = module_->GetFuncType(decl.type_var);

        Write("CALL_INDIRECT(",
              ExternalInstanceRef(ModuleFieldType::Table, table->name), ", ");
        WriteCallIndirectFuncDeclaration(decl, "(*)");
        Write(", ", FuncTypeExpr(func_type), ", ", StackVar(0));
        Write(", ", ExternalInstanceRef(ModuleFieldType::Table, table->name),
              ".data[", StackVar(0), "].module_instance");
        for (Index i = 0; i < num_params; ++i) {
          Write(", ", StackVar(num_params - i));
        }
        Write(");", Newline());
        DropTypes(num_params + 1);
        PushTypes(decl.sig.result_types);
        if (num_results > 1) {
          Unspill(decl.sig.result_types);
          Write(CloseBrace(), Newline());
        }
        break;
      }

      case ExprType::CodeMetadata:
        break;

      case ExprType::Compare:
        Write(*cast<CompareExpr>(&expr));
        break;

      case ExprType::Const: {
        const Const& const_ = cast<ConstExpr>(&expr)->const_;
        PushType(const_.type());
        Write(StackVar(0), " = ", const_, ";", Newline());
        break;
      }

      case ExprType::Convert:
        Write(*cast<ConvertExpr>(&expr));
        break;

      case ExprType::Drop:
        DropTypes(1);
        break;

      case ExprType::GlobalGet: {
        const Var& var = cast<GlobalGetExpr>(&expr)->var;
        PushType(module_->GetGlobal(var)->type);
        Write(StackVar(0), " = ", GlobalInstanceVar(var), ";", Newline());
        break;
      }

      case ExprType::GlobalSet: {
        const Var& var = cast<GlobalSetExpr>(&expr)->var;
        Write(GlobalInstanceVar(var), " = ", StackVar(0), ";", Newline());
        DropTypes(1);
        break;
      }

      case ExprType::If: {
        const IfExpr& if_ = *cast<IfExpr>(&expr);
        Write("if (", StackVar(0), ") ", OpenBrace());
        DropTypes(1);
        std::string label = DefineLabelName(if_.true_.label);
        DropTypes(if_.true_.decl.GetNumParams());
        size_t mark = MarkTypeStack();
        PushLabel(LabelType::If, if_.true_.label, if_.true_.decl.sig);
        PushTypes(if_.true_.decl.sig.param_types);
        Write(if_.true_.exprs, CloseBrace());
        if (!if_.false_.empty()) {
          ResetTypeStack(mark);
          PushTypes(if_.true_.decl.sig.param_types);
          Write(" else ", OpenBrace(), if_.false_, CloseBrace());
        }
        ResetTypeStack(mark);
        Write(Newline(), LabelDecl(label));
        PopLabel();
        PushTypes(if_.true_.decl.sig.result_types);
        break;
      }

      case ExprType::Load:
        Write(*cast<LoadExpr>(&expr));
        break;

      case ExprType::LocalGet: {
        const Var& var = cast<LocalGetExpr>(&expr)->var;
        PushType(func_->GetLocalType(var));
        Write(StackVar(0), " = ", var, ";", Newline());
        break;
      }

      case ExprType::LocalSet: {
        const Var& var = cast<LocalSetExpr>(&expr)->var;
        Write(var, " = ", StackVar(0), ";", Newline());
        DropTypes(1);
        break;
      }

      case ExprType::LocalTee: {
        const Var& var = cast<LocalTeeExpr>(&expr)->var;
        Write(var, " = ", StackVar(0), ";", Newline());
        break;
      }

      case ExprType::Loop: {
        const Block& block = cast<LoopExpr>(&expr)->block;
        if (!block.exprs.empty()) {
          Write(DefineLabelName(block.label), ": ");
          Indent();
          DropTypes(block.decl.GetNumParams());
          size_t mark = MarkTypeStack();
          PushLabel(LabelType::Loop, block.label, block.decl.sig);
          PushTypes(block.decl.sig.param_types);
          Write(Newline(), block.exprs);
          ResetTypeStack(mark);
          PopLabel();
          PushTypes(block.decl.sig.result_types);
          Dedent();
        }
        break;
      }

      case ExprType::MemoryFill: {
        const auto inst = cast<MemoryFillExpr>(&expr);
        Memory* memory =
            module_->memories[module_->GetMemoryIndex(inst->memidx)];
        Write("memory_fill(",
              ExternalInstancePtr(ModuleFieldType::Memory, memory->name), ", ",
              StackVar(2), ", ", StackVar(1), ", ", StackVar(0), ");",
              Newline());
        DropTypes(3);
      } break;

      case ExprType::MemoryCopy: {
        const auto inst = cast<MemoryCopyExpr>(&expr);
        Memory* dest_memory =
            module_->memories[module_->GetMemoryIndex(inst->destmemidx)];
        const Memory* src_memory = module_->GetMemory(inst->srcmemidx);
        Write("memory_copy(",
              ExternalInstancePtr(ModuleFieldType::Memory, dest_memory->name),
              ", ",
              ExternalInstancePtr(ModuleFieldType::Memory, src_memory->name),
              ", ", StackVar(2), ", ", StackVar(1), ", ", StackVar(0), ");",
              Newline());
        DropTypes(3);
      } break;

      case ExprType::MemoryInit: {
        const auto inst = cast<MemoryInitExpr>(&expr);
        Memory* dest_memory =
            module_->memories[module_->GetMemoryIndex(inst->memidx)];
        const DataSegment* src_data = module_->GetDataSegment(inst->var);
        Write("memory_init(",
              ExternalInstancePtr(ModuleFieldType::Memory, dest_memory->name),
              ", ");
        if (src_data->data.empty()) {
          Write("NULL, 0");
        } else {
          Write("data_segment_data_",
                GlobalName(ModuleFieldType::DataSegment, src_data->name), ", ");
          if (is_droppable(src_data)) {
            Write("(", "instance->data_segment_dropped_",
                  GlobalName(ModuleFieldType::DataSegment, src_data->name),
                  " ? 0 : ", src_data->data.size(), ")");
          } else {
            Write("0");
          }
        }

        Write(", ", StackVar(2), ", ", StackVar(1), ", ", StackVar(0), ");",
              Newline());
        DropTypes(3);
      } break;

      case ExprType::TableInit: {
        const auto inst = cast<TableInitExpr>(&expr);
        Table* dest_table =
            module_->tables[module_->GetTableIndex(inst->table_index)];
        const ElemSegment* src_segment =
            module_->GetElemSegment(inst->segment_index);

        WriteElemTableInit(false, src_segment, dest_table);
        DropTypes(3);
      } break;

      case ExprType::DataDrop: {
        const auto inst = cast<DataDropExpr>(&expr);
        const DataSegment* data = module_->GetDataSegment(inst->var);
        if (is_droppable(data)) {
          Write("instance->data_segment_dropped_",
                GlobalName(ModuleFieldType::DataSegment, data->name),
                " = true;", Newline());
        }
      } break;

      case ExprType::ElemDrop: {
        const auto inst = cast<ElemDropExpr>(&expr);
        const ElemSegment* seg = module_->GetElemSegment(inst->var);
        if (is_droppable(seg)) {
          Write("instance->elem_segment_dropped_",
                GlobalName(ModuleFieldType::ElemSegment, seg->name), " = true;",
                Newline());
        }
      } break;

      case ExprType::TableCopy: {
        const auto inst = cast<TableCopyExpr>(&expr);
        Table* dest_table =
            module_->tables[module_->GetTableIndex(inst->dst_table)];
        const Table* src_table = module_->GetTable(inst->src_table);
        if (dest_table->elem_type != src_table->elem_type) {
          WABT_UNREACHABLE;
        }

        Write(
            GetReferenceTypeName(dest_table->elem_type), "_table_copy(",
            ExternalInstancePtr(ModuleFieldType::Table, dest_table->name), ", ",
            ExternalInstancePtr(ModuleFieldType::Table, src_table->name), ", ",
            StackVar(2), ", ", StackVar(1), ", ", StackVar(0), ");", Newline());
        DropTypes(3);
      } break;

      case ExprType::TableGet: {
        const Table* table = module_->GetTable(cast<TableGetExpr>(&expr)->var);
        Write(StackVar(0, table->elem_type), " = ",
              GetReferenceTypeName(table->elem_type), "_table_get(",
              ExternalInstancePtr(ModuleFieldType::Table, table->name), ", ",
              StackVar(0), ");", Newline());
        DropTypes(1);
        PushType(table->elem_type);
      } break;

      case ExprType::TableSet: {
        const Table* table = module_->GetTable(cast<TableSetExpr>(&expr)->var);
        Write(GetReferenceTypeName(table->elem_type), "_table_set(",
              ExternalInstancePtr(ModuleFieldType::Table, table->name), ", ",
              StackVar(1), ", ", StackVar(0), ");", Newline());
        DropTypes(2);
      } break;

      case ExprType::TableGrow: {
        const Table* table = module_->GetTable(cast<TableGrowExpr>(&expr)->var);
        Write(StackVar(1, Type::I32), " = wasm_rt_grow_",
              GetReferenceTypeName(table->elem_type), "_table(",
              ExternalInstancePtr(ModuleFieldType::Table, table->name), ", ",
              StackVar(0), ", ", StackVar(1), ");", Newline());
        DropTypes(2);
        PushType(Type::I32);
      } break;

      case ExprType::TableSize: {
        const Table* table = module_->GetTable(cast<TableSizeExpr>(&expr)->var);

        PushType(Type::I32);
        Write(StackVar(0), " = ",
              ExternalInstanceRef(ModuleFieldType::Table, table->name),
              ".size;", Newline());
      } break;

      case ExprType::TableFill: {
        const Table* table = module_->GetTable(cast<TableFillExpr>(&expr)->var);
        Write(GetReferenceTypeName(table->elem_type), "_table_fill(",
              ExternalInstancePtr(ModuleFieldType::Table, table->name), ", ",
              StackVar(2), ", ", StackVar(1), ", ", StackVar(0), ");",
              Newline());
        DropTypes(3);
      } break;

      case ExprType::RefFunc: {
        const Func* func = module_->GetFunc(cast<RefFuncExpr>(&expr)->var);
        PushType(Type::FuncRef);
        const FuncDeclaration& decl = func->decl;

        assert(decl.has_func_type);
        const FuncType* func_type = module_->GetFuncType(decl.type_var);

        Write(StackVar(0), " = (wasm_rt_funcref_t){", FuncTypeExpr(func_type),
              ", (wasm_rt_function_ptr_t)",
              ExternalRef(ModuleFieldType::Func, func->name), ", {");
        if (options_.features.tail_call_enabled() &&
            (IsImport(func->name) || func->features_used.tailcall)) {
          Write(TailCallRef(func->name));
        } else {
          Write("NULL");
        }
        Write("}, ");

        if (IsImport(func->name)) {
          Write("instance->", GlobalName(ModuleFieldType::Import,
                                         import_module_sym_map_[func->name]));
        } else {
          Write("instance");
        }

        Write("};", Newline());
      } break;

      case ExprType::RefNull:
        PushType(cast<RefNullExpr>(&expr)->type);
        Write(StackVar(0), " = ",
              GetReferenceNullValue(cast<RefNullExpr>(&expr)->type), ";",
              Newline());
        break;

      case ExprType::RefIsNull:
        switch (StackType(0)) {
          case Type::FuncRef:
            Write(StackVar(0, Type::I32), " = (", StackVar(0), ".func == NULL",
                  ");", Newline());
            break;
          case Type::ExternRef:
            Write(StackVar(0, Type::I32), " = (", StackVar(0),
                  " == ", GetReferenceNullValue(Type::ExternRef), ");",
                  Newline());
            break;
          default:
            WABT_UNREACHABLE;
        }

        DropTypes(1);
        PushType(Type::I32);
        break;

      case ExprType::MemoryGrow: {
        Memory* memory = module_->memories[module_->GetMemoryIndex(
            cast<MemoryGrowExpr>(&expr)->memidx)];

        Write(StackVar(0), " = wasm_rt_grow_memory(",
              ExternalInstancePtr(ModuleFieldType::Memory, memory->name), ", ",
              StackVar(0), ");", Newline());
        break;
      }

      case ExprType::MemorySize: {
        Memory* memory = module_->memories[module_->GetMemoryIndex(
            cast<MemorySizeExpr>(&expr)->memidx)];

        PushType(memory->page_limits.IndexType());
        Write(StackVar(0), " = ",
              ExternalInstanceRef(ModuleFieldType::Memory, memory->name),
              ".pages;", Newline());
        break;
      }

      case ExprType::Nop:
        break;

      case ExprType::Return:
        // Goto the function label instead; this way we can do shared function
        // cleanup code in one place.
        Write(GotoLabel(Var(label_stack_.size() - 1, {})), Newline());
        // Stop processing this ExprList, since the following are unreachable.
        return;

      case ExprType::Select: {
        Type type = StackType(1);
        Write(StackVar(2), " = ", StackVar(0), " ? ", StackVar(2), " : ",
              StackVar(1), ";", Newline());
        DropTypes(3);
        PushType(type);
        break;
      }

      case ExprType::Store:
        Write(*cast<StoreExpr>(&expr));
        break;

      case ExprType::Unary:
        Write(*cast<UnaryExpr>(&expr));
        break;

      case ExprType::Ternary:
        Write(*cast<TernaryExpr>(&expr));
        break;

      case ExprType::SimdLaneOp: {
        Write(*cast<SimdLaneOpExpr>(&expr));
        break;
      }

      case ExprType::SimdLoadLane: {
        Write(*cast<SimdLoadLaneExpr>(&expr));
        break;
      }

      case ExprType::SimdStoreLane: {
        Write(*cast<SimdStoreLaneExpr>(&expr));
        break;
      }

      case ExprType::SimdShuffleOp: {
        Write(*cast<SimdShuffleOpExpr>(&expr));
        break;
      }

      case ExprType::LoadSplat:
        Write(*cast<LoadSplatExpr>(&expr));
        break;

      case ExprType::LoadZero:
        Write(*cast<LoadZeroExpr>(&expr));
        break;

      case ExprType::Unreachable:
        Write("UNREACHABLE;", Newline());
        return;

      case ExprType::Throw: {
        const Var& var = cast<ThrowExpr>(&expr)->var;
        const Tag* tag = module_->GetTag(var);

        Index num_params = tag->decl.GetNumParams();
        if (num_params == 0) {
          Write("wasm_rt_load_exception(", TagSymbol(tag->name), ", 0, NULL);",
                Newline());
        } else if (num_params == 1) {
          Write("wasm_rt_load_exception(", TagSymbol(tag->name), ", sizeof(",
                tag->decl.GetParamType(0), "), &", StackVar(0), ");",
                Newline());
        } else {
          Write(OpenBrace(), tag->decl.sig.param_types, " tmp;", Newline());
          Spill(tag->decl.sig.param_types);
          Write("wasm_rt_load_exception(", TagSymbol(tag->name),
                ", sizeof(tmp), &tmp);", Newline(), CloseBrace(), Newline());
        }

        WriteThrow();
      } break;

      case ExprType::Rethrow: {
        const RethrowExpr* rethrow = cast<RethrowExpr>(&expr);
        assert(rethrow->var.is_name());
        const LabelName ex{rethrow->var.name()};
        func_includes_.insert(rethrow->var.name());
        Write("wasm_rt_load_exception(", ex, "_tag, ", ex, "_size, ", ex, ");",
              Newline());
        WriteThrow();
      } break;

      case ExprType::Try: {
        const TryExpr& tryexpr = *cast<TryExpr>(&expr);
        switch (tryexpr.kind) {
          case TryKind::Plain:
            Write(tryexpr.block);
            break;
          case TryKind::Catch:
            WriteTryCatch(tryexpr);
            break;
          case TryKind::Delegate:
            WriteTryDelegate(tryexpr);
            break;
        }
      } break;

      case ExprType::AtomicLoad: {
        Write(*cast<AtomicLoadExpr>(&expr));
        break;
      }

      case ExprType::AtomicStore: {
        Write(*cast<AtomicStoreExpr>(&expr));
        break;
      }

      case ExprType::AtomicRmw: {
        Write(*cast<AtomicRmwExpr>(&expr));
        break;
      }

      case ExprType::AtomicRmwCmpxchg: {
        Write(*cast<AtomicRmwCmpxchgExpr>(&expr));
        break;
      }

      case ExprType::AtomicFence: {
        Write("atomic_fence();", Newline());
        break;
      }

      case ExprType::ReturnCall: {
        const auto inst = cast<ReturnCallExpr>(&expr);
        const Func& func = *module_->GetFunc(inst->var);

        const FuncDeclaration& decl = func.decl;
        assert(decl.sig.result_types == func_->decl.sig.result_types);
        WriteUnwindTryCatchStack(FindLabel(Var(label_stack_.size() - 1, {})));

        if (!IsImport(func.name) && !func.features_used.tailcall) {
          // make normal call, then return
          Write(ExprList{std::make_unique<CallExpr>(inst->var, inst->loc)});
          Write("goto ", LabelName(kImplicitFuncLabel), ";", Newline());
          return;
        }

        WriteTailCallAsserts(decl.sig);
        Write(OpenBrace());
        if (!in_tail_callee_) {
          WriteTailCallStack();
        }

        const Index num_params = decl.GetNumParams();
        if (decl.GetNumParams()) {
          Write(OpenBrace(), decl.sig.param_types, " tmp;", Newline());
          Spill(decl.sig.param_types);
          Write("wasm_rt_memcpy(tail_call_stack, &tmp, sizeof(tmp));",
                Newline());
          Write(CloseBrace(), Newline());
        }

        Write("next->fn = ", TailCallRef(func.name), ";", Newline());
        if (IsImport(func.name)) {
          Write("*instance_ptr = ",
                GlobalName(ModuleFieldType::Import,
                           import_module_sym_map_.at(func.name)),
                ";", Newline());
        }
        DropTypes(num_params);
        FinishReturnCall();
        return;
      }

      case ExprType::ReturnCallIndirect: {
        const auto inst = cast<ReturnCallIndirectExpr>(&expr);
        const FuncDeclaration& decl = inst->decl;
        assert(decl.sig.result_types == func_->decl.sig.result_types);
        assert(decl.has_func_type);
        const Index num_params = decl.GetNumParams();
        WriteTailCallAsserts(decl.sig);
        WriteUnwindTryCatchStack(FindLabel(Var(label_stack_.size() - 1, {})));
        const Table* table = module_->GetTable(inst->table);
        Write("CHECK_CALL_INDIRECT(",
              ExternalInstanceRef(ModuleFieldType::Table, table->name), ", ",
              FuncTypeExpr(module_->GetFuncType(decl.type_var)), ", ",
              StackVar(0), ");", Newline());

        Write("if (!", ExternalInstanceRef(ModuleFieldType::Table, table->name),
              ".data[", StackVar(0), "].func_tailcallee.fn) ", OpenBrace());
        auto ci = std::make_unique<CallIndirectExpr>(inst->loc);
        std::tie(ci->decl, ci->table) = std::make_pair(inst->decl, inst->table);
        Write(ExprList{std::move(ci)});
        if (in_tail_callee_) {
          Write("next->fn = NULL;", Newline());
        }
        Write(CloseBrace(), " else ", OpenBrace());

        DropTypes(decl.GetNumResults());
        PushTypes(decl.sig.param_types);
        PushType(Type::I32);

        if (!in_tail_callee_) {
          WriteTailCallStack();
        }

        if (num_params) {
          Write(OpenBrace(), decl.sig.param_types, " tmp;", Newline());
          Spill(decl.sig.param_types,
                [&](auto i) { return StackVar(num_params - i); });
          Write("wasm_rt_memcpy(tail_call_stack, &tmp, sizeof(tmp));",
                Newline());
          Write(CloseBrace(), Newline());
        }

        assert(decl.has_func_type);
        Write("next->fn = ",
              ExternalInstanceRef(ModuleFieldType::Table, table->name),
              ".data[", StackVar(0), "].func_tailcallee.fn;", Newline());
        Write("*instance_ptr = ",
              ExternalInstanceRef(ModuleFieldType::Table, table->name),
              ".data[", StackVar(0), "].module_instance;", Newline());

        DropTypes(num_params + 1);
        FinishReturnCall();
        return;
      }

      case ExprType::AtomicWait:
      case ExprType::AtomicNotify:
      case ExprType::CallRef:
        UNIMPLEMENTED("...");
        break;
    }
  }
}

void CWriter::WriteSimpleUnaryExpr(Opcode opcode, const char* op) {
  Type result_type = opcode.GetResultType();
  Write(StackVar(0, result_type), " = ", op, "(", StackVar(0), ");", Newline());
  DropTypes(1);
  PushType(opcode.GetResultType());
}

void CWriter::WriteInfixBinaryExpr(Opcode opcode,
                                   const char* op,
                                   AssignOp assign_op) {
  Type result_type = opcode.GetResultType();
  Write(StackVar(1, result_type));
  if (assign_op == AssignOp::Allowed) {
    Write(" ", op, "= ", StackVar(0));
  } else {
    Write(" = ", StackVar(1), " ", op, " ", StackVar(0));
  }
  Write(";", Newline());
  DropTypes(2);
  PushType(result_type);
}

void CWriter::WritePrefixBinaryExpr(Opcode opcode, const char* op) {
  Type result_type = opcode.GetResultType();
  Write(StackVar(1, result_type), " = ", op, "(", StackVar(1), ", ",
        StackVar(0), ");", Newline());
  DropTypes(2);
  PushType(result_type);
}

void CWriter::WriteSignedBinaryExpr(Opcode opcode, const char* op) {
  Type result_type = opcode.GetResultType();
  Type type = opcode.GetParamType1();
  assert(opcode.GetParamType2() == type);
  Write(StackVar(1, result_type), " = (", type, ")((", SignedType(type), ")",
        StackVar(1), " ", op, " (", SignedType(type), ")", StackVar(0), ");",
        Newline());
  DropTypes(2);
  PushType(result_type);
}

void CWriter::Write(const BinaryExpr& expr) {
  switch (expr.opcode) {
    case Opcode::I32Add:
    case Opcode::I64Add:
    case Opcode::F32Add:
    case Opcode::F64Add:
      WriteInfixBinaryExpr(expr.opcode, "+");
      break;

    case Opcode::I32Sub:
    case Opcode::I64Sub:
    case Opcode::F32Sub:
    case Opcode::F64Sub:
      WriteInfixBinaryExpr(expr.opcode, "-");
      break;

    case Opcode::I32Mul:
    case Opcode::I64Mul:
    case Opcode::F32Mul:
    case Opcode::F64Mul:
      WriteInfixBinaryExpr(expr.opcode, "*");
      break;

    case Opcode::I32DivS:
      WritePrefixBinaryExpr(expr.opcode, "I32_DIV_S");
      break;

    case Opcode::I64DivS:
      WritePrefixBinaryExpr(expr.opcode, "I64_DIV_S");
      break;

    case Opcode::I32DivU:
    case Opcode::I64DivU:
      WritePrefixBinaryExpr(expr.opcode, "DIV_U");
      break;

    case Opcode::F32Div:
    case Opcode::F64Div:
      WriteInfixBinaryExpr(expr.opcode, "/");
      break;

    case Opcode::I32RemS:
      WritePrefixBinaryExpr(expr.opcode, "I32_REM_S");
      break;

    case Opcode::I64RemS:
      WritePrefixBinaryExpr(expr.opcode, "I64_REM_S");
      break;

    case Opcode::I32RemU:
    case Opcode::I64RemU:
      WritePrefixBinaryExpr(expr.opcode, "REM_U");
      break;

    case Opcode::I32And:
    case Opcode::I64And:
      WriteInfixBinaryExpr(expr.opcode, "&");
      break;

    case Opcode::I32Or:
    case Opcode::I64Or:
      WriteInfixBinaryExpr(expr.opcode, "|");
      break;

    case Opcode::I32Xor:
    case Opcode::I64Xor:
      WriteInfixBinaryExpr(expr.opcode, "^");
      break;

    case Opcode::I32Shl:
    case Opcode::I64Shl:
      Write(StackVar(1), " <<= (", StackVar(0), " & ",
            GetShiftMask(expr.opcode.GetResultType()), ");", Newline());
      DropTypes(1);
      break;

    case Opcode::I32ShrS:
    case Opcode::I64ShrS: {
      Type type = expr.opcode.GetResultType();
      Write(StackVar(1), " = (", type, ")((", SignedType(type), ")",
            StackVar(1), " >> (", StackVar(0), " & ", GetShiftMask(type), "));",
            Newline());
      DropTypes(1);
      break;
    }

    case Opcode::I32ShrU:
    case Opcode::I64ShrU:
      Write(StackVar(1), " >>= (", StackVar(0), " & ",
            GetShiftMask(expr.opcode.GetResultType()), ");", Newline());
      DropTypes(1);
      break;

    case Opcode::I32Rotl:
      WritePrefixBinaryExpr(expr.opcode, "I32_ROTL");
      break;

    case Opcode::I64Rotl:
      WritePrefixBinaryExpr(expr.opcode, "I64_ROTL");
      break;

    case Opcode::I32Rotr:
      WritePrefixBinaryExpr(expr.opcode, "I32_ROTR");
      break;

    case Opcode::I64Rotr:
      WritePrefixBinaryExpr(expr.opcode, "I64_ROTR");
      break;

    case Opcode::F32Min:
    case Opcode::F64Min:
      WritePrefixBinaryExpr(expr.opcode, "FMIN");
      break;

    case Opcode::F32Max:
    case Opcode::F64Max:
      WritePrefixBinaryExpr(expr.opcode, "FMAX");
      break;

    case Opcode::F32Copysign:
      WritePrefixBinaryExpr(expr.opcode, "copysignf");
      break;

    case Opcode::F64Copysign:
      WritePrefixBinaryExpr(expr.opcode, "copysign");
      break;

    case Opcode::I8X16Add:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_i8x16_add");
      break;

    case Opcode::I8X16AddSatS:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_i8x16_add_sat");
      break;

    case Opcode::I8X16AddSatU:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_u8x16_add_sat");
      break;

    case Opcode::I8X16AvgrU:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_u8x16_avgr");
      break;

    case Opcode::I8X16MaxS:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_i8x16_max");
      break;

    case Opcode::I8X16MaxU:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_u8x16_max");
      break;

    case Opcode::I8X16MinS:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_i8x16_min");
      break;

    case Opcode::I8X16MinU:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_u8x16_min");
      break;

    case Opcode::I8X16NarrowI16X8S:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_i8x16_narrow_i16x8");
      break;

    case Opcode::I8X16NarrowI16X8U:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_u8x16_narrow_i16x8");
      break;

    case Opcode::I8X16Shl:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_i8x16_shl");
      break;

    case Opcode::I8X16ShrS:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_i8x16_shr");
      break;

    case Opcode::I8X16ShrU:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_u8x16_shr");
      break;

    case Opcode::I8X16Sub:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_i8x16_sub");
      break;

    case Opcode::I8X16SubSatS:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_i8x16_sub_sat");
      break;

    case Opcode::I8X16SubSatU:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_u8x16_sub_sat");
      break;

    case Opcode::I8X16Swizzle:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_i8x16_swizzle");
      break;

    case Opcode::I16X8Add:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_i16x8_add");
      break;

    case Opcode::I16X8AvgrU:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_u16x8_avgr");
      break;

    case Opcode::I16X8AddSatS:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_i16x8_add_sat");
      break;

    case Opcode::I16X8AddSatU:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_u16x8_add_sat");
      break;

    case Opcode::I16X8ExtmulHighI8X16S:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_i16x8_extmul_high_i8x16");
      break;

    case Opcode::I16X8ExtmulHighI8X16U:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_u16x8_extmul_high_u8x16");
      break;

    case Opcode::I16X8ExtmulLowI8X16S:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_i16x8_extmul_low_i8x16");
      break;

    case Opcode::I16X8ExtmulLowI8X16U:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_u16x8_extmul_low_u8x16");
      break;

    case Opcode::I16X8MaxS:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_i16x8_max");
      break;

    case Opcode::I16X8MaxU:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_u16x8_max");
      break;

    case Opcode::I16X8MinS:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_i16x8_min");
      break;

    case Opcode::I16X8MinU:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_u16x8_min");
      break;

    case Opcode::I16X8Mul:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_i16x8_mul");
      break;

    case Opcode::I16X8NarrowI32X4S:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_i16x8_narrow_i32x4");
      break;

    case Opcode::I16X8NarrowI32X4U:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_u16x8_narrow_i32x4");
      break;

    case Opcode::I16X8Q15mulrSatS:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_i16x8_q15mulr_sat");
      break;

    case Opcode::I16X8Shl:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_i16x8_shl");
      break;

    case Opcode::I16X8ShrS:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_i16x8_shr");
      break;

    case Opcode::I16X8ShrU:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_u16x8_shr");
      break;

    case Opcode::I16X8Sub:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_i16x8_sub");
      break;

    case Opcode::I16X8SubSatS:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_i16x8_sub_sat");
      break;

    case Opcode::I16X8SubSatU:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_u16x8_sub_sat");
      break;

    case Opcode::I32X4Add:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_i32x4_add");
      break;

    case Opcode::I32X4DotI16X8S:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_i32x4_dot_i16x8");
      break;

    case Opcode::I32X4ExtmulHighI16X8S:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_i32x4_extmul_high_i16x8");
      break;

    case Opcode::I32X4ExtmulHighI16X8U:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_u32x4_extmul_high_u16x8");
      break;

    case Opcode::I32X4ExtmulLowI16X8S:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_i32x4_extmul_low_i16x8");
      break;

    case Opcode::I32X4ExtmulLowI16X8U:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_u32x4_extmul_low_u16x8");
      break;

    case Opcode::I32X4MaxS:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_i32x4_max");
      break;

    case Opcode::I32X4MaxU:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_u32x4_max");
      break;

    case Opcode::I32X4MinS:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_i32x4_min");
      break;

    case Opcode::I32X4MinU:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_u32x4_min");
      break;

    case Opcode::I32X4Mul:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_i32x4_mul");
      break;

    case Opcode::I32X4Shl:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_i32x4_shl");
      break;

    case Opcode::I32X4ShrS:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_i32x4_shr");
      break;

    case Opcode::I32X4ShrU:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_u32x4_shr");
      break;

    case Opcode::I32X4Sub:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_i32x4_sub");
      break;

    case Opcode::I64X2Add:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_i64x2_add");
      break;

    case Opcode::I64X2ExtmulHighI32X4S:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_i64x2_extmul_high_i32x4");
      break;

    case Opcode::I64X2ExtmulHighI32X4U:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_u64x2_extmul_high_u32x4");
      break;

    case Opcode::I64X2ExtmulLowI32X4S:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_i64x2_extmul_low_i32x4");
      break;

    case Opcode::I64X2ExtmulLowI32X4U:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_u64x2_extmul_low_u32x4");
      break;

    case Opcode::I64X2Mul:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_i64x2_mul");
      break;

    case Opcode::I64X2Shl:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_i64x2_shl");
      break;

    case Opcode::I64X2ShrS:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_i64x2_shr");
      break;

    case Opcode::I64X2ShrU:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_u64x2_shr");
      break;

    case Opcode::I64X2Sub:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_i64x2_sub");
      break;

    case Opcode::F32X4Add:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_f32x4_add");
      break;

    case Opcode::F32X4Div:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_f32x4_div");
      break;

    case Opcode::F32X4Max:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_f32x4_max");
      break;

    case Opcode::F32X4Mul:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_f32x4_mul");
      break;

    case Opcode::F32X4Min:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_f32x4_min");
      break;

    case Opcode::F32X4PMax:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_f32x4_pmax");
      break;

    case Opcode::F32X4PMin:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_f32x4_pmin");
      break;

    case Opcode::F32X4Sub:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_f32x4_sub");
      break;

    case Opcode::F64X2Add:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_f64x2_add");
      break;

    case Opcode::F64X2Div:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_f64x2_div");
      break;

    case Opcode::F64X2Max:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_f64x2_max");
      break;

    case Opcode::F64X2Mul:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_f64x2_mul");
      break;

    case Opcode::F64X2Min:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_f64x2_min");
      break;

    case Opcode::F64X2PMax:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_f64x2_pmax");
      break;

    case Opcode::F64X2PMin:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_f64x2_pmin");
      break;

    case Opcode::F64X2Sub:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_f64x2_sub");
      break;

    case Opcode::V128And:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_v128_and");
      break;

    case Opcode::V128Andnot:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_v128_andnot");
      break;

    case Opcode::V128Or:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_v128_or");
      break;

    case Opcode::V128Xor:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_v128_xor");
      break;

    default:
      WABT_UNREACHABLE;
  }
}

void CWriter::Write(const CompareExpr& expr) {
  switch (expr.opcode) {
    case Opcode::I32Eq:
    case Opcode::I64Eq:
    case Opcode::F32Eq:
    case Opcode::F64Eq:
      WriteInfixBinaryExpr(expr.opcode, "==", AssignOp::Disallowed);
      break;

    case Opcode::I32Ne:
    case Opcode::I64Ne:
    case Opcode::F32Ne:
    case Opcode::F64Ne:
      WriteInfixBinaryExpr(expr.opcode, "!=", AssignOp::Disallowed);
      break;

    case Opcode::I32LtS:
    case Opcode::I64LtS:
      WriteSignedBinaryExpr(expr.opcode, "<");
      break;

    case Opcode::I32LtU:
    case Opcode::I64LtU:
    case Opcode::F32Lt:
    case Opcode::F64Lt:
      WriteInfixBinaryExpr(expr.opcode, "<", AssignOp::Disallowed);
      break;

    case Opcode::I32LeS:
    case Opcode::I64LeS:
      WriteSignedBinaryExpr(expr.opcode, "<=");
      break;

    case Opcode::I32LeU:
    case Opcode::I64LeU:
    case Opcode::F32Le:
    case Opcode::F64Le:
      WriteInfixBinaryExpr(expr.opcode, "<=", AssignOp::Disallowed);
      break;

    case Opcode::I32GtS:
    case Opcode::I64GtS:
      WriteSignedBinaryExpr(expr.opcode, ">");
      break;

    case Opcode::I32GtU:
    case Opcode::I64GtU:
    case Opcode::F32Gt:
    case Opcode::F64Gt:
      WriteInfixBinaryExpr(expr.opcode, ">", AssignOp::Disallowed);
      break;

    case Opcode::I32GeS:
    case Opcode::I64GeS:
      WriteSignedBinaryExpr(expr.opcode, ">=");
      break;

    case Opcode::I32GeU:
    case Opcode::I64GeU:
    case Opcode::F32Ge:
    case Opcode::F64Ge:
      WriteInfixBinaryExpr(expr.opcode, ">=", AssignOp::Disallowed);
      break;

    case Opcode::I8X16Eq:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_i8x16_eq");
      break;

    case Opcode::I8X16GeS:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_i8x16_ge");
      break;

    case Opcode::I8X16GeU:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_u8x16_ge");
      break;

    case Opcode::I8X16GtS:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_i8x16_gt");
      break;

    case Opcode::I8X16GtU:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_u8x16_gt");
      break;

    case Opcode::I8X16LeS:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_i8x16_le");
      break;

    case Opcode::I8X16LeU:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_u8x16_le");
      break;

    case Opcode::I8X16LtS:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_i8x16_lt");
      break;

    case Opcode::I8X16LtU:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_u8x16_lt");
      break;

    case Opcode::I8X16Ne:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_i8x16_ne");
      break;

    case Opcode::I16X8Eq:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_i16x8_eq");
      break;

    case Opcode::I16X8GeS:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_i16x8_ge");
      break;

    case Opcode::I16X8GeU:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_u16x8_ge");
      break;

    case Opcode::I16X8GtS:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_i16x8_gt");
      break;
    case Opcode::I16X8GtU:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_u16x8_gt");
      break;

    case Opcode::I16X8LeS:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_i16x8_le");
      break;

    case Opcode::I16X8LeU:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_u16x8_le");
      break;

    case Opcode::I16X8LtS:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_i16x8_lt");
      break;

    case Opcode::I16X8LtU:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_u16x8_lt");
      break;

    case Opcode::I16X8Ne:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_i16x8_ne");
      break;

    case Opcode::I32X4Eq:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_i32x4_eq");
      break;

    case Opcode::I32X4GeS:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_i32x4_ge");
      break;

    case Opcode::I32X4GeU:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_u32x4_ge");
      break;

    case Opcode::I32X4GtS:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_i32x4_gt");
      break;

    case Opcode::I32X4GtU:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_u32x4_gt");
      break;

    case Opcode::I32X4LeS:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_i32x4_le");
      break;

    case Opcode::I32X4LeU:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_u32x4_le");
      break;

    case Opcode::I32X4LtS:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_i32x4_lt");
      break;

    case Opcode::I32X4LtU:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_u32x4_lt");
      break;

    case Opcode::I32X4Ne:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_i32x4_ne");
      break;

    case Opcode::I64X2Eq:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_i64x2_eq");
      break;

    case Opcode::I64X2GeS:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_i64x2_ge");
      break;

    case Opcode::I64X2GtS:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_i64x2_gt");
      break;

    case Opcode::I64X2LeS:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_i64x2_le");
      break;

    case Opcode::I64X2LtS:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_i64x2_lt");
      break;

    case Opcode::I64X2Ne:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_i64x2_ne");
      break;

    case Opcode::F32X4Eq:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_f32x4_eq");
      break;

    case Opcode::F32X4Ge:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_f32x4_ge");
      break;

    case Opcode::F32X4Gt:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_f32x4_gt");
      break;

    case Opcode::F32X4Le:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_f32x4_le");
      break;

    case Opcode::F32X4Lt:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_f32x4_lt");
      break;

    case Opcode::F32X4Ne:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_f32x4_ne");
      break;

    case Opcode::F64X2Eq:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_f64x2_eq");
      break;

    case Opcode::F64X2Ge:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_f64x2_ge");
      break;

    case Opcode::F64X2Gt:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_f64x2_gt");
      break;

    case Opcode::F64X2Le:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_f64x2_le");
      break;

    case Opcode::F64X2Lt:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_f64x2_lt");
      break;

    case Opcode::F64X2Ne:
      WritePrefixBinaryExpr(expr.opcode, "simde_wasm_f64x2_ne");
      break;

    default:
      WABT_UNREACHABLE;
  }
}

void CWriter::Write(const ConvertExpr& expr) {
  switch (expr.opcode) {
    case Opcode::I32Eqz:
    case Opcode::I64Eqz:
      WriteSimpleUnaryExpr(expr.opcode, "!");
      break;

    case Opcode::I64ExtendI32S:
      WriteSimpleUnaryExpr(expr.opcode, "(u64)(s64)(s32)");
      break;

    case Opcode::I64ExtendI32U:
      WriteSimpleUnaryExpr(expr.opcode, "(u64)");
      break;

    case Opcode::I32WrapI64:
      WriteSimpleUnaryExpr(expr.opcode, "(u32)");
      break;

    case Opcode::I32TruncF32S:
      WriteSimpleUnaryExpr(expr.opcode, "I32_TRUNC_S_F32");
      break;

    case Opcode::I64TruncF32S:
      WriteSimpleUnaryExpr(expr.opcode, "I64_TRUNC_S_F32");
      break;

    case Opcode::I32TruncF64S:
      WriteSimpleUnaryExpr(expr.opcode, "I32_TRUNC_S_F64");
      break;

    case Opcode::I64TruncF64S:
      WriteSimpleUnaryExpr(expr.opcode, "I64_TRUNC_S_F64");
      break;

    case Opcode::I32TruncF32U:
      WriteSimpleUnaryExpr(expr.opcode, "I32_TRUNC_U_F32");
      break;

    case Opcode::I64TruncF32U:
      WriteSimpleUnaryExpr(expr.opcode, "I64_TRUNC_U_F32");
      break;

    case Opcode::I32TruncF64U:
      WriteSimpleUnaryExpr(expr.opcode, "I32_TRUNC_U_F64");
      break;

    case Opcode::I64TruncF64U:
      WriteSimpleUnaryExpr(expr.opcode, "I64_TRUNC_U_F64");
      break;

    case Opcode::I32TruncSatF32S:
      WriteSimpleUnaryExpr(expr.opcode, "I32_TRUNC_SAT_S_F32");
      break;

    case Opcode::I64TruncSatF32S:
      WriteSimpleUnaryExpr(expr.opcode, "I64_TRUNC_SAT_S_F32");
      break;

    case Opcode::I32TruncSatF64S:
      WriteSimpleUnaryExpr(expr.opcode, "I32_TRUNC_SAT_S_F64");
      break;

    case Opcode::I64TruncSatF64S:
      WriteSimpleUnaryExpr(expr.opcode, "I64_TRUNC_SAT_S_F64");
      break;

    case Opcode::I32TruncSatF32U:
      WriteSimpleUnaryExpr(expr.opcode, "I32_TRUNC_SAT_U_F32");
      break;

    case Opcode::I64TruncSatF32U:
      WriteSimpleUnaryExpr(expr.opcode, "I64_TRUNC_SAT_U_F32");
      break;

    case Opcode::I32TruncSatF64U:
      WriteSimpleUnaryExpr(expr.opcode, "I32_TRUNC_SAT_U_F64");
      break;

    case Opcode::I64TruncSatF64U:
      WriteSimpleUnaryExpr(expr.opcode, "I64_TRUNC_SAT_U_F64");
      break;

    case Opcode::F32ConvertI32S:
      WriteSimpleUnaryExpr(expr.opcode, "(f32)(s32)");
      break;

    case Opcode::F32ConvertI64S:
      WriteSimpleUnaryExpr(expr.opcode, "(f32)(s64)");
      break;

    case Opcode::F32ConvertI32U:
      WriteSimpleUnaryExpr(expr.opcode, "(f32)");
      break;

    case Opcode::F32DemoteF64:
      WriteSimpleUnaryExpr(expr.opcode, "(f32)wasm_quiet");
      break;

    case Opcode::F32ConvertI64U:
      // TODO(binji): This needs to be handled specially (see
      // wabt_convert_uint64_to_float).
      WriteSimpleUnaryExpr(expr.opcode, "(f32)");
      break;

    case Opcode::F64ConvertI32S:
      WriteSimpleUnaryExpr(expr.opcode, "(f64)(s32)");
      break;

    case Opcode::F64ConvertI64S:
      WriteSimpleUnaryExpr(expr.opcode, "(f64)(s64)");
      break;

    case Opcode::F64ConvertI32U:
      WriteSimpleUnaryExpr(expr.opcode, "(f64)");
      break;

    case Opcode::F64PromoteF32:
      WriteSimpleUnaryExpr(expr.opcode, "(f64)wasm_quietf");
      break;

    case Opcode::F64ConvertI64U:
      // TODO(binji): This needs to be handled specially (see
      // wabt_convert_uint64_to_double).
      WriteSimpleUnaryExpr(expr.opcode, "(f64)");
      break;

    case Opcode::F32ReinterpretI32:
      WriteSimpleUnaryExpr(expr.opcode, "f32_reinterpret_i32");
      break;

    case Opcode::I32ReinterpretF32:
      WriteSimpleUnaryExpr(expr.opcode, "i32_reinterpret_f32");
      break;

    case Opcode::F64ReinterpretI64:
      WriteSimpleUnaryExpr(expr.opcode, "f64_reinterpret_i64");
      break;

    case Opcode::I64ReinterpretF64:
      WriteSimpleUnaryExpr(expr.opcode, "i64_reinterpret_f64");
      break;

    case Opcode::I32X4TruncSatF32X4S:
      WriteSimpleUnaryExpr(expr.opcode, "simde_wasm_i32x4_trunc_sat_f32x4");
      break;

    case Opcode::I32X4TruncSatF32X4U:
      WriteSimpleUnaryExpr(expr.opcode, "simde_wasm_u32x4_trunc_sat_f32x4");
      break;

    case Opcode::I32X4TruncSatF64X2SZero:
      WriteSimpleUnaryExpr(expr.opcode,
                           "simde_wasm_i32x4_trunc_sat_f64x2_zero");
      break;

    case Opcode::I32X4TruncSatF64X2UZero:
      WriteSimpleUnaryExpr(expr.opcode,
                           "simde_wasm_u32x4_trunc_sat_f64x2_zero");
      break;

    case Opcode::F32X4ConvertI32X4S:
      WriteSimpleUnaryExpr(expr.opcode, "simde_wasm_f32x4_convert_i32x4");
      break;

    case Opcode::F32X4ConvertI32X4U:
      WriteSimpleUnaryExpr(expr.opcode, "simde_wasm_f32x4_convert_u32x4");
      break;

    case Opcode::F32X4DemoteF64X2Zero:
      WriteSimpleUnaryExpr(expr.opcode, "simde_wasm_f32x4_demote_f64x2_zero");
      break;

    case Opcode::F64X2ConvertLowI32X4S:
      WriteSimpleUnaryExpr(expr.opcode, "simde_wasm_f64x2_convert_low_i32x4");
      break;

    case Opcode::F64X2ConvertLowI32X4U:
      WriteSimpleUnaryExpr(expr.opcode, "simde_wasm_f64x2_convert_low_u32x4");
      break;

    case Opcode::F64X2PromoteLowF32X4:
      WriteSimpleUnaryExpr(expr.opcode, "simde_wasm_f64x2_promote_low_f32x4");
      break;

    default:
      WABT_UNREACHABLE;
  }
}

void CWriter::Write(const LoadExpr& expr) {
  const char* func = nullptr;
  // clang-format off
  switch (expr.opcode) {
    case Opcode::I32Load: func = "i32_load"; break;
    case Opcode::I64Load: func = "i64_load"; break;
    case Opcode::F32Load: func = "f32_load"; break;
    case Opcode::F64Load: func = "f64_load"; break;
    case Opcode::I32Load8S: func = "i32_load8_s"; break;
    case Opcode::I64Load8S: func = "i64_load8_s"; break;
    case Opcode::I32Load8U: func = "i32_load8_u"; break;
    case Opcode::I64Load8U: func = "i64_load8_u"; break;
    case Opcode::I32Load16S: func = "i32_load16_s"; break;
    case Opcode::I64Load16S: func = "i64_load16_s"; break;
    case Opcode::I32Load16U: func = "i32_load16_u"; break;
    case Opcode::I64Load16U: func = "i64_load16_u"; break;
    case Opcode::I64Load32S: func = "i64_load32_s"; break;
    case Opcode::I64Load32U: func = "i64_load32_u"; break;
    case Opcode::V128Load: func = "v128_load"; break;
    case Opcode::V128Load8X8S: func = "i16x8_load8x8"; break;
    case Opcode::V128Load8X8U: func = "u16x8_load8x8"; break;
    case Opcode::V128Load16X4S: func = "i32x4_load16x4"; break;
    case Opcode::V128Load16X4U: func = "u32x4_load16x4"; break;
    case Opcode::V128Load32X2S: func = "i64x2_load32x2"; break;
    case Opcode::V128Load32X2U: func = "u64x2_load32x2"; break;

    default:
      WABT_UNREACHABLE;
  }
  // clang-format on

  Memory* memory = module_->memories[module_->GetMemoryIndex(expr.memidx)];

  Type result_type = expr.opcode.GetResultType();
  Write(StackVar(0, result_type), " = ", func, "(",
        ExternalInstancePtr(ModuleFieldType::Memory, memory->name), ", (u64)(",
        StackVar(0), ")");
  if (expr.offset != 0)
    Write(" + ", expr.offset, "u");
  Write(");", Newline());
  DropTypes(1);
  PushType(result_type);
}

void CWriter::Write(const StoreExpr& expr) {
  const char* func = nullptr;
  // clang-format off
  switch (expr.opcode) {
    case Opcode::I32Store: func = "i32_store"; break;
    case Opcode::I64Store: func = "i64_store"; break;
    case Opcode::F32Store: func = "f32_store"; break;
    case Opcode::F64Store: func = "f64_store"; break;
    case Opcode::I32Store8: func = "i32_store8"; break;
    case Opcode::I64Store8: func = "i64_store8"; break;
    case Opcode::I32Store16: func = "i32_store16"; break;
    case Opcode::I64Store16: func = "i64_store16"; break;
    case Opcode::I64Store32: func = "i64_store32"; break;
    case Opcode::V128Store: func = "v128_store"; break;

    default:
      WABT_UNREACHABLE;
  }
  // clang-format on

  Memory* memory = module_->memories[module_->GetMemoryIndex(expr.memidx)];

  Write(func, "(", ExternalInstancePtr(ModuleFieldType::Memory, memory->name),
        ", (u64)(", StackVar(1), ")");
  if (expr.offset != 0)
    Write(" + ", expr.offset);
  Write(", ", StackVar(0), ");", Newline());
  DropTypes(2);
}

void CWriter::Write(const UnaryExpr& expr) {
  switch (expr.opcode) {
    case Opcode::I32Clz:
      WriteSimpleUnaryExpr(expr.opcode, "I32_CLZ");
      break;

    case Opcode::I64Clz:
      WriteSimpleUnaryExpr(expr.opcode, "I64_CLZ");
      break;

    case Opcode::I32Ctz:
      WriteSimpleUnaryExpr(expr.opcode, "I32_CTZ");
      break;

    case Opcode::I64Ctz:
      WriteSimpleUnaryExpr(expr.opcode, "I64_CTZ");
      break;

    case Opcode::I32Popcnt:
      WriteSimpleUnaryExpr(expr.opcode, "I32_POPCNT");
      break;

    case Opcode::I64Popcnt:
      WriteSimpleUnaryExpr(expr.opcode, "I64_POPCNT");
      break;

    case Opcode::F32Neg:
    case Opcode::F64Neg:
      WriteSimpleUnaryExpr(expr.opcode, "-");
      break;

    case Opcode::F32Abs:
      WriteSimpleUnaryExpr(expr.opcode, "wasm_fabsf");
      break;

    case Opcode::F64Abs:
      WriteSimpleUnaryExpr(expr.opcode, "wasm_fabs");
      break;

    case Opcode::F32Sqrt:
      WriteSimpleUnaryExpr(expr.opcode, "wasm_sqrtf");
      break;

    case Opcode::F64Sqrt:
      WriteSimpleUnaryExpr(expr.opcode, "wasm_sqrt");
      break;

    case Opcode::F32Ceil:
      WriteSimpleUnaryExpr(expr.opcode, "wasm_ceilf");
      break;

    case Opcode::F64Ceil:
      WriteSimpleUnaryExpr(expr.opcode, "wasm_ceil");
      break;

    case Opcode::F32Floor:
      WriteSimpleUnaryExpr(expr.opcode, "wasm_floorf");
      break;

    case Opcode::F64Floor:
      WriteSimpleUnaryExpr(expr.opcode, "wasm_floor");
      break;

    case Opcode::F32Trunc:
      WriteSimpleUnaryExpr(expr.opcode, "wasm_truncf");
      break;

    case Opcode::F64Trunc:
      WriteSimpleUnaryExpr(expr.opcode, "wasm_trunc");
      break;

    case Opcode::F32Nearest:
      WriteSimpleUnaryExpr(expr.opcode, "wasm_nearbyintf");
      break;

    case Opcode::F64Nearest:
      WriteSimpleUnaryExpr(expr.opcode, "wasm_nearbyint");
      break;

    case Opcode::I32Extend8S:
      WriteSimpleUnaryExpr(expr.opcode, "(u32)(s32)(s8)(u8)");
      break;

    case Opcode::I32Extend16S:
      WriteSimpleUnaryExpr(expr.opcode, "(u32)(s32)(s16)(u16)");
      break;

    case Opcode::I64Extend8S:
      WriteSimpleUnaryExpr(expr.opcode, "(u64)(s64)(s8)(u8)");
      break;

    case Opcode::I64Extend16S:
      WriteSimpleUnaryExpr(expr.opcode, "(u64)(s64)(s16)(u16)");
      break;

    case Opcode::I64Extend32S:
      WriteSimpleUnaryExpr(expr.opcode, "(u64)(s64)(s32)(u32)");
      break;

    case Opcode::I8X16Abs:
      WriteSimpleUnaryExpr(expr.opcode, "simde_wasm_i8x16_abs");
      break;

    case Opcode::I8X16AllTrue:
      WriteSimpleUnaryExpr(expr.opcode, "simde_wasm_i8x16_all_true");
      break;

    case Opcode::I8X16Bitmask:
      WriteSimpleUnaryExpr(expr.opcode, "simde_wasm_i8x16_bitmask");
      break;

    case Opcode::I8X16Neg:
      WriteSimpleUnaryExpr(expr.opcode, "simde_wasm_i8x16_neg");
      break;

    case Opcode::I8X16Popcnt:
      WriteSimpleUnaryExpr(expr.opcode, "simde_wasm_i8x16_popcnt");
      break;

    case Opcode::I8X16Splat:
      WriteSimpleUnaryExpr(expr.opcode, "simde_wasm_i8x16_splat");
      break;

    case Opcode::I16X8Abs:
      WriteSimpleUnaryExpr(expr.opcode, "simde_wasm_i16x8_abs");
      break;

    case Opcode::I16X8AllTrue:
      WriteSimpleUnaryExpr(expr.opcode, "simde_wasm_i16x8_all_true");
      break;

    case Opcode::I16X8Bitmask:
      WriteSimpleUnaryExpr(expr.opcode, "simde_wasm_i16x8_bitmask");
      break;

    case Opcode::I16X8ExtaddPairwiseI8X16S:
      WriteSimpleUnaryExpr(expr.opcode,
                           "simde_wasm_i16x8_extadd_pairwise_i8x16");
      break;

    case Opcode::I16X8ExtaddPairwiseI8X16U:
      WriteSimpleUnaryExpr(expr.opcode,
                           "simde_wasm_u16x8_extadd_pairwise_u8x16");
      break;

    case Opcode::I16X8ExtendHighI8X16S:
      WriteSimpleUnaryExpr(expr.opcode, "simde_wasm_i16x8_extend_high_i8x16");
      break;

    case Opcode::I16X8ExtendHighI8X16U:
      WriteSimpleUnaryExpr(expr.opcode, "simde_wasm_u16x8_extend_high_u8x16");
      break;

    case Opcode::I16X8ExtendLowI8X16S:
      WriteSimpleUnaryExpr(expr.opcode, "simde_wasm_i16x8_extend_low_i8x16");
      break;

    case Opcode::I16X8ExtendLowI8X16U:
      WriteSimpleUnaryExpr(expr.opcode, "simde_wasm_u16x8_extend_low_u8x16");
      break;

    case Opcode::I16X8Neg:
      WriteSimpleUnaryExpr(expr.opcode, "simde_wasm_i16x8_neg");
      break;

    case Opcode::I16X8Splat:
      WriteSimpleUnaryExpr(expr.opcode, "simde_wasm_i16x8_splat");
      break;

    case Opcode::I32X4Abs:
      WriteSimpleUnaryExpr(expr.opcode, "simde_wasm_i32x4_abs");
      break;

    case Opcode::I32X4AllTrue:
      WriteSimpleUnaryExpr(expr.opcode, "simde_wasm_i32x4_all_true");
      break;

    case Opcode::I32X4Bitmask:
      WriteSimpleUnaryExpr(expr.opcode, "simde_wasm_i32x4_bitmask");
      break;

    case Opcode::I32X4ExtaddPairwiseI16X8S:
      WriteSimpleUnaryExpr(expr.opcode,
                           "simde_wasm_i32x4_extadd_pairwise_i16x8");
      break;

    case Opcode::I32X4ExtaddPairwiseI16X8U:
      WriteSimpleUnaryExpr(expr.opcode,
                           "simde_wasm_u32x4_extadd_pairwise_u16x8");
      break;

    case Opcode::I32X4ExtendHighI16X8S:
      WriteSimpleUnaryExpr(expr.opcode, "simde_wasm_i32x4_extend_high_i16x8");
      break;

    case Opcode::I32X4ExtendHighI16X8U:
      WriteSimpleUnaryExpr(expr.opcode, "simde_wasm_u32x4_extend_high_u16x8");
      break;

    case Opcode::I32X4ExtendLowI16X8S:
      WriteSimpleUnaryExpr(expr.opcode, "simde_wasm_i32x4_extend_low_i16x8");
      break;

    case Opcode::I32X4ExtendLowI16X8U:
      WriteSimpleUnaryExpr(expr.opcode, "simde_wasm_u32x4_extend_low_u16x8");
      break;

    case Opcode::I32X4Neg:
      WriteSimpleUnaryExpr(expr.opcode, "simde_wasm_i32x4_neg");
      break;

    case Opcode::I32X4Splat:
      WriteSimpleUnaryExpr(expr.opcode, "simde_wasm_i32x4_splat");
      break;

    case Opcode::I64X2Abs:
      WriteSimpleUnaryExpr(expr.opcode, "simde_wasm_i64x2_abs");
      break;

    case Opcode::I64X2AllTrue:
      WriteSimpleUnaryExpr(expr.opcode, "simde_wasm_i64x2_all_true");
      break;

    case Opcode::I64X2Bitmask:
      WriteSimpleUnaryExpr(expr.opcode, "simde_wasm_i64x2_bitmask");
      break;

    case Opcode::I64X2ExtendHighI32X4S:
      WriteSimpleUnaryExpr(expr.opcode, "simde_wasm_i64x2_extend_high_i32x4");
      break;

    case Opcode::I64X2ExtendHighI32X4U:
      WriteSimpleUnaryExpr(expr.opcode, "simde_wasm_u64x2_extend_high_u32x4");
      break;

    case Opcode::I64X2ExtendLowI32X4S:
      WriteSimpleUnaryExpr(expr.opcode, "simde_wasm_i64x2_extend_low_i32x4");
      break;

    case Opcode::I64X2ExtendLowI32X4U:
      WriteSimpleUnaryExpr(expr.opcode, "simde_wasm_u64x2_extend_low_u32x4");
      break;

    case Opcode::I64X2Neg:
      WriteSimpleUnaryExpr(expr.opcode, "simde_wasm_i64x2_neg");
      break;

    case Opcode::I64X2Splat:
      WriteSimpleUnaryExpr(expr.opcode, "simde_wasm_i64x2_splat");
      break;

    case Opcode::F32X4Abs:
      WriteSimpleUnaryExpr(expr.opcode, "simde_wasm_f32x4_abs");
      break;

    case Opcode::F32X4Ceil:
      WriteSimpleUnaryExpr(expr.opcode, "simde_wasm_f32x4_ceil");
      break;

    case Opcode::F32X4Floor:
      WriteSimpleUnaryExpr(expr.opcode, "simde_wasm_f32x4_floor");
      break;

    case Opcode::F32X4Nearest:
      WriteSimpleUnaryExpr(expr.opcode, "simde_wasm_f32x4_nearest");
      break;

    case Opcode::F32X4Neg:
      WriteSimpleUnaryExpr(expr.opcode, "simde_wasm_f32x4_neg");
      break;

    case Opcode::F32X4Splat:
      WriteSimpleUnaryExpr(expr.opcode, "simde_wasm_f32x4_splat");
      break;

    case Opcode::F32X4Sqrt:
      WriteSimpleUnaryExpr(expr.opcode, "simde_wasm_f32x4_sqrt");
      break;

    case Opcode::F32X4Trunc:
      WriteSimpleUnaryExpr(expr.opcode, "simde_wasm_f32x4_trunc");
      break;

    case Opcode::F64X2Abs:
      WriteSimpleUnaryExpr(expr.opcode, "simde_wasm_f64x2_abs");
      break;

    case Opcode::F64X2Ceil:
      WriteSimpleUnaryExpr(expr.opcode, "simde_wasm_f64x2_ceil");
      break;

    case Opcode::F64X2Floor:
      WriteSimpleUnaryExpr(expr.opcode, "simde_wasm_f64x2_floor");
      break;

    case Opcode::F64X2Nearest:
      WriteSimpleUnaryExpr(expr.opcode, "simde_wasm_f64x2_nearest");
      break;

    case Opcode::F64X2Neg:
      WriteSimpleUnaryExpr(expr.opcode, "simde_wasm_f64x2_neg");
      break;

    case Opcode::F64X2Splat:
      WriteSimpleUnaryExpr(expr.opcode, "simde_wasm_f64x2_splat");
      break;

    case Opcode::F64X2Sqrt:
      WriteSimpleUnaryExpr(expr.opcode, "simde_wasm_f64x2_sqrt");
      break;

    case Opcode::F64X2Trunc:
      WriteSimpleUnaryExpr(expr.opcode, "simde_wasm_f64x2_trunc");
      break;

    case Opcode::V128AnyTrue:
      WriteSimpleUnaryExpr(expr.opcode, "simde_wasm_v128_any_true");
      break;

    case Opcode::V128Not:
      WriteSimpleUnaryExpr(expr.opcode, "simde_wasm_v128_not");
      break;

    default:
      WABT_UNREACHABLE;
  }
}

void CWriter::Write(const TernaryExpr& expr) {
  switch (expr.opcode) {
    case Opcode::V128BitSelect: {
      Type result_type = expr.opcode.GetResultType();
      Write(StackVar(2, result_type), " = ", "simde_wasm_v128_bitselect", "(",
            StackVar(2), ", ", StackVar(1), ", ", StackVar(0), ");", Newline());
      DropTypes(3);
      PushType(result_type);
      break;
    }
    default:
      WABT_UNREACHABLE;
  }
}

void CWriter::Write(const SimdLaneOpExpr& expr) {
  Type result_type = expr.opcode.GetResultType();

  switch (expr.opcode) {
    case Opcode::I8X16ExtractLaneS: {
      Write(StackVar(0, result_type), " = simde_wasm_i8x16_extract_lane(",
            StackVar(0), ", ", expr.val, ");", Newline());
      DropTypes(1);
      break;
    }
    case Opcode::I8X16ExtractLaneU: {
      Write(StackVar(0, result_type), " = simde_wasm_u8x16_extract_lane(",
            StackVar(0), ", ", expr.val, ");", Newline());
      DropTypes(1);
      break;
    }
    case Opcode::I16X8ExtractLaneS: {
      Write(StackVar(0, result_type), " = simde_wasm_i16x8_extract_lane(",
            StackVar(0), ", ", expr.val, ");", Newline());
      DropTypes(1);
      break;
    }
    case Opcode::I16X8ExtractLaneU: {
      Write(StackVar(0, result_type), " = simde_wasm_u16x8_extract_lane(",
            StackVar(0), ", ", expr.val, ");", Newline());
      DropTypes(1);
      break;
    }
    case Opcode::I32X4ExtractLane: {
      Write(StackVar(0, result_type), " = simde_wasm_i32x4_extract_lane(",
            StackVar(0), ", ", expr.val, ");", Newline());
      DropTypes(1);
      break;
    }
    case Opcode::I64X2ExtractLane: {
      Write(StackVar(0, result_type), " = simde_wasm_i64x2_extract_lane(",
            StackVar(0), ", ", expr.val, ");", Newline());
      DropTypes(1);
      break;
    }
    case Opcode::F32X4ExtractLane: {
      Write(StackVar(0, result_type), " = simde_wasm_f32x4_extract_lane(",
            StackVar(0), ", ", expr.val, ");", Newline());
      DropTypes(1);
      break;
    }
    case Opcode::F64X2ExtractLane: {
      Write(StackVar(0, result_type), " = simde_wasm_f64x2_extract_lane(",
            StackVar(0), ", ", expr.val, ");", Newline());
      DropTypes(1);
      break;
    }
    case Opcode::I8X16ReplaceLane: {
      Write(StackVar(1, result_type), " = simde_wasm_i8x16_replace_lane(",
            StackVar(1), ", ", expr.val, ", ", StackVar(0), ");", Newline());
      DropTypes(2);
      break;
    }
    case Opcode::I16X8ReplaceLane: {
      Write(StackVar(1, result_type), " = simde_wasm_i16x8_replace_lane(",
            StackVar(1), ", ", expr.val, ", ", StackVar(0), ");", Newline());
      DropTypes(2);
      break;
    }
    case Opcode::I32X4ReplaceLane: {
      Write(StackVar(1, result_type), " = simde_wasm_i32x4_replace_lane(",
            StackVar(1), ", ", expr.val, ", ", StackVar(0), ");", Newline());
      DropTypes(2);
      break;
    }
    case Opcode::I64X2ReplaceLane: {
      Write(StackVar(1, result_type), " = simde_wasm_i64x2_replace_lane(",
            StackVar(1), ", ", expr.val, ", ", StackVar(0), ");", Newline());
      DropTypes(2);
      break;
    }
    case Opcode::F32X4ReplaceLane: {
      Write(StackVar(1, result_type), " = simde_wasm_f32x4_replace_lane(",
            StackVar(1), ", ", expr.val, ", ", StackVar(0), ");", Newline());
      DropTypes(2);
      break;
    }
    case Opcode::F64X2ReplaceLane: {
      Write(StackVar(1, result_type), " = simde_wasm_f64x2_replace_lane(",
            StackVar(1), ", ", expr.val, ", ", StackVar(0), ");", Newline());
      DropTypes(2);
      break;
    }
    default:
      WABT_UNREACHABLE;
  }

  PushType(result_type);
}

void CWriter::Write(const SimdLoadLaneExpr& expr) {
  const char* func = nullptr;
  // clang-format off
  switch (expr.opcode) {
    case Opcode::V128Load8Lane: func = "v128_load8_lane"; break;
    case Opcode::V128Load16Lane: func = "v128_load16_lane"; break;
    case Opcode::V128Load32Lane: func = "v128_load32_lane"; break;
    case Opcode::V128Load64Lane: func = "v128_load64_lane"; break;
    default:
      WABT_UNREACHABLE;
  }
  // clang-format on
  Memory* memory = module_->memories[module_->GetMemoryIndex(expr.memidx)];
  Type result_type = expr.opcode.GetResultType();
  Write(StackVar(1, result_type), " = ", func, expr.val, "(",
        ExternalInstancePtr(ModuleFieldType::Memory, memory->name), ", (u64)(",
        StackVar(1), ")");

  if (expr.offset != 0)
    Write(" + ", expr.offset, "u");
  Write(", ", StackVar(0));
  Write(");", Newline());

  DropTypes(2);
  PushType(result_type);
}

void CWriter::Write(const SimdStoreLaneExpr& expr) {
  const char* func = nullptr;
  // clang-format off
  switch (expr.opcode) {
    case Opcode::V128Store8Lane: func = "v128_store8_lane"; break;
    case Opcode::V128Store16Lane: func = "v128_store16_lane"; break;
    case Opcode::V128Store32Lane: func = "v128_store32_lane"; break;
    case Opcode::V128Store64Lane: func = "v128_store64_lane"; break;
    default:
      WABT_UNREACHABLE;
  }
  // clang-format on
  Memory* memory = module_->memories[module_->GetMemoryIndex(expr.memidx)];

  Write(func, expr.val, "(",
        ExternalInstancePtr(ModuleFieldType::Memory, memory->name), ", (u64)(",
        StackVar(1), ")");

  if (expr.offset != 0)
    Write(" + ", expr.offset, "u");
  Write(", ", StackVar(0));
  Write(");", Newline());

  DropTypes(2);
}

void CWriter::Write(const SimdShuffleOpExpr& expr) {
  Type result_type = expr.opcode.GetResultType();
  switch (expr.opcode) {
    case Opcode::I8X16Shuffle: {
      Write(StackVar(1, result_type), " = simde_wasm_i8x16_shuffle(",
            StackVar(1), ", ", StackVar(0), ", ", expr.val.u8(0), ", ",
            expr.val.u8(1), ", ", expr.val.u8(2), ", ", expr.val.u8(3), ", ",
            expr.val.u8(4), ", ", expr.val.u8(5), ", ", expr.val.u8(6), ", ",
            expr.val.u8(7), ", ", expr.val.u8(8), ", ", expr.val.u8(9), ", ",
            expr.val.u8(10), ", ", expr.val.u8(11), ", ", expr.val.u8(12), ", ",
            expr.val.u8(13), ", ", expr.val.u8(14), ", ", expr.val.u8(15), ");",
            Newline());
      DropTypes(2);
      break;
    }
    default:
      WABT_UNREACHABLE;
  }
  PushType(result_type);
}

void CWriter::Write(const LoadSplatExpr& expr) {
  Memory* memory = module_->memories[module_->GetMemoryIndex(expr.memidx)];

  const char* func = nullptr;
  // clang-format off
  switch (expr.opcode) {
    case Opcode::V128Load8Splat: func = "v128_load8_splat"; break;
    case Opcode::V128Load16Splat: func = "v128_load16_splat"; break;
    case Opcode::V128Load32Splat: func = "v128_load32_splat"; break;
    case Opcode::V128Load64Splat: func = "v128_load64_splat"; break;
    default:
      WABT_UNREACHABLE;
  }
  // clang-format on
  Type result_type = expr.opcode.GetResultType();
  Write(StackVar(0, result_type), " = ", func, "(",
        ExternalInstancePtr(ModuleFieldType::Memory, memory->name), ", (u64)(",
        StackVar(0), ")");
  if (expr.offset != 0)
    Write(" + ", expr.offset);
  Write(");", Newline());

  DropTypes(1);
  PushType(result_type);
}

void CWriter::Write(const LoadZeroExpr& expr) {
  Memory* memory = module_->memories[module_->GetMemoryIndex(expr.memidx)];

  const char* func = nullptr;
  // clang-format off
  switch (expr.opcode) {
    case Opcode::V128Load32Zero: func = "v128_load32_zero"; break;
    case Opcode::V128Load64Zero: func = "v128_load64_zero"; break;
    default:
      WABT_UNREACHABLE;
  }
  // clang-format on

  Type result_type = expr.opcode.GetResultType();
  Write(StackVar(0, result_type), " = ", func, "(",
        ExternalInstancePtr(ModuleFieldType::Memory, memory->name), ", (u64)(",
        StackVar(0), ")");
  if (expr.offset != 0)
    Write(" + ", expr.offset);
  Write(");", Newline());

  DropTypes(1);
  PushType(result_type);
}

void CWriter::Write(const AtomicLoadExpr& expr) {
  const char* func = nullptr;
  // clang-format off
  switch (expr.opcode) {
    case Opcode::I32AtomicLoad: func = "i32_atomic_load"; break;
    case Opcode::I64AtomicLoad: func = "i64_atomic_load"; break;
    case Opcode::I32AtomicLoad8U: func = "i32_atomic_load8_u"; break;
    case Opcode::I64AtomicLoad8U: func = "i64_atomic_load8_u"; break;
    case Opcode::I32AtomicLoad16U: func = "i32_atomic_load16_u"; break;
    case Opcode::I64AtomicLoad16U: func = "i64_atomic_load16_u"; break;
    case Opcode::I64AtomicLoad32U: func = "i64_atomic_load32_u"; break;

    default:
      WABT_UNREACHABLE;
  }
  // clang-format on

  Memory* memory = module_->memories[module_->GetMemoryIndex(expr.memidx)];

  Type result_type = expr.opcode.GetResultType();
  Write(StackVar(0, result_type), " = ", func, "(",
        ExternalInstancePtr(ModuleFieldType::Memory, memory->name), ", (u64)(",
        StackVar(0), ")");
  if (expr.offset != 0)
    Write(" + ", expr.offset, "u");
  Write(");", Newline());
  DropTypes(1);
  PushType(result_type);
}

void CWriter::Write(const AtomicStoreExpr& expr) {
  const char* func = nullptr;
  // clang-format off
  switch (expr.opcode) {
    case Opcode::I32AtomicStore: func = "i32_atomic_store"; break;
    case Opcode::I64AtomicStore: func = "i64_atomic_store"; break;
    case Opcode::I32AtomicStore8: func = "i32_atomic_store8"; break;
    case Opcode::I64AtomicStore8: func = "i64_atomic_store8"; break;
    case Opcode::I32AtomicStore16: func = "i32_atomic_store16"; break;
    case Opcode::I64AtomicStore16: func = "i64_atomic_store16"; break;
    case Opcode::I64AtomicStore32: func = "i64_atomic_store32"; break;

    default:
      WABT_UNREACHABLE;
  }
  // clang-format on

  Memory* memory = module_->memories[module_->GetMemoryIndex(expr.memidx)];

  Write(func, "(", ExternalInstancePtr(ModuleFieldType::Memory, memory->name),
        ", (u64)(", StackVar(1), ")");
  if (expr.offset != 0)
    Write(" + ", expr.offset);
  Write(", ", StackVar(0), ");", Newline());
  DropTypes(2);
}

void CWriter::Write(const AtomicRmwExpr& expr) {
  const char* func = nullptr;
  // clang-format off
  switch (expr.opcode) {
    case Opcode::I32AtomicRmwAdd: func = "i32_atomic_rmw_add"; break;
    case Opcode::I64AtomicRmwAdd: func = "i64_atomic_rmw_add"; break;
    case Opcode::I32AtomicRmw8AddU: func = "i32_atomic_rmw8_add_u"; break;
    case Opcode::I32AtomicRmw16AddU: func = "i32_atomic_rmw16_add_u"; break;
    case Opcode::I64AtomicRmw8AddU: func = "i64_atomic_rmw8_add_u"; break;
    case Opcode::I64AtomicRmw16AddU: func = "i64_atomic_rmw16_add_u"; break;
    case Opcode::I64AtomicRmw32AddU: func = "i64_atomic_rmw32_add_u"; break;
    case Opcode::I32AtomicRmwSub: func = "i32_atomic_rmw_sub"; break;
    case Opcode::I64AtomicRmwSub: func = "i64_atomic_rmw_sub"; break;
    case Opcode::I32AtomicRmw8SubU: func = "i32_atomic_rmw8_sub_u"; break;
    case Opcode::I32AtomicRmw16SubU: func = "i32_atomic_rmw16_sub_u"; break;
    case Opcode::I64AtomicRmw8SubU: func = "i64_atomic_rmw8_sub_u"; break;
    case Opcode::I64AtomicRmw16SubU: func = "i64_atomic_rmw16_sub_u"; break;
    case Opcode::I64AtomicRmw32SubU: func = "i64_atomic_rmw32_sub_u"; break;
    case Opcode::I32AtomicRmwAnd: func = "i32_atomic_rmw_and"; break;
    case Opcode::I64AtomicRmwAnd: func = "i64_atomic_rmw_and"; break;
    case Opcode::I32AtomicRmw8AndU: func = "i32_atomic_rmw8_and_u"; break;
    case Opcode::I32AtomicRmw16AndU: func = "i32_atomic_rmw16_and_u"; break;
    case Opcode::I64AtomicRmw8AndU: func = "i64_atomic_rmw8_and_u"; break;
    case Opcode::I64AtomicRmw16AndU: func = "i64_atomic_rmw16_and_u"; break;
    case Opcode::I64AtomicRmw32AndU: func = "i64_atomic_rmw32_and_u"; break;
    case Opcode::I32AtomicRmwOr: func = "i32_atomic_rmw_or"; break;
    case Opcode::I64AtomicRmwOr: func = "i64_atomic_rmw_or"; break;
    case Opcode::I32AtomicRmw8OrU: func = "i32_atomic_rmw8_or_u"; break;
    case Opcode::I32AtomicRmw16OrU: func = "i32_atomic_rmw16_or_u"; break;
    case Opcode::I64AtomicRmw8OrU: func = "i64_atomic_rmw8_or_u"; break;
    case Opcode::I64AtomicRmw16OrU: func = "i64_atomic_rmw16_or_u"; break;
    case Opcode::I64AtomicRmw32OrU: func = "i64_atomic_rmw32_or_u"; break;
    case Opcode::I32AtomicRmwXor: func = "i32_atomic_rmw_xor"; break;
    case Opcode::I64AtomicRmwXor: func = "i64_atomic_rmw_xor"; break;
    case Opcode::I32AtomicRmw8XorU: func = "i32_atomic_rmw8_xor_u"; break;
    case Opcode::I32AtomicRmw16XorU: func = "i32_atomic_rmw16_xor_u"; break;
    case Opcode::I64AtomicRmw8XorU: func = "i64_atomic_rmw8_xor_u"; break;
    case Opcode::I64AtomicRmw16XorU: func = "i64_atomic_rmw16_xor_u"; break;
    case Opcode::I64AtomicRmw32XorU: func = "i64_atomic_rmw32_xor_u"; break;
    case Opcode::I32AtomicRmwXchg: func = "i32_atomic_rmw_xchg"; break;
    case Opcode::I64AtomicRmwXchg: func = "i64_atomic_rmw_xchg"; break;
    case Opcode::I32AtomicRmw8XchgU: func = "i32_atomic_rmw8_xchg_u"; break;
    case Opcode::I32AtomicRmw16XchgU: func = "i32_atomic_rmw16_xchg_u"; break;
    case Opcode::I64AtomicRmw8XchgU: func = "i64_atomic_rmw8_xchg_u"; break;
    case Opcode::I64AtomicRmw16XchgU: func = "i64_atomic_rmw16_xchg_u"; break;
    case Opcode::I64AtomicRmw32XchgU: func = "i64_atomic_rmw32_xchg_u"; break;
    default:
      WABT_UNREACHABLE;
  }
  // clang-format on

  Memory* memory = module_->memories[module_->GetMemoryIndex(expr.memidx)];
  Type result_type = expr.opcode.GetResultType();

  Write(StackVar(1, result_type), " = ", func, "(",
        ExternalInstancePtr(ModuleFieldType::Memory, memory->name), ", (u64)(",
        StackVar(1), ")");
  if (expr.offset != 0)
    Write(" + ", expr.offset);
  Write(", ", StackVar(0), ");", Newline());
  DropTypes(2);
  PushType(result_type);
}

void CWriter::Write(const AtomicRmwCmpxchgExpr& expr) {
  const char* func = nullptr;
  // clang-format off
  switch(expr.opcode) {
    case Opcode::I32AtomicRmwCmpxchg: func = "i32_atomic_rmw_cmpxchg"; break;
    case Opcode::I64AtomicRmwCmpxchg: func = "i64_atomic_rmw_cmpxchg"; break;
    case Opcode::I32AtomicRmw8CmpxchgU: func = "i32_atomic_rmw8_cmpxchg_u"; break;
    case Opcode::I32AtomicRmw16CmpxchgU: func = "i32_atomic_rmw16_cmpxchg_u"; break;
    case Opcode::I64AtomicRmw8CmpxchgU: func = "i64_atomic_rmw8_cmpxchg_u"; break;
    case Opcode::I64AtomicRmw16CmpxchgU: func = "i64_atomic_rmw16_cmpxchg_u"; break;
    case Opcode::I64AtomicRmw32CmpxchgU: func = "i64_atomic_rmw32_cmpxchg_u"; break;
    default:
      WABT_UNREACHABLE;
  }
  // clang-format on

  Memory* memory = module_->memories[module_->GetMemoryIndex(expr.memidx)];
  Type result_type = expr.opcode.GetResultType();

  Write(StackVar(2, result_type), " = ", func, "(",
        ExternalInstancePtr(ModuleFieldType::Memory, memory->name), ", (u64)(",
        StackVar(2), ")");
  if (expr.offset != 0)
    Write(" + ", expr.offset);
  Write(", ", StackVar(1), ", ", StackVar(0), ");", Newline());
  DropTypes(3);
  PushType(result_type);
}

void CWriter::ReserveExportNames() {
  for (const Export* export_ : module_->exports) {
    ReserveExportName(export_->name);
  }
}

void CWriter::WriteCHeader() {
  ReserveExportNames();

  stream_ = h_stream_;
  std::string guard = GenerateHeaderGuard();
  Write("/* Automatically generated by wasm2c */", Newline());
  Write("#ifndef ", guard, Newline());
  Write("#define ", guard, Newline());
  Write(Newline());
  ComputeSimdScope();
  WriteHeaderIncludes();
  Write(s_header_top);
  Write(Newline());
  WriteModuleInstance();
  WriteInitDecl();
  WriteFreeDecl();
  WriteGetFuncTypeDecl();
  WriteMultivalueResultTypes();
  WriteImports();
  WriteImportProperties(CWriterPhase::Declarations);
  WriteExports(CWriterPhase::Declarations);
  if (options_.features.tail_call_enabled()) {
    WriteTailCallExports(CWriterPhase::Declarations);
  }
  Write(Newline());
  Write(s_header_bottom);
  Write(Newline(), "#endif  /* ", guard, " */", Newline());
}

void CWriter::WriteCSource() {
  /* Write the "top" to h_impl stream */
  stream_ = h_impl_stream_;
  Write("/* Automatically generated by wasm2c */", Newline());
  WriteSourceTop();

  /* Write module-wide declarations to impl header */
  WriteFuncTypeDecls();
  WriteTagTypes();
  WriteTagDecls();
  WriteFuncDeclarations();
  WriteDataInitializerDecls();
  WriteElemInitializerDecls();
  if (options_.features.tail_call_enabled()) {
    WriteTailcalleeParamTypes();
  }

  /* Write the module-wide material to the first output stream */
  stream_ = c_streams_.front();
  WriteMultiCTop();
  WriteFuncTypes();
  WriteTags();
  WriteGlobalInitializers();
  WriteDataInitializers();
  WriteElemInitializers();
  WriteExports(CWriterPhase::Definitions);
  if (options_.features.tail_call_enabled()) {
    WriteTailCallExports(CWriterPhase::Definitions);
    WriteTailCallWeakImports();
  }
  WriteInitInstanceImport();
  WriteImportProperties(CWriterPhase::Definitions);
  WriteInit();
  WriteFree();
  WriteGetFuncType();

  /* Write function bodies across the different output streams */
  WriteFuncs();

  /* For any empty .c output, write a dummy typedef to avoid gcc warning */
  WriteMultiCTopEmpty();
}

Result CWriter::WriteModule(const Module& module) {
  WABT_USE(options_);
  module_ = &module;
  WriteCHeader();
  WriteCSource();
  return result_;
}

// static
const char* CWriter::GetReferenceTypeName(const Type& type) {
  switch (type) {
    case Type::FuncRef:
      return "funcref";
    case Type::ExternRef:
      return "externref";
    default:
      WABT_UNREACHABLE;
  }
}

// static
const char* CWriter::GetReferenceNullValue(const Type& type) {
  switch (type) {
    case Type::FuncRef:
      return "wasm_rt_funcref_null_value";
    case Type::ExternRef:
      return "wasm_rt_externref_null_value";
    default:
      WABT_UNREACHABLE;
  }
}

const char* CWriter::InternalSymbolScope() const {
  if (c_streams_.size() == 1) {
    return "static ";
  } else {
    return "";
  }
}

}  // end anonymous namespace

Result WriteC(std::vector<Stream*>&& c_streams,
              Stream* h_stream,
              Stream* h_impl_stream,
              const char* header_name,
              const char* header_impl_name,
              const Module* module,
              const WriteCOptions& options) {
  CWriter c_writer(std::move(c_streams), h_stream, h_impl_stream, header_name,
                   header_impl_name, options);
  return c_writer.WriteModule(*module);
}

}  // namespace wabt
