tc39 / proposal-ptc-syntax

Discussion and specification for an explicit syntactic opt-in for Tail Calls.
http://tc39.github.io/proposal-ptc-syntax/
168 stars 8 forks source link

Reviving proper tail calls? #23

Open pygy opened 6 years ago

pygy commented 6 years ago

Hi all,

as much as some people love to hate Safari ("the new IE", yadda, yadda), I've yet to see a complain about their support for (implicit) proper tail calls in the wild. The debugging problems mentioned in this article seem to be non-issues in practice.

Maybe it is time to revisit the situation?

ljharb commented 6 years ago

Since code using proper tail calls to avoid blowing the stack isn’t web-compatible, and would only work in Safari - which would imply that nobody is shipping it - how would you expect to see complaints about the implementation?

Pauan commented 6 years ago

@ljharb Tail call optimization applies even if the programmer didn't intend for it to apply.

So let's say somebody debugged their code in Safari, and their debugging experience was fine (despite the unintended tail call optimization). Obviously they would not complain.

On the flip side, if somebody is debugging code in Safari and they experience weird behavior (such as call stacks being missing), then they would complain!

So the lack of complaints is (a little bit of) evidence that tail call optimization does not harm the debugging experience, and thus is acceptable to implement in other browsers as well.

pygy commented 6 years ago

I'd argue that, as far as debugging is concerned, accidentally optimized tail calls are the only ones that matter.

People who use them deliberately would not complain, they know what they are doing.

A third case would be someone who relies on a 3rd party library that uses tail calls internally. It may make it harder to debug said lib if it were buggy. If it is a real problem in practice, people will avoid such libs and give them a bad name (I'd be surprised if it happened though.)

pygy commented 6 years ago

Another thing I see raised against proper tail calls is perf issues.

They can be made fast in LuaJIT where the equivalent of

function _while(condition, body) {
  if (condition() && !body()) { // return true in the body to break
    return _while(condition, body);
  }
}

let a = 0;
_while(()=> a < 1000000000, () => {a++});
console.log(a);

is as fast as a

let a = 0;
while (a < 1000000000) a++;
console.log(a);

(and that has been the case since 2010).

What does JS have that makes the code above impossible to optimize?

Edit: FWIW, Safari is almost as good as LuaJIT here.

while 100000000 394 msec
_while() 100000000 526 msec

Putting both benchmarks in the same tick has the plain while loop slower than the functional version.

while 100000000 873
_while() 100000000 503

Edit2: well LuaJIT has both versions at 150 ms, actually :-) But still, the overhead of the functional verison over the plain loop is rather small in Safari.

getify commented 6 years ago

@ljharb as I've pointed out multiple times, including in #4, tail calls are not web-incompatible. I have code shipped in production (that I wrote years ago) using that "adaptive" pattern, which ostensibly is working fine (as far as I know anyway) in non-PTC browsers, and in Safari has "progressively enhanced" to using tail calls.

Whether that pattern is common or not is a separate discussion. It is in fact web compatible and should therefore not be continually dismissed on such grounds.

getify commented 6 years ago

Also, @Pauan is correct here:

Tail call optimization applies even if the programmer didn't intend for it to apply.

There must be tons of code (especially non-recursive) out in the wild that is unintentionally being tail-call'd by Safari. One would have to assume that if tail calls were such a problem (either for performance or debugging), then surely by now enough of a complaint would have been raised in the community as backlash against Safari, we'd certainly know it.

I'm not aware of any such backlash against Safari, and they clearly haven't changed their minds to back it out. More than a year in production strongly suggests the resistance to PTC was at least partially FUD and not grounded in fact.

pygy commented 6 years ago

@getify you phrased it better than I did, and I too think that the resistance is misguided, but I'd avoid throwing arounds terms like "FUD" (which I associate with wilful disinformation), it will not help our cause.

ljharb commented 6 years ago

@getify your code is not "web compatible PTC code". It's specifically web-incompatible PTC code, combined with a fallback mechanism to the true web-compatible code.

By saying PTC is not web compatible, I'm not saying that you can't write code that utilizes PTC. I'm saying that PTC code without a fallback is web incompatible.

