Skip to content

Commit f1d6b9c

Browse files
Labels in parser: errors 'expected A but got B' (#1008)
Resolves #960 Most messages are now of the form "Expected $A but got $B". Now says something like: "expected block type but got `}`" instead "expected identifier but got `}`" (wrong position is #958) <img width="541" alt="Screenshot 2025-05-20 at 15 21 33" src="https://github.com/user-attachments/assets/55986b70-1c00-46c5-98f9-dcece177a27d" /> --------- Co-authored-by: Marcial Gaißert <[email protected]>
1 parent b115f3a commit f1d6b9c

14 files changed

+100
-44
lines changed

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

Lines changed: 69 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,11 @@ import scala.util.boundary
1515
import scala.util.boundary.break
1616

1717

18-
case class Fail(message: String, position: Int) extends Throwable(null, null, false, false)
18+
case class Fail(msg: String, position: Int) extends Throwable(null, null, false, false)
19+
object Fail {
20+
def expectedButGot(expected: String, got: String, position: Int): Fail =
21+
Fail(s"Expected ${expected} but got ${got}", position)
22+
}
1923
case class SoftFail(message: String, positionStart: Int, positionEnd: Int)
2024

2125
class RecursiveDescent(positions: Positions, tokens: Seq[Token], source: Source) {
@@ -67,6 +71,17 @@ class RecursiveDescent(positions: Positions, tokens: Seq[Token], source: Source)
6771
case Fail(msg, pos) => kiama.parsing.Error(msg, input.copy(offset = pos))
6872
}
6973

74+
var currentLabel: Option[String] = None
75+
76+
extension[T](inline p: => T) inline def labelled(inline label: String): T = {
77+
val labelBefore = currentLabel
78+
if (currentLabel.isEmpty) {
79+
currentLabel = Some(label)
80+
}
81+
val res = p
82+
currentLabel = labelBefore
83+
res
84+
}
7085

7186
// Interfacing with the token stream
7287
// ---------------------------------
@@ -124,6 +139,7 @@ class RecursiveDescent(positions: Positions, tokens: Seq[Token], source: Source)
124139
def skip(): Unit =
125140
previous = tokens(position)
126141
position += 1;
142+
currentLabel = None
127143
spaces()
128144

129145
def isSpace(kind: TokenKind): Boolean =
@@ -139,19 +155,16 @@ class RecursiveDescent(positions: Positions, tokens: Seq[Token], source: Source)
139155
}
140156

