tc39 / ecma262

Status, process, and documents for ECMA-262
https://tc39.es/ecma262/
Other
15.03k stars 1.28k forks source link

Purpose of [[TemplateMap]] in Realm Record. #2742

Open dSalieri opened 2 years ago

dSalieri commented 2 years ago

You know if you look at the definition of realm you will see following:

Before it is evaluated, all ECMAScript code must be associated with a realm. Conceptually, a realm consists of a set of intrinsic objects, an ECMAScript global environment, all of the ECMAScript code that is loaded within the scope of that global environment, and other associated state and resources.

A realm is represented in this specification as a Realm Record with the fields specified in Table 27

That's all.

Well, there is no definitions about special fields. But if we look at the table 27 we will see something...

Since, I am interested in [[TemplateMap]], I will ask about it. There is "meaning" in the table, but table don't tell you about her purpose, it says about structure of [[TemplateMap]], that's it, and there are no somewhere words about purpose of [[TemplateMap]]. Hence you don't understand purpose, if you're not maintainer of ecma262. So my question is "What is [[TemplateMap]] actual, why does the specification need the [[TemplateMap]]?"

P.S [[TemplateMap]] is the only thing you can't figure out by looking at the spec in the realm. Would like to see an explanation not only here but also in the specification.

Jack-Works commented 2 years ago

It is some magic behavior of the tagged template strings.

https://tc39.es/ecma262/multipage/ecmascript-language-expressions.html#sec-gettemplateobject

Jack-Works commented 2 years ago

image

dSalieri commented 2 years ago

@Jack-Works sure, I saw that link that you showed. Your example is very interesting, but this is not still answer to my question about purpose and needing of [[TemplateMap]]. I want to understand sense of this concept.

bathos commented 2 years ago

It allows caching results (of e.g. parsers of other languages) in association with the template literal parse node. Template tag functions can perform work related to the literal segments just once, with subsequent evaluations only needing to perform work related to the substitution values. This is what makes them "templates".

dSalieri commented 2 years ago

@bathos In other words, [[TemplateMap]] caches parts of template literals as the arguments so that when the same syntax node is called again, the template literals is not parsed. That is, it is an optimization. So? Is it a micro-optimization or redundant, what do you think?

I'm asking this because I'm trying to understand the expediency of such a decision. I see that [[TemplateMap]] was created back in ES6 and this concept has not changed. Is it really enough bottleneck where productivity seriously falls?

ljharb commented 2 years ago

It enables template tag functions to cache results without having to create a key based on every argument, so yes.

bathos commented 2 years ago

That is, it is an optimization.

It can be, but it's affording an identity associated with the parse node and this enables more than just performance optimizations. It is a kind of language extension point. I would recommend studying the source code of a library like lit-html to understand what it enables.

devsnek commented 2 years ago

concretely it allows you to write something like

const cache = new Map();
function myCoolTag(strings, ...values) {
  if (!cache.has(strings)) {
    cache.set(strings, computeComplexLogic(strings));
  }
  return cache.get(strings)(values);
}

myCoolTag`whatever ${1} stuff`;

the identify of the array strings is always the same for each spec parse node, so you can very efficiently parametrize the values.

dSalieri commented 2 years ago

@devsnek I am afraid, that your example won't work because each new execution of expression

myCoolTag`whatever ${1} stuff`;

will create new record in a Map.

Look at the folowing code:

function tag(){
    return arguments[0];
}
tag`123` === tag`123`; /// false

but

function tag(){
    return arguments[0];
}
function tagA(){
    return tag`123`
}
tagA() === tagA(); /// true

So it should be the same parse Node, otherwise you won't see optimization.

devsnek commented 2 years ago

@dSalieri you can think of it as the source location, here's an example: https://engine262.js.org/#gist=74ea9624bb9f61a9b1c42f7c26ce9417

dSalieri commented 2 years ago

@dSalieri you can think of it as the source location, here's an example: https://engine262.js.org/#gist=74ea9624bb9f61a9b1c42f7c26ce9417

That's it what I meant :)

bathos commented 2 years ago

@dSalieri It sounds like you’ve got the idea. The final expression in the example devsnek gave is the part of the source text with its “identity” exposed to ES code as the identity of a frozen array. The same array will be received by the tag function each time a specific tagged template expression (i.e., not just same-text but same “physical” source text location) is evaluated. This becomes apparent if such an expression appears within a function called multiple times or within the body of a for-loop, etc.

