Liveness barriers and finalization

As others have said, the notion of what constitutes an "observation" is intentionally left vague.

IIUC, @felix.s is looking for a fully portable liveness fence that works across all implementations, for all programs. I'm not sure that exists.

It is "easy" to craft such a fence for a given implementation for all programs. If you know what the optimizer considers observable, you can craft a liveness fence directly off that knowledge.

It is much harder, but still possible, I think, to craft such a fence for all implementations for a specific program, for a reasonable definition of observability. This is already getting into underspecification territory, since to be completely precise we must first say what we consider to be observable. My intention was the following.

There are some reasonable assumptions we can make for all implementations in practice: host APIs are observable and stuff that call out to IO are observable. An observability-preserving optimization must ensure that the all executions that can arise from the transformed program do not result in different values being observed or the observability changing, etc. Of course, if we consider FinalizationRegistry callbacks and WeakRef results to be observable, then we can't really do anything. So the conceit here is to pretend they don't exist when reasoning about executions: if, in the process of pretending they don't exist by saying all WeakRefs are always empty, the identity of some object affects the observable results of an execution (formally, I was imagining something like, if changing a reduction step that has an object's identity as input affects the observable outcome of the execution), then I consider that program to be "observing that object's identity". I think this is a fine formal semantics thing to say, it's not very edifying for practitioners. If you've reasoned about the observability of all possible executions of your program pretending WeakRefs don't exist and found out that some object's identity doesn't affect observable outcome, then you can write a liveness fence tailored for that specific object by forcing its identity to affect the observable outcome. IOW, you need to do whole program, all-possible-execution-style analysis.

In practice, that analysis is infeasible and there are shortcuts. It's probably not enough to just write this === {}, you'd need to force it to be observed somehow. Writing it to a global array or something somewhere probably suffices for all implementations in practice, points-to analysis is too hard and imprecise in JS to optimize away those kind of objects.

Finally, I don't know if it's possible to craft a liveness fence for all implementations for all programs. eval and new Function are well-known "have a bad time" deoptimizers, but like, that's just because it's stupid to trade off the analysis effort and time for dynamic code input strings and patch existing code, etc, so nobody makes that tradeoff. But I don't know as a semanticist I can say they always guarantee observability.

The FFI bit is interesting too. ecma262 doesn't say anything about FFIs or observability formally. If your environment has an FFI that makes stuff observable, then it's up to that environment to to flesh out what it considers observable.

Edit: Reading more of the above, new WeakRef(obj) or deref() on an non-empty WeakRef is probably the simplest universal liveness fence in practice, simpler than what I say above in the post. I'm still not sure if it's universal from a pure semantics POV given that changes to [[KeptAlive]] might not be observable if finalizers themselves aren't observable, but in practice, I'm pretty sure nobody is going to be trying to optimize [[KeptAlive]] stuff away.

2 Likes