The resistance to PTC is not solely based on debugging concerns; there's also the education aspect of implicit tail calls having magic behavior, but also, there's implementation concerns, where at least one of the major browsers is unable to implement PTC at all.

getify commented 6 years ago

@ljharb seems like nitpicking on words. you used "not web compatible" as an excuse to dismiss the possibility that anyone may have deployed PTC code (or something demonstrably like it) and thus that was the reason for "silence" in terms of complaints.

Not only is my code pattern a counter-example that excepts that reasoning, but you also don't account for web code that's affirmatively using PTC (for recursion or otherwise) but is deployed in a safari-only way, such as in web views in iOS apps.

I still maintain your reasoning is faulty, no matter what label we bikeshed on to label it.

pygy commented 6 years ago

@ljharb I'm surprised that the architecture of one of the VMs is incompatible with PTC.

I'm going to suppose it is Edge we're talking about, since Brendand Eich had started working on the Firefox implementation (I assume he had a general idea of the path forward when he wrote the parser patch), and V8 had it done before it was unshipped.

elibarzilay commented 6 years ago

TL;DR: +1, but with some more details:

  1. First of all: I completely agree with the point of this issue. It's even better than splitting the fine hair splittage of what's proper web code or not. The bottom line is that JS code cannot for the most part rely on tail calls for "random web stuff", so the safari move would get developers no real benefits due to portability, but it would get them into the diminished debugging experience that many people are afraid of. The fact that people did not complain is a good indicator that these fears are unfounded.

  2. I have said in the past that of all languages, JS should be extremely ready for having tail call elimination (even under a misguided "PTC" name...) -- a quick way to see this is the fact that JS programmers are notoriously using callback-rich code, as well as being comfortable with tricks like deferring code to a timeout which essentially destroys the stack context it originally has. Yet even JS developers are worried about the bad effects on debugging.

  3. I think that "FUD" is a very peroper term to use here. There are no hard facts about debugging problems so people are definitely left with uncertainty, there is definitely a fear of hurting productivity as a result, and the very existence of the idea in this repo or the pulling-out of tail call elimination from v8 are making it clear that there is no shortage of doubts. It's not FUD that is actively encouraged by some company, but I think that it is FUD.

  4. To make this a bit more concrete: see this reply in that v8 thread as a perfect example of the FUD effect, and later on the same person goes on to conclude that tail calls "kill debugging and profiling". And this is coming from a person who self-identified as someone who's working on DevTools.

    These are exactly the claims that would anticipate some developer backlash against an implementation. Apparently in the safari case such backlash didn't happen, so it looks like "kills debugging and profiling" is much exaggerated.

  5. As a side note, I seriously hope that this proposal for an explicit syntax will die. The thing about tail calls is that they are something that affects control flow, and as such there could be issues that are a result of using some 3rd-party library with higher-order functions (ie, callbacks). The main point of making tail calls implicitly is that in most cases such libraries are fine as they are, without me asking the developers of such libraries to change their code (which, if the explicit syntax is used, will undoubtedly lead to redundant arguments with people who would refuse to make such changes worrying about hurting debugging).

PinkaminaDianePie commented 6 years ago

Can it be implemented vice versa? Use proper TCO by default, and in case of greater need use some explicit keyword/expression/directive what else like debugger, which will deoptimize code and generate low-performance version, but with proper call stack? It would be much better to optimize by default and deoptimize on demand for debugging purposes only. Is it possible or such way also break some compatibility?

"education aspect" just makes me laugh, in that case, we can never add anything to standard because people will need to learn something, we should never create any new tools, libraries, frameworks etc, it will take an effort to learn them! In reality, anyone who will face with "broken" call stack will spend 30 seconds to read first entry from google search and will find solved question on StackOverflow with lots of links, and people who don't like to learn anything will still make mistakes like they do right now with any other language features. "some people won't like to learn something new" - it's totally not a valid reason to take it into consideration.

Only reason which can be valid, is the complexity of implementation in browser engines. But if it's not an issue - all other issues can be solved without introducing explicit keyword "optimize me please".

getify commented 6 years ago

Can it be implemented vice versa?

