Skip to content

SI-8943 Handle non-public case fields in pres. compiler #4081

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

Merged
merged 1 commit into from
Nov 2, 2014

Conversation

retronym
Copy link
Member

When a case class is type checked, synthetic methods are added,
such as the hashCode/equals, implementations of the Product
interface. At the same time, a case accessor method is added for
each non-public constructor parameter. This the accessor for a
parameter named x is named x$n, where n is a fresh suffix.

This is all done to retain universal pattern-matchability of
case classes, irrespective of access. What is the point of allowing
non-public parameters if pattern matching can subvert access? I
believe it is to enables private setters:

case class C(private var x: String)

scala> val c = new C("")
c: C = C()

scala> val C(x) = c
x: String = ""

scala> c.x
<console>:11: error: variable x in class C cannot be accessed in C
              c.x
                ^

scala> c.x = ""
<console>:13: error: variable x in class C cannot be accessed in C
val $ires2 = c.x
               ^
<console>:10: error: variable x in class C cannot be accessed in C
       c.x = ""
         ^

Perhaps I'm missing additional motivations.

If you think scheme sounds like a binary compatiblity nightmare,
you're right: https://issues.scala-lang.org/browse/SI-8944

caseFieldAccessors uses the naming convention to find the right
accessor; this in turn is used in pattern match translation.

The accessors are also needed in the synthetic unapply method
in the companion object. Here, we must tread lightly to avoid
triggering a typechecking cycles before; the synthesis of that method
is not allowed to force the info of the case class.

Instead, it uses a back channel, renamedCaseAccessors to see
which parameters have corresonding accessors.

This is pretty flaky: if the companion object is typechecked
before the case class, it uses the private param accessor directly,
which it happends to have access to, and which duly gets an
expanded name to allow JVM level access. If the companion
appears afterwards, it uses the case accessor method.

In the presentation compiler, it is possible to typecheck a source
file more than once, in which case we can redefine a case class. This
uses the same Symbol with a new type completer. Synthetics must
be re-added to its type.

The reported bug occurs when, during the second typecheck, an entry
in renamedCaseAccessors directs the unapply method to use x$1
before it has been added to the type of the case class symbol.

This commit clears corresponding entries from that map when we
detect that we are redefining a class symbol.

Case accessors are in need of a larger scale refactoring. But I'm
leaving that for SI-8944.

Review by @dragos @lrytz

When a case class is type checked, synthetic methods are added,
such as the `hashCode`/`equals`, implementations of the `Product`
interface. At the same time, a case accessor method is added for
each non-public constructor parameter. This the accessor for a
parameter named `x` is named `x$n`, where `n` is a fresh suffix.

This is all done to retain universal pattern-matchability of
case classes, irrespective of access. What is the point of allowing
non-public parameters if pattern matching can subvert access? I
believe it is to enables private setters:

```
case class C(private var x: String)

scala> val x = new C("")
x: C = C()

scala> val c = new C("")
c: C = C()

scala> val C(x) = c
x: String = ""

scala> c.x
<console>:11: error: variable x in class C cannot be accessed in C
              c.x
                ^

scala> c.x = ""
<console>:13: error: variable x in class C cannot be accessed in C
val $ires2 = c.x
               ^
<console>:10: error: variable x in class C cannot be accessed in C
       c.x = ""
         ^
```

Perhaps I'm missing additional motivations.

If you think scheme sounds like a binary compatiblity nightmare,
you're right: https://issues.scala-lang.org/browse/SI-8944

`caseFieldAccessors` uses the naming convention to find the right
accessor; this in turn is used in pattern match translation.

The accessors are also needed in the synthetic `unapply` method
in the companion object. Here, we must tread lightly to avoid
triggering a typechecking cycles before; the synthesis of that method
is not allowed to force the info of the case class.

Instead, it uses a back channel, `renamedCaseAccessors` to see
which parameters have corresonding accessors.

