Search code examples
javascriptecmascript-6javascript-objectsv8

Are internal slot and internal methods actually implemented by JavaScript engines?


I was reading ECMA2019 (the same is true in ES6 too), where I found:

Each object in an ECMAScript engine is associated with a set of internal methods that defines its runtime behaviour. These internal methods are not part of the ECMAScript language. They are defined by this specification purely for expository purposes. However, each object within an implementation of ECMAScript must behave as specified by the internal methods associated with it. The exact manner in which this is accomplished is determined by the implementation.

I also found these Stack Overflow question1 and question2 and that their answers don't seem to give me the answer I am looking for.

My question is simple. If JavaScript engines decide not to implement some of them, then how would they ensure this statement of above spec -

However, each object within an implementation of ECMAScript must behave as specified by the internal methods associated with it.

Let us take an example:

[[GetPrototypeOf]] , [[Get]] , [[Set]] , [[GetOwnProperty]] etc are essential internal methods. If a JavaScript engine refuses to implement them, how does it achieve this functionality? Clearly they have to implement it, just that they can choose to have different method name and different method signature as it is not enforced by spec on them?

Where am I wrong?

Similarly for internal slots too? If they don't have internal variables storing that state, how on earth will they maintain the state of that object when asked?

EDIT : I will add more details to clarify my question. Let us take an example of Object.getPrototypeOf(). This is an API for internal behaviour [[GetPrototypeOf]] and there are possible algorithm for implementing it. The question is not possible ways to implement it a behaviour - its about having a behaviour or not ! and still satisfying the spec overall object behaviour.


Solution

  • V8 developer here. I think this question has mostly been answered already in the comments, so I'll just summarize.

    Are internal slot and internal methods actually implemented by JavaScript engines?

    Generally not; the engine simply behaves as if its internals were structured in this way. Some parts of an implementation might be very close to the spec's structure, if it's convenient.

    One way to phrase it would be: you could implement a JavaScript engine by first faithfully translating the spec text to code (in whichever language you choose to use for your engine), and then you'd be allowed to refactor the invisible internals in any way you want (e.g.: inline functions, or split them up, or organize them as a helper class, or add a fast path or a cache, or generally turn the code inside out, etc). Which isn't surprising, really: as long as the observable behavior remains the same, any program is allowed to refactor its internals. What the ECMAScript is making clear at that point is simply that the "internal slots" really are guaranteed to always be internal and not observable.

    [[[Get]] etc] are essential internal methods. If a JavaScript engine refuses to implement them, how does it achieve this functionality?

    It's not about refusing to implement something. You can usually implement functionality in many different ways, i.e. with many different ways of structuring your code and your objects. Engines are free to structure their code and objects any way they want, as long as the resulting observable behavior is as specified.

    Let us take an example of Object.getPrototypeOf(). This is an API for internal behaviour [[GetPrototypeOf]]

    Not quite. Object.getPrototypeOf is a public function that's specified to behave in a certain way. The way the spec describes it is that it must *behave as if there were an internal slot [[GetPrototypeOf]].

    You seem to have trouble imagining an alternative way. Well, in many cases, engines will probably choose to have an implementation that's very close to having those internal slots -- perhaps mapped to fields and methods in a C++ class. But it doesn't have to be that way; for example, instead of class methods, there could be free functions: GetPrototypeImpl(internal::Object object) rather than internal::Object::GetPrototypeImpl(). Or instead of an inheritance/hierarchy structure, the engine could use switch-statements over types.

    One of the most common ways in which engines' implementations deviate from the structure defined by the spec's internal slots is by having additional fast paths. Typically, a fast path performs a few checks to see if it is applicable, and then does the simple, common case; if the applicability check fails, it falls back to a slower, more complete implementation, that might be much closer to the spec's structure. Or maybe neither function on its own contains the complete spec'ed behavior: you could have GetPrototypeFromRegularObject and GetPrototypeFromProxy plus a wrapper dispatching to the right one, and those all together behave like the spec's hypothetical system of having a [[GetPrototypeOf]] slot on both proxies and regular objects. All of that is perfectly okay because from the outside you can't see a difference in behavior -- all you can see is Object.getPrototypeOf.

    One particular example of a fast path is a compiler. If you implemented object behaviors as (private) methods, and loaded and called those methods every time, then your implementation would be extremely slow. Modern engines compile JavaScript functions to bytecode or even machine code, and that code will behave as if you had loaded and called an internal function with the given behavior, but it (usually) will not actually call any such functions. For example, optimized code for an array[index] access should only be a few machine instructions (type check, bounds check, memory load), there should be no call to a [[Get]] involved.

    Another very common example is object types. The spec typically uses wording like "if the object has a [[StringData]] internal slot, then ..."; an engine typically replaces that with "if the object's type is what I've chosen for representing strings internally, then ...". Again, the difference is not observable from the outside: Strings behave as if they had a [[StringData]] internal slot, but (in V8 at least) they don't have such a slot, they simply have an appropriate object type that identifies them as strings, and objects with string type know where their character payload is, they don't need any special slot for that.

    Edit: forgot to mention: see also https://v8.dev/blog/understanding-ecmascript-part-1 for another way to explain it.