/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- * vim: set ts=8 sts=2 et sw=2 tw=80: * * Copyright 2021 Mozilla Foundation * * 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. */ #ifndef wasm_valtype_h #define wasm_valtype_h #include "mozilla/HashTable.h" #include "mozilla/Maybe.h" #include #include "jit/IonTypes.h" #include "wasm/WasmConstants.h" #include "wasm/WasmSerialize.h" #include "wasm/WasmTypeDecls.h" namespace js { namespace wasm { using mozilla::Maybe; class RecGroup; class TypeDef; class TypeContext; enum class TypeDefKind : uint8_t; // A PackedTypeCode represents any value type. union PackedTypeCode { public: using PackedRepr = uint64_t; private: static constexpr size_t NullableBits = 1; static constexpr size_t TypeCodeBits = 8; static constexpr size_t TypeDefBits = 48; static constexpr size_t PointerTagBits = 2; static_assert(NullableBits + TypeCodeBits + TypeDefBits + PointerTagBits <= (sizeof(PackedRepr) * 8), "enough bits"); PackedRepr bits_; struct { PackedRepr nullable_ : NullableBits; PackedRepr typeCode_ : TypeCodeBits; // A pointer to the TypeDef this type references. We use 48-bits for this, // and rely on system memory allocators not allocating outside of this // range. This is also assumed by JS::Value, and so should be safe here. PackedRepr typeDef_ : TypeDefBits; // Reserve the bottom two bits for use as a tagging scheme for BlockType // and ResultType, which can encode a ValType inside themselves in special // cases. PackedRepr pointerTag_ : PointerTagBits; }; public: static constexpr PackedRepr NoTypeCode = ((uint64_t)1 << TypeCodeBits) - 1; static PackedTypeCode invalid() { PackedTypeCode ptc = {}; ptc.typeCode_ = NoTypeCode; return ptc; } static constexpr PackedTypeCode fromBits(PackedRepr bits) { PackedTypeCode ptc = {}; ptc.bits_ = bits; return ptc; } static PackedTypeCode pack(TypeCode tc, const TypeDef* typeDef, bool isNullable) { MOZ_ASSERT(uint32_t(tc) <= ((1 << TypeCodeBits) - 1)); MOZ_ASSERT_IF(tc != AbstractTypeRefCode, typeDef == nullptr); MOZ_ASSERT_IF(tc == AbstractTypeRefCode, typeDef != nullptr); // Double check that the type definition was allocated within 48-bits, as // noted above. MOZ_ASSERT((uint64_t)typeDef <= ((uint64_t)1 << TypeDefBits) - 1); PackedTypeCode ptc = {}; ptc.typeCode_ = PackedRepr(tc); ptc.typeDef_ = (uintptr_t)typeDef; ptc.nullable_ = isNullable; return ptc; } static PackedTypeCode pack(TypeCode tc, bool nullable) { return pack(tc, nullptr, nullable); } static PackedTypeCode pack(TypeCode tc) { return pack(tc, nullptr, false); } bool isValid() const { return typeCode_ != NoTypeCode; } PackedRepr bits() const { return bits_; } TypeCode typeCode() const { MOZ_ASSERT(isValid()); return TypeCode(typeCode_); } // Return the TypeCode, but return AbstractReferenceTypeCode for any reference // type. // // This function is very, very hot, hence what would normally be a switch on // the value `c` to map the reference types to AbstractReferenceTypeCode has // been distilled into a simple comparison; this is fastest. Should type // codes become too complicated for this to work then a lookup table also has // better performance than a switch. // // An alternative is for the PackedTypeCode to represent something closer to // what ValType needs, so that this decoding step is not necessary, but that // moves complexity elsewhere, and the perf gain here would be only about 1% // for baseline compilation throughput. TypeCode typeCodeAbstracted() const { TypeCode tc = typeCode(); return tc < LowestPrimitiveTypeCode ? AbstractReferenceTypeCode : tc; } // Return whether this type is a reference type. bool isRefType() const { return typeCodeAbstracted() == AbstractReferenceTypeCode; } // Return whether this type is represented by a reference at runtime. bool isRefRepr() const { return typeCode() < LowestPrimitiveTypeCode; } const TypeDef* typeDef() const { MOZ_ASSERT(isValid()); return (const TypeDef*)(uintptr_t)typeDef_; } bool isNullable() const { MOZ_ASSERT(isValid()); return bool(nullable_); } PackedTypeCode withIsNullable(bool nullable) const { MOZ_ASSERT(isRefType()); PackedTypeCode mutated = *this; mutated.nullable_ = (PackedRepr)nullable; return mutated; } bool operator==(const PackedTypeCode& rhs) const { return bits_ == rhs.bits_; } bool operator!=(const PackedTypeCode& rhs) const { return bits_ != rhs.bits_; } }; static_assert(sizeof(PackedTypeCode) == sizeof(uint64_t), "packed"); // A SerializableTypeCode represents any value type in a form that can be // serialized and deserialized. union SerializableTypeCode { using PackedRepr = uintptr_t; static constexpr size_t NullableBits = 1; static constexpr size_t TypeCodeBits = 8; static constexpr size_t TypeIndexBits = 20; PackedRepr bits; struct { PackedRepr nullable : NullableBits; PackedRepr typeCode : TypeCodeBits; PackedRepr typeIndex : TypeIndexBits; }; WASM_CHECK_CACHEABLE_POD(bits); static constexpr PackedRepr NoTypeIndex = (1 << TypeIndexBits) - 1; static_assert(NullableBits + TypeCodeBits + TypeIndexBits <= (sizeof(PackedRepr) * 8), "enough bits"); static_assert(NoTypeIndex < (1 << TypeIndexBits), "enough bits"); static_assert(MaxTypes < NoTypeIndex, "enough bits"); // Defined in WasmSerialize.cpp static inline SerializableTypeCode serialize(PackedTypeCode ptc, const TypeContext& types); inline PackedTypeCode deserialize(const TypeContext& types); }; WASM_DECLARE_CACHEABLE_POD(SerializableTypeCode); static_assert(sizeof(SerializableTypeCode) == sizeof(uintptr_t), "packed"); // [SMDOC] Matching type definitions // // WebAssembly type equality is structural, and we implement canonicalization // such that equality of pointers to type definitions means that the type // definitions are structurally equal. // // 'Matching' is the algorithm used to determine if two types are equal while // canonicalizing types. // // A match type code encodes a type code for use in equality and hashing // matching. It normalizes type references that are local to a recursion group // so that they can be bitwise compared to type references from other recursion // groups. // // This is useful for the following example: // (rec (func $a)) // (rec // (func $b) // (struct // (field (ref $a))) // (field (ref $b))) // ) // (rec // (func $c) // (struct // (field (ref $a))) // (field (ref $c))) // ) // // The last two recursion groups are identical and should canonicalize to the // same instance. However, they will be initially represented as two separate // recursion group instances each with an array type instance with element // types that point to the function type instance before them. A bitwise // comparison of the element type pointers would fail. // // To solve this, we use `MatchTypeCode` to convert the example to: // (rec (func $a)) // (rec // (func $b) // (struct // (field (ref nonlocal $a))) // (field (ref local 0))) // ) // (rec // (func $c) // (struct // (field (ref nonlocal $a))) // (field (ref local 0))) // ) // // Now, comparing the element types will see that these are local type // references of the same kinds. `MatchTypeCode` performs the same mechanism // as `tie` in the MVP presentation of type equality [1]. // // [1] // https://github.com/WebAssembly/gc/blob/main/proposals/gc/MVP.md#equivalence union MatchTypeCode { using PackedRepr = uint64_t; static constexpr size_t NullableBits = 1; static constexpr size_t TypeCodeBits = 8; static constexpr size_t TypeRefBits = 48; PackedRepr bits; struct { PackedRepr nullable : NullableBits; PackedRepr typeCode : TypeCodeBits; PackedRepr typeRef : TypeRefBits; }; WASM_CHECK_CACHEABLE_POD(bits); static_assert(NullableBits + TypeCodeBits + TypeRefBits <= (sizeof(PackedRepr) * 8), "enough bits"); // Defined in WasmTypeDef.h to avoid a cycle while allowing inlining static inline MatchTypeCode forMatch(PackedTypeCode ptc, const RecGroup* recGroup); bool operator==(MatchTypeCode other) const { return bits == other.bits; } bool operator!=(MatchTypeCode other) const { return bits != other.bits; } HashNumber hash() const { return HashNumber(bits); } }; // An enum that describes the representation classes for tables; The table // element type is mapped into this by Table::repr(). enum class TableRepr { Ref, Func }; // An enum that describes the different type hierarchies. enum class RefTypeHierarchy { Func, Extern, Any }; // The RefType carries more information about types t for which t.isRefType() // is true. class RefType { public: enum Kind { Func = uint8_t(TypeCode::FuncRef), Extern = uint8_t(TypeCode::ExternRef), Any = uint8_t(TypeCode::AnyRef), NoFunc = uint8_t(TypeCode::NullFuncRef), NoExtern = uint8_t(TypeCode::NullExternRef), None = uint8_t(TypeCode::NullAnyRef), Eq = uint8_t(TypeCode::EqRef), Struct = uint8_t(TypeCode::StructRef), Array = uint8_t(TypeCode::ArrayRef), TypeRef = uint8_t(AbstractTypeRefCode) }; private: PackedTypeCode ptc_; RefType(Kind kind, bool nullable) : ptc_(PackedTypeCode::pack(TypeCode(kind), nullable)) { MOZ_ASSERT(isValid()); } RefType(const TypeDef* typeDef, bool nullable) : ptc_(PackedTypeCode::pack(AbstractTypeRefCode, typeDef, nullable)) { MOZ_ASSERT(isValid()); } public: RefType() : ptc_(PackedTypeCode::invalid()) {} explicit RefType(PackedTypeCode ptc) : ptc_(ptc) { MOZ_ASSERT(isValid()); } static RefType fromTypeCode(TypeCode tc, bool nullable) { MOZ_ASSERT(tc != AbstractTypeRefCode); return RefType(Kind(tc), nullable); } static RefType fromTypeDef(const TypeDef* typeDef, bool nullable) { return RefType(typeDef, nullable); } Kind kind() const { return Kind(ptc_.typeCode()); } const TypeDef* typeDef() const { return ptc_.typeDef(); } PackedTypeCode packed() const { return ptc_; } PackedTypeCode* addressOfPacked() { return &ptc_; } const PackedTypeCode* addressOfPacked() const { return &ptc_; } #ifdef DEBUG bool isValid() const { MOZ_ASSERT((ptc_.typeCode() == AbstractTypeRefCode) == (ptc_.typeDef() != nullptr)); switch (ptc_.typeCode()) { case TypeCode::FuncRef: case TypeCode::ExternRef: case TypeCode::AnyRef: case TypeCode::EqRef: case TypeCode::StructRef: case TypeCode::ArrayRef: case TypeCode::NullFuncRef: case TypeCode::NullExternRef: case TypeCode::NullAnyRef: case AbstractTypeRefCode: return true; default: return false; } } #endif static RefType func() { return RefType(Func, true); } static RefType extern_() { return RefType(Extern, true); } static RefType any() { return RefType(Any, true); } static RefType nofunc() { return RefType(NoFunc, true); } static RefType noextern() { return RefType(NoExtern, true); } static RefType none() { return RefType(None, true); } static RefType eq() { return RefType(Eq, true); } static RefType struct_() { return RefType(Struct, true); } static RefType array() { return RefType(Array, true); } bool isFunc() const { return kind() == RefType::Func; } bool isExtern() const { return kind() == RefType::Extern; } bool isAny() const { return kind() == RefType::Any; } bool isNoFunc() const { return kind() == RefType::NoFunc; } bool isNoExtern() const { return kind() == RefType::NoExtern; } bool isNone() const { return kind() == RefType::None; } bool isEq() const { return kind() == RefType::Eq; } bool isStruct() const { return kind() == RefType::Struct; } bool isArray() const { return kind() == RefType::Array; } bool isTypeRef() const { return kind() == RefType::TypeRef; } bool isNullable() const { return bool(ptc_.isNullable()); } RefType asNonNullable() const { return withIsNullable(false); } RefType withIsNullable(bool nullable) const { return RefType(ptc_.withIsNullable(nullable)); } bool isRefBottom() const { return isNone() || isNoFunc() || isNoExtern(); } // These methods are defined in WasmTypeDef.h to avoid a cycle while allowing // inlining. inline RefTypeHierarchy hierarchy() const; inline TableRepr tableRepr() const; inline bool isFuncHierarchy() const; inline bool isExternHierarchy() const; inline bool isAnyHierarchy() const; static bool isSubTypeOf(RefType subType, RefType superType); static bool castPossible(RefType sourceType, RefType destType); // Gets the top of the given type's hierarchy, e.g. Any for structs and // arrays, and Func for funcs RefType topType() const; // Gets the TypeDefKind associated with this RefType, e.g. TypeDefKind::Struct // for RefType::Struct. TypeDefKind typeDefKind() const; bool operator==(const RefType& that) const { return ptc_ == that.ptc_; } bool operator!=(const RefType& that) const { return ptc_ != that.ptc_; } }; class FieldTypeTraits { public: enum Kind { I8 = uint8_t(TypeCode::I8), I16 = uint8_t(TypeCode::I16), I32 = uint8_t(TypeCode::I32), I64 = uint8_t(TypeCode::I64), F32 = uint8_t(TypeCode::F32), F64 = uint8_t(TypeCode::F64), V128 = uint8_t(TypeCode::V128), Ref = uint8_t(AbstractReferenceTypeCode), }; static bool isValidTypeCode(TypeCode tc) { switch (tc) { #ifdef ENABLE_WASM_GC case TypeCode::I8: case TypeCode::I16: #endif case TypeCode::I32: case TypeCode::I64: case TypeCode::F32: case TypeCode::F64: #ifdef ENABLE_WASM_SIMD case TypeCode::V128: #endif case TypeCode::FuncRef: case TypeCode::ExternRef: #ifdef ENABLE_WASM_GC case TypeCode::AnyRef: case TypeCode::EqRef: case TypeCode::StructRef: case TypeCode::ArrayRef: case TypeCode::NullFuncRef: case TypeCode::NullExternRef: case TypeCode::NullAnyRef: #endif #ifdef ENABLE_WASM_FUNCTION_REFERENCES case AbstractTypeRefCode: #endif return true; default: return false; } } static bool isNumberTypeCode(TypeCode tc) { switch (tc) { case TypeCode::I32: case TypeCode::I64: case TypeCode::F32: case TypeCode::F64: return true; default: return false; } } static bool isPackedTypeCode(TypeCode tc) { switch (tc) { #ifdef ENABLE_WASM_GC case TypeCode::I8: case TypeCode::I16: return true; #endif default: return false; } } static bool isVectorTypeCode(TypeCode tc) { switch (tc) { #ifdef ENABLE_WASM_SIMD case TypeCode::V128: return true; #endif default: return false; } } }; class ValTypeTraits { public: enum Kind { I32 = uint8_t(TypeCode::I32), I64 = uint8_t(TypeCode::I64), F32 = uint8_t(TypeCode::F32), F64 = uint8_t(TypeCode::F64), V128 = uint8_t(TypeCode::V128), Ref = uint8_t(AbstractReferenceTypeCode), }; static bool isValidTypeCode(TypeCode tc) { switch (tc) { case TypeCode::I32: case TypeCode::I64: case TypeCode::F32: case TypeCode::F64: #ifdef ENABLE_WASM_SIMD case TypeCode::V128: #endif case TypeCode::FuncRef: case TypeCode::ExternRef: #ifdef ENABLE_WASM_GC case TypeCode::AnyRef: case TypeCode::EqRef: case TypeCode::StructRef: case TypeCode::ArrayRef: case TypeCode::NullFuncRef: case TypeCode::NullExternRef: case TypeCode::NullAnyRef: #endif #ifdef ENABLE_WASM_FUNCTION_REFERENCES case AbstractTypeRefCode: #endif return true; default: return false; } } static bool isNumberTypeCode(TypeCode tc) { switch (tc) { case TypeCode::I32: case TypeCode::I64: case TypeCode::F32: case TypeCode::F64: return true; default: return false; } } static bool isPackedTypeCode(TypeCode tc) { return false; } static bool isVectorTypeCode(TypeCode tc) { switch (tc) { #ifdef ENABLE_WASM_SIMD case TypeCode::V128: return true; #endif default: return false; } } }; // The PackedType represents the storage type of a WebAssembly location, whether // parameter, local, field, or global. See specializations below for ValType and // FieldType. template class PackedType : public T { public: using Kind = typename T::Kind; protected: PackedTypeCode tc_; explicit PackedType(TypeCode c) : tc_(PackedTypeCode::pack(c)) { MOZ_ASSERT(c != AbstractTypeRefCode); MOZ_ASSERT(isValid()); } TypeCode typeCode() const { MOZ_ASSERT(isValid()); return tc_.typeCode(); } public: PackedType() : tc_(PackedTypeCode::invalid()) {} MOZ_IMPLICIT PackedType(Kind c) : tc_(PackedTypeCode::pack(TypeCode(c))) { MOZ_ASSERT(c != Kind::Ref); MOZ_ASSERT(isValid()); } MOZ_IMPLICIT PackedType(RefType rt) : tc_(rt.packed()) { MOZ_ASSERT(isValid()); } explicit PackedType(PackedTypeCode ptc) : tc_(ptc) { MOZ_ASSERT(isValid()); } static PackedType fromMIRType(jit::MIRType mty) { switch (mty) { case jit::MIRType::Int32: return PackedType::I32; break; case jit::MIRType::Int64: return PackedType::I64; break; case jit::MIRType::Float32: return PackedType::F32; break; case jit::MIRType::Double: return PackedType::F64; break; case jit::MIRType::Simd128: return PackedType::V128; break; case jit::MIRType::RefOrNull: return PackedType::Ref; default: MOZ_CRASH("fromMIRType: unexpected type"); } } static PackedType fromNonRefTypeCode(TypeCode tc) { #ifdef DEBUG switch (tc) { case TypeCode::I8: case TypeCode::I16: case TypeCode::I32: case TypeCode::I64: case TypeCode::F32: case TypeCode::F64: case TypeCode::V128: break; default: MOZ_CRASH("Bad type code"); } #endif return PackedType(tc); } static PackedType fromBitsUnsafe(PackedTypeCode::PackedRepr bits) { return PackedType(PackedTypeCode::fromBits(bits)); } bool isValid() const { if (!tc_.isValid()) { return false; } return T::isValidTypeCode(tc_.typeCode()); } MatchTypeCode forMatch(const RecGroup* recGroup) const { return MatchTypeCode::forMatch(tc_, recGroup); } PackedTypeCode packed() const { MOZ_ASSERT(isValid()); return tc_; } PackedTypeCode* addressOfPacked() { return &tc_; } const PackedTypeCode* addressOfPacked() const { return &tc_; } PackedTypeCode::PackedRepr bitsUnsafe() const { MOZ_ASSERT(isValid()); return tc_.bits(); } bool isNumber() const { return T::isNumberTypeCode(tc_.typeCode()); } bool isPacked() const { return T::isPackedTypeCode(tc_.typeCode()); } bool isVector() const { return T::isVectorTypeCode(tc_.typeCode()); } bool isRefType() const { return tc_.isRefType(); } bool isFuncRef() const { return tc_.typeCode() == TypeCode::FuncRef; } bool isExternRef() const { return tc_.typeCode() == TypeCode::ExternRef; } bool isAnyRef() const { return tc_.typeCode() == TypeCode::AnyRef; } bool isNoFunc() const { return tc_.typeCode() == TypeCode::NullFuncRef; } bool isNoExtern() const { return tc_.typeCode() == TypeCode::NullExternRef; } bool isNone() const { return tc_.typeCode() == TypeCode::NullAnyRef; } bool isEqRef() const { return tc_.typeCode() == TypeCode::EqRef; } bool isStructRef() const { return tc_.typeCode() == TypeCode::StructRef; } bool isArrayRef() const { return tc_.typeCode() == TypeCode::ArrayRef; } bool isTypeRef() const { return tc_.typeCode() == AbstractTypeRefCode; } bool isRefRepr() const { return tc_.isRefRepr(); } // Returns whether the type has a default value. bool isDefaultable() const { return !(isRefType() && !isNullable()); } // Returns whether the type has a representation in JS. bool isExposable() const { #if defined(ENABLE_WASM_SIMD) return kind() != Kind::V128; #else return true; #endif } bool isNullable() const { return tc_.isNullable(); } const TypeDef* typeDef() const { return tc_.typeDef(); } Kind kind() const { return Kind(tc_.typeCodeAbstracted()); } RefType refType() const { MOZ_ASSERT(isRefType()); return RefType(tc_); } RefType::Kind refTypeKind() const { MOZ_ASSERT(isRefType()); return RefType(tc_).kind(); } // Some types are encoded as JS::Value when they escape from Wasm (when passed // as parameters to imports or returned from exports). For ExternRef the // Value encoding is pretty much a requirement. For other types it's a choice // that may (temporarily) simplify some code. bool isEncodedAsJSValueOnEscape() const { return isRefType(); } uint32_t size() const { switch (tc_.typeCodeAbstracted()) { case TypeCode::I8: return 1; case TypeCode::I16: return 2; case TypeCode::I32: return 4; case TypeCode::I64: return 8; case TypeCode::F32: return 4; case TypeCode::F64: return 8; case TypeCode::V128: return 16; case AbstractReferenceTypeCode: return sizeof(void*); default: MOZ_ASSERT_UNREACHABLE(); return 0; } } uint32_t alignmentInStruct() const { return size(); } uint32_t indexingShift() const { switch (size()) { case 1: return 0; case 2: return 1; case 4: return 2; case 8: return 3; case 16: return 4; default: MOZ_ASSERT_UNREACHABLE(); return 0; } } PackedType widenToValType() const { switch (tc_.typeCodeAbstracted()) { case TypeCode::I8: case TypeCode::I16: return PackedType::I32; default: return PackedType(tc_); } } PackedType valType() const { MOZ_ASSERT(isValType()); return PackedType(tc_); } // Note, ToMIRType is only correct within Wasm, where an AnyRef is represented // as a pointer. At the JS/wasm boundary, an AnyRef can be represented as a // JS::Value, and the type translation may have to be handled specially and on // a case-by-case basis. jit::MIRType toMIRType() const { switch (tc_.typeCodeAbstracted()) { case TypeCode::I32: return jit::MIRType::Int32; case TypeCode::I64: return jit::MIRType::Int64; case TypeCode::F32: return jit::MIRType::Float32; case TypeCode::F64: return jit::MIRType::Double; case TypeCode::V128: return jit::MIRType::Simd128; case AbstractReferenceTypeCode: return jit::MIRType::RefOrNull; default: MOZ_CRASH("bad type"); } } bool isValType() const { switch (tc_.typeCode()) { case TypeCode::I8: case TypeCode::I16: return false; default: return true; } } PackedType fieldType() const { MOZ_ASSERT(isValid()); return PackedType(tc_); } static bool isSubTypeOf(PackedType subType, PackedType superType) { // Anything is a subtype of itself. if (subType == superType) { return true; } // A reference may be a subtype of another reference if (subType.isRefType() && superType.isRefType()) { return RefType::isSubTypeOf(subType.refType(), superType.refType()); } return false; } bool operator==(const PackedType& that) const { MOZ_ASSERT(isValid() && that.isValid()); return tc_ == that.tc_; } bool operator!=(const PackedType& that) const { MOZ_ASSERT(isValid() && that.isValid()); return tc_ != that.tc_; } bool operator==(Kind that) const { MOZ_ASSERT(isValid()); MOZ_ASSERT(that != Kind::Ref); return Kind(typeCode()) == that; } bool operator!=(Kind that) const { return !(*this == that); } }; using ValType = PackedType; using FieldType = PackedType; // The dominant use of this data type is for locals and args, and profiling // with ZenGarden and Tanks suggests an initial size of 16 minimises heap // allocation, both in terms of blocks and bytes. using ValTypeVector = Vector; // ValType utilities extern bool ToValType(JSContext* cx, HandleValue v, ValType* out); extern bool ToRefType(JSContext* cx, HandleValue v, RefType* out); extern UniqueChars ToString(RefType type, const TypeContext* types); extern UniqueChars ToString(ValType type, const TypeContext* types); extern UniqueChars ToString(FieldType type, const TypeContext* types); extern UniqueChars ToString(const Maybe& type, const TypeContext* types); } // namespace wasm } // namespace js #endif // wasm_valtype_h