# Tag Variable Hoisting ## Scoping Rules ### Tag Scope Tag Variables follow similar scoping rules to JavaScript’s `let` and `const`, but instead of [being _block-scoped_](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/block#block_scoping_rules_with_let_const_class_or_function_declaration_in_strict_mode), Tag Variables `<let>` and `<const>` are _tag-scoped_. #### Redeclaring A Tag Variable cannot be declared more than once per scope: ```htmlembedded= <let/x = 0/> <let/x = 1/> // SyntaxError: 'x' has already been declared in the current scope <const/x = 1/> // Same error as above ``` #### Shadowing Like JavaScript’s block-level variables, Tag Variables can be “shadowed” in child tags: you can reuse an existing variable name in a deeper scope. If we have the same variable name outside and inside a tag, the variable _inside_ the tag takes priority over the outer variable: ```htmlembedded= <let/x = 0/> <div> <div> <let/x = 1/> ${x} // 1 </div> ${x} // 0 </div> ``` #### Preceding References If a variable name is used before its Tag Variable’s declaration in a `<let>` or `<const>`, it is considered a _preceding reference_. A _preceding reference_ cannot be read during the render phase. Attempting to do so will throw an error: ```htmlembedded= <div> ${x} // ReferenceError: 'x' cannot be read during render (preceding reference) </div> <let/x = 0/> ``` ### Hoisted References Outside of the tag they are defined in, Tag Variables are not accessible under normal scoping rules. Referencing a variable from outside the tag it’s defined in is considered a _hoisted reference_. ```htmlembedded= <div> <span/el/> </div> <effect() { el().textContent = "Hello"; }/> ``` Like _preceding references_, _hoisted references_ cannot be read during the render phase. Attempting to do so will throw an error: ``` ReferenceError: 'el' cannot be read during render (hoisted reference) ``` #### Shadowing If a Tag Variable shadows another Tag Variable, the inner variable will _never_ have a hoisted reference. It's a sort of reverse shadowing for hoisting purposes. ```htmlembedded= <div> <span/el> <button/el/> </span> </div> <effect() { el(); // HTMLSpanElement, not HTMLButtonElement }/> ``` #### Conditional References If a hoisted reference refers to a Tag Variable that is not rendered, it will resolve to `undefined`. ```htmlembedded= <if=test> <span/el/> </if> <effect() { el?.().textContent = "Hello"; }/> ``` #### Dynamic References Standard tag-scoped references are unambiguous: the closest scope wins and a variable cannot be redeclared in a scope. However, with hoisted references, all references have the same specificity: they will refer to whatever Tag Variable is rendered. ```htmlembedded= <if=test> <span/el/> </if> <else> <button/el/> </else> <effect() { el().textContent = "Hello"; }/> ``` #### Ambiguous References However, if more than one Tag Variable for a binding is rendered, it is considered an _ambiguous reference_. ```htmlembedded= <div> <span/el/> <effect() { el; // HTMLSpanElement }/> </div> <div> <button/el/> <effect() { el; // HTMLButtonElement }/> </div> <effect() { el; // ReferenceError: 'el' is an ambiguous reference }/> ``` Even if one Tag Variable is "further" than another, it is considered ambiguous, because all hoisted references have the same specificity. ```htmlembedded= <div> <span/el/> </div> <div> <div> <button/el/> </div> </div> <effect() { el; // ReferenceError: 'el' is an ambiguous reference }/> ``` This ambiguity holds if hoisting from a scope that renders multiple times: ```htmlembedded= <for|item| of=["a", "b", "c"]> <span/el>${item}</span> </for> <effect() { el; // ReferenceError: 'el' is an ambiguous reference }/> ``` ## Implementation - If there are hoisted references to a variable, it will be hoisted to a scope where it can be reached by all references. - Hoisted references will use a `hoistedRead` helper that will throw if called during render. ## Rules ### When should something be hoisted? 1. From a tag variable, walk up the tree. 2. If a local definition is found, the variable is **not** hoistable. 3. Otherwise, the variable is available everywhere in the template. #### Runtime implications - When hoisting, hoist _just enough_ to capture all references. (This is an optimization; conceptually it is hoisted to the root.) ### How are references resolved? 1. If a local definition is discovered while walking up the tree, always use that. 2. If there is a no definition, should read a hoisted reference. #### Runtime implications - In dev-mode only, when reading a hoisted reference, all reads should be wrapped in a function that: - Ensures there is only one declaration currently writing to the hoisted variable. - Ensures that the read happens outside of the render phase. - A workaround for transferring a hoisted variable to another tag in the render phase: wrap it in a closure that reads it _after_ the render phase, like `<some-tag ref=() => el()>`. ## Examples ### (1) ```htmlembedded <div> <const/x=1/> </div> <div> ${x} </div> ``` Output: ```htmlembedded <div></div> <div>1</div> ``` ### (2) ```htmlembedded <const/x=1/> <div> <const/x=2/> ${x} </div> ${x} ``` Output: ```htmlembedded <div>2</div> 1 ``` ### (3) ```htmlembedded <div> <const/x=1/> ${x} </div> ${x} <const/x=2/> ``` Output: ```htmlembedded <div>1</div> 2 ``` ### (4) ```htmlembedded ${x} <div> <if=true> <const/x=1/> </if> </div> ``` Output: ```htmlembedded 1 <div></div> ``` <details> - A tag variable is available everywhere within a template. - A **standard reference** is a reference to a tag variable that precedes the reference and is owned by a direct parent tag of the reference. ```htmlembedded <let/x = 0/> <div>${x}</div> ``` - A **hoisted reference** is a reference to a tag variable that comes _after_ the reference, or is _not_ owned by a direct parent tag of the reference. **A hoisted reference cannot be read during render.** ```htmlembedded <effect() { console.log(x); }/> <let/x = 0/> ``` ```htmlembedded <div> <let/x = 0/> </div> <div> <effect() { console.log(x); }/> </div> ``` - A tag variable can be shadowed in tags directly above or below them. ```htmlembedded ${x /* HIGH */} <div> <let/x = "HIGH"/> ${x /* HIGH */} <div> ${x /* LOW (ReferenceError: read preceding reference in render) */} <let/x = "LOW"/> ${x /* LOW */} <div> ${x /* LOW */} </div> </div> </div> ``` ```htmlembedded <div> ${x /* HIGH (Error: read hoisted in render) */} <div> <let/x = "LOW"/> </div> </div> <let/x = "HIGH"/> ``` ```htmlembedded <div> <let/x = "HIGH"/> </div> <div> ${x /* HIGH & LOW (Error: read hoisted reference in render) */} <effect() { x /* HIGH & LOW (Error: ambiguous reference) */ /> <div> <let/x = "LOW"/> </div> ${x /* HIGH & LOW (Error: read hoisted in render) */} </div> ``` </details>