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

Class Lifter #266

Open
wants to merge 101 commits into
base: hkmc2
Choose a base branch
from
Open

Class Lifter #266

wants to merge 101 commits into from

Conversation

CAG2Mark
Copy link
Contributor

@CAG2Mark CAG2Mark commented Jan 27, 2025

Lifts classes and nested functions to the top-level.

Method

General idea: when functions have to capture local variables, we need to rewrite their parameter lists. For example:

fun f(arg) =
  let unused = 0
  fun g(arg2) = set arg1 = 1
  let foo = g
  foo(unused)
  arg

is transformed into

fun g(f_capture)(arg2) =
  set f_capture.arg1 = 1 
class f$capture(arg1)
fun f(arg) =
  let capture = f$capture(arg)
  let foo = g(capture)
  foo(unused)
  arg

Variables which are not mutated will be passed as a parameter directly.

TODO

  • Lift functions.
  • Lift classes:
    • Instantiate does not work at all
    • Functions within functions within classes are lifted out of the class. This is technically fine, but it's better to have them as class methods. However, we need private methods for this, which is not yet implemented.
    • Private variables can't be accessed by nested classes/functions after being lifted out.
    • Modules are not lifted properly
  • Only pass mutated variables through the capture class. Move other variables by passing them as an argument directly. Note that there are some subtle cases where this causes problems, such as
    fun f() =
      let x = g
      let y = 2
      fun g() = y
      x()
    so for now, we only do this when the function is not passed around as a first-class object. More thorough analysis could be done in another PR.
    • More advanced analysis is work-in-progress.
  • Replace calls with a single call where we can. For example
    fun f(arg) =
      let unused = 0
      fun g(arg2) = set arg1 = 1
      g(unused)
      arg
    could be rewritten as
    fun g(f_capture, arg2) =
      set f_capture.arg1 = 1
    fun g$(f_capture)(arg2) = g(f_capture, arg2)
    class f$capture(arg1)
    fun f(arg) =
      let capture = f$capture(arg)
      g(capture, unused)
      arg
    where the call g(unused) is symbolically replaced.
    ("lifting" curried functions is much easier than the lifting in general, so it can be done later)

Auxiliary Parameters

Classes can now have auxiliary parameters (IR only currently) with curried constructors.

@CAG2Mark CAG2Mark self-assigned this Jan 27, 2025
@LPTK
Copy link
Contributor

LPTK commented Jan 27, 2025

What's the intended scheme regarding mutable local variables?

FWIW, we should distinguish mutable from immutable variables. I intend to let mut be used to annotate pattern variables, like in Rust, and to reject re-assignments to non-mut variables. Note that this would still leave let x; if b do x = 1 well-formed.

When there are only immutable variables being captured, the scheme can be simpler.

@CAG2Mark CAG2Mark marked this pull request as draft January 27, 2025 10:12
@CAG2Mark
Copy link
Contributor Author

CAG2Mark commented Jan 27, 2025

What's the intended scheme regarding mutable local variables?

FWIW, we should distinguish mutable from immutable variables. I intend to let mut be used to annotate pattern variables, like in Rust, and to reject re-assignments to non-mut variables. Note that this would still leave let x; if b do x = 1 well-formed.

When there are only immutable variables being captured, the scheme can be simpler.

I don't distinguish between local variables that are mutated / not mutated for now. I intend to move every local variable that's referenced inside a nested class / function into a closure.

Would it be better to add the immutable local variables to each class/function directly?

P.S. seems I accidentally marked this PR as ready earlier, but it's definitely not. It's now marked as a draft.

@CAG2Mark
Copy link
Contributor Author

Added much more into to the the first comment.

@@ -15,7 +15,7 @@ foo()

let x = 1
let f = () => x
//│ f = [function f]
//│ f = [function lambda]
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hm. Did this actually rewrite it as a proper function before? Or is this just due to the way it's printed?

Copy link
Contributor

Choose a reason for hiding this comment

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

What do you mean? The name is taken from the name of the binding, because we generate things like f = function f ....

@CAG2Mark
Copy link
Contributor Author

OK, I think it's ready for review.

@CAG2Mark CAG2Mark marked this pull request as ready for review February 12, 2025 15:07
@@ -238,3 +244,28 @@ class BlockTransformerShallow(subst: SymbolSubst) extends BlockTransformer(subst
then b else HandleBlock(l2, res2, par2, args2, cls2, hdr2, bod, rst2)
case _ => super.applyBlock(b)

// does not traverse into any other block
Copy link
Contributor

Choose a reason for hiding this comment

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

The semantics of this transformer seems broken wrt Begin. If you have Begin{foo()}; bar() you're not going to change foo even though you would have for the equivalent foo(); bar().

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The job of this transformer is just to modify the paths and results in a block which do not require traversing another block to reach -- it's the job of the person using this transformer to traverse the sub-blocks, probably used in combination with one of the other block transformers.

As for Begin, both of its sub-blocks are expected to be traversed by whoever is using this transformer, not the transformer itself.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ok, then please pick a better name for this class and document in the code what you've just written here. The current comment is inadequate as documentation.

Copy link
Contributor

@LPTK LPTK Feb 15, 2025

Choose a reason for hiding this comment

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

Also it feels like a better architecture might be to use a method called applySubBlock in BlockTransformer whenever a sub-block (not a top-level block) is traversed, which would mean you only have to override that here to return input unchanged, instead of all this boilerplate.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good point, I think that will be a better solution.

Comment on lines -279 to +282
//│ Foo1 = function Foo(x1) { return new Foo.class(x1); };
//│ Foo1 = function Foo(x1) {
//│ return new Foo.class(x1);
//│ };
Copy link
Contributor

Choose a reason for hiding this comment

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

Any idea why there are no longer on one line?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

When there's parameter lists, we need to curry the ctor using lambdas, and it's quite ugly to have it all on one line. I decided it was not worth special-casing non-curried ctors to be on one line such it's purely a superficial change.

Copy link
Contributor

Choose a reason for hiding this comment

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

Oh I see, I didn't realize the line was previously missing the breaks.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Can this be marked as resolved?

//│ undefined
//│ ═══[RUNTIME ERROR] Error: Function expected at least 1 argument but got 0
//│ ═══[RUNTIME ERROR] Error: Function 'lambda' expected at least 1 argument but got 0
Copy link
Contributor

Choose a reason for hiding this comment

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

It would be good if we could avoid making every lambda get the name "lambda", at least in such error messages. Ideally they'd be unnamed and we would still have the old message here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Currently it seems BlockMemberSymbols must have names. Maybe we could add an flag which disables the name from being printed in error messages -- I don't know if it makes sense for BlockMemberSymbols to be unnamed.

@LPTK
Copy link
Contributor

LPTK commented Feb 13, 2025

OK, I think it's ready for review.

Thanks, I'll try to properly review it tomorrow. In the meantime please fix the conflicts and comments.

@@ -91,12 +92,37 @@ sealed abstract class Block extends Product with AutoLocated:
case TryBlock(sub, finallyDo, rest) => sub.freeVars ++ finallyDo.freeVars ++ rest.freeVars
case Assign(lhs, rhs, rest) => Set(lhs) ++ rhs.freeVars ++ rest.freeVars
case AssignField(lhs, nme, rhs, rest) => lhs.freeVars ++ rhs.freeVars ++ rest.freeVars
case AssignDynField(lhs, fld, arrayIdx, rhs, rest) => lhs.freeVarsLLIR ++ fld.freeVarsLLIR ++ rhs.freeVarsLLIR ++ rest.freeVarsLLIR
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this really meant to use freeVarsLLIR? 🤔

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