Quasis: assignable substitutions?

# Axel Rauschmayer (14 years ago)

harmony:quasis

re_matchfoo (${=x}\d+) bar

How do these work? Are the changes that re_match makes written back to x? I can image a solution if substitutions were given to the handler as an array, but not if they are given as trailing parameters.

# Mike Samuel (14 years ago)

2011/6/16 Axel Rauschmayer <axel at rauschma.de>:

harmony:quasis

re_matchfoo (${=x}\d+) bar

How do these work? Are the changes that re_match makes written back to x? I can image a solution if substitutions were given to the handler as an array, but not if they are given as trailing parameters.

Thanks!

These were somewhat controversial when I first presented. So the proposal as written does not include them, but the semantics are specified as an optional extra that the committee could consider.

You may have noticed this somewhat strange production.

//SubstitutionModifier// ::
    ε

The alternative syntax and desugaring detailed at strawman:quasis-substitutions-slot redefines

//SubstitutionModifier// ::

    ε
    =

and redefines the SVE of ${=x.y} to be

 function () { return arguments.length ? (x.y = arguments[0]) : x.y }

so instead of passing substitution values, for read-only substitutions (those without the = modifier) you get a thunk, and for writable substitutions you get a function that returns the value when called with 0 arguments or assigns the first parameter if passed one.

# Axel Rauschmayer (14 years ago)

and redefines the SVE of ${=x.y} to be

function () { return arguments.length ? (x.y = arguments[0]) : x.y }

so instead of passing substitution values, for read-only substitutions (those without the = modifier) you get a thunk, and for writable substitutions you get a function that returns the value when called with 0 arguments or assigns the first parameter if passed one.

Ah, nice. I would have handed in an array of substitutions, let the handler change an array element and then performed an assignment such as x.y = substs[0].

The above solution is better, because the desugaring is local.

Axel

# Mike Samuel (14 years ago)

2011/6/16 Axel Rauschmayer <axel at rauschma.de>:

and redefines the SVE of ${=x.y} to be

function () { return arguments.length ? (x.y = arguments[0]) : x.y }

so instead of passing substitution values, for read-only substitutions (those without the = modifier) you get a thunk, and for writable substitutions you get a function that returns the value when called with 0 arguments or assigns the first parameter if passed one.

Ah, nice. I would have handed in an array of substitutions, let the handler change an array element and then performed an assignment such as    x.y = substs[0].

The above solution is better, because the desugaring is local.

It also allows for multiple evaluation of substitutions which might be useful for looping constructs in the template use-case.

# Axel Rauschmayer (14 years ago)

and redefines the SVE of ${=x.y} to be

function () { return arguments.length ? (x.y = arguments[0]) : x.y }

so instead of passing substitution values, for read-only substitutions (those without the = modifier) you get a thunk, and for writable substitutions you get a function that returns the value when called with 0 arguments or assigns the first parameter if passed one.

It also allows for multiple evaluation of substitutions which might be useful for looping constructs in the template use-case.

Yes, useful. I assume that if the expression is not assignable, trying to use the function as a setter will produce an exception(?)

# Mike Samuel (14 years ago)

2011/6/17 Axel Rauschmayer <axel at rauschma.de>:

and redefines the SVE of ${=x.y} to be

function () { return arguments.length ? (x.y = arguments[0]) : x.y }

so instead of passing substitution values, for read-only substitutions

(those without the = modifier) you get a thunk, and for writable

substitutions you get a function that returns the value when called

with 0 arguments or assigns the first parameter if passed one.

It also allows for multiple evaluation of substitutions which might be useful for looping constructs in the template use-case.

Yes, useful. I assume that if the expression is not assignable, trying to use the function as a setter will produce an exception(?)

That's a good point. It should but does not currently.

# Mark S. Miller (14 years ago)

On Fri, Jun 17, 2011 at 12:18 PM, Mike Samuel <mikesamuel at gmail.com> wrote:

2011/6/17 Axel Rauschmayer <axel at rauschma.de>:

and redefines the SVE of ${=x.y} to be

function () { return arguments.length ? (x.y = arguments[0]) : x.y }

so instead of passing substitution values, for read-only substitutions

(those without the = modifier) you get a thunk, and for writable

substitutions you get a function that returns the value when called

with 0 arguments or assigns the first parameter if passed one.

It also allows for multiple evaluation of substitutions which might be useful for looping constructs in the template use-case.

Yes, useful. I assume that if the expression is not assignable, trying to use the function as a setter will produce an exception(?)

That's a good point. It should but does not currently.

