Topic `var` binding in sequence expression (comma operator)

I haven't been able to come up with a good, self-explanatory name for this feature, yet.

When evaluating the comma operator, automatically assign the result of each evaluated operand to a binding named var. It can then be used in the next operand.

A few contrived examples

// get a value that can be null, provide default only when undefined
const opt = (getConfigValue('opt'), var === undefined ? 'default' : var);
// use computed value in multiple conditions, without using a temporary variable
if (calculate(stuff), var % 2 === 0 || var % 3 === 0) ...
(var, 1); // throws ReferenceError
// var is uninitialized before the first comma
(1, var + 2); // returns (1 + 2) -> 3
(1, (var + 2, var * 3)); // returns ((1 + 2) * 3) -> 9
// in a nested comma operator, var before the first comma
// refers to the outer sequence's var;
// this ensures that removing superfluous parentheses works
// as expected: (X, (Y, Z)) must be equivalent to (X, Y, Z)
// and so the above expression gives the same result as:
(1, var + 2, var * 3);
('A', (var + 'b', var + 'c') + (var + 'x', var + 'y'))
// returns (('A' + 'b') + 'c') + (('A' + 'x') + 'y') -> 'AbcAxy'

Mimic Hack-style pipeline, using var as topic token and , instead of |>

Examples taken from GitHub - js-choi/proposal-hack-pipes: Draft specification for Hack pipes in JavaScript.

return list
 , take(prefix.length, var)
 , equals(var, prefix);
envars
 , Object.keys(var)
 , var.map(envar =>
    `${envar}=${envars[envar]}`,
  )
 , var.join(' ')
 , `$ ${var}`
 , chalk.dim(var, 'node', args.join(' '))
 , console.log(var);

There's a caveat, though. Hack-style pipeline can use topic in a nested function:

2 |> [1, 2, 3].map(x => x ** #)
// returns [1, 4, 9]

I deliberately chose to restrict the var binding to the comma operator, and not allow referencing it from nested scope (more precisely, nested scope has its own uninitialized var binding):

2, [1, 2, 3].map(x => x ** var)
// throws ReferenceError: 'var' cannot be used before initialization

This can of course be changed if carrying the binding into closures is considered useful. For now I did it this way because I think it's less confusing, and also easier to implement.

PoC implementation

To try it out:

git clone --depth 10 --branch feat-var-expression \
    https://github.com/lightmare/engine262
cd engine262
pnpm install
pnpm run build
node bin/engine262.js --features=var-expression

This is an interesting and unique idea - I find it intriguing.

It sort of feels like an alternative formulation of the pipeline operator, where you've cause the "," to become the pipeline operator. Even with your first "contrived examples":

const opt = (getConfigValue('opt'), var === undefined ? 'default' : var);
// is pretty much the same as
const opt = getConfigValue('opt') |> % === undefined ? 'default' : var;

In my "dream language", I had a similar idea to help solve some of your earlier use cases. It was an "@" operator that assigned the LHS to the identifier on the RHS, and had a very high precedence. These bindings were scoped to the duration of the expression. e.g. filter(data.user@u.groups, u.metadata) is the same as { let u; filter((u=data.user).groups, u.metadata) }. It's intended to be a non-obtrusive way to collect temporary bindings in the middle of an expression.

Examples:

const opt = getConfigValue('opt')@x === undefined ? 'default' : x;

if (calculate(stuff)@x % 2 === 0 || x % 3 === 0)

I suppose you could use it to mimic pipelines too, if you kept re-binding to the same identifier, but in this case, I would probably rather have a pipeline operator, or maybe your "var/comma" idea:

return list@$
 , take(prefix.length, $)@$
 , equals($, prefix);

Personally I don’t find that the gain is significant relatively to using a temporary variable. For comparison, here are how I would write your examples:

var _;

// Instead of:
const opt = (getConfigValue('opt'), var === undefined ? 'default' : var);
// I am using, as of today:
const opt = (_ = getConfigValue('opt')) === undefined ? 'default' : _);

// Instead of:
if (calculate(stuff), var % 2 === 0 || var % 3 === 0) ...
// I am using, as of today:
if ((_ = calculate(stuff)) % 2 === 0 || _ % 3 === 0) ...

As for pipelines, the following ersatz works for me:

_ = envars
_ = Object.keys(_)
_ = _.map(envar =>
      `${envar}=${envars[envar]}`
    )
_ = _.join(' ')
_ = `$ ${_}`
_ = chalk.dim(_, 'node', args.join(' '))
console.log(_)

Using the comma operator and var keyword lead to very confusing syntax imo. It's JavaScript, not Perl!

There's nothing wrong with temporary variables:

const val = getConfigValue('opt');
const opt = val === undefined ? 'default' : val;

If you insist on not tainting the outer scope, what you want are variable declarations in statements, as supported by the do expressions proposal:

const opt = do {
  const val = getConfigValue('opt');
  val === undefined ? 'default' : val;
}

possibly we could simplify (shorten) this to

const opt = do const val = getConfigValue('opt') in val === undefined ? 'default' : val;

The do const x = whatever in somethingElse would be something I would love to have, and a similar idea is getting pushed for in this issue in do expressions.

*giggles* That is straight from Perl, and doesn't even fit JavaScript, because other blocks don't have return values.

Yet ;)

(That's the other half of what's being proposed in the link I shared. Instead of do blocks, the proposal is to implement some form of declaration-in-expression syntax + turning statements into expressions)