diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index 05d4c13a59..71e9c75789 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -6,3 +6,6 @@ # Scala Steward: Reformat with scalafmt 3.7.6 1b2091c13571348b534076f2183ced2cd2ff67a9 + +# Scala Steward: Reformat with scalafmt 3.8.3 +60dd752b3c25a2b578b9f0a967878904d6941982 diff --git a/.github/workflows/README.md b/.github/workflows/README.md index b3226f8407..d8d29ba024 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -16,15 +16,15 @@ - Triggered manually. - The workflow prepares a release and opens a `[release]` PR. - **Requirements**: - - A personal API token is required to authenticate the API call that opens the - PR. - - We use a token belonging to our machine user [@apalache-bot][]. apalache-bot + - A personal API token called `APALACHE_BOT_TOKEN` is required to authenticate the API + call that opens the PR. + - We use a token belonging to our machine user [@coffeeinprogress][]. coffeeinprogress creates the PR from their fork of the repo, and they have no permissions in this repo itself. - Secrets are configured [here][secret-config]. -[@apalache-bot]: https://github.com/apalache-bot -[secret-config]: https://github.com/informalsystems/apalache/settings/secrets/actions +[@@coffeeinprogress]: https://github.com/coffeeinprogress +[secret-config]: https://github.com/apalache-mc/apalache/settings/secrets/actions ## [./release.yml](./release.yml) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 43f314e500..5a95199b64 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -166,6 +166,8 @@ jobs: TEST_FILTER: ${{ matrix.smt-encoding == 'arrays' && 'array-encoding' || '' }} docker-tests: + # provisionally disable docker tests until we repair them + if: false runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml index ba77106216..fa189eb981 100644 --- a/.github/workflows/prepare-release.yml +++ b/.github/workflows/prepare-release.yml @@ -3,7 +3,7 @@ name: prepare-release on: workflow_dispatch: # Allows manually triggering release via - # https://github.com/informalsystems/apalache/actions?query=workflow%3A%22Prepare+Release%22 + # https://github.com/apalache-mc/apalache/actions?query=workflow%3A%22Prepare+Release%22 inputs: release_version: description: "Version (leave empty to increment patch version)" @@ -12,7 +12,7 @@ on: jobs: prepare-release: - if: github.repository_owner == 'informalsystems' + if: github.repository_owner == 'apalache-mc' env: RELEASE_VERSION: ${{ github.event.inputs.release_version }} # NOTE: We must not use the default GITHUB_TOKEN for auth here, diff --git a/.scalafmt.conf b/.scalafmt.conf index 11af66aa14..2f274e9290 100644 --- a/.scalafmt.conf +++ b/.scalafmt.conf @@ -1,4 +1,4 @@ -version = "3.8.0" +version = "3.8.3" runner.dialect = scala213 fileOverride { diff --git a/.unreleased/features/timeout-smt.md b/.unreleased/features/timeout-smt.md new file mode 100644 index 0000000000..dd27979cfb --- /dev/null +++ b/.unreleased/features/timeout-smt.md @@ -0,0 +1 @@ +Add `--timeout-smt` to limit SMT queries (#2936) diff --git a/.unreleased/features/translate-generate.md b/.unreleased/features/translate-generate.md new file mode 100644 index 0000000000..326f24450d --- /dev/null +++ b/.unreleased/features/translate-generate.md @@ -0,0 +1 @@ +Translate Quint's generate into `Apalache!Gen` (#2916) diff --git a/FUNDING.md b/FUNDING.md new file mode 100644 index 0000000000..7d6cffe670 --- /dev/null +++ b/FUNDING.md @@ -0,0 +1,19 @@ +## Apalache Funding + +We are grateful to the following organizations for financially supporting +the project Apalache for significant duration of time in the past: + +- [Informal Systems][]: 2020-2024 +- [Vienna Business Agency][]: 2021-2023 +- [Interchain Foundation][]: 2019-2023 +- [WWTF][] (Austria): Vienna Science and Technology Fund 2016-2020 +- [Inria Nancy][] and [LORIA][] (France): 2018-2019 +- [TU Wien][] (Austria): 2016-2020 + +[WWTF]: https://wwtf.at/index.php?lang=EN +[TU Wien]: https://www.tuwien.at/ +[Inria Nancy]: https://www.inria.fr/en/inria-centre-universite-lorraine +[LORIA]: https://loria.fr +[Interchain Foundation]: https://interchain.io/ +[Informal Systems]: https://informal.systems/ +[Vienna Business Agency]: https://viennabusinessagency.at/ diff --git a/docs/src/apalache/tuning.md b/docs/src/apalache/tuning.md index d3a60c5eeb..22aa77b99a 100644 --- a/docs/src/apalache/tuning.md +++ b/docs/src/apalache/tuning.md @@ -22,14 +22,6 @@ The following options are supported: `smt.randomSeed=` passes the random seed to `z3` (via `z3`'s parameters `sat.random_seed` and `smt.random_seed`). -## Timeouts - -`search.smt.timeout=` defines the timeout to the SMT solver in seconds. -The default value is `0`, which stands for the unbounded timeout. For instance, -the timeout is used in the following cases: checking if a transition is enabled, -checking an invariant, checking for deadlocks. If the solver times out, it -reports 'UNKNOWN', and the model checker reports a runtime error. - ## Invariant mode `search.invariant.mode=(before|after)` defines the moment when the invariant is diff --git a/docs/src/lang/apalache-operators.md b/docs/src/lang/apalache-operators.md index 7166c4fc8a..41d1a8c81e 100644 --- a/docs/src/lang/apalache-operators.md +++ b/docs/src/lang/apalache-operators.md @@ -142,7 +142,44 @@ IN The operators `ApaFoldSet` and `ApaFoldSeqLeft` are explained in more detail in a dedicated section [here](../apalache/principles/folds.md). ---------------------------------------------------------------------------- + +## Operator iteration +**Notation:** `Repeat(Op, N, x)` + +**LaTeX notation:** `Repeat(Op, N, x)` + +**Arguments:** Three arguments: An operator `Op`, an iteration counter `N` (a nonnegative constant integer expression), and an +initial value `x`. + +**Apalache type:** `((a, Int), Int, a) => a`, for some type `a`. + +**Effect:** For a given constant bound `N`, computes the value +`F(F(F(F(x,1), 2), ...), N)`. If `N=0` it evaluates to `x`. + +```tla +Repeat(Op, N, x) == + ApaFoldSeqLeft(Op, x, MkSeq(N, LAMBDA i:i)) +``` + +Apalache implements a more efficient encoding of this operator than the default one. + +**Determinism:** Deterministic. + +**Errors:** +If any argument is ill-typed, or `N` is not a nonnegative constant integer expression, Apalache reports an error. + +**Example in TLA+:** + +```tla +Op(a) == a + 1 +LET OpModified(a,i) == Op(i) +IN Repeat(OpModified, 0, 5) = 5 \* TRUE + +Op2(a,i) == a + i +Repeat(Op2, 0, 5) = 15 \* TRUE +``` +---------------------------------------------------------------------------- ## Convert a set of pairs to a function diff --git a/mod-infra/src/main/scala/at/forsyte/apalache/infra/passes/options.scala b/mod-infra/src/main/scala/at/forsyte/apalache/infra/passes/options.scala index 4d676aa3c4..201e26ae1a 100644 --- a/mod-infra/src/main/scala/at/forsyte/apalache/infra/passes/options.scala +++ b/mod-infra/src/main/scala/at/forsyte/apalache/infra/passes/options.scala @@ -162,6 +162,8 @@ object Config { * maximal number of Next steps * @param maxError * whether to stop on the first error or to produce up to a given number of counterexamples + * @param timeoutSmtSec + * the time limit on SMT queries in seconds * @param noDeadLocks * do not check for deadlocks * @param smtEncoding @@ -182,6 +184,7 @@ object Config { next: Option[String] = None, length: Option[Int] = Some(10), maxError: Option[Int] = Some(1), + timeoutSmtSec: Option[Int] = Some(0), noDeadlocks: Option[Boolean] = Some(false), smtEncoding: Option[SMTEncoding] = Some(SMTEncoding.OOPSLA19), temporalProps: Option[List[String]] = None, @@ -660,6 +663,7 @@ object OptionGroup extends LazyLogging { discardDisabled: Boolean, length: Int, maxError: Int, + timeoutSmtSec: Int, noDeadlocks: Boolean, smtEncoding: SMTEncoding, tuning: Map[String, String]) @@ -684,11 +688,13 @@ object OptionGroup extends LazyLogging { smtEncoding <- checker.smtEncoding.toTry("checker.smtEncoding") tuning <- checker.tuning.toTry("checker.tuning") maxError <- checker.maxError.toTry("checker.maxError").flatMap(validateMaxError) + timeoutSmtSec <- checker.timeoutSmtSec.toTry("checker.timeoutSmtSec") } yield Checker( algo = algo, discardDisabled = discardDisabled, length = length, maxError = maxError, + timeoutSmtSec = timeoutSmtSec, noDeadlocks = noDeadlocks, smtEncoding = smtEncoding, tuning = tuning, diff --git a/mod-tool/src/main/scala/at/forsyte/apalache/tla/tooling/opt/CheckCmd.scala b/mod-tool/src/main/scala/at/forsyte/apalache/tla/tooling/opt/CheckCmd.scala index ea957e1bce..20a5773436 100644 --- a/mod-tool/src/main/scala/at/forsyte/apalache/tla/tooling/opt/CheckCmd.scala +++ b/mod-tool/src/main/scala/at/forsyte/apalache/tla/tooling/opt/CheckCmd.scala @@ -69,6 +69,11 @@ class CheckCmd(name: String = "check", description: String = "Check a TLA+ speci opt[Boolean](name = "output-traces", description = "save an example trace for each symbolic run, default: false", default = false) + var timeoutSmtSec: Option[Int] = + opt[Option[Int]](name = "timeout-smt", + description = "limit the duration of a single SMT check query with `n` seconds, default: 0 (unlimited)", + default = None) + override def toConfig(): Try[Config.ApalacheConfig] = { val loadedTuningOptions = tuningOptionsFile.map(f => loadProperties(f)).getOrElse(Map()) val combinedTuningOptions = overrideProperties(loadedTuningOptions, tuningOptions.getOrElse("")) ++ Map( @@ -83,6 +88,7 @@ class CheckCmd(name: String = "check", description: String = "Check a TLA+ speci discardDisabled = discardDisabled, noDeadlocks = noDeadlocks, maxError = maxError, + timeoutSmtSec = timeoutSmtSec, view = view, ), typechecker = cfg.typechecker.copy( diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 5c0269f1ce..b858a56f58 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -12,7 +12,7 @@ object Dependencies { object Deps { // Versions - lazy val logbackVersion = "1.5.3" + lazy val logbackVersion = "1.5.6" lazy val clistVersion = "3.5.1" // Libraries @@ -21,7 +21,7 @@ object Dependencies { val commonsBeanutils = "commons-beanutils" % "commons-beanutils" % "1.9.4" // Apparently an untracked dependency of commonsConfiguration2 val commonsConfiguration2 = "org.apache.commons" % "commons-configuration2" % "2.9.0" - val commonsIo = "commons-io" % "commons-io" % "2.15.1" + val commonsIo = "commons-io" % "commons-io" % "2.16.1" val guice = "com.google.inject" % "guice" % "7.0.0" val kiama = "org.bitbucket.inkytonik.kiama" %% "kiama" % "2.5.1" val logbackClassic = "ch.qos.logback" % "logback-classic" % logbackVersion @@ -31,16 +31,16 @@ object Dependencies { val scalaParserCombinators = "org.scala-lang.modules" %% "scala-parser-combinators" % "2.3.0" val scalaCollectionContrib = "org.scala-lang.modules" %% "scala-collection-contrib" % "0.3.0" val scalaz = "org.scalaz" %% "scalaz-core" % "7.3.5" - val slf4j = "org.slf4j" % "slf4j-api" % "2.0.12" + val slf4j = "org.slf4j" % "slf4j-api" % "2.0.16" val shapeless = "com.chuusai" %% "shapeless" % "2.3.10" val tla2tools = "org.lamport" % "tla2tools" % "1.7.0-SNAPSHOT" val ujson = "com.lihaoyi" %% "ujson" % "3.2.0" val upickle = "com.lihaoyi" %% "upickle" % "3.2.0" - val z3 = "tools.aqua" % "z3-turnkey" % "4.12.5" + val z3 = "tools.aqua" % "z3-turnkey" % "4.12.6" val zio = "dev.zio" %% "zio" % zioVersion // Keep up to sync with version in plugins.sbt val zioGrpcCodgen = "com.thesamet.scalapb.zio-grpc" %% "zio-grpc-codegen" % "0.6.0-test3" % "provided" - val grpcNetty = "io.grpc" % "grpc-netty" % "1.62.2" + val grpcNetty = "io.grpc" % "grpc-netty" % "1.66.0" val scalapbRuntimGrpc = "com.thesamet.scalapb" %% "scalapb-runtime-grpc" % scalapb.compiler.Version.scalapbVersion // Ensures we have access to commonly used protocol buffers (e.g., google.protobuf.Struct) @@ -54,7 +54,7 @@ object Dependencies { // Libraries val junit = "junit" % "junit" % "4.13.2" % Test val scalacheck = "org.scalacheck" %% "scalacheck" % "1.17.0" % Test - val easymock = "org.easymock" % "easymock" % "5.2.0" % Test + val easymock = "org.easymock" % "easymock" % "5.4.0" % Test val scalaTestVersion = "3.2.15" val scalatest = "org.scalatest" %% "scalatest" % scalaTestVersion % Test diff --git a/project/build.properties b/project/build.properties index 04267b14af..ee4c672cd0 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.9.9 +sbt.version=1.10.1 diff --git a/project/plugins.sbt b/project/plugins.sbt index 9c743847ca..4a92db28e4 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -7,7 +7,7 @@ addSbtPlugin("se.marcuslonnberg" % "sbt-docker" % "1.11.0") // https://github.com/scoverage/sbt-scoverage addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.0.11") // https://github.com/sbt/sbt-buildinfo -addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.11.0") +addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.12.0") // https://github.com/sbt/sbt-native-packager addSbtPlugin("com.github.sbt" % "sbt-native-packager" % "1.10.4") // https://scalacenter.github.io/scalafix/docs/users/installation.html diff --git a/project/sbt-changeling/build.sbt b/project/sbt-changeling/build.sbt index 2d86b9cb52..2df02ef524 100644 --- a/project/sbt-changeling/build.sbt +++ b/project/sbt-changeling/build.sbt @@ -2,7 +2,7 @@ ThisBuild / version := "0.1.0-SNAPSHOT" ThisBuild / organization := "systems.informal" libraryDependencies ++= Seq( - "org.scala-sbt" % "sbt" % "1.9.9" + "org.scala-sbt" % "sbt" % "1.10.1" ) lazy val sbt_changeling = (project in file(".")) diff --git a/project/sbt-changeling/project/build.properties b/project/sbt-changeling/project/build.properties index 04267b14af..ee4c672cd0 100644 --- a/project/sbt-changeling/project/build.properties +++ b/project/sbt-changeling/project/build.properties @@ -1 +1 @@ -sbt.version=1.9.9 +sbt.version=1.10.1 diff --git a/src/tla/Apalache.tla b/src/tla/Apalache.tla index b8bb5cb1c5..ea5188fb9b 100644 --- a/src/tla/Apalache.tla +++ b/src/tla/Apalache.tla @@ -152,4 +152,17 @@ ApaFoldSeqLeft(__Op(_,_), __v, __seq) == THEN __v ELSE ApaFoldSeqLeft(__Op, __Op(__v, Head(__seq)), Tail(__seq)) +(** + * The repetition operator, used to consecutively apply an operator, starting from + * an initial value. + * + * @type: ((a, Int) => a, Int, a) => a; + *) +RECURSIVE Repeat(_,_,_) +Repeat(__F(_,_), __N, __x) == + \* This is the TLC implementation. Apalache does it differently. + IF __N <= 0 + THEN __x + ELSE __F(Repeat(__F, __N - 1, __x), __N) + =============================================================================== diff --git a/test/tla/Repeat.tla b/test/tla/Repeat.tla new file mode 100644 index 0000000000..93aa7adda9 --- /dev/null +++ b/test/tla/Repeat.tla @@ -0,0 +1,66 @@ +------------------------ MODULE Repeat ------------------------------------ +EXTENDS Apalache, Integers + +Inv1 == + LET Op(a, i) == a + 1 + IN Repeat(Op, 5, 0) = 5 + +Inv2 == + LET Op(a, i) == a + i + IN Repeat(Op, 5, 0) = 15 + +\* Cyclical Op: \E k: Op^k = Id +Inv3 == + LET Op(a,i) == (a + i) % 3 + IN LET + v == 1 + x0 == Repeat(Op, 0, v) + x3 == Repeat(Op, 3, v) + x6 == Repeat(Op, 6, v) + IN + /\ v = x0 + /\ x0 = x3 + /\ x3 = x6 + +\* Idempotent Op: Op^2 = Op +Inv4 == + LET + \* @type: (Set(Set(Int)), Int) => Set(Set(Int)); + Op(a, i) == {UNION a} + IN LET + v == {{1}, {2}, {3,4}} + x1 == Repeat(Op, 1, v) + x2 == Repeat(Op, 2, v) + x3 == Repeat(Op, 3, v) + IN + /\ v /= x1 + /\ x1 = x2 + /\ x2 = x3 + +\* Nilpotent Op: \E k: Op^k = 0 +Inv5 == + LET + \* @type: (Set(Int), Int) => Set(Int); + Op(a, i) == a \ { x \in a: \A y \in a: x <= y } + IN LET + v == {1,2,3} + x1 == Repeat(Op, 2, v) + x2 == Repeat(Op, 3, v) + x3 == Repeat(Op, 4, v) + IN + /\ x1 /= x2 + /\ x2 = x3 + /\ x3 = {} + + +Init == TRUE +Next == TRUE + +Inv == + /\ Inv1 + /\ Inv2 + /\ Inv3 + /\ Inv4 + /\ Inv5 + +=============================================================================== diff --git a/test/tla/RepeatBad.tla b/test/tla/RepeatBad.tla new file mode 100644 index 0000000000..3951d35d80 --- /dev/null +++ b/test/tla/RepeatBad.tla @@ -0,0 +1,11 @@ +------------------------ MODULE RepeatBad ------------------------------------ +EXTENDS Apalache, Integers + +Inv == + LET Op(a, i) == a + 1 + \* The 2nd argument to Repeat must be an integer literal + IN \E x \in {5} : Repeat(Op, x, 0) = 5 + +Init == TRUE +Next == TRUE +=============================================================================== diff --git a/test/tla/cli-integration-tests.md b/test/tla/cli-integration-tests.md index 6cec251917..cfa06d6d28 100644 --- a/test/tla/cli-integration-tests.md +++ b/test/tla/cli-integration-tests.md @@ -894,6 +894,24 @@ The outcome is: NoError EXITCODE: OK ``` +### check Repeat succeeds + +```sh +$ apalache-mc check --inv=Inv --length=0 Repeat.tla | sed 's/I@.*//' +... +The outcome is: NoError +... +EXITCODE: OK +``` + +### check RepeatBad fails + +```sh +$ apalache-mc check --inv=Inv --length=0 RepeatBad.tla | sed 's/I@.*//' +... +EXITCODE: ERROR (255) +``` + ### check Counter.tla errors (array-encoding) ```sh @@ -1752,6 +1770,18 @@ The outcome is: NoError EXITCODE: OK ``` +### simulate Paxos.tla with timeout succeeds + +While we cannot rely on an actual timeout happening or not, we can make sure that the option is properly parsed. + +```sh +$ apalache-mc simulate --timeout-smt=1 --length=10 --inv=Inv Paxos.tla | sed 's/I@.*//' +... +The outcome is: NoError +... +EXITCODE: OK +``` + ### simulate y2k with --output-traces succeeds ```sh @@ -3874,6 +3904,7 @@ checker { smt-encoding { type=oopsla-19 } + timeout-smt-sec=0 tuning { "search.outputTraces"="false" } diff --git a/tla-bmcmt/src/main/scala/at/forsyte/apalache/tla/bmcmt/Checker.scala b/tla-bmcmt/src/main/scala/at/forsyte/apalache/tla/bmcmt/Checker.scala index 1723856d11..8bef8a9b07 100644 --- a/tla-bmcmt/src/main/scala/at/forsyte/apalache/tla/bmcmt/Checker.scala +++ b/tla-bmcmt/src/main/scala/at/forsyte/apalache/tla/bmcmt/Checker.scala @@ -23,6 +23,8 @@ object Checker { Obj("checking_result" -> "Error", "counterexamples" -> counterexamples, "nerrors" -> nerrors) case Deadlock(counterexample) => Obj("checking_result" -> "Deadlock", "counterexamples" -> counterexample.toList) + case SmtTimeout(nTimeouts) => + Obj("checking_result" -> "SmtTimeout", "ntimeouts" -> nTimeouts) case other => Obj("checking_result" -> other.toString()) } @@ -60,6 +62,20 @@ object Checker { override val isOk: Boolean = true } + /** + * The SMT solver reported a timeout. It should still be possible to check other verification conditions. + * @param nTimeouts + * the number of timeouts, normally, just 1. + */ + case class SmtTimeout(nTimeouts: Int) extends CheckerResult { + override def toString: String = "SmtTimeout" + + /** + * Whether this result shall be reported as success (`true`) or error (`false`). + */ + override val isOk: Boolean = true + } + case class Interrupted() extends CheckerResult { override def toString: String = "Interrupted" diff --git a/tla-bmcmt/src/main/scala/at/forsyte/apalache/tla/bmcmt/SeqModelChecker.scala b/tla-bmcmt/src/main/scala/at/forsyte/apalache/tla/bmcmt/SeqModelChecker.scala index e7360504ff..da36921b93 100644 --- a/tla-bmcmt/src/main/scala/at/forsyte/apalache/tla/bmcmt/SeqModelChecker.scala +++ b/tla-bmcmt/src/main/scala/at/forsyte/apalache/tla/bmcmt/SeqModelChecker.scala @@ -94,7 +94,7 @@ class SeqModelChecker[ExecutorContextT]( */ private def outputExampleRun(): Unit = { logger.info("Constructing an example run") - trex.sat(params.smtTimeoutSec) match { + trex.sat(params.timeoutSmtSec) match { case Some(true) => listeners.foreach(_.onExample(checkerInput.rootModule, trex.decodedExecution(), searchState.nRunsLeft)) case Some(false) => @@ -128,8 +128,8 @@ class SeqModelChecker[ExecutorContextT]( if (!params.discardDisabled && params.checkForDeadlocks) { // We do this check only if all transitions are unconditionally enabled. // Otherwise, we should have found it already. - logger.info(s"Step %d: checking for deadlocks".format(trex.stepNo)) - trex.sat(params.smtTimeoutSec) match { + logger.info(f"Step ${trex.stepNo}: checking for deadlocks") + trex.sat(params.timeoutSmtSec) match { case Some(true) => () // OK case Some(false) => @@ -145,7 +145,8 @@ class SeqModelChecker[ExecutorContextT]( searchState.onResult(Deadlock(counterexample)) case None => - searchState.onResult(RuntimeError()) + logger.info(f"Step ${trex.stepNo}: checking for deadlocks => TIMEOUT") + searchState.onResult(SmtTimeout(1)) } } @@ -204,6 +205,9 @@ class SeqModelChecker[ExecutorContextT]( val transitionIndices = if (params.isRandomSimulation) Random.shuffle(transitions.indices.toList) else transitions.indices + // keep track of SMT timeouts + var nTimeouts = 0 + for (no <- transitionIndices) { var snapshot: Option[SnapshotT] = None if (params.discardDisabled) { @@ -221,7 +225,7 @@ class SeqModelChecker[ExecutorContextT]( val assumeSnapshot = trex.snapshot() // assume that the transition is fired and check, whether the constraints are satisfiable trex.assumeTransition(no) - trex.sat(params.smtTimeoutSec) match { + trex.sat(params.timeoutSmtSec) match { case Some(true) => logger.debug(s"Step ${trex.stepNo}: Transition #$no is enabled") // recover to the state before the transition was fired @@ -263,7 +267,8 @@ class SeqModelChecker[ExecutorContextT]( case None => if (params.isRandomSimulation) { - logger.info(s"Step ${trex.stepNo}: Transition #$no enabledness is UNKNOWN; assuming it is disabled") + logger.info(s"Step ${trex.stepNo}: Transition #$no => TIMEOUT. Transition ignored.") + nTimeouts += 1 } else { searchState.onResult(RuntimeError()) return (Set.empty, Set.empty) // UNKNOWN or timeout @@ -284,7 +289,13 @@ class SeqModelChecker[ExecutorContextT]( } if (trex.preparedTransitionNumbers.isEmpty) { - if (params.checkForDeadlocks) { + if (nTimeouts > 0 || !params.checkForDeadlocks) { + // Either (1) there were timeouts, and we cannot claim to have found a deadlock, + // or (2) the user does not care about deadlocks + val msg = "All executions are shorter than the provided bound." + logger.warn(msg) + searchState.onResult(ExecutionsTooShort()) + } else { val counterexample = if (trex.sat(0).contains(true)) { val cx = Counterexample(checkerInput.rootModule, trex.decodedExecution(), tla.bool(true)) notifyOnError(cx, searchState.nFoundErrors) @@ -295,10 +306,6 @@ class SeqModelChecker[ExecutorContextT]( None } searchState.onResult(Deadlock(counterexample)) - } else { - val msg = "All executions are shorter than the provided bound." - logger.warn(msg) - searchState.onResult(ExecutionsTooShort()) } (Set.empty, Set.empty) } else { @@ -355,7 +362,7 @@ class SeqModelChecker[ExecutorContextT]( // check the invariant trex.assertState(notInv) - trex.sat(params.smtTimeoutSec) match { + trex.sat(params.timeoutSmtSec) match { case Some(true) => val counterexample = Counterexample(checkerInput.rootModule, trex.decodedExecution(), notInv) searchState.onResult(Error(1, Seq(counterexample))) @@ -369,8 +376,10 @@ class SeqModelChecker[ExecutorContextT]( logger.info(f"State ${stateNo}: ${kind} invariant ${invNo} holds.") case None => - // UNKNOWN or timeout - searchState.onResult(RuntimeError()) + // UNKNOWN or timeout. Assume that the invariant holds true, so we can continue. + invariantHolds = true + logger.info(f"State ${stateNo}: ${kind} invariant ${invNo} => TIMEOUT. Assuming it holds true.") + searchState.onResult(SmtTimeout(1)) } // rollback the context @@ -401,7 +410,7 @@ class SeqModelChecker[ExecutorContextT]( val traceInvApp = applyTraceInv(notInv) trex.assertState(traceInvApp) - trex.sat(params.smtTimeoutSec) match { + trex.sat(params.timeoutSmtSec) match { case Some(true) => val counterexample = Counterexample(checkerInput.rootModule, trex.decodedExecution(), traceInvApp) searchState.onResult(Error(1, Seq(counterexample))) @@ -415,7 +424,10 @@ class SeqModelChecker[ExecutorContextT]( invariantHolds = true case None => - searchState.onResult(RuntimeError()) + // Timeout. Assume that the invariant holds true, so we can continue. + invariantHolds = true + logger.info(f"State ${stateNo}: trace invariant ${invNo} => TIMEOUT. Assuming it holds true.") + searchState.onResult(SmtTimeout(1)) } // rollback the context diff --git a/tla-bmcmt/src/main/scala/at/forsyte/apalache/tla/bmcmt/SymbStateRewriterImpl.scala b/tla-bmcmt/src/main/scala/at/forsyte/apalache/tla/bmcmt/SymbStateRewriterImpl.scala index 11539bc03b..be9b21862c 100644 --- a/tla-bmcmt/src/main/scala/at/forsyte/apalache/tla/bmcmt/SymbStateRewriterImpl.scala +++ b/tla-bmcmt/src/main/scala/at/forsyte/apalache/tla/bmcmt/SymbStateRewriterImpl.scala @@ -303,11 +303,13 @@ class SymbStateRewriterImpl( -> List(new LabelRule(this)), key(OperEx(ApalacheOper.gen, tla.int(2))) -> List(new GenRule(this)), - // folds and MkSeq + // folds, repeat and MkSeq key(OperEx(ApalacheOper.foldSet, tla.name("A"), tla.name("v"), tla.name("S"))) -> List(new FoldSetRule(this, renaming)), key(OperEx(ApalacheOper.foldSeq, tla.name("A"), tla.name("v"), tla.name("s"))) -> List(new FoldSeqRule(this, renaming)), + key(OperEx(ApalacheOper.repeat, tla.name("Op"), tla.int(10), tla.name("s"))) + -> List(new RepeatRule(this, renaming)), key(OperEx(ApalacheOper.mkSeq, tla.int(10), tla.name("A"))) -> List(new MkSeqRule(this, renaming)), ) diff --git a/tla-bmcmt/src/main/scala/at/forsyte/apalache/tla/bmcmt/passes/BoundedCheckerPassImpl.scala b/tla-bmcmt/src/main/scala/at/forsyte/apalache/tla/bmcmt/passes/BoundedCheckerPassImpl.scala index 39cebd8a91..aa2b3bb34a 100644 --- a/tla-bmcmt/src/main/scala/at/forsyte/apalache/tla/bmcmt/passes/BoundedCheckerPassImpl.scala +++ b/tla-bmcmt/src/main/scala/at/forsyte/apalache/tla/bmcmt/passes/BoundedCheckerPassImpl.scala @@ -70,6 +70,7 @@ class BoundedCheckerPassImpl @Inject() ( params.discardDisabled = options.checker.discardDisabled params.checkForDeadlocks = !options.checker.noDeadlocks params.nMaxErrors = options.checker.maxError + params.timeoutSmtSec = options.checker.timeoutSmtSec params.smtEncoding = smtEncoding val smtProfile = options.common.smtprof diff --git a/tla-bmcmt/src/main/scala/at/forsyte/apalache/tla/bmcmt/rules/RepeatRule.scala b/tla-bmcmt/src/main/scala/at/forsyte/apalache/tla/bmcmt/rules/RepeatRule.scala new file mode 100644 index 0000000000..7bec841e0a --- /dev/null +++ b/tla-bmcmt/src/main/scala/at/forsyte/apalache/tla/bmcmt/rules/RepeatRule.scala @@ -0,0 +1,83 @@ +package at.forsyte.apalache.tla.bmcmt.rules + +import at.forsyte.apalache.tla.bmcmt._ +import at.forsyte.apalache.tla.lir._ +import at.forsyte.apalache.tla.lir.oper.ApalacheOper +import at.forsyte.apalache.tla.lir.transformations.impl.IdleTracker +import at.forsyte.apalache.tla.lir.transformations.standard.IncrementalRenaming +import at.forsyte.apalache.tla.lir.values.TlaInt +import at.forsyte.apalache.tla.pp.Inliner +import at.forsyte.apalache.tla.types.tla + +/** + * Rewriting rule for Repeat. This rule is similar to [[FoldSeqRule]]. + * + * This rule is more efficient than the fold- rules, because it does not require an underlying data structure (Seq or + * Set). In particular, folding over 1..N, despite the overapproximation being tight by construction, still introduces + * O(N*N) constraints, since the SMT solver must assert element uniqueness (i /= j for all i,j \in 1..N). OTOH, + * RepeatRule consumes 1..N in the canonical order natively as a 1.to(N) in Scala. + * + * @author + * Jure Kukovec + */ +class RepeatRule(rewriter: SymbStateRewriter, renaming: IncrementalRenaming) extends RewritingRule { + + override def isApplicable(symbState: SymbState): Boolean = { + symbState.ex match { + case OperEx(ApalacheOper.repeat, LetInEx(NameEx(appName), TlaOperDecl(operName, params, _)), _, _) => + appName == operName && params.size == 2 + case _ => false + } + } + + override def apply(state: SymbState): SymbState = state.ex match { + // assume isApplicable + case ex @ OperEx(ApalacheOper.repeat, LetInEx(NameEx(_), opDecl), boundEx, baseEx) => + boundEx match { + case ValEx(TlaInt(n)) if n.isValidInt && n.toInt >= 0 => + // rewrite baseEx to its final cell form + val baseState = rewriter.rewriteUntilDone(state.setRex(baseEx)) + + // We need the type signature. The signature of Repeat is + // \A a : ((a,Int) => a, Int, a) => a + // so the operator type must be (a,Int) => a + val a = TlaType1.fromTypeTag(baseEx.typeTag) + val opT = OperT1(Seq(a, IntT1), a) + // sanity check + TlaType1.fromTypeTag(opDecl.typeTag) match { + case `opT` => // all good + case badType => + val msg = s"FoldSet argument ${opDecl.name} should have the tag $opT, found $badType." + throw new TypingException(msg, opDecl.ID) + } + + // expressions are transient, we don't need tracking + val inliner = new Inliner(new IdleTracker, renaming) + // We can make the scope directly, since InlinePass already ensures all is well. + val seededScope: Inliner.Scope = Map(opDecl.name -> opDecl) + + // To implement the Repeat rule, we generate a sequence of cells. + // At each step, we perform one application of the operator + // defined by `opDecl` to the previous partial result, + // and pass the iteration index as the second parameter + (1 to n.toInt).foldLeft(baseState) { case (partialState, i) => + // partialState currently holds the cell representing the previous application step + val oldResultCell = partialState.asCell + + // First, we inline the operator application, with cell names as parameters + val appEx = tla.appOp( + tla.name(opDecl.name, opT), + oldResultCell.toBuilder, + tla.int(i), + ) + + val inlinedEx = inliner.transform(seededScope)(appEx) + rewriter.rewriteUntilDone(partialState.setRex(inlinedEx)) + } + case _ => + throw new RewriterException("Apalache!Repeat expects a constant positive integer. Found: " + boundEx, ex) + } + case _ => + throw new RewriterException("%s is not applicable".format(getClass.getSimpleName), state.ex) + } +} diff --git a/tla-bmcmt/src/main/scala/at/forsyte/apalache/tla/bmcmt/search/ModelCheckerParams.scala b/tla-bmcmt/src/main/scala/at/forsyte/apalache/tla/bmcmt/search/ModelCheckerParams.scala index 2c7a052aad..8847475fb0 100644 --- a/tla-bmcmt/src/main/scala/at/forsyte/apalache/tla/bmcmt/search/ModelCheckerParams.scala +++ b/tla-bmcmt/src/main/scala/at/forsyte/apalache/tla/bmcmt/search/ModelCheckerParams.scala @@ -68,6 +68,11 @@ class ModelCheckerParams( */ var nMaxErrors: Int = 1 + /** + * The limit on a single SMT query. The default value is 0 (unlimited). + */ + var timeoutSmtSec: Int = 0 + /** * A timeout upon which a transition is split in its own group. This is the minimal timeout. The actual timeout is * updated at every step using `search.split.timeout.factor`. In our experiments, small timeouts lead to explosion of @@ -96,9 +101,6 @@ class ModelCheckerParams( */ def idleTimeoutMs: Long = idleTimeoutSec * 1000 - val smtTimeoutSec: Int = - tuningOptions.getOrElse("search.smt.timeout", "0").toInt - /** * The SMT encoding to be used. */ diff --git a/tla-bmcmt/src/main/scala/at/forsyte/apalache/tla/bmcmt/search/SearchState.scala b/tla-bmcmt/src/main/scala/at/forsyte/apalache/tla/bmcmt/search/SearchState.scala index 0981dbf004..3a8497250d 100644 --- a/tla-bmcmt/src/main/scala/at/forsyte/apalache/tla/bmcmt/search/SearchState.scala +++ b/tla-bmcmt/src/main/scala/at/forsyte/apalache/tla/bmcmt/search/SearchState.scala @@ -18,6 +18,7 @@ class SearchState(params: ModelCheckerParams) { private var _result: CheckerResult = NoError() private var _nFoundErrors: Int = 0 + private var _nTimeouts: Int = 0 private val _counterexamples: ListBuffer[Counterexample] = ListBuffer.empty private var _nRunsLeft: Int = if (params.isRandomSimulation) params.nSimulationRuns else 1 @@ -30,6 +31,14 @@ class SearchState(params: ModelCheckerParams) { */ def nFoundErrors: Int = _nFoundErrors + /** + * Get the number of SMT timeouts that occurred in the search. + * + * @return + * number of timeouts + */ + def nTimeouts: Int = _nTimeouts + /** * Get the number of simulation runs to explore. * @@ -49,6 +58,8 @@ class SearchState(params: ModelCheckerParams) { case NoError() => if (_nFoundErrors > 0) { Error(_nFoundErrors, _counterexamples.toList) + } else if (_nTimeouts > 0) { + SmtTimeout(_nTimeouts) } else { NoError() } @@ -98,6 +109,11 @@ class SearchState(params: ModelCheckerParams) { _result = Deadlock(counterexample) } + case SmtTimeout(nTimeouts) => + // we still continue the search, but report incompleteness in [[this.finalResult]]. + _nTimeouts += nTimeouts + _result = NoError() + case _ => _result = result } diff --git a/tla-bmcmt/src/main/scala/at/forsyte/apalache/tla/bmcmt/util/TlaExUtil.scala b/tla-bmcmt/src/main/scala/at/forsyte/apalache/tla/bmcmt/util/TlaExUtil.scala index 4010c2c726..68662f3919 100644 --- a/tla-bmcmt/src/main/scala/at/forsyte/apalache/tla/bmcmt/util/TlaExUtil.scala +++ b/tla-bmcmt/src/main/scala/at/forsyte/apalache/tla/bmcmt/util/TlaExUtil.scala @@ -32,6 +32,11 @@ object TlaExUtil { findRec(localBody) baseExAndCollectionEx.foreach(findRec) + // ignore the names in the auxiliary let-in definition + case OperEx(ApalacheOper.repeat, LetInEx(_, TlaOperDecl(_, _, localBody)), boundAndBaseEx @ _*) => + findRec(localBody) + boundAndBaseEx.foreach(findRec) + // ignore the names in the auxiliary let-in definition case OperEx(ApalacheOper.mkSeq, len, LetInEx(_, TlaOperDecl(_, _, localBody))) => findRec(localBody) diff --git a/tla-bmcmt/src/test/scala/at/forsyte/apalache/tla/bmcmt/TestRewriterWithArrays.scala b/tla-bmcmt/src/test/scala/at/forsyte/apalache/tla/bmcmt/TestRewriterWithArrays.scala index 1a79c4efd5..f146e7cbb6 100644 --- a/tla-bmcmt/src/test/scala/at/forsyte/apalache/tla/bmcmt/TestRewriterWithArrays.scala +++ b/tla-bmcmt/src/test/scala/at/forsyte/apalache/tla/bmcmt/TestRewriterWithArrays.scala @@ -18,7 +18,7 @@ class TestRewriterWithArrays with TestSymbStateRewriterPowerset with TestSymbStateRewriterRecord with TestSymbStateRewriterSequence with TestSymbStateRewriterSet with TestSymbStateRewriterStr with TestSymbStateRewriterTuple with TestPropositionalOracle with TestSparseOracle with TestUninterpretedConstOracle - with TestSymbStateRewriterApalache with TestSymbStateRewriterMkSeq { + with TestSymbStateRewriterApalache with TestSymbStateRewriterMkSeq with TestSymbStateRewriterRepeat { override protected def withFixture(test: OneArgTest): Outcome = { solverContext = new PreproSolverContext(new Z3SolverContext(SolverConfig.default.copy(debug = true, smtEncoding = SMTEncoding.Arrays))) diff --git a/tla-bmcmt/src/test/scala/at/forsyte/apalache/tla/bmcmt/TestRewriterWithFunArrays.scala b/tla-bmcmt/src/test/scala/at/forsyte/apalache/tla/bmcmt/TestRewriterWithFunArrays.scala index a7647d39c6..43b6cc0a18 100644 --- a/tla-bmcmt/src/test/scala/at/forsyte/apalache/tla/bmcmt/TestRewriterWithFunArrays.scala +++ b/tla-bmcmt/src/test/scala/at/forsyte/apalache/tla/bmcmt/TestRewriterWithFunArrays.scala @@ -18,7 +18,7 @@ class TestRewriterWithFunArrays with TestSymbStateRewriterPowerset with TestSymbStateRewriterRecord with TestSymbStateRewriterSequence with TestSymbStateRewriterSet with TestSymbStateRewriterStr with TestSymbStateRewriterTuple with TestPropositionalOracle with TestSparseOracle with TestUninterpretedConstOracle - with TestSymbStateRewriterApalache with TestSymbStateRewriterMkSeq { + with TestSymbStateRewriterApalache with TestSymbStateRewriterMkSeq with TestSymbStateRewriterRepeat { override protected def withFixture(test: OneArgTest): Outcome = { solverContext = new PreproSolverContext(new Z3SolverContext(SolverConfig.default.copy(debug = true, smtEncoding = SMTEncoding.FunArrays))) diff --git a/tla-bmcmt/src/test/scala/at/forsyte/apalache/tla/bmcmt/TestRewriterWithOOPSLA19.scala b/tla-bmcmt/src/test/scala/at/forsyte/apalache/tla/bmcmt/TestRewriterWithOOPSLA19.scala index 468aabb431..b455b76665 100644 --- a/tla-bmcmt/src/test/scala/at/forsyte/apalache/tla/bmcmt/TestRewriterWithOOPSLA19.scala +++ b/tla-bmcmt/src/test/scala/at/forsyte/apalache/tla/bmcmt/TestRewriterWithOOPSLA19.scala @@ -19,7 +19,7 @@ class TestRewriterWithOOPSLA19 with TestSymbStateRewriterVariant with TestSymbStateRewriterSequence with TestSymbStateRewriterSet with TestSymbStateRewriterStr with TestSymbStateRewriterTuple with TestPropositionalOracle with TestSparseOracle with TestUninterpretedConstOracle with TestSymbStateRewriterApalache with TestSymbStateRewriterMkSeq - with TestDefaultValueFactory { + with TestDefaultValueFactory with TestSymbStateRewriterRepeat { override protected def withFixture(test: OneArgTest): Outcome = { solverContext = new PreproSolverContext(new Z3SolverContext(SolverConfig.default.copy(debug = true, smtEncoding = SMTEncoding.OOPSLA19))) diff --git a/tla-bmcmt/src/test/scala/at/forsyte/apalache/tla/bmcmt/TestSymbStateRewriterRepeat.scala b/tla-bmcmt/src/test/scala/at/forsyte/apalache/tla/bmcmt/TestSymbStateRewriterRepeat.scala new file mode 100644 index 0000000000..aea018c7dd --- /dev/null +++ b/tla-bmcmt/src/test/scala/at/forsyte/apalache/tla/bmcmt/TestSymbStateRewriterRepeat.scala @@ -0,0 +1,45 @@ +package at.forsyte.apalache.tla.bmcmt + +import at.forsyte.apalache.infra.passes.options.SMTEncoding +import at.forsyte.apalache.tla.lir._ +import at.forsyte.apalache.tla.types.tla + +trait TestSymbStateRewriterRepeat extends RewriterBase { + test("""Repeat(LET Op(a,i) == a + 1 IN Op, 5, 0) = 5""") { rewriterType: SMTEncoding => + // Op(x, i) == x + 1 + val opT = OperT1(Seq(IntT1, IntT1), IntT1) + + val plusOneEx = tla.plus(tla.name("x", IntT1), tla.int(1)) + val plusOneOper = tla.decl("Op", plusOneEx, tla.param("x", IntT1), tla.param("i", IntT1)) + val letEx = tla.letIn(tla.name(plusOneOper.name, opT), plusOneOper) + val repeatEx = tla.repeat(letEx, 5, tla.int(0)) + + val rewriter = create(rewriterType) + var state = new SymbState(repeatEx, arena, Binding()) + state = rewriter.rewriteUntilDone(state) + val asCell = state.asCell + + // compare the value + val eqn = tla.eql(asCell.toBuilder, tla.int(5)) + assertTlaExAndRestore(rewriter, state.setRex(eqn)) + } + + test("""Repeat(LET Op(a,i) == a + i IN Op, 5, 0) = 15""") { rewriterType: SMTEncoding => + // Op(x, i) == x + i + val opT = OperT1(Seq(IntT1, IntT1), IntT1) + + val plusiEx = tla.plus(tla.name("x", IntT1), tla.name("i", IntT1)) + val plusiOper = tla.decl("Op", plusiEx, tla.param("x", IntT1), tla.param("i", IntT1)) + val letEx = tla.letIn(tla.name(plusiOper.name, opT), plusiOper) + val repeatEx = tla.repeat(letEx, 5, tla.int(0)) + + val rewriter = create(rewriterType) + var state = new SymbState(repeatEx, arena, Binding()) + state = rewriter.rewriteUntilDone(state) + val asCell = state.asCell + + // compare the value + val eqn = tla.eql(asCell.toBuilder, tla.int(15)) + assertTlaExAndRestore(rewriter, state.setRex(eqn)) + } +} diff --git a/tla-io/src/main/scala/at/forsyte/apalache/io/json/BuilderCallByName.scala b/tla-io/src/main/scala/at/forsyte/apalache/io/json/BuilderCallByName.scala index 94a2443f74..600d10911c 100644 --- a/tla-io/src/main/scala/at/forsyte/apalache/io/json/BuilderCallByName.scala +++ b/tla-io/src/main/scala/at/forsyte/apalache/io/json/BuilderCallByName.scala @@ -103,6 +103,7 @@ object BuilderCallByName { ApalacheOper.mkSeq.name -> ApalacheOper.mkSeq, ApalacheOper.foldSet.name -> ApalacheOper.foldSet, ApalacheOper.foldSeq.name -> ApalacheOper.foldSeq, + ApalacheOper.repeat.name -> ApalacheOper.repeat, ApalacheInternalOper.selectInSet.name -> ApalacheInternalOper.selectInSet, ApalacheInternalOper.selectInFun.name -> ApalacheInternalOper.selectInFun, ApalacheInternalOper.storeInSet.name -> ApalacheInternalOper.storeInSet, @@ -375,6 +376,15 @@ object BuilderCallByName { case ApalacheOper.foldSeq => val Seq(f, v, s) = args tla.foldSeq(f, v, s) + case ApalacheOper.repeat => + val Seq(f, n, x) = args + val nEx: TlaEx = n + nEx match { + case ValEx(TlaInt(n)) => + tla.repeat(f, n, x) + // should never happen, for case-completeness + case _ => throw new JsonDeserializationError(s"${oper.name} requires an integer argument.") + } case ApalacheInternalOper.selectInSet => val Seq(x, s) = args tla.selectInSet(x, s) diff --git a/tla-io/src/main/scala/at/forsyte/apalache/io/quint/Quint.scala b/tla-io/src/main/scala/at/forsyte/apalache/io/quint/Quint.scala index fc16c54ec2..5c965f921f 100644 --- a/tla-io/src/main/scala/at/forsyte/apalache/io/quint/Quint.scala +++ b/tla-io/src/main/scala/at/forsyte/apalache/io/quint/Quint.scala @@ -655,9 +655,13 @@ class Quint(quintOutput: QuintOutput) { // Convert Quint's nondet variable binding // - // val nondet name = oneOf(domain); scope + // nondet name = oneOf(domain); scope // ~~> // \E name \in domain: scope + // + // nondet name = generate(sz, typeSet); scope + // ~~> + // \E name \in { Apalache!Gen(sz) }: scope private val nondetBinding: (QuintDef.QuintOpDef, QuintEx) => NullaryOpReader[TBuilderInstruction] = { case (QuintDef.QuintOpDef(_, name, "nondet", QuintApp(id, "oneOf", Seq(domain))), scope) => val elemType = typeConv.convert(types(id).typ) @@ -666,6 +670,19 @@ class Quint(quintOutput: QuintOutput) { tlaDomain <- tlaExpression(domain) tlaScope <- tlaExpression(scope) } yield tla.exists(tlaName, tlaDomain, tlaScope) + + case (QuintDef.QuintOpDef(_, name, "nondet", QuintApp(id, "generate", Seq(bound, _))), scope) => + val elemType = typeConv.convert(types(id).typ) + val boundIntConst = intFromExpr(bound) + if (boundIntConst.isEmpty) { + throw new QuintIRParseError(s"nondet $name = generate($bound) ... expects an integer constant") + } + val genExpr = tla.enumSet(tla.gen(boundIntConst.get, elemType)) + val tlaName = tla.name(name, elemType) + for { + tlaScope <- tlaExpression(scope) + } yield tla.exists(tlaName, genExpr, tlaScope) + case invalidValue => throw new QuintIRParseError(s"nondet keyword used to bind invalid value ${invalidValue}") } @@ -804,4 +821,14 @@ class Quint(quintOutput: QuintOutput) { .reverse // Return the declarations to their original order } } yield TlaModule(module.name, declarations) + + private def intFromExpr(expr: QuintEx): Option[BigInt] = { + expr match { + case QuintInt(_, value) => + Some(value) + + case _ => + None + } + } } diff --git a/tla-io/src/main/scala/at/forsyte/apalache/tla/imp/StandardLibrary.scala b/tla-io/src/main/scala/at/forsyte/apalache/tla/imp/StandardLibrary.scala index 380e43075d..2c601465a4 100644 --- a/tla-io/src/main/scala/at/forsyte/apalache/tla/imp/StandardLibrary.scala +++ b/tla-io/src/main/scala/at/forsyte/apalache/tla/imp/StandardLibrary.scala @@ -67,6 +67,7 @@ object StandardLibrary { ("Apalache", "ApaFoldSet") -> ApalacheOper.foldSet, ("__apalache_folds", "__ApalacheFoldSet") -> ApalacheOper.foldSet, ("Apalache", "ApaFoldSeqLeft") -> ApalacheOper.foldSeq, + ("Apalache", "Repeat") -> ApalacheOper.repeat, // Variants ("Variants", "Variant") -> VariantOper.variant, ("Variants", "VariantFilter") -> VariantOper.variantFilter, diff --git a/tla-io/src/test/scala/at/forsyte/apalache/io/quint/TestQuintEx.scala b/tla-io/src/test/scala/at/forsyte/apalache/io/quint/TestQuintEx.scala index 06dfd20177..ee84a40fb5 100644 --- a/tla-io/src/test/scala/at/forsyte/apalache/io/quint/TestQuintEx.scala +++ b/tla-io/src/test/scala/at/forsyte/apalache/io/quint/TestQuintEx.scala @@ -172,7 +172,15 @@ class TestQuintEx extends AnyFunSuite { val chooseSomeFromIntSet = app("chooseSome", intSet)(QuintIntT()) val oneOfSet = app("oneOf", intSet)(QuintIntT()) val nondetBinding = - e(QuintLet(uid, d(QuintDef.QuintOpDef(uid, "n", "nondet", oneOfSet), QuintIntT()), nIsGreaterThan0), QuintIntT()) + e(QuintLet(uid, d(QuintDef.QuintOpDef(uid, "n", "nondet", oneOfSet), QuintIntT()), nIsGreaterThan0), QuintBoolT()) + val generateSet = app("generate", _42, app("Set", _42)(QuintSetT(QuintIntT())))(QuintSetT(QuintIntT())) + val nondetGenerateId = uid + val appGenSet = + app("eq", e(QuintName(uid, "S"), QuintSetT(QuintIntT())), app("Set")(QuintSetT(QuintIntT())))(QuintBoolT()) + val nondetGenerate = + e(QuintLet(uid, + d(QuintDef.QuintOpDef(nondetGenerateId, "S", "nondet", generateSet), + QuintOperT(Seq(), QuintSetT(QuintIntT()))), appGenSet), QuintBoolT()) // Requires ID registered with type val selectGreaterThanZero = app("select", intList, intIsGreaterThanZero)(QuintSeqT(QuintIntT())) val addOne = app("iadd", name, _1)(QuintIntT()) @@ -710,10 +718,14 @@ class TestQuintEx extends AnyFunSuite { assert(convert(Q.app("put", Q.intMap, Q._3, Q._42)(intMapT)) == expected) } - test("can convert nondet bindings") { + test("can convert nondet...oneOf") { assert(convert(Q.nondetBinding) == "∃n ∈ {1, 2, 3}: (n > 0)") } + test("can convert nondet...generate") { + assert(convert(Q.nondetGenerate) == "∃S ∈ {Apalache!Gen(42)}: (S = {})") + } + test("can convert let binding with reference to name in scope") { assert(convert(Q.letNbe42inNisGreaterThan0) == "LET n ≜ 42 IN n() > 0") } diff --git a/tla-typechecker/src/main/scala/at/forsyte/apalache/tla/typecheck/etc/ToEtcExpr.scala b/tla-typechecker/src/main/scala/at/forsyte/apalache/tla/typecheck/etc/ToEtcExpr.scala index 50765b5387..780320bf44 100644 --- a/tla-typechecker/src/main/scala/at/forsyte/apalache/tla/typecheck/etc/ToEtcExpr.scala +++ b/tla-typechecker/src/main/scala/at/forsyte/apalache/tla/typecheck/etc/ToEtcExpr.scala @@ -810,6 +810,12 @@ class ToEtcExpr( mkExRefApp(opsig, Seq(v, variantEx, defaultEx)) // ******************************************** Apalache ************************************************** + case OperEx(ApalacheOper.repeat, op, bound, base) => + val a = varPool.fresh + // ((a, Int) => a, Int, a) => a + val opsig = OperT1(Seq(OperT1(Seq(a, IntT1), a), IntT1, a), a) + mkExRefApp(opsig, Seq(op, bound, base)) + case OperEx(ApalacheOper.mkSeq, len, ctor) => val a = varPool.fresh // (Int, (Int => a)) => Seq(a) diff --git a/tlair/src/main/scala/at/forsyte/apalache/tla/lir/oper/ApalacheOper.scala b/tlair/src/main/scala/at/forsyte/apalache/tla/lir/oper/ApalacheOper.scala index bd291c2241..f17344c907 100644 --- a/tlair/src/main/scala/at/forsyte/apalache/tla/lir/oper/ApalacheOper.scala +++ b/tlair/src/main/scala/at/forsyte/apalache/tla/lir/oper/ApalacheOper.scala @@ -167,6 +167,19 @@ object ApalacheOper { override val precedence: (Int, Int) = (100, 100) } + /** + * Repeated applicaiton of an operator. + * + * The type signature is: `\forall T: Repeat: ((T, Int) => T, Int, T) => T` + */ + object repeat extends ApalacheOper { + override def name: String = "Apalache!Repeat" + + override def arity: OperArity = FixedArity(3) + + override val precedence: (Int, Int) = (100, 100) + } + /** *

