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

Improved variable names in generated JS #4280

Open
wants to merge 14 commits into
base: main
Choose a base branch
from

Conversation

RunDevelopment
Copy link
Contributor

This PR implements a change I wanted to do for a while. The bindings defined JS variables inconsistently, using const, let, and var all over the place. As it turns out, most variables can be declared as const and everything else can be var. So I create a simple API for creating variables. This API also takes care of resolving naming conflicts, so we'll never have to worry about temporary variables conflicting with argument names ever again. In the future, this system may also be used to resolve other naming conflicts, but I kept it simple for now.

For those needing a refresher on var, let, and const in JS:

  • const name = value: A block-scoped variable that cannot be reassigned.
  • let name = value: A block-scoped variable that can be reassigned.
  • var name = value: A function-scoped variable that can be reassigned.

Changes:

  • New API for JsBinding that handles the creating of variables. The new function automatically resolves name conflicts and ensure that temporary variable names never conflict with parameter names. The main functions of the new API are:
    • alias(name, expr): This creates a JS const variable. The variable is immutable.
    • var(name, initial): This creates a JS var variable. The variable is mutable AND visible in the finally block.
    • guarnatee_alias: Same as alias, but panics if the name is taken. This is necessary, because one instruction assumes that a variable called retptr has been created, so we can't rename that variable.
  • Use the new API everywhere. All variables are now created with the new API.

@RunDevelopment RunDevelopment marked this pull request as draft November 19, 2024 16:40
@RunDevelopment
Copy link
Contributor Author

Please merge #4281 before this PR, because it includes code gen for most of the binding instructions this PR changes.

@RunDevelopment RunDevelopment marked this pull request as ready for review November 29, 2024 18:17
Copy link
Collaborator

@daxpedda daxpedda left a comment

Choose a reason for hiding this comment

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

I don't know what to think of the change from let to var. I always read to not use var. After some research, it also seems to be the recommendation I find most. Personally, var make it just harder to read code compared to let. Is there some guidance somewhere we could follow?

The deferred stuff makes sense to me to use var on, but on anything else I would prefer to stick with let, const taking precedence over both ofc.

crates/cli-support/src/js/binding.rs Outdated Show resolved Hide resolved
crates/cli-support/src/js/binding.rs Show resolved Hide resolved
crates/cli-support/src/js/binding.rs Outdated Show resolved Hide resolved
crates/cli-support/src/js/binding.rs Outdated Show resolved Hide resolved
@daxpedda daxpedda added the waiting for author Waiting for author to respond label Dec 6, 2024
@RunDevelopment
Copy link
Contributor Author

The reason I didn't use let is to minimize the API of JsBuilder.

The only difference between var and let inside function bodies is that var is function-scoped while let is block-scoped. Both can be re-assigned, but only var can be accessed from anywhere within the function. So let is strictly less powerful than var. That power is also the reason let was added to JS. Turns out, people make coding mistakes and var makes it easy to use a variable before it is defined. Example:

try {
  throw new Error();
  var foo = 42;
} catch {
  console.log(foo);
}

Accessing foo inside the catch block before it has been assigned a value is valid and will log undefined. Did you expect this? If no, then you now know the reason people say you shouldn't use var. People are very used to block-scoped variables from other programming languages, and so var goes against their intuition.

Btw the reason the above logs undefined is that var foo is not block-scoped. So the above is executed by JS like this:

let foo = undefined;
try {
  throw new Error();
  foo = 42;
} catch {
  console.log(foo);
}

Now the behavior is a bit more clear, right?

Why did I use var everywhere, then? Again, to minimize the API, and because most of the reasons people recommend let over var don't apply to us. The code is generated, so human error in writing code doesn't really apply, and scoping rules of var are actually useful to us because they make code gen a bit easier.

I just don't see the point in adding support for let when var already does everything let can do and more.

@daxpedda
Copy link
Collaborator

daxpedda commented Dec 7, 2024

Now the behavior is a bit more clear, right?

I was already aware of all that. AFAIU to me it all boils down to scoping and hoisting vs TDZ. My personal preference is let because I find function level scoping horrible to read and if I've had to choose between temporal dead zoning and hoisting I would pick TDZ.

Why did I use var everywhere, then? Again, to minimize the API, and because most of the reasons people recommend let over var don't apply to us. The code is generated, so human error in writing code doesn't really apply, and scoping rules of var are actually useful to us because they make code gen a bit easier.

I agree that we should prioritize WBG over the output. But the readability of the generated output is not worth nothing. E.g. removing the complexity around deferred in the implementation is a plus in my book. But the cost of using let in all the other cases seems incredibly small to me especially because it is very self-contained with the new changes you made. I would definitely not like to decrease the readability of the code unless the tradeoff makes sense, even if its not a priority.

So unless there are other reasons to use var, lets stick with let apart from the deferred stuff.

@RunDevelopment
Copy link
Contributor Author

Done. I used let wherever possible.

I also added reference tests for closures, because they had some code gen unique to them.

@RunDevelopment
Copy link
Contributor Author

I'm going insane. Code gen for closures isn't consistent between compilers. I'm not having another raw.js. Bye bye closure reference tests.

Comment on lines +1370 to +1372
let val = js.r#let("v", "undefined".to_string());
js.prelude(&format!("if ({} !== 0) {{", ptr));
js.prelude(&format!("v{} = {}({}, {}).slice();", i, f, ptr, len));
js.prelude(&format!("{val} = {f}({ptr}, {len}).slice();"));
Copy link
Collaborator

@daxpedda daxpedda Dec 9, 2024

Choose a reason for hiding this comment

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

Shouldn't we just be able to use var here and skip a line, like in deferred?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

With the introduction of let, the purpose of var has now become to declare variables as accessible from within the finally block.

The goal is to generate correct JS code, not code golfing.

Copy link
Collaborator

Choose a reason for hiding this comment

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

In lack of a proper response: please use var here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
waiting for author Waiting for author to respond
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants