Skip to content

Commit d769c1e

Browse files
authored
Merge pull request #713 from viperproject/meilers_chained_comp
Support for chained comparisons
2 parents 5babb18 + eb6bb5e commit d769c1e

4 files changed

Lines changed: 155 additions & 47 deletions

File tree

src/main/scala/viper/silver/parser/FastParser.scala

Lines changed: 38 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,16 @@ package viper.silver.parser
88

99
import java.net.URL
1010
import java.nio.file.{Files, Path, Paths}
11-
1211
import viper.silver.ast.{FilePosition, LabelledOld, LineCol, NoPosition, Position, SourcePosition}
1312
import viper.silver.parser.FastParserCompanion.{LW, LeadingWhitespace}
1413
import viper.silver.plugin.{ParserPluginTemplate, SilverPluginManager}
15-
import viper.silver.verifier.ParseError
14+
import viper.silver.verifier.{ParseError, ParseWarning}
1615

1716
import scala.collection.{immutable, mutable}
1817

1918
case class ParseException(msg: String, pos: (Position, Position)) extends Exception
2019

21-
case class SuffixedExpressionGenerator[E <: PExp](func: PExp => E) extends (PExp => PExp) {
20+
case class SuffixedExpressionGenerator[+E <: PExp](func: PExp => E) extends (PExp => PExp) {
2221
override def apply(v1: PExp): E = func(v1)
2322
}
2423

@@ -125,6 +124,7 @@ class FastParser {
125124
var _line_offset: Array[Int] = null
126125
/** The file we are currently parsing (for creating positions later). */
127126
var _file: Path = null
127+
var _warnings: Seq[ParseWarning] = Seq()
128128

129129
def parse(s: String, f: Path, plugins: Option[SilverPluginManager] = None) = {
130130
// Strategy to handle imports
@@ -390,9 +390,8 @@ class FastParser {
390390

391391
def quoted[$: P, T](p: => P[T]) = "\"" ~ p ~ "\""
392392

393-
def foldPExp[E <: PExp](e: PExp, es: Seq[SuffixedExpressionGenerator[E]]): E =
394-
es.foldLeft(e) { (t, a) => a(t)
395-
}.asInstanceOf[E]
393+
def foldPExp[E <: PExp](e: E, es: Seq[SuffixedExpressionGenerator[E]]): E =
394+
es.foldLeft(e) { (t, a) => a(t) }
396395

397396
def isFieldAccess(obj: Any) = {
398397
obj.isInstanceOf[PFieldAccess]
@@ -481,49 +480,47 @@ class FastParser {
481480
def exp[$: P]: P[PExp] = P(iteExpr)
482481

483482
def suffix[$: P]: P[SuffixedExpressionGenerator[PExp]] =
484-
P(FP("." ~ idnuse).map { case (pos, id) => SuffixedExpressionGenerator[PExp]((e: PExp) => PFieldAccess(e, id)(pos)) } |
485-
FP("[" ~ Pass ~ ".." ~/ exp ~ "]").map { case (pos, n) => SuffixedExpressionGenerator[PExp]((e: PExp) => PSeqTake(e, n)(pos)) } |
486-
FP("[" ~ exp ~ ".." ~ Pass ~ "]").map { case (pos, n) => SuffixedExpressionGenerator[PExp]((e: PExp) => PSeqDrop(e, n)(pos)) } |
487-
FP("[" ~ exp ~ ".." ~ exp ~ "]").map { case (pos, (n, m)) => SuffixedExpressionGenerator[PExp]((e: PExp) => PSeqDrop(PSeqTake(e, m)(pos), n)(pos)) } |
488-
FP("[" ~ exp ~ "]").map { case (pos, e1) => SuffixedExpressionGenerator[PExp]((e0: PExp) => PLookup(e0, e1)(pos)) } |
489-
FP("[" ~ exp ~ ":=" ~ exp ~ "]").map { case (pos, (i, v)) => SuffixedExpressionGenerator[PExp]((e: PExp) => PUpdate(e, i, v)(pos)) })
490-
491-
/*
492-
Maps:
493-
def suffix[$: P]: P[SuffixedExpressionGenerator[PExp]] =
494-
P(FP("." ~ idnuse).map { case (pos, id) => SuffixedExpressionGenerator[PExp]((e: PExp) => {
495-
PFieldAccess(e, id)(pos)
496-
}) } |
497-
FP("[" ~ Pass ~ ".." ~/ exp ~ "]").map { case (pos, n) => SuffixedExpressionGenerator[PExp]((e: PExp) => PSeqTake(e, n)(pos)) } |
498-
FP("[" ~ exp ~ ".." ~ Pass ~ "]").map { case (pos, n) => SuffixedExpressionGenerator[PExp]((e: PExp) => PSeqDrop(e, n)(pos)) } |
499-
FP("[" ~ exp ~ ".." ~ exp ~ "]").map { case (pos, (n, m)) => SuffixedExpressionGenerator[PExp]((e: PExp) => PSeqDrop(PSeqTake(e, m)(), n)(pos)) } |
500-
FP("[" ~ exp ~ "]").map { case (pos, e1) => SuffixedExpressionGenerator[PExp]((e0: PExp) => PSeqIndex(e0, e1)(pos)) } |
501-
FP("[" ~ exp ~ ":=" ~ exp ~ "]").map { case (pos, (i, v)) => SuffixedExpressionGenerator[PExp]((e: PExp) => PSeqUpdate(e, i, v)(pos)) })
502-
*/
483+
P(FP("." ~ idnuse).map { case (pos, id) => SuffixedExpressionGenerator[PExp](e => PFieldAccess(e, id)(e.pos._1, pos._2)) } |
484+
FP("[" ~ Pass ~ ".." ~/ exp ~ "]").map { case (pos, n) => SuffixedExpressionGenerator[PExp](e => PSeqTake(e, n)(e.pos._1, pos._2)) } |
485+
FP("[" ~ exp ~ ".." ~ Pass ~ "]").map { case (pos, n) => SuffixedExpressionGenerator[PExp](e => PSeqDrop(e, n)(e.pos._1, pos._2)) } |
486+
FP("[" ~ exp ~ ".." ~ exp ~ "]").map { case (pos, (n, m)) => SuffixedExpressionGenerator[PExp](e => PSeqDrop(PSeqTake(e, m)(e.pos._1, pos._2), n)(e.pos._1, pos._2)) } |
487+
FP("[" ~ exp ~ "]").map { case (pos, e1) => SuffixedExpressionGenerator[PExp](e0 => PLookup(e0, e1)(e0.pos._1, pos._2)) } |
488+
FP("[" ~ exp ~ ":=" ~ exp ~ "]").map { case (pos, (i, v)) => SuffixedExpressionGenerator[PExp](e => PUpdate(e, i, v)(e.pos._1, pos._2)) })
503489

504-
def suffixExpr[$: P]: P[PExp] = P((atom ~~~ suffix.lw.rep).map { case (fac, ss) => foldPExp[PExp](fac, ss) })
490+
def suffixExpr[$: P]: P[PExp] = P((atom ~~~ suffix.lw.rep).map { case (fac, ss) => foldPExp(fac, ss) })
505491

506492
def termOp[$: P]: P[String] = P(StringIn("*", "/", "\\", "%").!)
507493

508-
def term[$: P]: P[PExp] = P((suffixExpr ~~~ termd.lw.rep).map { case (a, ss) => foldPExp[PExp](a, ss) })
494+
def term[$: P]: P[PExp] = P((suffixExpr ~~~ termd.lw.rep).map { case (a, ss) => foldPExp(a, ss) })
509495

510-
def termd[$: P]: P[SuffixedExpressionGenerator[PExp]] = FP(termOp ~ suffixExpr).map { case (pos, (op, id)) => SuffixedExpressionGenerator[PExp]((e: PExp) => PBinExp(e, op, id)(pos)) }
496+
def termd[$: P]: P[SuffixedExpressionGenerator[PBinExp]] = FP(termOp ~ suffixExpr).map { case (pos, (op, id)) => SuffixedExpressionGenerator(e => PBinExp(e, op, id)(e.pos._1, pos._2)) }
511497

512498
def sumOp[$: P]: P[String] = P(StringIn("++", "+", "-").! | keyword("union").! | keyword("intersection").! | keyword("setminus").! | keyword("subset").!)
513499

514-
def sum[$: P]: P[PExp] = P((term ~~~ sumd.lw.rep).map { case (a, ss) => foldPExp[PBinExp](a, ss) })
500+
def sum[$: P]: P[PExp] = P((term ~~~ sumd.lw.rep).map { case (a, ss) => foldPExp(a, ss) })
515501

516-
def sumd[$: P]: P[SuffixedExpressionGenerator[PBinExp]] = FP(sumOp ~ term).map { case (pos, (op, id)) => SuffixedExpressionGenerator[PBinExp]((e: PExp) => PBinExp(e, op, id)(pos)) }
502+
def sumd[$: P]: P[SuffixedExpressionGenerator[PBinExp]] = FP(sumOp ~ term).map { case (pos, (op, id)) => SuffixedExpressionGenerator(e => PBinExp(e, op, id)(e.pos._1, pos._2)) }
517503

518504
def cmpOp[$: P] = P(StringIn("<=", ">=", "<", ">").! | keyword("in").!)
519505

520-
def cmpExp[$: P]: P[PExp] = FP(sum ~~~ (cmpOp ~ cmpExp).lw.?).map {
521-
case (pos, (a, b)) => b match {
522-
case Some(c) => PBinExp(a, c._1, c._2)(pos)
523-
case None => a
524-
}
506+
val cmpOps = Set("<=", ">=", "<", ">", "in")
507+
508+
def cmpd[$: P]: P[PExp => SuffixedExpressionGenerator[PBinExp]] = FP(cmpOp ~ sum).map {
509+
case (pos, (op, right)) => chainComp(op, right, pos)
525510
}
526511

512+
def chainComp(op: String, right: PExp, pos: (FilePosition, FilePosition))(from: PExp) = SuffixedExpressionGenerator(_ match {
513+
case left@PBinExp(_, op0, middle) if cmpOps.contains(op0) && left != from =>
514+
PBinExp(left, "&&", PBinExp(middle, op, right)(middle.pos._1, pos._2))(left.pos._1, pos._2)
515+
case left@PBinExp(_, "&&", PBinExp(_, op0, middle)) if cmpOps.contains(op0) && left != from =>
516+
PBinExp(left, "&&", PBinExp(middle, op, right)(middle.pos._1, pos._2))(left.pos._1, pos._2)
517+
case left => PBinExp(left, op, right)(left.pos._1, pos._2)
518+
})
519+
520+
def cmpExp[$: P]: P[PExp] = P((sum ~~~ cmpd.lw.rep).map {
521+
case (from, others) => foldPExp(from, others.map(_(from)))
522+
})
523+
527524
def eqOp[$: P] = P(StringIn("==", "!=").!)
528525

529526
def eqExp[$: P]: P[PExp] = FP(cmpExp ~~~ (eqOp ~ eqExp).lw.?).map {
@@ -631,11 +628,15 @@ class FastParser {
631628
case (pos, (keyType, valueType)) => PMapType(keyType, valueType)(pos)
632629
}
633630

631+
634632
/** Only for call-like macros, `idnuse`-like ones are parsed by `domainTyp`. */
635633
def macroType[$: P] : P[PMacroType] = funcApp.map(PMacroType(_))
636634

637-
def primitiveTyp[$: P]: P[PPrimitiv] = P(FP(keyword("Rational")).map{ case (pos, _) => PPrimitiv("Perm")(pos)}
638-
| FP((StringIn("Int", "Bool", "Perm", "Ref") ~~ !identContinues).!).map{ case (pos, name) => PPrimitiv(name)(pos)})
635+
def primitiveTyp[$: P]: P[PPrimitiv] = P(FP(keyword("Rational")).map {
636+
case (pos, _) =>
637+
_warnings = _warnings :+ ParseWarning("Rational is deprecated, use Perm instead", SourcePosition(_file, pos._1.line, pos._1.column))
638+
PPrimitiv("Perm")(pos)
639+
} | FP((StringIn("Int", "Bool", "Perm", "Ref") ~~ !identContinues).!).map { case (pos, name) => PPrimitiv(name)(pos) })
639640
/* Maps:
640641
lazy val primitiveTyp: P[PType] = P(keyword("Rational").map(_ => PPrimitiv("Perm"))
641642
| (StringIn("Int", "Bool", "Perm", "Ref") ~~ !identContinues).!.map(PPrimitiv))
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
// Any copyright is dedicated to the Public Domain.
2+
// http://creativecommons.org/publicdomain/zero/1.0/
3+
4+
method chain(i1: Int, i2: Int, i3: Int, i4: Int)
5+
requires i1 < i2 <= i3 > i4
6+
{
7+
assert i1 < i2
8+
assert i2 <= i3
9+
assert i3 > i4
10+
//:: ExpectedOutput(assert.failed:assertion.false)
11+
assert i2 < i3
12+
}
13+
14+
method chain1In(s1: Seq[Int], s2: Seq[Seq[Int]], s3: Seq[Seq[Seq[Int]]], s4: Set[Seq[Seq[Seq[Int]]]])
15+
requires s1 in s2 in s3 in s4
16+
{
17+
assert s1 in s2
18+
assert s3 in s4
19+
assert s2 in s3
20+
//:: ExpectedOutput(assert.failed:assertion.false)
21+
assert 3 in s1
22+
}
23+
24+
25+
method chainEq(i1: Int, i2: Int, i3: Int, i4: Int)
26+
requires i1 < i2 == i3 > i4
27+
{
28+
//:: ExpectedOutput(assert.failed:assertion.false)
29+
assert i1 < i2
30+
}
31+
32+
method nonChain(i1: Int, i2: Int, i3: Int, i4: Int)
33+
requires i1 < i2 && i3 > i4
34+
{
35+
assert i1 < i2
36+
assert i3 > i4
37+
//:: ExpectedOutput(assert.failed:assertion.false)
38+
assert i2 <= i3
39+
}
40+
41+
method chainParen(i1: Int, i2: Multiset[Int], i3: Int, i4: Int)
42+
requires (i1 in i2) <= i3 > i4
43+
{
44+
assert i3 > i4
45+
assume (i1 in i2) == 3
46+
assert 3 <= i3
47+
}
48+
49+
method chainParen2(i1: Int, i2: Int, i3: Int, i4: Multiset[Int])
50+
requires i1 < i2 <= (i3 in i4)
51+
{
52+
assert i1 < i2
53+
assume (i3 in i4) == 3
54+
assert i2 <= 3
55+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Any copyright is dedicated to the Public Domain.
2+
// http://creativecommons.org/publicdomain/zero/1.0/
3+
4+
method chain(i1: Int, i2: Ref, i3: Int, i4: Int)
5+
//:: ExpectedOutput(typechecker.error)
6+
requires i1 < i2 <= i3 > i4
7+
{
8+
}
9+
10+
method chain1In(s1: Seq[Int], s2: Seq[Seq[Int]], s3: Seq[Seq[Seq[Ref]]], s4: Set[Seq[Seq[Seq[Int]]]])
11+
//:: ExpectedOutput(typechecker.error)
12+
requires s1 in s2 in s3 in s4
13+
{
14+
}
15+
16+
method chainParen(i1: Int, i2: Int, i3: Int, i4: Int)
17+
//:: ExpectedOutput(typechecker.error)
18+
requires (i1 < i2) <= i3 > i4
19+
{
20+
}
21+
22+
method chainParen2(i1: Int, i2: Int, i3: Int, i4: Int)
23+
//:: ExpectedOutput(typechecker.error)
24+
requires i1 < i2 <= (i3 > i4)
25+
{
26+
}

src/test/scala/AstPositionsTests.scala

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ class AstPositionsTests extends AnyFunSuite {
5353
|method sum(x: Ref, g: Set[Ref])
5454
| returns (res: Bool)
5555
| ensures false && 4/x.foo > 0
56+
| ensures 3 < 4 < 5
5657
|{
5758
| inhale forall r: Ref :: r in g ==> acc(r.foo)
5859
| assert acc(x.foo)
@@ -82,7 +83,7 @@ class AstPositionsTests extends AnyFunSuite {
8283
val m: Method = res.methods(0)
8384
m.pos match {
8485
case spos: AbstractSourcePosition => {
85-
assert(spos.start.line === 2 && spos.end.get.line === 10)
86+
assert(spos.start.line === 2 && spos.end.get.line === 11)
8687
assert(spos.start.column === 1 && spos.end.get.column === 2)
8788
}
8889
case _ =>
@@ -99,22 +100,47 @@ class AstPositionsTests extends AnyFunSuite {
99100
case _ =>
100101
fail("method args must have start and end positions set")
101102
}
102-
// Check position of method post
103-
assert(m.posts.length === 1)
104-
val post: Exp = m.posts(0).asInstanceOf[BinExp].args(1);
105-
post.pos match {
103+
// Check positions of method posts
104+
assert(m.posts.length === 2)
105+
val post1: Exp = m.posts(0).asInstanceOf[BinExp].args(1);
106+
post1.pos match {
106107
case spos: AbstractSourcePosition => {
107108
assert(spos.start.line === 4 && spos.end.get.line === 4)
108109
assert(spos.start.column === 20 && spos.end.get.column === 31)
109110
}
110111
case _ =>
111112
fail("method posts must have start and end positions set")
112113
}
114+
val post2: BinExp = m.posts(1).asInstanceOf[BinExp]
115+
post2.pos match {
116+
case spos: AbstractSourcePosition => {
117+
assert(spos.start.line === 5 && spos.end.get.line === 5)
118+
assert(spos.start.column === 11 && spos.end.get.column === 20)
119+
}
120+
case _ =>
121+
fail("method posts must have start and end positions set")
122+
}
123+
post2.left.pos match {
124+
case spos: AbstractSourcePosition => {
125+
assert(spos.start.line === 5 && spos.end.get.line === 5)
126+
assert(spos.start.column === 11 && spos.end.get.column === 16)
127+
}
128+
case _ =>
129+
fail("method posts must have start and end positions set")
130+
}
131+
post2.right.pos match {
132+
case spos: AbstractSourcePosition => {
133+
assert(spos.start.line === 5 && spos.end.get.line === 5)
134+
assert(spos.start.column === 15 && spos.end.get.column === 20)
135+
}
136+
case _ =>
137+
fail("method posts must have start and end positions set")
138+
}
113139
// Check position of body
114140
val block = m.body.get
115141
block.pos match {
116142
case spos: AbstractSourcePosition => {
117-
assert(spos.start.line === 5 && spos.end.get.line === 10)
143+
assert(spos.start.line === 6 && spos.end.get.line === 11)
118144
assert(spos.start.column === 1 && spos.end.get.column === 2)
119145
}
120146
case _ =>
@@ -125,7 +151,7 @@ class AstPositionsTests extends AnyFunSuite {
125151
val forall: Stmt = block.ss(0)
126152
forall.pos match {
127153
case spos: AbstractSourcePosition => {
128-
assert(spos.start.line === 6 && spos.end.get.line === 6)
154+
assert(spos.start.line === 7 && spos.end.get.line === 7)
129155
assert(spos.start.column === 3 && spos.end.get.column === 48)
130156
}
131157
case _ =>
@@ -135,7 +161,7 @@ class AstPositionsTests extends AnyFunSuite {
135161
val assert_exp: Exp = block.ss(1).asInstanceOf[Assert].exp
136162
assert_exp.pos match {
137163
case spos: AbstractSourcePosition => {
138-
assert(spos.start.line === 7 && spos.end.get.line === 7)
164+
assert(spos.start.line === 8 && spos.end.get.line === 8)
139165
assert(spos.start.column === 10 && spos.end.get.column === 20)
140166
}
141167
case _ =>
@@ -146,7 +172,7 @@ class AstPositionsTests extends AnyFunSuite {
146172
val m2: Method = res.methods(1);
147173
m2.pos match {
148174
case spos: AbstractSourcePosition => {
149-
assert(spos.start.line === 11 && spos.end.get.line === 12)
175+
assert(spos.start.line === 12 && spos.end.get.line === 13)
150176
assert(spos.start.column === 1 && spos.end.get.column === 26)
151177
}
152178
case _ =>
@@ -156,7 +182,7 @@ class AstPositionsTests extends AnyFunSuite {
156182
val pre: Exp = m2.pres(0);
157183
pre.pos match {
158184
case spos: AbstractSourcePosition => {
159-
assert(spos.start.line === 12 && spos.end.get.line === 12)
185+
assert(spos.start.line === 13 && spos.end.get.line === 13)
160186
assert(spos.start.column === 13 && spos.end.get.column === 26)
161187
}
162188
case _ =>

0 commit comments

Comments
 (0)