This is pretty flaky: if the companion object is typechecked
before the case class, it uses the private param accessor directly,
which it happends to have access to, and which duly gets an
expanded name to allow JVM level access. If the companion
appears afterwards, it uses the case accessor method.

In the presentation compiler, it is possible to typecheck a source
file more than once, in which case we can redefine a case class. This
uses the same `Symbol` with a new type completer. Synthetics must
be re-added to its type.

The reported bug occurs when, during the second typecheck, an entry
in `renamedCaseAccessors` directs the unapply method to use `x$1`
before it has been added to the type of the case class symbol.

This commit clears corresponding entries from that map when we
detect that we are redefining a class symbol.

Case accessors are in need of a larger scale refactoring. But I'm
leaving that for SI-8944.
@scala-jenkins scala-jenkins added this to the 2.11.5 milestone Oct 29, 2014
@lrytz
Copy link
Member

lrytz commented Oct 30, 2014

LGTM! What a journey..

retronym added a commit that referenced this pull request Nov 2, 2014
SI-8943 Handle non-public case fields in pres. compiler
@retronym retronym merged commit d1a76d5 into scala:2.11.x Nov 2, 2014
retronym added a commit to retronym/scala that referenced this pull request Nov 10, 2014
Case class parameters that are less that public have an extra
accessor method created to ensure universal pattern matchability.
See scala#4081 for more background.

Currently, this is given a fresh name based on the parameter name.
However, this is fragile and the name can change based on unrelated
edits higher up in the source file.

This commit switches to a stable naming scheme for these methods.

A non-public case field `foo` has a corresponding accessor
`foo$access$N`, where `N` is the index of the parameter within
the constructor parameter list.

I have added a backwards compatiblity stub for `::$tl$1` in
anticipation of problems with partest otherwise.

The enclosed tests show a case that used to trigger a linkage
error under separate compilation that now works; shows that by
choosing the `foo$access$1` rather than `foo$1` we don't clash with
lambda lifted methods in the class; and shows the names of the
accessor methods as seen via Java reflection.
retronym added a commit to retronym/scala that referenced this pull request Nov 12, 2014
Case class parameters that are less that public have an extra
accessor method created to ensure universal pattern matchability.
See scala#4081 for more background.

Currently, this is given a fresh name based on the parameter name.
However, this is fragile and the name can change based on unrelated
edits higher up in the source file.

This commit switches to a stable naming scheme for these methods.

A non-public case field `foo` has a corresponding accessor
`foo$access$N`, where `N` is the index of the parameter within
the constructor parameter list.

The enclosed tests show a case that used to trigger a linkage
error under separate compilation that now works; shows that by
choosing the `foo$access$1` rather than `foo$1` we don't clash with
lambda lifted methods in the class; and shows the names of the
accessor methods as seen via Java reflection.
retronym added a commit to retronym/scala that referenced this pull request Nov 12, 2014
Case class parameters that are less that public have an extra
accessor method created to ensure universal pattern matchability.
See scala#4081 for more background.

Currently, this is given a fresh name based on the parameter name.
However, this is fragile and the name can change based on unrelated
edits higher up in the source file.

This commit switches to a stable naming scheme for these methods.

A non-public case field `foo` has a corresponding accessor
`foo$access$N`, where `N` is the index of the parameter within
the constructor parameter list.

The enclosed tests show a case that used to trigger a linkage
error under separate compilation that now works; shows that by
choosing the `foo$access$1` rather than `foo$1` we don't clash with
lambda lifted methods in the class; and shows the names of the
accessor methods as seen via Java reflection.
@gkossakowski
Copy link
Contributor

What is the point of allowing non-public parameters if pattern matching can subvert access? I believe it is to enables private setters:

Private setters can be achieved with declaring an explicit def. Given how many hoops we have to go through to support this feature, could we consider deprecating it?

@retronym
Copy link
Member Author

How would we rewrite this?

final case class ::[B](override val head: B, private[scala] var tl: List[B]) extends List[B] {
  override def tail : List[B] = tl
  override def isEmpty: Boolean = false
}

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