-
Notifications
You must be signed in to change notification settings - Fork 1.1k
fix(#19266): better warning for impure synthetic lambda in stmt position #19540
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
base: main
Are you sure you want to change the base?
fix(#19266): better warning for impure synthetic lambda in stmt position #19540
Conversation
tests/pos/i19266.scala
Outdated
def fn2(x: Int)(y: String) = Some(s"$x$y") | ||
|
||
def checkCompile = | ||
fn1(1) |
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.
Scala 2.13.12 with -Xsource:3
compiles with warnings; " a pure expression does nothing in statement position".
Scala 2.13.12 without -Xsource:3
raises errors for fn1(1)
, fn2(2)
and fn2(3)
.
Unapplied methods are only converted to functions when a function type is expected.
You can make this conversion explicit by writingfn2 _
orfn2(_)(_)
instead offn2
.
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.
Do the following to unapplied methods used as value
What characteristic allows us to assume unapplied methods are actually used as value?
* (4) Do the following to unapplied methods used as values:
* (4.1) If the method has only implicit parameters pass implicit arguments
* (4.2) otherwise, if `pt` is a function type and method is not a constructor,
* convert to function by eta-expansion,
* (4.3) otherwise, if the method is nullary with a result type compatible to `pt`
* and it is not a constructor, apply it to ()
* otherwise issue an error
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.
https://github.com/lampepfl/dotty/blob/main/docs/_spec/06-expressions.md
Method Conversions
The following four implicit conversions can be applied to methods which are not applied to some argument list.
Evaluation
A parameterless method ´m´ of type
=> ´T´
is always converted to type ´T´ by evaluating the expression to which ´m´ is bound.Implicit Application
If the method takes only implicit parameters, implicit arguments are passed following the rules here.
Eta Expansion
Otherwise, if the method is not a constructor, and the expected type ´\mathit{pt}´ is a function type, or, for methods of non-zero arity, a type sam-convertible to a function type, ´(\mathit{Ts}') \Rightarrow T'´, eta-expansion is performed on the expression ´e´.
(The exception for zero-arity methods is to avoid surprises due to unexpected sam conversion.)
Empty Application
Otherwise, if ´e´ has method type ´()T´, it is implicitly applied to the empty argument list, yielding ´e()´.
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.
In the following example m can be partially applied to the first two parameters. Assigning m to f1 will automatically eta-expand
def m(x: Boolean, y: String)(z: Int): List[Int] val f1 = m val f2 = m(true, "abc")
(The emphasis is added by me)
What should happen if m
is not assigned to value?
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.
tests/pos/i19266.scala
Outdated
@@ -0,0 +1,9 @@ | |||
object i19266: |
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.
note:
def fn1(x: Int, y: String = "y")(z: Double) = Some(s"$x$y$z")
def fn2(p: Int)(q: String) = Some(s"$p$q")
def checkCompile =
// This is NOT regarded as `isPureExpr`, but I think should be to warn `nonunit-statement`.
// See https://github.com/lampepfl/dotty/issues/19266#issuecomment-1856248380
fn1(1)
// This is regarded as `isPureExpr` and therefore it results in error.
fn2(2)
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.
Block(
List(
ValDef(
y$1,
TypeTree[AnnotatedType(TypeRef(TermRef(ThisType(TypeRef(NoPrefix,module class scala)),object Predef),type String),ConcreteAnnotation(Apply(Select(New(TypeTree[TypeRef(ThisType(TypeRef(NoPrefix,module class unchecked)),class uncheckedVariance)]),<init>),List())))],
Select(This(Ident(i19266$)),fn1$default$2)
)
),
Block(
List(
DefDef(
$anonfun,
List(List(ValDef(z,TypeTree[TypeRef(TermRef(ThisType(TypeRef(NoPrefix,module class <root>)),object scala),class Double)],EmptyTree))),
TypeTree[AppliedType(TypeRef(ThisType(TypeRef(NoPrefix,module class scala)),class Some),List(TypeRef(ThisType(TypeRef(NoPrefix,module class lang)),class String)))],
Apply(Apply(Ident(fn1),List(Literal(Constant(1)), Ident(y$1))),List(Ident(z)))
)
),
Closure(List(),Ident($anonfun),EmptyTree)
)
)
Block(
List(
DefDef(
$anonfun,
List(List(ValDef(q,TypeTree[TypeRef(TermRef(ThisType(TypeRef(NoPrefix,module class scala)),object Predef),type String)],EmptyTree))),
TypeTree[AppliedType(TypeRef(ThisType(TypeRef(NoPrefix,module class scala)),class Some),List(TypeRef(ThisType(TypeRef(NoPrefix,module class lang)),class String)))],
Apply(Apply(Ident(fn2),List(Literal(Constant(2)))),List(Ident(q)))
)
),
Closure(List(),Ident($anonfun),EmptyTree)
)
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.
f1(1)
is regarded as Block with ValDef
statement and Block
expr whereas f2(2)
is regarded as Block
with DefDef
statement and Closure
.
Note for myself. Default by-value arguments may have side-effect, so it is reasonable not to warn " A pure expression does nothing in statement position". Perhaps, we should warn NonUnitUnusedValue. Method without default argument and method with default arguments for by-name parameters are pure It seems an expected behavior for compiler to raise error rather than warning. import scala.collection.mutable.ArrayBuffer
import scala.util.Random
val arr = ArrayBuffer[Int]()
def fn0(x: Int)(y: String) = Some(s"$x$y")
def fn1(x: Int, y: Unit = arr.addOne(Random.nextInt()))(z:Double) = Some(s"$x,$y,$z")
def fn2(x: Int, y: => Unit = arr.addOne(Random.nextInt()))(z:Double) = Some(s"$x,$y,$z")
// This is pure and falls into the missing argument arm
// here https://github.com/lampepfl/dotty/blob/1716bcd9dbefbef88def848c09768a698b6b9ed9/tests/pos-with-compiler-cc/dotc/typer/Typer.scala#L4203
fn0(0)
// This is impure(perform side-effect to mutate `arr`). Therefore this passes through `checkStatementPurity`.
fn1(1)
// This is pure and falls into the missing argument arm
// https://github.com/lampepfl/dotty/blob/1716bcd9dbefbef88def848c09768a698b6b9ed9/tests/pos-with-compiler-cc/dotc/typer/Typer.scala#L4203
fn2(2)
|
It seems expected behavior to raise error rather than warning 🤔
|
2645e36
to
a953135
Compare
synthetic lambda generated from a method with default arguments skipped through `closureDef` pattern.
Before this commit, closureDef matching rule was not strict enough to filter out false-positive cases, which caused tests/init-global/neg/return2.scala to fail. It matched a plain block with ValDef at the begining and Closure at the end. As we want to match `closureDef` against a closure or a synthetic closure generated from eta-expansion, we should match against 1. a simple closure 2. a closure inside nested blocks without statement, which is generated from eta-expansion on method without default arguments 3. a closure inside nested blocks with statements consisting only of ValDef of default arguments, which is generated from eta-expansion on method with default arguments. This commit adds the condition for 3.
@i10416 before I merge this, what's the status from your perspective? Are your commits here ready to be merged? I think your arguments are very reasonable and your changes are right. But I wonder if it would be better to be bolder to make things simpler. I'm thinking we make closures not compile, whether they're pure or not. Using your example: import scala.collection.mutable.ArrayBuffer
import scala.util.Random
val arr = ArrayBuffer[Int]()
def fn0(x: Int)(y: String) = Some(s"$x,$y")
def fn1(x: Int, y: Unit = arr.addOne(Random.nextInt()))(z:Double) = Some(s"$x,$y,$z")
def fn2(x: Int, y: => Unit = arr.addOne(Random.nextInt()))(z:Double) = Some(s"$x,$y,$z")
// Thus, it shouldn't compile.
fn0(0)
// This is impure(perform side-effect to mutate `arr`). Therefore this passes through `checkStatementPurity`.
fn1(1)
// This is pure and falls into the missing argument arm
// https://github.com/lampepfl/dotty/blob/1716bcd9dbefbef88def848c09768a698b6b9ed9/tests/pos-with-compiler-cc/dotc/typer/Typer.scala#L4203
// Thus, it shouldn't compile.
fn2(2) I think we should just also make |
Yes, it is ready for review.
I agree with you. It feels consistent to prohibit lambda in statement position regardless of its purity. I will add some changes and let's see whether any projects in the community build fail to compile. |
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 think if we want an unused warning (which for me is debatable, but I am I know little about unused policy and am generally considered a skeptic), we need to implement this in the linting phase, not typer. That phase could use a new extractor similar to clsoureDef
, but it should not change the original closureDef
extractor.
In particular, we should not change fundamental abstractions like a closureDef
extractor just because it is more convenient for some lint. The risk this breaks something in type inference or code generation is far too big.
if isPure then | ||
missingArgs(tree, tree.tpe.widen) | ||
else | ||
report.warning(UnusedNonUnitValue(tree.tpe),original.srcPos) |
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.
A UnusedNonUnitValue warning would have to be issued by the linter. It's a conscious design decision not to mix typing and linting one one phase. Typing is mandatory, linting is optional. Typing is precise according to spec, whereas linting is ad-hoc and not specified by the language.
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.
FWIW, this is a challenge in scala 2, which is more constrained. Typer has information you want to exploit which may not be available later, but you don't want to bloat or break typer; scala 2 resorts to attachments. In this case, I don't care about purity, -Wnonunit-statement
should always warn. Maybe it won't issue a duplicate warning if the positions are the same.
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 challenge in scala 2, ... Typer has information you want to exploit which may not be available later
I wonder we can take advantage of Tasty information for linting. I guess the difficulty would be some linting might depend on compiler state that would not be available from Tasty.
@@ -790,10 +790,13 @@ trait TypedTreeInfo extends TreeInfo[Type] { self: Trees.Instance[Type] => | |||
|
|||
/** An extractor for def of a closure contained the block of the closure. */ | |||
object closureDef { | |||
private def isDefaultArg(tree:Tree): Boolean = tree match |
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 don't think we should mix default arguments with this extractor. I prefer to leave it as it was before.
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.
just because it is more convenient for some lint. ...
#19540 (review)
I also cared about it and when I added isDefaultArg
, I thought Block(Nil, expr)
here is the special case for closure where default argument stmts
is empty, therefore this change would be safe.
Do you think the // closure 1
in the following example is not closure?
Block(Nil, expr)
{
{
// closure 0
}
}
Block(stmts, expr)
{
// These are synthesized ValDefs from default arguments invisible to users.
val meth$default0 = ???
val meth$default1 = ???
// ...
{
// closure 1
}
}
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 understand, but I don't think we should generalize closureDef
that way.
I also cared about it and when I added isDefaultArg, I thought Block(Nil, expr) here is the special case for closure where default argument stmts is empty, therefore this change would be safe.
That's one of the scenarios. But the compiler is also free to wrap closureDef in a block in other situations.
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.
Sure, I will change it back. Thank you for feedback.
What do you think about prohibiting impure lambda in statement position?
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.
We could try that and see what breaks. The comments on #11769 indicate that this won't be easy, though.
Apply code review to simplify condition arms - https://github.com/lampepfl/dotty/pull/19540/files#r1501178130
Needs a rebase, for the new onnx submodule version. |
close #19266
Before this PR,
checkStatementPurity
filtered out impure expression too eagerly andclosureDef
ignores closure definition generated from eta-expansion on method with default arguments, which prevented impure lambda at statement position from showing a warning.This PR modifies
checkStatementPurity
to check expression purity at later so that impure lambda will not slip through the checkclosureDef
to match against a synthetic closure from eta-expansion on method with default argumentsRelated to