Skip to content

Commit e41c91f

Browse files
Change explicit box capture syntax (#1017)
Closes #965.
1 parent 3904cd4 commit e41c91f

File tree

11 files changed

+113
-38
lines changed

11 files changed

+113
-38
lines changed

effekt/js/src/main/scala/effekt/LanguageServer.scala

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
package effekt
22

3-
import effekt.context.{ Context, VirtualFileSource, VirtualModuleDB }
4-
5-
import effekt.util.{ PlainMessaging, getOrElseAborting }
6-
import effekt.util.messages.{ BufferedMessaging, EffektError, EffektMessaging, FatalPhaseError }
3+
import effekt.Intelligence.CaptureInfo
4+
import effekt.context.{Context, VirtualFileSource, VirtualModuleDB}
5+
import effekt.util.{PlainMessaging, getOrElseAborting}
6+
import effekt.util.messages.{BufferedMessaging, EffektError, EffektMessaging, FatalPhaseError}
77
import effekt.util.paths.*
88
import effekt.generator.js.JavaScriptWeb
9-
import kiama.util.{ Messaging, Position, Positions, Severities, Source, StringSource }
9+
import kiama.util.{Messaging, Position, Positions, Severities, Source, StringSource}
1010

1111
import scala.scalajs.js
1212
import scala.scalajs.js.JSConverters.*
@@ -117,7 +117,7 @@ class LanguageServer extends Intelligence {
117117
val source = VirtualFileSource(path)
118118
val range = kiama.util.Range(source.offsetToPosition(0), source.offsetToPosition(source.charCount))
119119
getInferredCaptures(range).map {
120-
case (p, c) => new lsp.CaptureInfo(toLSPPosition(p), c.toString)
120+
case CaptureInfo(p, c, _) => new lsp.CaptureInfo(toLSPPosition(p), c.toString)
121121
}.toJSArray
122122
}
123123

effekt/jvm/src/main/scala/effekt/Server.scala

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package effekt
22

33
import com.google.gson.JsonElement
4+
import effekt.Intelligence.CaptureInfo
45
import effekt.context.Context
56
import effekt.source.Def.FunDef
67
import effekt.source.Term.Hole
@@ -374,15 +375,19 @@ class Server(config: EffektConfig, compileOnChange: Boolean=false) extends Langu
374375
hints = {
375376
val range = fromLSPRange(params.getRange, source)
376377
val captures = getInferredCaptures(range)(using context).map {
377-
case (p, c) =>
378+
case CaptureInfo(p, c, atSyntax) =>
378379
val prettyCaptures = TypePrinter.show(c)
379-
val inlayHint = new InlayHint(convertPosition(p), messages.Either.forLeft(prettyCaptures))
380+
val codeEdit = if atSyntax then s"at ${prettyCaptures}" else prettyCaptures
381+
val inlayHint = new InlayHint(convertPosition(p), messages.Either.forLeft(codeEdit))
380382
inlayHint.setKind(InlayHintKind.Type)
381383
val markup = new MarkupContent()
382384
markup.setValue(s"captures: `${prettyCaptures}`")
383385
markup.setKind("markdown")
384386
inlayHint.setTooltip(markup)
385-
inlayHint.setPaddingRight(true)
387+
if (atSyntax) then
388+
inlayHint.setPaddingLeft(true)
389+
else
390+
inlayHint.setPaddingRight(true)
386391
inlayHint.setData("capture")
387392
inlayHint
388393
}.toVector

effekt/jvm/src/test/scala/effekt/LSPTests.scala

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -572,7 +572,7 @@ class LSPTests extends FunSuite {
572572
//
573573
//
574574

575-
test("inlayHint should show the io effect") {
575+
test("inlayHint shows the io capture on a def") {
576576
withClientAndServer { (client, server) =>
577577
val (textDoc, positions) = raw"""
578578
|↑
@@ -609,6 +609,43 @@ class LSPTests extends FunSuite {
609609
}
610610
}
611611

612+
test("inlayHint shows the io capture in explicit box syntax") {
613+
withClientAndServer { (client, server) =>
614+
val (textDoc, positions) = raw"""def foo(): Unit = { println("foo") }
615+
|
616+
|↑
617+
|val bar = box { () => foo() }
618+
| ↑
619+
|
620+
|↑
621+
|""".textDocumentAndPositions
622+
623+
val inlayHint = new InlayHint()
624+
inlayHint.setKind(InlayHintKind.Type)
625+
inlayHint.setPosition(positions(1))
626+
inlayHint.setLabel("at {io}")
627+
val markup = new MarkupContent()
628+
markup.setKind("markdown")
629+
markup.setValue("captures: `{io}`")
630+
inlayHint.setTooltip(markup)
631+
inlayHint.setPaddingLeft(true)
632+
inlayHint.setData("capture")
633+
634+
val expectedInlayHints = List(inlayHint)
635+
636+
val didOpenParams = new DidOpenTextDocumentParams()
637+
didOpenParams.setTextDocument(textDoc)
638+
server.getTextDocumentService().didOpen(didOpenParams)
639+
640+
val params = new InlayHintParams()
641+
params.setTextDocument(textDoc.versionedTextDocumentIdentifier)
642+
params.setRange(new Range(positions(0), positions(2)))
643+
644+
val inlayHints = server.getTextDocumentService().inlayHint(params).get()
645+
assertEquals(inlayHints, expectedInlayHints.asJava)
646+
}
647+
}
648+
612649
test("inlayHint shows omitted return type and effect") {
613650
withClientAndServer { (client, server) =>
614651
val (textDoc, positions) =

effekt/jvm/src/test/scala/effekt/RecursiveDescentTests.scala

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,10 @@ class RecursiveDescentTests extends munit.FunSuite {
255255
assertEquals(peek(tokens, 3).kind, EOF)
256256
}
257257

258+
// Parsing tests
259+
//
260+
//
261+
258262
test("Simple expressions") {
259263
parseExpr("42")
260264
parseExpr("f")
@@ -316,13 +320,27 @@ class RecursiveDescentTests extends munit.FunSuite {
316320
)
317321
assertNotEqualModuloSpans(
318322
parseExpr("box { 42 }"),
319-
parseExpr("box {} { 42 }")
323+
parseExpr("box { 42 } at {}")
320324
)
321325
parseExpr("box { (x: Int) => x }")
322326
parseExpr("box new Fresh { def fresh() = \"42\" }")
323327
parseExpr("box foo()")
324328
parseExpr("box bar(1)")
325329
parseExpr("box baz(quux)")
330+
parseExpr("box { (x, y) => compareByteString(x, y) } at {io, global}")
331+
332+
{
333+
val (source, pos) =
334+
raw"""box { (x, y) => compareByteString(x, y) }
335+
| ↑
336+
|""".sourceAndPosition
337+
val b = parseExpr(source.content)
338+
b match {
339+
case Term.Box(c, _) => assertEquals(c.span, Span(source, pos, pos, Synthesized))
340+
case other =>
341+
throw new IllegalArgumentException(s"Expected Box but got ${other.getClass.getSimpleName}")
342+
}
343+
}
326344

327345
// { f } is parsed as a capture set and not backtracked.
328346
intercept[Throwable] { parseExpr("box { f }") }

effekt/shared/src/main/scala/effekt/Intelligence.scala

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
package effekt
22

33
import effekt.context.{Annotations, Context}
4-
import effekt.source.{FunDef, Include, ModuleDecl, Span, Tree}
5-
import effekt.symbols.Hole
4+
import effekt.source.{FunDef, Include, Maybe, ModuleDecl, Span, Tree}
5+
import effekt.symbols.{CaptureSet, Hole}
66
import kiama.util.{Position, Source}
77
import effekt.symbols.scopes.Scope
88

@@ -182,7 +182,7 @@ trait Intelligence {
182182
C.annotationOption(Annotations.CaptureForFile, src).getOrElse(Nil)
183183

184184
// For now, we only show captures of function definitions and calls to box
185-
def getInferredCaptures(range: kiama.util.Range)(using C: Context): List[(Position, CaptureSet)] =
185+
def getInferredCaptures(range: kiama.util.Range)(using C: Context): List[CaptureInfo] =
186186
val src = range.from.source
187187
allCaptures(src).filter {
188188
// keep only captures in the current range
@@ -192,14 +192,13 @@ trait Intelligence {
192192
}.collect {
193193
case (t: source.FunDef, c) => for {
194194
pos <- C.positions.getStart(t)
195-
} yield (pos, c)
195+
} yield CaptureInfo(pos, c)
196196
case (t: source.DefDef, c) => for {
197197
pos <- C.positions.getStart(t)
198-
} yield (pos, c)
199-
case (source.Box(None, block), _) if C.inferredCaptureOption(block).isDefined => for {
200-
pos <- C.positions.getStart(block)
198+
} yield CaptureInfo(pos, c)
199+
case (source.Box(Maybe(None, span), block), _) if C.inferredCaptureOption(block).isDefined => for {
201200
capt <- C.inferredCaptureOption(block)
202-
} yield (pos, capt)
201+
} yield CaptureInfo(span.range.from, capt, true)
203202
}.flatten
204203

205204
def getInfoOf(sym: Symbol)(using C: Context): Option[SymbolInfo] = PartialFunction.condOpt(resolveCallTarget(sym)) {
@@ -354,4 +353,13 @@ object Intelligence {
354353
terms ++ other.terms,
355354
types ++ other.types)
356355
}
356+
357+
case class CaptureInfo(
358+
position: Position,
359+
captures: CaptureSet,
360+
/**
361+
* Whether this capture set could be written into the source code at `position` using the `at { captures }` syntax
362+
*/
363+
atSyntax: Boolean = false,
364+
)
357365
}

effekt/shared/src/main/scala/effekt/RecursiveDescent.scala

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -674,20 +674,22 @@ class RecursiveDescent(positions: Positions, tokens: Seq[Token], source: Source)
674674
nonterminal:
675675
Region(`region` ~> idDef(), stmt())
676676

677-
def boxExpr(): Term =
677+
def boxExpr(): Term = {
678678
nonterminal:
679-
val captures = `box` ~> backtrack(captureSet())
679+
consume(`box`)
680680
val expr = if (peek(`{`)) functionArg()
681-
else if (peek(`new`)) newExpr()
682-
else callExpr()
683-
Box(captures.unspan, expr)
684-
681+
else callExpr()
682+
val captures = backtrack {
683+
`at` ~> captureSet()
684+
}
685+
Box(captures, expr)
686+
}
685687

686688
// TODO deprecate
687689
def funExpr(): Term =
688690
nonterminal:
689-
`fun` ~> Box(None, BlockLiteral(Nil, valueParams().unspan, Nil, braces { stmts() }))
690-
// TODO positions
691+
val blockLiteral = `fun` ~> BlockLiteral(Nil, valueParams().unspan, Nil, braces { stmts() })
692+
Box(Maybe.None(Span(source, pos(), pos(), Synthesized)), blockLiteral)
691693

692694
def unboxExpr(): Term =
693695
nonterminal:
@@ -1347,8 +1349,13 @@ class RecursiveDescent(positions: Positions, tokens: Seq[Token], source: Source)
13471349

13481350
inline def backtrack[T](inline p: => T): Maybe[T] =
13491351
val before = position
1350-
try { Maybe.Some(p, span(before)) } catch {
1351-
case Fail(_, _) => position = before; Maybe.None(span(before))
1352+
val beforePrevious = previous
1353+
try { Maybe.Some(p, span(tokens(before).end)) } catch {
1354+
case Fail(_, _) => {
1355+
position = before
1356+
previous = beforePrevious
1357+
Maybe.None(Span(source, previous.end + 1, previous.end + 1, Synthesized))
1358+
}
13521359
}
13531360

13541361
def interleave[A](xs: List[A], ys: List[A]): List[A] = (xs, ys) match {

effekt/shared/src/main/scala/effekt/source/Tree.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -456,7 +456,7 @@ enum Term extends Tree {
456456
case Hole(id: IdDef, stmts: Stmt, override val span: Span)
457457

458458
// Boxing and unboxing to represent first-class values
459-
case Box(capt: Option[CaptureSet], block: Term)
459+
case Box(capt: Maybe[CaptureSet], block: Term)
460460
case Unbox(term: Term)
461461

462462
/**

effekt/shared/src/main/scala/effekt/typer/BoxUnboxInference.scala

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,12 +50,12 @@ object BoxUnboxInference extends Phase[NameResolved, NameResolved] {
5050
case v: Var => v.definition match {
5151
// TODO maybe we should synthesize a call to get here already?
5252
case sym: (ValueSymbol | symbols.RefBinder) => v
53-
case sym: BlockSymbol => Box(None, v).inheritPosition(v)
53+
case sym: BlockSymbol => Box(Maybe.None(v.span.emptyAfter), v).inheritPosition(v)
5454
}
5555

56-
case n: New => Box(None, rewriteAsBlock(n)).inheritPosition(n)
56+
case n: New => Box(Maybe.None(n.span.emptyAfter), rewriteAsBlock(n)).inheritPosition(n)
5757

58-
case b: BlockLiteral => Box(None, rewriteAsBlock(b)).inheritPosition(b)
58+
case b: BlockLiteral => Box(Maybe.None(b.span.emptyAfter), rewriteAsBlock(b)).inheritPosition(b)
5959

6060
case l: Literal => l
6161

examples/neg/issue362.check

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
[error] examples/neg/issue362.effekt:4:31: Not allowed {arr}
2-
def foo() {arr: Exc} = box {} { read {arr} }
3-
^^^^^^^^^^^^^^
1+
[error] examples/neg/issue362.effekt:4:28: Not allowed {arr}
2+
def foo() {arr: Exc} = box { read {arr} } at {}
3+
^^^^^^^^^^^^^^

examples/neg/issue362.effekt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
interface Exc { def op(): Unit }
22
def read {t: Exc}: Unit = t.op()
33

4-
def foo() {arr: Exc} = box {} { read {arr} }
4+
def foo() {arr: Exc} = box { read {arr} } at {}
55

66
def main() = {
77
val cap = try {

0 commit comments

Comments
 (0)