syndicate-2017/racket/typed/proto.rkt

1103 lines
37 KiB
Racket

#lang racket
(require (only-in racket/hash hash-union))
(module+ test
(require rackunit))
;; -------------------------------------------------------------------------
;; Role Type Data Definitions
;; a FacetName is a symbol
;; a T is one of
;; - (Role FacetName (Listof EP)), also abbreviated as just Role
;; - (Spawn τ)
;; - (Stop FacetName Body)
(struct Role (nm eps) #:transparent)
(struct Spawn (ty) #:transparent)
(struct Stop (nm body) #:transparent)
;; a EP is one of
;; - (Reacts D Body), describing an event handler
;; - (Shares τ), describing an assertion
(struct Reacts (evt body) #:transparent)
(struct Shares (ty) #:transparent)
;; a Body describes actions carried out in response to some event, and
;; is one of
;; - T
;; - (Listof Body)
;; - (Branch (Listof Body))
(struct Branch (arms) #:transparent)
;; a D is one of
;; - (Know τ), reaction to assertion
;; - (¬Know τ), reaction to retraction
(struct Know (ty) #:transparent)
(struct ¬Know (ty) #:transparent)
;; a τ is one of
;; - (U (Listof τ))
;; - (Struct StructName (Listof τ ...))
;; - (Observe τ)
;; - ⋆
;; - Int
;; - String
;; - Bool
(struct U (tys) #:transparent)
(struct Struct (nm tys) #:transparent)
(struct Observe (ty) #:transparent)
(struct Mk⋆ () #:transparent)
;; TODO this might be a problem when used as a match pattern
(define (Mk⋆))
(struct MkInt () #:transparent)
(define Int (MkInt))
(struct MkString () #:transparent)
(define String (MkString))
(struct MkBool () #:transparent)
(define Bool (MkBool))
;; a StructName is a Symbol
;; --------------------------------------------------------------------------
;; Derived Types
;; τ (Listof EP) -> EP
(define (During assertion eps)
(define facet-name (gensym 'during-inner))
(Reacts (Know assertion)
(Role facet-name
(cons (Reacts (¬Know assertion)
(Stop facet-name '()))
eps))))
;; --------------------------------------------------------------------------
;; Examples
(define manager
(Role 'account-manager
(list (Shares (Struct 'account (list Int)))
(Reacts (Know (Struct 'deposit '())) '()))))
(define client
(Role 'client
(list (Reacts (Know (Struct 'account (list Int))) '()))))
;; τ τ -> τ
;; short hand for creating a book quote struct type
(define (book-quote ty1 ty2)
(Struct 'book-quote (list ty1 ty2)))
;; τ τ τ -> τ
;; short hand for creating a book quote interest type
(define (book-interest ty1 ty2 ty3)
(Struct 'book-interest (list ty1 ty2 ty3)))
;; τ -> τ
;; short hand for creating a book of the month type
(define (book-of-the-month ty)
(Struct 'book-of-the-month (list ty)))
;; τ -> τ
;; short hand for creating a club member type
(define (club-member ty)
(Struct 'club-member (list ty)))
(define seller
(Role 'seller
(list
(Reacts (Know (Observe (book-quote String )))
(Role 'fulfill
(list (Shares (book-quote String Int))))))))
(define seller-actual
(Role
'seller27
(list
(Reacts
(Know (Observe (book-quote String )))
(Role
'during-inner29
(list
(Shares (book-quote String (U (list Int Int))))
(Reacts
(¬Know (Observe (book-quote String )))
(Stop 'during-inner29 '()))))))))
(define leader-spec
(Role 'leader
(list
(Reacts
(Know (book-quote String Int))
(Role 'poll
(list
(Reacts
(Know (book-interest String String Bool))
(Branch
(list
(Stop 'leader
(Role 'announce
(list
(Shares (book-of-the-month String)))))
(Stop 'poll (list)))))))))))
(define leader-actual
(Role
'get-quotes
(list
(Reacts
(Know (book-quote String Int))
(Branch
(list
;; problem 1: spec doesn't say actor can give up when running out of books
(Stop 'get-quotes '())
(Role
'poll-members
(list
(Reacts
(Know (book-interest String String ))
(Branch (list
;; problem 2: combining poll-members and get-quotes here (should be another branch)
(Stop 'poll-members
(Stop 'get-quotes '()))
(Stop 'get-quotes
(Role 'announce
(list
(Shares (book-of-the-month String))))))))
(Reacts (¬Know (book-interest String String Bool)) (list))
(Reacts (Know (book-interest String String Bool)) (list))
(Reacts (¬Know (book-interest String String Bool)) (list))
(Reacts (Know (book-interest String String Bool)) (list)))))))
(Reacts (¬Know (club-member String)) (list))
(Reacts (Know (club-member String)) (list)))))
(define leader-fixed?
(Role 'get-quotes
(list
(Reacts (Know (book-quote String Int))
(Branch (list
(Role 'poll-members
(list
(Reacts (Know (book-interest String String ))
(Branch (list
(Stop 'poll-members
'())
(Stop 'get-quotes
(Role 'announce
(list
(Shares (book-of-the-month String))))))))
(Reacts (¬Know (book-interest String String Bool)) (list))
(Reacts (Know (book-interest String String Bool)) (list))
(Reacts (¬Know (book-interest String String Bool)) (list))
(Reacts (Know (book-interest String String Bool)) (list)))))))
(Reacts (¬Know (club-member String)) (list))
(Reacts (Know (club-member String)) (list)))))
(define leader-revised
(Role
'get-quotes
(list
(Reacts
(Know (book-quote String Int))
(Branch
(list
(Branch (list (Stop 'get-quotes (list)) (list)))
(Role
'poll-members
(list
(Reacts
(Know (book-interest String String ))
(list
(Branch
(list
(Stop 'poll-members
(Branch (list
(Stop 'get-quotes (list))
(list))))
(list)))
(Branch
(list
(Stop
'get-quotes
(Role 'announce (list (Shares (book-of-the-month String)))))
(list)))))
(Reacts (¬Know (book-interest String String Bool)) (list))
(Reacts (Know (book-interest String String Bool)) (list))
(Reacts (¬Know (book-interest String String Bool)) (list))
(Reacts (Know (book-interest String String Bool)) (list)))))))
(Reacts (¬Know (club-member String)) (list))
(Reacts (Know (club-member String)) (list)))))
(define member-spec
(Role
'member
(list
(Shares (club-member String))
(Reacts (Know (Observe (book-interest String )))
(Role 'respond
(list
(Shares (book-interest String String Bool))))))))
(define member-actual
(Role
'member41
(list
(Shares (club-member String))
(Reacts
(Know (Observe (book-interest String )))
(Role
'during-inner42
(list
(Shares (book-interest String String Bool))
(Reacts
(¬Know (Observe (book-interest String )))
;; this bit is a noticeable deviation from the spec
(Stop 'during-inner42 '()))))))))
;; -----------------------------------------------------------------------------
;; Compiling Roles to state machines
;; a State is a (state StateName (Hashof D (Setof StateName)))
;; a StateName is a (Setof FacetName)
;; let's assume that all FacetNames are unique
;; ok, this is also ignoring Spawn actions for now, would show up in the transitions hash
(struct state (name transitions) #:transparent)
;; a FacetTree is a
;; (facet-tree (Hashof (U #f FacetName) (Listof FacetName))
;; (Hashof FacetName (U #f FacetName)))
;; describing the potential immediate children of each facet
;; and each facet's parent. The parent of the root facet is #f.
(struct facet-tree (down up) #:transparent)
;; a RoleGraph is a
;; (role-graph StateName (Hashof StateName State))
;; describing the initial state and the behavior in each state.
(struct role-graph (st0 states) #:transparent)
;; Role -> RoleGraph
;; in each state, the transitions will include the reactions of the parent
;; facet(s)
(define (compile role)
(define roles# (describe-roles role))
(define ft (make-facet-tree role))
(let loop ([work-list (list (set (Role-nm role)))]
[states (hash)])
(match work-list
[(cons current more)
(define all-txns
(for/list ([nm (in-set current)])
(hash-ref roles# nm)))
(define agg-txn
(for/fold ([agg (hash)])
([txns (in-list all-txns)])
(hash-union agg txns
#:combine combine-effect-sets)))
(define transitions
(for/hash ([(D effs) (in-hash agg-txn)])
;; TODO - may want to remove self loops here
(define destinations
(for/set ([eff* (in-set effs)])
(apply-effects eff* current ft)))
(values D destinations)))
(define new-work
(for*/list ([st-set (in-hash-values transitions)]
[st (in-set st-set)]
#:unless (equal? st current)
#:unless (hash-has-key? states st))
st))
(loop (append more new-work)
(hash-set states current (state current transitions)))]
['()
(role-graph (set (Role-nm role)) states)])))
(module+ test
(test-case
"compile seller"
(define rg (compile seller))
(check-true (role-graph? rg))
(match-define (role-graph sn0 seller#) rg)
(check-equal? sn0 (set 'seller))
(check-true (hash-has-key? seller# (set 'seller)))
(check-true (hash-has-key? seller# (set 'seller 'fulfill)))
(check-equal? (hash-keys seller#)
(list (set 'seller 'fulfill)
(set 'seller)))
(define st0 (hash-ref seller# (set 'seller)))
(define transitions (state-transitions st0))
(define quote-request
(Observe (book-quote String )))
(check-true (hash-has-key? transitions (Know quote-request)))
(check-equal? (hash-ref transitions (Know quote-request))
(set (set 'seller 'fulfill))))
(test-case
"compile role that quits"
(define r
(Role 'x
(list (Reacts (Know Int)
(Stop 'x '())))))
(define rg (compile r))
(check-true (role-graph? rg))
(match-define (role-graph sn0 state#) rg)
(check-equal? sn0
(set 'x))
(check-true (hash-has-key? state# (set)))
(check-true (hash-has-key? state# (set 'x)))
(define state0 (hash-ref state# (set 'x)))
(define transitions (state-transitions state0))
(check-true (hash-has-key? transitions (Know Int)))
(check-equal? (hash-ref transitions (Know Int))
(set (set))))
(test-case
"leader-revised should have a quote/poll cycle"
(define rg (compile leader-revised))
(check-true (role-graph? rg))
(match-define (role-graph sn0 state#) rg)
(check-true (hash? state#))
(check-true (hash-has-key? state# (set 'get-quotes)))
(define gq-st (hash-ref state# (set 'get-quotes)))
(check-true (state? gq-st))
(match-define (state _ gq-transitions) gq-st)
(define bq (Know (book-quote String Int)))
(check-true (hash? gq-transitions))
(check-true (hash-has-key? gq-transitions bq))
(define dests (hash-ref gq-transitions bq))
(check-true (set? dests))
(check-true (set-member? dests (set 'get-quotes 'poll-members)))
(check-true (hash-has-key? state# (set 'get-quotes 'poll-members)))
(define gqpm-st (hash-ref state# (set 'get-quotes 'poll-members)))
(check-true (state? gqpm-st))
(match-define (state _ gqpm-transitions) gqpm-st)
(define bi (Know (book-interest String String )))
(check-true (hash? gqpm-transitions))
(check-true (hash-has-key? gqpm-transitions bi))
(define dests2 (hash-ref gqpm-transitions bi))
(check-true (set? dests2))
(check-true (set-member? dests2 (set 'get-quotes))))
(test-case
"simplified poll should quit"
(define poll/simpl
(Role
'poll-members
(list
(Reacts
(Know (book-interest String String ))
(list
(Branch
(list
(Stop 'poll-members
(Branch (list
(Stop 'get-quotes (list))
(list))))
(list))))))))
(define transition# (describe-role poll/simpl))
(check-true (hash? transition#))
(define bi (Know (book-interest String String )))
(check-true (hash-has-key? transition# bi))
(define effs (hash-ref transition# bi))
(check-true (set? effs))
(check-true (set-member? effs (list (stop 'poll-members))))
)
(test-case
"Body->effects of simplified poll"
(define poll/simpl/body
(Stop 'poll-members
(Branch (list
(Stop 'get-quotes (list))
(list)))))
(define effs (Body->effects poll/simpl/body))
(check-true (set? effs))
(check-true (set-member? effs (list (stop 'poll-members) (stop 'get-quotes))))
(check-true (set-member? effs (list (stop 'poll-members)))))
(test-case
"Body->effects of even more simplified poll"
(define poll/simpl/body/simpl
(Branch (list
(Stop 'get-quotes (list))
(list))))
(define effs (Body->effects poll/simpl/body/simpl))
(check-true (set? effs))
(check-equal? effs
(set (list (stop 'get-quotes)) (list)))))
;; Role -> FacetTree
(define (make-facet-tree role)
(let loop (;; the work list contains pairs describing the immediate parent and a thing to analyze
[work (list (cons #f role))]
[downs (hash)]
[ups (hash)])
(match work
['()
(facet-tree downs ups)]
[(cons (cons parent T) rest)
(match T
;; TODO - handle Spawn
[(Role nm eps)
;; record the parent/child relationship
(define downs2 (hash-update downs
parent
((curry cons) nm)
(list)))
(define downs3 (hash-set downs2 nm (list)))
(define ups2 (hash-set ups nm parent))
(define more-work*
(for/list ([ep (in-list eps)]
#:when (Reacts? ep))
(match-define (Reacts _ body) ep)
(map ((curry cons) nm) (Body->actions body))))
(loop (apply append rest more-work*)
downs3
ups2)]
[(Stop target body)
(define new-parent (hash-ref ups target))
(define more-work
(for/list ([k (in-list (Body->actions body))])
(cons new-parent k)))
(loop (append rest more-work)
downs
ups)])])))
;; Body -> (Listof T)
;; extract the list of all Role, Stop, and Spawn types from a Body
(define (Body->actions body)
(match body
[(? list?)
(apply append (map Body->actions body))]
[(Branch arms)
(apply append (map Body->actions arms))]
[T (list T)]))
(module+ test
(test-case
"Body->actions Branch"
(define body (Branch
(list
(Stop 'leader
(Role 'announce
(list
(Shares (Struct 'book-of-the-month String)))))
(Stop 'poll (list)))))
(check-equal? (Body->actions body)
(list (Stop 'leader
(Role 'announce
(list
(Shares (Struct 'book-of-the-month String)))))
(Stop 'poll (list))))))
(module+ test
(test-case
"manager facet tree (one facet)"
(define ft (make-facet-tree manager))
(check-true (facet-tree? ft))
(match-define (facet-tree downs ups) ft)
(check-equal? (hash-ref downs #f)
(list 'account-manager))
(check-equal? (hash-ref downs 'account-manager)
(list))
(check-equal? (hash-ref ups 'account-manager)
#f))
(test-case
"seller facet tree (two facets)"
(define ft (make-facet-tree seller))
(check-true (facet-tree? ft))
(match-define (facet-tree downs ups) ft)
(check-equal? (hash-ref downs #f)
(list 'seller))
(check-equal? (hash-ref downs 'seller)
(list 'fulfill))
(check-equal? (hash-ref ups 'seller)
#f)
(check-equal? (hash-ref ups 'fulfill)
'seller)
(test-case
"leader-spec facet tree (stops facets)"
(define ft (make-facet-tree leader-spec))
(check-true (facet-tree? ft))
(match-define (facet-tree downs ups) ft)
(check-equal? (list->set (hash-ref downs #f))
(set 'leader 'announce))
(check-equal? (hash-ref downs 'leader)
(list 'poll))
(check-equal? (hash-ref downs 'poll)
(list))
(check-equal? (hash-ref downs 'announce)
(list))
(check-equal? (hash-ref ups 'leader)
#f)
(check-equal? (hash-ref ups 'announce)
#f)
(check-equal? (hash-ref ups 'poll)
'leader))
))
;; FacetName FacetName FacetTree -> Bool
;; determine if the first argument is a child*, or equal to, the second
(define (ancestor? desc ansc ft)
(cond
[(equal? desc ansc)
#t]
[else
(define parent (hash-ref (facet-tree-up ft) desc))
(and parent
(ancestor? parent ansc ft))]))
(module+ test
(test-case
"ancestors in leader-spec facet tree"
(define ft (make-facet-tree leader-spec))
(check-true (ancestor? 'leader 'leader ft))
(check-true (ancestor? 'poll 'leader ft))
(check-false (ancestor? 'leader 'poll ft))
(check-false (ancestor? 'announce 'leader ft))))
;; a RoleEffect is one of
;; - (start RoleName)
;; - (stop RoleName)
;; TODO - leaving out Spawn here
(struct start (nm) #:transparent)
(struct stop (nm) #:transparent)
;; a TransitionDesc is a (Hashof D (Setof (Listof RoleEffect)), describing the
;; possible ways an event (+/- of an assertion) can alter the facet tree
;; (Listof RoleEffect) StateName FacetTree -> StateName determine the state
;; resulting from some effects, given the currently active facets and a
;; description of possible facet locations.
(define (apply-effects effs st ft)
(for/fold ([st st])
([eff (in-list effs)])
(match eff
[(start nm)
(set-add st nm)]
[(stop nm)
(for/set ([f-name (in-set st)]
#:unless (ancestor? f-name nm ft))
f-name)])))
;; Role -> (Hashof FacetName TransitionDesc)
;; Extract a description of all roles mentioned in a Role
(define (describe-roles role)
(define all-roles (enumerate-roles role))
(for/hash ([r (in-list all-roles)])
(define txn (describe-role r))
(values (Role-nm r)
txn)))
;; T -> (Listof Role)
;; Find all nested role descriptions
(define (enumerate-roles t)
(match t
[(Role _ eps)
(define rs
(for*/list ([ep (in-list eps)]
#:when (Reacts? ep)
[body (in-value (Reacts-body ep))]
[act (in-list (Body->actions body))]
[role (in-list (enumerate-roles act))])
role))
(cons t rs)]
[(Stop _ body)
(for*/list ([act (in-list (Body->actions body))]
[role (in-list (enumerate-roles act))])
role)]
[(Spawn _)
(error)]))
;; Role -> TransitionDesc
;; determine how the event handlers in a role alter the facet tree
(define (describe-role role)
(match role
[(Role nm eps)
(for/fold ([txns (hash)])
([ep (in-list eps)]
#:when (Reacts? ep))
(match-define (Reacts evt body) ep)
(define effects (Body->effects body))
(cond
[(or (set-empty? effects)
(equal? effects (set '())))
txns]
[else
(define (update-effect-set existing)
(combine-effect-sets effects existing))
(hash-update txns evt update-effect-set (set))]))]))
;; (Setof (Listof X)) (Setof (Listof X)) -> (Setof (Listof X))
;; two separately analyzed sets of effects may combine in any way
(define (combine-effect-sets s1 s2)
(cond
[(set-empty? s1)
s2]
[(set-empty? s2)
s1]
[else
(for*/set ([e1* (in-set s1)]
[e2* (in-set s2)])
(append e1* e2*))]))
(module+ test
(test-case
"describe simple role"
(define desc (describe-roles manager))
(check-true (hash-has-key? desc 'account-manager))
(check-equal? (hash-ref desc 'account-manager)
(hash)))
(test-case
"describe nested role"
(define desc (describe-roles seller))
(check-true (hash-has-key? desc 'seller))
(check-true (hash-has-key? desc 'fulfill))
(check-equal? (hash-ref desc 'fulfill)
(hash))
(define seller-txns (hash-ref desc 'seller))
(define quote-request
(Observe (book-quote String )))
(check-true (hash-has-key? seller-txns (Know quote-request)))
(check-equal? (hash-ref seller-txns (Know quote-request))
(set (list (start 'fulfill)))))
(test-case
"describe-roles bug"
(define role (Role 'poll
(list
(Reacts (Know Int)
(Branch
(list (Stop 'leader (Role 'announce (list (Shares Int))))
(Stop 'poll (list))))))))
(define desc (describe-roles role))
(check-true (hash? desc))
(check-true (hash-has-key? desc 'poll))
(define txns (hash-ref desc 'poll))
(check-true (hash-has-key? txns (Know Int)))
(check-equal? (hash-ref txns (Know Int))
(set (list (stop 'leader) (start 'announce))
(list (stop 'poll)))))
(test-case
"leader-spec announce"
(define desc (describe-roles leader-spec))
(check-true (hash-has-key? desc 'announce))
(check-equal? (hash-ref desc 'announce)
(hash)))
(test-case
"leader-spec transitions from {leader,poll} to {leader}"
(define desc (describe-roles leader-spec))
(check-true (hash-has-key? desc 'poll))
(define poll-txns (hash-ref desc 'poll))
(define evt (Know (book-interest String String Bool)))
(check-true (hash-has-key? poll-txns evt))
(define effs (hash-ref poll-txns evt))
(check-true (set-member? effs (list (stop 'poll))))))
;; Body -> (Setof (Listof RoleEffect))
(define (Body->effects body)
(match body
['()
(set)]
[(cons b more)
(define fst (Body->effects b))
(define later (Body->effects more))
(cond
[(set-empty? fst)
later]
[(set-empty? later)
fst]
[else
(for*/set ([f (in-set fst)]
[l (in-set later)])
(append f l))])]
[(Branch (list b ...))
(for/fold ([agg (set)])
([b (in-list b)])
(define effs (Body->effects b))
;; it's important to remember when "do nothing" is one of the alternatives of a branch
(define effs++
(if (set-empty? effs)
(set '())
effs))
(set-union agg effs++))]
[(Role nm _)
(set (list (start nm)))]
[(Stop nm more)
(define effects (Body->effects more))
(cond
[(set-empty? effects)
(set (list (stop nm)))]
[else
(for/set ([eff* (in-set effects)])
(cons (stop nm) eff*))])]
[(Spawn _)
(error)]))
(module+ test
(test-case
"Body->effects"
(check-equal? (Body->effects '())
(set))
(check-equal? (Body->effects (Branch '()))
(set))
(check-equal? (Body->effects manager)
(set (list (start 'account-manager))))
(check-equal? (Body->effects (list manager))
(set (list (start 'account-manager))))
(check-equal? (Body->effects (Branch (list manager)))
(set (list (start 'account-manager))))
(check-equal? (Body->effects (list manager client))
(set (list (start 'account-manager)
(start 'client))))
(check-equal? (Body->effects (Branch (list manager client)))
(set (list (start 'account-manager))
(list (start 'client))))
(check-equal? (Body->effects (list manager
(Branch (list client seller))))
(set (list (start 'account-manager) (start 'client))
(list (start 'account-manager) (start 'seller)))))
(test-case
"Body->effects bug?"
(define body (Branch
(list (Stop 'leader (Role 'announce (list (Shares Int))))
(Stop 'poll (list)))))
(check-equal? (Body->effects body)
(set (list (stop 'leader) (start 'announce))
(list (stop 'poll))))))
;; ---------------------------------------------------------------------------
;; "Simulation"
;; τ τ -> Bool
;; subtyping on basic types
(define (<:? τ1 τ2)
(cond
[(eq? τ1 τ2)
#t]
[else
(match (list τ1 τ2)
[(list _ (== ))
#t]
[(list (== Int) (== Int))
#t]
[(list (== String) (== String))
#t]
[(list (== Bool) (== Bool))
#t]
[(list (U τs) _)
(for/and ([τ (in-list τs)])
(<:? τ τ2))]
[(list _ (U τs))
(for/or ([τ (in-list τs)])
(<:? τ1 τ))]
[(list (Observe τ11) (Observe τ22))
(<:? τ11 τ22)]
[(list (Struct nm1 τs1) (Struct nm2 τs2))
(and (equal? nm1 nm2)
(= (length τs1) (length τs2))
(for/and ([τ11 (in-list τs1)]
[τ22 (in-list τs2)])
(<:? τ11 τ22)))]
[_
#f])]))
;; D D -> Bool
;; subtyping lifted over event descriptions
(define (D<:? D1 D2)
(match (list D1 D2)
[(list (Know τ1) (Know τ2))
(<:? τ1 τ2)]
[(list (¬Know τ1) (¬Know τ2))
(<:? τ1 τ2)]
[_
#f]))
;; Role -> (Setof τ)
;; Compute the set of assertions the role contributes (on its own, not
;; considering parent assertions)
(define (role-assertions r)
(for/set ([ep (in-list (Role-eps r))])
(match ep
[(Shares τ)
τ]
[(Reacts evt _)
;; TODO - this doesn't put ⋆ in where an underlying pattern uses a capture
(match evt
[(Know τ)
(Observe τ)]
[(¬Know τ)
(Observe τ)])])))
;; an Equation is (equiv StateName StateName)
;;
;; a Goal is one of
;; - Equation
;; - (one-of (Setof StateMatch))
;;
;; a StateMatch is a (Setof Equation)
(struct equiv (a b) #:transparent)
(struct one-of (opts) #:transparent)
;; (Setof StateName) (Setof StateName) -> (Setof (Setof Equation))
;; Create potential state matchings
;; In each state matching, each element a of the first set (as) is
;; matched with an element b of bs, where each b has at least one state
;; matched with it.
(define (make-combinations as bs)
(define combos (make-combinations* as bs))
(define (all-bs? combo)
(for/and ([b (in-set bs)])
(for/or ([eqn (in-set combo)])
(match-define (equiv _ bb) eqn)
(equal? b bb))))
(for/set ([combo (in-set combos)]
#:when (all-bs? combo))
combo))
;; (Setof StateName) (Setof StateName) -> (Setof (Setof Equation))
;; Like make-combinations, but don't enforce that each b occurs at
;; least once in each combination
(define (make-combinations* as bs)
(cond
[(= (set-count as) 1)
(for*/set ([a (in-value (set-first as))]
[b (in-set bs)])
(set (equiv a b)))]
[else
(for*/fold ([agg (set)])
([a (in-set as)]
[b (in-set bs)])
(define combos (make-combinations* (set-remove as a) bs))
(define combos+
(for/set ([c (in-set combos)])
(set-add c (equiv a b))))
(set-union agg combos+))]))
;; Role Role -> Bool
;; determine if the first role acts suitably like the second role.
;; at all times, it is asserting a superset of the second's assertions
;; role1 ~ actual
;; role2 ~ spec
(define (simulates? role1 role2)
(match-define (role-graph st0-1 st#1) (compile role1))
(match-define (role-graph st0-2 st#2) (compile role2))
(define ft1 (make-facet-tree role1))
(define ft2 (make-facet-tree role2))
(define all-roles1 (enumerate-roles role1))
(define all-roles2 (enumerate-roles role2))
(define assertion#1
(for/hash ([role (in-list all-roles1)])
(values (Role-nm role)
(role-assertions role))))
(define assertion#2
(for/hash ([role (in-list all-roles2)])
(values (Role-nm role)
(role-assertions role))))
(define state-assertions1
(for/hash ([sn (in-hash-keys st#1)])
(values sn
(for/fold ([assertions (set)])
([facet-name (in-set sn)])
(set-union assertions (hash-ref assertion#1 facet-name (set)))))))
(define state-assertions2
(for/hash ([sn (in-hash-keys st#2)])
(values sn
(for/fold ([assertions (set)])
([facet-name (in-set sn)])
(set-union assertions (hash-ref assertion#2 facet-name (set)))))))
;; Goal (Setof Equation) -> Bool
(define (verify goal assumptions)
(let/ec return
(match goal
[(equiv sn1 sn2)
(when (set-member? assumptions goal)
(return #t))
(define assertions1 (hash-ref state-assertions1 sn1))
(define assertions2 (hash-ref state-assertions2 sn2))
(define superset?
(for/and ([assertion2 (in-set assertions2)])
(for/or ([assertion1 (in-set assertions1)])
(<:? assertion1 assertion2))))
(unless superset?
(return #f))
(define transitions1 (state-transitions (hash-ref st#1 sn1)))
(define transitions2 (state-transitions (hash-ref st#2 sn2)))
(define evts1 (hash-keys transitions1))
(define evts2 (hash-keys transitions2))
(define same-on-specified-events?
(for/and ([(D dests2) (in-hash transitions2)])
(define dests1
(for/fold ([agg (set)])
([(D1 dests) (in-hash transitions1)]
#:when (D<:? D D1))
(set-union agg dests)))
(unless dests1
(return #f))
(define combos (make-combinations dests1 dests2))
(verify (one-of combos) (set-add assumptions goal))))
(unless same-on-specified-events?
(return #f))
(define extra-evts
(for/set ([evt1 (in-list evts1)]
#:unless (for/or ([evt2 (in-list evts2)])
(D<:? evt2 evt1)))
evt1))
(define same-on-extra-evts?
(for*/and ([evt (in-set extra-evts)]
[dest (in-set (hash-ref transitions1 evt))])
(verify (equiv dest sn2)
(set-add assumptions goal))))
same-on-extra-evts?]
[(one-of matchings)
(for/or ([matching (in-set matchings)])
(for/and ([goal (in-set matching)])
(define hypotheses (set-remove matching goal))
(verify goal (set-union hypotheses assumptions))))])))
(verify (equiv st0-1 st0-2) (set)))
(module+ test
(test-case
"simplest simul"
(define r (Role 'x (list)))
(check-true (simulates? r r)))
(test-case
"identity simulation"
(check-true (simulates? manager manager))
(check-true (simulates? client client))
(check-true (simulates? seller seller)))
(test-case
"simulation isn't vacuous"
(check-false (simulates? manager client))
(check-false (simulates? client manager))
(check-false (simulates? manager seller))
(check-false (simulates? seller manager))
(check-false (simulates? client seller))
(check-false (simulates? seller client)))
(test-case
"leader-spec identity simulation"
(check-true (simulates? leader-spec leader-spec)))
(test-case
"things aren't quite right with leader-actual"
(check-false (simulates? leader-actual leader-spec))
(check-true (simulates? leader-fixed? leader-spec)))
(test-case
"things aren't quite right with leader-revised"
(check-false (simulates? leader-revised leader-spec)))
(test-case
"things aren't quite right with member role"
(check-false (simulates? member-actual member-spec))
(define member-actual/revised
(Role
'member41
(list
(Shares (club-member String))
(Reacts
(Know (Observe (book-interest String )))
(Role
'during-inner42
(list
(Shares (book-interest String String Bool))
(Reacts
(¬Know (Observe (book-interest String )))
;; removed (Stop 'during-inner42 '()) here
'())))))))
(check-true (simulates? member-actual/revised member-spec)))
(test-case
"things aren't quite right with seller role"
(check-false (simulates? seller-actual seller))
(define seller-spec/revised
(Role 'seller
;; change body to a During
(list
(During (Observe (book-quote String ))
(list (Shares (book-quote String Int)))))))
(check-true (simulates? seller-actual seller-spec/revised))))
;; ---------------------------------------------------------------------------
;; Visualization
(module+ vis
;; TODO - for now, assume there are no names that need escaping
;; RoleGraph -> DotString
;; name is an optional string
;; translate the states to DOT that can be passed to graphviz
(define (render rg
#:name [name #f])
(match-define (role-graph st0 st#) rg)
(define graph-name (or name "Roles"))
(define entry-node (format "~a;" (state-name->dot-name st0)))
(define edges
(for/list ([(sn st) (in-hash st#)])
(define dot-name (state-name->dot-name sn))
(define txns (state-transitions st))
(define dot-edges
(for*/list ([(D targets) (in-hash txns)]
[target (in-set targets)])
(render-edge dot-name D target)))
(string-join dot-edges "\n")))
(string-join (cons entry-node edges)
"\n"
#:before-first (format "digraph ~a {\n" graph-name)
#:after-last "\n}"))
;; RoleGraph PathString -> DotString
;; Like render but write the output to a file
(define (render-to-file rg dest
#:name [name #f])
(with-output-to-file dest
(lambda () (write-string (render rg #:name name)))
#:exists 'replace))
;; StateName -> String
(define (state-name->dot-name sn)
(define nms
(for/list ([nm (in-set sn)])
(~a nm)))
(string-join nms ","
#:before-first "\"{"
#:after-last "}\""))
;; String D StateName -> DotString
;; describe an edge between the states with the corresponding label
(define (render-edge from evt to)
(define target-dot (state-name->dot-name to))
(define edge-label (D->label evt))
(format "~a -> ~a [label=\"~a\"];" from target-dot edge-label))
;; D -> DotString
;; give a description of an event suitable for rendering
(define (D->label evt)
(match evt
[(Know τ)
(string-append "+" (τ->string τ))]
[(¬Know τ)
(string-append "-" (τ->string τ))]))
;; - (U (Listof τ))
;; - (Struct StructName (Listof τ ...))
;; - (Observe τ)
;; - ⋆
;; - Int
;; - String
;; τ -> String
(define (τ->string τ)
(match τ
[(== Int) "Int"]
[(== String) "String"]
[(== Bool) "Bool"]
[(== ) ""]
[(Observe τ2)
(string-append "?" (τ->string τ2))]
[(Struct nm τs)
(define slots (string-join (map τ->string τs) " "))
(string-append "("
(~a nm)
(if (empty? slots) "" " ")
slots
")")]
[(U τs)
(define slots (string-join (map τ->string τs) " "))
(string-append "(U"
slots
")")]))
)