I've thought about a similar path, too. For example, for any libs that want to collect (error).stack in the background, we could have a static boolean property like Error.stack to opt-in to full stack traces. This isn't really a deopt like stopping tail calls, but just indicating to the engine that the shadow stackframes should be collected. If it's not set, it's up to the engine if they want to collect, but if it's set the engine knows the page's code wants that data if possible so it should collect if it can.

For the open devtools use case, it could automatically set that same flag to trigger the same behavior.

Othreumaru commented 6 years ago

TCO is just an optimization, stacktraces which are smaller in size than the limit of a stack in browsers which is usually few thousand may not be optimized at all, the optimization should imho trigger when the stack reaches this limit. At this point stack would be overflown anyway with exception. Usecases where TCO is useful are when code is running recursion over huge number of function calls something like billions or possibly infinity.

getify commented 6 years ago

@Odrinwhite disagree. PTC (not necessarily TCO) is important even without recursion. Recursion is an obvious and acute use case but certainly not the only one.

I want (in FP) to make multiple wrapping layers of functions on top of functions, preserving parameters at each level, via closure, and I don't want there to be a deeper call stack as a result. In that perspective, function wrapping calls being exposed in the call stack is only extra noise for debugging and is thus a leaky implementation.

Pauan commented 6 years ago

@Odrinwhite The strategy of "wait until the stack overflows and then activate TCO" is used in Chicken Scheme (and other compilers as well).

It is a valid way of implementing TCO, so browsers can use that implementation strategy if they wish.

But that has nothing to do with the spec: the spec does not specify how browsers achieve TCO, it merely mandates that they use some method of achieving TCO.

@getify That can be easily handled by having the browser record which frames are tail-call frames, so it can hide them from the debugger. So there's nothing stopping browsers from implementing the Chicken Scheme style of TCO. I doubt that browsers will actually do that, since there are downsides to it, but they could if they wanted to.

getify commented 6 years ago

@Pauan of course, I don't only care about the debugger output... that was just one convenient illustration of a non-recursion use case.

What I actually want is all those wrapped FP functions to not actually cost function calls. And BTW, "cost" is not as much wanting to save CPU cycles in those calls, but wanting to minimize all the memory/GC churn of all those extra stack frame allocations/deallocations.

IMO the entire theory of FP rests on being able to assume there's no "penalty" (memory or otherwise) for an extra wrapping layer of function. In JS we just whistle along, merrily wrapping all these calls, and just winking and nodding to pretend it's OK... that what is conceptually one function call is actually 9 calls... because the cost of the extra 8 calls is generally still small so who cares?

But as soon as you wire up such a wrapped abstraction as a transducer, and hook that up to a continual stream of data events, each one firing every few hundred milliseconds, and run that on a user's mobile device for hours on end, you start wondering if FP is actually worth all that. memory burn.

I'm just trying ensure it's on record, because it seems all too easily lost/ignored/overlooked in these discussions, that the need for PTC goes well beyond recursion.

You can "solve" the lack of tail call recursion with a trampoline and even a code transform or macro... but you can't prevent all my extra function wrappings in FP from churning memory... unless you give me PTC.

Sorry I'm being so persistent/insistent in this thread. It's frankly quite frustrating to have spent basically 5 years campaigning for this vital feature and yet people still casually discount it as niche or suggest narrow work-arounds that aren't relevant to the broader perspective.

Othreumaru commented 6 years ago

@getify Don't get me wrong I would like to see your version implemented but I think the argument that it breaks the web may be valid here for some people. And not breaking web was a big deal in designing ES6 so my proposition is to get the foot into the doors and allow people write TCO in some code which may not need to be very efficient but still fun to do. And with more people trying and using this style of programming a better or more aggressive solution can be established.

PinkaminaDianePie commented 6 years ago

@Odrinwhite proper TCO doesn't break the web itself. Only some ways of implementation break it, and only in some cases. So instead of rejecting standard, it's better to find other ways of implementation, which will be acceptable. One proposal was to give developer possibility to deoptimize code on demand and see a proper stack trace. We will have proper TCO, developers will be able to check stack trace. Why not to go this way?

Pauan commented 6 years ago

