Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Parser changes for documentation generator #780

Draft
wants to merge 14 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 11 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
54 changes: 36 additions & 18 deletions src/main/scala/viper/silver/parser/FastParser.scala
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ object FastParserCompanion {
implicit val whitespace = {
import NoWhitespace._
implicit ctx: ParsingRun[_] =>
NoTrace((("/*" ~ (!StringIn("*/") ~ AnyChar).rep ~ "*/") | ("//" ~ CharsWhile(_ != '\n').? ~ ("\n" | End)) | " " | "\t" | "\n" | "\r").rep)
NoTrace((("/*" ~ (!"*/" ~ AnyChar).rep ~ "*/") | ((("//" ~ !("/")) | "////") ~ CharsWhile(_ != '\n').? ~ ("\n" | End)) | " " | "\t" | "\n" | "\r").rep)
}

def identStarts[$: P] = CharIn("A-Z", "a-z", "$_")
Expand Down Expand Up @@ -312,14 +312,7 @@ class FastParser {
//////////////////////////

import fastparse._
import FastParserCompanion.{ExtendedParsing, identContinues, identStarts, LeadingWhitespace, Pos, PositionParsing, reservedKw, reservedSym}


implicit val whitespace = {
import NoWhitespace._
implicit ctx: ParsingRun[_] =>
NoTrace((("/*" ~ (!StringIn("*/") ~ AnyChar).rep ~ "*/") | ("//" ~ CharsWhile(_ != '\n').? ~ ("\n" | End)) | " " | "\t" | "\n" | "\r").rep)
}
import FastParserCompanion.{ExtendedParsing, identContinues, identStarts, LeadingWhitespace, Pos, PositionParsing, reservedKw, reservedSym, whitespace}

implicit val lineCol: LineCol = new LineCol(this)

Expand Down Expand Up @@ -403,7 +396,15 @@ class FastParser {

def stringLiteral[$: P]: P[PStringLiteral] = P((CharsWhile(_ != '\"').! map PRawString.apply).pos.quotes map (PStringLiteral.apply _)).pos

def annotation[$: P]: P[PAnnotation] = P((P(PSym.At) ~~ annotationIdentifier ~ argList(stringLiteral)) map (PAnnotation.apply _).tupled).pos
def docAnnotation[$: P]: P[PAnnotation] = P("///" ~~ CharsWhile(_ != '\n', 0).!).map{
Copy link
Contributor

Choose a reason for hiding this comment

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

The /// should be parsed with e.g. P(PSym.TripleSlash), the position of the CharsWhile(_ != '\n', 0).! should actually be precise when saving it to a PRawString - desugaring during parsing is the wrong place to do it. I would create a sealed PAnnotation trait with all the methods that the current PAnnotation has and have two classes implementing it.

case s: String => p: Pos =>
val annotationKey = PRawString("doc")(NoPosition, NoPosition)
val docstring = PStringLiteral(PGrouped[PSym.Quote.type, PRawString](PReserved.implied(PSym.Quote), PRawString(s)(p), PReserved.implied(PSym.Quote))(p))(p)
val annotationValues = PDelimited[PStringLiteral, PSym.Comma](Some(docstring), Seq(), None)(p)
PAnnotation(at = PReserved.implied(PSym.At), key = annotationKey, values = PGrouped.impliedParen(annotationValues))(p)
}.pos

def annotation[$: P]: P[PAnnotation] = P(P((P(PSym.At) ~~ annotationIdentifier ~ argList(stringLiteral)) map (PAnnotation.apply _).tupled).pos | docAnnotation)

def annotated[$: P, T](inner: => P[PAnnotationsPosition => T]): P[T] = P((annotation.rep(0) ~ inner).map {
case (annotations, i) => pos: Pos => i(PAnnotationsPosition(annotations, pos))
Expand Down Expand Up @@ -486,7 +487,7 @@ class FastParser {

def suffixExpr[$: P](bracketed: Boolean = false): P[PExp] = P((atomParen(bracketed) ~~~ suffix.lw.rep).map { case (fac, ss) => foldPExp(fac, ss) })

def termOp[$: P] = P(reservedSymMany(None, StringIn("*", "/", "\\", "%"), _ match {
def termOp[$: P] = P(reservedSymMany(None, !("///") ~ StringIn("*", "/", "\\", "%"), _ match {
case "*" => PSymOp.Mul
case "/" => PSymOp.Div
case "\\" => PSymOp.ArithDiv
Expand Down Expand Up @@ -807,9 +808,15 @@ class FastParser {
P((P(PKw.Else) ~ stmtBlock()) map (PElse.apply _).tupled).pos

def whileStmt[$: P]: P[PKw.While => Pos => PWhile] =
P((parenthesizedExp ~~ semiSeparated(invariant) ~ stmtBlock()) map { case (cond, invs, body) => PWhile(_, cond, invs, body) })
P((parenthesizedExp ~~ semiSeparated(annotatedInvariant) ~ stmtBlock()) map { case (cond, invs, body) => PWhile(_, cond, invs, body) })

def invariant(implicit ctx : P[_]) : P[PSpecification[PKw.InvSpec]] = P((P(PKw.Invariant) ~ exp).map((PSpecification.apply _).tupled).pos | ParserExtension.invSpecification(ctx))
def annotatedInvariant(implicit ctx : P[_]) : P[PSpecification[PKw.InvSpec]] =
NoCut(P(annotation.rep(0) ~ invariant)).map{ case (anns, spec) => p: Pos =>
PSpecification[PKw.InvSpec](spec.k, spec.e, anns)(p) }.pos

def invariant(implicit ctx : P[_]) : P[PSpecification[PKw.InvSpec]] =
P((P(PKw.Invariant) ~ exp).map{ case (kw, e) => p: Pos =>
PSpecification[PKw.InvSpec](kw, e)(p) }.pos | ParserExtension.invSpecification(ctx))

def localVars[$: P]: P[PKw.Var => Pos => PVars] =
P((nonEmptyIdnTypeList(PLocalVarDecl(_)) ~~~ (P(PSymOp.Assign) ~ exp).lw.?) map { case (a, i) => PVars(_, a, i) })
Expand All @@ -824,7 +831,7 @@ class FastParser {
def goto[$: P]: P[PKw.Goto => Pos => PGoto] = P(idnref[$, PLabel] map { i => PGoto(_, i) _ })

def label[$: P]: P[PKw.Label => Pos => PLabel] =
P((idndef ~~ semiSeparated(invariant)) map { case (i, inv) => k=> PLabel(k, i, inv) _ })
P((idndef ~~ semiSeparated(annotatedInvariant)) map { case (i, inv) => k=> PLabel(k, i, inv) _ })

def packageWand[$: P]: P[PKw.Package => Pos => PPackageWand] =
P((magicWandExp() ~~~ stmtBlock().lw.?) map { case (wand, proof) => PPackageWand(_, wand, proof) _ })
Expand Down Expand Up @@ -915,23 +922,34 @@ class FastParser {
})

def functionDecl[$: P]: P[PKw.Function => PAnnotationsPosition => PFunction] = P((idndef ~ argList(formalArg) ~ PSym.Colon ~ typ
~~ semiSeparated(precondition) ~~ semiSeparated(postcondition) ~~~ bracedExp.lw.?
~~ semiSeparated(annotatedPrecondition) ~~ semiSeparated(annotatedPostcondition) ~~~ bracedExp.lw.?
) map { case (idn, args, c, typ, d, e, f) => k =>
ap: PAnnotationsPosition => PFunction(ap.annotations, k, idn, args, c, typ, d, e, f)(ap.pos)
})

def annotatedPrecondition(implicit ctx : P[_]) : P[PSpecification[PKw.PreSpec]] =
NoCut(P(annotation.rep(0) ~ precondition)).map{ case (anns, spec) => p: Pos =>
PSpecification[PKw.PreSpec](spec.k, spec.e, anns)(p) }.pos

def precondition(implicit ctx : P[_]) : P[PSpecification[PKw.PreSpec]] =
P((P(PKw.Requires) ~ exp).map{ case (kw, e) => p: (FilePosition, FilePosition) =>
PSpecification[PKw.PreSpec](kw, e)(p)}.pos | ParserExtension.preSpecification(ctx))

def precondition(implicit ctx : P[_]) : P[PSpecification[PKw.PreSpec]] = P((P(PKw.Requires) ~ exp).map((PSpecification.apply _).tupled).pos | ParserExtension.preSpecification(ctx))
def annotatedPostcondition(implicit ctx : P[_]) : P[PSpecification[PKw.PostSpec]] =
NoCut(P(annotation.rep(0) ~ postcondition)).map{ case (anns, spec) => p: Pos =>
PSpecification[PKw.PostSpec](spec.k, spec.e, anns)(p) }.pos

def postcondition(implicit ctx : P[_]) : P[PSpecification[PKw.PostSpec]] = P((P(PKw.Ensures) ~ exp).map((PSpecification.apply _).tupled).pos | ParserExtension.postSpecification(ctx))
def postcondition(implicit ctx : P[_]) : P[PSpecification[PKw.PostSpec]] =
P((P(PKw.Ensures) ~ exp).map{ case (kw, e) => p: Pos =>
PSpecification[PKw.PostSpec](kw, e)(p)}.pos | ParserExtension.postSpecification(ctx))

def predicateDecl[$: P]: P[PKw.Predicate => PAnnotationsPosition => PPredicate] = P(idndef ~ argList(formalArg) ~~~ bracedExp.lw.?).map {
case (idn, args, c) => k =>
ap: PAnnotationsPosition => PPredicate(ap.annotations, k, idn, args, c)(ap.pos)
}

def methodDecl[$: P]: P[PKw.Method => PAnnotationsPosition => PMethod] =
P((idndef ~ argList(formalArg) ~~~ methodReturns.lw.? ~~ semiSeparated(precondition) ~~ semiSeparated(postcondition) ~~~ stmtBlock().lw.?) map {
P((idndef ~ argList(formalArg) ~~~ methodReturns.lw.? ~~ semiSeparated(annotatedPrecondition) ~~ semiSeparated(annotatedPostcondition) ~~~ stmtBlock().lw.?) map {
case (idn, args, rets, pres, posts, body) => k =>
ap: PAnnotationsPosition => PMethod(ap.annotations, k, idn, args, rets, pres, posts, body)(ap.pos)
})
Expand Down
2 changes: 1 addition & 1 deletion src/main/scala/viper/silver/parser/ParseAst.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1668,7 +1668,7 @@ case class PFields(annotations: Seq[PAnnotation], field: PKw.Field, fields: PDel
override def declares: Seq[PGlobalDeclaration] = fields.toSeq
}

case class PSpecification[+T <: PKw.Spec](k: PReserved[PKw.Spec], e: PExp)(val pos: (Position, Position)) extends PNode with PPrettySubnodes {
case class PSpecification[+T <: PKw.Spec](k: PReserved[PKw.Spec], e: PExp, annotations: Seq[PAnnotation] = Seq())(val pos: (Position, Position)) extends PNode with PPrettySubnodes {
override def pretty: String = "\n " + super.pretty
}

Expand Down
14 changes: 11 additions & 3 deletions src/main/scala/viper/silver/parser/Translator.scala
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,9 @@ case class Translator(program: PProgram) {

val newBody = body.map(actualBody => stmt(actualBody).asInstanceOf[Seqn])

val finalMethod = m.copy(pres = pres.toSeq map (p => exp(p.e)), posts = posts.toSeq map (p => exp(p.e)), body = newBody)(m.pos, m.info, m.errT)
val finalMethod = m.copy(pres = pres.toSeq map (p => exp(wrapAnnotations(p.annotations, p.e)(p.pos))),
posts = posts.toSeq map (p => exp(wrapAnnotations(p.annotations, p.e)(p.pos))),
body = newBody)(m.pos, m.info, m.errT)

members(m.name) = finalMethod

Expand All @@ -107,7 +109,9 @@ case class Translator(program: PProgram) {
private def translate(f: PFunction): Function = f match {
case PFunction(_, _, idndef, _, _, _, pres, posts, body) =>
val f = findFunction(idndef)
val ff = f.copy( pres = pres.toSeq map (p => exp(p.e)), posts = posts.toSeq map (p => exp(p.e)), body = body map (_.e.inner) map exp)(f.pos, f.info, f.errT)
val ff = f.copy( pres = pres.toSeq map (p => exp(wrapAnnotations(p.annotations, p.e)(p.pos))),
posts = posts.toSeq map (p => exp(wrapAnnotations(p.annotations, p.e)(p.pos))),
body = body map (_.e.inner) map exp)(f.pos, f.info, f.errT)
members(f.name) = ff
ff
}
Expand Down Expand Up @@ -232,7 +236,8 @@ case class Translator(program: PProgram) {
}) getOrElse Statements.EmptyStmt)(pos, info)
case PElse(_, els) => stmt(els)
case PWhile(_, cond, invs, body) =>
While(exp(cond.inner), invs.toSeq map (inv => exp(inv.e)), stmt(body).asInstanceOf[Seqn])(pos, info)
While(exp(cond.inner), invs.toSeq map (inv => exp(wrapAnnotations(inv.annotations, inv.e)(inv.pos))),
stmt(body).asInstanceOf[Seqn])(pos, info)
case PQuasihavoc(_, lhs, e) =>
val (newLhs, newE) = havocStmtHelper(lhs, e)
Quasihavoc(newLhs, newE)(pos, info)
Expand Down Expand Up @@ -313,6 +318,9 @@ case class Translator(program: PProgram) {
}
}

def wrapAnnotations(annotations: Seq[PAnnotation], pexp: PExp)(pos: (Position, Position)): PExp =
annotations.foldRight(pexp) { (ann, e) => new PAnnotatedExp(ann, e)(pos) }

def extractAnnotation(pexp: PExp): (PExp, Map[String, Seq[String]]) = {
pexp match {
case PAnnotatedExp(ann, e) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ package viper.silver.plugin.standard.termination

import viper.silver.ast.utility.{Functions, ViperStrategy}
import viper.silver.ast.utility.rewriter.{SimpleContext, Strategy, StrategyBuilder}
import viper.silver.ast.{Applying, Assert, CondExp, CurrentPerm, Exp, FuncApp, Function, InhaleExhaleExp, MagicWand, Method, Node, Program, Unfolding, While}
import viper.silver.ast.{Applying, Assert, CondExp, CurrentPerm, Exp, FilePosition, FuncApp, Function, InhaleExhaleExp, MagicWand, Method, Node, Program, Unfolding, While}
import viper.silver.parser._
import viper.silver.plugin.standard.predicateinstance.{PMarkerSymbol, PPredicateInstance}
import viper.silver.plugin.standard.termination.transformation.Trafo
Expand Down Expand Up @@ -45,7 +45,8 @@ class TerminationPlugin(@unused reporter: viper.silver.reporter.Reporter,
* decreases *
*/
def decreases[$: P]: P[PSpecification[PDecreasesKeyword.type]] =
P((P(PDecreasesKeyword) ~ (decreasesWildcard | decreasesStar | decreasesTuple)) map (PSpecification.apply _).tupled).pos
P((P(PDecreasesKeyword) ~ (decreasesWildcard | decreasesStar | decreasesTuple)) map {
case (k, e) => p: (FilePosition, FilePosition) => PSpecification(k, e)(p) }).pos
def decreasesTuple[$: P]: P[PDecreasesTuple] =
P((exp.delimited(PSym.Comma) ~~~ condition.lw.?) map (PDecreasesTuple.apply _).tupled).pos
def decreasesWildcard[$: P]: P[PDecreasesWildcard] = P((P(PWildcardSym) ~~~ condition.lw.?) map (PDecreasesWildcard.apply _).tupled).pos
Expand Down Expand Up @@ -239,7 +240,7 @@ class TerminationPlugin(@unused reporter: viper.silver.reporter.Reporter,
* Remove decreases clauses from the AST and add them as information to the AST nodes
*/
override def beforeVerify(input: Program): Program = {
// Prevent potentially unsafe (mutually) recursive function calls in function postcondtions
// Prevent potentially unsafe (mutually) recursive function calls in function postconditions
// for all functions that don't have a decreases clause
if (!deactivated) {
lazy val cycles = Functions.findFunctionCyclesViaOptimized(input, func => func.body.toSeq)
Expand Down
149 changes: 149 additions & 0 deletions src/test/scala/DocAnnotationTests.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2011-2024 ETH Zurich.

import org.scalatest.funsuite.AnyFunSuite
import viper.silver.ast.Program
import viper.silver.frontend._
import viper.silver.logger.ViperStdOutLogger
import viper.silver.reporter.StdIOReporter
import viper.silver.parser.{PProgram, PAnnotatedExp, PWhile}

class DocAnnotationTests extends AnyFunSuite {
object AstProvider extends ViperAstProvider(StdIOReporter(), ViperStdOutLogger("DocAnnotationTestsLogger").get) {
def setCode(code: String): Unit = {
_input = Some(code)
}

override def reset(input: java.nio.file.Path): Unit = {
if (state < DefaultStates.Initialized) sys.error("The translator has not been initialized.")
_state = DefaultStates.InputSet
_inputFile = Some(input)
/** must be set by [[setCode]] */
// _input = None
_errors = Seq()
_parsingResult = None
_semanticAnalysisResult = None
_verificationResult = None
_program = None
resetMessages()
}
}

def generatePAstAndAst(code: String): Option[(PProgram, Program)] = {
val code_id = code.hashCode.asInstanceOf[Short].toString
AstProvider.setCode(code)
AstProvider.execute(Seq("--ignoreFile", code_id))

if (AstProvider.errors.isEmpty) {
Some(AstProvider.parsingResult, AstProvider.translationResult)
} else {
AstProvider.logger.error(s"An error occurred while translating ${AstProvider.errors}")
None
}
}

test("Basic parsing of documentation") {
import viper.silver.ast._

val code =
"""/// a field
|field f: Int
|/// P is a predicate
|predicate P(x: Int)
|
|/// a function
|function fun(x: Int): Int {
| (x / 1 == x) ? 42 : 0
|}
|/// a second function
|function fun2(x: Int): Int
| /// precondition
| requires x > 0
| /// post-
| /// condition
| ensures result > 0
|
|/// very important domain
|domain ImportantType {
|
| /// this function
| /// is crucial
| function phi(ImportantType): Int
|
| /// the only axiom
| axiom {
| /// documenting an expression
| true
| }
|}
|
|/// a macro
|define plus(a, b) (a+b)
|
|/// this is a method
|/// it does something
|method m(x: Ref, y: Ref)
| /// this documents the first precondition
| requires acc(x.f)
| /// documentation of the second precondition
| requires acc(y.f)
|{
| var local: Bool
|
| while (true)///the invariant
| /// is always true
| invariant true
| /// termination
| decreases x.f
| {}
|
|}
|""".stripMargin

val pAst: PProgram = generatePAstAndAst(code).get._1

val fieldAnn = pAst.fields.head.annotations.head.values.inner.first.get.str
assert(fieldAnn == " a field")

val predicateAnnotation = pAst.predicates.head.annotations.head.values.inner.first.get.str
assert(predicateAnnotation == " P is a predicate")

val functionAnnotation = pAst.functions.head.annotations.head.values.inner.first.get.str
assert(functionAnnotation == " a function")

val fun2Annotation = pAst.functions(1).annotations.head.values.inner.first.get.str
val fun2PreAnnotations = pAst.functions(1).pres.head.annotations.map(_.values.inner.first.get.str)
val fun2PostAnnotations = pAst.functions(1).posts.head.annotations.map(_.values.inner.first.get.str)
assert(fun2Annotation == " a second function")
assert(fun2PreAnnotations == Seq(" precondition"))
assert(fun2PostAnnotations == Seq(" post-", " condition"))

val domainAnnotation = pAst.domains.head.annotations.head.values.inner.first.get.str
assert(domainAnnotation == " very important domain")

val domainFunctionAnnotations = pAst.domains.head.members.inner.funcs.head.annotations.map(_.values.inner.first.get.str)
assert(domainFunctionAnnotations == Seq(" this function", " is crucial"))

val axiomAnnotations = pAst.domains.head.members.inner.axioms.head.annotations.map(_.values.inner.first.get.str)
assert(axiomAnnotations == Seq(" the only axiom"))
val axiomExpAnnotations = pAst.domains.head.members.inner.axioms.head.exp.e.inner.asInstanceOf[PAnnotatedExp].annotation.values.inner.first.get.str
assert(axiomExpAnnotations == " documenting an expression")

val macroAnnotation = pAst.macros.head.annotations.head.values.inner.first.get.str
assert(macroAnnotation == " a macro")

val methodAnnotations = pAst.methods.head.annotations.map(_.values.inner.first.get.str)
assert(methodAnnotations == Seq(" this is a method", " it does something"))

val methodPreAnnotations = pAst.methods.head.pres.toSeq.map(_.annotations.head.values.inner.first.get.str)
assert(methodPreAnnotations == Seq(" this documents the first precondition", " documentation of the second precondition"))

val loopInvariantAnnotations = pAst.methods.head.body.get.ss.inner.inner.collect {
case (_, w: PWhile) => w.invs.toSeq.flatMap(_.annotations.map(_.values.inner.first.get.str))
}.flatten
assert(loopInvariantAnnotations == Seq("the invariant", " is always true", " termination"))
}
}
Loading