The operator SetAsFun converts a set of pairs `R` to a function `F`. The function `F` could be expressed as * follows:

diff --git a/tlair/src/main/scala/at/forsyte/apalache/tla/lir/transformations/standard/KeraLanguagePred.scala b/tlair/src/main/scala/at/forsyte/apalache/tla/lir/transformations/standard/KeraLanguagePred.scala index 3a1cd4b1fd..7ca7451a75 100644 --- a/tlair/src/main/scala/at/forsyte/apalache/tla/lir/transformations/standard/KeraLanguagePred.scala +++ b/tlair/src/main/scala/at/forsyte/apalache/tla/lir/transformations/standard/KeraLanguagePred.scala @@ -59,6 +59,11 @@ class KeraLanguagePred extends ContextualLanguagePred { isOkInContext(letDefs, opName) .and(isOkInContext(letDefs, len)) + case OperEx(ApalacheOper.repeat, opName, bound, base) => + isOkInContext(letDefs, opName) + .and(isOkInContext(letDefs, bound)) + .and(isOkInContext(letDefs, base)) + case OperEx(oper, args @ _*) if oper == TlaSetOper.map || oper == TlaFunOper.funDef || oper == TlaFunOper.recFunDef => val evenArgs = args.zipWithIndex.filter { p => p._2 % 2 == 0 }.map { diff --git a/tlair/src/main/scala/at/forsyte/apalache/tla/typecomp/signatures/ApalacheOperSignatures.scala b/tlair/src/main/scala/at/forsyte/apalache/tla/typecomp/signatures/ApalacheOperSignatures.scala index 764794091f..e7a7a8b025 100644 --- a/tlair/src/main/scala/at/forsyte/apalache/tla/typecomp/signatures/ApalacheOperSignatures.scala +++ b/tlair/src/main/scala/at/forsyte/apalache/tla/typecomp/signatures/ApalacheOperSignatures.scala @@ -46,6 +46,12 @@ object ApalacheOperSignatures { // (Int, Int => t) => Seq(t) val mkSeqSig = signatureMapEntry(mkSeq, { case Seq(IntT1, OperT1(Seq(IntT1), t)) => SeqT1(t) }) + // ((t, Int) => t, Int, t) => t + val repeatSig = signatureMapEntry(repeat, + { + case Seq(OperT1(Seq(IntT1, t), t1), IntT1, t2) if compatible(t, t1) && compatible(t, t2) => t + }) + // ((a,b) => a, a, Set(b)) => a val foldSetSig = signatureMapEntry(foldSet, { @@ -72,6 +78,7 @@ object ApalacheOperSignatures { expandSig, constCardSig, mkSeqSig, + repeatSig, foldSetSig, foldSeqSig, setAsFunSig, diff --git a/tlair/src/main/scala/at/forsyte/apalache/tla/typecomp/subbuilder/ApalacheBuilder.scala b/tlair/src/main/scala/at/forsyte/apalache/tla/typecomp/subbuilder/ApalacheBuilder.scala index b77bd92ece..086eec7375 100644 --- a/tlair/src/main/scala/at/forsyte/apalache/tla/typecomp/subbuilder/ApalacheBuilder.scala +++ b/tlair/src/main/scala/at/forsyte/apalache/tla/typecomp/subbuilder/ApalacheBuilder.scala @@ -36,6 +36,16 @@ trait ApalacheBuilder { */ def gen(n: BigInt, t: TlaType1): TBuilderInstruction = unsafeBuilder.gen(n, t).point[TBuilderInternalState] + /** + * {{{Repeat(F, N, x): t}}} + * @param n + * must be a nonnegative constant expression + * @param F + * must be an expression of the shape {{{LET Op(i) == ... IN Op}}} + */ + def repeat(F: TBuilderInstruction, n: BigInt, x: TBuilderInstruction): TBuilderInstruction = + binaryFromUnsafe(F, x)(unsafeBuilder.repeat(_, n, _)) + /** * {{{Skolem(ex)}}} * @param ex diff --git a/tlair/src/main/scala/at/forsyte/apalache/tla/typecomp/unsafe/UnsafeApalacheBuilder.scala b/tlair/src/main/scala/at/forsyte/apalache/tla/typecomp/unsafe/UnsafeApalacheBuilder.scala index c42401d7e8..42c63bce5a 100644 --- a/tlair/src/main/scala/at/forsyte/apalache/tla/typecomp/unsafe/UnsafeApalacheBuilder.scala +++ b/tlair/src/main/scala/at/forsyte/apalache/tla/typecomp/unsafe/UnsafeApalacheBuilder.scala @@ -43,6 +43,19 @@ class UnsafeApalacheBuilder(private val strict: Boolean = true) extends ProtoBui OperEx(ApalacheOper.gen, mkTlaInt(n))(Typed(t)) } + /** + * {{{Repeat(F, N, x): t}}} + * @param n + * must be a nonnegative constant expression + * @param F + * must be an expression of the shape {{{LET Op(i) == ... IN Op}}} + */ + def repeat(F: TlaEx, n: BigInt, x: TlaEx): TlaEx = { + if (strict) require(n > 0, s"Expected n to be positive, found $n.") + if (strict) require(isNaryPassByName(n = 2)(F), s"Expected F to be a binary operator passed by name, found $F.") + buildBySignatureLookup(ApalacheOper.repeat, F, mkTlaInt(n), x) + } + /** * {{{Skolem(ex)}}} * @param ex diff --git a/tlair/src/test/scala/at/forsyte/apalache/tla/typecomp/TestSetBuilder.scala b/tlair/src/test/scala/at/forsyte/apalache/tla/typecomp/TestSetBuilder.scala index 2fee63793c..6ebb57ec76 100644 --- a/tlair/src/test/scala/at/forsyte/apalache/tla/typecomp/TestSetBuilder.scala +++ b/tlair/src/test/scala/at/forsyte/apalache/tla/typecomp/TestSetBuilder.scala @@ -292,7 +292,7 @@ class TestSetBuilder extends BuilderTest { builder.name(s"x$i", ti), builder.name(s"S$i", InvalidTypeMethods.notSet), ) - } + }, ) } @@ -441,7 +441,7 @@ class TestSetBuilder extends BuilderTest { ts.zipWithIndex.map { case ((ti, _), i) => builder.name(s"x$i", ti) -> builder.name(s"S$i", InvalidTypeMethods.notSet) - } + }, ) }