2
votes

I am modeling a Google Docs-like inline commenting system in Alloy. Specifically, I'm modeling the UI interactions that the user can do. The constraints, in English, are that:

  • users can have any number of comments
  • users can have any number of comments composing at once
  • comments can be either composing or editing, not both
  • (eventually) users can expand one comment at a time to view it—text is truncated otherwise.

so far I have only modeled adding a new comment—or so I think! When I run the model below, I expect to see that Alloy cannot find any counterexamples, but it finds one where a comment is in both the composed and composing relations at a single time.

I think there may be something off in my mental model of how Alloy works in this case. I can't see how a comment would possibly be added to the composed relation here: I think init would set all the drafts to have empty relations there, and that the actions in Traces would preclude a comment ever being added to composed. BUT! I'm clearly wrong, since Alloy finds an example where composed is non-empty. Does anyone know what's actually happening here?

module inlineComments

open util/ordering [Time] as TimeOrdering

sig Time {}

sig Draft {
  composing: set Comment -> Time,
  composed: set Comment -> Time,
  -- expanded: lone Comment -> Time,
}

sig Comment {}

-- What can we do with these?

pred newComment (t, t': Time, c: Comment, d: Draft) {
  -- comment is not already known to us
  c not in d.composing.t
  c not in d.composed.t

  -- start composing the comment
  d.composing.t' = d.composing.t + c
}

pred init (t: Time) {
  all d: Draft, c: Comment | c not in d.composing.t
  all d: Draft, c: Comment | c not in d.composed.t
}

fact Traces {
  init[TimeOrdering/first]

  all t: Time - TimeOrdering/last |
    let t' = TimeOrdering/next[t] |
      some d: Draft, c: Comment |
        newComment [t, t', c, d]
}

-- Is the world how we expect it to be?

assert validState {
  -- comments cannot be composing and composed at the same time
  all t: Time, d: Draft | d.composing.t & d.composed.t = none
}

check validState for 3 but 1 Draft
1

1 Answers

1
votes

This is known as the "frame problem": you've specified that new comments can be put into composing, but not that nothing else happens! You have to make it explicit that the only way the system may change is via newComment. Here's one way you could do that:

fact Traces {
  init[TimeOrdering/first]

  all t: Time - TimeOrdering/last |
    let t' = TimeOrdering/next[t] |
      some d: Draft, c: Comment {
        newComment [t, t', c, d]
        d.composed.t = d.composed.t' -- d.composed doesn't change
      }
}

Note this only works because you've exlicitly scoped to a maximum of 1 draft. If you test with two drafts, you also have to show that none of the drafts change in any way. If that will always be the case that there's only one Draft, you can write one sig Draft to enforce that.