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

Implement multiple assignments (SIP-59) #19597

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

Conversation

kyouko-taiga
Copy link
Contributor

@kyouko-taiga kyouko-taiga commented Feb 2, 2024

This PR is a proof of concept for the implementation of multiple assignments (see SIP-59). It is still a work in progress. In particular, it doesn't satisfy Scala's left-to-right evaluation order.

Design overview

The main challenge is to respect Scala's left-to-right evaluation order, which implies that all effectual operations on the left-hand side of the assignment be evaluated before the right-hand side.

An assignment generally desugars to one of two forms:

  • If the left-hand side is a simple local variable, then lhs = rhs remains an Assign tree.
  • If the left-hand side is anything more sophisticated, then lhs = rhs is rewritten as an Apply tree (e.g., a(0) = 1 becomes a.update(0, 1)).

In the case of a single assignment, the current implementation can "simply" perform the translation in one go as it has access to the expressions of both the left-hand and the right-hand sides. In the case of a multiple-assignment, however, the right-hand side is not available during the desugaring of the left-hand side because it may be the result of an expression that we have not yet evaluated. For example:

def makePair(): (Int, Int) = ???
val a = Array(0, 0)
(a(0), a(1)) = makePair()

One solution to address this issue is to assign the result of all effectful sub-expressions to some temporary definitions an construct functions untpd.Tree => tpd.Tree that return the assignment of some left-hand side to the value represented by their arguments. These functions will be called after the tree representing their corresponding right-hand side is evaluated. They are called "assignment builders" in this PR.

Consider this example to illustrate:

def f(x: Int): Int = ??? // possibly effectful expression
def makePair(): (Int, Int) = ???
val a = Array(0, 0)

// before
(a(f(0)), a(f(1))) = makePair()

// after
val x0 = f(0)
val x1 = f(1)
val x2 = makePair()
a.update(x0, x2._1)
a.update(x1, x2._2)

x0 and x1 represent the "hoisting" of the effectful operations performed in the left-hand side of the assignment. Then x2 stores the result of the right-hand side. Finally the two updates perform the assignment. They are the result of calling instances of the aforementioned assignment builders.

Note: the evaluation order is not "strictly" left-to-right in the sense that the assignments are notionally applications of functions that occur in sources before the right-hand side has been evaluated. These semantics are nonetheless inevitable and match the way an assignment "work" in Scala.

An assignment composed of a so-called "lvalue" (borrowing from C/C++ terminology) together with a function that constructs a typed tree given an untyped right-hand side.

final class PartialAssignment[+T <: LValue](val lhs: T)(
    perform: (T, untpd.Tree) => untpd.Tree
)

A lvalue notionally represents the target of an assignment. It may be a simple value (e.g., a local variable) or the partial application of some update function or setter. It also contains the definitions of the sub-expressions whose values must be hoisted, which are called "locals" in the implementation:

sealed abstract class LValue:
  def locals: List[tpd.ValDef]
  def formAssignment(rhs: untpd.Tree)(using Context): untpd.Tree
end LValue

Partial assignments are constructed in the typer by the method formPartialAssignmentTo. This method is essentially a refactoring of typedAssign that constructs lvalues rather than desugaring left-hand sides directly while visiting the tree.

Once partial assignment has been constructed, the final result is built by creating a block containing, in this order:

  • The definitions of all hoisted values preserving the order in which sub-expressions must be evaluated
  • The evaluation of the right-hand side
  • The assignments of all lvalues, from left to right

No value is hoisted in the case of a single assignment so that the result is equivalent to the current behavior.

compiler/src/dotty/tools/dotc/typer/Typer.scala Outdated Show resolved Hide resolved
compiler/src/dotty/tools/dotc/typer/Typer.scala Outdated Show resolved Hide resolved
compiler/src/dotty/tools/dotc/typer/Typer.scala Outdated Show resolved Hide resolved
@kyouko-taiga kyouko-taiga marked this pull request as ready for review July 27, 2024 06:36
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.

4 participants