A failed strict assignment throws, and therefore a failed ES-next assignment does. So are you sure it does not currently throw? From your expansion, I'd expect it does.

# Axel Rauschmayer (14 years ago)

Syntax error (in some cases)?

# Mark S. Miller (14 years ago)

On Fri, Jun 17, 2011 at 12:46 PM, Axel Rauschmayer <axel at rauschma.de> wrote:

Syntax error (in some cases)?

Yes:

const x = 8;
foo`${=x}`

expands to

const x = 8;
foo(callSiteId, function() { ... x = arguments[0] ... })

throws an early SyntaxError

# Mike Samuel (14 years ago)

2011/6/17 Mark S. Miller <erights at google.com>:

A failed strict assignment throws, and therefore a failed ES-next assignment does. So are you sure it does not currently throw? From your expansion, I'd expect it does.

The SVE of ${x} in the slot expansion variant is

function() { return x; }

which silently fails to assign to x when passed a value.

# Mark S. Miller (14 years ago)

On Fri, Jun 17, 2011 at 1:50 PM, Mike Samuel <mikesamuel at gmail.com> wrote:

2011/6/17 Mark S. Miller <erights at google.com>:

A failed strict assignment throws, and therefore a failed ES-next assignment does. So are you sure it does not currently throw? From your expansion, I'd expect it does.

The SVE of ${x} in the slot expansion variant is

function() { return x; }

which silently fails to assign to x when passed a value.

I see. I think I misunderstood the question.

In any case, I'd like to propose an alternate slotification you and I have talked about:

"${x}" continue to expand to "x", as in the current not-slotified

proposal.

"${=x}" expand to

ice9({
    get: function() { return x; },
    set: function(newVal) { x = newVal; }
})

where ice9 freezes the record, the functions within the record, and the prototypes of those functions, since syntactic expansions should not introduce new implicit communications channels. But the point here isn't ice9.

