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

Quint -> TLA+ transpilation fixes #3041

Merged
merged 10 commits into from
Dec 4, 2024
1 change: 1 addition & 0 deletions .unreleased/bug-fixes/quint-to-tla-transpilation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fixed a few problems on parsing Quint and pretty printing TLA, see #3041
Original file line number Diff line number Diff line change
Expand Up @@ -297,14 +297,6 @@ class PrettyWriter(
text("{") <> nest(line <> binding <> ":" <> nest(line <> filter)) <> line <> text("}")
) ///

// a function of multiple arguments that are packed into a tuple: don't print the angular brackets <<...>>
case OperEx(op @ TlaFunOper.app, funEx, OperEx(TlaFunOper.tuple, args @ _*)) =>
val argDocs = args.map(exToDoc(op.precedence, _, nameResolver))
val commaSeparatedArgs = folddoc(argDocs.toList, _ <> text(",") <@> _)
group(
exToDoc(TlaFunOper.app.precedence, funEx, nameResolver) <> brackets(commaSeparatedArgs)
) ///

Comment on lines -300 to -307
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why do we have to remove this? In TLA+, f[x, y] is a short-form for f[<<x, y>>]. Look at this example: https://gist.github.com/konnov/ce0ca9071dbe75b936e96dbd99224f0d

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

It does work like that for tuples, but not for sequences:

--------------------------------- MODULE test ---------------------------------
EXTENDS Integers, Sequences

VARIABLES
  f

Init ==
  f = [ l \in {<<1>>, <<>>, <<1, 2>>} |-> Len(l) ]

Next ==
  UNCHANGED f

Inv ==
  f[<<1>>] = 1

Inv2 ==
  f[1] = 1

=============================================================================

TLC fails to check Inv2 here:

Error: Attempted to check equality of the function << >> with the value:
1
While working on the initial state:
f = (<<>> :> 0 @@ <<1>> :> 1 @@ <<1, 2>> :> 2)

Error: TLC was unable to fingerprint.

I believe there is no way to disambiguate between tuples and sequences at this point, so the only option was to remove it completely.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Interesting. I think the short-form syntax only applies to pairs, triples, etc.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yeah, these invariants work for your example:

Inv ==
  f[<<1, 2>>] = 2

Inv2 ==
  f[1, 2] = 2

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Ah interesting. Yeah, the problem I hit was on a list of one element (from the hashes you wrote in ICS23 btw).