141157
def consume(kind: TokenKind): Unit =
142-
// if !hasNext() then fail(s"Expected ${kind}, but reached end of file")
143-
val positionBefore = position
144-
val t = next()
145-
146-
if (t.kind != kind) {
147-
// we need to fail at the position before consuming
148-
position = positionBefore
149-
fail(s"Expected ${explain(kind)} but got ${explain(t.kind)}")
158+
if !hasNext() then fail(s"Expected ${kind}, but reached end of file")
159+
if (peek.kind != kind) {
160+
fail(explain(kind), peek.kind) // s"Expected ${explain(kind)} but got ${explain(t.kind)}")
150161
}
162+
val t = next()
163+
()
151164

152165
inline def expect[T](expected: String)(inline f: PartialFunction[TokenKind, T]): T =
153166
val kind = peek.kind
154-
if f.isDefinedAt(kind) then { skip(); f(kind) } else fail(s"Expected ${expected}")
167+
if f.isDefinedAt(kind) then { skip(); f(kind) } else fail(expected, kind)
155168

156169
/* The actual parser itself
157170
* ------------------------
@@ -180,7 +193,7 @@ class RecursiveDescent(positions: Positions, tokens: Seq[Token], source: Source)
180193
*/
181194
def stmts(): Stmt =
182195
nonterminal:
183-
peek.kind match {
196+
(peek.kind match {
184197
case `val` => valStmt()
185198
case _ if isDefinition => DefStmt(definition(), semi() ~> stmts())
186199
case `with` => withStmt()
@@ -197,7 +210,7 @@ class RecursiveDescent(positions: Positions, tokens: Seq[Token], source: Source)
197210
semi()
198211
if returnPosition then Return(e)
199212
else ExprStmt(e, stmts())
200-
}
213+
}) labelled "statements"
201214

202215
// ATTENTION: here the grammar changed (we added `with val` to disambiguate)
203216
// with val <ID> (: <TYPE>)? = <EXPR>; <STMTS>
@@ -265,8 +278,10 @@ class RecursiveDescent(positions: Positions, tokens: Seq[Token], source: Source)
265278

266279
def stmt(): Stmt =
267280
nonterminal:
268-
if peek(`{`) then braces { BlockStmt(stmts()) }
269-
else when(`return`) { Return(expr()) } { Return(expr()) }
281+
{
282+
if peek(`{`) then braces { BlockStmt(stmts()) }
283+
else when(`return`) { Return(expr()) } { Return(expr()) }
284+
} labelled "statement"
270285

271286

272287
/**
@@ -309,7 +324,7 @@ class RecursiveDescent(positions: Positions, tokens: Seq[Token], source: Source)
309324
Include(`import` ~> moduleName())
310325

311326
def moduleName(): String =
312-
some(ident, `/`).mkString("/")
327+
some(ident, `/`).mkString("/") labelled "module name"
313328

314329
def isToplevel: Boolean = peek.kind match {
315330
case `val` | `fun` | `def` | `type` | `effect` | `namespace` |
@@ -450,7 +465,7 @@ class RecursiveDescent(positions: Positions, tokens: Seq[Token], source: Source)
450465

451466
peek.kind match {
452467
case `=` => `=` ~> TypeDef(id, tps.unspan, valueType())
453-
case _ => braces { DataDef(id, tps, manyWhile({ constructor() <~ semi() }, !peek(`}`))) }
468+
case _ => braces { DataDef(id, tps, manyUntil({ constructor() <~ semi() }, `}`)) }
454469
}
455470

456471
def recordDef(): Def =
@@ -459,7 +474,7 @@ class RecursiveDescent(positions: Positions, tokens: Seq[Token], source: Source)
459474

460475
def constructor(): Constructor =
461476
nonterminal:
462-
Constructor(idDef(), maybeTypeParams(), valueParams())
477+
Constructor(idDef(), maybeTypeParams(), valueParams()) labelled "constructor"
463478

464479
// On the top-level both
465480
// effect Foo = {}
@@ -492,7 +507,7 @@ class RecursiveDescent(positions: Positions, tokens: Seq[Token], source: Source)
492507

493508
def interfaceDef(): InterfaceDef =
494509
nonterminal:
495-
InterfaceDef(`interface` ~> idDef(), maybeTypeParams(), `{` ~> manyWhile(`def` ~> operation(), `def`) <~ `}`)
510+
InterfaceDef(`interface` ~> idDef(), maybeTypeParams(), `{` ~> manyUntil({ `def` ~> operation() } labelled "operation declaration", `}`) <~ `}`)
496511

497512
def namespaceDef(): Def =
498513
nonterminal:
@@ -570,10 +585,10 @@ class RecursiveDescent(positions: Positions, tokens: Seq[Token], source: Source)
570585
def externBody(): ExternBody =
571586
nonterminal:
572587
peek.kind match {
573-
case _: Ident => peek(1).kind match {
588+
case _: Ident => (peek(1).kind match {
574589
case `{` => ExternBody.EffektExternBody(featureFlag(), `{` ~> stmts() <~ `}`)
575590
case _ => ExternBody.StringExternBody(maybeFeatureFlag(), template())
576-
}
591+
}) labelled "extern body (string or block)"
577592
case _ => ExternBody.StringExternBody(maybeFeatureFlag(), template())
578593
}
579594

@@ -629,18 +644,18 @@ class RecursiveDescent(positions: Positions, tokens: Seq[Token], source: Source)
629644

630645
def returnAnnotation(): Effectful =
631646
if peek(`:`) then `:` ~> effectful()
632-
else fail("Expected return type annotation")
647+
else fail("return type annotation", peek.kind)
633648

634649
def valueTypeAnnotation(): ValueType =
635650
if peek(`:`) then `:` ~> valueType()
636-
else fail("Expected a type annotation")
651+
else fail("a type annotation", peek.kind)
637652

638653
def blockTypeAnnotation(): BlockType =
639654
if peek(`:`) then `:` ~> blockType()
640-
else fail("Expected a type annotation")
655+
else fail("a type annotation", peek.kind)
641656

642657
def expr(): Term = peek.kind match {
643-
case _ => matchExpr()
658+
case _ => matchExpr() labelled "expression"
644659
}
645660

646661
def ifExpr(): Term =
@@ -722,7 +737,7 @@ class RecursiveDescent(positions: Positions, tokens: Seq[Token], source: Source)
722737
if !peek(`def`) then fail("Expected at least one operation definition to implement this interface.")
723738
tpe
724739
} map { tpe =>
725-
Implementation(tpe, manyWhile(opClause(), `def`)) <~ `}`
740+
Implementation(tpe, manyUntil(opClause() labelled "operation clause", `}`)) <~ `}`
726741
}
727742

728743
// Interface[...] { () => ... }
@@ -735,7 +750,7 @@ class RecursiveDescent(positions: Positions, tokens: Seq[Token], source: Source)
735750
Implementation(interface, List(operation))
736751
}
737752

738-
emptyImplementation() orElse interfaceImplementation() getOrElse operationImplementation()
753+
(emptyImplementation() orElse interfaceImplementation() getOrElse operationImplementation()) labelled "interface implementation (starting with its name)"
739754

740755
def opClause(): OpClause =
741756
nonterminal:
@@ -809,7 +824,7 @@ class RecursiveDescent(positions: Positions, tokens: Seq[Token], source: Source)
809824
case Many(p :: Nil , _) => fail("Pattern matching on tuples requires more than one element")
810825
case Many(ps, span) => TagPattern(IdRef(List("effekt"), s"Tuple${ps.size}", span.synthesized), ps)
811826
}
812-
case _ => fail("Expected pattern")
827+
case k => fail("pattern", k)
813828
}
814829

815830
def matchExpr(): Term =
@@ -929,7 +944,7 @@ class RecursiveDescent(positions: Positions, tokens: Seq[Token], source: Source)
929944
// () ()
930945
def isArguments: Boolean = lookbehind(1).kind != Newline && (peek(`(`) || peek(`[`) || peek(`{`))
931946
def arguments(): (List[ValueType], List[Term], List[Term]) =
932-
if (!isArguments) fail("Expected at least one argument section (types, values, or blocks)")
947+
if (!isArguments) fail("at least one argument section (types, values, or blocks)", peek.kind)
933948
(maybeTypeArgs().unspan, maybeValueArgs(), maybeBlockArgs())
934949

935950
def maybeTypeArgs(): Many[ValueType] =
@@ -1005,7 +1020,7 @@ class RecursiveDescent(positions: Positions, tokens: Seq[Token], source: Source)
10051020
case _ if isHole => hole()
10061021
case _ if isTupleOrGroup => tupleOrGroup()
10071022
case _ if isListLiteral => listLiteral()
1008-
case _ => fail(s"Expected variables, literals, tuples, lists, holes or group")
1023+
case k => fail("variables, literals, tuples, lists, holes or group", k)
10091024
}
10101025

10111026
def isListLiteral: Boolean = peek.kind match {
@@ -1037,7 +1052,7 @@ class RecursiveDescent(positions: Positions, tokens: Seq[Token], source: Source)
10371052
case `<{` =>
10381053
val s = `<{` ~> stmts() <~ `}>`
10391054
Hole(IdDef("hole", span().synthesized), s, span())
1040-
case _ => fail("Expected hole")
1055+
case k => fail("hole", k)
10411056
}
10421057
}
10431058

@@ -1085,7 +1100,7 @@ class RecursiveDescent(positions: Positions, tokens: Seq[Token], source: Source)
10851100
case `true` => skip(); BooleanLit(true)
10861101
case `false` => skip(); BooleanLit(false)
10871102
case t if isUnitLiteral => skip(); skip(); UnitLit()
1088-
case t => fail("Expected a literal")
1103+
case t => fail("a literal", t)
10891104
}
10901105

10911106
// Will also recognize ( ) as unit if we do not emit space in the lexer...
@@ -1134,14 +1149,15 @@ class RecursiveDescent(positions: Positions, tokens: Seq[Token], source: Source)
11341149
* SetType ::= Type
11351150
* | '{' Type ',' ... '}'
11361151
*/
1137-
def effects(): Effects =
1152+
def effects(): Effects = {
11381153
nonterminal:
11391154
if (peek(`{`)) {
11401155
val effects = many(refType, `{`, `,`, `}`)
11411156
Effects(effects.unspan, effects.span)
11421157
}
11431158
else
11441159
Effects(List(refType()), span())
1160+
} labelled "effect set"
11451161

11461162
def maybeEffects(): Effects = {
11471163
nonterminal:
@@ -1172,8 +1188,10 @@ class RecursiveDescent(positions: Positions, tokens: Seq[Token], source: Source)
11721188
private def functionType(): Type = {
11731189
// Complex function type: [T]*(Int, String)*{Exc} => Int / {Effect}
11741190
def functionTypeComplex: Maybe[Type] = backtrack {
1175-
maybeTypeParams() ~ maybeValueTypes() ~ (maybeBlockTypeParams() <~ `=>`) ~ atomicType() ~ maybeEffects() match {
1176-
case tparams ~ vparams ~ bparams ~ t ~ effs => FunctionType(tparams, vparams, bparams, t, effs)
1191+
maybeTypeParams() ~ maybeValueTypes() ~ (maybeBlockTypeParams() <~ `=>`)
1192+
} map { case tparams ~ vparams ~ bparams =>
1193+
(atomicType() labelled "return type") ~ maybeEffects() match {
1194+
case t ~ effs => FunctionType(tparams, vparams, bparams, t, effs)
11771195
}
11781196
}
11791197

@@ -1211,8 +1229,8 @@ class RecursiveDescent(positions: Positions, tokens: Seq[Token], source: Source)
12111229
}
12121230

12131231
// NOTE: ValueType, BlockType are just aliases for Type.
1214-
def blockType(): BlockType = boxedType()
1215-
def valueType(): ValueType = boxedType()
1232+
def blockType(): BlockType = boxedType() labelled "block type"
1233+
def valueType(): ValueType = boxedType() labelled "value type"
12161234

12171235
// Completely specialized for TypeRef: we only parse `refType` here, we don't go through the whole hierarchy.
12181236
// This results in slightly worse errors, but massively simplifies the design.
@@ -1222,11 +1240,11 @@ class RecursiveDescent(positions: Positions, tokens: Seq[Token], source: Source)
12221240
// then pretend the effect set is empty. This seems to work out fine :)
12231241
def effectful(): Effectful = {
12241242
nonterminal:
1225-
boxedType() match
1243+
(boxedType() match
12261244
case eff: Effectful => eff
12271245
case tpe => {
12281246
Effectful(tpe, Effects.Pure(Span(source, pos(), pos(), Synthesized)), span())
1229-
}
1247+
}) labelled "return-type and effects"
12301248
}
12311249

12321250
def maybeTypeParams(): Many[Id] =
@@ -1336,7 +1354,11 @@ class RecursiveDescent(positions: Positions, tokens: Seq[Token], source: Source)
13361354
/**
13371355
* Aborts parsing with the given message
13381356
*/
1339-
def fail(message: String): Nothing = throw Fail(message, position)
1357+
def fail(expected: String, got: TokenKind): Nothing =
1358+
throw Fail.expectedButGot(currentLabel.getOrElse { expected }, explain(got), position)
1359+
1360+
def fail(msg: String): Nothing =
1361+
throw Fail(msg, position)
13401362

13411363
def softFail(message: String, start: Int, end: Int): Unit = {
13421364
softFails += SoftFail(message, start, end)
@@ -1351,10 +1373,12 @@ class RecursiveDescent(positions: Positions, tokens: Seq[Token], source: Source)
13511373
inline def backtrack[T](inline p: => T): Maybe[T] =
13521374
val before = position
13531375
val beforePrevious = previous
1376+
val labelBefore = currentLabel
13541377
try { Maybe.Some(p, span(tokens(before).end)) } catch {
13551378
case Fail(_, _) => {
13561379
position = before
13571380
previous = beforePrevious
1381+
currentLabel = labelBefore
13581382
Maybe.None(Span(source, previous.end + 1, previous.end + 1, Synthesized))
13591383
}
13601384
}
@@ -1430,13 +1454,19 @@ class RecursiveDescent(positions: Positions, tokens: Seq[Token], source: Source)
14301454
inline def manyWhile[T](p: => T, lookahead: TokenKind): List[T] =
14311455
manyWhile(p, peek(lookahead))
14321456

1457+
inline def manyUntil[T](p: => T, lookahead: TokenKind): List[T] =
1458+
manyUntil(p, peek(lookahead))
1459+
14331460
inline def manyWhile[T](p: => T, predicate: => Boolean): List[T] =
14341461
val components: ListBuffer[T] = ListBuffer.empty
14351462
while (predicate) {
14361463
components += p
14371464
}
14381465
components.toList
14391466

1467+
inline def manyUntil[T](p: => T, predicate: => Boolean): List[T] =
1468+
manyWhile(p, !predicate)
1469+
14401470
inline def parens[T](p: => T): T =
14411471
consume(`(`)
14421472
val res = p

examples/neg/paramsnotypes.check

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
[error] examples/neg/paramsnotypes.effekt:1:15: Expected a type annotation
1+
[error] examples/neg/paramsnotypes.effekt:1:15: Expected a type annotation but got )
22
def identity(x) = x
33
^
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[error] examples/neg/parsing/eof-instead-of-token.effekt:1:16: Expected operation declaration but got end of file
2+
interface Foo {
3+
^
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
interface Foo {
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
[error] examples/neg/parsing/extern-def-string.effekt:1:35: Expected string literal
1+
[error] examples/neg/parsing/extern-def-string.effekt:1:35: Expected string literal but got identifier here
22
extern def foo(): Bool = whatever here
33
^^^^
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
[error] examples/neg/parsing/extern-path.effekt:1:16: Expected path as string literal
1+
[error] examples/neg/parsing/extern-path.effekt:1:16: Expected path as string literal but got integer 42
22
extern include 42
33
^^
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
[error] examples/neg/parsing/extern-string.effekt:1:11: Expected string literal
1+
[error] examples/neg/parsing/extern-string.effekt:1:11: Expected string literal but got integer 42
22
extern js 42
33
^^
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[error] examples/neg/parsing/half-done-effect-type.effekt:1:26: Expected effect set but got }
2+
def foo{ body: => Unit / }: Unit = {
3+
^
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
def foo{ body: => Unit / }: Unit = {
2+
body()
3+
}
4+
5+
def main() = ()
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
[error] examples/neg/parsing/identifiers.effekt:1:5: Expected identifier
1+
[error] examples/neg/parsing/identifiers.effekt:1:5: Expected identifier but got integer 42
22
val 42 = 42
33
^^

0 commit comments

Comments
 (0)