Historically these arrays were keyed by the specific combination of string values, not the parse nodes... ...which led to problems (e.g. for GC); this was also less rather than more powerful in a strict sense because key-by-strings can be polyfilled, but key-by-parse-node is a unique capability afforded only by this syntax. Nonetheless, the change [did lead to problems](https://webreflection.medium.com/a-tiny-disastrous-ecmascript-change-fadc05c83e69) as it was made after the original semantics had been around a while. The author of that article, @webreflection, has written a number of other articles about template tags and their usage and has studied the problem space deeply + authored several libraries that would be instructive, so I believe that taken together, those materials would be a great resource to explore next if you’re looking to understand how it this gets leveraged in practice more.

Earlier I suggested checking out lit-html as an example. Here is a tiny demo I made just now with the intention of illustrating aspects that are likely to be interesting. In the “updates” log panel there:

In this HTML example, keying by identity is more than optimization because the identities of the elements being created matter. The elements can be stateful in various ways beyond what’s controlled and updated by the template itself. For example there may be associated CSS animation timelines, elements may have focus, form controls may be dirty, iframes may be loading child browsing contexts, video and audio media may have a current playback position, etc. The unique identities of the elements that may carry this independent state “emerge” from the composite “weak keys” which took the parse node as the key’s “root.” Recreating those elements on every evaluation wouldn’t just be slower — it wouldn’t achieve the same thing at all.

(There may be and probably are better terms for these things that I just don’t know, btw.)

Broadly you could say that if not mapping the parse node identity to a frozen array identity, “template tags” wouldn’t be able to meaningfully “template” anything except strings (which is what untagged templates provide). The unique syntax used for template tags is itself part of this: if there were no template array being persistently associated with the parse node, there’d be no need for the special syntax, either, and they could as easily have just been regular function calls().

dSalieri commented 2 years ago

@bathos you've written:

Recreating those elements on every evaluation wouldn’t just be slower — it wouldn’t achieve the same thing at all.

Hmm. How do we get different results with the same template literals but different parse nodes?

bathos commented 2 years ago

How do we get different results with the same template literals but different parse nodes?

I’m not 100% sure what “the same template literals” means here, but since you’re contrasting it with the same-parse-node behavior, I’m guessing you’re asking about how one might make tag below prepare one template based on [ "a", "b" ] in (1) and but then reuse that same template in (2) based on the fact that the second array’s contents are also [ "a", "b" ], despite the expressions themselves being different parse nodes?

tag`a${ 1 }b`; // 1
tag`a${ 2 }b`: // 2

The “folded” portion of my last response mentioned that it was once true that the template arrays here would have shared identity, but they don’t anymore. String-based identity remains attainable though because string identities are undeniable/forgeable. In general this isn’t terribly expensive, either, but YMMV depending on what kinds of templates they are. It could look something like this:

let state = { __proto__: null };
let kTemplate = Symbol();
let tag = (strings, ...subs) => {
  let terminus = strings.reduce((state, string) => state[string] ??= { __proto__: null }, state);
  terminus[kTemplate] ??= createSomeKindOfTemplateThingOnceForThisSequenceOfStrings(strings);

  // Assume the cSKOTTOFTSOS function here does the “parsing” or equiv prep and returns a new function
  // that expects values that will be slotted into a specific result “instance” somehow. The cSKOTTOFTSOS
  // function will run for (1), but for (2), the prior result will already be there. In both cases at this
  // point the `state` object will look like `{ a: { b: { [kTemplate]: fn } } }` and `terminus === state.a.b`.

  return terminus[kTemplate](subs);
};

But note that this state could continue growing indefinitely — which is the exact problem the original behavior faced. There’s no way to know when a forgeable primitive like a string is “collectable”, so in turn you don’t know if the prepared template is collectable, either. Not ideal.

(This may also help clarify what ljharb meant by “It enables template tag functions to cache results without having to create a key based on every argument” if that hadn’t already clicked before.)

dSalieri commented 2 years ago

@bathos sorry, I should have made my question more precise. Let's do it again. You wrote:

Recreating those elements on every evaluation wouldn’t just be slower — it wouldn’t achieve the same thing at all.

I asked the question:

How do we get different results with the same template literals but different parse nodes?

Under that I mean following:

function tag(){
    return arguments[0];
}
/// (1)         (2)
tag`123` === tag`123`; /// false
/// (1),(2): same template literals but different parse nodes

How can I get different results by equal template literals? Your quote: it wouldn’t achieve the same thing at all.

bathos commented 2 years ago

How do we get different results with the same template literals but different parse nodes?

You wouldn't if "result" here means the template string array itself. The template string arrays will be the same, but this isn't the end result.

I think I see what might be throwing you off though as the example I gave creates two "instances". The "template" (result of the initial parsing step) is what is keyed by the template array. The template in turn holds a WeakMap that maps host nodes to "live" instances which the template spawns. This means the final 'instances' are keyed by unique pairs of identities: one template string array plus one "rendering target" host node. This is how you end up parsing once (template array identity) but rendering two unique instances (template array + host node). Roughly, anyway. I'm eliding some (many, actually!) details and only meant to get the gist of what's in play across. This is one particular example/pattern out of many that are possible, and although I think it provides a solid illustration, there's a risk that overemphasizing aspects of this that are more unique to the needs of HTML templating could end up making things more confusing.

dSalieri commented 2 years ago

@bathos excuse me for repeating myself, but could you demonstrate your words?

Recreating those elements on every evaluation wouldn’t just be slower — it wouldn’t achieve the same thing at all.

Could I look at this behaviour practically? I want to see confirmation of your words.

P.S All that you've written upper I read.

bathos commented 2 years ago

I provided a glitch demo earlier that illustrates interpolated-values-only updates where the static portions of the template string (corresponding to an HTML element in the example) are likewise static, i.e. their identity is preserved across every update. If you poke around in the console there maybe it’ll be more evident what I meant? For example you could run a CSS animation on them: if the elements were being recreated/replaced every time it updated, then the animation would also reset every second instead of continuing to run. Perhaps I’m misunderstanding your question, not sure.

dSalieri commented 2 years ago

@bathos yeah, I saw your demonstration that based on caching the same parse node. The thing is what I asked to show visa versa behaviour of template literals when recreating elements wouldn't achieve the same thing

if the elements were being recreated/replaced every time it updated, then the animation would also reset every second instead of continuing to run

Well, it is not necessary to recreate the element, it is enough to mutate it. Your example does just that.

I think I kind of understand you.