From ad79d74deef6e624aa7048543207ec97810f07f5 Mon Sep 17 00:00:00 2001 From: Jason Zaugg Date: Mon, 25 Mar 2013 08:53:24 +0100 Subject: SI-7296 Avoid crash with nested 23-param case class The implementation restriction doesn't stop subsequent typechecking in the same compilation unit from triggering type completion which tries to synthesize the unapply method. This commit predicates generation of the unapply method on having 22 or fewer parameters. --- src/compiler/scala/tools/nsc/typechecker/Namers.scala | 4 +++- test/files/neg/t7296.check | 4 ++++ test/files/neg/t7296.scala | 6 ++++++ 3 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 test/files/neg/t7296.check create mode 100644 test/files/neg/t7296.scala diff --git a/src/compiler/scala/tools/nsc/typechecker/Namers.scala b/src/compiler/scala/tools/nsc/typechecker/Namers.scala index e966cc9060..1f54671ad0 100644 --- a/src/compiler/scala/tools/nsc/typechecker/Namers.scala +++ b/src/compiler/scala/tools/nsc/typechecker/Namers.scala @@ -1373,7 +1373,9 @@ trait Namers extends MethodSynthesis { if (!cdef.symbol.hasAbstractFlag) namer.enterSyntheticSym(caseModuleApplyMeth(cdef)) - namer.enterSyntheticSym(caseModuleUnapplyMeth(cdef)) + val primaryConstructorArity = treeInfo.firstConstructorArgs(cdef.impl.body).size + if (primaryConstructorArity <= MaxTupleArity) + namer.enterSyntheticSym(caseModuleUnapplyMeth(cdef)) } def addCopyMethod(cdef: ClassDef, namer: Namer) { diff --git a/test/files/neg/t7296.check b/test/files/neg/t7296.check new file mode 100644 index 0000000000..66e8353ee3 --- /dev/null +++ b/test/files/neg/t7296.check @@ -0,0 +1,4 @@ +t7296.scala:5: error: Implementation restriction: case classes cannot have more than 22 parameters. + case class Foo(a: A, b: A, c: A, d: A, e: A, f: A, g: A, h: A, i: A, j: A, k: A, l: A, m: A, n: A, o: A, p: A, q: A, r: A, s: A, t: A, u: A, v: A, w: A, x: A, y: A, Z: A) + ^ +one error found diff --git a/test/files/neg/t7296.scala b/test/files/neg/t7296.scala new file mode 100644 index 0000000000..0c078d3657 --- /dev/null +++ b/test/files/neg/t7296.scala @@ -0,0 +1,6 @@ +object Test { + type A = Int + // Emits the implementation restriction but then proceeds to crash + // when creating the Foo.unapply. + case class Foo(a: A, b: A, c: A, d: A, e: A, f: A, g: A, h: A, i: A, j: A, k: A, l: A, m: A, n: A, o: A, p: A, q: A, r: A, s: A, t: A, u: A, v: A, w: A, x: A, y: A, Z: A) +} -- cgit v1.2.3 From 844cef628c809de24d908b9a51760ff33d0db345 Mon Sep 17 00:00:00 2001 From: Jason Zaugg Date: Sat, 19 Jan 2013 00:43:15 +0100 Subject: SI-7296 Remove arity limit for case classes When venturing above the pre-ordained limit of twenty two, `Companion extends FunctionN` and `Companion.unapply` are sacrificed. But oh-so-many other case class features work perfectly: equality/hashing/stringification, the apply method, and even pattern matching (which already bypasses unapply.) There was some understandable fear of the piecemeal when I tabled this idea on scala-internals [1]. But I'd like to persist as this limit is a needless source of pain for anyone using case classes to bind to database, XML or JSON schemata. [1] https://groups.google.com/forum/#!topic/scala-internals/RRu5bppi16Y --- .../tools/nsc/typechecker/ContextErrors.scala | 3 - .../scala/tools/nsc/typechecker/Namers.scala | 3 - .../scala/tools/nsc/typechecker/Typers.scala | 106 +++++++++++---------- .../scala/tools/nsc/typechecker/Unapplies.scala | 15 ++- test/files/neg/t3631.check | 4 - test/files/neg/t3631.scala | 3 - test/files/neg/t7296.check | 4 - test/files/neg/t7296.scala | 6 -- test/files/pos/t3631.scala | 3 + test/files/pos/t7296.scala | 6 ++ test/files/run/case-class-23.check | 2 + test/files/run/case-class-23.scala | 33 +++++++ 12 files changed, 111 insertions(+), 77 deletions(-) delete mode 100644 test/files/neg/t3631.check delete mode 100644 test/files/neg/t3631.scala delete mode 100644 test/files/neg/t7296.check delete mode 100644 test/files/neg/t7296.scala create mode 100644 test/files/pos/t3631.scala create mode 100644 test/files/pos/t7296.scala create mode 100644 test/files/run/case-class-23.check create mode 100644 test/files/run/case-class-23.scala diff --git a/src/compiler/scala/tools/nsc/typechecker/ContextErrors.scala b/src/compiler/scala/tools/nsc/typechecker/ContextErrors.scala index 8723046728..85c44a7ec4 100644 --- a/src/compiler/scala/tools/nsc/typechecker/ContextErrors.scala +++ b/src/compiler/scala/tools/nsc/typechecker/ContextErrors.scala @@ -1058,9 +1058,6 @@ trait ContextErrors { issueSymbolTypeError(currentSym, prevSym.name + " is already defined as " + s2 + s3 + where) } - def MaxParametersCaseClassError(tree: Tree) = - issueNormalTypeError(tree, "Implementation restriction: case classes cannot have more than " + definitions.MaxFunctionArity + " parameters.") - def MissingParameterOrValTypeError(vparam: Tree) = issueNormalTypeError(vparam, "missing parameter type") diff --git a/src/compiler/scala/tools/nsc/typechecker/Namers.scala b/src/compiler/scala/tools/nsc/typechecker/Namers.scala index 1f54671ad0..4920f32bf3 100644 --- a/src/compiler/scala/tools/nsc/typechecker/Namers.scala +++ b/src/compiler/scala/tools/nsc/typechecker/Namers.scala @@ -654,9 +654,6 @@ trait Namers extends MethodSynthesis { tree.symbol setInfo completerOf(tree) if (mods.isCase) { - if (primaryConstructorArity > MaxFunctionArity) - MaxParametersCaseClassError(tree) - val m = ensureCompanionObject(tree, caseModuleDef) m.moduleClass.updateAttachment(new ClassForCaseCompanionAttachment(tree)) } diff --git a/src/compiler/scala/tools/nsc/typechecker/Typers.scala b/src/compiler/scala/tools/nsc/typechecker/Typers.scala index 0e57145343..8669ef3bb5 100644 --- a/src/compiler/scala/tools/nsc/typechecker/Typers.scala +++ b/src/compiler/scala/tools/nsc/typechecker/Typers.scala @@ -944,6 +944,57 @@ trait Typers extends Adaptations with Tags { // due to wrapClassTagUnapply, but when we support parameterized extractors, it will become // more common place) val extractor = overloadedExtractorOfObject orElse unapplyMember(tree.tpe) + def convertToCaseConstructor(clazz: Symbol): TypeTree = { + // convert synthetic unapply of case class to case class constructor + val prefix = tree.tpe.prefix + val tree1 = TypeTree(clazz.primaryConstructor.tpe.asSeenFrom(prefix, clazz.owner)) + .setOriginal(tree) + + val skolems = new mutable.ListBuffer[TypeSymbol] + object variantToSkolem extends TypeMap(trackVariance = true) { + def apply(tp: Type) = mapOver(tp) match { + // !!! FIXME - skipping this when variance.isInvariant allows unsoundness, see SI-5189 + case TypeRef(NoPrefix, tpSym, Nil) if !variance.isInvariant && tpSym.isTypeParameterOrSkolem && tpSym.owner.isTerm => + // must initialize or tpSym.tpe might see random type params!! + // without this, we'll get very weird types inferred in test/scaladoc/run/SI-5933.scala + // TODO: why is that?? + tpSym.initialize + val bounds = if (variance.isPositive) TypeBounds.upper(tpSym.tpe) else TypeBounds.lower(tpSym.tpe) + // origin must be the type param so we can deskolemize + val skolem = context.owner.newGADTSkolem(unit.freshTypeName("?"+tpSym.name), tpSym, bounds) + // println("mapping "+ tpSym +" to "+ skolem + " : "+ bounds +" -- pt= "+ pt +" in "+ context.owner +" at "+ context.tree ) + skolems += skolem + skolem.tpe + case tp1 => tp1 + } + } + + // have to open up the existential and put the skolems in scope + // can't simply package up pt in an ExistentialType, because that takes us back to square one (List[_ <: T] == List[T] due to covariance) + val ptSafe = variantToSkolem(pt) // TODO: pt.skolemizeExistential(context.owner, tree) ? + val freeVars = skolems.toList + + // use "tree" for the context, not context.tree: don't make another CaseDef context, + // as instantiateTypeVar's bounds would end up there + val ctorContext = context.makeNewScope(tree, context.owner) + freeVars foreach ctorContext.scope.enter + newTyper(ctorContext).infer.inferConstructorInstance(tree1, clazz.typeParams, ptSafe) + + // simplify types without losing safety, + // so that we get rid of unnecessary type slack, and so that error messages don't unnecessarily refer to skolems + val extrapolate = new ExistentialExtrapolation(freeVars) extrapolate (_: Type) + val extrapolated = tree1.tpe match { + case MethodType(ctorArgs, res) => // ctorArgs are actually in a covariant position, since this is the type of the subpatterns of the pattern represented by this Apply node + ctorArgs foreach (p => p.info = extrapolate(p.info)) // no need to clone, this is OUR method type + copyMethodType(tree1.tpe, ctorArgs, extrapolate(res)) + case tp => tp + } + + // once the containing CaseDef has been type checked (see typedCase), + // tree1's remaining type-slack skolems will be deskolemized (to the method type parameter skolems) + tree1 setType extrapolated + } + if (extractor != NoSymbol) { // if we did some ad-hoc overloading resolution, update the tree's symbol // do not update the symbol if the tree's symbol's type does not define an unapply member @@ -959,59 +1010,16 @@ trait Typers extends Adaptations with Tags { val clazz = unapplyParameterType(unapply) if (unapply.isCase && clazz.isCase) { - // convert synthetic unapply of case class to case class constructor - val prefix = tree.tpe.prefix - val tree1 = TypeTree(clazz.primaryConstructor.tpe.asSeenFrom(prefix, clazz.owner)) - .setOriginal(tree) - - val skolems = new mutable.ListBuffer[TypeSymbol] - object variantToSkolem extends TypeMap(trackVariance = true) { - def apply(tp: Type) = mapOver(tp) match { - // !!! FIXME - skipping this when variance.isInvariant allows unsoundness, see SI-5189 - case TypeRef(NoPrefix, tpSym, Nil) if !variance.isInvariant && tpSym.isTypeParameterOrSkolem && tpSym.owner.isTerm => - // must initialize or tpSym.tpe might see random type params!! - // without this, we'll get very weird types inferred in test/scaladoc/run/SI-5933.scala - // TODO: why is that?? - tpSym.initialize - val bounds = if (variance.isPositive) TypeBounds.upper(tpSym.tpe) else TypeBounds.lower(tpSym.tpe) - // origin must be the type param so we can deskolemize - val skolem = context.owner.newGADTSkolem(unit.freshTypeName("?"+tpSym.name), tpSym, bounds) - // println("mapping "+ tpSym +" to "+ skolem + " : "+ bounds +" -- pt= "+ pt +" in "+ context.owner +" at "+ context.tree ) - skolems += skolem - skolem.tpe - case tp1 => tp1 - } - } - - // have to open up the existential and put the skolems in scope - // can't simply package up pt in an ExistentialType, because that takes us back to square one (List[_ <: T] == List[T] due to covariance) - val ptSafe = variantToSkolem(pt) // TODO: pt.skolemizeExistential(context.owner, tree) ? - val freeVars = skolems.toList - - // use "tree" for the context, not context.tree: don't make another CaseDef context, - // as instantiateTypeVar's bounds would end up there - val ctorContext = context.makeNewScope(tree, context.owner) - freeVars foreach ctorContext.scope.enter - newTyper(ctorContext).infer.inferConstructorInstance(tree1, clazz.typeParams, ptSafe) - - // simplify types without losing safety, - // so that we get rid of unnecessary type slack, and so that error messages don't unnecessarily refer to skolems - val extrapolate = new ExistentialExtrapolation(freeVars) extrapolate (_: Type) - val extrapolated = tree1.tpe match { - case MethodType(ctorArgs, res) => // ctorArgs are actually in a covariant position, since this is the type of the subpatterns of the pattern represented by this Apply node - ctorArgs foreach (p => p.info = extrapolate(p.info)) // no need to clone, this is OUR method type - copyMethodType(tree1.tpe, ctorArgs, extrapolate(res)) - case tp => tp - } - - // once the containing CaseDef has been type checked (see typedCase), - // tree1's remaining type-slack skolems will be deskolemized (to the method type parameter skolems) - tree1 setType extrapolated + convertToCaseConstructor(clazz) } else { tree } } else { - CaseClassConstructorError(tree) + val clazz = tree.tpe.typeSymbol.linkedClassOfClass + if (clazz.isCase) + convertToCaseConstructor(clazz) + else + CaseClassConstructorError(tree) } } diff --git a/src/compiler/scala/tools/nsc/typechecker/Unapplies.scala b/src/compiler/scala/tools/nsc/typechecker/Unapplies.scala index 589e5ce6fd..d55dce70c7 100644 --- a/src/compiler/scala/tools/nsc/typechecker/Unapplies.scala +++ b/src/compiler/scala/tools/nsc/typechecker/Unapplies.scala @@ -115,11 +115,16 @@ trait Unapplies extends ast.TreeDSL /** The module corresponding to a case class; overrides toString to show the module's name */ def caseModuleDef(cdef: ClassDef): ModuleDef = { - // > MaxFunctionArity is caught in Namers, but for nice error reporting instead of - // an abrupt crash we trim the list here. - def primaries = constrParamss(cdef).head take MaxFunctionArity map (_.tpt) - def inheritFromFun = !cdef.mods.hasAbstractFlag && cdef.tparams.isEmpty && constrParamss(cdef).length == 1 - def createFun = gen.scalaFunctionConstr(primaries, toIdent(cdef), abstractFun = true) + val params = constrParamss(cdef) + def inheritFromFun = !cdef.mods.hasAbstractFlag && cdef.tparams.isEmpty && (params match { + case List(ps) if ps.length <= MaxFunctionArity => true + case _ => false + }) + def createFun = { + def primaries = params.head map (_.tpt) + gen.scalaFunctionConstr(primaries, toIdent(cdef), abstractFun = true) + } + def parents = if (inheritFromFun) List(createFun) else Nil def toString = DefDef( Modifiers(OVERRIDE | FINAL | SYNTHETIC), diff --git a/test/files/neg/t3631.check b/test/files/neg/t3631.check deleted file mode 100644 index 6d8feca1ed..0000000000 --- a/test/files/neg/t3631.check +++ /dev/null @@ -1,4 +0,0 @@ -t3631.scala:3: error: Implementation restriction: case classes cannot have more than 22 parameters. -case class X23(x1: Int, x2: Int, x3: Int, x4: Int, x5: Int, x6: Int, x7: Int, x8: Int, x9: Int, x10: Int, x11: Int, x12: Int, x13: Int, x14: Int, x15: Int, x16: Int, x17: Int, x18: Int, x19: Int, x20: Int, x21: Int, x22: Int, x23: Int) { } - ^ -one error found diff --git a/test/files/neg/t3631.scala b/test/files/neg/t3631.scala deleted file mode 100644 index bcf91619ee..0000000000 --- a/test/files/neg/t3631.scala +++ /dev/null @@ -1,3 +0,0 @@ -case class X22(x1: Int, x2: Int, x3: Int, x4: Int, x5: Int, x6: Int, x7: Int, x8: Int, x9: Int, x10: Int, x11: Int, x12: Int, x13: Int, x14: Int, x15: Int, x16: Int, x17: Int, x18: Int, x19: Int, x20: Int, x21: Int, x22: Int) { } - -case class X23(x1: Int, x2: Int, x3: Int, x4: Int, x5: Int, x6: Int, x7: Int, x8: Int, x9: Int, x10: Int, x11: Int, x12: Int, x13: Int, x14: Int, x15: Int, x16: Int, x17: Int, x18: Int, x19: Int, x20: Int, x21: Int, x22: Int, x23: Int) { } \ No newline at end of file diff --git a/test/files/neg/t7296.check b/test/files/neg/t7296.check deleted file mode 100644 index 66e8353ee3..0000000000 --- a/test/files/neg/t7296.check +++ /dev/null @@ -1,4 +0,0 @@ -t7296.scala:5: error: Implementation restriction: case classes cannot have more than 22 parameters. - case class Foo(a: A, b: A, c: A, d: A, e: A, f: A, g: A, h: A, i: A, j: A, k: A, l: A, m: A, n: A, o: A, p: A, q: A, r: A, s: A, t: A, u: A, v: A, w: A, x: A, y: A, Z: A) - ^ -one error found diff --git a/test/files/neg/t7296.scala b/test/files/neg/t7296.scala deleted file mode 100644 index 0c078d3657..0000000000 --- a/test/files/neg/t7296.scala +++ /dev/null @@ -1,6 +0,0 @@ -object Test { - type A = Int - // Emits the implementation restriction but then proceeds to crash - // when creating the Foo.unapply. - case class Foo(a: A, b: A, c: A, d: A, e: A, f: A, g: A, h: A, i: A, j: A, k: A, l: A, m: A, n: A, o: A, p: A, q: A, r: A, s: A, t: A, u: A, v: A, w: A, x: A, y: A, Z: A) -} diff --git a/test/files/pos/t3631.scala b/test/files/pos/t3631.scala new file mode 100644 index 0000000000..bcf91619ee --- /dev/null +++ b/test/files/pos/t3631.scala @@ -0,0 +1,3 @@ +case class X22(x1: Int, x2: Int, x3: Int, x4: Int, x5: Int, x6: Int, x7: Int, x8: Int, x9: Int, x10: Int, x11: Int, x12: Int, x13: Int, x14: Int, x15: Int, x16: Int, x17: Int, x18: Int, x19: Int, x20: Int, x21: Int, x22: Int) { } + +case class X23(x1: Int, x2: Int, x3: Int, x4: Int, x5: Int, x6: Int, x7: Int, x8: Int, x9: Int, x10: Int, x11: Int, x12: Int, x13: Int, x14: Int, x15: Int, x16: Int, x17: Int, x18: Int, x19: Int, x20: Int, x21: Int, x22: Int, x23: Int) { } \ No newline at end of file diff --git a/test/files/pos/t7296.scala b/test/files/pos/t7296.scala new file mode 100644 index 0000000000..0c078d3657 --- /dev/null +++ b/test/files/pos/t7296.scala @@ -0,0 +1,6 @@ +object Test { + type A = Int + // Emits the implementation restriction but then proceeds to crash + // when creating the Foo.unapply. + case class Foo(a: A, b: A, c: A, d: A, e: A, f: A, g: A, h: A, i: A, j: A, k: A, l: A, m: A, n: A, o: A, p: A, q: A, r: A, s: A, t: A, u: A, v: A, w: A, x: A, y: A, Z: A) +} diff --git a/test/files/run/case-class-23.check b/test/files/run/case-class-23.check new file mode 100644 index 0000000000..888ed2c9eb --- /dev/null +++ b/test/files/run/case-class-23.check @@ -0,0 +1,2 @@ +23 +(1,23) diff --git a/test/files/run/case-class-23.scala b/test/files/run/case-class-23.scala new file mode 100644 index 0000000000..92b719574a --- /dev/null +++ b/test/files/run/case-class-23.scala @@ -0,0 +1,33 @@ +case class TwentyThree( + _1: Int, + _2: Int, + _3: Int, + _4: Int, + _5: Int, + _6: Int, + _7: Int, + _8: Int, + _9: Int, + _10: Int, + _11: Int, + _12: Int, + _13: Int, + _14: Int, + _15: Int, + _16: Int, + _17: Int, + _18: Int, + _19: Int, + _20: Int, + _21: Int, + _22: Int, + _23: Int +) + +object Test extends App { + val x = new TwentyThree(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23) + println(x._23) + assert(x.copy(_1 = 1) == x) + val TwentyThree(a, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, b) = x + println((a, b)) +} -- cgit v1.2.3