@getify What I actually want is all those wrapped FP functions to not actually cost function calls. And BTW, "cost" is not as much wanting to save CPU cycles in those calls, but wanting to minimize all the memory/GC churn of all those extra stack frame allocations/deallocations.

That is not guaranteed by the spec, the spec only guarantees amortized behavior. In general the spec doesn't say much about performance.

Also, stack allocations/deallocations are not the same as heap allocations: stack allocations are not managed by the garbage collector, and they are generally free (zero-cost). There is no performance penalty to using the stack.

Things get more complicated with JS, because browsers might have shadow stacks, or put additional information onto the stack/heap, etc. But the general principles of stack vs heap still applies.

Also, trying to guarantee no-extra-stack-frames-ever can actually make the performance worse, not better. Performance is complicated.

that what is conceptually one function call is actually 9 calls... because the cost of the extra 8 calls is generally still small so who cares?

That's not how Chicken Scheme works. It doesn't actually pay any performance penalty for the function calls.

It doesn't say "oh we'll create extra stack frames because the cost is low". Instead it says "we do normal function calls without any performance penalty, but then once in a while (when the stack overflows) we have to do garbage collection of the stack (which is slow, but not as slow as heap garbage collection)".

It's similar to how vectors/arrays work: when you insert elements into an array, most of the time it is extremely fast. But sometimes it has to resize the array, which is very slow. The idea is that because the resizing happens very rarely (usually exponentially rarely), it "averages out" to be very fast in practice. The same principle applies to hash tables. Amortized performance is extremely common.

I'm just trying ensure it's on record, because it seems all too easily lost/ignored/overlooked in these discussions, that the need for PTC goes well beyond recursion.

I agree with that: PTC is not merely an optimization, it is a feature of the language which enables new kinds of programs to be written (including, but not limited to: state machines, CPS transforming compilers, new flow control constructs, call-with-continuation, async code, etc.)


@Odrinwhite Don't get me wrong I would like to see your version implemented but I think the argument that it breaks the web may be valid here for some people.

As far as I can tell, TCO/PTC does not break the web, as indicated by Safari. The resistance to PTC is not about breaking the web, it is because PTC makes the JS engines more complicated, and it possibly has some performance penalties as well.

elibarzilay commented 6 years ago

It seems to me that it will be helpful to clarify the purpose of tail call elimination: the main goal is not to reduce runtime, but to reduce memory use --- and the actual stack frames are not the main concern there. When I write this code:

function foo(x) {
  let y = ...;
  return bar();
}

then having PTC means that when bar() is called I want to know that the x and y bindings no longer exists, and can be GCed.

[This is the main thing that Guido missed (or more likely chose to miss) in his shutting down of tail calls --- the example he uses makes it a requirement to keep x and y for debugging. So if the "imperative viewpoint" is to keep this information for the sake of debugging, then the "functional view" would raise an eyebrow and ask why is the old value of x after x = f(x) is not preserved by the same coin. (Or more practically, view saving these values as a debugger de-optimization.)

It's also interesting to see Guido's initial bottom line, "After all TRE only addresses recursion that can easily be replaced by a loop", is something that is clearly questionable to modern JS programmers (for example, trying to convert a tail-recursive promise loop into a while), and that's besides the added fragility of an imperative loop over using tail calls.]

glathoud commented 6 years ago

Here is one possibility for developers to use fast tail calls today, without needing extra keywords in JavaScript: https://github.com/glathoud/fext

kaizhu256 commented 5 years ago

+@douglascrockford

recent jslint versions have 2 tail-call "bugs" where it raises RangeError in certain cases in v8/nodejs/chrome (but not safari) [1], [2]. these bugs were closed by the author as "wont fix" (under assumption bugs will go away when engines eventually fall inline with ptc-spec).

will that assumption happen in the forseeable future? yes/no/maybe? an authoritative answer would provide clarity/guidance to development-roadmap of jslint.

[1] jslint-issue - RangeError when linting excessive leading-newlines https://github.com/douglascrockford/JSLint/pull/244

[2] jslint-issue - RangeError when linting json with large string-values https://github.com/douglascrockford/JSLint/issues/239

kaizhu256 commented 5 years ago

oh also, firefox doesn't seem to have jslint tail-call issues under normal use-cases - because it has a ridiculously large callstack-depth limit (~100,000 vs. chrome's measly ~12,000). you can get these numbers by running the script below in the respective browser's console.

