Skip to content
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

Merged
merged 22 commits into from
Jul 9, 2023
Merged

Let statements #8

merged 22 commits into from
Jul 9, 2023

Conversation

imteekay
Copy link
Owner

@imteekay imteekay commented May 20, 2023

  • Add the Let token and AST node
  • Add a test for a single let statement
  • Add a test for redeclaring let after var and var after let with the same variable "name"
  • Add a test to handle the use-before-declaration error
  • bind: handle re-declaration of variables with the same name (let after var and the other way around)
  • bind: resolving symbols for let statements too (not only var)
  • typechecker: handle use-before-declaration error if an identifier expression is being called before the let variable declaration (used pos to handle this logic)
  • emitter: emit the let statement as a var statement in the final JS code

@imteekay imteekay self-assigned this May 20, 2023
@imteekay
Copy link
Owner Author

cc @sandersn

Copy link

@sandersn sandersn left a 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:

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.

Copy link
Owner Author

@imteekay imteekay Jun 1, 2023

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)

Copy link

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:

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)

Copy link
Owner Author

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 targeting es5
  • emitting let when it's a let ast node

Copy link
Owner Author

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

Copy link

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

Copy link
Owner Author

@imteekay imteekay Jun 3, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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),

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.

Copy link
Owner Author

@imteekay imteekay Jun 3, 2023

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;

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.

Copy link
Owner Author

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

Screen Shot 2023-06-03 at 18 08 18

Lmk if this is what you meant.

Copy link

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

  1. Have a list
  2. Take the first
  3. Take the last
  4. 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.

Copy link
Owner Author

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.

imteekay/mini-typescript@e224a68

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,

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.

Copy link
Owner Author

@imteekay imteekay Jun 4, 2023

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?

Copy link

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.

Copy link
Owner Author

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
Comment on lines 66 to 76
if (
letSymbol.valueDeclaration &&
letSymbol.valueDeclaration.pos < expression.pos
) {
return checkStatement(letSymbol.valueDeclaration!);
}

error(
expression.pos,
`Block-scoped variable '${expression.text}' used before its declaration.`,
);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍🏼

Copy link

@sandersn sandersn left a 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.

@imteekay imteekay requested a review from sandersn June 4, 2023 01:21
src/check.ts Outdated
`Block-scoped variable '${expression.text}' used before its declaration.`,
);

return errorType;
Copy link

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.

Copy link
Owner Author

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

@imteekay imteekay merged commit 4237faa into master Jul 9, 2023
@imteekay imteekay deleted the let branch July 9, 2023 18:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants