Skip to content

Conversation

@jiribenes
Copy link
Contributor

@jiribenes jiribenes commented Dec 7, 2025

Resolves #1186
Supersedes #1046

foo.bar(baz) { quux } is expanded by UnboxInference into:

  1. an operation call bar on foo (if bar is a known operation call)
  2. a UFCS call bar(foo, baz) { quux }
  3. [NEW] a UFCS call bar(baz) { foo } { quux } iff foo is syntactically not a block literal

See the two added examples, no idea if this breaks any code.

@jiribenes jiribenes added area:typer experiment Experimental branch, do not merge! area:parser/lexer labels Dec 7, 2025
@jiribenes
Copy link
Contributor Author

jiribenes commented Dec 7, 2025

This change might allow stuff that could be considered poor taste, such as { n => n + 1 }.list::map([1, 2, 3]), in addition to "just" making overloads worse.
My hope is that it's worth it for { <stream> }.limit(10).collect[Int] and stuff like [1, 2, 3].each.for { (x: Int) => do emit(x * 2) }.filter { (x: Int) => x > 3 }.collect[Int]

@jiribenes
Copy link
Contributor Author

jiribenes commented Dec 8, 2025

This doesn't currently work well "in practice" for two reasons:

  1. [1, 2, 3].each.for does not work, because [1, 2, 3].each is a value, so it still needs to be { [1, 2, 3].each }.for -- I don't think there's anything I could do here, guessing "oh, would this fit if it were a block literal" would work, I guess, but I find it hard to explain... This also means that it doesn't compose well for blocks at all: { { { [1, 2, 3].each }.indexed }.limit(10) }.for is ridiculous.
  2. parsing this is a nightmare since we have two constructs that are almost the same: block literals and statement blocks -- it would be much easier if the parser did not, in fact, distinguish between the two (each block literal without args is a statement block, done)
  3. I think that the UFCS resolution should move from UnboxInference to Typer where we can do "proper" overloading. Unbox inference would do almost nothing:
case m @ MethodCall(receiver, id, targs, vargs, bargs, span) =>
  val vargsTransformed = vargs.map(rewriteAsExpr)
  val bargsTransformed = bargs.map(rewriteAsBlock)
  
  // Don't disambiguate here, instead let Typer handle it with full type information
  // Just rewrite the receiver appropriately based on what candidates exist
  val hasMethods = m.definition match {
    case symbols.CallTarget(syms) => syms.flatten.exists(_.isInstanceOf[symbols.Operation])
    case _: symbols.Operation => true
    case _ => false
  }
  
  if (hasMethods) {
    // Might be a true method call — receiver needs to be a block
    MethodCall(rewriteAsBlock(receiver), id, targs, vargsTransformed, bargsTransformed, span)
  } else {
    // Pure UFCS — keep as MethodCall, Typer will decide value vs block UFCS
    // Receiver stays as-is; Typer will rewrite appropriately based on types
    MethodCall(receiver, id, targs, vargsTransformed, bargsTransformed, span)
  }

and then Typer:

def checkOverloadedMethodCall(
  call: source.CallLike,
  receiver: source.Term,
  id: source.IdRef,
  targs: List[ValueType],
  vargs: List[source.ValueArg],
  bargs: List[source.Term],
  expected: Option[ValueType]
)(using Context, Captures): Result[ValueType] = {
  val sym = id.symbol
  
  val (methods, functions) = sym match {
    case CallTarget(syms) => 
      val all = syms.flatten
      (all.collect { case op: Operation => op }, 
       all.collect { case fn: Callable => fn })
    case sym: Operation => (List(sym), Nil)
    case sym: Callable => (Nil, List(sym))
    case s => Context.panic(s"Not a valid method/function: ${s}")
  }
  
  // Try 1: True method calls (receiver is interface instance)
  val methodResults = if (methods.nonEmpty) {
    val Result(recvTpe, recvEffs) = checkExprAsBlock(receiver, None)
    // ...  existing method resolution logic
  } else Nil
  
  // Try 2: Value-UFCS (receiver as first value arg)
  val valueUFCSResults = tryEach(functions.filter(_.vparams.nonEmpty)) { fn =>
    val Result(recvTpe, recvEffs) = checkExpr(receiver, Some(fn.vparams.head.tpe.get))
    checkCallTo(call, fn.name.name, ..., ValueArg.Unnamed(receiver) :: vargs, bargs, expected)
  }
  
  // Try 3: Block-UFCS (receiver as first block arg)  
  val blockUFCSResults = tryEach(functions.filter(_.bparams.nonEmpty)) { fn =>
    val Result(recvTpe, recvEffs) = checkExprAsBlock(receiver, Some(fn.bparams.head.tpe.get))
    checkCallTo(call, fn.name.name, ..., vargs, receiver :: bargs, expected)
  }
  
  // Combine and resolve
  resolveOverload(id, List(methodResults, valueUFCSResults, blockUFCSResults), allErrors)
}

@jiribenes jiribenes changed the title Allow UFCS for blocks Allow UFCS for non-literal blocks Dec 9, 2025
@jiribenes

This comment was marked as resolved.

@jiribenes jiribenes changed the title Allow UFCS for non-literal blocks Allow UFCS for (non-literal) blocks Dec 9, 2025
@jiribenes jiribenes removed the experiment Experimental branch, do not merge! label Dec 9, 2025
@jiribenes jiribenes marked this pull request as ready for review December 9, 2025 19:24
@jiribenes jiribenes changed the title Allow UFCS for (non-literal) blocks Allow UFCS on (non-literal) block receivers Dec 13, 2025
@jiribenes
Copy link
Contributor Author

This was uncontroversial on the Effekt Working Group meeting and doesn't interfere with anyone else's work, so I'll just merge it :)

@jiribenes jiribenes merged commit 53cc164 into master Dec 13, 2025
7 checks passed
@jiribenes jiribenes deleted the experiment/ufcs-on-blocks branch December 13, 2025 20:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

extension function syntax should work on block types too

2 participants