summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--scripts/fuzz_opt.py5
-rw-r--r--src/ir/ExpressionAnalyzer.cpp6
-rw-r--r--src/ir/ExpressionManipulator.cpp5
-rw-r--r--src/ir/effects.h10
-rw-r--r--src/passes/Asyncify.cpp6
-rw-r--r--src/passes/DeadArgumentElimination.cpp54
-rw-r--r--src/passes/DeadCodeElimination.cpp10
-rw-r--r--src/passes/Directize.cpp3
-rw-r--r--src/passes/I64ToI32Lowering.cpp14
-rw-r--r--src/passes/Inlining.cpp87
-rw-r--r--src/tools/fuzzing.h23
-rw-r--r--src/wasm-builder.h2
-rw-r--r--src/wasm-interpreter.h24
-rw-r--r--src/wasm/wasm-validator.cpp60
-rw-r--r--src/wasm/wasm.cpp10
-rw-r--r--src/wasm2js.h6
-rw-r--r--test/binaryen.js/kitchen-sink.js.txt56
-rw-r--r--test/example/c-api-kitchen-sink.txt56
-rw-r--r--test/example/c-api-kitchen-sink.txt.txt28
-rw-r--r--test/passes/dae_enable-tail-call.txt (renamed from test/passes/dae.txt)38
-rw-r--r--test/passes/dae_enable-tail-call.wast (renamed from test/passes/dae.wast)32
-rw-r--r--test/passes/directize_enable-tail-call.txt (renamed from test/passes/directize.txt)14
-rw-r--r--test/passes/directize_enable-tail-call.wast (renamed from test/passes/directize.wast)15
-rw-r--r--test/passes/inlining_enable-tail-call.txt (renamed from test/passes/inlining.txt)164
-rw-r--r--test/passes/inlining_enable-tail-call.wast (renamed from test/passes/inlining.wast)113
-rwxr-xr-xtest/unit/input/tail_call_target_feature.wasmbin129 -> 113 bytes
-rw-r--r--test/unit/test_features.py14
-rw-r--r--test/unit/test_tail_call_type.py42
28 files changed, 758 insertions, 139 deletions
diff --git a/scripts/fuzz_opt.py b/scripts/fuzz_opt.py
index 7c4a87953..c84d9a6af 100644
--- a/scripts/fuzz_opt.py
+++ b/scripts/fuzz_opt.py
@@ -34,7 +34,6 @@ NANS = True
# simd: known issues with d8
# atomics, bulk memory: doesn't work in wasm2js
# truncsat: https://github.com/WebAssembly/binaryen/issues/2198
-# tail-call: WIP
CONSTANT_FEATURE_OPTS = ['--all-features']
# possible feature options that are sometimes passed to the tools.
@@ -298,7 +297,7 @@ class Wasm2JS(TestCaseHandler):
return out
def can_run_on_feature_opts(self, feature_opts):
- return all([x in feature_opts for x in ['--disable-exception-handling', '--disable-simd', '--disable-threads', '--disable-bulk-memory', '--disable-nontrapping-float-to-int']])
+ return all([x in feature_opts for x in ['--disable-exception-handling', '--disable-simd', '--disable-threads', '--disable-bulk-memory', '--disable-nontrapping-float-to-int', '--disable-tail-call']])
class Asyncify(TestCaseHandler):
@@ -343,7 +342,7 @@ class Asyncify(TestCaseHandler):
compare(before, after_asyncify, 'Asyncify (before/after_asyncify)')
def can_run_on_feature_opts(self, feature_opts):
- return all([x in feature_opts for x in ['--disable-exception-handling', '--disable-simd']])
+ return all([x in feature_opts for x in ['--disable-exception-handling', '--disable-simd', '--disable-tail-call']])
# The global list of all test case handlers
diff --git a/src/ir/ExpressionAnalyzer.cpp b/src/ir/ExpressionAnalyzer.cpp
index 170202888..99c2798b0 100644
--- a/src/ir/ExpressionAnalyzer.cpp
+++ b/src/ir/ExpressionAnalyzer.cpp
@@ -132,9 +132,13 @@ template<typename T> void visitImmediates(Expression* curr, T& visitor) {
}
visitor.visitScopeName(curr->default_);
}
- void visitCall(Call* curr) { visitor.visitNonScopeName(curr->target); }
+ void visitCall(Call* curr) {
+ visitor.visitNonScopeName(curr->target);
+ visitor.visitInt(curr->isReturn);
+ }
void visitCallIndirect(CallIndirect* curr) {
visitor.visitNonScopeName(curr->fullType);
+ visitor.visitInt(curr->isReturn);
}
void visitLocalGet(LocalGet* curr) { visitor.visitIndex(curr->index); }
void visitLocalSet(LocalSet* curr) { visitor.visitIndex(curr->index); }
diff --git a/src/ir/ExpressionManipulator.cpp b/src/ir/ExpressionManipulator.cpp
index b52a8dd82..783342780 100644
--- a/src/ir/ExpressionManipulator.cpp
+++ b/src/ir/ExpressionManipulator.cpp
@@ -71,7 +71,8 @@ flexibleCopy(Expression* original, Module& wasm, CustomCopier custom) {
copy(curr->value));
}
Expression* visitCall(Call* curr) {
- auto* ret = builder.makeCall(curr->target, {}, curr->type);
+ auto* ret =
+ builder.makeCall(curr->target, {}, curr->type, curr->isReturn);
for (Index i = 0; i < curr->operands.size(); i++) {
ret->operands.push_back(copy(curr->operands[i]));
}
@@ -79,7 +80,7 @@ flexibleCopy(Expression* original, Module& wasm, CustomCopier custom) {
}
Expression* visitCallIndirect(CallIndirect* curr) {
auto* ret = builder.makeCallIndirect(
- curr->fullType, copy(curr->target), {}, curr->type);
+ curr->fullType, copy(curr->target), {}, curr->type, curr->isReturn);
for (Index i = 0; i < curr->operands.size(); i++) {
ret->operands.push_back(copy(curr->operands[i]));
}
diff --git a/src/ir/effects.h b/src/ir/effects.h
index 14cbd6217..dac5b878a 100644
--- a/src/ir/effects.h
+++ b/src/ir/effects.h
@@ -223,6 +223,9 @@ struct EffectAnalyzer
void visitCall(Call* curr) {
calls = true;
+ if (curr->isReturn) {
+ branches = true;
+ }
if (debugInfo) {
// debugInfo call imports must be preserved very strongly, do not
// move code around them
@@ -230,7 +233,12 @@ struct EffectAnalyzer
branches = true;
}
}
- void visitCallIndirect(CallIndirect* curr) { calls = true; }
+ void visitCallIndirect(CallIndirect* curr) {
+ calls = true;
+ if (curr->isReturn) {
+ branches = true;
+ }
+ }
void visitLocalGet(LocalGet* curr) { localsRead.insert(curr->index); }
void visitLocalSet(LocalSet* curr) { localsWritten.insert(curr->index); }
void visitGlobalGet(GlobalGet* curr) { globalsRead.insert(curr->name); }
diff --git a/src/passes/Asyncify.cpp b/src/passes/Asyncify.cpp
index cb80b90c7..98b4425b1 100644
--- a/src/passes/Asyncify.cpp
+++ b/src/passes/Asyncify.cpp
@@ -349,6 +349,9 @@ public:
}
struct Walker : PostWalker<Walker> {
void visitCall(Call* curr) {
+ if (curr->isReturn) {
+ Fatal() << "tail calls not yet supported in aysncify";
+ }
auto* target = module->getFunction(curr->target);
if (target->imported() && target->module == ASYNCIFY) {
// Redirect the imports to the functions we'll add later.
@@ -375,6 +378,9 @@ public:
info->callsTo.insert(target);
}
void visitCallIndirect(CallIndirect* curr) {
+ if (curr->isReturn) {
+ Fatal() << "tail calls not yet supported in aysncify";
+ }
if (canIndirectChangeState) {
info->canChangeState = true;
}
diff --git a/src/passes/DeadArgumentElimination.cpp b/src/passes/DeadArgumentElimination.cpp
index 789332b93..9ef762f98 100644
--- a/src/passes/DeadArgumentElimination.cpp
+++ b/src/passes/DeadArgumentElimination.cpp
@@ -57,6 +57,16 @@ struct DAEFunctionInfo {
// Map of all calls that are dropped, to their drops' locations (so that
// if we can optimize out the drop, we can replace the drop there).
std::unordered_map<Call*, Expression**> droppedCalls;
+ // Whether this function contains any tail calls (including indirect tail
+ // calls) and the set of functions this function tail calls. Tail-callers and
+ // tail-callees cannot have their dropped returns removed because of the
+ // constraint that tail-callees must have the same return type as
+ // tail-callers. Indirectly tail called functions are already not optimized
+ // because being in a table inhibits DAE. TODO: Allow the removal of dropped
+ // returns from tail-callers if their tail-callees can have their returns
+ // removed as well.
+ bool hasTailCalls = false;
+ std::unordered_set<Name> tailCallees;
// Whether the function can be called from places that
// affect what we can do. For now, any call we don't
// see inhibits our optimizations, but TODO: an export
@@ -117,6 +127,16 @@ struct DAEScanner
if (!getModule()->getFunction(curr->target)->imported()) {
info->calls[curr->target].push_back(curr);
}
+ if (curr->isReturn) {
+ info->hasTailCalls = true;
+ info->tailCallees.insert(curr->target);
+ }
+ }
+
+ void visitCallIndirect(CallIndirect* curr) {
+ if (curr->isReturn) {
+ info->hasTailCalls = true;
+ }
}
void visitDrop(Drop* curr) {
@@ -239,6 +259,7 @@ struct DAE : public Pass {
DAEScanner(&infoMap).run(runner, module);
// Combine all the info.
std::unordered_map<Name, std::vector<Call*>> allCalls;
+ std::unordered_set<Name> tailCallees;
for (auto& pair : infoMap) {
auto& info = pair.second;
for (auto& pair : info.calls) {
@@ -247,6 +268,9 @@ struct DAE : public Pass {
auto& allCallsToName = allCalls[name];
allCallsToName.insert(allCallsToName.end(), calls.begin(), calls.end());
}
+ for (auto& callee : info.tailCallees) {
+ tailCallees.insert(callee);
+ }
for (auto& pair : info.droppedCalls) {
allDroppedCalls[pair.first] = pair.second;
}
@@ -314,14 +338,11 @@ struct DAE : public Pass {
// Great, it's not used. Check if none of the calls has a param with
// side effects, as that would prevent us removing them (flattening
// should have been done earlier).
- bool canRemove = true;
- for (auto* call : calls) {
- auto* operand = call->operands[i];
- if (EffectAnalyzer(runner->options, operand).hasSideEffects()) {
- canRemove = false;
- break;
- }
- }
+ bool canRemove =
+ std::none_of(calls.begin(), calls.end(), [&](Call* call) {
+ auto* operand = call->operands[i];
+ return EffectAnalyzer(runner->options, operand).hasSideEffects();
+ });
if (canRemove) {
// Wonderful, nothing stands in our way! Do it.
// TODO: parallelize this?
@@ -348,18 +369,21 @@ struct DAE : public Pass {
if (infoMap[name].hasUnseenCalls) {
continue;
}
+ if (infoMap[name].hasTailCalls) {
+ continue;
+ }
+ if (tailCallees.find(name) != tailCallees.end()) {
+ continue;
+ }
auto iter = allCalls.find(name);
if (iter == allCalls.end()) {
continue;
}
auto& calls = iter->second;
- bool allDropped = true;
- for (auto* call : calls) {
- if (!allDroppedCalls.count(call)) {
- allDropped = false;
- break;
- }
- }
+ bool allDropped =
+ std::all_of(calls.begin(), calls.end(), [&](Call* call) {
+ return allDroppedCalls.count(call);
+ });
if (!allDropped) {
continue;
}
diff --git a/src/passes/DeadCodeElimination.cpp b/src/passes/DeadCodeElimination.cpp
index 6e6f2c2b9..61d16afc0 100644
--- a/src/passes/DeadCodeElimination.cpp
+++ b/src/passes/DeadCodeElimination.cpp
@@ -365,7 +365,12 @@ struct DeadCodeElimination
return curr;
}
- void visitCall(Call* curr) { handleCall(curr); }
+ void visitCall(Call* curr) {
+ handleCall(curr);
+ if (curr->isReturn) {
+ reachable = false;
+ }
+ }
void visitCallIndirect(CallIndirect* curr) {
if (handleCall(curr) != curr) {
@@ -380,6 +385,9 @@ struct DeadCodeElimination
block->finalize(curr->type);
replaceCurrent(block);
}
+ if (curr->isReturn) {
+ reachable = false;
+ }
}
// Append the reachable operands of the current node to a block, and replace
diff --git a/src/passes/Directize.cpp b/src/passes/Directize.cpp
index f6969cce5..663b8257b 100644
--- a/src/passes/Directize.cpp
+++ b/src/passes/Directize.cpp
@@ -65,7 +65,8 @@ struct FunctionDirectizer : public WalkerPass<PostWalker<FunctionDirectizer>> {
}
// Everything looks good!
replaceCurrent(
- Builder(*getModule()).makeCall(name, curr->operands, curr->type));
+ Builder(*getModule())
+ .makeCall(name, curr->operands, curr->type, curr->isReturn));
}
}
diff --git a/src/passes/I64ToI32Lowering.cpp b/src/passes/I64ToI32Lowering.cpp
index e893b6296..5d7d5998c 100644
--- a/src/passes/I64ToI32Lowering.cpp
+++ b/src/passes/I64ToI32Lowering.cpp
@@ -262,9 +262,14 @@ struct I64ToI32Lowering : public WalkerPass<PostWalker<I64ToI32Lowering>> {
return call;
}
void visitCall(Call* curr) {
+ if (curr->isReturn &&
+ getModule()->getFunction(curr->target)->result == i64) {
+ Fatal()
+ << "i64 to i32 lowering of return_call values not yet implemented";
+ }
auto* fixedCall = visitGenericCall<Call>(
curr, [&](std::vector<Expression*>& args, Type ty) {
- return builder->makeCall(curr->target, args, ty);
+ return builder->makeCall(curr->target, args, ty, curr->isReturn);
});
// If this was to an import, we need to call the legal version. This assumes
// that legalize-js-interface has been run before.
@@ -275,10 +280,15 @@ struct I64ToI32Lowering : public WalkerPass<PostWalker<I64ToI32Lowering>> {
}
void visitCallIndirect(CallIndirect* curr) {
+ if (curr->isReturn &&
+ getModule()->getFunctionType(curr->fullType)->result == i64) {
+ Fatal()
+ << "i64 to i32 lowering of return_call values not yet implemented";
+ }
visitGenericCall<CallIndirect>(
curr, [&](std::vector<Expression*>& args, Type ty) {
return builder->makeCallIndirect(
- curr->fullType, curr->target, args, ty);
+ curr->fullType, curr->target, args, ty, curr->isReturn);
});
}
diff --git a/src/passes/Inlining.cpp b/src/passes/Inlining.cpp
index a625464cd..9ba01c4c1 100644
--- a/src/passes/Inlining.cpp
+++ b/src/passes/Inlining.cpp
@@ -146,7 +146,17 @@ struct Planner : public WalkerPass<PostWalker<Planner>> {
// plan to inline if we know this is valid to inline, and if the call is
// actually performed - if it is dead code, it's pointless to inline.
// we also cannot inline ourselves.
- if (state->worthInlining.count(curr->target) && curr->type != unreachable &&
+ bool isUnreachable;
+ if (curr->isReturn) {
+ // Tail calls are only actually unreachable if an argument is
+ isUnreachable =
+ std::any_of(curr->operands.begin(),
+ curr->operands.end(),
+ [](Expression* op) { return op->type == unreachable; });
+ } else {
+ isUnreachable = curr->type == unreachable;
+ }
+ if (state->worthInlining.count(curr->target) && !isUnreachable &&
curr->target != getFunction()->name) {
// nest the call in a block. that way the location of the pointer to the
// call will not change even if we inline multiple times into the same
@@ -164,32 +174,69 @@ private:
InliningState* state;
};
+struct Updater : public PostWalker<Updater> {
+ Module* module;
+ std::map<Index, Index> localMapping;
+ Name returnName;
+ Builder* builder;
+ void visitReturn(Return* curr) {
+ replaceCurrent(builder->makeBreak(returnName, curr->value));
+ }
+ // Return calls in inlined functions should only break out of the scope of
+ // the inlined code, not the entire function they are being inlined into. To
+ // achieve this, make the call a non-return call and add a break. This does
+ // not cause unbounded stack growth because inlining and return calling both
+ // avoid creating a new stack frame.
+ template<typename T> void handleReturnCall(T* curr, Type targetType) {
+ curr->isReturn = false;
+ curr->type = targetType;
+ if (isConcreteType(targetType)) {
+ replaceCurrent(builder->makeBreak(returnName, curr));
+ } else {
+ replaceCurrent(builder->blockify(curr, builder->makeBreak(returnName)));
+ }
+ }
+ void visitCall(Call* curr) {
+ if (curr->isReturn) {
+ handleReturnCall(curr, module->getFunction(curr->target)->result);
+ }
+ }
+ void visitCallIndirect(CallIndirect* curr) {
+ if (curr->isReturn) {
+ handleReturnCall(curr, module->getFunctionType(curr->fullType)->result);
+ }
+ }
+ void visitLocalGet(LocalGet* curr) {
+ curr->index = localMapping[curr->index];
+ }
+ void visitLocalSet(LocalSet* curr) {
+ curr->index = localMapping[curr->index];
+ }
+};
+
// Core inlining logic. Modifies the outside function (adding locals as
// needed), and returns the inlined code.
static Expression*
doInlining(Module* module, Function* into, InliningAction& action) {
Function* from = action.contents;
auto* call = (*action.callSite)->cast<Call>();
+ // Works for return_call, too
+ Type retType = module->getFunction(call->target)->result;
Builder builder(*module);
- auto* block = Builder(*module).makeBlock();
+ auto* block = builder.makeBlock();
block->name = Name(std::string("__inlined_func$") + from->name.str);
- *action.callSite = block;
- // Prepare to update the inlined code's locals and other things.
- struct Updater : public PostWalker<Updater> {
- std::map<Index, Index> localMapping;
- Name returnName;
- Builder* builder;
-
- void visitReturn(Return* curr) {
- replaceCurrent(builder->makeBreak(returnName, curr->value));
+ if (call->isReturn) {
+ if (isConcreteType(retType)) {
+ *action.callSite = builder.makeReturn(block);
+ } else {
+ *action.callSite = builder.makeSequence(block, builder.makeReturn());
}
- void visitLocalGet(LocalGet* curr) {
- curr->index = localMapping[curr->index];
- }
- void visitLocalSet(LocalSet* curr) {
- curr->index = localMapping[curr->index];
- }
- } updater;
+ } else {
+ *action.callSite = block;
+ }
+ // Prepare to update the inlined code's locals and other things.
+ Updater updater;
+ updater.module = module;
updater.returnName = block->name;
updater.builder = &builder;
// Set up a locals mapping
@@ -215,12 +262,12 @@ doInlining(Module* module, Function* into, InliningAction& action) {
}
updater.walk(contents);
block->list.push_back(contents);
- block->type = call->type;
+ block->type = retType;
// If the function returned a value, we just set the block containing the
// inlined code to have that type. or, if the function was void and
// contained void, that is fine too. a bad case is a void function in which
// we have unreachable code, so we would be replacing a void call with an
- // unreachable; we need to handle
+ // unreachable.
if (contents->type == unreachable && block->type == none) {
// Make the block reachable by adding a break to it
block->list.push_back(builder.makeBreak(block->name));
diff --git a/src/tools/fuzzing.h b/src/tools/fuzzing.h
index 1d68219e2..6a947d80c 100644
--- a/src/tools/fuzzing.h
+++ b/src/tools/fuzzing.h
@@ -1200,12 +1200,15 @@ private:
Expression* makeCall(Type type) {
// seems ok, go on
int tries = TRIES;
+ bool isReturn;
while (tries-- > 0) {
Function* target = func;
if (!wasm.functions.empty() && !oneIn(wasm.functions.size())) {
target = vectorPick(wasm.functions).get();
}
- if (target->result != type) {
+ isReturn = type == unreachable && wasm.features.hasTailCall() &&
+ func->result == target->result;
+ if (target->result != type && !isReturn) {
continue;
}
// we found one!
@@ -1213,7 +1216,7 @@ private:
for (auto argType : target->params) {
args.push_back(make(argType));
}
- return builder.makeCall(target->name, args, type);
+ return builder.makeCall(target->name, args, type, isReturn);
}
// we failed to find something
return make(type);
@@ -1227,11 +1230,14 @@ private:
// look for a call target with the right type
Index start = upTo(data.size());
Index i = start;
- Function* func;
+ Function* targetFn;
+ bool isReturn;
while (1) {
// TODO: handle unreachable
- func = wasm.getFunction(data[i]);
- if (func->result == type) {
+ targetFn = wasm.getFunction(data[i]);
+ isReturn = type == unreachable && wasm.features.hasTailCall() &&
+ func->result == targetFn->result;
+ if (targetFn->result == type || isReturn) {
break;
}
i++;
@@ -1251,11 +1257,12 @@ private:
target = make(i32);
}
std::vector<Expression*> args;
- for (auto type : func->params) {
+ for (auto type : targetFn->params) {
args.push_back(make(type));
}
- func->type = ensureFunctionType(getSig(func), &wasm)->name;
- return builder.makeCallIndirect(func->type, target, args, func->result);
+ targetFn->type = ensureFunctionType(getSig(targetFn), &wasm)->name;
+ return builder.makeCallIndirect(
+ targetFn->type, target, args, targetFn->result, isReturn);
}
Expression* makeLocalGet(Type type) {
diff --git a/src/wasm-builder.h b/src/wasm-builder.h
index 69fcef3f1..923effb76 100644
--- a/src/wasm-builder.h
+++ b/src/wasm-builder.h
@@ -207,6 +207,7 @@ public:
call->target = target;
call->operands.set(args);
call->isReturn = isReturn;
+ call->finalize();
return call;
}
CallIndirect* makeCallIndirect(FunctionType* type,
@@ -226,6 +227,7 @@ public:
call->target = target;
call->operands.set(args);
call->isReturn = isReturn;
+ call->finalize();
return call;
}
// FunctionType
diff --git a/src/wasm-interpreter.h b/src/wasm-interpreter.h
index 42e904fd3..f683f9b38 100644
--- a/src/wasm-interpreter.h
+++ b/src/wasm-interpreter.h
@@ -1435,6 +1435,15 @@ private:
#ifdef WASM_INTERPRETER_DEBUG
std::cout << "(returned to " << scope.function->name << ")\n";
#endif
+ // TODO: make this a proper tail call (return first)
+ if (curr->isReturn) {
+ Const c;
+ c.value = ret.value;
+ c.finalize();
+ Return return_;
+ return_.value = &c;
+ return this->visit(&return_);
+ }
return ret;
}
Flow visitCallIndirect(CallIndirect* curr) {
@@ -1449,8 +1458,19 @@ private:
return target;
}
Index index = target.value.geti32();
- return instance.externalInterface->callTable(
- index, arguments, curr->type, *instance.self());
+ Type type = curr->isReturn ? scope.function->result : curr->type;
+ Flow ret = instance.externalInterface->callTable(
+ index, arguments, type, *instance.self());
+ // TODO: make this a proper tail call (return first)
+ if (curr->isReturn) {
+ Const c;
+ c.value = ret.value;
+ c.finalize();
+ Return return_;
+ return_.value = &c;
+ return this->visit(&return_);
+ }
+ return ret;
}
Flow visitLocalGet(LocalGet* curr) {
diff --git a/src/wasm/wasm-validator.cpp b/src/wasm/wasm-validator.cpp
index 8098116be..372c5bf6d 100644
--- a/src/wasm/wasm-validator.cpp
+++ b/src/wasm/wasm-validator.cpp
@@ -549,6 +549,10 @@ void FunctionValidator::noteBreak(Name name,
}
void FunctionValidator::visitBreak(Break* curr) {
noteBreak(curr->name, curr->value, curr);
+ if (curr->value) {
+ shouldBeTrue(
+ curr->value->type != none, curr, "break value must not have none type");
+ }
if (curr->condition) {
shouldBeTrue(curr->condition->type == unreachable ||
curr->condition->type == i32,
@@ -593,6 +597,33 @@ void FunctionValidator::visitCall(Call* curr) {
getStream() << "(on argument " << i << ")\n";
}
}
+ if (curr->isReturn) {
+ shouldBeEqual(curr->type,
+ unreachable,
+ curr,
+ "return_call should have unreachable type");
+ shouldBeEqual(
+ getFunction()->result,
+ target->result,
+ curr,
+ "return_call callee return type must match caller return type");
+ } else {
+ if (curr->type == unreachable) {
+ bool hasUnreachableOperand =
+ std::any_of(curr->operands.begin(),
+ curr->operands.end(),
+ [](Expression* op) { return op->type == unreachable; });
+ shouldBeTrue(
+ hasUnreachableOperand,
+ curr,
+ "calls may only be unreachable if they have unreachable operands");
+ } else {
+ shouldBeEqual(curr->type,
+ target->result,
+ curr,
+ "call type must match callee return type");
+ }
+ }
}
void FunctionValidator::visitCallIndirect(CallIndirect* curr) {
@@ -622,6 +653,35 @@ void FunctionValidator::visitCallIndirect(CallIndirect* curr) {
getStream() << "(on argument " << i << ")\n";
}
}
+ if (curr->isReturn) {
+ shouldBeEqual(curr->type,
+ unreachable,
+ curr,
+ "return_call_indirect should have unreachable type");
+ shouldBeEqual(
+ getFunction()->result,
+ type->result,
+ curr,
+ "return_call_indirect callee return type must match caller return type");
+ } else {
+ if (curr->type == unreachable) {
+ if (curr->target->type != unreachable) {
+ bool hasUnreachableOperand =
+ std::any_of(curr->operands.begin(),
+ curr->operands.end(),
+ [](Expression* op) { return op->type == unreachable; });
+ shouldBeTrue(hasUnreachableOperand,
+ curr,
+ "call_indirects may only be unreachable if they have "
+ "unreachable operands");
+ }
+ } else {
+ shouldBeEqual(curr->type,
+ type->result,
+ curr,
+ "call_indirect type must match callee return type");
+ }
+ }
}
void FunctionValidator::visitConst(Const* curr) {
diff --git a/src/wasm/wasm.cpp b/src/wasm/wasm.cpp
index d0a916072..04abbcd9f 100644
--- a/src/wasm/wasm.cpp
+++ b/src/wasm/wasm.cpp
@@ -405,10 +405,18 @@ template<typename T> void handleUnreachableOperands(T* curr) {
}
}
-void Call::finalize() { handleUnreachableOperands(this); }
+void Call::finalize() {
+ handleUnreachableOperands(this);
+ if (isReturn) {
+ type = unreachable;
+ }
+}
void CallIndirect::finalize() {
handleUnreachableOperands(this);
+ if (isReturn) {
+ type = unreachable;
+ }
if (target->type == unreachable) {
type = unreachable;
}
diff --git a/src/wasm2js.h b/src/wasm2js.h
index c5fa54028..b1548c0ca 100644
--- a/src/wasm2js.h
+++ b/src/wasm2js.h
@@ -1115,6 +1115,9 @@ Ref Wasm2JSBuilder::processFunctionBody(Module* m,
}
Ref visitCall(Call* curr) {
+ if (curr->isReturn) {
+ Fatal() << "tail calls not yet supported in wasm2js";
+ }
Ref theCall =
ValueBuilder::makeCall(fromName(curr->target, NameScope::Top));
// For wasm => wasm calls, we don't need coercions. TODO: even imports
@@ -1136,6 +1139,9 @@ Ref Wasm2JSBuilder::processFunctionBody(Module* m,
}
Ref visitCallIndirect(CallIndirect* curr) {
+ if (curr->isReturn) {
+ Fatal() << "tail calls not yet supported in wasm2js";
+ }
// If the target has effects that interact with the operands, we must
// reorder it to the start.
bool mustReorder = false;
diff --git a/test/binaryen.js/kitchen-sink.js.txt b/test/binaryen.js/kitchen-sink.js.txt
index 3ec65d02b..74268dd44 100644
--- a/test/binaryen.js/kitchen-sink.js.txt
+++ b/test/binaryen.js/kitchen-sink.js.txt
@@ -1416,22 +1416,18 @@ getExpressionInfo(f64.const)={"id":14,"type":4,"value":9.5}
(return
(i32.const 1337)
)
- (drop
- (return_call "$kitchen()sinker"
- (i32.const 13)
- (i64.const 37)
- (f32.const 1.2999999523162842)
- (f64.const 3.7)
- )
- )
- (drop
- (return_call_indirect (type $iiIfF)
- (i32.const 13)
- (i64.const 37)
- (f32.const 1.2999999523162842)
- (f64.const 3.7)
- (i32.const 2449)
- )
+ (return_call "$kitchen()sinker"
+ (i32.const 13)
+ (i64.const 37)
+ (f32.const 1.2999999523162842)
+ (f64.const 3.7)
+ )
+ (return_call_indirect (type $iiIfF)
+ (i32.const 13)
+ (i64.const 37)
+ (f32.const 1.2999999523162842)
+ (f64.const 3.7)
+ (i32.const 2449)
)
(nop)
(unreachable)
@@ -4800,22 +4796,18 @@ getExpressionInfo(f64.const)={"id":14,"type":4,"value":9.5}
(return
(i32.const 1337)
)
- (drop
- (return_call "$kitchen()sinker"
- (i32.const 13)
- (i64.const 37)
- (f32.const 1.2999999523162842)
- (f64.const 3.7)
- )
- )
- (drop
- (return_call_indirect (type $iiIfF)
- (i32.const 13)
- (i64.const 37)
- (f32.const 1.2999999523162842)
- (f64.const 3.7)
- (i32.const 2449)
- )
+ (return_call "$kitchen()sinker"
+ (i32.const 13)
+ (i64.const 37)
+ (f32.const 1.2999999523162842)
+ (f64.const 3.7)
+ )
+ (return_call_indirect (type $iiIfF)
+ (i32.const 13)
+ (i64.const 37)
+ (f32.const 1.2999999523162842)
+ (f64.const 3.7)
+ (i32.const 2449)
)
(nop)
(unreachable)
diff --git a/test/example/c-api-kitchen-sink.txt b/test/example/c-api-kitchen-sink.txt
index d18e1728e..14ed2c50e 100644
--- a/test/example/c-api-kitchen-sink.txt
+++ b/test/example/c-api-kitchen-sink.txt
@@ -1380,22 +1380,18 @@ BinaryenFeatureAll: 255
(return
(i32.const 1337)
)
- (drop
- (return_call "$kitchen()sinker"
- (i32.const 13)
- (i64.const 37)
- (f32.const 1.2999999523162842)
- (f64.const 3.7)
- )
- )
- (drop
- (return_call_indirect (type $iiIfF)
- (i32.const 13)
- (i64.const 37)
- (f32.const 1.2999999523162842)
- (f64.const 3.7)
- (i32.const 2449)
- )
+ (return_call "$kitchen()sinker"
+ (i32.const 13)
+ (i64.const 37)
+ (f32.const 1.2999999523162842)
+ (f64.const 3.7)
+ )
+ (return_call_indirect (type $iiIfF)
+ (i32.const 13)
+ (i64.const 37)
+ (f32.const 1.2999999523162842)
+ (f64.const 3.7)
+ (i32.const 2449)
)
(nop)
(unreachable)
@@ -4743,22 +4739,18 @@ int main() {
(return
(i32.const 1337)
)
- (drop
- (return_call "$kitchen()sinker"
- (i32.const 13)
- (i64.const 37)
- (f32.const 1.2999999523162842)
- (f64.const 3.7)
- )
- )
- (drop
- (return_call_indirect (type $iiIfF)
- (i32.const 13)
- (i64.const 37)
- (f32.const 1.2999999523162842)
- (f64.const 3.7)
- (i32.const 2449)
- )
+ (return_call "$kitchen()sinker"
+ (i32.const 13)
+ (i64.const 37)
+ (f32.const 1.2999999523162842)
+ (f64.const 3.7)
+ )
+ (return_call_indirect (type $iiIfF)
+ (i32.const 13)
+ (i64.const 37)
+ (f32.const 1.2999999523162842)
+ (f64.const 3.7)
+ (i32.const 2449)
)
(nop)
(unreachable)
diff --git a/test/example/c-api-kitchen-sink.txt.txt b/test/example/c-api-kitchen-sink.txt.txt
index c73646727..fd8ee7e94 100644
--- a/test/example/c-api-kitchen-sink.txt.txt
+++ b/test/example/c-api-kitchen-sink.txt.txt
@@ -1362,22 +1362,18 @@
(return
(i32.const 1337)
)
- (drop
- (return_call "$kitchen()sinker"
- (i32.const 13)
- (i64.const 37)
- (f32.const 1.2999999523162842)
- (f64.const 3.7)
- )
- )
- (drop
- (return_call_indirect (type $iiIfF)
- (i32.const 13)
- (i64.const 37)
- (f32.const 1.2999999523162842)
- (f64.const 3.7)
- (i32.const 2449)
- )
+ (return_call "$kitchen()sinker"
+ (i32.const 13)
+ (i64.const 37)
+ (f32.const 1.2999999523162842)
+ (f64.const 3.7)
+ )
+ (return_call_indirect (type $iiIfF)
+ (i32.const 13)
+ (i64.const 37)
+ (f32.const 1.2999999523162842)
+ (f64.const 3.7)
+ (i32.const 2449)
)
(nop)
(unreachable)
diff --git a/test/passes/dae.txt b/test/passes/dae_enable-tail-call.txt
index 2789d5206..1854f8a33 100644
--- a/test/passes/dae.txt
+++ b/test/passes/dae_enable-tail-call.txt
@@ -243,3 +243,41 @@
)
)
)
+(module
+ (type $FUNCSIG$ii (func (param i32) (result i32)))
+ (func $foo (; 0 ;) (type $FUNCSIG$ii) (param $x i32) (result i32)
+ (drop
+ (return_call $bar)
+ )
+ (i32.const 42)
+ )
+ (func $bar (; 1 ;) (result i32)
+ (local $0 i32)
+ (local.set $0
+ (i32.const 0)
+ )
+ (i32.const 7)
+ )
+)
+(module
+ (type $T (func (result i32)))
+ (type $FUNCSIG$ii (func (param i32) (result i32)))
+ (type $FUNCSIG$v (func))
+ (table $0 1 1 funcref)
+ (func $foo (; 0 ;) (result i32)
+ (local $0 i32)
+ (local.set $0
+ (i32.const 42)
+ )
+ (drop
+ (return_call_indirect (type $T)
+ (i32.const 0)
+ )
+ )
+ )
+ (func $bar (; 1 ;) (type $FUNCSIG$v)
+ (drop
+ (call $foo)
+ )
+ )
+)
diff --git a/test/passes/dae.wast b/test/passes/dae_enable-tail-call.wast
index 2fdf6958d..e70cde1bb 100644
--- a/test/passes/dae.wast
+++ b/test/passes/dae_enable-tail-call.wast
@@ -130,4 +130,34 @@
(local.get $x)
)
)
-
+(module ;; tail calls inhibit dropped result removal
+ (func $foo (param $x i32) (result i32)
+ (drop
+ (return_call $bar
+ (i32.const 0)
+ )
+ )
+ (i32.const 42)
+ )
+ (func $bar (param $x i32) (result i32)
+ (i32.const 7)
+ )
+)
+(module ;; indirect tail calls inhibit dropped result removal
+ (type $T (func (result i32)))
+ (table 1 1 funcref)
+ (func $foo (param $x i32) (result i32)
+ (drop
+ (return_call_indirect (type $T)
+ (i32.const 0)
+ )
+ )
+ )
+ (func $bar
+ (drop
+ (call $foo
+ (i32.const 42)
+ )
+ )
+ )
+)
diff --git a/test/passes/directize.txt b/test/passes/directize_enable-tail-call.txt
index 109140a8f..2a74aff68 100644
--- a/test/passes/directize.txt
+++ b/test/passes/directize_enable-tail-call.txt
@@ -195,3 +195,17 @@
)
)
)
+(module
+ (type $ii (func (param i32 i32)))
+ (table $0 5 5 funcref)
+ (elem (i32.const 1) $foo)
+ (func $foo (; 0 ;) (type $ii) (param $0 i32) (param $1 i32)
+ (unreachable)
+ )
+ (func $bar (; 1 ;) (type $ii) (param $x i32) (param $y i32)
+ (return_call $foo
+ (local.get $x)
+ (local.get $y)
+ )
+ )
+)
diff --git a/test/passes/directize.wast b/test/passes/directize_enable-tail-call.wast
index 4385aa18a..e111cf615 100644
--- a/test/passes/directize.wast
+++ b/test/passes/directize_enable-tail-call.wast
@@ -191,3 +191,18 @@
)
)
)
+(module ;; indirect tail call
+ (type $ii (func (param i32 i32)))
+ (table $0 5 5 funcref)
+ (elem (i32.const 1) $foo)
+ (func $foo (param i32) (param i32)
+ (unreachable)
+ )
+ (func $bar (param $x i32) (param $y i32)
+ (return_call_indirect (type $ii)
+ (local.get $x)
+ (local.get $y)
+ (i32.const 1)
+ )
+ )
+)
diff --git a/test/passes/inlining.txt b/test/passes/inlining_enable-tail-call.txt
index 738b580de..3119de526 100644
--- a/test/passes/inlining.txt
+++ b/test/passes/inlining_enable-tail-call.txt
@@ -261,3 +261,167 @@
)
)
)
+(module
+ (type $FUNCSIG$i (func (result i32)))
+ (func $0 (; 0 ;) (type $FUNCSIG$i) (result i32)
+ (return
+ (block $__inlined_func$1 (result i32)
+ (i32.const 42)
+ )
+ )
+ )
+)
+(module
+ (type $FUNCSIG$v (func))
+ (type $FUNCSIG$vi (func (param i32)))
+ (func $0 (; 0 ;) (type $FUNCSIG$v)
+ (local $0 i32)
+ (block
+ (block $__inlined_func$1
+ (local.set $0
+ (i32.const 42)
+ )
+ (drop
+ (local.get $0)
+ )
+ )
+ (return)
+ )
+ )
+)
+(module
+ (type $FUNCSIG$i (func (result i32)))
+ (type $FUNCSIG$ii (func (param i32) (result i32)))
+ (func $0 (; 0 ;) (type $FUNCSIG$i) (result i32)
+ (local $0 i32)
+ (return
+ (block $__inlined_func$1 (result i32)
+ (local.set $0
+ (i32.const 42)
+ )
+ (local.get $0)
+ )
+ )
+ )
+)
+(module
+ (type $FUNCSIG$v (func))
+ (type $FUNCSIG$i (func (result i32)))
+ (func $0 (; 0 ;) (type $FUNCSIG$v)
+ (drop
+ (block (result i32)
+ (block $__inlined_func$1 (result i32)
+ (block
+ (br $__inlined_func$1
+ (block (result i32)
+ (block $__inlined_func$2 (result i32)
+ (i32.const 42)
+ )
+ )
+ )
+ )
+ )
+ )
+ )
+ )
+)
+(module
+ (type $FUNCSIG$v (func))
+ (type $FUNCSIG$vi (func (param i32)))
+ (func $0 (; 0 ;) (type $FUNCSIG$v)
+ (local $0 i32)
+ (block $__inlined_func$1
+ (block
+ (block
+ (block
+ (block $__inlined_func$2
+ (local.set $0
+ (i32.const 42)
+ )
+ (drop
+ (local.get $0)
+ )
+ )
+ )
+ (br $__inlined_func$1)
+ )
+ )
+ (br $__inlined_func$1)
+ )
+ )
+)
+(module
+ (type $T (func (param i32) (result i32)))
+ (type $FUNCSIG$v (func))
+ (type $FUNCSIG$i (func (result i32)))
+ (table $0 10 funcref)
+ (func $0 (; 0 ;) (type $FUNCSIG$v)
+ (drop
+ (block (result i32)
+ (block $__inlined_func$1 (result i32)
+ (br $__inlined_func$1
+ (call_indirect (type $T)
+ (i32.const 42)
+ (i32.const 0)
+ )
+ )
+ )
+ )
+ )
+ )
+)
+(module
+ (type $T (func (param i32)))
+ (type $FUNCSIG$v (func))
+ (table $0 10 funcref)
+ (func $0 (; 0 ;) (type $FUNCSIG$v)
+ (block $__inlined_func$1
+ (block
+ (call_indirect (type $T)
+ (i32.const 42)
+ (i32.const 0)
+ )
+ (br $__inlined_func$1)
+ )
+ (br $__inlined_func$1)
+ )
+ )
+)
+(module
+ (type $6 (func))
+ (memory $0 1 1)
+ (global $global$0 (mut i32) (i32.const 10))
+ (export "func_102_invoker" (func $19))
+ (func $19 (; 0 ;) (type $6)
+ (block
+ (block
+ (block $__inlined_func$13
+ (block
+ (if
+ (global.get $global$0)
+ (unreachable)
+ )
+ (block
+ (block
+ (block $__inlined_func$2
+ (block
+ (if
+ (global.get $global$0)
+ (br $__inlined_func$2)
+ )
+ (global.set $global$0
+ (i32.const 1)
+ )
+ )
+ )
+ (br $__inlined_func$13)
+ )
+ )
+ )
+ (br $__inlined_func$13)
+ )
+ )
+ )
+ (unreachable)
+ )
+)
diff --git a/test/passes/inlining.wast b/test/passes/inlining_enable-tail-call.wast
index 050ee9bc2..0ca64267a 100644
--- a/test/passes/inlining.wast
+++ b/test/passes/inlining_enable-tail-call.wast
@@ -174,4 +174,115 @@
(call $0)
)
)
-
+(module
+ (func $0 (result i32)
+ (return_call $1)
+ )
+ (func $1 (result i32)
+ (i32.const 42)
+ )
+)
+(module
+ (func $0
+ (return_call $1
+ (i32.const 42)
+ )
+ )
+ (func $1 (param i32)
+ (drop
+ (local.get 0)
+ )
+ )
+)
+(module
+ (func $0 (result i32)
+ (return_call $1
+ (i32.const 42)
+ )
+ )
+ (func $1 (param i32) (result i32)
+ (local.get 0)
+ )
+)
+(module
+ (func $0
+ (drop
+ (call $1)
+ )
+ )
+ (func $1 (result i32)
+ (return_call $2)
+ )
+ (func $2 (result i32)
+ (i32.const 42)
+ )
+)
+(module
+ (func $0
+ (call $1)
+ )
+ (func $1
+ (return_call $2
+ (i32.const 42)
+ )
+ )
+ (func $2 (param i32)
+ (drop
+ (local.get 0)
+ )
+ )
+)
+(module
+ (type $T (func (param i32) (result i32)))
+ (table 10 funcref)
+ (func $0
+ (drop
+ (call $1)
+ )
+ )
+ (func $1 (result i32)
+ (return_call_indirect (type $T)
+ (i32.const 42)
+ (i32.const 0)
+ )
+ )
+)
+(module
+ (type $T (func (param i32)))
+ (table 10 funcref)
+ (func $0
+ (call $1)
+ )
+ (func $1
+ (return_call_indirect (type $T)
+ (i32.const 42)
+ (i32.const 0)
+ )
+ )
+)
+(module
+ (type $6 (func))
+ (memory $0 1 1)
+ (global $global$0 (mut i32) (i32.const 10))
+ (export "func_102_invoker" (func $19))
+ (func $2 (; 2 ;) (type $6)
+ (if
+ (global.get $global$0)
+ (return)
+ )
+ (global.set $global$0
+ (i32.const 1)
+ )
+ )
+ (func $13 (; 13 ;) (type $6)
+ (if
+ (global.get $global$0)
+ (unreachable)
+ )
+ (return_call $2)
+ )
+ (func $19 (; 19 ;) (type $6)
+ (call $13)
+ (unreachable)
+ )
+)
diff --git a/test/unit/input/tail_call_target_feature.wasm b/test/unit/input/tail_call_target_feature.wasm
index 063b6d2a5..651c92ec8 100755
--- a/test/unit/input/tail_call_target_feature.wasm
+++ b/test/unit/input/tail_call_target_feature.wasm
Binary files differ
diff --git a/test/unit/test_features.py b/test/unit/test_features.py
index 968aaa252..2a0eca39b 100644
--- a/test/unit/test_features.py
+++ b/test/unit/test_features.py
@@ -131,6 +131,20 @@ class FeatureValidationTest(BinaryenTestCase):
'''
self.check_tail_call(module, 'return_call requires tail calls to be enabled')
+ def test_tail_call_indirect(self):
+ module = '''
+ (module
+ (type $T (func))
+ (table $0 1 1 funcref)
+ (func $foo
+ (return_call_indirect (type $T)
+ (i32.const 0)
+ )
+ )
+ )
+ '''
+ self.check_tail_call(module, 'return_call_indirect requires tail calls to be enabled')
+
class TargetFeaturesSectionTest(BinaryenTestCase):
def test_atomics(self):
diff --git a/test/unit/test_tail_call_type.py b/test/unit/test_tail_call_type.py
new file mode 100644
index 000000000..4db0589c4
--- /dev/null
+++ b/test/unit/test_tail_call_type.py
@@ -0,0 +1,42 @@
+from scripts.test.shared import WASM_OPT, run_process
+from utils import BinaryenTestCase
+import os
+
+
+class TailCallTypeTest(BinaryenTestCase):
+ def test_return_call(self):
+ module = '''
+ (module
+ (func $foo (result f32)
+ (return_call $bar)
+ )
+ (func $bar (result i32)
+ (i32.const 1)
+ )
+ )
+ '''
+ p = run_process(WASM_OPT + ['--enable-tail-call', '-o', os.devnull],
+ input=module, check=False, capture_output=True)
+ self.assertNotEqual(p.returncode, 0)
+ self.assertIn(
+ 'return_call callee return type must match caller return type',
+ p.stderr)
+
+ def test_return_call_indirect(self):
+ module = '''
+ (module
+ (type $T (func (result i32)))
+ (table $0 1 1 funcref)
+ (func $foo (result f32)
+ (return_call_indirect (type $T)
+ (i32.const 0)
+ )
+ )
+ )
+ '''
+ p = run_process(WASM_OPT + ['--enable-tail-call', '-o', os.devnull],
+ input=module, check=False, capture_output=True)
+ self.assertNotEqual(p.returncode, 0)
+ self.assertIn(
+ 'return_call_indirect callee return type must match caller return type',
+ p.stderr)