Skip to content

Commit 761e417

Browse files
committed
Fix interpretation of return_call*
We previously interpreted return calls as calls followed by returns, but that is not correct both because it grows the size of the execution stack and because it runs the called functions in the wrong context, which can be observable in the case of exception handling. Update the interpreter to handle return calls correctly by adding a new `RETURN_CALL_FLOW` that behaves like a return, but carries the arguments and reference to the return-callee rather than normal return values. `callFunctionInternal` is updated to intercept this flow and call return-called functions in a loop until a function returns with some other kind of flow. Pull in the upstream spec tests return_call.wast, return_call_indirect.wast, and return_call_ref.wast with light editing so that we parse and validate them successfully.
1 parent 67491c1 commit 761e417

File tree

5 files changed

+1219
-65
lines changed

5 files changed

+1219
-65
lines changed

src/wasm-interpreter.h

Lines changed: 103 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ std::ostream& operator<<(std::ostream& o, const WasmException& exn);
5151

5252
// Utilities
5353

54-
extern Name WASM, RETURN_FLOW, NONCONSTANT_FLOW;
54+
extern Name WASM, RETURN_FLOW, RETURN_CALL_FLOW, NONCONSTANT_FLOW;
5555

5656
// Stuff that flows around during executing expressions: a literal, or a change
5757
// in control flow.
@@ -63,6 +63,8 @@ class Flow {
6363
Flow(Literals&& values) : values(std::move(values)) {}
6464
Flow(Name breakTo) : values(), breakTo(breakTo) {}
6565
Flow(Name breakTo, Literal value) : values{value}, breakTo(breakTo) {}
66+
Flow(Name breakTo, Literals&& values)
67+
: values(std::move(values)), breakTo(breakTo) {}
6668

6769
Literals values;
6870
Name breakTo; // if non-null, a break is going on
@@ -2997,32 +2999,33 @@ class ModuleRunnerBase : public ExpressionRunner<SubType> {
29972999
Flow visitCall(Call* curr) {
29983000
NOTE_ENTER("Call");
29993001
NOTE_NAME(curr->target);
3002+
Name target = curr->target;
30003003
Literals arguments;
30013004
Flow flow = self()->generateArguments(curr->operands, arguments);
30023005
if (flow.breaking()) {
30033006
return flow;
30043007
}
30053008
auto* func = wasm.getFunction(curr->target);
3006-
Flow ret;
3009+
auto funcType = func->type;
30073010
if (Intrinsics(*self()->getModule()).isCallWithoutEffects(func)) {
30083011
// The call.without.effects intrinsic is a call to an import that actually
30093012
// calls the given function reference that is the final argument.
3010-
auto newArguments = arguments;
3011-
auto target = newArguments.back();
3012-
newArguments.pop_back();
3013-
ret.values = callFunctionInternal(target.getFunc(), newArguments);
3014-
} else if (func->imported()) {
3015-
ret.values = externalInterface->callImport(func, arguments);
3016-
} else {
3017-
ret.values = callFunctionInternal(curr->target, arguments);
3013+
target = arguments.back().getFunc();
3014+
funcType = arguments.back().type.getHeapType();
3015+
arguments.pop_back();
3016+
}
3017+
3018+
if (curr->isReturn) {
3019+
// Return calls are represented by their arguments followed by a reference
3020+
// to the function to be called.
3021+
arguments.push_back(Literal::makeFunc(target, funcType));
3022+
return Flow(RETURN_CALL_FLOW, std::move(arguments));
30183023
}
3024+
3025+
Flow ret = callFunctionInternal(target, arguments);
30193026
#ifdef WASM_INTERPRETER_DEBUG
30203027
std::cout << "(returned to " << scope->function->name << ")\n";
30213028
#endif
3022-
// TODO: make this a proper tail call (return first)
3023-
if (curr->isReturn) {
3024-
ret.breakTo = RETURN_FLOW;
3025-
}
30263029
return ret;
30273030
}
30283031

@@ -3039,18 +3042,28 @@ class ModuleRunnerBase : public ExpressionRunner<SubType> {
30393042
}
30403043

30413044
Index index = target.getSingleValue().geti32();
3042-
Type type = curr->isReturn ? scope->function->getResults() : curr->type;
30433045

30443046
auto info = getTableInterfaceInfo(curr->table);
3045-
Flow ret = info.interface->callTable(
3046-
info.name, index, curr->heapType, arguments, type, *self());
30473047

3048-
// TODO: make this a proper tail call (return first)
30493048
if (curr->isReturn) {
3050-
ret.breakTo = RETURN_FLOW;
3049+
// Return calls are represented by their arguments followed by a reference
3050+
// to the function to be called.
3051+
auto funcref = info.interface->tableLoad(info.name, index);
3052+
if (!Type::isSubType(funcref.type, Type(curr->heapType, NonNullable))) {
3053+
trap("cast failure in call_indirect");
3054+
}
3055+
arguments.push_back(funcref);
3056+
return Flow(RETURN_CALL_FLOW, std::move(arguments));
30513057
}
3058+
3059+
Flow ret = info.interface->callTable(
3060+
info.name, index, curr->heapType, arguments, curr->type, *self());
3061+
#ifdef WASM_INTERPRETER_DEBUG
3062+
std::cout << "(returned to " << scope->function->name << ")\n";
3063+
#endif
30523064
return ret;
30533065
}
3066+
30543067
Flow visitCallRef(CallRef* curr) {
30553068
NOTE_ENTER("CallRef");
30563069
Literals arguments;
@@ -3062,24 +3075,22 @@ class ModuleRunnerBase : public ExpressionRunner<SubType> {
30623075
if (target.breaking()) {
30633076
return target;
30643077
}
3065-
if (target.getSingleValue().isNull()) {
3078+
auto targetRef = target.getSingleValue();
3079+
if (targetRef.isNull()) {
30663080
trap("null target in call_ref");
30673081
}
3068-
Name funcName = target.getSingleValue().getFunc();
3069-
auto* func = wasm.getFunction(funcName);
3070-
Flow ret;
3071-
if (func->imported()) {
3072-
ret.values = externalInterface->callImport(func, arguments);
3073-
} else {
3074-
ret.values = callFunctionInternal(funcName, arguments);
3082+
3083+
if (curr->isReturn) {
3084+
// Return calls are represented by their arguments followed by a reference
3085+
// to the function to be called.
3086+
arguments.push_back(targetRef);
3087+
return Flow(RETURN_CALL_FLOW, std::move(arguments));
30753088
}
3089+
3090+
Flow ret = callFunctionInternal(targetRef.getFunc(), arguments);
30763091
#ifdef WASM_INTERPRETER_DEBUG
30773092
std::cout << "(returned to " << scope->function->name << ")\n";
30783093
#endif
3079-
// TODO: make this a proper tail call (return first)
3080-
if (curr->isReturn) {
3081-
ret.breakTo = RETURN_FLOW;
3082-
}
30833094
return ret;
30843095
}
30853096

@@ -4130,12 +4141,7 @@ class ModuleRunnerBase : public ExpressionRunner<SubType> {
41304141

41314142
// Call a function, starting an invocation.
41324143
Literals callFunction(Name name, const Literals& arguments) {
4133-
auto* func = wasm.getFunction(name);
4134-
if (func->imported()) {
4135-
return externalInterface->callImport(func, arguments);
4136-
}
4137-
4138-
// if the last call ended in a jump up the stack, it might have left stuff
4144+
// If the last call ended in a jump up the stack, it might have left stuff
41394145
// for us to clean up here
41404146
callDepth = 0;
41414147
functionStack.clear();
@@ -4144,47 +4150,79 @@ class ModuleRunnerBase : public ExpressionRunner<SubType> {
41444150

41454151
// Internal function call. Must be public so that callTable implementations
41464152
// can use it (refactor?)
4147-
Literals callFunctionInternal(Name name, const Literals& arguments) {
4153+
Literals callFunctionInternal(Name name, Literals arguments) {
41484154
if (callDepth > maxDepth) {
41494155
externalInterface->trap("stack limit");
41504156
}
4151-
auto previousCallDepth = callDepth;
4152-
callDepth++;
4153-
auto previousFunctionStackSize = functionStack.size();
4154-
functionStack.push_back(name);
41554157

4156-
Function* function = wasm.getFunction(name);
4157-
assert(function);
4158-
FunctionScope scope(function, arguments, *self());
4158+
Flow flow;
4159+
std::optional<Type> resultType;
4160+
4161+
// We may have to call multiple functions in the event of return calls.
4162+
while (true) {
4163+
Function* function = wasm.getFunction(name);
4164+
assert(function);
4165+
4166+
// Return calls can only make the result type more precise.
4167+
if (resultType) {
4168+
assert(Type::isSubType(function->getResults(), *resultType));
4169+
}
4170+
resultType = function->getResults();
4171+
4172+
if (function->imported()) {
4173+
// TODO: Allow imported functions to tail call as well.
4174+
return externalInterface->callImport(function, arguments);
4175+
}
4176+
4177+
auto previousCallDepth = callDepth;
4178+
callDepth++;
4179+
auto previousFunctionStackSize = functionStack.size();
4180+
functionStack.push_back(name);
4181+
4182+
FunctionScope scope(function, arguments, *self());
41594183

41604184
#ifdef WASM_INTERPRETER_DEBUG
4161-
std::cout << "entering " << function->name << "\n with arguments:\n";
4162-
for (unsigned i = 0; i < arguments.size(); ++i) {
4163-
std::cout << " $" << i << ": " << arguments[i] << '\n';
4164-
}
4185+
std::cout << "entering " << function->name << "\n with arguments:\n";
4186+
for (unsigned i = 0; i < arguments.size(); ++i) {
4187+
std::cout << " $" << i << ": " << arguments[i] << '\n';
4188+
}
4189+
#endif
4190+
4191+
flow = self()->visit(function->body);
4192+
4193+
// may decrease more than one, if we jumped up the stack
4194+
callDepth = previousCallDepth;
4195+
// if we jumped up the stack, we also need to pop higher frames
4196+
// TODO can FunctionScope handle this automatically?
4197+
while (functionStack.size() > previousFunctionStackSize) {
4198+
functionStack.pop_back();
4199+
}
4200+
#ifdef WASM_INTERPRETER_DEBUG
4201+
std::cout << "exiting " << function->name << " with " << flow.values
4202+
<< '\n';
41654203
#endif
41664204

4167-
Flow flow = self()->visit(function->body);
4205+
if (flow.breakTo != RETURN_CALL_FLOW) {
4206+
break;
4207+
}
4208+
4209+
// There was a return call, so we need to call the next function before
4210+
// returning to the caller. The flow carries the function arguments and a
4211+
// function reference.
4212+
name = flow.values.back().getFunc();
4213+
flow.values.pop_back();
4214+
arguments = flow.values;
4215+
}
4216+
41684217
// cannot still be breaking, it means we missed our stop
41694218
assert(!flow.breaking() || flow.breakTo == RETURN_FLOW);
41704219
auto type = flow.getType();
4171-
if (!Type::isSubType(type, function->getResults())) {
4172-
std::cerr << "calling " << function->name << " resulted in " << type
4173-
<< " but the function type is " << function->getResults()
4174-
<< '\n';
4220+
if (!Type::isSubType(type, *resultType)) {
4221+
std::cerr << "calling " << name << " resulted in " << type
4222+
<< " but the function type is " << *resultType << '\n';
41754223
WASM_UNREACHABLE("unexpected result type");
41764224
}
4177-
// may decrease more than one, if we jumped up the stack
4178-
callDepth = previousCallDepth;
4179-
// if we jumped up the stack, we also need to pop higher frames
4180-
// TODO can FunctionScope handle this automatically?
4181-
while (functionStack.size() > previousFunctionStackSize) {
4182-
functionStack.pop_back();
4183-
}
4184-
#ifdef WASM_INTERPRETER_DEBUG
4185-
std::cout << "exiting " << function->name << " with " << flow.values
4186-
<< '\n';
4187-
#endif
4225+
41884226
return flow.values;
41894227
}
41904228

src/wasm/wasm.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ namespace wasm {
2525

2626
Name WASM("wasm");
2727
Name RETURN_FLOW("*return:)*");
28+
Name RETURN_CALL_FLOW("*return-call:)*");
2829
Name NONCONSTANT_FLOW("*nonconstant:)*");
2930

3031
namespace BinaryConsts {

0 commit comments

Comments
 (0)