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

Type compatibility error message, when reading from .cfg #2757

Merged
merged 7 commits into from
Oct 17, 2023
Merged
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package at.forsyte.apalache.infra.tlc.config

import at.forsyte.apalache.infra.tlc.config.ConfigModelValue.STR_PREFIX
import at.forsyte.apalache.tla.lir.convenience.tla
import at.forsyte.apalache.tla.lir.{BoolT1, IntT1, SetT1, StrT1, TlaEx, Typed, VarT1}
import at.forsyte.apalache.tla.lir.TypedPredefs._
import at.forsyte.apalache.tla.lir.{TlaType1, Typed, VarT1}
import at.forsyte.apalache.tla.typecomp.TBuilderInstruction
import at.forsyte.apalache.tla.types.{tla, ModelValueHandler}

import scala.util.parsing.input.NoPosition

Expand Down Expand Up @@ -46,7 +46,7 @@ sealed abstract class ConfigConstExpr {
* @return
* the TLA IR expression that represents the parsed constant expression
*/
def toTlaEx: TlaEx
def toTla: TBuilderInstruction
}

object ConfigModelValue {
Expand All @@ -64,11 +64,11 @@ object ConfigModelValue {
* the name of a model value
*/
case class ConfigModelValue(name: String) extends ConfigConstExpr {
Copy link
Collaborator

Choose a reason for hiding this comment

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

What is the difference between ConfigModelValue and ConfigStrValue now? It looks like ConfigStrValue handles model values too.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

ConfigModelValue is what you get when TLC model values are parsed (e.g. a and b in Producers = {a,b}).
ConfigStrValue is what you get when a "-value is parsed (e.g. "abc" or "1_OF_X")

override def toTlaEx: TlaEx = {
override def toTla: TBuilderInstruction = {
// currently, we use the type Str for all model values.
// In the future, we might want to distinguish between different uninterpreted types.
// See https://github.com/informalsystems/apalache/issues/570
tla.str(STR_PREFIX + name).typed(StrT1)
tla.str(STR_PREFIX + name)
}
}

Expand All @@ -79,7 +79,7 @@ case class ConfigModelValue(name: String) extends ConfigConstExpr {
* an integer as BigInt
*/
case class ConfigIntValue(num: BigInt) extends ConfigConstExpr {
override def toTlaEx: TlaEx = tla.bigInt(num).typed(IntT1)
override def toTla: TBuilderInstruction = tla.int(num)
}

/**
Expand All @@ -89,7 +89,7 @@ case class ConfigIntValue(num: BigInt) extends ConfigConstExpr {
* a boolean
*/
case class ConfigBoolValue(b: Boolean) extends ConfigConstExpr {
override def toTlaEx: TlaEx = tla.bool(b).typed(BoolT1)
override def toTla: TBuilderInstruction = tla.bool(b)
}

/**
Expand All @@ -99,7 +99,13 @@ case class ConfigBoolValue(b: Boolean) extends ConfigConstExpr {
* a string
*/
case class ConfigStrValue(str: String) extends ConfigConstExpr {
override def toTlaEx: TlaEx = tla.str(str).typed(StrT1)
override def toTla: TBuilderInstruction = {
ModelValueHandler.typeAndIndex(str) match {
case None => tla.str(str)
case Some((constT, idx)) => tla.const(idx, constT)
}

}
}

/**
Expand All @@ -109,18 +115,18 @@ case class ConfigStrValue(str: String) extends ConfigConstExpr {
* the set elements, which are constant expression themselves.
*/
case class ConfigSetValue(elems: ConfigConstExpr*) extends ConfigConstExpr {
override def toTlaEx: TlaEx = {
val setElems = elems.map(_.toTlaEx)
override def toTla: TBuilderInstruction = {
val setElems = elems.map(_.toTla)
if (setElems.isEmpty) {
// the element type is uknown, introduce a polymorphic type Set(a)
tla.enumSet().typed(SetT1(VarT1(0)))
tla.emptySet(VarT1(0))
} else {
// the element type should be unique
val headType = setElems.head.typeTag.asTlaType1()
val headType = TlaType1.fromTypeTag(setElems.head.typeTag)
if (setElems.tail.exists(_.typeTag != Typed(headType))) {
throw new TlcConfigParseError("Set elements have different types: " + setElems.mkString(", "), NoPosition)
} else {
tla.enumSet(setElems: _*).typed(SetT1(headType))
tla.enumSet(setElems: _*)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import at.forsyte.apalache.infra.tlc.config.TlcConfig
import at.forsyte.apalache.infra.tlc.config.InitNextSpec
import at.forsyte.apalache.infra.tlc.config.TemporalSpec
import at.forsyte.apalache.infra.tlc.config.NullSpec
import at.forsyte.apalache.tla.typecheck.TypingInputException

/**
* The pass that collects the configuration parameters and overrides constants and definitions. This pass also
Expand Down Expand Up @@ -50,8 +51,33 @@ class ConfigurationPassImpl @Inject() (
case Some((tlcConfig, tlcConfigFile)) => loadOptionsFromTlcConfig(tlaModule, tlcConfig, tlcConfigFile)
}

val constOverrideNames = tlaModule.constDeclarations.map(_.name).map(ConstAndDefRewriter.OVERRIDE_PREFIX + _).toSet
val (constOverrides, otherOverrides) = overrides.partition(d => constOverrideNames.contains(d.name))
val constOverrideNamesAndTypes =
tlaModule.constDeclarations.map { decl =>
(ConstAndDefRewriter.OVERRIDE_PREFIX + decl.name) -> decl.typeTag
}.toMap

val (constOverrides, otherOverrides) = overrides.partition(d => constOverrideNamesAndTypes.contains(d.name))

// Typecheck
// Since config files are not typecheckable, blind override substitution can introduce type inconsistencies
// see #2750
// To circumvent this, we manually perform a type-consistency check, to verify that values overriding constants
// have the same type as the constant declaration
constOverrides.foreach { decl =>
val constDeclTag = constOverrideNamesAndTypes(decl.name)
val overrideTag = decl match {
case d: TlaOperDecl => d.body.typeTag
case _ => decl.typeTag
}

if (overrideTag != constDeclTag) {
throw new TypingInputException(
s"Constant ${decl.name.drop(ConstAndDefRewriter.OVERRIDE_PREFIX.length)} declared in the specification has the type tag $constDeclTag, while the value defined in the .cfg file has the type tag $overrideTag.\n" +
s"Please make sure the values in the .cfg file have types matching those in the specification, or use --cinit instead.",
decl.ID,
)
}
}

val newDecls =
if (constOverrides.nonEmpty) {
Expand Down
7 changes: 7 additions & 0 deletions test/tla/Bug2750.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
\* Add statements after this line.
SPECIFICATION Spec
CONSTANTS
Producers = {p1,p2}
Consumers = {c1,c2}
BufCapacity = 1
INVARIANT Inv
87 changes: 87 additions & 0 deletions test/tla/Bug2750.tla
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
--------------------------- MODULE Bug2750 ---------------------------
(***************************************************************************)
(* Original problem and spec by Michel Charpentier *)
(* http://www.cs.unh.edu/~charpov/programming-tlabuffer.html *)
(***************************************************************************)
EXTENDS Naturals, Sequences

CONSTANTS
\* @type: Set(PROC);
Producers, (* the (nonempty) set of producers *)
\* @type: Set(PROC);
Consumers, (* the (nonempty) set of consumers *)
\* @type: Int;
BufCapacity (* the maximum number of messages in the bounded buffer *)

ASSUME Assumption ==
/\ Producers # {} (* at least one producer *)
/\ Consumers # {} (* at least one consumer *)
/\ Producers \intersect Consumers = {} (* no thread is both consumer and producer *)
/\ BufCapacity \in (Nat \ {0}) (* buffer capacity is at least 1 *)

-----------------------------------------------------------------------------

VARIABLES
\* @type: Seq(PROC);
buffer,
\* @type: Set(PROC);
waitP,
\* @type: Set(PROC);
waitC

(* define statement *)
isfull(b) == Len(b) = BufCapacity
isempty(b) == Len(b) = 0

\* @type: << Seq(PROC), Set(PROC), Set(PROC) >>;
vars == << buffer, waitP, waitC >>

ProcSet == (Producers) \cup (Consumers)

Init == (* Global variables *)
/\ buffer = << >>
/\ waitP = {}
/\ waitC = {}

p(self) == /\ (self \notin waitP)
/\ IF isfull(buffer)
THEN /\ IF self \in Producers
THEN /\ waitP' = (waitP \union {self})
/\ waitC' = waitC
ELSE /\ waitC' = (waitC \union {self})
/\ waitP' = waitP
/\ UNCHANGED buffer
ELSE /\ buffer' = Append(buffer, self)
/\ IF waitC # {}
THEN /\ \E t \in waitC:
waitC' = waitC \ {t}
ELSE /\ TRUE
/\ waitC' = waitC
/\ waitP' = waitP

c(self) == /\ (self \notin waitC)
/\ IF isempty(buffer)
THEN /\ IF self \in Producers
THEN /\ waitP' = (waitP \union {self})
/\ waitC' = waitC
ELSE /\ waitC' = (waitC \union {self})
/\ waitP' = waitP
/\ UNCHANGED buffer
ELSE /\ buffer' = Tail(buffer)
/\ IF waitP # {}
THEN /\ \E t \in waitP:
waitP' = waitP \ {t}
ELSE /\ TRUE
/\ waitP' = waitP
/\ waitC' = waitC

Next == (\E self \in Producers: p(self))
\/ (\E self \in Consumers: c(self))

Spec == Init /\ [][Next]_vars

\* END TRANSLATION

Inv == ~(waitP = Producers /\ waitC = Consumers)

=============================================================================
8 changes: 8 additions & 0 deletions test/tla/Test2750.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
INIT
Init
NEXT
Next
INVARIANT
Inv
CONSTANT
C = "1_OF_A"
19 changes: 19 additions & 0 deletions test/tla/Test2750.tla
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
----------------------- MODULE Test2750 -----------------------------
VARIABLES
\* @type: A;
x

CONSTANT
\* @type: A;
C


Init ==
/\ x = C

Next ==
UNCHANGED x

Inv == TRUE

===============================================================================
16 changes: 16 additions & 0 deletions test/tla/cli-integration-tests.md
Original file line number Diff line number Diff line change
Expand Up @@ -2316,6 +2316,22 @@ $ apalache-mc check --length=1 --cinit=CInit Bug2268.tla | sed 's/[IEW]@.*//'
EXITCODE: OK
```

### check Bug2750.tla

```sh
$ apalache-mc check --config=Bug2750.cfg Bug2750.tla | sed 's/[IEW]@.*//'
...
EXITCODE: ERROR (255)
```

### check Test2750.tla

```sh
$ apalache-mc check --config=Test2750.cfg Test2750.tla | sed 's/[IEW]@.*//'
...
EXITCODE: OK
```

### check profiling

Check that the profiler output is produced as explained in
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
package at.forsyte.apalache.tla.pp

import at.forsyte.apalache.infra.tlc.config._
import at.forsyte.apalache.tla.lir.TypedPredefs._
import at.forsyte.apalache.tla.lir._
import at.forsyte.apalache.tla.lir.convenience._
import at.forsyte.apalache.tla.lir.transformations.TlaModuleTransformation
import at.forsyte.apalache.tla.typecomp.TBuilderInstruction
import at.forsyte.apalache.tla.types.tla
import com.typesafe.scalalogging.LazyLogging

/**
Expand All @@ -17,28 +17,24 @@ import com.typesafe.scalalogging.LazyLogging
* Andrey Kuprianov
*/
class TlcConfigImporter(config: TlcConfig) extends TlaModuleTransformation with LazyLogging {
private val boolOperT = OperT1(Seq(), BoolT1)

private def mkBoolName(name: String): TlaEx = {
tla.name(name).typed(BoolT1)
private def mkBoolName(name: String): TBuilderInstruction = {
tla.name(name, BoolT1)
}

override def apply(mod: TlaModule): TlaModule = {

val assignments = config.constAssignments.map { case (param, value) =>
val valueEx = value.toTlaEx
val operT = OperT1(Seq(), valueEx.typeTag.asTlaType1())
tla.declOp(ConstAndDefRewriter.OVERRIDE_PREFIX + param, value.toTlaEx).as(operT)
val valueEx = value.toTla
tla.decl(ConstAndDefRewriter.OVERRIDE_PREFIX + param, valueEx)
}
val replacements = config.constReplacements.map { case (param, value) =>
mod.declarations.find(_.name == value) match {
case Some(d: TlaOperDecl) =>
if (d.formalParams.isEmpty) {
val tt = d.typeTag.asTlaType1()
val tt = TlaType1.fromTypeTag(d.typeTag)
assert(tt.isInstanceOf[OperT1])
val operT = tt.asInstanceOf[OperT1]
val application = tla.appOp(tla.name(value).typed(operT)).typed(operT.res)
tla.declOp(ConstAndDefRewriter.OVERRIDE_PREFIX + param, application).as(operT)
val application = tla.appOp(tla.name(value, tt))
tla.decl(ConstAndDefRewriter.OVERRIDE_PREFIX + param, application)
} else {
val nparams = d.formalParams.size
throw new TLCConfigurationError(
Expand All @@ -47,42 +43,43 @@ class TlcConfigImporter(config: TlcConfig) extends TlaModuleTransformation with

case Some(d) =>
// This is a branch from the old untyped encoding. Does it make sense in the type encoding?
val tt = d.typeTag.asTlaType1()
tla.declOp(ConstAndDefRewriter.OVERRIDE_PREFIX + param, tla.name(value).typed(tt)).as(OperT1(Seq(), tt))
val tt = TlaType1.fromTypeTag(d.typeTag)
tla.decl(ConstAndDefRewriter.OVERRIDE_PREFIX + param, tla.name(value, tt))

case None =>
throw new TLCConfigurationError(s"Met a replacement $param <- $value, but $value is not found")
}
}
val stateConstraints = config.stateConstraints.zipWithIndex.map { case (value, index) =>
tla.declOp(TlcConfigImporter.STATE_PREFIX + index, mkBoolName(value)).as(boolOperT)
tla.decl(TlcConfigImporter.STATE_PREFIX + index, mkBoolName(value))
}
val actionConstraints = config.actionConstraints.zipWithIndex.map { case (value, index) =>
tla.declOp(TlcConfigImporter.ACTION_PREFIX + index, mkBoolName(value)).as(boolOperT)
tla.decl(TlcConfigImporter.ACTION_PREFIX + index, mkBoolName(value))
}
val invariants = config.invariants.zipWithIndex.map { case (value, index) =>
tla.declOp(TlcConfigImporter.INVARIANT_PREFIX + index, mkBoolName(value)).as(boolOperT)
tla.decl(TlcConfigImporter.INVARIANT_PREFIX + index, mkBoolName(value))
}
val temporalProps = config.temporalProps.zipWithIndex.map { case (value, index) =>
tla.declOp(TlcConfigImporter.TEMPORAL_PREFIX + index, mkBoolName(value)).as(boolOperT)
tla.decl(TlcConfigImporter.TEMPORAL_PREFIX + index, mkBoolName(value))
}
val behaviorSpec = config.behaviorSpec match {
case InitNextSpec(init, next) =>
List(
tla.declOp(TlcConfigImporter.INIT, mkBoolName(init)).as(boolOperT),
tla.declOp(TlcConfigImporter.NEXT, mkBoolName(next)).as(boolOperT),
tla.decl(TlcConfigImporter.INIT, mkBoolName(init)),
tla.decl(TlcConfigImporter.NEXT, mkBoolName(next)),
)

case TemporalSpec(name) =>
List(tla.declOp(TlcConfigImporter.SPEC, mkBoolName(name)).as(boolOperT))
List(tla.decl(TlcConfigImporter.SPEC, mkBoolName(name)))

case NullSpec() =>
throw new TLCConfigurationError("Neither INIT and NEXT, nor SPECIFICATION found in the TLC configuration file")
}
new TlaModule(mod.name,
mod.declarations
++ assignments ++ replacements ++ stateConstraints ++ actionConstraints
++ invariants ++ temporalProps ++ behaviorSpec)

val extendedDecls: Iterable[TlaDecl] =
(assignments ++ replacements ++ stateConstraints ++ actionConstraints ++ invariants ++ temporalProps ++ behaviorSpec)
.map(_.build)
TlaModule(mod.name, mod.declarations ++ extendedDecls)
}
}

Expand Down
Loading