Closed Jack-Works closed 3 years ago
Not mentioned in the memo, but be careful with super.*
when wrapping do expressions with yield
with a generator iife!
@nicolo-ribaudo hi can you give me a code example?
class A extends B {
*method() {
let res = do {
yield super.foo();
}
}
}
This doesn't work if you transform it as
class A extends B {
*method() {
var _Completion;
let res = ((yield* function*(){
_Completion = yield super.foo();
return _completion;
}.call(this, arguments)), _Completion)
}
}
because super.
changes its meaning when moved inside a different function.
Thanks, it's a nice catch!
Uncaught SyntaxError: 'super' keyword unexpected here
I'll think about how to handle this correctly. Are there any other similar syntax elements that cannot / will change meaning if I do this? I know function.sent
proposal might be but it's not landed yet.
I think only new.target
and arguments
(but this one is easier, just pass arguments
to .call
).
I don't have a clue on how to fix super.*
calls in this pattern. I cannot hoist those super.*
access cause they might have side effects. Maybe hoist them as a function?
class A extends B {
*method() {
var _Completion;
var _super_foo = (...args) => super.foo(...args)
let res = ((yield* function*(){
_Completion = yield _super_foo();
return _completion;
}.call(this, arguments)), _Completion)
}
}
(but it's broken because it doesn't inject the generator)
I think only
new.target
andarguments
(but this one is easier, just passarguments
to.call
).
Consider this code:
function x(a, b) {
function y(a, b) {
arguments[1] = 2
}
y.call(this, arguments)
console.log(b)
}
The assignment doesn't "popup" to the outer scope so it's a wrong transformation. I think I can ban it as a TypeScript error and use this wrong transformation since no one use arguments
today.
@nicolo-ribaudo hi thanks! I have fixed it in my memo. And I add another section Part 0
to see if we can conver it without a function wrapper.
For example for the common case:
const rnd = do {
let tmp = rand()
tmp * tmp;
}
We can convert it into
let _a
const rnd = ((_a = rand()), _a * _a);
Wait, how can I down-level compile do expressions in the class fields position? 🤔
class X {
#a = do {
if (super.x) a; else b;
}
}
Maybe into this?
var _super_x, completion_value
class X {
#a = ((_super_x = () => super.x), (() => { ... })(), completion_value)
}
That might cause problems if the do-expression contains a new X
itself. I think the sacred thing to do is to store _super_x
and completion_value
as private fields.
so if it is not a private field but a public field, I think it is not acceptable to introduce a new private field for code that not using a private field (cause down-level compiling are based on WeakMap)
class X {
#a_super_x = () => super.x
#a_completion_value
a = (((setCompletionValue) => { ... })(x => this.#a_completion_value = x), this.#a_completion_value)
}
hmm, I think I need to revisit my compiling method. it seems like my memo don't work for class private/public fields
Is there a non-static version for https://github.com/tc39/proposal-class-static-block? If so I can compile this easily into things like
class X {
a;
init block {
// ...
this.a = completion_a
}
}
The non-static thing is “inside the constructor”
The non-static version is the constructor.
However, in class fields you cannot have yield
/await
so this might be enough?
class X {
#a = (() => {
var _completion_value;
if (super.x) _completion_value = a; else _completion_value = b;
return _completion_value;
})();
}
Oh wait I forgot super
is not a function scoped thing (like arguments
)
It’s exactly like arguments, in that arrow functions don’t bind either.
I have to say if we have an arrow version of the generator function & async generator function things will be easier
Can you use super inside a nested non-arrow generator function such that it matters?
Not related to the super case, just cause I need to take care about this
arguments
that won't cross the normal function but do across the arrow version
https://github.com/bakkot/do-expressions-v2/issues/9#issuecomment-768803688
The transformation of control flow jumps is tricker than I think.
Consider this code:
const x = do {
try { if (y) break; 1; } catch {}
}
It will accidentally catch the "breaking" signal behind the transformation so I need to also add a "re-throw" code in every catch user writes.
What about this compile result?
function f(y) {
const x = do {
if (y) return 2
1
}
return x
}
Pros of this manner: How to transform yield inside do expression is already known. And yield is not able to accidentally caught by the userland like the try-catch way.
function f(y) {
const returnSign = { value: undefined }
function* f_inner() {
const x = do {
if (y) ((returnSign.value = 2), (yield returnSign));
1
}
yield x
}
for (const _a of f_inner()) {
if (_a === returnSign) return returnSign.value
return _a
}
}
Further into
function f(y) {
const returnSign = { value: undefined }
function* f_inner() {
var completionValue
const x = ((yield* (function* () {
if (y) ((returnSign.value = 2), (yield returnSign));
completionValue = 1
})()), completionValue)
yield x
}
for (const _a of f_inner()) {
if (_a === returnSign) return returnSign.value
return _a
}
}
I think you can make that work, but introducing a generator where none was previously adds a lot of overhead - more than just rewriting catch
blocks to add a rethrow
.
Ok... I found out this code
switch (expr) {
case 1:
let x = do {
if (cond) break
1;
}
console.log(x)
function f() {
return x
}
break
case 2:
x = f()
}
This basically not translatable in a reasonable way. Is it enough to analyze all things in the current scope and hoist them, or other edge cases?
Because switch
doesn't have a "block" for me that I can write those things:
switch (expr) {
try {
case ....
} catch (e) {...}
}
I need to convert them clause-by-clause but that make cross-clause definitions harder to handle with
Isn't this
switch (expr) {
case 1:
let x = do {
if (cond) break
1;
}
console.log(x)
function f() {
return x
}
break
case 2:
x = f()
}
equivalent to this?
{
let x;
function f() {
return x
}
switch (expr) {
case 1: {
x = do {
if (cond) break
1;
}
console.log(x)
} break
case 2: {
x = f()
}
}
}
So it would be possible to hoist all declarations to a containing block and convert each case as a block?
Yes, I hope this kind of analysis does not miss any edge case in the switch statement.
Ok I got two more edge cases...
function f(x = do { if (expr) return; 1 }) {}
for (const { prop = do { if (expr) continue; 1 } } of arr) {}
Is that allowed today?
I haven't written spec text for how to deal with those cases yet. My intention is that break
and continue
within loop heads (as in your second example) will be forbidden, but I haven't decided about return
in parameter lists (as in your first). Seems fine to forbid both for now, at least.
Seems fine to forbid both for now, at least.
Yes, it will make the transformer easier. And I don't think any one will need it
Some progress on control flow changes within do expression. (Note I temporarily disabled the transform of do expression because it does not work well with the control flow transform).
Loving this. Any idea when this is gonna be semi usable?
Loving this. Any idea when this is gonna be semi usable?
You can use babel to try this. For TypeScript, it is still in progress.
I think most of the transformation work in TypeScript has been done in https://github.com/microsoft/TypeScript/pull/42437. I'll close this.
Moved from https://github.com/bakkot/do-expressions-v2/issues/9. About how to compile this to ES2021 correctly.
I wrote a memo https://blog.jackworks.dev/2021/memo-transfrom-do-expressions/ to make sure I got things right before I actually start the coding. Please point out any edge case I missed thanks!