Skip to content

Conversation

@ychenfo
Copy link
Member

@ychenfo ychenfo commented Nov 27, 2025

This PR introduces Scoped blocks. It is still a draft because there is a lot of clean up to be done.

Currently, this PR:

  • Only symbols that are known to be declared in the elaborated tree and symbols generated during lowering are put in the Scoped block
  • Does not update later passes to properly maintain the Scoped blocks
  • Does not generate nested Scoped blocks for if branches

NeilKleistGao and others added 30 commits November 11, 2025 17:01
This reverts commit 5f9e63c.
getting runtime errors of some thing undefined
different match branch share the same `argumentn$` symbol, so maybe only nest scopes in patmat
This reverts commit 79bd913.
This reverts commit 3108bac.
NeilKleistGao and others added 5 commits December 5, 2025 17:43
- no declared vars in `Scoped` blocks
- pCtor also gets a `Scoped` block, which is merged with ctor's `Scoped` block in jsbuilder because in the js backend, parent ctor and ctor are in the same js block
- fix scoped blocks for lambdas for shortcut boolean ops
@ychenfo ychenfo marked this pull request as ready for review December 5, 2025 12:07
Copy link
Contributor

@LPTK LPTK left a comment

Choose a reason for hiding this comment

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

Ok, I think this is almost ready to merge. Thanks for your sustained work through this complicated change!

The nice thing is that this change also lets us avoid the needless assignment to undefined when writing let x without an initializer.

case Scoped(syms, body) =>
blockPreamble(p.imports.map(_._1).toSeq ++ syms.toSeq) ->
(imps.mkDocument(doc" # ") :/: block(body, endSemi = false).stripBreaks)
case _ =>
Copy link
Contributor

Choose a reason for hiding this comment

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

Does it actually happen that we get a non-empty definedVarsNoScoped but there is no scope surrounding it? Sounds like if it does, it should be fixed. But we could fix it later (then, add a comment).

Copy link
Contributor

Choose a reason for hiding this comment

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

(Note: the comment is also not outdated.)

Copy link
Member

Choose a reason for hiding this comment

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

This is also related to lambda lifting. Some generated variables, like lambda, can only be handled by definedVarsNoScoped. I've left a comment as well.


