[wasm-gc] Implement 'let' opcode.
Changes: - Implement the 'let' opcode, as per https://github.com/WebAssembly/function-references/blob/master/proposals/function-references/Overview.md#local-bindings - Use a WasmDecoder in place of a plain decoder in OpcodeLength and AnalyzeLoopAssignment. - Change ControlBase to accept an additional 'locals_count' parameter. - Implement required test infrastructure and write some simple tests. Bug: v8:7748 Change-Id: I39d60d1f0c26016c8f89c009dc5f4119b0c73c87 Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/2204107 Commit-Queue: Manos Koukoutos <manoskouk@chromium.org> Reviewed-by: Clemens Backes <clemensb@chromium.org> Reviewed-by: Jakob Kummerow <jkummerow@chromium.org> Cr-Commit-Position: refs/heads/master@{#67937}
This commit is contained in:
parent
cf7731e261
commit
491a94b0ff
@ -1617,6 +1617,16 @@ class LiftoffCompiler {
|
||||
LocalSet(imm.index, true);
|
||||
}
|
||||
|
||||
void AllocateLocals(FullDecoder* decoder, Vector<Value> local_values) {
|
||||
// TODO(7748): Introduce typed functions bailout reason
|
||||
unsupported(decoder, kGC, "let");
|
||||
}
|
||||
|
||||
void DeallocateLocals(FullDecoder* decoder, uint32_t count) {
|
||||
// TODO(7748): Introduce typed functions bailout reason
|
||||
unsupported(decoder, kGC, "let");
|
||||
}
|
||||
|
||||
Register GetGlobalBaseAndOffset(const WasmGlobal* global,
|
||||
LiftoffRegList* pinned, uint32_t* offset) {
|
||||
Register addr = pinned->set(__ GetUnusedRegister(kGpReg, {})).gp();
|
||||
|
@ -729,7 +729,7 @@ struct Merge {
|
||||
// Reachability::kReachable.
|
||||
bool reached;
|
||||
|
||||
Merge(bool reached = false) : reached(reached) {}
|
||||
explicit Merge(bool reached = false) : reached(reached) {}
|
||||
|
||||
Value& operator[](uint32_t i) {
|
||||
DCHECK_GT(arity, i);
|
||||
@ -742,6 +742,7 @@ enum ControlKind : uint8_t {
|
||||
kControlIfElse,
|
||||
kControlBlock,
|
||||
kControlLoop,
|
||||
kControlLet,
|
||||
kControlTry,
|
||||
kControlTryCatch
|
||||
};
|
||||
@ -759,6 +760,7 @@ enum Reachability : uint8_t {
|
||||
template <typename Value>
|
||||
struct ControlBase {
|
||||
ControlKind kind = kControlBlock;
|
||||
uint32_t locals_count = 0;
|
||||
uint32_t stack_depth = 0; // stack height at the beginning of the construct.
|
||||
const uint8_t* pc = nullptr;
|
||||
Reachability reachability = kReachable;
|
||||
@ -769,13 +771,16 @@ struct ControlBase {
|
||||
|
||||
MOVE_ONLY_NO_DEFAULT_CONSTRUCTOR(ControlBase);
|
||||
|
||||
ControlBase(ControlKind kind, uint32_t stack_depth, const uint8_t* pc,
|
||||
Reachability reachability)
|
||||
ControlBase(ControlKind kind, uint32_t locals_count, uint32_t stack_depth,
|
||||
const uint8_t* pc, Reachability reachability)
|
||||
: kind(kind),
|
||||
locals_count(locals_count),
|
||||
stack_depth(stack_depth),
|
||||
pc(pc),
|
||||
reachability(reachability),
|
||||
start_merge(reachability == kReachable) {}
|
||||
start_merge(reachability == kReachable) {
|
||||
DCHECK(kind == kControlLet || locals_count == 0);
|
||||
}
|
||||
|
||||
// Check whether the current block is reachable.
|
||||
bool reachable() const { return reachability == kReachable; }
|
||||
@ -795,6 +800,7 @@ struct ControlBase {
|
||||
bool is_onearmed_if() const { return kind == kControlIf; }
|
||||
bool is_if_else() const { return kind == kControlIfElse; }
|
||||
bool is_block() const { return kind == kControlBlock; }
|
||||
bool is_let() const { return kind == kControlLet; }
|
||||
bool is_loop() const { return kind == kControlLoop; }
|
||||
bool is_incomplete_try() const { return kind == kControlTry; }
|
||||
bool is_try_catch() const { return kind == kControlTryCatch; }
|
||||
@ -841,6 +847,8 @@ struct ControlBase {
|
||||
F(LocalSet, const Value& value, const LocalIndexImmediate<validate>& imm) \
|
||||
F(LocalTee, const Value& value, Value* result, \
|
||||
const LocalIndexImmediate<validate>& imm) \
|
||||
F(AllocateLocals, Vector<Value> local_values) \
|
||||
F(DeallocateLocals, uint32_t count) \
|
||||
F(GlobalGet, Value* result, const GlobalIndexImmediate<validate>& imm) \
|
||||
F(GlobalSet, const Value& value, const GlobalIndexImmediate<validate>& imm) \
|
||||
F(TableGet, const Value& index, Value* result, \
|
||||
@ -1010,6 +1018,7 @@ class WasmDecoder : public Decoder {
|
||||
}
|
||||
*total_length += length;
|
||||
if (insert_position.has_value()) {
|
||||
// Move the insertion iterator to the end of the newly inserted locals.
|
||||
insert_iterator =
|
||||
local_types_->insert(insert_iterator, count, type) + count;
|
||||
}
|
||||
@ -1018,7 +1027,7 @@ class WasmDecoder : public Decoder {
|
||||
return true;
|
||||
}
|
||||
|
||||
static BitVector* AnalyzeLoopAssignment(Decoder* decoder, const byte* pc,
|
||||
static BitVector* AnalyzeLoopAssignment(WasmDecoder* decoder, const byte* pc,
|
||||
uint32_t locals_count, Zone* zone) {
|
||||
if (pc >= decoder->end()) return nullptr;
|
||||
if (*pc != kExprLoop) return nullptr;
|
||||
@ -1385,7 +1394,7 @@ class WasmDecoder : public Decoder {
|
||||
return true;
|
||||
}
|
||||
|
||||
static uint32_t OpcodeLength(Decoder* decoder, const byte* pc) {
|
||||
static uint32_t OpcodeLength(WasmDecoder* decoder, const byte* pc) {
|
||||
WasmOpcode opcode = static_cast<WasmOpcode>(*pc);
|
||||
switch (opcode) {
|
||||
#define DECLARE_OPCODE_CASE(name, opcode, sig) case kExpr##name:
|
||||
@ -1430,6 +1439,15 @@ class WasmDecoder : public Decoder {
|
||||
return 1 + imm.length;
|
||||
}
|
||||
|
||||
case kExprLet: {
|
||||
BlockTypeImmediate<validate> imm(WasmFeatures::All(), decoder, pc);
|
||||
uint32_t locals_length;
|
||||
bool locals_result =
|
||||
decoder->DecodeLocals(decoder->pc() + 1 + imm.length,
|
||||
&locals_length, base::Optional<uint32_t>());
|
||||
return 1 + imm.length + (locals_result ? locals_length : 0);
|
||||
}
|
||||
|
||||
case kExprThrow: {
|
||||
ExceptionIndexImmediate<validate> imm(decoder, pc);
|
||||
return 1 + imm.length;
|
||||
@ -1719,6 +1737,9 @@ class WasmDecoder : public Decoder {
|
||||
case kExprReturnCallIndirect:
|
||||
case kExprUnreachable:
|
||||
return {0, 0};
|
||||
case kExprLet:
|
||||
// TODO(7748): Implement
|
||||
return {0, 0};
|
||||
case kNumericPrefix:
|
||||
case kAtomicPrefix:
|
||||
case kSimdPrefix: {
|
||||
@ -2117,6 +2138,33 @@ class WasmFullDecoder : public WasmDecoder<validate> {
|
||||
}
|
||||
break;
|
||||
}
|
||||
case kExprLet: {
|
||||
CHECK_PROTOTYPE_OPCODE(typed_funcref);
|
||||
BlockTypeImmediate<validate> imm(this->enabled_, this, this->pc_);
|
||||
if (!this->Validate(imm)) break;
|
||||
uint32_t current_local_count =
|
||||
static_cast<uint32_t>(local_type_vec_.size());
|
||||
// Temporarily add the let-defined values
|
||||
// to the beginning of the function locals.
|
||||
uint32_t locals_length;
|
||||
if (!this->DecodeLocals(this->pc() + 1 + imm.length, &locals_length,
|
||||
0)) {
|
||||
break;
|
||||
}
|
||||
len = 1 + imm.length + locals_length;
|
||||
uint32_t locals_count = static_cast<uint32_t>(local_type_vec_.size() -
|
||||
current_local_count);
|
||||
ArgVector let_local_values =
|
||||
PopArgs(VectorOf(local_type_vec_.data(), locals_count));
|
||||
ArgVector args = PopArgs(imm.sig);
|
||||
Control* let_block = PushControl(kControlLet, locals_count);
|
||||
SetBlockType(let_block, imm, args.begin());
|
||||
CALL_INTERFACE_IF_REACHABLE(Block, let_block);
|
||||
PushMergeValues(let_block, &let_block->start_merge);
|
||||
CALL_INTERFACE_IF_REACHABLE(AllocateLocals,
|
||||
VectorOf(let_local_values));
|
||||
break;
|
||||
}
|
||||
case kExprLoop: {
|
||||
BlockTypeImmediate<validate> imm(this->enabled_, this, this->pc_);
|
||||
if (!this->Validate(imm)) break;
|
||||
@ -2182,6 +2230,12 @@ class WasmFullDecoder : public WasmDecoder<validate> {
|
||||
}
|
||||
if (!TypeCheckOneArmedIf(c)) break;
|
||||
}
|
||||
if (c->is_let()) {
|
||||
this->local_types_->erase(
|
||||
this->local_types_->begin(),
|
||||
this->local_types_->begin() + c->locals_count);
|
||||
CALL_INTERFACE_IF_REACHABLE(DeallocateLocals, c->locals_count);
|
||||
}
|
||||
if (!TypeCheckFallThru()) break;
|
||||
|
||||
if (control_.size() == 1) {
|
||||
@ -2716,6 +2770,7 @@ class WasmFullDecoder : public WasmDecoder<validate> {
|
||||
break;
|
||||
case kControlIfElse:
|
||||
case kControlTryCatch:
|
||||
case kControlLet: // TODO(7748): Implement
|
||||
break;
|
||||
}
|
||||
if (c.start_merge.arity) TRACE_PART("%u-", c.start_merge.arity);
|
||||
@ -2818,15 +2873,24 @@ class WasmFullDecoder : public WasmDecoder<validate> {
|
||||
return args;
|
||||
}
|
||||
|
||||
V8_INLINE ArgVector PopArgs(Vector<ValueType> arg_types) {
|
||||
ArgVector args(arg_types.size());
|
||||
for (int i = static_cast<int>(arg_types.size()) - 1; i >= 0; i--) {
|
||||
args[i] = Pop(i, arg_types[i]);
|
||||
}
|
||||
return args;
|
||||
}
|
||||
|
||||
ValueType GetReturnType(const FunctionSig* sig) {
|
||||
DCHECK_GE(1, sig->return_count());
|
||||
return sig->return_count() == 0 ? kWasmStmt : sig->GetReturn();
|
||||
}
|
||||
|
||||
Control* PushControl(ControlKind kind) {
|
||||
Control* PushControl(ControlKind kind, uint32_t locals_count = 0) {
|
||||
Reachability reachability =
|
||||
control_.empty() ? kReachable : control_.back().innerReachability();
|
||||
control_.emplace_back(kind, stack_size(), this->pc_, reachability);
|
||||
control_.emplace_back(kind, locals_count, stack_size(), this->pc_,
|
||||
reachability);
|
||||
return &control_.back();
|
||||
}
|
||||
|
||||
|
@ -64,7 +64,9 @@ DecodeResult VerifyWasmCode(AccountingAllocator* allocator,
|
||||
}
|
||||
|
||||
unsigned OpcodeLength(const byte* pc, const byte* end) {
|
||||
Decoder decoder(pc, end);
|
||||
WasmFeatures no_features = WasmFeatures::None();
|
||||
WasmDecoder<Decoder::kNoValidate> decoder(nullptr, no_features, &no_features,
|
||||
nullptr, pc, end, 0);
|
||||
return WasmDecoder<Decoder::kNoValidate>::OpcodeLength(&decoder, pc);
|
||||
}
|
||||
|
||||
@ -293,7 +295,9 @@ bool PrintRawWasmCode(AccountingAllocator* allocator, const FunctionBody& body,
|
||||
|
||||
BitVector* AnalyzeLoopAssignmentForTesting(Zone* zone, size_t num_locals,
|
||||
const byte* start, const byte* end) {
|
||||
Decoder decoder(start, end);
|
||||
WasmFeatures no_features = WasmFeatures::None();
|
||||
WasmDecoder<Decoder::kValidate> decoder(nullptr, no_features, &no_features,
|
||||
nullptr, start, end, 0);
|
||||
return WasmDecoder<Decoder::kValidate>::AnalyzeLoopAssignment(
|
||||
&decoder, start, static_cast<uint32_t>(num_locals), zone);
|
||||
}
|
||||
|
@ -296,6 +296,19 @@ class WasmGraphBuildingInterface {
|
||||
ssa_env_->locals[imm.index] = value.node;
|
||||
}
|
||||
|
||||
void AllocateLocals(FullDecoder* decoder, Vector<Value> local_values) {
|
||||
ZoneVector<TFNode*>* locals = &ssa_env_->locals;
|
||||
locals->insert(locals->begin(), local_values.size(), nullptr);
|
||||
for (uint32_t i = 0; i < local_values.size(); i++) {
|
||||
(*locals)[i] = local_values[i].node;
|
||||
}
|
||||
}
|
||||
|
||||
void DeallocateLocals(FullDecoder* decoder, uint32_t count) {
|
||||
ZoneVector<TFNode*>* locals = &ssa_env_->locals;
|
||||
locals->erase(locals->begin(), locals->begin() + count);
|
||||
}
|
||||
|
||||
void GlobalGet(FullDecoder* decoder, Value* result,
|
||||
const GlobalIndexImmediate<validate>& imm) {
|
||||
result->node = BUILD(GlobalGet, imm.index);
|
||||
|
@ -367,6 +367,7 @@ const char* WasmOpcodes::OpcodeName(WasmOpcode opcode) {
|
||||
CASE_OP(RefCast, "ref.cast")
|
||||
CASE_OP(BrOnCast, "br_on_cast")
|
||||
CASE_OP(RefEq, "ref.eq")
|
||||
CASE_OP(Let, "let")
|
||||
|
||||
|
||||
case kNumericPrefix:
|
||||
|
@ -38,7 +38,8 @@ bool IsJSCompatibleSignature(const FunctionSig* sig, const WasmFeatures&);
|
||||
V(BrIf, 0x0d, _) \
|
||||
V(BrTable, 0x0e, _) \
|
||||
V(Return, 0x0f, _) \
|
||||
V(BrOnNull, 0xd4, _) /* gc prototype */
|
||||
V(Let, 0x17, _ /* gc prototype */) \
|
||||
V(BrOnNull, 0xd4, _ /* gc prototype */)
|
||||
|
||||
// Constants, locals, globals, and calls.
|
||||
#define FOREACH_MISC_OPCODE(V) \
|
||||
|
@ -158,6 +158,8 @@ WASM_EXEC_TEST(BasicStruct) {
|
||||
n->EmitCode(n_code, sizeof(n_code));
|
||||
// Result: 0b1001
|
||||
|
||||
/************************* End of test definitions *************************/
|
||||
|
||||
ZoneBuffer buffer(&zone);
|
||||
builder->WriteTo(&buffer);
|
||||
|
||||
@ -165,10 +167,11 @@ WASM_EXEC_TEST(BasicStruct) {
|
||||
HandleScope scope(isolate);
|
||||
testing::SetupIsolateForWasmModule(isolate);
|
||||
ErrorThrower thrower(isolate, "Test");
|
||||
Handle<WasmInstanceObject> instance =
|
||||
MaybeHandle<WasmInstanceObject> maybe_instance =
|
||||
testing::CompileAndInstantiateForTesting(
|
||||
isolate, &thrower, ModuleWireBytes(buffer.begin(), buffer.end()))
|
||||
.ToHandleChecked();
|
||||
isolate, &thrower, ModuleWireBytes(buffer.begin(), buffer.end()));
|
||||
if (thrower.error()) FATAL("%s", thrower.error_msg());
|
||||
Handle<WasmInstanceObject> instance = maybe_instance.ToHandleChecked();
|
||||
|
||||
CHECK_EQ(42, testing::CallWasmFunctionForTesting(isolate, instance, &thrower,
|
||||
"f", 0, nullptr));
|
||||
@ -202,6 +205,106 @@ WASM_EXEC_TEST(BasicStruct) {
|
||||
isolate, instance, &thrower, "n", 0, nullptr));
|
||||
}
|
||||
|
||||
WASM_EXEC_TEST(LetInstruction) {
|
||||
// TODO(7748): Implement support in other tiers.
|
||||
if (execution_tier == ExecutionTier::kLiftoff) return;
|
||||
if (execution_tier == ExecutionTier::kInterpreter) return;
|
||||
TestSignatures sigs;
|
||||
EXPERIMENTAL_FLAG_SCOPE(gc);
|
||||
EXPERIMENTAL_FLAG_SCOPE(typed_funcref);
|
||||
EXPERIMENTAL_FLAG_SCOPE(anyref);
|
||||
v8::internal::AccountingAllocator allocator;
|
||||
Zone zone(&allocator, ZONE_NAME);
|
||||
|
||||
WasmModuleBuilder* builder = new (&zone) WasmModuleBuilder(&zone);
|
||||
StructType::Builder type_builder(&zone, 2);
|
||||
type_builder.AddField(kWasmI32);
|
||||
type_builder.AddField(kWasmI32);
|
||||
int32_t type_index = builder->AddStructType(type_builder.Build());
|
||||
ValueType kRefTypes[] = {ValueType(ValueType::kRef, type_index)};
|
||||
FunctionSig sig_q_v(1, 0, kRefTypes);
|
||||
|
||||
WasmFunctionBuilder* let_test_1 = builder->AddFunction(sigs.i_v());
|
||||
let_test_1->builder()->AddExport(CStrVector("let_test_1"), let_test_1);
|
||||
uint32_t let_local_index = 0;
|
||||
uint32_t let_field_index = 0;
|
||||
byte let_code[] = {
|
||||
WASM_LET_1_I(WASM_REF_TYPE(type_index),
|
||||
WASM_STRUCT_NEW(type_index, WASM_I32V(42), WASM_I32V(52)),
|
||||
WASM_STRUCT_GET(type_index, let_field_index,
|
||||
WASM_GET_LOCAL(let_local_index))),
|
||||
kExprEnd};
|
||||
let_test_1->EmitCode(let_code, sizeof(let_code));
|
||||
|
||||
WasmFunctionBuilder* let_test_2 = builder->AddFunction(sigs.i_v());
|
||||
let_test_2->builder()->AddExport(CStrVector("let_test_2"), let_test_2);
|
||||
uint32_t let_2_field_index = 0;
|
||||
byte let_code_2[] = {
|
||||
WASM_LET_2_I(kLocalI32, WASM_I32_ADD(WASM_I32V(42), WASM_I32V(-32)),
|
||||
WASM_REF_TYPE(type_index),
|
||||
WASM_STRUCT_NEW(type_index, WASM_I32V(42), WASM_I32V(52)),
|
||||
WASM_I32_MUL(WASM_STRUCT_GET(type_index, let_2_field_index,
|
||||
WASM_GET_LOCAL(1)),
|
||||
WASM_GET_LOCAL(0))),
|
||||
kExprEnd};
|
||||
let_test_2->EmitCode(let_code_2, sizeof(let_code_2));
|
||||
|
||||
WasmFunctionBuilder* let_test_locals = builder->AddFunction(sigs.i_i());
|
||||
let_test_locals->builder()->AddExport(CStrVector("let_test_locals"),
|
||||
let_test_locals);
|
||||
let_test_locals->AddLocal(kWasmI32);
|
||||
byte let_code_locals[] = {
|
||||
WASM_SET_LOCAL(1, WASM_I32V(100)),
|
||||
WASM_LET_2_I(
|
||||
kLocalI32, WASM_I32V(1), kLocalI32, WASM_I32V(10),
|
||||
WASM_I32_SUB(WASM_I32_ADD(WASM_GET_LOCAL(0), // 1st let-local
|
||||
WASM_GET_LOCAL(2)), // Parameter
|
||||
WASM_I32_ADD(WASM_GET_LOCAL(1), // 2nd let-local
|
||||
WASM_GET_LOCAL(3)))), // Function local
|
||||
kExprEnd};
|
||||
// Result: (1 + 1000) - (10 + 100) = 891
|
||||
let_test_locals->EmitCode(let_code_locals, sizeof(let_code_locals));
|
||||
|
||||
WasmFunctionBuilder* let_test_erase = builder->AddFunction(sigs.i_v());
|
||||
let_test_erase->builder()->AddExport(CStrVector("let_test_erase"),
|
||||
let_test_erase);
|
||||
uint32_t let_erase_local_index = let_test_erase->AddLocal(kWasmI32);
|
||||
byte let_code_erase[] = {WASM_SET_LOCAL(let_erase_local_index, WASM_I32V(0)),
|
||||
WASM_LET_1_V(kLocalI32, WASM_I32V(1), WASM_NOP),
|
||||
WASM_GET_LOCAL(let_erase_local_index), kExprEnd};
|
||||
// The result should be 0 and not 1, as local_get(0) refers to the original
|
||||
// local.
|
||||
let_test_erase->EmitCode(let_code_erase, sizeof(let_code_erase));
|
||||
|
||||
/************************* End of test definitions *************************/
|
||||
|
||||
ZoneBuffer buffer(&zone);
|
||||
builder->WriteTo(&buffer);
|
||||
|
||||
Isolate* isolate = CcTest::InitIsolateOnce();
|
||||
HandleScope scope(isolate);
|
||||
testing::SetupIsolateForWasmModule(isolate);
|
||||
ErrorThrower thrower(isolate, "Test");
|
||||
MaybeHandle<WasmInstanceObject> maybe_instance =
|
||||
testing::CompileAndInstantiateForTesting(
|
||||
isolate, &thrower, ModuleWireBytes(buffer.begin(), buffer.end()));
|
||||
if (thrower.error()) FATAL("%s", thrower.error_msg());
|
||||
Handle<WasmInstanceObject> instance = maybe_instance.ToHandleChecked();
|
||||
|
||||
CHECK_EQ(42, testing::CallWasmFunctionForTesting(isolate, instance, &thrower,
|
||||
"let_test_1", 0, nullptr));
|
||||
|
||||
CHECK_EQ(420, testing::CallWasmFunctionForTesting(isolate, instance, &thrower,
|
||||
"let_test_2", 0, nullptr));
|
||||
|
||||
Handle<Object> let_local_args[] = {handle(Smi::FromInt(1000), isolate)};
|
||||
CHECK_EQ(891, testing::CallWasmFunctionForTesting(isolate, instance, &thrower,
|
||||
"let_test_locals", 1,
|
||||
let_local_args));
|
||||
CHECK_EQ(0, testing::CallWasmFunctionForTesting(
|
||||
isolate, instance, &thrower, "let_test_erase", 0, nullptr));
|
||||
}
|
||||
|
||||
WASM_EXEC_TEST(BasicArray) {
|
||||
// TODO(7748): Implement support in other tiers.
|
||||
if (execution_tier == ExecutionTier::kLiftoff) return;
|
||||
|
@ -458,6 +458,23 @@ inline WasmOpcode LoadStoreOpcodeOf(MachineType type, bool store) {
|
||||
#define WASM_RETURN_CALL_INDIRECT(sig_index, ...) \
|
||||
__VA_ARGS__, kExprReturnCallIndirect, static_cast<byte>(sig_index), TABLE_ZERO
|
||||
|
||||
#define WASM_REF_TYPE(typeidx) kLocalRef, U32V_1(typeidx)
|
||||
|
||||
// shift locals by 1; let (locals[0]: local_type) = value in ...
|
||||
#define WASM_LET_1_V(local_type, value, ...) \
|
||||
value, kExprLet, kLocalVoid, U32V_1(1), U32V_1(1), local_type, __VA_ARGS__, \
|
||||
kExprEnd
|
||||
#define WASM_LET_1_I(local_type, value, ...) \
|
||||
value, kExprLet, kLocalI32, U32V_1(1), U32V_1(1), local_type, __VA_ARGS__, \
|
||||
kExprEnd
|
||||
// shift locals by 2;
|
||||
// let (locals[0]: local_type_1) = value_1,
|
||||
// (locals[1]: local_type_2) = value_2
|
||||
// in ...
|
||||
#define WASM_LET_2_I(local_type_1, value_1, local_type_2, value_2, ...) \
|
||||
value_1, value_2, kExprLet, kLocalI32, U32V_1(2), U32V_1(1), local_type_1, \
|
||||
U32V_1(1), local_type_2, __VA_ARGS__, kExprEnd
|
||||
|
||||
#define WASM_NOT(x) x, kExprI32Eqz
|
||||
#define WASM_SEQ(...) __VA_ARGS__
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user