This makes ${x} more different from ${=x} which I know you wanted to avoid. But it gives ${x}, which is by far the typical case, a simpler semantics and a cheaper implementation. With the typical case made this cheap, the atypical ${=x} can afford the extra allocations of a more natural object representation. (And one that's compatible with the property descriptor of an accessor property.)

# Mike Samuel (14 years ago)

2011/6/17 Mark S. Miller <erights at google.com>:

On Fri, Jun 17, 2011 at 1:50 PM, Mike Samuel <mikesamuel at gmail.com> wrote:

2011/6/17 Mark S. Miller <erights at google.com>:

A failed strict assignment throws, and therefore a failed ES-next assignment does. So are you sure it does not currently throw? From your expansion, I'd expect it does.

The SVE of ${x} in the slot expansion variant is

function() { return x; }

which silently fails to assign to x when passed a value.

I see. I think I misunderstood the question. In any case, I'd like to propose an alternate slotification you and I have talked about:     "${x}" continue to expand to "x", as in the current not-slotified proposal.     "${=x}" expand to     ice9({         get: function() { return x; },         set: function(newVal) { x = newVal; }     }) where ice9 freezes the record, the functions within the record, and the prototypes of those functions, since syntactic expansions should not introduce new implicit communications channels. But the point here isn't ice9.

Shouldn't ice9 enumerate all reachable properties from its starting point and then delete them all?

# Mark S. Miller (14 years ago)

On Fri, Jun 17, 2011 at 2:42 PM, Mike Samuel <mikesamuel at gmail.com> wrote:

2011/6/17 Mark S. Miller <erights at google.com>:

On Fri, Jun 17, 2011 at 1:50 PM, Mike Samuel <mikesamuel at gmail.com> wrote:

2011/6/17 Mark S. Miller <erights at google.com>:

A failed strict assignment throws, and therefore a failed ES-next assignment does. So are you sure it does not currently throw? From your expansion,

I'd expect it does.

The SVE of ${x} in the slot expansion variant is

function() { return x; }

which silently fails to assign to x when passed a value.

I see. I think I misunderstood the question. In any case, I'd like to propose an alternate slotification you and I have talked about: "${x}" continue to expand to "x", as in the current not-slotified proposal. "${=x}" expand to ice9({ get: function() { return x; }, set: function(newVal) { x = newVal; } }) where ice9 freezes the record, the functions within the record, and the prototypes of those functions, since syntactic expansions should not introduce new implicit communications channels. But the point here isn't ice9.

Shouldn't ice9 enumerate all reachable properties from its starting point and then delete them all?

Why? What do you have in mind?

All I was thinking was the equivalent of:

Given function freezeFunc(func) { Object.freeze(func.prototype); return Object.freeze(func); } then Object.freeze({ get: freezeFunc(function() { return x; }), set: freezeFunc(function(newVal) { x = newVal; }) })

had we const functions or #-functions, this wouldn't be so hard to say.

# Mike Samuel (14 years ago)

2011/6/17 Mark S. Miller <erights at google.com>:

On Fri, Jun 17, 2011 at 2:42 PM, Mike Samuel <mikesamuel at gmail.com> wrote:

Shouldn't ice9 enumerate all reachable properties from its starting point and then delete them all?

Why? What do you have in mind? All I was thinking was the equivalent of:

In some Pacino movie there was a virus named ice-9 in tribute to Vonnegut. It was supposed to be able to wipe every machine in the world, even those not networked.

# Mark S. Miller (14 years ago)

On Fri, Jun 17, 2011 at 6:46 PM, Mike Samuel <mikesamuel at gmail.com> wrote:

2011/6/17 Mark S. Miller <erights at google.com>:

On Fri, Jun 17, 2011 at 2:42 PM, Mike Samuel <mikesamuel at gmail.com> wrote:

Shouldn't ice9 enumerate all reachable properties from its starting point and then delete them all?

Why? What do you have in mind? All I was thinking was the equivalent of:

In some Pacino movie there was a virus named ice-9 in tribute to Vonnegut. It was supposed to be able to wipe every machine in the world, even those not networked.

Gotcha. No, I was just referring to Vonnegut directly, not by way of Pacino.

# Mike Samuel (14 years ago)

2011/6/17 Mark S. Miller <erights at google.com>:

On Fri, Jun 17, 2011 at 1:50 PM, Mike Samuel <mikesamuel at gmail.com> wrote:

2011/6/17 Mark S. Miller <erights at google.com>:

A failed strict assignment throws, and therefore a failed ES-next assignment does. So are you sure it does not currently throw? From your expansion, I'd expect it does.

The SVE of ${x} in the slot expansion variant is

function() { return x; }

which silently fails to assign to x when passed a value.

I see. I think I misunderstood the question. In any case, I'd like to propose an alternate slotification you and I have talked about:     "${x}" continue to expand to "x", as in the current not-slotified proposal.     "${=x}" expand to     ice9({         get: function() { return x; },         set: function(newVal) { x = newVal; }     }) where ice9 freezes the record, the functions within the record, and the prototypes of those functions, since syntactic expansions should not introduce new implicit communications channels. But the point here isn't ice9.

So ice9 unpacks property descriptors to freeze the getter and setter? Is that what you mean by "the functions within the record?"

This makes ${x} more different from ${=x} which I know you wanted to avoid. But it gives ${x}, which is by far the typical case, a simpler semantics and a cheaper implementation. With the typical case made this cheap, the atypical ${=x} can afford the extra allocations of a more natural object representation. (And one that's compatible with the property descriptor of an accessor property.)

What is the advantage of this representation of a writable slot?

What idiom should quasi handler authors use to distinguish a writable slot from a read-only slot?

# Mark S. Miller (14 years ago)

On Sat, Jun 18, 2011 at 10:53 AM, Mike Samuel <mikesamuel at gmail.com> wrote:

2011/6/17 Mark S. Miller <erights at google.com>:

[...]

"${x}" continue to expand to "x", as in the current not-slotified

proposal. "${=x}" expand to ice9({ get: function() { return x; }, set: function(newVal) { x = newVal; } }) where ice9 freezes the record, the functions within the record, and the prototypes of those functions, since syntactic expansions should not introduce new implicit communications channels. But the point here isn't ice9.

So ice9 unpacks property descriptors to freeze the getter and setter? Is that what you mean by "the functions within the record?"

The functions above are not actually the getters and setters of any actual property. They're just the values of the 'get' and 'set' properties of the record. Forget ice9. If we had < strawman:const_functions> I would

have just written

Object.freeze({
  get: const() { return x; },
  set: const(newVal) { x = newVal; }
})

#-functions or lambdas would be even better, but we don't have any of these yet. Speaking of lambdas, even after prohibiting "arguments" in the x position, there's a remaining TCP problem with "this". I think the expansion should really be:

Object.freeze({
  get: Object.freeze(const() { return x; }.bind(this)),
  set: Object.freeze(const(newVal) { x = newVal; }.bind(this))
})

Since we don't have const functions, consider this just a specification ploy, in the same manner that we qualify the above by saying "As if using the original values of Object.freeze and Function.prototype.bind.

Since the only TCP problems in JS Expressions are "this" and "arguments", prohibiting "arguments" and binding "this" takes care of all TCP problems. (If we extend JS to allow statements within expressions, then we'll need to revisit this issue. Indeed, I would argue that we should only allow statements in expressions if we chose lambda over arrow functions, in order to avoid multiplying TCP problems. But that's an argument for another thread.)

The point about getters/setters is only that the record itself has the form of (a subset of) the property descriptor of an accessor property, leading to some useful idioms:

Given a quasiliteral position where a slot is expected, perhaps

re`(${=x.y}\w)`

to assign the matching capture text to x.y, one could instead do

re`(${Object.getOwnPropertyDescriptor(x, 'y')}\w)`

which is not only more verbose, it only works if x.y is an accessor property. Ok, maybe this idiom isn't all that useful ;).

This makes ${x} more different from ${=x} which I know you wanted to avoid. But it gives ${x}, which is by far the typical case, a simpler semantics and a cheaper implementation. With the typical case made this cheap, the atypical ${=x} can afford the extra allocations of a more natural object representation. (And one that's compatible with the property descriptor of an accessor property.)

What is the advantage of this representation of a writable slot?

It reifies the ability to read vs the ability to write into two separate methods.

FWIW, it also plays well with the property descriptor representation of accessor properties. But the above example suggests that this second point may indeed not be worth much.

What idiom should quasi handler authors use to distinguish a writable slot from a read-only slot?

The expansion only creates expressions that evaluate immediately to values, or to read-write slots. If you want to pass a read only slot in a position where a quasi expects a read-write slot, you could do it manually. Ignoring freezing issues:

re`(${ { get: function() { return x.y; }.bind(this) } }\w)`

If there's actually a real need for read-only slots in quasis, then the absence of syntactic support for this third case is a significant criticism of my suggested variation of your slotification strawman. Is there?

# Mike Samuel (14 years ago)

2011/6/18 Mark S. Miller <erights at google.com>:

On Sat, Jun 18, 2011 at 10:53 AM, Mike Samuel <mikesamuel at gmail.com> wrote:

2011/6/17 Mark S. Miller <erights at google.com>:

[...]

"${x}" continue to expand to "x", as in the current not-slotified proposal.     "${=x}" expand to     ice9({         get: function() { return x; },         set: function(newVal) { x = newVal; }     }) where ice9 freezes the record, the functions within the record, and the prototypes of those functions, since syntactic expansions should not introduce new implicit communications channels. But the point here isn't ice9.

So ice9 unpacks property descriptors to freeze the getter and setter? Is that what you mean by "the functions within the record?"

The functions above are not actually the getters and setters of any actual property. They're just the values of the 'get' and 'set' properties of the record. Forget ice9. If we had strawman:const_functions I would have just written     Object.freeze({       get: const() { return x; },       set: const(newVal) { x = newVal; }     })

My mistake. I assumed that they were property descriptors because I saw the contextually-reserved keyword "get" and didn't notice there was no actual property name after it.

What is the advantage of this representation of a writable slot?

It reifies the ability to read vs the ability to write into two separate methods. FWIW, it also plays well with the property descriptor representation of accessor properties. But the above example suggests that this second point may indeed not be worth much.

Ok, and those methods can be dealt with as separate capabilities.

What idiom should quasi handler authors use to distinguish a writable slot from a read-only slot?

The expansion only creates expressions that evaluate immediately to values, or to read-write slots. If you want to pass a read only slot in a position where a quasi expects a read-write slot, you could do it manually. Ignoring freezing issues:     re(${ { get: function() { return x.y; }.bind(this) } }\w) If there's actually a real need for read-only slots in quasis, then the absence of syntactic support for this third case is a significant criticism of my suggested variation of your slotification strawman. Is there?

By read-only slot vs read-write slot I meant

example${x} is read only ${=y} is read-write

I should have phrased that as read-only substitution vs read-write substitution using the alternate slot spec for SVE.

# Mark S. Miller (14 years ago)

On Sat, Jun 18, 2011 at 5:59 PM, Mike Samuel <mikesamuel at gmail.com> wrote: [...]

What idiom should quasi handler authors use to distinguish a writable slot from a read-only slot?

[...]

By read-only slot vs read-write slot I meant

example${x} is read only ${=y} is read-write

I should have phrased that as read-only substitution vs read-write substitution using the alternate slot spec for SVE.

I think this is really just a special case of the general question "What idiom should quasi handler authors use to distinguish what kind of value is expected in a given hole position?" There's no general answer, and the answer for a particular quasi handler depends on the user's understanding of the purpose of the quasi handler. If the kind of thing expected at a given hole position is a record with a zero argument get method and/or a one argument set method, then ${=x} is just a particularly convenient method for generating one on the fly whose two methods happen to provide read-write access to x.

That said, for each hole position, the user's understanding of the meaning of the quasi handler will usually be one of "consumes a value here", in which case ${x} would be natural, or "extracts a value from the specimen and assigns it here", in which case ${=x} would be natural.

Returning to a recent example, for regex capture positions, i.e., after an unescaped "(", I'd always expect to see ${=x} unless something unusual is going on. Elsewhere, I'd expect ${x} to provide text to match against. If we use this convention, then to provide text to match against after an unescaped "(" that appears for other reasons, you'd use "(?:" instead. I admit this is accident prone, but is I think still the best balance.

# Mike Samuel (14 years ago)

2011/6/18 Mark S. Miller <erights at google.com>:

On Sat, Jun 18, 2011 at 5:59 PM, Mike Samuel <mikesamuel at gmail.com> wrote: [...]

What idiom should quasi handler authors use to distinguish a writable slot from a read-only slot?

[...]

By read-only slot vs read-write slot I meant example${x} is read only  ${=y} is read-write

I should have phrased that as read-only substitution vs read-write substitution using the alternate slot spec for SVE.

I think this is really just a special case of the general question "What idiom should quasi handler authors use to distinguish what kind of value is expected in a given hole position?" There's no general answer, and the answer for a particular quasi handler depends on the user's understanding of the purpose of the quasi handler. If the kind of thing expected at a given hole position is a record with a zero argument get method and/or a one argument set method, then ${=x} is just a particularly convenient method for generating one on the fly whose two methods happen to provide read-write access to x. That said, for each hole position, the user's understanding of the meaning of the quasi handler will usually be one of "consumes a value here", in which case ${x} would be natural, or "extracts a value from the specimen and assigns it here", in which case ${=x} would be natural. Returning to a recent example, for regex capture positions, i.e., after an unescaped "(", I'd always expect to see ${=x} unless something unusual is going on. Elsewhere, I'd expect ${x} to provide text to match against. If we use this convention, then to provide text to match against after an unescaped "(" that appears for other reasons, you'd use "(?:" instead. I admit this is accident prone, but is I think still the best balance.

I can imagine that in

refoo(${bar}.)baz.match(str)

where bar="BAR" the user would expect this to do

new RegExp("foo(BAR.)baz").exec(str)

making no assignments.

While with

refoo(${=bar}.baz

the user would expect this to do something like

(function () {
  var match = new RegExp("foo(.)baz").exec(str);
  if (match) {
    bar = match[1];
  }
  return match;
}())

The quasi handler could be implemented as below. See the parts where I test "function" === typeof subst.

function (callSiteId /*, sve... */) {
  var pattern, exec, flags;
  // Cache fetch of pattern, exec and flags elided.

  // Build the pattern and flags.
  var raw = callSiteId.raw;
  var substs = callSiteId.subst;  // Slots/thunks stored on

callSiteId somehow. var pattern = ""; var lastIndex = raw.length - 1; for (var i = 0; i < lastIndex;) { var subst = substs[i]; pattern += raw[++i] // Idiom to test whether a thunk or a slot. ("function" === typeof subst ? regexpEncode(subst()) : ""); } // The flags might be glommed onto the last item, e.g. // refoo#i // or some similar syntax. var last = raw[lastIndex]; flags = // extract flags from last pattern += last;

  // For each group index besides 0, an optional function that

receives its value. var groupReceivers; // Define an exec method that assigns groups to recievers on match. exec = function (str) { var match = RegExp.prototype.exec.apply(this, arguments); if (match && match.length > 1) { if (!groupReceivers) { groupReceivers = []; for (var i = 0; i < lastIndex; ++i) { var subst = substs[i]; if ("function" !== typeof subst && "function" === typeof subst.set) { groupReceivers[i + 1] = subst.set; } } } for (var i = 1; i < match.length; ++i) { var groupReceiver = groupReceivers[i]; if (groupReceiver) { groupReceiver[i].call(null, match[i]); } } } return match; }; // Freeze of exec and its prototype elided.

  // Cache put of pattern, flags, exec elided.

  // Create a regex with an overridden exec.
  var re = new RegExp(pattern, flags);
  re.exec = exec;
  return re;
}
# Mike Samuel (14 years ago)

2011/6/20 Mike Samuel <mikesamuel at gmail.com>:

I can imagine that in

refoo(${bar}.)baz.match(str)

"match" -> "exec"