Awesome
Reference (ref
) declarations and expressions for ECMAScript
This proposal defines new syntax to allow for the declaration and creation of user-defined references to bindings.
Motivations
- Decorators cannot refer to block-scoped declarations that follow them.
- Simplifies capturing references to bindings in one scope and handing it to another scope.
Prior Art
Proposal
This proposal introduces three main concepts:
- Reference expressions (e.g.
let r = ref x
) - Reference declarations (e.g.
let ref y = r
, orfunction f(ref y) { }
) Reference
objects
Reference expressions
A Reference expression is a prefix unary expression that creates a Reference
object that defines a binding to its operand.
Reference expressions have the following semantics:
- The operand must be a valid simple assignment target.
- When the operand is a property accessor using dot notation, the target of the property accessor is evaluated immediately
and a
Reference
object is created for the actual property access. - When the operand is a property accessor using bracket notation, the target and the expression of the accessor are evaluated
immediately and a
Reference
object is created for the actual property access. - When the operand is an identifier, a
Reference
object is created for the binding. - A Reference expression is unambiguously a reference to a binding. Host engines can leverage this fact to optimize reference passing.
This behavior can be illustrated by the following syntactic conversion:
const x = ref y;
is roughly identical in its behavior to:
const x = Object.freeze({
__proto__: Reference.prototype,
get value() { return y; },
set value(_) { y = _; }
});
Reference declarations
A Reference declaration is the declaration of a parameter or variable that dereferences a Reference
, creating a binding
in the current scope.
Reference declarations have the following semantics:
- A Reference declaration is unambiguously a dereference of some Reference expression. Host engines can leverage this fact to optimize away
the
Reference
object if they can statically determine that the only use-sites are arguments to call expressions whose parameters are declaredref
. - A
ref x
parameter introduces a mutable binding to the underlyingReference
.- Reading from
x
reads the value of the underlyingReference
. - Assigning to
x
assigns to the value of the underlyingReference
.
- Reading from
- A
let ref x
declaration introduces a mutable binding to theReference
supplied as the initializer.- Reading from
x
reads the value of the underlyingReference
. - Assigning to
x
assigns to the value of the underlyingReference
.
- Reading from
- A
const ref x
declaration introduces an immutable binding.- Reading from
x
reads the value of the underlyingReference
. - Assigning to
x
is an error. - Taking a
ref
ofx
will result in an immutableReference
.
- Reading from
The behavior of a reference declaration can be illustrated by the following syntactic conversion:
function f(ref y) {
y = 1;
}
let ref x1 = someRef;
x1 = 1;
const ref x2 = someRef;
console.log(x2);
is roughly identical in its behavior to:
function f(ref_y) {
ref_y.value = 1;
}
let ref_x1 = someRef;
ref_x1.value = 1;
const ref_x2 = ((someRef) => Object.freeze({
__proto__: Reference.prototype,
get value() { return someRef.value; }
}))(someRef);
console.log(ref_x2.value);
Reference
objects
A Reference
object is a reified reference that contains a value
property that can be used to read from and write to a reference.
Reference objects have the following shape:
interface Reference<T> {
value: T;
[Symbol.toStringTag]: "Reference";
}
Examples
Take a reference to a variable:
let x = 1;
const r = ref x;
print(r.value); // 1
r.value = 2;
print(x); // 2;
Take a reference to a property:
let o = { x: 1 };
const r = ref o.x;
print(r.value); // 1
r.value = 2;
print(o); // { x: 2 }
Take a reference to an element:
let ar = [1];
const r = ref ar[0];
print(r.value); // 1
r.value = 2;
print(ar); // [2]
Object Binding Patterns:
let o = { x: 1 };
const { ref x } = o;
// or:
// const { x: ref x } = 0;
print(x.value); // 1
x.value = 2;
print(o); // { x: 2 }
Array Binding Patterns:
// NOTE: If an Array Binding Pattern has a `ref` declaration, array indexing is used
// instead of Symbol.iterator and rest elements are not allowed.
let ar = [1];
const [ref r] = ar;
print(r.value); // 1
r.value = 2;
print(ar); // [2]
Dereferencing:
// dereference a binding
let x = 1;
let ref y = ref x; // 'y' effectively points to 'x'
print(y); // 1
y = 2;
print(x); // 2
Dereferencing a non-Reference (other than undefined
) is a ReferenceError:
let x = 1;
let ref y = x; // TypeError: Value is not a Reference.
Dereferencing undefined
is ok, but accessing its value is a ReferenceError (typeof
can still be used to test the reference):
function f(ref y) {
typeof y; // ok, type is 'undefined'
y; // ReferenceError: y is not defined.
}
f(undefined); // ok
let x;
function g(ref y = ref x) {}
g(); // ok, parameter initialization will check whether the *argument* is undefined, not the binding.
Dereferencing an immutable Reference into a mutable Reference does not make it mutable:
let x = 1;
const ref y = ref x; // ok, `x` is mutable
let ref z = ref y; // ok, but `z` is actually immutable
z = 2; // error
Reference passing:
function update(ref r) {
r = 2;
}
let x = 1;
update(ref x);
print(x); // 2
Referencing a local declaration creates a closure:
function f() {
let x = 1;
return [ref x, () => print(x)];
}
const [r, p] = f();
print(r.value); // 1
r.value = 2;
p(); // 2
Combining reference expressions, reference parameters, and reference variables:
function max(ref first, ref second, ref third) {
const ref max = first > second ? ref first : ref second;
return max > third ? ref max : ref third;
}
let x = 1, y = 2, z = 3;
let ref w = max(ref x, ref y, ref z);
w = 4;
print(x); // 1
print(y); // 2
print(z); // 4
Forward reference to a block-scoped variable and TDZ:
let ref a_ = ref a; // ok, no error from TDZ
let a = 1;
let ref b_ = ref b;
b_ = 1; // error due to TDZ
let b;
Forward reference to member of block-scoped variable:
let ref b_ = ref b.x; // error, TDZ for `b`
let b = { x: 1 };
Forward reference to var
:
let ref d_ = ref d; // ok, no TDZ
d_ = 2; // ok
var d = 1;
Forward references for decorators:
class Node {
@Type(ref Container) // ok, no error due to TDZ
get parent() { /*...*/ }
@Type(ref Node)
get nextSibling() { /*...*/ }
}
class Container extends Node {
@Type(ref Node)
get firstChild() { /*...*/ }
}
Side effects:
let count = 0;
let e = [0, 1, 2];
let ref e_ = ref e[count++]; // `count++` evaluated when reference is taken.
print(e_); // 0
print(e_); // 0
print(count); // 1
Grammar
UpdateExpression[Yield, Await]:
`ref` LeftHandSideExpression[?Yield, ?Await]
RefBinding[Yield, Await]:
`ref` BindingIdentifier[?Yield, ?Await]
LexicalBinding[In, Yield, Await]:
RefBinding[?Yield, ?Await] Initializer[?In, ?Yield, ?Await]?
VariableDeclaration[In, Yield, Await]:
RefBinding[?Yield, ?Await] Initializer[?In, ?Yield, ?Await]?
ForBinding[Yield, Await]:
RefBinding[?Yield, ?Await]
SingleNameBinding[Yield, ?Await]:
RefBinding[?Yield, ?Await] Initializer[+In, ?Yield, ?Await]?
Desugaring
The following is an approximate desugaring for this proposal:
// proposed syntax
function max(ref first, ref second, ref third) {
const ref max = first > second ? ref first : ref second;
return max > third ? ref max : ref third;
}
let x = 1, y = 2, z = 3;
let ref w = max(ref x, ref y, ref z);
w = 4;
print(x); // 1
print(y); // 2
print(z); // 4
// desugaring
const __ref = (get, set) => Object.freeze(Object.create(null, { value: { get, set } }));
function max(ref_first, ref_second, ref_third) {
const ref_max = ref_first.value > ref_second.value ? ref_first : ref_second;
return ref_max.value > ref_third.value ? ref_max : ref_third;
}
let x = 1, y = 2, z = 3;
const ref_x = __ref(() => x, _ => x = _);
const ref_y = __ref(() => y, _ => y = _);
const ref_z = __ref(() => z, _ => z = _);
const ref_w = max(ref_x, ref_y, ref_z);
ref_w.value = 4;
print(x); // 1
print(y); // 2
print(z); // 4
And here's the same example using an array:
// proposed syntax
function max(ref first, ref second, ref third) {
const ref max = first > second ? ref first : ref second;
return max > third ? ref max : ref third;
}
// arrays
let ar = [1, 2, 3];
let ref w = max(ref ar[0], ref ar[1], ref ar[2]);
w = 4;
print(ar[0]); // 1;
print(ar[1]); // 2;
print(ar[2]); // 4;
// desugaring
const __ref = (get, set) => Object.freeze(Object.create(null, { value: { get, set } }));
const __elemRef = (o, p) => __ref(() => o[p], _ => o[p] = _);
function max(ref_first, ref_second, ref_third) {
const ref_max = ref_first.value > ref_second.value ? ref_first : ref_second;
return ref_max.value > ref_third.value ? ref_max : ref_third;
}
let ar = [1, 2, 3];
const ref_ar0 = __elemRef(ar, 0);
const ref_ar1 = __elemRef(ar, 1);
const ref_ar2 = __elemRef(ar, 2);
const ref_w = max(ref_x, ref_y, ref_z);
ref_w.value = 4;
print(ar[0]); // 1;
print(ar[1]); // 2;
print(ar[2]); // 4;
Here's an example using private names:
// proposed syntax
class C {
#counter = 0;
get count() { return this.#counter; }
provideCounter(cb) {
cb(ref this.#counter);
}
}
function increment(ref counter) {
counter++;
}
const c = new C();
c.provideCounter(increment);
c.provideCounter(increment);
print(c.count); // 2
// desugared
const __ref = (get, set) => Object.freeze(Object.create(null, { value: { get, set } }));
class C {
#counter = 0;
get count() { return this.#counter; }
provideCounter(cb) {
cb(__ref(() => this.#counter, _ => this.#counter = _));
}
}
function increment(ref_counter) {
ref_counter.value++;
}
const c = new C();
c.provideCounter(increment);
c.provideCounter(increment);
print(c.count); // 2
Future Considerations
We may want to make it possible to revoke a reference, for example:
let a = 1;
let { ref reference: b, revoke } = Reference.revocable(ref a);
b = 2;
console.log(a); // 2
revoke();
b = 3; // ReferenceError
However, it may be possible to do this in userland (though engines may not be able to optimize away a userland type):
function revocableReference(ref_value) {
const reference = Object.create(Reference.prototype, {
value: {
get() {
if (ref_value === null) throw new ReferenceError();
return ref_value.value;
},
set(v) {
if (ref_value === null) throw new ReferenceError();
ref_value.value = v;
}
}
});
function revoke() {
ref_value = null;
}
return { reference, revoke };
}
let a = 1;
let { ref reference: b, revoke } = revocableReference(ref a);
b = 2;
console.log(a); // 2
revoke();
b = 3; // ReferenceError