Return type annotations


#1

Section 5.2 and Section 6.5.5 both give some meaning for return statements. Section 5.2 tells us that the last return statement in a function MUST be annotated or be NumericLiteralish (the -ish because of const), whereas section 6.5.5 says that any other return statement needs only for EvaluateExpression to give a subtype of the formal return type. Is there any reason why not allow any EvaluateExpression that gives a subtype of signed, double, or float for the last return statement as well? Personally, I would find that a lot more attractive.

Thinking more, I could see the argument that having formal return type defined in this way would make it easier to throw good error messages (at a return statement that doesn’t match the formal return type) without having to first evaluate the entire formal return statement. So with that, I guess loosening the formal return statement doesn’t look as appealing. Still, I would be interested in hearing the actual reasoning.


#2

That’s a great question. The reasoning behind this peculiarity is that, for the first half year, asm.js didn’t require callsite coercions (f()|0). Instead, the spec implicitly required you to take make passes over the module: one to collect signatures, and one to type-check bodies. Only with the signatures collected could you type check call expressions. In this context, you have a cyclic problem if deriving the return type requires type checking an expression which can include calls to the functions whose signatures you are trying to determine. Of course, when we added callsite coercions (to avoid the two-pass requirement so that we could parse and discard ASTs function-at-a-time) there ceased to be any benefit to having a limited-form final-return-statement.

So, getting to your question: I agree we should drop the special syntactic requirements on the final return statement. I’ll give others a chance to weigh in, though.


#3

I am +1 for dropping the special syntactic requirement for final return statement (there is no backward compat issue as well)


#4

Ah yes, I remember this sequence of events now. Thanks for the historical reminder :slight_smile:

This makes sense to me. From a spec perspective, I think we could change it to just assert the existence of a return type for the function without saying where that type came from, and then require each return argument expression to typecheck to a subtype of that type (math is allowed to just pull objects out of thin air). IIUC, an actual algorithm would have to collect the synthesized types of all return arguments in the function body in an accumulated list and take their least upper bound to determine the function’s formal return type. Do I have that right?

Dave


#5

Yes, at least for Odin. (Technically, we don’t accumulate a list, but rather a Maybe<RetType>, but in spirit that’s what we’re doing.)


#6

/me nods

Thanks!

Dave


#7

FWIW, landed in FF38 (currently Nightly).


#8

How is this related with allowing proper tail calls in the future? When all callsite are required to have coercions, this would ban PTCs from asm.js, which would be really bad.

Now with relaxing the requirements for return statements and collecting all return arguments to form the least upper bound as suggested here, is it doable to allow non-coercing callsites in tail position as long as there are enough coerced ones to deduce a proper upper least bound?


#9

This change simply loosens the coercion required by a return, but does not relax the coercions already required for callsites (such that, an int-returning function call must always by immediately coerced with f()|0), so it is not possible to express an int- or double-returning tail-call atm.

Regardless, the important missing piece of work would be the actual implementation of PTC which is no small piece of work. At that point, though, we could indeed extend the asm.js subset to drop the callsite-coercion requirement when the return type is implied by a prior return, as you’ve suggested.


#10

So you mean that after extending the asm.js subset, the way to code tail calls would be

function f() {
  if (0) return 0;
  return f();
}

while

function f() {
  return f();
  return 0;
}

would not be allowed to allow for a linear scan of the function. Did I get it right?

As to actually implementing proper tail calls, one needs this change to the asm.js grammar before it makes sense to code it. Do PTC will have to be implemented in general in SpiderMonkey first or would their implementation in OdinMonkey be independent (because the asm.js pass collects enough information to detect all calls in tail position)? I am willing to help out by doing some coding and targeting OdinMonkey first sounds like a more manageable piece of work.


#11

The former would be my default but obviously it’s a little goofy so we could consider variations on the latter. One would hope for cases of simple recursion that there would actually be a base-case return before the PTC so the if(0) return wouldn’t be necessary in usual cases.

Thanks for your offer to help! One way to start out would be to implement the tail-call optimization for almost-tail-position calls in asm.js (e.g., “f() { return +f() }”) and, based on this experience (in particular, I’m eager to see that we don’t need to penalize non-tail-call performance), we could propose an asm.js extension as a new topic on specifiction.

I’m happy to discuss PTC impl in Odin, but probably this isn’t the right forum for Odin implementation discussion; could you file a bug instead?


#12

I can file a bug so that the OdinMonkey related discussion isn’t happening here. But I think there are three things one has to keep in mind:

  1. Whether or whether not a particular implementation will penalize non tail-call performance (which shouldn’t be the case if done properly), any asm.js compile will eventually have to support PTC (when allowed by the asm.js language subset) because a general ES6 will have to.

  2. Optimizing pseudo tail calls like return +f() by just one engine wouldn’t help application programmers/compiler writers a lot as a ES6-compliant implementation does not have to do a tail call here, so they can’t rely on it.

  3. The most simple cases of proper tail calls like self-recursion or mutual recursion are not very interesting for compilers targeting asm.js as these use cases can easily be rewritten in an imperative, non-recursive way. Being able to output a form of continuation passing style would, however, be very helpful for compilers or even byte-code interpreters.


#13

Yes, of course; I was suggesting “return +f()” as an incremental step. I actually expect PTC won’t negatively impact non-tail call perf, but it’s possible I’m forgetting an impl constraint.