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
4 changes: 4 additions & 0 deletions docs/language/expressions/lambdas.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

@everdance everdance Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

can we actually accept (x, y: Int) as equal to (x: Int, y: Int)? it would be more concise if the function has multiple parameters of the same type

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

That is possible, but it would break integrity: in many languages (x, y: Int) mean that both x and y have the type Int.

From evolution perspective, we should be more strict in the beginning.


When the body fits on one line, the lambda is written inline. When it needs multiple phrases, the `=>` introduces a block:

```jo
Expand Down
5 changes: 3 additions & 2 deletions docs/language/syntax/syntax-summary.md
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
3 changes: 2 additions & 1 deletion docs/language/types/type-inference.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
8 changes: 8 additions & 0 deletions stack-lang/parsing/Parser.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
16 changes: 16 additions & 0 deletions tests/pos/lambda-parameter-annotations.jo
Original file line number Diff line number Diff line change
@@ -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)
7 changes: 7 additions & 0 deletions tests/pos/lambda-parameter-annotations.jo.check
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
5
9
12
14
42
[1, 3]
[1, 3]
12 changes: 12 additions & 0 deletions tests/warn/lambda-partial-annotations.jo
Original file line number Diff line number Diff line change
@@ -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)
21 changes: 21 additions & 0 deletions tests/warn/lambda-partial-annotations.jo.check
Original file line number Diff line number Diff line change
@@ -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)
Loading