diff --git a/docs/language/expressions/lambdas.md b/docs/language/expressions/lambdas.md index edf7bf389..9fc5218e2 100644 --- a/docs/language/expressions/lambdas.md +++ b/docs/language/expressions/lambdas.md @@ -16,6 +16,10 @@ x => x + 1 // single parameter, type inferred (x: Int, y: Int) => x + y // multiple parameters with annotations ``` +Parameter type annotations must be given for every parameter or omitted for every +parameter. Partially annotated forms such as `(x: Int, y) => x + y` and +`(x, y: Int) => x + y` are rejected, even when an expected function type is available. + When the body fits on one line, the lambda is written inline. When it needs multiple phrases, the `=>` introduces a block: ```jo diff --git a/docs/language/syntax/syntax-summary.md b/docs/language/syntax/syntax-summary.md index 6bd50dc98..2d1da09b2 100644 --- a/docs/language/syntax/syntax-summary.md +++ b/docs/language/syntax/syntax-summary.md @@ -325,8 +325,9 @@ simple_params = simple_param {"," simple_param} simple_param = name ":" type lambda_param_section = "(" [lambda_params] ")" -lambda_params = lambda_param {"," lambda_param} -lambda_param = name [":" type] +lambda_params = name {"," name} + | lambda_typed_param {"," lambda_typed_param} +lambda_typed_param = name ":" type post_params = post_param {"," post_param} post_param = name ":" type ["=" default_value] diff --git a/docs/language/types/type-inference.md b/docs/language/types/type-inference.md index 4b6f7809b..871157eca 100644 --- a/docs/language/types/type-inference.md +++ b/docs/language/types/type-inference.md @@ -22,7 +22,8 @@ val flag = true ## Lambda Parameter Inference -Lambda parameter types can be either inferred from usage or from expected type: +Lambda parameters must either all have type annotations or all omit them. +When annotations are omitted, their types can be inferred from usage or from the expected type: ```jo // `x` inferred to have type `Int` diff --git a/stack-lang/parsing/Parser.scala b/stack-lang/parsing/Parser.scala index 6fc1e8a02..91f42e271 100644 --- a/stack-lang/parsing/Parser.scala +++ b/stack-lang/parsing/Parser.scala @@ -2144,6 +2144,14 @@ class Parser(code: String)(using reporter: Reporter, source: Source): def lambda(): Word = val lparen = peekItem() val paramList = params(typeOptional = true) + val typedCount = paramList.count(param => !param.tpt.isEmpty) + + if typedCount > 0 && typedCount < paramList.size then + error( + "Lambda parameters must either all have type annotations or all omit them", + (paramList.head.span | paramList.last.span).toPos, + ) + val arrow = eat(Token.RARROW) val body = block(lparen.indent, arrow) Lambda(paramList, body)(lparen.span | body.span) diff --git a/tests/pos/lambda-parameter-annotations.jo b/tests/pos/lambda-parameter-annotations.jo new file mode 100644 index 000000000..526aa0f69 --- /dev/null +++ b/tests/pos/lambda-parameter-annotations.jo @@ -0,0 +1,16 @@ +def main = + val typed = (x: Int, y: Int) => x + y + val inferred: (Int, Int) => Int = (x, y) => x + y + val unaryTyped = (x: Int) => x * 2 + val unaryInferred: Int => Int = x => x * 2 + val nullary = () => 42 + + println: typed(2, 3) + println: inferred(4, 5) + println: unaryTyped(6) + println: unaryInferred(7) + println: nullary() + + val xs: List[Int ~ Int] = [1 ~ 2, 3 ~ 4] + println: xs.fold([], (acc: List[Int], p: Int ~ Int) => acc + p.fst) + println: xs.fold[List[Int]]([], (acc, p) => acc + p.fst) diff --git a/tests/pos/lambda-parameter-annotations.jo.check b/tests/pos/lambda-parameter-annotations.jo.check new file mode 100644 index 000000000..6d839a86b --- /dev/null +++ b/tests/pos/lambda-parameter-annotations.jo.check @@ -0,0 +1,7 @@ +5 +9 +12 +14 +42 +[1, 3] +[1, 3] diff --git a/tests/warn/lambda-partial-annotations.jo b/tests/warn/lambda-partial-annotations.jo new file mode 100644 index 000000000..74c986711 --- /dev/null +++ b/tests/warn/lambda-partial-annotations.jo @@ -0,0 +1,12 @@ +def annotatedFirst = + val f: (Int, Int) => Int = (x: Int, y) => x + y + +def annotatedLast = + val f: (Int, Int) => Int = (x, y: Int) => x + y + +def annotatedMiddle = + val f: (Int, Int, Int) => Int = (x, y: Int, z) => x + y + z + +def foldAccumulator = + val xs: List[Int ~ Int] = [1 ~ 2] + println: xs.fold([], (acc: List[Int], p) => acc + p.fst) diff --git a/tests/warn/lambda-partial-annotations.jo.check b/tests/warn/lambda-partial-annotations.jo.check new file mode 100644 index 000000000..bfb4b2dba --- /dev/null +++ b/tests/warn/lambda-partial-annotations.jo.check @@ -0,0 +1,21 @@ +---------- Error at tests/warn/lambda-partial-annotations.jo:2:31 --------------- +| val f: (Int, Int) => Int = (x: Int, y) => x + y +| ^^^^^^^^^ +| Lambda parameters must either all have type annotations or all omit them + +---------- Error at tests/warn/lambda-partial-annotations.jo:5:31 --------------- +| val f: (Int, Int) => Int = (x, y: Int) => x + y +| ^^^^^^^^^ +| Lambda parameters must either all have type annotations or all omit them + +---------- Error at tests/warn/lambda-partial-annotations.jo:8:36 --------------- +| val f: (Int, Int, Int) => Int = (x, y: Int, z) => x + y + z +| ^^^^^^^^^^^^ +| Lambda parameters must either all have type annotations or all omit them + +---------- Error at tests/warn/lambda-partial-annotations.jo:12:25 --------------- +| println: xs.fold([], (acc: List[Int], p) => acc + p.fst) +| ^^^^^^^^^^^^^^^^^ +| Lambda parameters must either all have type annotations or all omit them + +4 error(s), 0 warning(s)