syndicate-2017/racket/syndicate/little-actors/core.rkt

693 lines
23 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 syndicate/monolithic)
(require syndicate/trie)
(require racket/set)
(module+ test
(require rackunit))
(define mt-scn (scn trie-empty))
;; an `exp` is either
;; ('lambda (var ...) exp) or
;; (exp exp ...) or
;; ('begin exp ...) or
;; ('let (var exp) exp) or
;; ('if exp exp exp) or
;; ('send! exp) or
;; ('react O ...) or
;; ('actor ('react O ...)) or
;; ('dataspace actor ...) or
;; ('outbound exp) or
;; ('inbound exp) or
;; ('set! var exp) or
;; ('read var) or
;; ('list exp ...) or
;; (primop exp ...) or
;; atom
;; a `val` is either
;; atom or
;; ('list val ...) or
;; (outbound val) or
;; (inbound val) or
;; (closure Γ ('lambda (var ...) exp))
(struct closure (env fun) #:transparent)
;; `primop` is one of
;; + * / - and or not equal? null? car cdr printf
;; an `O` (endpoint) is either
;; ('field var exp) or
;; ('assert exp) or
;; ('on E exp ...) or
;; ('stop-when E exp ...) or
;; ('on-start exp ...)
;; a `facet` is ('react O ...)
;; an `actor` is
;; ('actor facet) or
;; ('dataspace actor ...)
;; an E is either
;; ('asserted pat) or
;; ('retracted pat) or
;; (message pat)
;; a pat is either
;; $var or
;; _ or
;; exp or
;; ('observe pat) or
;; ('inbound pat) or
;; ('outbound pat) or
;; ('list pat ...)
;; a Γ is a (Listof Binding)
;; a Binding is (binding var val)
(struct binding (id v) #:transparent)
;; a σ is either
;; (Hashof var val) or
;; (store-concat σ σ)
(struct store-concat (σ1 σ2) #:transparent)
;; σ1 is the "parent" to σ2 (local)
;; a FacetTree is (facet-tree '(react O ...) Γ σ (Listof FacetTree))
(struct facet-tree (stx env sto children) #:transparent)
;; an ActorState is (actor-state π FacetTree)
(struct actor-state (π ft) #:transparent)
;; a π is a trie
(define π-union assertion-set-union)
;; a Program is a (Listof actor)
(define mt-Γ (list))
(define mt-σ (hash))
;; env-lookup : Γ var -> val
;; or throws an error for unbound variables
(define (env-lookup Γ id)
(match Γ
['() (error 'env-lookup "unbound variable: ~v" id)]
[(cons (binding x v) rest)
(if (equal? id x)
v
(env-lookup rest id))]))
;; extend-env : Γ var val -> Γ
(define (extend-env Γ id v)
(cons (binding id v) Γ))
;; update-sto : σ var val -> σ
;; update the value of var in the store, if present.
;; otherwise throw an error
(define (update-sto σ id v)
(let search ([σ σ]
[k-succ identity]
[k-fail (lambda () (error 'update-sto "unbound field: ~v" id))])
(match σ
[(store-concat σ1 σ2)
(search σ2
(lambda (new-σ2) (k-succ (store-concat σ1 new-σ2)))
(lambda () (search σ1
(lambda (new-σ1) (k-succ (store-concat new-σ1 σ2)))
k-fail)))]
[_
(if (hash-has-key? σ id)
(k-succ (hash-set σ id v))
(k-fail))])))
(module+ test
(let* ([s1 (make-store '(balance . 100))]
[s2 (store-concat mt-σ s1)]
[s3 (store-concat s1 mt-σ)])
(check-equal? (update-sto s2 'balance 50)
(store-concat mt-σ (make-store '(balance . 50))))
(check-equal? (update-sto s3 'balance 50)
(store-concat (make-store '(balance . 50)) mt-σ))))
(define (primop? x)
(member x '(+ - * / - and or not equal? null? car cdr printf)))
;; apply-primop : primop (Listof val) -> val
(define (apply-primop op args)
(match* (op args)
[('+ `(,v1 ,v2))
(+ v1 v2)]
[('- `(,v1 ,v2))
(- v1 v2)]
[('* `(,v1 ,v2))
(* v1 v2)]
[('/ `(,v1 ,v2))
(/ v1 v2)]
[('and `(,v1 ,v2))
(and v1 v2)]
[('or `(,v1 ,v2))
(and v1 v2)]
[('equal? `(,v1 ,v2))
(equal? v1 v2)]
[('not `(,v))
(not v)]
[('null? '(list))
#t]
[('null? _)
#f]
[('car `(list ,e ,es ...))
e]
[('cdr `(list ,e ,es ...))
es]
[('printf args)
(apply printf args)]
[(_ _)
(error 'apply-primop "invalid primitive application: ~v ~v" op args)]))
;; sto-fetch : σ var -> val
;; retrieve the value of field var.
;; if not present throw an error
(define (sto-fetch σ id)
(let search ([σ σ]
[k (lambda () (error 'update-sto "unbound field: ~v" id))])
(match σ
[(store-concat σ1 σ2)
(search σ2 (lambda () (search σ1 k)))]
[_
(if (hash-has-key? σ id)
(hash-ref σ id)
(k))])))
;; make-store : (Listof (cons var val)) -> σ
(define (make-store . vs)
(make-immutable-hash vs))
;; A (Continue A) is (continue A σ (Listof Action) (Listof FacetTree))
(struct continue (v sto as fs) #:transparent)
;; A Stop is (stop σ (Listof Action))
(struct stop (sto as) #:transparent)
;; A (Result A) is a Stop or (Continue A)
;; result-bind : Result (Any σ Any ... -> Result) Any ... -> Result
(define (result-bind r f . extra-args)
(match r
[(continue v σ as fs)
(match (apply f (cons v (cons σ extra-args)))
[(continue next-v next-σ more-as more-fs)
(continue next-v next-σ (append as more-as) (append fs more-fs))]
[s s])]
[s s]))
;; result-map : (Result A) (A -> B) -> (Result B)
(define (result-map f r)
(match r
[(continue v σ as fs)
(continue (f v) σ as fs)]
[s s]))
;; sequence-steps : Any σ (Listof (Any σ -> Result)) -> Result
(define (sequence-steps v σ steps)
(for/fold ([r (inj-result v σ)])
([s (in-list steps)])
(result-bind r s)))
;; inj-result : A σ -> (Continue A)
(define (inj-result v σ)
(continue v σ (list) (list)))
;; for-steps : A σ (Sequenceof B) (A σ B -> (Result A)) -> (Result A)
(define (for-steps v σ seq f)
(for/fold ([r (inj-result v σ)])
([x seq])
(result-bind r f x)))
;; boot-facet : facet Γ σ -> (Values σ (Listof Action) FacetTree)
(define (boot-facet f Γ σ)
(define initial-sto (initial-store f Γ σ))
(match-define (continue _ (store-concat parent-sto facet-sto) as fs)
(eval-start-actions f Γ (store-concat σ initial-sto)))
(values parent-sto as (facet-tree f Γ facet-sto fs)))
;; initial-store : facet Γ σ -> σ
;; only bad people would put effects here.
(define (initial-store f Γ σ)
(match-define `(react ,O ...) f)
(define locations
(for/fold ([locations (list)])
([o (in-list O)])
(match o
[`(field ,id ,exp)
(match-define (continue v _ _ _) (eval-exp exp Γ σ))
(cons (cons id v) locations)]
[_ locations])))
(apply make-store locations))
;; eval-start-actions : facet Γ σ -> (Continue #f)
(define (eval-start-actions f Γ σ)
(match-define `(react ,O ...) f)
(for-steps #f σ (in-list O)
(lambda (_ σ o)
(match o
[`(on-start ,exp ...)
(eval-exp* exp Γ σ)]
[_
(inj-result #f σ)]))))
;; boot-actor : actor Γ -> Action
(define (boot-actor a Γ)
(match a
[`(actor ,facet)
(define-values (_ as ft) (boot-facet facet Γ mt-σ))
(define assertions (ft-assertions ft mt-σ))
(spawn actor-behavior
(actor-state trie-empty ft)
(cons (scn assertions) as))]
[`(dataspace ,as ...)
(define boot-actions (for/list ([a (in-list as)])
(boot-actor a Γ)))
(spawn-dataspace boot-actions)]))
;; eval-exp : exp Γ σ -> (Continue val)
(define (eval-exp e Γ σ)
(match e
[`(react ,O ...)
(define-values (new-sto as ft) (boot-facet e Γ σ))
(continue (void) new-sto as (list ft))]
[`(actor ,_)
;; don't pass in parent store
(define spawn-action (boot-actor e Γ))
(continue (void) σ (list spawn-action) (list))]
[`(dataspace ,actors ...)
(continue (void) σ (list (boot-actor e Γ)) (list))]
[`(outbound ,exp)
(match-define (continue v new-sto as facets) (eval-exp exp Γ σ))
(continue (outbound v) σ as facets)]
[`(inbound ,exp)
(match-define (continue v new-sto as facets) (eval-exp exp Γ σ))
(continue (inbound v) σ as facets)]
[(? symbol? id)
(let ([v (env-lookup Γ id)])
(continue v σ (list) (list)))]
[`(lambda (,vars ...) ,exp)
(define c (closure Γ e))
(continue c σ (list) (list))]
[`(begin ,es ...)
(for-steps (void) σ (in-list es)
(lambda (v σ e) (eval-exp e Γ σ)))]
[`(list ,es ...)
(define res (for-steps (list) σ (in-list es)
(lambda (rev-vs σ e)
(result-map (lambda (v) (cons v rev-vs))
(eval-exp e Γ σ)))))
(result-map (lambda (rev-vs) (cons 'list (reverse rev-vs)))
res)]
[`(let (,x ,exp) ,body-exp)
(result-bind (eval-exp exp Γ σ)
(lambda (v new-sto)
(define new-Γ (extend-env Γ x v))
(eval-exp body-exp new-Γ new-sto)))]
[`(if ,pred-exp ,then-exp ,else-exp)
(result-bind (eval-exp pred-exp Γ σ)
(lambda (v new-sto)
(if v
(eval-exp then-exp Γ new-sto)
(eval-exp else-exp Γ new-sto))))]
[`(send! ,exp)
(match-define (continue v new-sto as facets) (eval-exp exp Γ σ))
(continue (void) new-sto (append as (list (message v))) facets)]
[`(set! ,id ,exp)
(match-define (continue v new-sto as facets) (eval-exp exp Γ σ))
(define result-sto (update-sto new-sto id v))
(continue (void) result-sto as facets)]
[`(read ,id)
(define v (sto-fetch σ id))
(continue v σ (list) (list))]
[`(,primop ,exp ..1)
#:when (primop? primop)
(result-bind (eval-exp* exp Γ σ)
(lambda (arg-vs new-sto)
(inj-result (apply-primop primop arg-vs) σ)))]
[`(,f-exp ,exps ...)
(result-bind (eval-exp f-exp Γ σ)
(lambda (f-v new-sto)
(unless (closure? f-v) (error 'eval-exp "tried to apply non-function ~v" f-v))
(result-bind (eval-exp* exps Γ σ)
(lambda (arg-vs final-sto)
(match-define (closure clo-env `(lambda (,vars ...) ,body-exp)) f-v)
(unless (= (length arg-vs) (length vars))
(error 'eval-exp "wrong number of arguments; expected ~v, got ~v" (length vars) (length arg-vs)))
(define new-env (append (map binding vars arg-vs) clo-env))
(eval-exp body-exp new-env final-sto)))))]
[x (continue x σ (list) (list))]))
(module+ test
;; sequencing result
(match-let ([(continue v s as f) (eval-exp `(begin 1 2 3) mt-Γ mt-σ)])
(check-equal? v 3))
;; variable lookup
(match-let ([(continue v s as f) (eval-exp 'x (list (binding 'x "hello")
(binding 'y "bye")
(binding 'x "world"))
mt-σ)])
(check-equal? v "hello"))
;; variable binding
(match-let ([(continue v s as f) (eval-exp '(let (y 12) "cake") mt-Γ mt-σ)])
(check-equal? v "cake"))
(match-let ([(continue v s as f) (eval-exp '(let (y 12) y) mt-Γ mt-σ)])
(check-equal? v 12))
;; if
(match-let ([(continue v s as f) (eval-exp '(if #f 5 6) mt-Γ mt-σ)])
(check-equal? v 6))
(match-let ([(continue v s as f) (eval-exp '(if #t 5 6) mt-Γ mt-σ)])
(check-equal? v 5))
;; send!
(match-let ([(continue v s as f) (eval-exp '(send! 5) mt-Γ mt-σ)])
(check-equal? as (list (message 5)))
(check-true (void? v)))
;; set!
(match-let ([(continue v s as f) (eval-exp '(set! x 12) mt-Γ (make-store '(x . "hello")))])
(check-true (void? v))
(check-equal? (hash-ref s 'x) 12))
(match-let ([(continue v s as f) (eval-exp '(begin (set! x (+ 1 (read x)))
(set! x (+ 1 (read x)))
(set! x (+ 1 (read x)))
(set! x (+ 1 (read x)))
(read x))
mt-Γ (make-store '(x . 0)))])
(check-equal? v 4))
;; store read
(match-let ([(continue v s as f) (eval-exp '(read x) mt-Γ (make-store '(y . 5) '(x . "hello")))])
(check-equal? v "hello"))
(match-let ([(continue v s as f) (eval-exp '(+ (- 5 1) (/ 4 (if (not #t) 1 2))) mt-Γ mt-σ)])
(check-equal? v 6))
;; lambda
(match-let ([(continue v s as f) (eval-exp '(let (f (lambda (x) (+ x 1))) (f 3)) mt-Γ mt-σ)])
(check-equal? v 4))
(match-let ([(continue v s as f) (eval-exp '(let (z 12)
(let (f (lambda (x) (+ x z)))
(let (z 4)
(f 3)))) mt-Γ mt-σ)])
(check-equal? v 15))
(match-let ([(continue v s as f) (eval-exp '(let (f (lambda () (actor (react (assert 5)))))
(f)) mt-Γ mt-σ)])
(check-false (empty? as))))
;; dollar-id? : any -> bool
;; test if the input is a symbol whose first character is $
(define (dollar-id? s)
(and (symbol? s)
(char=? (string-ref (symbol->string s) 0) #\$)))
;; undollar s : dollar-id? -> var
(define (undollar s)
(string->symbol (substring (symbol->string s) 1)))
;; eval-pat : pat Γ σ -> meta-pattern
;; technically this results in a Projection because it includes captures
;; if you put effects in your pattern then you deserve bad things
(define (eval-pat pat Γ σ)
(match pat
[`(list ,pats ...)
(cons 'list
(for/list ([p (in-list pats)])
(eval-pat p Γ σ)))]
[`(observe ,pat)
(observe (eval-pat pat Γ σ))]
[`(inbound ,pat)
(inbound (eval-pat pat Γ σ))]
[`(outbound ,pat)
(outbound (eval-pat pat Γ σ))]
[(? dollar-id? s)
(?!)]
['_ ?]
[exp
(match-define (continue v _ _ _) (eval-exp exp Γ σ))
v]))
;; pat-bindings : pat -> (Listof var)
(define (pat-bindings pat)
(match pat
[`(list ,pats ...)
(flatten (for/list ([p (in-list pats)])
(pat-bindings p)))]
[`(observe ,pat)
(pat-bindings pat)]
[`(inbound ,pat)
(pat-bindings pat)]
[`(outbound ,pat)
(pat-bindings pat)]
[(? dollar-id? s)
(list (undollar s))]
[_ (list)]))
(module+ test
(check-equal? (pat-bindings '(list hello $x world 5))
(list 'x)))
;; pat-matches : pat Γ σ π -> (Listof Γ)
;; evaluate the pattern and then project matching assertions
;; out of the given trie.
(define (pat-matches pat Γ σ π)
(define concrete-pat (eval-pat pat Γ σ))
(define bindings (pat-bindings pat))
(define s? (trie-project/set #:take (projection-arity concrete-pat) π concrete-pat))
(unless s? (error 'pat-matches "pattern resulted in an infinite set: ~v" pat))
(for/list ([captures (in-set s?)])
(map binding bindings captures)))
;; E-pat : E -> pat
(define E-pat second)
;; occurrences : E Event π Γ σ -> (Listof Γ)
(define (occurrences E e π-old Γ σ)
(define pat (E-pat E))
(match* (E e)
[(`(message ,_) (message v))
(pat-matches pat Γ σ (assertion v))]
[(`(asserted ,_) (scn π-new))
(define candidates (trie-subtract π-new π-old))
(pat-matches pat Γ σ candidates)]
[(`(retracted ,_) (scn π-new))
(define candidates (trie-subtract π-old π-new))
(pat-matches pat Γ σ candidates)]
[(_ _) (list)]))
(module+ test
(check-equal? (occurrences `(asserted 5) (scn (assertion 5)) trie-empty mt-Γ mt-σ)
(list (list)))
(check-equal? (occurrences `(retracted 5) (scn (assertion 5)) trie-empty mt-Γ mt-σ)
(list))
(check-equal? (occurrences `(retracted 5) (scn trie-empty) (assertion 5) mt-Γ mt-σ)
(list (list)))
(check-equal? (occurrences `(asserted 5) (message 5) trie-empty mt-Γ mt-σ)
(list))
(check-equal? (occurrences `(asserted (list "price" $x))
(scn (assertion '(list "price" 12)))
(assertion '(list "price" 5))
mt-Γ mt-σ)
(list (list (binding 'x 12))))
(check-equal? (list->set
(occurrences `(asserted (list "price" $x))
(scn (π-union (assertion '(list "price" 12)) (assertion '(list "price" 16))))
(assertion '(list "price" 5))
mt-Γ mt-σ))
(set (list (binding 'x 12)) (list (binding 'x 16)))))
;; subscription : E Γ σ -> π
(define (subscription E Γ σ)
;; projection->pattern to convert captures to wildcards
(assertion (projection->pattern (observe (eval-pat (E-pat E) Γ σ)))))
;; eval-exp* : (Listof exp) Γ σ -> (Result (Listof Values))
;; evaluate a sequence of expressions
(define (eval-exp* exps Γ σ)
(for-steps (list) σ (in-list exps)
(lambda (vs σ e)
(result-map (lambda (v) (append vs (list v)))
(eval-exp e Γ σ)))))
;; run-facet : facet π σ Γ Event -> Result
(define (run-facet f π-old σ Γ e)
(match-define `(react ,O ...) f)
(for-steps #f σ (in-list O)
(lambda (_ σ o)
(run-endpoint o π-old σ Γ e))))
;; run-endpoint : O π σ Γ Event -> Result
;; determine the effects of an endpoint in response to an event
(define (run-endpoint O π-old σ Γ e)
(match O
;; event-insensitive endpoints
[`(field ,_ ,_)
(inj-result #f σ)]
[`(on-start ,exp ...)
(inj-result #f σ)]
[`(assert ,exp)
(inj-result #f σ)]
;; event sensitive
[`(stop-when ,E ,exps ...)
(define bindings (occurrences E e π-old Γ σ))
(cond
[(empty? bindings)
(inj-result #f σ)]
[else
(match-define (continue _ sto as _)
(for-steps #f σ (in-list bindings)
(lambda (_ σ captures)
(define extended-env (append captures Γ))
(eval-exp* exps extended-env σ))))
(stop sto as)])]
[`(on ,E ,exps ...)
(define bindings (occurrences E e π-old Γ σ))
(cond
[(empty? bindings)
(inj-result #f σ)]
[else
(for-steps #f σ (in-list bindings)
(lambda (_ sto captures)
(define extended-env (append captures Γ))
(eval-exp* exps extended-env sto)))])]))
;; endpoint-assertions : O Γ σ -> π
;; IGNORE effects from such expressions (yadda yadda evil yadda yadda)
(define (endpoint-assertions O Γ σ)
(match O
[`(field ,_ ,_)
trie-empty]
[`(on-start ,exp ...)
trie-empty]
[`(assert ,exp)
(match-define (continue v _ _ _) (eval-exp exp Γ σ))
(assertion v)]
[`(stop-when ,E ,exps ...)
(subscription E Γ σ)]
[`(on ,E ,exps ...)
(subscription E Γ σ)]))
;; facet-assertions : facet Γ σ -> π
(define (facet-assertions f Γ σ)
(match-define `(react ,O ...) f)
(for/fold ([π trie-empty])
([o (in-list O)])
(π-union π (endpoint-assertions o Γ σ))))
;; an OK is (ok σ FacetTree (ListofAction))
(struct ok (sto ft as) #:transparent)
;; run-facets : FacetTree π σ Event -> (U OK (Listof Action))
(define (run-facets ft π parent-sto e)
(match-define (facet-tree stx env sto children) ft)
(define facet-sto (store-concat parent-sto sto))
;; I'm really not confident about the way the stores are being handled here
(match (run-facet stx π facet-sto env e)
[(continue _ new-sto as new-facets)
(define-values (final-sto final-as new-children)
(for/fold ([sto new-sto]
[as as]
[new-children (list)])
([ft (in-list (append children new-facets))])
(match (run-facets ft π sto e)
[(ok new-sto new-ft more-as)
(values new-sto
(append as more-as)
;; n^2 but let's keep the order the same
(append new-children (list new-ft)))]
[more-as
(values sto
(append as more-as)
new-children)])))
(match-define (store-concat new-parent-sto new-facet-sto) final-sto)
(ok new-parent-sto (facet-tree stx env new-facet-sto new-children) final-as)]
[(stop _ as)
as]))
;; ft-assertions : FacetTree σ -> π
(define (ft-assertions ft σ)
(match-define (facet-tree stx env sto children) ft)
(define extended-sto (store-concat σ sto))
(for/fold ([π (facet-assertions stx env extended-sto)])
([f (in-list children)])
(π-union π (ft-assertions f env extended-sto))))
;; actor-behavior : ActorState Event -> Transition
;; leaf behavior function
(define (actor-behavior e s)
(cond
[e
(with-handlers ([exn:fail? (lambda (e) (eprintf "exception: ~v\n" e) (quit #:exception e (list)))])
(match-define (actor-state π-old ft) s)
(match (run-facets ft π-old mt-σ e)
[(ok _ ft as)
(define assertions (ft-assertions ft mt-σ))
(define next-π (if (scn? e) (scn-trie e) π-old))
(transition (actor-state next-π ft)
(cons (scn assertions) as))]
[as
(quit as)]))]
[else #f]))
;; run : Program -> Syndicate
(define (run p)
(define boot-actions
(for/list ([boot (in-list p)])
(boot-actor boot mt-Γ)))
(run-ground boot-actions))
(define test-program
`((actor (react (on-start (printf "hello,world\n"))))))
(define test-program2
`(
(actor (react (on (asserted 5)
(printf "wat\n"))))
(actor (react (assert 5)))))
(define ping-pong
`(
(actor (react (on (message "ping")
(printf "ping\n")
(send! "pong"))))
(actor (react (on (message "pong")
(printf "pong\n")
(send! "ping"))
(on-start (send! "ping"))))))
(define bank-account
`(
(actor (react (field balance 0)
(assert (list "account" (read balance)))
(on (message (list "deposit" $amount))
(set! balance (+ (read balance) amount)))))
(actor (react (on (asserted (list "account" $balance))
(printf "Balance changed to ~a\n" balance))
(stop-when (asserted (list "account" 70))
(printf "bye\n"))))
(actor (react (stop-when (asserted (observe (list "deposit" _)))
(send! (list "deposit" +100))
(send! (list "deposit" -30)))))))
(define multi-level-ex
'(
(actor (react (on (asserted "hello")
(printf "goodbye"))))
(dataspace (actor (react (assert (outbound "hello")))))))
(define ff
'(
(actor (react (on (message 5)
(printf "5\n"))
(on (asserted (observe 12))
(printf "12\n"))
(on (asserted (observe 16))
(printf "16\n"))))
(actor (react (on (asserted 12))
(on-start (send! 5))
(on (asserted 16))
(on-start (send! 5))))))
(run bank-account)