Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions effekt/shared/src/main/scala/effekt/Parser.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1142,8 +1142,9 @@ class Parser(tokens: Seq[Token], source: Source) {
braces {
peek.kind match {
// { case ... => ... }
case `case` => someWhile(matchClause(), `case`) match { case cs =>
case `case` =>
nonterminal:
val cs = someWhile(matchClause(), `case`)
val argSpans = cs match {
case Many(MatchClause(MultiPattern(ps, _), _, _, _) :: _, _) => ps.map(_.span)
case p => List(p.span)
Expand All @@ -1164,7 +1165,7 @@ class Parser(tokens: Seq[Token], source: Source) {
), span().synthesized),
span().synthesized
)
}

case _ =>
// { (x: Int) => ... }
nonterminal:
Expand Down
153 changes: 112 additions & 41 deletions effekt/shared/src/main/scala/effekt/Typer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -975,7 +975,7 @@ object Typer extends Phase[NameResolved, Typechecked] {
val bt @ FunctionType(tps, cps, vps, bps, tpe1, effs) = expected

// (2) Check wellformedness (that type, value, block params and args align)
assertArgsParamsAlign("function", tparams.size, vparams.size, bparams.size, tps.size, vps.size, bps.size)
assertArgsParamsAlign(name = None, Aligned(tparams, tps), Aligned(vparams, vps), Aligned(bparams, bps))

// (3) Substitute type parameters
val typeParams = tparams.map { p => p.symbol.asTypeParam }
Expand Down Expand Up @@ -1268,63 +1268,134 @@ object Typer extends Phase[NameResolved, Typechecked] {
}
}

/** Result of trying to align 'got' and 'expected' with matched pairs and mismatches */
case class Aligned[+A, +B](
matched: List[(A, B)], /// got and expected paired up
extra: List[A], /// got but not expected
missing: List[B] /// expected but not got
) {
def isAligned: Boolean = extra.isEmpty && missing.isEmpty

def gotCount: Int = matched.size + extra.size
def expectedCount: Int = matched.size + missing.size
def delta: Int = extra.size - missing.size // > 0 => too many
}

object Aligned {
def apply[A, B](got: List[A], expected: List[B]): Aligned[A, B] = {
@scala.annotation.tailrec
def loop(got: List[A], expected: List[B], matched: List[(A, B)]): Aligned[A, B] =
(got, expected) match {
case (Nil, Nil) => Aligned(matched.reverse, Nil, Nil)
case (extra, Nil) => Aligned(matched.reverse, extra, Nil)
case (Nil, missing) => Aligned(matched.reverse, Nil, missing)
case (g :: gs, e :: es) => loop(gs, es, (g, e) :: matched)
}

loop(got, expected, Nil)
}
}

/**
* Asserts that number of {type, value, block} arguments is the same as
* the number of {type, value, block} parameters.
* If not, aborts the context with a nice error message.
* Also tries to add 'did you mean' context for the user on common errors.
*
* @param name None if it's a block literal, otherwise the expected name
*/
private def assertArgsParamsAlign(
name: String,
gotTypes: Int, gotValues: Int, gotBlocks: Int,
expectedTypes: Int, expectedValues: Int, expectedBlocks: Int
)(using Context): Unit = {
name: Option[String],
types: Aligned[source.Id | ValueType, TypeParam],
values: Aligned[source.ValueParam | source.ValueArg, ValueType],
blocks: Aligned[source.BlockParam | source.Term, BlockType]
)(using Context): Unit = {

val targsOk = gotTypes == 0 || gotTypes == expectedTypes
val vargsOk = gotValues == expectedValues
val bargsOk = gotBlocks == expectedBlocks
// Type args are OK iff nothing provided or perfectly aligned
val typesOk = types.gotCount == 0 || types.isAligned

def pluralized(n: Int, singular: String): String =
if (n == 1) s"$n $singular" else s"$n ${singular}s"

def formatArgs(types: Option[Int], values: Option[Int], blocks: Option[Int]): String = {
val parts = List(
types.map { pluralized(_, "type argument") },
values.map { pluralized(_, "value argument") },
blocks.map { pluralized(_, "block argument") }
).flatten

parts match {
case Nil => "no arguments"
case single :: Nil => single
case init :+ last => init.mkString(", ") + " and " + last
case _ => parts.mkString(", ")
// Hint: Tuple vs arg-list confusion (also covers lambda case)
if (!values.isAligned) {
// 1. User wrote `case (x, y) => ...` but function expects multiple arguments
// => Did we match exactly 1 value param with `__arg... ` name, and have multiple args missing
// HACK: Hardcoded '__arg' to recognize lambda case
(values.matched, values.extra, values.missing) match {
case (List((param: source.ValueParam, _)), Nil, _ :: _)
if param.id.name.startsWith("__arg") =>
Context.info(pretty"Did you mean to use `(x, y) => ...` instead of `case (x, y) => ...`?")
case _ => ()
}
}

val expected = formatArgs(
Option.when(!targsOk) { expectedTypes },
Option.when(!vargsOk) { expectedValues },
Option.when(!bargsOk) { expectedBlocks }
)
val got = formatArgs(
Option.when(!targsOk) { gotTypes },
Option.when(!vargsOk) { gotValues },
Option.when(!bargsOk) { gotBlocks }
)
// 2a. User wrote `(x, y) => ... ` but function expects a single tuple
// 2b. User wrote `foo(x, y)` but the function expects a single tuple
// => Multiple extra value args, exactly 1 missing that's a tuple matching the count
// HACK: Hardcoded "tuple is a type whose name starts with 'Tuple'"
(values.matched, values.extra, values.missing) match {
case (List((_, tupleTpe @ ValueTypeApp(TypeConstructor.Record(tupleName, _, _, _), args))), extras, Nil)
if tupleName.name.startsWith("Tuple") && args.size == 1 + extras.size =>
name match {
case None => Context.info(pretty"Did you mean to use `case (x, y) => ...` to pattern match on the tuple ${tupleTpe}?")
case Some(givenName) => Context.info(pretty"Did you mean to call ${givenName} with a single tuple argument instead of separate arguments?")
}
case _ => ()
}

// 3. User wrote `foo((x, y))` (tuple literal) but function expects multiple arguments
// => Single matched arg that's a Call to a Tuple constructor, with some missing params
// HACK: Hardcoded "tuple constructor is IdRef to TupleN in effekt namespace"
(values.matched, values.extra, values.missing) match {
case (List((arg: source.ValueArg, _)), Nil, missing@(_ :: _)) =>
(arg.value, name) match {
case (source.Call(source.IdTarget(source.IdRef(List("effekt"), tupleName, _)), _, tupleArgs, _, _), Some(givenName))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Polymorphism boxing used to use findPrelude to find these more precisely.

if tupleName.startsWith("Tuple") && tupleArgs.size == 1 + missing.size =>
Context.info(pretty"Did you mean to call ${givenName} with ${tupleArgs.size} separate arguments instead of a tuple?")
case _ => ()
}
case _ => ()
}
}

if (!vargsOk && !bargsOk && gotValues + gotBlocks == expectedValues + expectedBlocks) {
// Hint: Value vs block argument confusion
if (!values.isAligned && !blocks.isAligned && values.delta + blocks.delta == 0) {
// If total counts match, but individual do not, it's likely a value vs computation issue
if (gotBlocks > expectedBlocks) {
val diff = gotBlocks - expectedBlocks
Context.info(pretty"Did you mean to pass ${pluralized(diff, "block argument")} as a value? e.g. box it using `box { ... }`")
} else if (gotValues > expectedValues) {
val diff = gotValues - expectedValues
Context.info(pretty"Did you mean to pass ${pluralized(diff, "value argument")} as a block (computation)?")
if (blocks.delta > 0) {
Context.info(pretty"Did you mean to pass ${pluralized(blocks.delta, "block argument")} as a value? e.g. box it using `box { ... }`")
} else if (values.delta > 0) {
Context.info(pretty"Did you mean to pass ${pluralized(values.delta, "value argument")} as a block (computation)? ")
}
}

if (!targsOk || !vargsOk || !bargsOk) {
Context.abort(s"Wrong number of arguments to ${name}: expected ${expected}, but got ${got}")
if (!typesOk || !values.isAligned || !blocks.isAligned) {
def formatArgs(types: Option[Int], values: Option[Int], blocks: Option[Int]): String = {
val parts = List(
types.map { pluralized(_, "type argument") },
values.map { pluralized(_, "value argument") },
blocks.map { pluralized(_, "block argument") }
).flatten

parts match {
case Nil => "no arguments"
case single :: Nil => single
case init :+ last => init.mkString(", ") + " and " + last
case _ => parts.mkString(", ")
}
}

val expected = formatArgs(
Option.when(!typesOk) { types.expectedCount },
Option.when(!values.isAligned) { values.expectedCount },
Option.when(!blocks.isAligned) { blocks.expectedCount }
)
val got = formatArgs(
Option.when(!typesOk) { types.gotCount },
Option.when(!values.isAligned) { values.gotCount },
Option.when(!blocks.isAligned) { blocks.gotCount }
)

Context.abort(s"Wrong number of arguments to ${name getOrElse "function"}: expected ${expected}, but got ${got}")
}
}

Expand All @@ -1341,7 +1412,7 @@ object Typer extends Phase[NameResolved, Typechecked] {
val callsite = currentCapture

// (0) Check that arg & param counts align
assertArgsParamsAlign(name, targs.size, vargs.size, bargs.size, funTpe.tparams.size, funTpe.vparams.size, funTpe.bparams.size)
assertArgsParamsAlign(name = Some(name), Aligned(targs, funTpe.tparams), Aligned(vargs, funTpe.vparams), Aligned(bargs, funTpe.bparams))

// (1) Instantiate blocktype
// e.g. `[A, B] (A, A) => B` becomes `(?A, ?A) => ?B`
Expand Down
6 changes: 6 additions & 0 deletions examples/neg/typer/expected_args_got_lambdacase.check
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[error] examples/neg/typer/expected_args_got_lambdacase.effekt:3:20: Wrong number of arguments to function: expected 2 value arguments, but got 1 value argument
def main() = foo { case (n, s) =>
^
[info] examples/neg/typer/expected_args_got_lambdacase.effekt:3:20: Did you mean to use `(x, y) => ...` instead of `case (x, y) => ...`?
def main() = foo { case (n, s) =>
^
5 changes: 5 additions & 0 deletions examples/neg/typer/expected_args_got_lambdacase.effekt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
def foo { f: (Int, String) => Unit }: Unit = f(42, "hello")

def main() = foo { case (n, s) =>
println(n)
}
6 changes: 6 additions & 0 deletions examples/neg/typer/expected_args_got_tuple.check
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[error] examples/neg/typer/expected_args_got_tuple.effekt:4:11: Wrong number of arguments to foo: expected 2 value arguments, but got 1 value argument
val _ = foo((42, "hello"))
^^^^^^^^^^^^^^^^^^
[info] examples/neg/typer/expected_args_got_tuple.effekt:4:11: Did you mean to call foo with 2 separate arguments instead of a tuple?
val _ = foo((42, "hello"))
^^^^^^^^^^^^^^^^^^
5 changes: 5 additions & 0 deletions examples/neg/typer/expected_args_got_tuple.effekt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
def foo(x: Int, s: String): Int = 42

def main() = {
val _ = foo((42, "hello"))
}
6 changes: 6 additions & 0 deletions examples/neg/typer/expected_lambdacase_got_args.check
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[error] examples/neg/typer/expected_lambdacase_got_args.effekt:3:20: Wrong number of arguments to function: expected 1 value argument, but got 2 value arguments
def main() = foo { (n, s) =>
^
[info] examples/neg/typer/expected_lambdacase_got_args.effekt:3:20: Did you mean to use `case (x, y) => ...` to pattern match on the tuple Tuple2[Int, String]?
def main() = foo { (n, s) =>
^
5 changes: 5 additions & 0 deletions examples/neg/typer/expected_lambdacase_got_args.effekt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
def foo { f: ((Int, String)) => Unit }: Unit = f((42, "hello"))

def main() = foo { (n, s) =>
println(n)
}
6 changes: 6 additions & 0 deletions examples/neg/typer/expected_tuple_got_args.check
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[error] examples/neg/typer/expected_tuple_got_args.effekt:4:11: Wrong number of arguments to foo: expected 1 value argument, but got 2 value arguments
val _ = foo(42, "hello")
^^^^^^^^^^^^^^^^
[info] examples/neg/typer/expected_tuple_got_args.effekt:4:11: Did you mean to call foo with a single tuple argument instead of separate arguments?
val _ = foo(42, "hello")
^^^^^^^^^^^^^^^^
5 changes: 5 additions & 0 deletions examples/neg/typer/expected_tuple_got_args.effekt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
def foo(x: (Int, String)): Int = 42

def main() = {
val _ = foo(42, "hello")
}