syndicate-2017/racket/typed/proto.rkt

1374 lines
46 KiB
Racket
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#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
;; - StartEvt, reaction to facet startup
;; - StopEvt, reaction to facet shutdown
(struct Know (ty) #:transparent)
(struct ¬Know (ty) #:transparent)
(define StartEvt 'Start)
(define StopEvt 'Stop)
;; a τ is one of
;; - (U (Listof τ))
;; - (Struct StructName (Listof τ ...))
;; - (Observe τ)
;; - ⋆
;; - (Base Symbol)
(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 Base (name) #:transparent)
(define Int (Base 'Int))
(define String (Base 'String))
(define Bool (Base 'Bool))
(define Symbol (Base 'Symbol))
;; 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 'BookQuoteT (list ty1 ty2)))
;; τ τ τ -> τ
;; short hand for creating a book quote interest type
(define (book-interest ty1 ty2 ty3)
(Struct 'BookInterestT (list ty1 ty2 ty3)))
;; τ -> τ
;; short hand for creating a book of the month type
(define (book-of-the-month ty)
(Struct 'BookOfTheMonthT (list ty)))
;; τ -> τ
;; short hand for creating a club member type
(define (club-member ty)
(Struct 'ClubMemberT (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)))
;; where each D in the hash satisfies external-evt?
;; 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)]
#:when (external-evt? D))
;; TODO - may want to remove self loops here
(define destinations
(for*/set ([eff* (in-set effs)]
[dst (in-set (apply-effects eff* current ft roles#))])
dst))
(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)])))
;; D -> Bool
;; test if D corresponds to an external event (assertion, message)
(define (external-evt? D)
(or (Know? D) (¬Know? D)))
(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))]))
;; FacetName FacetName FacetTree -> (U #f Nat)
;; determine if the first argument is a child*, or equal to, the second; if so,
;; return their distance from one another in the tree
(define (ancestor?/dist desc ansc ft)
(cond
[(equal? desc ansc)
0]
[else
(define parent (hash-ref (facet-tree-up ft) desc))
(define ans? (and parent (ancestor?/dist parent ansc ft)))
(and ans?
(add1 ans?))]))
(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)))
(test-case
"ancestor?/dist in leader-spec facet tree"
(define ft (make-facet-tree leader-spec))
(check-equal? (ancestor?/dist 'leader 'leader ft) 0)
(check-equal? (ancestor?/dist 'poll 'leader ft) 1)
(check-false (ancestor?/dist 'leader 'poll ft))
(check-false (ancestor?/dist '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.
;; It always includes the keys StartEvt and StopEvt.
(define txn-desc0 (hash StartEvt (set) StopEvt (set)))
;; (Listof RoleEffect) StateName
;; FacetTree
;; (Hashof FacetName TransitionDesc)
;; -> (Setof StateName)
;; determine the state resulting from some effects, given the currently active
;; facets and a description of possible facet locations and behavior.
(define (apply-effects effs st ft txn#)
#;(printf "apply-effects: ~a\n" effs)
(let loop ([st st]
[effs effs])
(match effs
['()
(set st)]
[(cons eff rest)
(match eff
[(start nm)
(define st+ (set-add st nm))
(define start-effs (hash-ref (hash-ref txn# nm) StartEvt))
(cond
[(set-empty? start-effs)
(loop st+ rest)]
[else
(for/set ([eff* (in-set start-effs)])
(loop st+ (append rest eff*)))])]
[(stop nm)
;; better include nm
(define children (find-children ft nm st))
(define st-
(for/fold ([st st])
([c (in-list children)])
(set-remove st c)))
(for/fold ([sts (set st-)])
([f-name (in-list children)])
(define stop-effs (hash-ref (hash-ref txn# f-name) StopEvt))
(cond
[(set-empty? stop-effs)
(for*/set ([st (in-set sts)]
[result (in-set (loop st rest))])
result)]
[else
(for*/set ([st (in-set sts)]
[effs* (in-set stop-effs)]
[result (in-set (loop st (append rest effs*)))])
result)]))])])))
;; FacetTree FacetName (Setof FacetName) -> (List FacetName)
;; return the facets in names that are children of the given facet nm, ordered
;; by their distance (farthest children first etc.)
(define (find-children ft nm names)
(define relations
(for*/list ([n (in-set names)]
[ans? (in-value (ancestor?/dist n nm ft))]
#:when ans?)
(list n ans?)))
(define farthest-to-nearest (sort relations > #:key second))
(map first farthest-to-nearest))
;; 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 txn-desc0])
([ep (in-list eps)]
#:when (Reacts? ep))
(match-define (Reacts evt body) ep)
(define effects (Body->effects body))
(when (equal? StopEvt evt)
;; facets that start inside a stop handler will get shutdown.
(define effects+
(for/set ([effs* (in-set effects)])
(define extra-stops
(for/list ([eff (in-list effs*)]
#:when (start? eff))
(stop (start-nm eff))))
(append effs* extra-stops)))
(set! effects effects+))
(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)
txn-desc0))
(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)
txn-desc0)
(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)
txn-desc0))
(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 (Base t1) (Base t2))
(equal? t1 t2)]
[(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 τ))]
[(== StartEvt)
"Start"]
[(== StopEvt)
"Stop"]))
;; - (U (Listof τ))
;; - (Struct StructName (Listof τ ...))
;; - (Observe τ)
;; - ⋆
;; - Int
;; - String
;; τ -> String
(define (τ->string τ)
(match τ
[(Base name)
(symbol->string name)]
[(== ) ""]
[(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
")")]))
)
;; ---------------------------------------------------------------------------
;; Flink Examples
(define job-manager-actual
'(Role
(jm89)
(Shares (JobManagerAlive))
(Reacts
(Know (Job (Bind Symbol) (Bind (List- InputTask))))
(Role (during-inner97)
(Reacts OnDataflow
(Role (perform114)
(Reacts OnStart
(Role (this-facet123)
(Reacts OnDataflow
(Branch
(Effs (Branch (Effs
(Role (this-facet120)
(Shares (TaskAssignment ID Symbol ConcreteTask))
(Reacts (Know (TaskState ID Symbol Int (Bind (U* (Finished TaskResult) Symbol))))
(Branch (Effs) (Effs)
(Effs (Stop this-facet))
;; TODO - why is there a ρ here?
(Effs (Stop perform ρ))))
(Reacts OnStart
(Role (take-slot121)
(Reacts (Know (TaskState ID Symbol Int Discard))
(Stop take-slot))))
(Reacts (¬Know (TaskManager ID Discard))
(Stop this-facet))))
(Effs)))
(Effs)))))
(Reacts OnStop)
(Reacts OnStart)))
(Reacts (¬Know (Job Symbol (List- InputTask)))
(Stop during-inner))))
(Reacts (¬Know (TaskManager (Bind Symbol) (Bind Int))))
(Reacts (Know (TaskManager (Bind Symbol) (Bind Int))))))
;; ---------------------------------------------------------------------------
;; Converting types from the turnstile implementation
;; QuotedType -> T
(define (parse-T ty)
(match ty
[(list 'Role (list name) eps ...)
(define parsed-eps (map parse-EP eps))
(Role name parsed-eps)]
[(list 'Spawn t)
(Spawn (parse-T t))]
[(list 'Stop name body ...)
(define bdy (if (= (length body) 1)
(first body)
body))
(Stop name (parse-Body bdy))]
))
;; Sexp -> EP
(define (parse-EP ep)
(match ep
[(list 'Shares ty)
(define parsed-ty (parse-τ ty))
(Shares parsed-ty)]
[(list 'Reacts D b ...)
(define bdy (if (= (length b) 1)
(first b)
(cons 'Effs b)))
(Reacts (parse-D D) (parse-Body bdy))]))
(define (parse-Body b)
(match b
[(list 'Branch bs ...)
(Branch (map parse-Body bs))]
[(list 'Effs bs ...)
(list (map parse-Body bs))]
[(list)
(list)]
[_
(parse-T b)]))
(define (parse-D d)
(match d
[(list 'Know t)
(Know (parse-τ t))]
[(list '¬Know t)
(¬Know (parse-τ t))]
['OnStart
StartEvt]
['OnStop
StopEvt]))
;; Sexp -> τ
(define (parse-τ ty)
(match ty
[(list 'Observe t)
(Observe (parse-τ t))]
['★/t
]
[(list (or 'U* 'U) t ...)
(U (map parse-τ t))]
[(list 'Bind t)
;; TODO : questionable
(parse-τ t)]
['Discard
]
[(list struct-name tys ...)
(Struct struct-name (map parse-τ tys))]
[(? symbol?)
(Base ty)])
)
(module+ test
(check-equal? (parse-T '(Stop during-inner))
(Stop 'during-inner (list)))
(test-case
"real seller type"
(check-true (Role? (parse-T real-seller-ty))))
(test-case
"Stop with a single continuation effect"
(check-true (Stop? (parse-T '(Stop poll-members
(Branch (Effs (Stop get-quotes)) (Effs)))))))
(test-case
"parsed types are the same as my manual conversions"
(check-true (simulates? (parse-T real-seller-ty) seller-actual))
(check-true (simulates? seller-actual (parse-T real-seller-ty)))
(check-true (simulates? (parse-T real-member-ty) member-actual))
(check-true (simulates? member-actual (parse-T real-member-ty)))
(check-true (simulates? (parse-T real-leader-ty) leader-actual))
;; interestingly, this doesn't work because leader-actual is less precise
;; than real-leader-ty (I don't remember how leader-actual lost that
;; precision)
#;(check-true (simulates? leader-actual (parse-T real-leader-ty)))
(check-true (simulates? (parse-T real-leader-ty) leader-revised))
(check-true (simulates? leader-revised (parse-T real-leader-ty)))))
(define real-seller-ty
'(Role
(seller)
(Reacts
(Know (Observe (BookQuoteT (Bind String) Discard)))
(Role
(during-inner)
(Shares (BookQuoteT String Int))
(Reacts
(¬Know (Observe (BookQuoteT String Discard)))
(Stop during-inner))))))
(define real-member-ty
'(Role
(member)
(Shares (ClubMemberT String))
(Reacts
(Know (Observe (BookInterestT (Bind String) Discard Discard)))
(Role
(during-inner)
(Shares (BookInterestT String String Bool))
(Reacts
(¬Know (Observe (BookInterestT String Discard Discard)))
(Stop during-inner))))))
(define real-leader-ty
'(Role
(get-quotes)
(Reacts
(Know (BookQuoteT String (Bind Int)))
(Branch
(Effs (Branch (Effs (Stop get-quotes)) (Effs)))
(Effs
(Role
(poll-members)
(Reacts
(Know (BookInterestT String (Bind String) Discard))
(Branch
(Effs (Stop poll-members (Branch (Effs (Stop get-quotes)) (Effs))))
(Effs))
(Branch
(Effs
(Stop get-quotes (Role (announce) (Shares (BookOfTheMonthT String)))))
(Effs)))
(Reacts (¬Know (BookInterestT String (Bind String) Bool)))
(Reacts (Know (BookInterestT String (Bind String) Bool)))
(Reacts (¬Know (BookInterestT String (Bind String) Bool)))
(Reacts (Know (BookInterestT String (Bind String) Bool)))))))
(Reacts (¬Know (ClubMemberT (Bind String))))
(Reacts (Know (ClubMemberT (Bind String))))))