maybe a happy-medium/short-term solution is for v8/chakra to raise their callstack depth-limit to the same level as firefox?

function computeMaxCallStackSize() {
    try {
        return 1 + computeMaxCallStackSize();
    } catch (e) {
        // Call stack overflow
        return 1;
    }
}
computeMaxCallStackSize()
screen shot 2018-11-05 at 12 11 52 am screen shot 2018-11-05 at 12 11 17 am
slikts commented 5 years ago

Why would you use jslint, though.

kaizhu256 commented 5 years ago

@slikts because eslint is too slow on the ultra-portable laptop i use while backpacking. javascript product-development should not follow java's model where the only reason you need powerful laptops (with poor battery-life) is because of bloated-tooling.

also i don't have strong feelings about PTC either way. i'm just unhappy that jslint is currently in a state thats not ideal for production-use, and wish its author and nodejs/tc39 can come to some compromise to resolve that. raising v8's stackcall-limit to firefox's seems like something reasonable to me.

ljharb commented 5 years ago

This repo isn’t the place to complain about, or seek change in, any of those things. jslint’s author by their own admission doesn’t care about web reality, only what’s in the spec; file a bug on Firefox if you want them to implement something; use something other than eslint if you like, just be aware that eslint is the de facto standard for “product-development” in the entire JS community.

While PTC is in the spec, STC (this proposal) is untenable, and there’s nothing of use to discuss here.

kaizhu256 commented 5 years ago

@ljharb, my understanding is this thread is about PTC (not STC), and i'm bringing up a PTC issue in the wild.

ljharb commented 5 years ago

The thread is also not appropriate for this repo, since this entire repo is about removing PTC and replacing it with STC.

You’ll have to to take it up with each engine that has chosen not to ship PTC if you want PTC “revived”, or with the one engine that has if you want it removed.

kaizhu256 commented 5 years ago

