-
Notifications
You must be signed in to change notification settings - Fork 39
Add Scoped blocks
#356
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
base: hkmc2
Are you sure you want to change the base?
Add Scoped blocks
#356
The head ref may contain hidden characters: "scope\u{1F968}"
Conversation
hkmc2/shared/src/test/mlscript/codegen/ScopedBlocksAndHandlers.mls
Outdated
Show resolved
Hide resolved
LPTK
left a comment
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.
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 _ => |
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.
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).
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.
(Note: the comment is also not outdated.)
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 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 => |
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.
With this PR, the ugly lookup should no longer be needed. I saw that removing it currently crashes the compilation tests. Any idea why?
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.
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.
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.
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?
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.
Why do we need to use
t.definedVarsNoScoped, here? We should already know what is supposed to be in scope, thanks toScoped.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?
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.
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.
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.
I've added a comment to track it.
| //│ } | ||
| //│ }; | ||
|
|
||
| :sjs |
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.
Where does this useless tmp come from? Why is it collected in the first place?
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.
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.
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.
Ok, fair enough. This will be better dealt with through a pass of dead-binding elimination.
Co-authored-by: Lionel Parreaux <[email protected]>
LPTK
left a comment
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.
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; |
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.
It looks like the discard variable is not properly collected.
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.
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; |
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.
Why is this lambda bound separately? Is it not properly collected?
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 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()) |
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.
Why is f2 bound separately? Is it not properly collected?
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 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.
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.
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.
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.
Ah, I just noticed Lambda is already no longer a value. I guess it also does not need to be a Result...
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.
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.
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.
Ok, sure. Pls do it.
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 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; |
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.
Why is this bound separately? Is it not properly collected?
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 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).
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.
Ok, but FYI this PR is not really huge 👀
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 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 |
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 comment will be impossible to understand for someone else. Pls rephrase it.
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.
Same remark for the blocks below.
|
|
||
| :ge | ||
| // with Scoped blocks, `x` is declared | ||
| // :ge |
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.
Why would we keep this commented line?
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.
Same remark for the blocks below.
| val l = new TempSymbol(S(ref)) | ||
| loweringCtx.collectScopedSym(l) |
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.
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!
This PR introduces
Scopedblocks.It is still a draft because there is a lot of clean up to be done.Currently, this PR:
ScopedblockScopedblocksScopedblocks for if branches