From ef141bdceb812d69eed3a202ff0cde93c4c0b1c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Bene=C5=A1?= Date: Thu, 27 Nov 2025 14:56:38 +0100 Subject: [PATCH 1/7] WIP --- .../shared/src/main/scala/effekt/Typer.scala | 92 +++++++++++++------ 1 file changed, 66 insertions(+), 26 deletions(-) diff --git a/effekt/shared/src/main/scala/effekt/Typer.scala b/effekt/shared/src/main/scala/effekt/Typer.scala index a394e21ad..b900b6c19 100644 --- a/effekt/shared/src/main/scala/effekt/Typer.scala +++ b/effekt/shared/src/main/scala/effekt/Typer.scala @@ -975,7 +975,9 @@ 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("function", tparams.size, vparams.size, bparams.size, tps.size, vps.size, bps.size) + assertArgsParamsAlign("function", tparams, vparams, bparams, tps, vps, bps) + // (3) Substitute type parameters val typeParams = tparams.map { p => p.symbol.asTypeParam } @@ -1274,14 +1276,26 @@ object Typer extends Phase[NameResolved, Typechecked] { * If not, aborts the context with a nice error message. */ private def assertArgsParamsAlign( - name: String, - gotTypes: Int, gotValues: Int, gotBlocks: Int, - expectedTypes: Int, expectedValues: Int, expectedBlocks: Int - )(using Context): Unit = { - - val targsOk = gotTypes == 0 || gotTypes == expectedTypes - val vargsOk = gotValues == expectedValues - val bargsOk = gotBlocks == expectedBlocks + name: String, + gotTypes: List[source.Id] | List[ValueType], + gotValues: List[source.ValueParam] | List[source.ValueArg], + gotBlocks: List[source.BlockParam] | List[source.Term], + expectedTypes: List[TypeParam], + expectedValues: List[ValueType], + expectedBlocks: List[BlockType] + )(using Context): Unit = { + + val gotTypesCount = gotTypes.size + val gotValuesCount = gotValues.size + val gotBlocksCount = gotBlocks.size + val expectedTypesCount = expectedTypes.size + val expectedValuesCount = expectedValues.size + val expectedBlocksCount = expectedBlocks.size + + // Type args are ok if none provided (inference) or count matches + val targsOk = gotTypesCount == 0 || gotTypesCount == expectedTypesCount + val vargsOk = gotValuesCount == expectedValuesCount + val bargsOk = gotBlocksCount == expectedBlocksCount def pluralized(n: Int, singular: String): String = if (n == 1) s"$n $singular" else s"$n ${singular}s" @@ -1302,29 +1316,54 @@ object Typer extends Phase[NameResolved, Typechecked] { } val expected = formatArgs( - Option.when(!targsOk) { expectedTypes }, - Option.when(!vargsOk) { expectedValues }, - Option.when(!bargsOk) { expectedBlocks } + Option.when(!targsOk) { expectedTypesCount }, + Option.when(!vargsOk) { expectedValuesCount }, + Option.when(!bargsOk) { expectedBlocksCount } ) val got = formatArgs( - Option.when(!targsOk) { gotTypes }, - Option.when(!vargsOk) { gotValues }, - Option.when(!bargsOk) { gotBlocks } + Option.when(!targsOk) { gotTypesCount }, + Option.when(!vargsOk) { gotValuesCount }, + Option.when(!bargsOk) { gotBlocksCount } ) - if (!vargsOk && !bargsOk && gotValues + gotBlocks == expectedValues + expectedBlocks) { - // 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)?") + // Hint: Tuple vs case-lambda confusion + if (!vargsOk) { + // Case 1: User wrote `case (x, y) => ... ` but function expects multiple arguments + val isCaseLambda = gotValues match { + case (params: List[source.ValueParam] @unchecked) => params match { + case List(source.ValueParam(source.IdDef(paramName, _), _, _)) => + paramName. startsWith("__arg") + case _ => false + } + case _ => false + } + + if (isCaseLambda && expectedValuesCount > 1) { + Context.info(pretty"Did you mean to use `(x, y) => ... ` instead of `case (x, y) => ... `?") + } + + // Case 2: User wrote `(x, y) => ...` but function expects a single tuple + expectedValues match { + case List(tupleTpe @ ValueTypeApp(TypeConstructor.Record(tupleName, _, _, _), args)) + if tupleName.name.startsWith("Tuple") && args.size == gotValuesCount => + Context.info(pretty"Did you mean to use `case (x, y) => ... ` to pattern match on the tuple ${tupleTpe}?") + case _ => () + } + } + + // Hint: Value vs block argument confusion + if (!vargsOk && !bargsOk && gotValuesCount + gotBlocksCount == expectedValuesCount + expectedBlocksCount) { + if (gotBlocksCount > expectedBlocksCount) { + val diff = gotBlocksCount - expectedBlocksCount + Context.info(pretty"Did you mean to pass ${pluralized(diff, "block argument")} as a value? e.g. box it using `box { ... }`") + } else if (gotValuesCount > expectedValuesCount) { + val diff = gotValuesCount - expectedValuesCount + Context.info(pretty"Did you mean to pass ${pluralized(diff, "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 (!targsOk || !vargsOk || ! bargsOk) { + Context. abort(s"Wrong number of arguments to ${name}: expected ${expected}, but got ${got}") } } @@ -1341,7 +1380,8 @@ 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, targs.size, vargs.size, bargs.size, funTpe.tparams.size, funTpe.vparams.size, funTpe.bparams.size) + assertArgsParamsAlign(name, targs, vargs, bargs, funTpe.tparams, funTpe.vparams, funTpe.bparams) // (1) Instantiate blocktype // e.g. `[A, B] (A, A) => B` becomes `(?A, ?A) => ?B` From 73da81e4019364cd9a8e08a4b5aadeec42fcbccb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Bene=C5=A1?= Date: Thu, 27 Nov 2025 15:41:05 +0100 Subject: [PATCH 2/7] WIP --- .../shared/src/main/scala/effekt/Typer.scala | 176 ++++++++++-------- 1 file changed, 95 insertions(+), 81 deletions(-) diff --git a/effekt/shared/src/main/scala/effekt/Typer.scala b/effekt/shared/src/main/scala/effekt/Typer.scala index b900b6c19..0d3d0be92 100644 --- a/effekt/shared/src/main/scala/effekt/Typer.scala +++ b/effekt/shared/src/main/scala/effekt/Typer.scala @@ -975,9 +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("function", tparams, vparams, bparams, tps, vps, bps) - + assertArgsParamsAlign(name = None, Aligned(tparams, tps), Aligned(vparams, vps), Aligned(bparams, bps)) // (3) Substitute type parameters val typeParams = tparams.map { p => p.symbol.asTypeParam } @@ -1270,100 +1268,117 @@ object Typer extends Phase[NameResolved, Typechecked] { } } - /** - * 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. - */ - private def assertArgsParamsAlign( - name: String, - gotTypes: List[source.Id] | List[ValueType], - gotValues: List[source.ValueParam] | List[source.ValueArg], - gotBlocks: List[source.BlockParam] | List[source.Term], - expectedTypes: List[TypeParam], - expectedValues: List[ValueType], - expectedBlocks: List[BlockType] - )(using Context): Unit = { + /** 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 - val gotTypesCount = gotTypes.size - val gotValuesCount = gotValues.size - val gotBlocksCount = gotBlocks.size - val expectedTypesCount = expectedTypes.size - val expectedValuesCount = expectedValues.size - val expectedBlocksCount = expectedBlocks.size + def gotCount: Int = matched.size + extra.size + def expectedCount: Int = matched.size + missing.size + def delta: Int = extra.size - missing.size // > 0 => too many - // Type args are ok if none provided (inference) or count matches - val targsOk = gotTypesCount == 0 || gotTypesCount == expectedTypesCount - val vargsOk = gotValuesCount == expectedValuesCount - val bargsOk = gotBlocksCount == expectedBlocksCount + def show: String = pp"Aligned(matched=${matched}, gotExtra=${extra}, expectedMissing=${missing})" + } - def pluralized(n: Int, singular: String): String = - if (n == 1) s"$n $singular" else s"$n ${singular}s" + 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 (extras, Nil) => Aligned(matched. reverse, extras, Nil) + case (Nil, missing) => Aligned(matched.reverse, Nil, missing) + case (g :: gs, e :: es) => loop(gs, es, (g, e) :: matched) + } - 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(", ") - } + loop(got, expected, Nil) } + } - val expected = formatArgs( - Option.when(!targsOk) { expectedTypesCount }, - Option.when(!vargsOk) { expectedValuesCount }, - Option.when(!bargsOk) { expectedBlocksCount } - ) - val got = formatArgs( - Option.when(!targsOk) { gotTypesCount }, - Option.when(!vargsOk) { gotValuesCount }, - Option.when(!bargsOk) { gotBlocksCount } - ) + private def assertArgsParamsAlign( + name: Option[String], // None if it's a block literal + types: Aligned[source.Id | ValueType, TypeParam], + values: Aligned[source.ValueParam | source.ValueArg, ValueType], + blocks: Aligned[source.BlockParam | source.Term, BlockType] + )(using Context): Unit = { - // Hint: Tuple vs case-lambda confusion - if (!vargsOk) { - // Case 1: User wrote `case (x, y) => ... ` but function expects multiple arguments - val isCaseLambda = gotValues match { - case (params: List[source.ValueParam] @unchecked) => params match { - case List(source.ValueParam(source.IdDef(paramName, _), _, _)) => - paramName. startsWith("__arg") - case _ => false - } - case _ => false - } + // Type args: truly aligned if nothing provided (inference) or perfectly aligned + val targsAligned = (types.matched.isEmpty && types.extra.isEmpty) || types.isAligned + + def pluralized(n: Int, singular: String): String = + if (n == 1) s"$n $singular" else s"$n ${singular}s" - if (isCaseLambda && expectedValuesCount > 1) { - Context.info(pretty"Did you mean to use `(x, y) => ... ` instead of `case (x, y) => ... `?") + Context.info(pretty"DEBUG: in assertArgsParamsAlign: Values: ${values.show}, Blocks: ${blocks.show}") + + // Hint: Tuple vs lambda-case confusion + 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 _ => () } - // Case 2: User wrote `(x, y) => ...` but function expects a single tuple - expectedValues match { - case List(tupleTpe @ ValueTypeApp(TypeConstructor.Record(tupleName, _, _, _), args)) - if tupleName.name.startsWith("Tuple") && args.size == gotValuesCount => - Context.info(pretty"Did you mean to use `case (x, y) => ... ` to pattern match on the tuple ${tupleTpe}?") + // 2. User wrote `(x, y) => ... ` but 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 Some(name) => Context.info(pretty"Did you mean to call ${name} with a single tuple argument?") + case None => Context.info(pretty"Did you mean to use `case (x, y) => ...` to pattern match on the tuple ${tupleTpe}?") + } case _ => () } } // Hint: Value vs block argument confusion - if (!vargsOk && !bargsOk && gotValuesCount + gotBlocksCount == expectedValuesCount + expectedBlocksCount) { - if (gotBlocksCount > expectedBlocksCount) { - val diff = gotBlocksCount - expectedBlocksCount - Context.info(pretty"Did you mean to pass ${pluralized(diff, "block argument")} as a value? e.g. box it using `box { ... }`") - } else if (gotValuesCount > expectedValuesCount) { - val diff = gotValuesCount - expectedValuesCount - Context.info(pretty"Did you mean to pass ${pluralized(diff, "value argument")} as a block (computation)? ") + if (!values.isAligned && !blocks.isAligned) { + if (values.delta + blocks.delta == 0) { + // If total counts match, but individual do not, it's likely a value vs computation issue + 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 (!targsAligned || !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(!targsAligned) { types.expectedCount }, + Option.when(!values.isAligned) { values.expectedCount }, + Option.when(!blocks.isAligned) { blocks.expectedCount } + ) + val got = formatArgs( + Option.when(!targsAligned) { 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}") } } @@ -1380,8 +1395,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, targs, vargs, bargs, funTpe.tparams, funTpe.vparams, funTpe.bparams) + 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` From 094380f8a7306e9b5e9b3f763654869670e635cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Bene=C5=A1?= Date: Thu, 27 Nov 2025 15:53:05 +0100 Subject: [PATCH 3/7] More hacks --- .../shared/src/main/scala/effekt/Typer.scala | 43 ++++++++++++++----- 1 file changed, 32 insertions(+), 11 deletions(-) diff --git a/effekt/shared/src/main/scala/effekt/Typer.scala b/effekt/shared/src/main/scala/effekt/Typer.scala index 0d3d0be92..cb9bc4570 100644 --- a/effekt/shared/src/main/scala/effekt/Typer.scala +++ b/effekt/shared/src/main/scala/effekt/Typer.scala @@ -1288,8 +1288,8 @@ object Typer extends Phase[NameResolved, Typechecked] { @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 (extras, Nil) => Aligned(matched. reverse, extras, Nil) + 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) } @@ -1298,8 +1298,16 @@ object Typer extends Phase[NameResolved, Typechecked] { } } + /** + * 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: Option[String], // None if it's a block literal + name: Option[String], types: Aligned[source.Id | ValueType, TypeParam], values: Aligned[source.ValueParam | source.ValueArg, ValueType], blocks: Aligned[source.BlockParam | source.Term, BlockType] @@ -1311,12 +1319,10 @@ object Typer extends Phase[NameResolved, Typechecked] { def pluralized(n: Int, singular: String): String = if (n == 1) s"$n $singular" else s"$n ${singular}s" - Context.info(pretty"DEBUG: in assertArgsParamsAlign: Values: ${values.show}, Blocks: ${blocks.show}") - - // Hint: Tuple vs lambda-case confusion + // 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 + // => 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, _ :: _) @@ -1325,15 +1331,30 @@ object Typer extends Phase[NameResolved, Typechecked] { case _ => () } - // 2. User wrote `(x, y) => ... ` but function expects a single tuple - // ? Multiple extra value args, exactly 1 missing that's a tuple matching the count + // 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 Some(name) => Context.info(pretty"Did you mean to call ${name} with a single tuple argument?") - case None => Context.info(pretty"Did you mean to use `case (x, y) => ...` to pattern match on the tuple ${tupleTpe}?") + 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, _, _), givenName) + 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 _ => () } From 485c6fa7a635f845b9f4eb207fa9e209b0b0ee00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Bene=C5=A1?= Date: Thu, 27 Nov 2025 15:57:31 +0100 Subject: [PATCH 4/7] Forgotten 'name' --- effekt/shared/src/main/scala/effekt/Typer.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/effekt/shared/src/main/scala/effekt/Typer.scala b/effekt/shared/src/main/scala/effekt/Typer.scala index cb9bc4570..f44463157 100644 --- a/effekt/shared/src/main/scala/effekt/Typer.scala +++ b/effekt/shared/src/main/scala/effekt/Typer.scala @@ -1351,7 +1351,7 @@ object Typer extends Phase[NameResolved, Typechecked] { (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, _, _), givenName) + case (source.Call(source.IdTarget(source.IdRef(List("effekt"), tupleName, _)), _, tupleArgs, _, _), Some(givenName)) 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 _ => () From 6e2d84e0b7ed52b554e42f2149452f25910a258e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Bene=C5=A1?= Date: Thu, 27 Nov 2025 16:10:57 +0100 Subject: [PATCH 5/7] Add neg tests for newly added hints --- examples/neg/typer/expected_args_got_lambdacase.check | 5 +++++ examples/neg/typer/expected_args_got_lambdacase.effekt | 5 +++++ examples/neg/typer/expected_args_got_tuple.check | 6 ++++++ examples/neg/typer/expected_args_got_tuple.effekt | 5 +++++ examples/neg/typer/expected_lambdacase_got_args.check | 6 ++++++ examples/neg/typer/expected_lambdacase_got_args.effekt | 5 +++++ examples/neg/typer/expected_tuple_got_args.check | 6 ++++++ examples/neg/typer/expected_tuple_got_args.effekt | 5 +++++ 8 files changed, 43 insertions(+) create mode 100644 examples/neg/typer/expected_args_got_lambdacase.check create mode 100644 examples/neg/typer/expected_args_got_lambdacase.effekt create mode 100644 examples/neg/typer/expected_args_got_tuple.check create mode 100644 examples/neg/typer/expected_args_got_tuple.effekt create mode 100644 examples/neg/typer/expected_lambdacase_got_args.check create mode 100644 examples/neg/typer/expected_lambdacase_got_args.effekt create mode 100644 examples/neg/typer/expected_tuple_got_args.check create mode 100644 examples/neg/typer/expected_tuple_got_args.effekt diff --git a/examples/neg/typer/expected_args_got_lambdacase.check b/examples/neg/typer/expected_args_got_lambdacase.check new file mode 100644 index 000000000..60ec85907 --- /dev/null +++ b/examples/neg/typer/expected_args_got_lambdacase.check @@ -0,0 +1,5 @@ +[error] examples/neg/typer/expected_args_got_lambdacase.effekt:4:13: Wrong number of arguments to function: expected 2 value arguments, but got 1 value argument + println(n) + +[info] examples/neg/typer/expected_args_got_lambdacase.effekt:4:13: Did you mean to use `(x, y) => ...` instead of `case (x, y) => ...`? + println(n) \ No newline at end of file diff --git a/examples/neg/typer/expected_args_got_lambdacase.effekt b/examples/neg/typer/expected_args_got_lambdacase.effekt new file mode 100644 index 000000000..bda16182c --- /dev/null +++ b/examples/neg/typer/expected_args_got_lambdacase.effekt @@ -0,0 +1,5 @@ +def foo { f: (Int, String) => Unit }: Unit = f(42, "hello") + +def main() = foo { case (n, s) => + println(n) +} \ No newline at end of file diff --git a/examples/neg/typer/expected_args_got_tuple.check b/examples/neg/typer/expected_args_got_tuple.check new file mode 100644 index 000000000..2aa2b0d7c --- /dev/null +++ b/examples/neg/typer/expected_args_got_tuple.check @@ -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")) + ^^^^^^^^^^^^^^^^^^ \ No newline at end of file diff --git a/examples/neg/typer/expected_args_got_tuple.effekt b/examples/neg/typer/expected_args_got_tuple.effekt new file mode 100644 index 000000000..4b3cc4fc4 --- /dev/null +++ b/examples/neg/typer/expected_args_got_tuple.effekt @@ -0,0 +1,5 @@ +def foo(x: Int, s: String): Int = 42 + +def main() = { + val _ = foo((42, "hello")) +} \ No newline at end of file diff --git a/examples/neg/typer/expected_lambdacase_got_args.check b/examples/neg/typer/expected_lambdacase_got_args.check new file mode 100644 index 000000000..a9563e49b --- /dev/null +++ b/examples/neg/typer/expected_lambdacase_got_args.check @@ -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) => + ^ \ No newline at end of file diff --git a/examples/neg/typer/expected_lambdacase_got_args.effekt b/examples/neg/typer/expected_lambdacase_got_args.effekt new file mode 100644 index 000000000..74a9fc448 --- /dev/null +++ b/examples/neg/typer/expected_lambdacase_got_args.effekt @@ -0,0 +1,5 @@ +def foo { f: ((Int, String)) => Unit }: Unit = f((42, "hello")) + +def main() = foo { (n, s) => + println(n) +} \ No newline at end of file diff --git a/examples/neg/typer/expected_tuple_got_args.check b/examples/neg/typer/expected_tuple_got_args.check new file mode 100644 index 000000000..3800801a5 --- /dev/null +++ b/examples/neg/typer/expected_tuple_got_args.check @@ -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") + ^^^^^^^^^^^^^^^^ \ No newline at end of file diff --git a/examples/neg/typer/expected_tuple_got_args.effekt b/examples/neg/typer/expected_tuple_got_args.effekt new file mode 100644 index 000000000..515550172 --- /dev/null +++ b/examples/neg/typer/expected_tuple_got_args.effekt @@ -0,0 +1,5 @@ +def foo(x: (Int, String)): Int = 42 + +def main() = { + val _ = foo(42, "hello") +} \ No newline at end of file From 3275c677fe5a5e282e898c701bec5a6162e760d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Bene=C5=A1?= Date: Thu, 27 Nov 2025 16:40:03 +0100 Subject: [PATCH 6/7] Drive-by: fix lambda case position info --- effekt/shared/src/main/scala/effekt/Parser.scala | 5 +++-- examples/neg/typer/expected_args_got_lambdacase.check | 11 ++++++----- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/effekt/shared/src/main/scala/effekt/Parser.scala b/effekt/shared/src/main/scala/effekt/Parser.scala index 053943b4a..c888dc4de 100644 --- a/effekt/shared/src/main/scala/effekt/Parser.scala +++ b/effekt/shared/src/main/scala/effekt/Parser.scala @@ -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) @@ -1164,7 +1165,7 @@ class Parser(tokens: Seq[Token], source: Source) { ), span().synthesized), span().synthesized ) - } + case _ => // { (x: Int) => ... } nonterminal: diff --git a/examples/neg/typer/expected_args_got_lambdacase.check b/examples/neg/typer/expected_args_got_lambdacase.check index 60ec85907..4fd539b2a 100644 --- a/examples/neg/typer/expected_args_got_lambdacase.check +++ b/examples/neg/typer/expected_args_got_lambdacase.check @@ -1,5 +1,6 @@ -[error] examples/neg/typer/expected_args_got_lambdacase.effekt:4:13: Wrong number of arguments to function: expected 2 value arguments, but got 1 value argument - println(n) - -[info] examples/neg/typer/expected_args_got_lambdacase.effekt:4:13: Did you mean to use `(x, y) => ...` instead of `case (x, y) => ...`? - println(n) \ No newline at end of file +[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) => + ^ \ No newline at end of file From 666cffd881e424a6056c6617ef715b5d79750203 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Bene=C5=A1?= Date: Thu, 27 Nov 2025 17:10:29 +0100 Subject: [PATCH 7/7] Small tweaks: reduce nesting, remove debug code --- .../shared/src/main/scala/effekt/Typer.scala | 26 ++++++++----------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/effekt/shared/src/main/scala/effekt/Typer.scala b/effekt/shared/src/main/scala/effekt/Typer.scala index f44463157..375af08aa 100644 --- a/effekt/shared/src/main/scala/effekt/Typer.scala +++ b/effekt/shared/src/main/scala/effekt/Typer.scala @@ -1279,8 +1279,6 @@ object Typer extends Phase[NameResolved, Typechecked] { def gotCount: Int = matched.size + extra.size def expectedCount: Int = matched.size + missing.size def delta: Int = extra.size - missing.size // > 0 => too many - - def show: String = pp"Aligned(matched=${matched}, gotExtra=${extra}, expectedMissing=${missing})" } object Aligned { @@ -1313,8 +1311,8 @@ object Typer extends Phase[NameResolved, Typechecked] { blocks: Aligned[source.BlockParam | source.Term, BlockType] )(using Context): Unit = { - // Type args: truly aligned if nothing provided (inference) or perfectly aligned - val targsAligned = (types.matched.isEmpty && types.extra.isEmpty) || types.isAligned + // 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" @@ -1361,18 +1359,16 @@ object Typer extends Phase[NameResolved, Typechecked] { } // Hint: Value vs block argument confusion - if (!values.isAligned && !blocks.isAligned) { - if (values.delta + blocks.delta == 0) { - // If total counts match, but individual do not, it's likely a value vs computation issue - 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 (!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 (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 (!targsAligned || !values.isAligned || !blocks.isAligned) { + 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") }, @@ -1389,12 +1385,12 @@ object Typer extends Phase[NameResolved, Typechecked] { } val expected = formatArgs( - Option.when(!targsAligned) { types.expectedCount }, + Option.when(!typesOk) { types.expectedCount }, Option.when(!values.isAligned) { values.expectedCount }, Option.when(!blocks.isAligned) { blocks.expectedCount } ) val got = formatArgs( - Option.when(!targsAligned) { types.gotCount }, + Option.when(!typesOk) { types.gotCount }, Option.when(!values.isAligned) { values.gotCount }, Option.when(!blocks.isAligned) { blocks.gotCount } )