THIS DRAFT HAS BEEN SUPERCEEDED BY THIS
This is a stream of random notes taken while talking to @rbuckton and @dherman about throw expressions and do expressions, as well as an early exploration of an attempt to get some clarity towards a cohesive/coherent story between the different statement types.
This is not a perfect categorization, just one of many :) Also, originally categorized by @rbuckton in his presentation (and polished here talking f2f).
- things that change the inner flow (continue, break)
- things that return through a single pass (if, switch, try/catch)
- things that return through many iterations (while, for)
- things that abruptly terminate the scope (throw, return)
- where to put
yield
?
Some tough problems currently with how do expressions are formulated:
- what happens when you return/throw from these expressions (example)?
- what happens when you continue/break from these expresions?
- how do you return values? how do you return arrays?
- can these things be async/awaited?
- if you declare variables inside the expression do they go into the outer scope?
In this exploration (inspired by scala's for-expressions), here are some of the possibly different rules:
- we disallow returning/throwning from these expressions (SyntaxError)
- we disallow continuing/breaking from these expressions (SyntaxError)
- we create a new keyword (e.g. yield, make, produce, use, generate, end, tail, etc) to enable things to be returned
In this specific exploration, we pick use
as an example because it is short/meaningful, but other options should be considered.
This is a bit shady, but here is a strawman: use
works like yield
in that it can be called multiple times and the statement block can return multiple values. in if
, try
and switch
it returns a single value (stops at first use of use
?) and for for
, while
it returns an array (all use
are called).
// let a = 2
let a = if (true) { use 1; } else { use 2; };
// let a = 2
let a = try { use throwsError(); } catch (e) { use 2; };
let a = switch (encoding) {
case "utf8": use new UTF8Encoder()
case "utf161e": use new UTF16Encoder()
case "utf16be": use new UTF16Encoder()
default: throw new Error("Unsupported encoding")
};
For iterators:
// let a = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
let a = for (let i = 0; i < 10; i++) { use i; };
// let a = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
let a = while (i < 10) { i++; use i; };
We deliberate throw a SyntaxError if use expressions
call certain invalid statements to avoid ambiguities. For example:
TODO(goto): should "throws" be allowed? if so, why?
function b() {
let a = while (true) {
// SyntaxError is thrown because `return` isn't allowed inside a while-expression
return 1
}
}
for (let i = 0; i < 10; i++) {
let b = for (let j = 0; j < 10; j++) {
// SyntaxError is thrown here because `break` isn't allowed inside for-expressions.
break;
}
}
What happens in these cases?
// is a 'undefined'?
let a = if (b) {
use c;
}
// Error thrown?
let a = try { throwsError(); };
// is a 'undefined'?
let a = while (false) { use c; }
// what does this return?
let a = for (let i = 0; i < 10; i++) {
use for (let j = 0; j < 10; j++) {
use j;
}
}
Option 2) return-like semantics
let result = [];
let a = do { i++; result.append(i); if (i == 1) use result; } while (1);
let result = [];
let a = for (let i = 0; i < 10; i++) { result.append(i) if (i == 9) use result; };
Option 3) new syntax
let result = [];
let a = do { i++; result.append(i); } while (1) use result;
let result = [];
let a = for (let i = 0; i < 10; i++) { result.append(i) } use result;
The first example should say
let a = 1
.