-
Notifications
You must be signed in to change notification settings - Fork 3.1k
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
fix backend crash: Cannot create ClassBType from non-class symbol; also fix SI-7139 #5482
Conversation
This fixes scala/scala-dev#248, where a type alias reached the backend through this method. This is very similar to the fix for SI-5031, which changed it only in ModuleSymbol, but not in Symbol. The override in ModuleSymbol is actually unnecessary (it's identical), so it's removed in this commit. It was added for unclear reasons in 296b706.
This makes getClassByName fail / getClassIfDefined return NoSymbol when querying an alias. The current behavior can confuse the classfile parser: when parsing a class, a cross-check verifies that `pool.getClassSymbol(nameIdx)` returns the symbol of the class currently being parsed. If there's a type alias that shadows the linked class, following the alias would return an unrelated class. (The cross-check didn't fail because there are some other guards around it) The logic to follow aliases was was added in ff98878, without a clear explanation. Note that `requiredClass[C]` works if `C` is an alias, it is expanded by the compiler.
Awesome, this looks to fix https://issues.scala-lang.org/browse/SI-7139!
|
case ex: IOException => | ||
throw ex | ||
case ex: MissingRequirementError => | ||
throw ex | ||
case ex: Throwable => |
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.
Maybe ex: NonFatal
instead?
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.
that's reasonable, will do
sym = owner.newClass(ss.toTypeName) setInfoAndEnter completer | ||
debuglog("loaded "+sym+" from file "+file) | ||
sym | ||
} |
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 a nice cleanup! :-)
There was a piece of logic essentially duplicating getClassByName in Mirrors (split up a fully qualified class name by ".", look up pieces). That piece of code was added in 0ce0ad5 to fix one example in SI-2464. However, since 020053c (2012, 2.10) that code was broken: the line ss = name.subName(0, start) should be ss = name.subName(start, name.length).toTypeName As a result, the code would always create a stub symbol. Returning a stub seems to be the right thing to do anyway, and the fact that we were doing so during two major releases is a good proof.
One of the first entries in the classfile is the class name. The classfile parser performs a cross-check by looking up the class sybmol corresponding to that name and ensures it's the same as `clazz`, the class symbol that the parser currently populates. Since 322c980 ("Another massive IDE checkin"), if at the time of the check `clazz` but the lookup returns some class, the `clazz` field is assigned. The commit following this one makes sure `clazz` is never NoSymbol, so the assignment can safely be removed.
In SymbolLoaders, when seeing a classfile `Foo.class`, we always (unconditionally) create 3 symbols: a class, a module and a module class. Some symbols get invalidated later (`.exists`). Until now, the classfile parser (and unpickler) received the "root" symbol as argument, which is the symbol whose type is being completed. This is either the class symbol or the module symbol. The classfile parser would then try to lookup the other symbol through `root.companionClass` or `root.companionModule`. Howver, this lookup can fail. One example is scala-dev#248: when a type alias (in a package object) shadows a class symbol, `companionClass` will fail. The implementations of the classfile parser / unpickler assume that both the `clazz` and the `staticModule` symbols are available. This change makes sure that they are always passed in explicitly. Before this patch, in the example of scala-dev#248, the `classRoot` of the unpickler was NoSymbol. This caused a bug when unpickling the module class symbol, causing a second module class symbol to be created mistakingly. The next commit cleans up this logic, more details there. This second symbol would then cause the crash in the backend because it doesn't have an `associatedFile`, therefore `isCoDefinedWith` would spuriously return `true`.
When unpickling a class, if the name and owner matches the current `classRoot` of the unpickling Scan, that `classRoot` symbol is used instead of creating a new symbol. If, in addition, the class being unpickled has the MODULE flag, the unpickler should use the `moduleRoot.moduleClass` symbol (instead of creating a new one). To identify the module class, the current implementation compares the name and owner to the `classRoot`. This fails in case the `classRoot` is `NoSymbol`, which can happen in corner cases (when a type alias shadows a class symbol, scala-dev#248). In this patch we identify the module class by comparing the name and owner to the `moduleRoot` symbol directly (using a `toTypeName`).
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.
Really good stuff! I have a few small suggestions for locking this down a bit more while we're at it.
* This is intended as a hook allowing to support loading symbols from | ||
* files other than .class files. | ||
*/ | ||
protected def newClassLoader(bin: AbstractFile): SymbolLoader = |
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 verified this hook (re-introduced by #2963) is no longer used by scala-js (since scala-js/scala-js@8d85779)
if (isTopLevel) { | ||
if (c != clazz) { | ||
if ((clazz eq NoSymbol) && (c ne NoSymbol)) clazz = c |
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.
Is there any scenario in e.g. presentation/interactive mode where we needed this? /cc @dragos
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.
No, I don't think so. This looks more like defensive coding. I don't know if there's any scenario in which pool.getClassSymbol
returns NoSymbol
, but that seems to be the only purpose of this call. Maybe the new logic involving StubSymbol
subsumes 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.
With this PR, clazz cannot ever be NoSymbol.
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, thanks.
clazz setInfo completer | ||
enterIfNew(owner, clazz, completer) | ||
} | ||
|
||
def newModule(owner: Symbol, name: String): Symbol = owner.newModule(newTermName(name)) |
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.
: ModuleSymbol
as the result? (Allows tightening enterClassAndModule
and ClassfileLoader
).
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.
Implemented in 5c7b8ed
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, done.
def enterClassAndModule(root: Symbol, name: String, completer: SymbolLoader) { | ||
val clazz = enterClass(root, name, completer) | ||
val module = enterModule(root, name, completer) | ||
def enterClassAndModule(root: Symbol, name: String, getCompleter: (Symbol, Symbol) => SymbolLoader) { |
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.
(Symbol, ModuleSymbol) => SymbolLoader
seems within reach to avoid confusion in the future.
@@ -277,7 +281,7 @@ abstract class SymbolLoaders { | |||
} | |||
} | |||
|
|||
class ClassfileLoader(val classfile: AbstractFile) extends SymbolLoader with FlagAssigningCompleter { | |||
class ClassfileLoader(val classfile: AbstractFile, clazz: Symbol, module: Symbol) extends SymbolLoader with FlagAssigningCompleter { |
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.
module: ModuleSymbol
def parse(file: AbstractFile, root: Symbol): Unit = { | ||
debuglog("[class] >> " + root.fullName) | ||
|
||
def parse(file: AbstractFile, clazz: Symbol, module: Symbol): Unit = { |
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.
module: ModuleSymbol
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 allows callers to pass unrelated symbols. Maybe some warning in a comment is in order (I assume calls to companionModule
& friends are also "forbidden" inside this method). If so, also worth a comment.
@@ -309,7 +313,7 @@ abstract class SymbolLoaders { | |||
// errors. More concretely, the classfile parser calls "sym.companionModule", which calls | |||
// "isModuleNotMethod" on the companion. After refchecks, this method forces the info, which | |||
// may run the classfile parser. This produces the error. | |||
enteringPhase(phaseBeforeRefchecks)(classfileParser.parse(classfile, root)) | |||
enteringPhase(phaseBeforeRefchecks)(classfileParser.parse(classfile, clazz, module)) |
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 comment seems outdated, the classfile parser doesn't call companionModule
anymore. Maybe there's no need to call enteringPhase
either?
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.
Hmm.. I think it's a bit risky to remove the phase travel here, the classfile parser and unpickler call various methods on symbols. I'll update the 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.
Though I see it was added not too long ago: 0ccdb15
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'll give it a try.
Skipping other annotations not only saves some cycles / GC, but also prevents some spurious warnings / errors related to cyclic dependencies when parsing annotation arguments refering to members of the class.
Tighten some types (Symbol -> ClassSymbol / ModuleSymbol), use NonFatal instead of catching Throwable. Also don't run the classfile parser enteringPhase(phaseBeforeRefchecks) anymore. This was added in 0ccdb15 but seems no longer required.
LGTM! |
Frontend fixes for scala/scala-dev#248
See individual commit messages.