Closed eholk closed 4 years ago
I still don't see how this can fly. Modules aren't isolated. There are imports and exports of functions, and there will be function references in the future. In both cases, what would it mean to use a function from one module in another module with a different convention value? You would have to know under which convention the function was defined originally, so AFAICS, you couldn't get around tracking this information in the function type.
The easy option for imports and exports is just to fail instantiation if you import a function from a module with a different calling convention. A better option would be to generate a trampoline for functions imported with a mismatched calling convention, although the trampoline will eliminate the benefits of whatever convention you're using.
For function references (assuming they are just function pointers and don't include an environment), you could make a table indexed by calling convention, where each entry is either a pointer to a function in its native calling convention, or a trampoline to adjust the calling convention. At the call site, you'd just call through the correct index for the calling convention you expect.
I do like this direction, as I feel that it is important whatever design we end with allows programs that don't use tail calls at all to have zero overhead from their existence, because all information regarding tail calls is known statically.
I actually think failing instantiation is better than trampoline insertion. The latter can mask bugs in codegen or what function you were intending to call, and it can have unexpected performance effects.
Yea, having some kind of metadata on tail-call-ability is important for making sure non-tail-calls take absolutely no performance or semantic hits. In fact, I think we don't want this metadata at the module level because a set of functions A
that use no tail calls in the same module as another set of functions B
that do use tail calls should be completely identical to the same set A
being placed in a module with no such tail calls. Not having this property could be very unintuitive; for instance, (as I understand it) one of the reasons we don't want tail-call-ability on all functions is that it won't work with the stdcall ABI that Windows / Edge wants for wasm functions, since the stdcall ABI supports certain debugging features. I don't think we want functions to lose these debugging features just because some completely unrelated function in the same module performs a tail-call.
TL;DR: I think we want tail metadata, but only on the function level, not the module level.
@eholk, imports are individual functions, not modules. So I still don't see how a module flag helps, you would need to mark individual functions. When you pass around function references as anyfunc or put them in a table, you couln't even check until you try to call them.
@rossberg, the engine can tell where the functions come from though. V8 does calls from Wasm-to-Wasm differently than Wasm-to-JS. In the Wasm-to-Wasm case we'd have to add another step to check the calling convention of the module we're importing from and make sure they match.
For tables, if the table is only used by one instance, then you could check the calling convention at the point you go to insert a reference into the table. Although admittedly this sounds equivalent to just saying the type of the table is not anyfunc but anyfunc
@ElvishJerricco:
TL;DR: I think we want tail metadata, but only on the function level, not the module level.
Having metadata on the function level sounds to me like putting tail call information in the function signature, which we've been talking about on #1.
The point I was trying to raise here is whether we could do something at the module level that would get us enough of the benefits with significantly less complexity.
@eholk, it sounds like you're saying that the calling convention effectively would be part of the function's type except that we don't spell it out there but do a dynamic type check for it. Clearly, that's all you would be able to do for first-class functions (table elements or other references) in the general case. That seems strictly worse than making it part of the static function type and checking it properly.
I think we can close this one, too.
I've been thinking more about @flagxor's suggestion to add a module-level bit that specifies functions in a given module should be compiled to support tail calls.
At first I was somewhat skeptical of this idea, because I interpreted it as a custom section that is not part of the core spec that includes tail calling just one of many optimization hints. For example, we've also tossed around the idea of having tools provide register allocation hints that could be freely ignored but could be used to do better register allocation at codegen time based on analysis the tools do at their leisure. To me, tail calls are a matter of correctness and not merely optimization, so I'm uncomfortable making this a section that can be ignored.
However, thinking of it as a section that lists calling convention requirements makes more sense to me. It's common for compilers to offer several different calling conventions. For example, LLVM lists support for 12 different conventions currently. Some of these conventions support certain features, while others don't. If we imagine Wasm impementations also supporting a number of conventions, we could the module specify which features it needs from the calling convention. Wasm engines would then choose their best calling convention that meets the requirements.
I'm not convinced yet that this is the way to go (I'd still prefer tail calling always be supported), but I think this is an idea that's worth exploring.
Some questions: