-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Let
statements
#8
Conversation
cc @sandersn |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks pretty good. Mostly comments on how to make the code more general.
@@ -30,6 +30,11 @@ function emitStatement(statement: Statement): string { | |||
return `var ${statement.name.text}${typestring} = ${emitExpression( | |||
statement.init, | |||
)}`; | |||
case Node.Let: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let
should be converted to var
in a transform; otherwise there's no way to emit modern JS.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So, to clarify it. Maybe there are more details here but in general, can we say:
- Transform: transform TS into JS (depending on the configuration - es2017, es2015, ...)
- Emit: emit JS code (the transformed TS but without the types)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Almost: emit should actually even emit TS if the types are still in the tree. That is, emit should be able to emit anything that can be parsed, with 100% fidelity (in theory). The transformer is the thing solely responsible for turning TS into ES20xx.
@@ -13,6 +13,7 @@ function typescript(statements: Statement[]) { | |||
case Node.ExpressionStatement: | |||
return [statement]; | |||
case Node.Var: | |||
case Node.Let: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
a transform should convert let
to var
, but it shouldn't be the typescript
one. Make a new function called es2015
that converts es2015
to es5
. For now, the only thing it has to do is the let->var transformation.
(Also, while you're here, EmptyStatement
shouldn't be dropped in a transform since it's a JS statement too. It should be emitted as empty in emit.ts)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
a transform should convert let to var, but it shouldn't be the typescript one. Make a new function called es2015 that converts es2015 to es5. For now, the only thing it has to do is the let->var transformation.
I built a short version in a separate branch just to get feedback on it. https://github.com/imteekay/mini-typescript/compare/let...imteekay:mini-typescript:let-es5?expand=1
- added compiler options with the target just to test it out
- transforming
es2015
when targetinges5
- emitting let when it's a
let
ast node
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(Also, while you're here, EmptyStatement shouldn't be dropped in a transform since it's a JS statement too. It should be emitted as empty in emit.ts)
Just opened a new PR to fix it. https://github.com/imteekay/mini-typescript/pull/10
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, that looks about right
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, just merged both PRs.
src/bind.ts
Outdated
(d) => | ||
d.kind === statement.kind || | ||
(d.kind === Node.Var && statement.kind === Node.Let) || | ||
(d.kind === Node.Let && statement.kind === Node.Var), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
now that you have let
, you should allow redeclaration of var
, since that's how real JS behaves.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added the possibility to redeclare var
imteekay/mini-typescript@03e83b2
src/bind.ts
Outdated
@@ -20,14 +27,16 @@ export function bind(m: Module) { | |||
); | |||
} else { | |||
symbol.declarations.push(statement); | |||
if (statement.kind === Node.Var) { | |||
if ([Node.Var, Node.Let].includes(statement.kind)) { | |||
symbol.valueDeclaration = statement; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is fine, but consider what happens to valueDeclaration in the following program for this code versus the alternative, where you don't set valueDeclaration once it's been set once:
var x = 1;
var x = 2;
This changes how goto-def behaves in Typescript, for example.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So in all cases, valueDeclaration
should actually be a list of all variable declaration statements?
Meaning, I should rename it to valueDeclarations
(plural) and push the statement to this list
e.g.
when redeclaring variables
if ([Node.Var, Node.Let].includes(statement.kind)) {
- symbol.valueDeclaration = statement;
+ symbol.valueDeclarations.push(statement);
}
set the first declaration symbol
locals.set(statement.name.text, {
declarations: [statement],
valueDeclaration: [Node.Var, Node.Let].includes(statement.kind)
- ? statement
+ ? [statement]
: undefined,
});
So editors can make use of this and have a better goto-def, listing the definitions of the variable
Lmk if this is what you meant.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's one way to solve it!
Basically, you can either
- Have a list
- Take the first
- Take the last
- Not even have valueDeclarations and search all of declarations every time.
Typescript chooses (2) because valueDeclaration is basically a shortcut that's good for 99% of declarations and it's OK if 1% of declarations have slightly incorrect behaviour with goto-def.
I brought this up because small decisions like this can have non-obvious ramifications for corner cases.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the clarification. Following the second approach (Take the first), I changed the code using the logical or assignment operator (||=
) to handle that.
src/bind.ts
Outdated
@@ -37,7 +46,7 @@ export function bind(m: Module) { | |||
export function resolve( | |||
locals: Table, | |||
name: string, | |||
meaning: Node.Var | Node.TypeAlias, | |||
meaning: Node.Var | Node.TypeAlias | Node.Let, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
now that you have two different kinds of value declarations, you need to abstract meaning
into something like enum Meaning { Value, Type }
-- and Meaning.Value
should match declarations with kind Var
or Let
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just made this change imteekay/mini-typescript@5f853d7
Another approach would be adding an optional fourth parameter to the resolve
function. So we could distinguish if it is a let
or var
and then I don't need to this kind of logic symbol?.valueDeclaration?.kind === Node.Let
. It could just check if the symbol
was found.
Which approach do you think is best for this scenario? There's any other?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What the full TS compiler does is have the binder save a meaning flag when binding a symbol. So: you declare enum Meaning { Value = 1 << 1; Type = 1 << 2 }
, and then add a flags
property on Symbol. Then when binding a let or var, if the symbol already exists, you set symbol.flags |= Meaning.Value
. (If it doesn't you start withsymbol.flags = Meaning.Value
)
That way you don't have to check the list of declarations at all when resolving.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Very interesting use of bitwise! Made the change, it became way simpler imteekay/mini-typescript@926bcd2
src/check.ts
Outdated
if ( | ||
letSymbol.valueDeclaration && | ||
letSymbol.valueDeclaration.pos < expression.pos | ||
) { | ||
return checkStatement(letSymbol.valueDeclaration!); | ||
} | ||
|
||
error( | ||
expression.pos, | ||
`Block-scoped variable '${expression.text}' used before its declaration.`, | ||
); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍🏼
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks pretty good. Mostly comments on how to make the code more general.
src/check.ts
Outdated
`Block-scoped variable '${expression.text}' used before its declaration.`, | ||
); | ||
|
||
return errorType; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you should still return the type of the symbol, even if it's used before its declaration. Otherwise you'll get much worse intellisense in an editor scenario.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now returning the actual statement type imteekay/mini-typescript@0c43054
Let
token and AST nodebind
: handle re-declaration of variables with the same name (let after var and the other way around)bind
: resolving symbols forlet
statements too (not onlyvar
)typechecker
: handle use-before-declaration error if an identifier expression is being called before the let variable declaration (usedpos
to handle this logic)emitter
: emit thelet
statement as avar
statement in the final JS code