fyi, i did file a related-bug with v8-team back in september (https://bugs.chromium.org/p/v8/issues/detail?id=8234). they gave it a low-priority, with no owner currently assigned.

ljharb commented 5 years ago

Sounds like you have your answer, then.

littledan commented 5 years ago

To answer @kaizhu256 's question in https://github.com/tc39/proposal-ptc-syntax/issues/23#issuecomment-435680890 , V8 has previously stated its position on tail calls clearly in https://v8.dev/blog/modern-javascript#proper-tail-calls , and I haven't heard any reason to believe that this logic doesn't continue to hold. I think the most likely way towards agreement across engines would be something along the lines of the proposal in this repository, rather than implicit tail calls.

kaizhu256 commented 5 years ago

thx @littledan. that's the kind of clarity i'm seeking

pygy commented 5 years ago

@littledan @ljharb this thread is actually a rebuttal of the v8 article you link to, and a plea to drop the STC proposal in favour of PTC.

PTC are implemented in Safari, and literally no one complains about them from a debugging standpoint. That article was posturing as "but think of the Web developer" from a team that was either completely wrong about what actual Web devs think, or doesn't want to implement something for other reasons.

Provided that Safari is widely deployed and compatible it is also pretty obvious that shipping PTC in a UA doesn't Break the Web™.

mathiasbynens commented 5 years ago

@pygy I’m going to quote your last comment, and emphasize part of it:

That article was posturing as "but think of the Web developer" from a team that was either completely wrong about what actual Web devs think, or doesn't want to implement something for other reasons.

As the particular section of the article that @littledan linked to states, V8 actually implemented both PTC and STC behind different flags.

getify commented 5 years ago

IIUC, v8 ended up backing out their PTC implementation in larger part because it seemed clear that this situation was not going to be resolved any time soon (been years now). It seemed less like they had strong objections to PTC the way Firefox and Edge did, given that they had a working implementation even if some there didn't like it.

The effect of Chrome not shipping PTC was basically to reduce the pressure to resolve the impasse between Safari and Firefox/Edge. Hence, this unfortunate indefinite standoff.

getify commented 5 years ago

And just for clarity, @littledan @ljharb, this thread is actually about PTC, not STC. There's another repo/thread about STC, and I think it's been confused with this repo/thread.

ljharb commented 5 years ago

@getify this repo's tagline says "Discussion and specification for an explicit syntactic opt-in for Tail Calls. " - it's definitely for STC. PTC is in the spec, and thus es-discuss, or ecma262, is the place to discuss it.

edit: this comment is no longer relevant since the above comment was edited

getify commented 5 years ago

@ljharb this repo is called "Proposal PTC Syntax" (which I guess later came to be called STC), but this thread, which is where we're currently discussing, is unquestionably about PTC.

pygy commented 5 years ago

@mathiasbynens thanks for chiming in. I know it used to ship behind a flag and was removed and I imagined it could have clashed with TurboFan. So Chrome unshipped it to avoid putting pressure on the Mozilla and Edge teams?

Implementations issues have been raised upthread, but without mentioning engines. By elimination, my guess was that Edge was probably problematic, but I wasn't sure.

I'm actually surprised FF has issues with it provided Brendan Eich had started the implementation, at least at the parser level. Was he the only champion for that feature at Mozilla? Pinging @annevk :-)

concavelenz commented 5 years ago

Today, PTC would have never made it into the spec as there would not be concensus to put it in. But the same process currently prevents it from being removed as it is designed to favor inaction.

concavelenz commented 5 years ago

I am curious, those advocating for PTC. Do you collect client side error reports? I did an analysis of a few applications and found the % of all calls that would be converted to tail calls ranged from 10-20%. With that it is entirely possible your entire callstack could be erased, leaving you with no clue as to why your clients are failing.

Shadow stacks don't help here because these things aren't running with devtools enabled. The same is true of Node applications using stack traces for post mortem debugging.

That functions/libraries/files don't opt-in in anyway is the problem, as it affect all code everywhere, not just the select places that actually benefit from a deeper/infinite stack. But we can't have explicit tail calls because the spec has PTC...

pygy commented 5 years ago

That's a good point @concavelenz.

I've re-read the README of this proposal, which actually exposes the various implementation woes for Firefox and Chakra, I had forgotten about the details.

https://github.com/Microsoft/ChakraCore/issues/796 make things even more clear clear as far as Chakra is concerned. To properly implement PTC (per spec and without perf loss), they'd have to change their calling conventions, which represents significant work.

The cross-realm issues in Firefox bother me a lot less, since they could be spec'ed as a special case where TC are not optimized.

The more I look at it, the more STC make actual sense, but I'm still worried the calling conventions/perf compromise will still be present in Chakra and that we'll end up with functions with STC that perform poorly, either when doing a tail call or when being called from non-tail position...

getify commented 5 years ago

As I've mentioned various times throughout these threads, the spec does not require the engine to throw away all frames, only that the stack not grow faster than O(1) magnitude.

One way they could explore doing tail calls would be to keep the first 5 (or 10 or whatever) frames, and then a rolling queue of the last 5/10/etc frames, effectively throwing away the middle frames. I would imagine the vast majority of bugs could be identified from a stack trace that has the first frames and the last frames, as the problem is far more likely there than in the middle.

The point is, the concerns about the idea of tail calls (regardless of what syntax or not is used) are valid but also addressable using various tricks/strategies. A sufficiently motivated engine has more than enough opportunities there.

If there was no way to debug or error track on tail-call-eliminated tail-call-stack-frame-elided programs, tail calls wouldn't be "a thing" in any language or compiler.

edit: I didn't mean "tail-call-eliminated", that was my mistake. I meant "tail-call-stack-frame-elided". Sorry.

PinkaminaDianePie commented 5 years ago

Nice double standards, really.

concavelenz commented 5 years ago

@pygy The important thing is that with syntax, you don't regress performance everywhere.

@getify Tail-call-elimination isn't a problem. Tail-call-elimination is very different that PTC. Tail call elimination, is transforming self recursive functions into loops and that doesn't cause debugging/stack frame issues. PTC says that if the return value isn't consumed by the function, it is removed from the stack, this is a PTC:

function f() { return g(); }

This is not:

function f() { return 1 + g(); }

If tricks were sufficient, wouldn't you expect other major languages to have adopted them? Which major performant languages have these attributes? Go, Rust, Swift, C++, Java?

These languages may eliminate stack frames due to function inlining or TCO but to my knowledge they don't have PTC guarantees.

concavelenz commented 5 years ago

@PinkaminaDiane I agree PTC is in an unfortunate state. It should be removed from the standard as it would have never reached the requirements for a Stage 4 proposal (two implementations). This is a bug in the process: it is basically stuck in limbo until either Apple or the other 3 major vendors change their minds.

The committee while producing ES2015, as an outside observer anyway, was able to side step keep issues because Error objects "stack" attribute was not part of the spec and as such they didn't feel the need to address the interaction between PTC and the stack traces it produced.

Pauan commented 5 years ago

@concavelenz The drawbacks of missing stack frames also applies to self-recursive tail calls, and it also applies to function inlining, yet JS engines handle stack traces correctly even with inlined functions.

Also, the strategies @getify mentioned work equally well with full PTC. They have been used in many languages, including various Schemes, Common Lisp, Haskell, F#, OCaml, etc.

It's a logical fallacy to claim that "these languages don't implement PTC, therefore it must be because PTC is bad".

The reason Go, Rust, Swift, C++, and Java don't have PTC isn't because PTC is bad, it's because of their history, and because they're not functional languages (and thus don't have much need for PTC).

Rust in particular considered the idea of adding in PTC, but postponed it because it made the FFI with C a lot worse, and it didn't offer much benefit for Rust, since Rust isn't a functional language.

There are legitimate drawbacks with PTC, but debugging is not one of them, because there are well-known implementation strategies that avoid those drawbacks (just like how there are well-known strategies to recover a proper stack trace even after inlining functions).

There are actual experts who have worked on Racket who have made it clear that PTC is not a problem for debugging (and I agree with them).

I do agree that this stalemate is not beneficial for anybody, so something must be changed (either all the browsers implement PTC, or STC gets accepted, or PTC is removed from the spec).

pygy commented 5 years ago

@concavelenz there doesn't have to be a perf penalty to all calls. Safari is still plenty fast (look at https://github.com/tc39/proposal-ptc-syntax/issues/23#issuecomment-386919365 which compares while loops with a functional _while(conditionFn, bodyFn) functional equivalent).

Having PTC fast and per spec would add extra work for the Chakra team (Michael Saboff spent one month changing calling conventions in Safari, and Brian Terlson estimated it would take several months of work to update ChakraCore).

The STC proposal leaves the possibility to implement TCO as a poorly performant second thought, which means that it will not let one write fast code with tail calls, making them a curiosity. TCO is especially suited for writing algorithms, and those lend themselves well to become libraries. But no one is going to use them if there are faster alternatives (or cue in an imperative rewrite as soon as a given lib becomes popular). The fact that PTC has stalled makes me worry that tail calls are not being taken seriously by implementers and will be neglected even if they are implemented as STC.

As @Pauan mentioned, the one argument against PTC-friendly calling conventions is that they are not compatible with the C ABI, which adds complexity if you want to write a FFI. Not sure how relevant that is for JS which is sandboxed anyway.

Perf is not a fundamental issue (even though reworking Chakra to switch calling conventions would probably be tedious). Debugging is not an issue. Cross-realm issues have already almost have been spec'ed away.

@dilijev on Feb 27, you said:

The Chakra team is continuing to track PTC and the associated proposals and community discussion since it is an unimplemented spec feature, and we will continue to prioritize PTC against other work items.

Is it still accurate? Is there any chance that work on actual PTC could resume or is Microsoft officially favouring STC?

concavelenz commented 5 years ago

Function inlining however is static and you can recreate stack traces. I expect TCO lose stack information (I haven't looked if optimizers try to recreate it, it doesn't seem worth while) , but it doesn't lose how you got there. The loss of stack information may prevent various VMs/optimizers from doing TCO (I don't really know). PTC, however, can be dynamic you have to record something to create a stack trace and that is the problem.

With regard to the performance penalty, I agree it seems like it shouldn't be that way, but it is for Microsoft. They know what they are doing and why they don't want to do PTC. V8, as far as I understand, didn't have any performance issues with PTC.