// a function of a single argument
case OperEx(TlaFunOper.app, funEx, argEx) =>
group(
Expand Down Expand Up @@ -473,14 +465,19 @@ class PrettyWriter(
wrapWithParen(parentPrecedence, op.precedence, group(doc))

case OperEx(op @ TlaOper.apply, NameEx(name), args @ _*) =>
val (decls, newArgs) = extractDecls(args)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Do you have an example of what does not work exactly? For instance, this one works in TLC: https://gist.github.com/konnov/e7df8586a9865027dc4abfc07188e060

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Oh interesting, I only realize this now, but this problem only happens with high-order operators. Here's an example that doesn't work:

--------------------------------- MODULE test ---------------------------------
EXTENDS Integers

VARIABLES z

F(f(_), x) == f(x)

G(x) == F(LET H(y) == y * 2 IN H, x)

Init ==
  z = G(3)

Next ==
  UNCHANGED z

=============================================================================

TLC Errors:

*** Errors: 2

line 8, col 11 to line 8, col 32 of module test

An expression appears as argument number 1 (counting from 1) to operator 'F', in a position an operator is required.


line 8, col 9 to line 8, col 36 of module test

Argument number 1 to operator 'F'
should be a 1-parameter operator.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Right. This is because operators are not values in TLA+


// apply an operator by its name, e.g., F(x)
val argDocs = args.map(exToDoc(op.precedence, _, nameResolver)).toList
val argDocs = newArgs.map(exToDoc(op.precedence, _, nameResolver)).toList
val commaSeparated = ssep(argDocs, "," <> softline)

val doc =
if (args.isEmpty) {
text(parseableName(name))
} else {
} else if (decls.isEmpty) {
group(parseableName(name) <> parens(commaSeparated))
} else {
group(ssep(decls, line) <> line <> parseableName(name) <> parens(commaSeparated))
}

wrapWithParen(parentPrecedence, op.precedence, doc)
Expand All @@ -494,16 +491,20 @@ class PrettyWriter(
wrapWithParen(parentPrecedence, op.precedence, doc)

case OperEx(op, args @ _*) =>
val argDocs = args.map(exToDoc(op.precedence, _, nameResolver)).toList
val (decls, newArgs) = extractDecls(args)
val argDocs = newArgs.map(exToDoc(op.precedence, _, nameResolver)).toList
val commaSeparated = ssep(argDocs, "," <> softline)
// The standard operators may contain a '!' that refers to the standard module. Remove it.
val lastIndexOfBang = op.name.lastIndexOf("!")
val unqualifiedName = if (lastIndexOfBang < 0) op.name else (op.name.substring(lastIndexOfBang + 1))

val doc =
if (args.isEmpty) {
text(unqualifiedName)
} else {
} else if (decls.isEmpty) {
group(unqualifiedName <> parens(commaSeparated))
} else {
group(ssep(decls, line) <> line <> unqualifiedName <> parens(commaSeparated))
}

wrapWithParen(parentPrecedence, op.precedence, doc)
Expand All @@ -527,6 +528,25 @@ class PrettyWriter(
}
}

/**
* On TLA+ files, we can't have LET..IN expressions as arguments. This is a helper function to extract the
* declarations so they can be printed before the operator application that would use them as arguments.
Comment on lines +532 to +533
Copy link
Collaborator

Choose a reason for hiding this comment

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

Do you mean something like this?

LET A(x, y) == x + y IN F(A)

Actually, this one should work if F is declared as F(A(_)) == ...

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Not sure if I understand the question. I'm saying this one works:

LET A(x, y) == x + y IN F(A)

while this one doesn't:

F(LET A(x, y) == x + y IN A)

*/
def extractDecls(exprs: Seq[TlaEx]): (List[Doc], Seq[TlaEx]) = {
val decls = exprs.collect {
case LetInEx(_, TlaOperDecl("LAMBDA", _, _)) => Nil
case LetInEx(_, decls @ _*) => decls
case _ => Nil
}
val newArgs = exprs.collect {
case expr @ LetInEx(_, TlaOperDecl("LAMBDA", _, _)) => expr
case LetInEx(body, _) => body
case expr => expr
}

(decls.flatten.map(d => group("LET" <> space <> declToDoc(d) <> line <> "IN")).toList, newArgs)
}

/**
* Pretty-writes the given decl, while replacing names with the values in the NameReplacementMap. Then, wrap the
* result as a comment, since the substituted names might not be valid TLA.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -446,7 +446,7 @@ class Quint(quintOutput: QuintOutput) {
case ValEx(TlaStr("_")) =>
// We have a default case, which is always paired with an eliminator that
// can be applied to the unit value (an empty record).
(cs, Some(tla.appOp(defaultElim, tla.rowRec(None))))
(cs, Some(tla.appOp(defaultElim, tla.emptySet(IntT1))))
case _ =>
// All cases have match expressions
(allCases, None)
Expand Down Expand Up @@ -573,7 +573,7 @@ class Quint(quintOutput: QuintOutput) {
case "Tup" =>
if (quintArgs.isEmpty) {
// Translate empty tuples to values of type UNIT
(_) => Reader((_) => tla.const("U", ConstT1(("UNIT"))))
(_) => Reader((_) => tla.rowRec(None, "tag" -> tla.str("UNIT")))
} else {
variadicApp(args => tla.tuple(args: _*))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,11 @@ private class QuintTypeConverter extends LazyLogging {
case QuintSeqT(elem) => SeqT1(convert(elem))
case QuintFunT(arg, res) => FunT1(convert(arg), convert(res))
case QuintOperT(args, res) => OperT1(args.map(convert), convert(res))
// Use the Unit type for empty tuples, since they are the unit type in Quint
// The unit type is a record like [ "tag" |-> "UNIT" ] so it can be compared to other constructed types.
// This is relevant when we produce a TLA+ file to be used with TLC.
case QuintTupleT(Row.Nil() | Row.Cell(Seq(), _)) =>
ConstT1("UNIT") // Use the Unit type for empty tuples, since they are the unit type in Quint
RecRowT1(RowT1("tag" -> StrT1))
case QuintTupleT(row) => rowToTupleT1(row)
case QuintRecordT(row) => RecRowT1(rowToRowT1(row))
case QuintSumT(row) => VariantT1(rowToRowT1(row))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -844,6 +844,24 @@ class TestPrettyWriter extends AnyFunSuite with BeforeAndAfterEach {
assert(expected == stringWriter.toString)
}

test("operator application with LET-IN as argument") {
val writer = new PrettyWriter(printWriter, layout40)
val decl1 =
TlaOperDecl("A", List(OperParam("param1"), OperParam("param2")), plus(name("param1"), name("param2")))
val letInEx = letIn(appDecl(decl1, int(1), int(2)), decl1)
// Foo(LET A(param1, param2) == param1 + param2 IN A(1, 2))
val expr = OperEx(TlaOper.apply, NameEx("Foo"), letInEx)

writer.write(expr)
printWriter.flush()
// LET declaration needs to be printed before the application
val expected =
"""LET A(param1, param2) == param1 + param2
|IN
|Foo((A(1, 2)))""".stripMargin
assert(expected == stringWriter.toString)
}

test("a LAMBDA as LET-IN") {
val writer = new PrettyWriter(printWriter, layout40)
val aDecl = TlaOperDecl("LAMBDA", List(OperParam("x")), NameEx("x"))
Expand Down Expand Up @@ -989,7 +1007,7 @@ class TestPrettyWriter extends AnyFunSuite with BeforeAndAfterEach {
writer.write(fDecl)
printWriter.flush()
val expected =
"""f[x \in S, y \in S] == f[y, x]
"""f[x \in S, y \in S] == f[<<y, x>>]
|
|""".stripMargin
assert(expected == stringWriter.toString)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -625,7 +625,7 @@ class TestQuintEx extends AnyFunSuite {
}

test("can convert builtin empty Tup operator application to uninterpreted value") {
assert(convert(Q.app("Tup")(QuintTupleT.ofTypes())) == "\"U_OF_UNIT\"")
assert(convert(Q.app("Tup")(QuintTupleT.ofTypes())) == "[\"tag\" ↦ \"UNIT\"]")
}

/// SUM TYPES
Expand All @@ -651,7 +651,7 @@ class TestQuintEx extends AnyFunSuite {
val expected =
"""|CASE (Variants!VariantTag(Variants!Variant("F1", 42)) = "F1") → LET __QUINT_LAMBDA0(x) ≜ 1 IN __QUINT_LAMBDA0(Variants!VariantGetUnsafe("F1", Variants!Variant("F1", 42)))
|☐ (Variants!VariantTag(Variants!Variant("F1", 42)) = "F2") → LET __QUINT_LAMBDA1(y) ≜ 2 IN __QUINT_LAMBDA1(Variants!VariantGetUnsafe("F2", Variants!Variant("F1", 42)))
|☐ OTHER → LET __QUINT_LAMBDA2(_) ≜ 2 IN __QUINT_LAMBDA2([])""".stripMargin
|☐ OTHER → LET __QUINT_LAMBDA2(_) ≜ 2 IN __QUINT_LAMBDA2({})""".stripMargin
.replace('\n', ' ')
assert(convert(quintMatch) == expected)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import org.scalatest.funsuite.AnyFunSuite
import org.scalatestplus.junit.JUnitRunner
import at.forsyte.apalache.io.quint.QuintType._
import at.forsyte.apalache.tla.lir.{
ConstT1, FunT1, IntT1, RecRowT1, RowT1, SparseTupT1, StrT1, TlaType1, TupT1, VarT1, VariantT1,
FunT1, IntT1, RecRowT1, RowT1, SparseTupT1, StrT1, TlaType1, TupT1, VarT1, VariantT1,
}

/**
Expand Down Expand Up @@ -35,7 +35,7 @@ class TestQuintTypes extends AnyFunSuite {

test("empty Quint tuple types are converted to the UNIT uninterpreted type") {
val tuple = QuintTupleT(Row.Nil())
assert(translate(tuple) == ConstT1("UNIT"))
assert(translate(tuple) == RecRowT1(RowT1("tag" -> StrT1)))
}

test("Polymorphic Quint tuples types are converted to sparse tuples") {
Expand Down
Loading