forked from nodejs/node
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcanonical-types.cc
More file actions
498 lines (454 loc) Β· 21.5 KB
/
canonical-types.cc
File metadata and controls
498 lines (454 loc) Β· 21.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
// Copyright 2022 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "src/wasm/canonical-types.h"
#include "src/base/hashing.h"
#include "src/execution/isolate.h"
#include "src/handles/handles-inl.h"
#include "src/heap/heap-inl.h"
#include "src/init/v8.h"
#include "src/roots/roots-inl.h"
#include "src/utils/utils.h"
#include "src/wasm/names-provider.h"
#include "src/wasm/std-object-sizes.h"
#include "src/wasm/wasm-engine.h"
namespace v8::internal::wasm {
TypeCanonicalizer* GetTypeCanonicalizer() {
return GetWasmEngine()->type_canonicalizer();
}
TypeCanonicalizer::TypeCanonicalizer() { AddPredefinedArrayTypes(); }
void TypeCanonicalizer::CheckMaxCanonicalIndex() const {
if (V8_UNLIKELY(canonical_supertypes_.size() > kMaxCanonicalTypes)) {
V8::FatalProcessOutOfMemory(nullptr, "too many canonicalized types");
}
}
void TypeCanonicalizer::AddRecursiveGroup(WasmModule* module, uint32_t size) {
if (size == 0) return;
// If the caller knows statically that {size == 1}, it should have called
// {AddRecursiveSingletonGroup} directly. For cases where this is not
// statically determined we add this dispatch here.
if (size == 1) return AddRecursiveSingletonGroup(module);
uint32_t start_index = static_cast<uint32_t>(module->types.size() - size);
// Multiple threads could try to register recursive groups concurrently.
// TODO(manoskouk): Investigate if we can fine-grain the synchronization.
base::MutexGuard mutex_guard(&mutex_);
// Compute the first canonical index in the recgroup in the case that it does
// not already exist.
CanonicalTypeIndex first_new_canonical_index{
static_cast<uint32_t>(canonical_supertypes_.size())};
// Create a snapshot of the zone; this will be restored in case we find a
// matching recursion group.
ZoneSnapshot zone_snapshot = zone_.Snapshot();
DCHECK_GE(module->types.size(), start_index + size);
CanonicalGroup group{&zone_, size, first_new_canonical_index};
for (uint32_t i = 0; i < size; i++) {
group.types[i] = CanonicalizeTypeDef(
module, ModuleTypeIndex{start_index + i}, ModuleTypeIndex{start_index},
first_new_canonical_index);
}
if (CanonicalTypeIndex canonical_index = FindCanonicalGroup(group);
canonical_index.valid()) {
// Delete zone memory from {CanonicalizeTypeDef} and {CanonicalGroup}.
zone_snapshot.Restore(&zone_);
// Identical group found. Map new types to the old types's canonical
// representatives.
for (uint32_t i = 0; i < size; i++) {
CanonicalTypeIndex existing_type_index =
CanonicalTypeIndex{canonical_index.index + i};
module->isorecursive_canonical_type_ids[start_index + i] =
existing_type_index;
}
return;
}
canonical_supertypes_.resize(first_new_canonical_index.index + size);
CheckMaxCanonicalIndex();
canonical_types_.reserve(first_new_canonical_index.index + size, &zone_);
for (uint32_t i = 0; i < size; i++) {
CanonicalType& canonical_type = group.types[i];
CanonicalTypeIndex canonical_id{first_new_canonical_index.index + i};
// {CanonicalGroup} allocates types in the Zone.
DCHECK(zone_.Contains(&canonical_type));
canonical_types_.set(canonical_id, &canonical_type);
canonical_supertypes_[canonical_id.index] = canonical_type.supertype;
module->isorecursive_canonical_type_ids[start_index + i] = canonical_id;
}
// Check that this canonical ID is not used yet.
DCHECK(std::none_of(
canonical_singleton_groups_.begin(), canonical_singleton_groups_.end(),
[=](auto& entry) { return entry.index == first_new_canonical_index; }));
DCHECK(std::none_of(
canonical_groups_.begin(), canonical_groups_.end(),
[=](auto& entry) { return entry.first == first_new_canonical_index; }));
canonical_groups_.emplace(group);
}
void TypeCanonicalizer::AddRecursiveSingletonGroup(WasmModule* module) {
DCHECK(!module->types.empty());
uint32_t type_index = static_cast<uint32_t>(module->types.size() - 1);
base::MutexGuard guard(&mutex_);
CanonicalTypeIndex new_canonical_index{
static_cast<uint32_t>(canonical_supertypes_.size())};
// Snapshot the zone before allocating the new type; the zone will be reset if
// we find an identical type.
ZoneSnapshot zone_snapshot = zone_.Snapshot();
CanonicalType type =
CanonicalizeTypeDef(module, ModuleTypeIndex{type_index},
ModuleTypeIndex{type_index}, new_canonical_index);
CanonicalSingletonGroup group{type, new_canonical_index};
if (CanonicalTypeIndex index = FindCanonicalGroup(group); index.valid()) {
zone_snapshot.Restore(&zone_);
module->isorecursive_canonical_type_ids[type_index] = index;
return;
}
// Check that the new canonical ID is not used yet.
DCHECK(std::none_of(
canonical_singleton_groups_.begin(), canonical_singleton_groups_.end(),
[=](auto& entry) { return entry.index == new_canonical_index; }));
DCHECK(std::none_of(
canonical_groups_.begin(), canonical_groups_.end(),
[=](auto& entry) { return entry.first == new_canonical_index; }));
// {group.type} is stack-allocated, whereas {canonical_singleton_groups_}
// creates a long-lived zone-allocated copy of it.
auto stored_group = canonical_singleton_groups_.emplace(group).first;
canonical_supertypes_.push_back(type.supertype);
CheckMaxCanonicalIndex();
canonical_types_.reserve(new_canonical_index.index + 1, &zone_);
canonical_types_.set(new_canonical_index, &stored_group->type);
module->isorecursive_canonical_type_ids[type_index] = new_canonical_index;
}
CanonicalTypeIndex TypeCanonicalizer::AddRecursiveGroup(
const FunctionSig* sig) {
// Types in the signature must be module-independent.
#if DEBUG
for (ValueType type : sig->all()) DCHECK(!type.has_index());
#endif
const bool kFinal = true;
// Because of the checks above, we can treat the type_def as canonical.
// TODO(366180605): It would be nice to not have to rely on a cast here.
// Is there a way to avoid it? In the meantime, these asserts provide at
// least partial assurances that the cast is safe:
static_assert(sizeof(CanonicalValueType) == sizeof(ValueType));
static_assert(
CanonicalValueType::Primitive(NumericKind::kI32).raw_bit_field() ==
ValueType::Primitive(kI32).raw_bit_field());
CanonicalType canonical{reinterpret_cast<const CanonicalSig*>(sig),
CanonicalTypeIndex{kNoSuperType}, kFinal, kNotShared};
base::MutexGuard guard(&mutex_);
// Fast path lookup before canonicalizing (== copying into the
// TypeCanonicalizer's zone) the function signature.
CanonicalTypeIndex new_canonical_index{
static_cast<uint32_t>(canonical_supertypes_.size())};
CanonicalTypeIndex index = FindCanonicalGroup(
CanonicalSingletonGroup{canonical, new_canonical_index});
if (index.valid()) return index;
// Copy into this class's zone to store this as a new canonical function type.
CanonicalSig::Builder builder(&zone_, sig->return_count(),
sig->parameter_count());
for (ValueType ret : sig->returns()) {
builder.AddReturn(CanonicalValueType{ret});
}
for (ValueType param : sig->parameters()) {
builder.AddParam(CanonicalValueType{param});
}
canonical.function_sig = builder.Get();
CanonicalSingletonGroup group{canonical, new_canonical_index};
// Copying the signature shouldn't make a difference: There is no match.
DCHECK(!FindCanonicalGroup(group).valid());
// Check that the new canonical ID is not used yet.
DCHECK(std::none_of(
canonical_singleton_groups_.begin(), canonical_singleton_groups_.end(),
[=](auto& entry) { return entry.index == new_canonical_index; }));
DCHECK(std::none_of(
canonical_groups_.begin(), canonical_groups_.end(),
[=](auto& entry) { return entry.first == new_canonical_index; }));
// {group.type} is stack-allocated, whereas {canonical_singleton_groups_}
// creates a long-lived zone-allocated copy of it.
const CanonicalSingletonGroup& stored_group =
*canonical_singleton_groups_.emplace(group).first;
canonical_supertypes_.push_back(CanonicalTypeIndex{kNoSuperType});
CheckMaxCanonicalIndex();
canonical_types_.reserve(new_canonical_index.index + 1, &zone_);
canonical_types_.set(new_canonical_index, &stored_group.type);
return new_canonical_index;
}
const CanonicalSig* TypeCanonicalizer::LookupFunctionSignature(
CanonicalTypeIndex index) const {
const CanonicalType* type = canonical_types_[index];
SBXCHECK_EQ(type->kind, CanonicalType::kFunction);
return type->function_sig;
}
const CanonicalStructType* TypeCanonicalizer::LookupStruct(
CanonicalTypeIndex index) const {
const CanonicalType* type = canonical_types_[index];
SBXCHECK_EQ(type->kind, CanonicalType::kStruct);
return type->struct_type;
}
const CanonicalArrayType* TypeCanonicalizer::LookupArray(
CanonicalTypeIndex index) const {
const CanonicalType* type = canonical_types_[index];
SBXCHECK_EQ(type->kind, CanonicalType::kArray);
return type->array_type;
}
void TypeCanonicalizer::AddPredefinedArrayTypes() {
static constexpr std::pair<CanonicalTypeIndex, CanonicalValueType>
kPredefinedArrayTypes[] = {{kPredefinedArrayI8Index, {kWasmI8}},
{kPredefinedArrayI16Index, {kWasmI16}}};
canonical_types_.reserve(kNumberOfPredefinedTypes, &zone_);
for (auto [index, element_type] : kPredefinedArrayTypes) {
DCHECK_GT(kNumberOfPredefinedTypes, index.index);
DCHECK_EQ(index.index, canonical_singleton_groups_.size());
static constexpr bool kMutable = true;
static constexpr bool kFinal = true;
static constexpr bool kShared = false; // TODO(14616): Fix this.
CanonicalArrayType* type =
zone_.New<CanonicalArrayType>(element_type, kMutable);
CanonicalSingletonGroup group{
.type = CanonicalType(type, CanonicalTypeIndex{kNoSuperType}, kFinal,
kShared),
.index = index};
const CanonicalSingletonGroup& stored_group =
*canonical_singleton_groups_.emplace(group).first;
canonical_types_.set(index, &stored_group.type);
canonical_supertypes_.emplace_back(CanonicalTypeIndex{kNoSuperType});
DCHECK_LE(canonical_supertypes_.size(), kMaxCanonicalTypes);
}
}
bool TypeCanonicalizer::IsCanonicalSubtype(CanonicalTypeIndex sub_index,
CanonicalTypeIndex super_index) {
// Fast path without synchronization:
if (sub_index == super_index) return true;
// Multiple threads could try to register and access recursive groups
// concurrently.
// TODO(manoskouk): Investigate if we can improve this synchronization.
base::MutexGuard mutex_guard(&mutex_);
return IsCanonicalSubtype_Locked(sub_index, super_index);
}
bool TypeCanonicalizer::IsCanonicalSubtype_Locked(
CanonicalTypeIndex sub_index, CanonicalTypeIndex super_index) const {
while (sub_index.valid()) {
if (sub_index == super_index) return true;
// TODO(jkummerow): Investigate if replacing this with
// `sub_index = canonical_types_[sub_index].supertype;`
// has acceptable performance, which would allow us to save the memory
// cost of storing {canonical_supertypes_}.
sub_index = canonical_supertypes_[sub_index.index];
}
return false;
}
bool TypeCanonicalizer::IsCanonicalSubtype(ModuleTypeIndex sub_index,
ModuleTypeIndex super_index,
const WasmModule* sub_module,
const WasmModule* super_module) {
CanonicalTypeIndex canonical_super =
super_module->canonical_type_id(super_index);
CanonicalTypeIndex canonical_sub = sub_module->canonical_type_id(sub_index);
return IsCanonicalSubtype(canonical_sub, canonical_super);
}
bool TypeCanonicalizer::IsHeapSubtype(CanonicalTypeIndex sub,
CanonicalTypeIndex super) const {
DCHECK_NE(sub, super);
base::MutexGuard mutex_guard(&mutex_);
return IsCanonicalSubtype_Locked(sub, super);
}
void TypeCanonicalizer::EmptyStorageForTesting() {
// Any remaining native modules might reference the types we're about to
// clear.
CHECK_EQ(GetWasmEngine()->NativeModuleCount(), 0);
base::MutexGuard mutex_guard(&mutex_);
canonical_types_.ClearForTesting();
canonical_supertypes_.clear();
canonical_groups_.clear();
canonical_singleton_groups_.clear();
zone_.Reset();
AddPredefinedArrayTypes();
}
TypeCanonicalizer::CanonicalType TypeCanonicalizer::CanonicalizeTypeDef(
const WasmModule* module, ModuleTypeIndex module_type_idx,
ModuleTypeIndex recgroup_start,
CanonicalTypeIndex canonical_recgroup_start) {
mutex_.AssertHeld(); // The caller must hold the mutex.
auto CanonicalizeTypeIndex = [=](ModuleTypeIndex type_index) {
if (!type_index.valid()) return CanonicalTypeIndex::Invalid();
DCHECK(type_index.valid());
if (type_index < recgroup_start) {
// This references a type from an earlier recgroup; use the
// already-canonicalized type index.
return module->canonical_type_id(type_index);
}
// For types within the same recgroup, generate indexes assuming that this
// is a new canonical recgroup. To prevent truncation in the
// CanonicalValueType's bit field, we must check the range here.
uint32_t new_index = canonical_recgroup_start.index +
(type_index.index - recgroup_start.index);
if (V8_UNLIKELY(new_index >= kMaxCanonicalTypes)) {
V8::FatalProcessOutOfMemory(nullptr, "too many canonicalized types");
}
return CanonicalTypeIndex{new_index};
};
auto CanonicalizeValueType = [=](ValueType type) {
if (!type.has_index()) return CanonicalValueType{type};
static_assert(kMaxCanonicalTypes <=
(1 << CanonicalValueType::kNumIndexBits));
return type.Canonicalize(CanonicalizeTypeIndex(type.ref_index()));
};
TypeDefinition type = module->type(module_type_idx);
CanonicalTypeIndex supertype = CanonicalizeTypeIndex(type.supertype);
switch (type.kind) {
case TypeDefinition::kFunction: {
const FunctionSig* original_sig = type.function_sig;
CanonicalSig::Builder builder(&zone_, original_sig->return_count(),
original_sig->parameter_count());
for (ValueType ret : original_sig->returns()) {
builder.AddReturn(CanonicalizeValueType(ret));
}
for (ValueType param : original_sig->parameters()) {
builder.AddParam(CanonicalizeValueType(param));
}
return CanonicalType(builder.Get(), supertype, type.is_final,
type.is_shared);
}
case TypeDefinition::kStruct: {
const StructType* original_type = type.struct_type;
CanonicalStructType::Builder builder(&zone_, original_type->field_count(),
original_type->is_descriptor());
for (uint32_t i = 0; i < original_type->field_count(); i++) {
builder.AddField(CanonicalizeValueType(original_type->field(i)),
original_type->mutability(i),
original_type->field_offset(i));
}
builder.set_total_fields_size(original_type->total_fields_size());
return CanonicalType(
builder.Build(CanonicalStructType::Builder::kUseProvidedOffsets),
supertype, CanonicalizeTypeIndex(type.descriptor),
CanonicalizeTypeIndex(type.describes), type.is_final, type.is_shared);
}
case TypeDefinition::kArray: {
CanonicalValueType element_type =
CanonicalizeValueType(type.array_type->element_type());
CanonicalArrayType* array_type = zone_.New<CanonicalArrayType>(
element_type, type.array_type->mutability());
return CanonicalType(array_type, supertype, type.is_final,
type.is_shared);
}
case TypeDefinition::kCont: {
CanonicalTypeIndex canonical_index =
CanonicalizeTypeIndex(type.cont_type->contfun_typeindex());
CanonicalContType* canonical_cont =
zone_.New<CanonicalContType>(canonical_index);
return CanonicalType(canonical_cont, supertype, type.is_final,
type.is_shared);
}
}
}
// Returns the index of the canonical representative of the first type in this
// group if it exists, and `CanonicalTypeIndex::Invalid()` otherwise.
CanonicalTypeIndex TypeCanonicalizer::FindCanonicalGroup(
const CanonicalGroup& group) const {
// Groups of size 0 do not make sense here; groups of size 1 should use
// {CanonicalSingletonGroup} (see below).
DCHECK_LT(1, group.types.size());
auto it = canonical_groups_.find(group);
return it == canonical_groups_.end() ? CanonicalTypeIndex::Invalid()
: it->first;
}
// Returns the canonical index of the given group if it already exists.
CanonicalTypeIndex TypeCanonicalizer::FindCanonicalGroup(
const CanonicalSingletonGroup& group) const {
auto it = canonical_singleton_groups_.find(group);
static_assert(kMaxCanonicalTypes <= kMaxInt);
return it == canonical_singleton_groups_.end() ? CanonicalTypeIndex::Invalid()
: it->index;
}
size_t TypeCanonicalizer::EstimateCurrentMemoryConsumption() const {
UPDATE_WHEN_CLASS_CHANGES(TypeCanonicalizer, 8048);
// The storage of the canonical group's types is accounted for via the
// allocator below (which tracks the zone memory).
base::MutexGuard mutex_guard(&mutex_);
size_t result = ContentSize(canonical_supertypes_);
result += ContentSize(canonical_groups_);
result += ContentSize(canonical_singleton_groups_);
// Note: the allocator also tracks zone allocations of `canonical_types_`.
result += allocator_.GetCurrentMemoryUsage();
if (v8_flags.trace_wasm_offheap_memory) {
PrintF("TypeCanonicalizer: %zu\n", result);
}
return result;
}
size_t TypeCanonicalizer::GetCurrentNumberOfTypes() const {
base::MutexGuard mutex_guard(&mutex_);
return canonical_supertypes_.size();
}
// static
void TypeCanonicalizer::PrepareForCanonicalTypeId(Isolate* isolate,
CanonicalTypeIndex id) {
if (!id.valid()) return;
Heap* heap = isolate->heap();
// {2 * (id + 1)} needs to fit in an int.
CHECK_LE(id.index, kMaxInt / 2 - 1);
// Canonical types and wrappers are zero-indexed.
const int length = id.index + 1;
// The fast path is non-handlified.
Tagged<WeakFixedArray> old_rtts_raw = heap->wasm_canonical_rtts();
Tagged<WeakFixedArray> old_wrappers_raw = heap->js_to_wasm_wrappers();
// Fast path: Lengths are sufficient.
int old_length = old_rtts_raw->length();
DCHECK_EQ(old_length, old_wrappers_raw->length());
if (old_length >= length) return;
// Allocate bigger WeakFixedArrays for rtts and wrappers. Grow them
// exponentially.
const int new_length = std::max(old_length * 3 / 2, length);
CHECK_LT(old_length, new_length);
// Allocation can invalidate previous unhandled pointers.
DirectHandle<WeakFixedArray> old_rtts{old_rtts_raw, isolate};
DirectHandle<WeakFixedArray> old_wrappers{old_wrappers_raw, isolate};
old_rtts_raw = old_wrappers_raw = {};
// We allocate the WeakFixedArray filled with undefined values, as we cannot
// pass the cleared value in a handle (see https://crbug.com/364591622). We
// overwrite the new entries via {MemsetTagged} afterwards.
DirectHandle<WeakFixedArray> new_rtts =
WeakFixedArray::New(isolate, new_length, AllocationType::kOld);
WeakFixedArray::CopyElements(isolate, *new_rtts, 0, *old_rtts, 0, old_length);
MemsetTagged(new_rtts->RawFieldOfFirstElement() + old_length,
ClearedValue(isolate), new_length - old_length);
DirectHandle<WeakFixedArray> new_wrappers =
WeakFixedArray::New(isolate, new_length, AllocationType::kOld);
WeakFixedArray::CopyElements(isolate, *new_wrappers, 0, *old_wrappers, 0,
old_length);
MemsetTagged(new_wrappers->RawFieldOfFirstElement() + old_length,
ClearedValue(isolate), new_length - old_length);
heap->SetWasmCanonicalRttsAndJSToWasmWrappers(*new_rtts, *new_wrappers);
}
// static
void TypeCanonicalizer::ClearWasmCanonicalTypesForTesting(Isolate* isolate) {
ReadOnlyRoots roots(isolate);
isolate->heap()->SetWasmCanonicalRttsAndJSToWasmWrappers(
roots.empty_weak_fixed_array(), roots.empty_weak_fixed_array());
}
bool TypeCanonicalizer::IsFunctionSignature(CanonicalTypeIndex index) const {
return canonical_types_[index]->kind == CanonicalType::kFunction;
}
bool TypeCanonicalizer::IsStruct(CanonicalTypeIndex index) const {
return canonical_types_[index]->kind == CanonicalType::kStruct;
}
bool TypeCanonicalizer::IsArray(CanonicalTypeIndex index) const {
return canonical_types_[index]->kind == CanonicalType::kArray;
}
bool TypeCanonicalizer::IsShared(CanonicalTypeIndex index) const {
return canonical_types_[index]->is_shared;
}
CanonicalTypeIndex TypeCanonicalizer::FindIndex_Slow(
const CanonicalSig* sig) const {
// TODO(397489547): Make this faster. The plan is to allocate an extra
// slot in the Zone immediately preceding each CanonicalSig, so we can
// get from the sig's address to that slot's address via pointer arithmetic.
// For now, just search through all known signatures, which is acceptable
// as long as only the type-reflection proposal needs this.
// TODO(42210967): Improve this before shipping Type Reflection.
return canonical_types_.FindIndex_Slow(sig);
}
#ifdef DEBUG
bool TypeCanonicalizer::Contains(const CanonicalSig* sig) const {
base::MutexGuard mutex_guard(&mutex_);
return zone_.Contains(sig);
}
#endif
} // namespace v8::internal::wasm