def blockPreamble(ss: Iterable[Symbol])(using Raise, Scope): Document =
// TODO document: mutable var assnts require the lookup
val vars = ss.filter(scope.lookup(_).isEmpty).toArray.sortBy(_.uid).iterator.map(l =>
Copy link
Contributor

Choose a reason for hiding this comment

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

With this PR, the ugly lookup should no longer be needed. I saw that removing it currently crashes the compilation tests. Any idea why?

Copy link
Member

Choose a reason for hiding this comment

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

I think this is mainly because in blockk function, we have val pre = blockPreamble(t.definedVarsNoScoped). Then t.definedVarsNoScoped will also cover symbols that have been collected by the outer Scoped object. Without the filtering will fail the assertion when we try to duplicate those symbols.

Copy link
Contributor

Choose a reason for hiding this comment

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

Why do we need to use t.definedVarsNoScoped, here? We should already know what is supposed to be in scope, thanks to Scoped.

Do you have a concrete example of this?

Copy link
Member

Choose a reason for hiding this comment

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

Why do we need to use t.definedVarsNoScoped, here? We should already know what is supposed to be in scope, thanks to Scoped.

Do you have a concrete example of this?

One example is the let done() = ... in mlscript-compile/Render.mls. If we do not call blockPreamble to allocate names, then this done will not be found later. I guess it is related to lambda lifting. Should we leave a comment here and handle it in another PR?

Copy link
Contributor

Choose a reason for hiding this comment

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

Should we leave a comment here and handle it in another PR?

Ok, let's do that.

Lambda lifting should be updated to deal with this later.

Copy link
Member

Choose a reason for hiding this comment

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

I've added a comment to track it.

//│ }
//│ };

:sjs
Copy link
Contributor

Choose a reason for hiding this comment

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

Where does this useless tmp come from? Why is it collected in the first place?

Copy link
Member Author

Choose a reason for hiding this comment

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

With :noTailRec, fun f = f is compiled to the following program, where tmp is the result of application.

:noTailRec
:sjs
fun f = f
//│ JS (unsanitized):
//│ let f; f = function f() { let tmp; tmp = f(); return tmp };

tmp is created and collected here.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ok, fair enough. This will be better dealt with through a pass of dead-binding elimination.

Copy link
Contributor

@LPTK LPTK left a comment

Choose a reason for hiding this comment

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

Ok, I think this PR is now almost ready to be merged. Nice work on this major change of the IR!

But please address the comments I left first.

let discard = drop _
//│ JS (unsanitized):
//│ let discard, discard1; discard1 = function discard(_0) { return runtime.Unit }; discard = discard1;
//│ let discard;
Copy link
Contributor

Choose a reason for hiding this comment

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

It looks like the discard variable is not properly collected.

Copy link
Member Author

Choose a reason for hiding this comment

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

One of the discard symbols is created by lambda rewriter, so it's indeed not collected now.

//│ baz = function baz() {
//│ let w, z, lambda;
//│ let w, z;
//│ let lambda;
Copy link
Contributor

Choose a reason for hiding this comment

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

Why is this lambda bound separately? Is it not properly collected?

Copy link
Member Author

Choose a reason for hiding this comment

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

This lambda is also created by the lambda rewriter and not collected now.

f()
//│ JS (unsanitized):
//│ let x, f, x1, f1; x = 1; f1 = function f() { return x }; f = f1; x1 = 2; runtime.safeCall(f())
//│ let x, f, x1; let f1; x = 1; f1 = function f() { return x }; f = f1; x1 = 2; runtime.safeCall(f())
Copy link
Contributor

Choose a reason for hiding this comment

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

Why is f2 bound separately? Is it not properly collected?

Copy link
Member Author

Choose a reason for hiding this comment

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

This f1 is also created by the lambda rewriter and not collected now.

Actually do we still want to generate Result.Lambda at all during lowering (since lambda rewriter is always on)? If we don't do so all these un-collected symbols related to lambdas should be gone.

Copy link
Contributor

Choose a reason for hiding this comment

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

Clearly, either the "lambda rewriter" should be fixed (which should be very easy; it's distinct from the lifter) or we should just lower directly to a function on the fly and remove lambda rewriting. I am not sure why we currently still have lambdas in values. I think it's a historical artifact.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah, I just noticed Lambda is already no longer a value. I guess it also does not need to be a Result...

Copy link
Member Author

Choose a reason for hiding this comment

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

Oh, so do we want to totally remove the Lambda form (since Lambdas don't need to be either Values or Results)...?

I guess in this PR it would be easier to only add fixes in LambdaRewriter.scala.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ok, sure. Pls do it.

Copy link
Contributor

Choose a reason for hiding this comment

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

There is too much :sjs output in this file. Please only keep one or two representative cases.

//│ tmp6 = globalThis.Object.freeze(new Term.Symbol("x"));
//│ x1 = globalThis.Object.freeze(new Term.Ref(tmp6));
//│ tmp7 = Predef.print(x1);
//│ let tmp6;
Copy link
Contributor

Choose a reason for hiding this comment

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

Why is this bound separately? Is it not properly collected?

Copy link
Member

Choose a reason for hiding this comment

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

This is because we have not handled temporary variables in quotations. I think I will do it in the next PR (this PR is already huge and complicated).

Copy link
Contributor

Choose a reason for hiding this comment

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

Ok, but FYI this PR is not really huge 👀

Copy link
Contributor

Choose a reason for hiding this comment

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

There is too much :sjs output in this file. Please only keep one or two representative cases and those where the comments specifically talks about the generated code.

val x

:ge
// with Scoped blocks, `x` is declared
Copy link
Contributor

Choose a reason for hiding this comment

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

This comment will be impossible to understand for someone else. Pls rephrase it.

Copy link
Contributor

Choose a reason for hiding this comment

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

Same remark for the blocks below.


:ge
// with Scoped blocks, `x` is declared
// :ge
Copy link
Contributor

Choose a reason for hiding this comment

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

Why would we keep this commented line?

Copy link
Contributor

Choose a reason for hiding this comment

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

Same remark for the blocks below.

Comment on lines 437 to +448
val l = new TempSymbol(S(ref))
loweringCtx.collectScopedSym(l)
Copy link
Contributor

Choose a reason for hiding this comment

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

The new TempSymbol + collectScopedSym pattern is all over the place. This is a time where you introduce a helper function (call it registerTempSymbol) to avoid the needless repetition. Keep it DRY!

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.

3 participants