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

591 lines
20 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
;; ('begin exp ...) or
;; ('let (var exp) exp) or
;; ('if exp exp exp) or
;; ('send! exp) or
;; ('react O ...) or
;; ('actor ('react O ...)) or
;; ('set! var exp) or
;; ('read var) or
;; ('list exp ...)
;; (primop exp ...) or
;; atom
;; `primop` is one of
;; + * / - and or not equal? null? car cdr printf
;; an `O` 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 E is either
;; ('asserted pat) or
;; ('retracted pat) or
;; (message pat)
;; a pat is either
;; $var or
;; _ or
;; exp or
;; ('observe pat)
;; ('list pat ...)
;; a Γ is a (Listof Binding)
;; a Binding is (binding var atom)
(struct binding (id v) #:transparent)
;; a σ is either
;; (Hashof var atom) 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 ('react O ...)))
(define mt-Γ (list))
(define mt-σ (hash))
;; env-lookup : Γ var -> atom
;; 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 atom -> Γ
(define (extend-env Γ id v)
(cons (binding id v) Γ))
;; update-sto : σ var atom -> σ
;; 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 atom) -> atom
(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 -> atom
;; 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 atom)) -> σ
(define (make-store . vs)
(make-immutable-hash vs))
;; boot-facet : facet Γ σ -> (Values σ π (Listof Action) FacetTree)
(define (boot-facet f Γ σ)
(define initial-sto (initial-store f Γ σ))
(match-define-values ((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)
(define-values (v s π as fs) (eval-exp exp Γ σ))
(cons (cons id v) locations)]
[_ locations])))
(apply make-store locations))
;; eval-start-actions : facet Γ σ -> (Values σ π (Listof Action) (Listof FacetTree))
(define (eval-start-actions f Γ σ)
(match-define `(react ,O ...) f)
(for/fold ([sto σ]
[π trie-empty]
[as (list)]
[facets (list)])
([o (in-list O)])
(match o
[`(field ,_ ,_)
(values sto π as facets)]
[`(on-start ,exp ...)
(define-values (new-sto more-π more-as more-facets) (eval-exp* exp Γ σ))
(values new-sto (π-union π more-π) (append as more-as) (append facets more-facets))]
[`(on ,E ,exp ...)
(define π-new (π-union (subscription E Γ σ) π))
(values sto π-new as facets)]
[`(stop-when ,E ,exp ...)
(define π-new (π-union (subscription E Γ σ) π))
(values sto π-new as facets)]
[`(assert ,exp)
(define-values (v s p a f) (eval-exp exp Γ σ))
(define π-new (π-union (assertion v) π))
(values sto π-new as facets)])))
;; eval-exp : exp Γ σ -> (Values atom σ π (Listof Action) (Listof FacetTree))
(define (eval-exp e Γ σ)
(match e
[`(react ,O ...)
(define-values (new-sto π as ft) (boot-facet e Γ σ))
(values (void) new-sto π as (list ft))]
[`(actor (react ,O ...))
;; don't pass in parent store
(define-values (_ π as ft) (boot-facet (second e) Γ mt-σ))
(define a (spawn actor-behavior
(actor-state trie-empty ft)
(cons (scn π) as)))
(values (void) σ trie-empty (list a) (list))]
[(? symbol? id)
(let ([v (env-lookup Γ id)])
(values v σ trie-empty (list) (list)))]
[`(begin ,es ...)
(for/fold ([v (void)]
[σ σ]
[π trie-empty]
[as (list)]
[facets (list)])
([e (in-list es)])
(define-values (v new-sto more-π more-as more-facets) (eval-exp e Γ σ))
(values v new-sto (π-union π more-π) (append as more-as) (append facets more-facets)))]
[`(list ,es ...)
(define-values (rev-vs new-sto π as facets)
(for/fold ([rev-vs (list)]
[σ σ]
[π trie-empty]
[as (list)]
[facets (list)])
([e (in-list es)])
(define-values (v new-sto more-π more-as more-facets) (eval-exp e Γ σ))
(values (cons v rev-vs) new-sto (π-union π more-π) (append as more-as) (append facets more-facets))))
(values (cons 'list (reverse rev-vs)) new-sto π as facets)]
[`(let (,x ,exp) ,body-exp)
(define-values (v new-sto π as facets) (eval-exp exp Γ σ))
(define new-Γ (extend-env Γ x v))
(define-values (result-v final-sto more-π more-as more-facets) (eval-exp body-exp new-Γ new-sto))
(values result-v final-sto (π-union π more-π) (append as more-as) (append facets more-facets))]
[`(if ,pred-exp ,then-exp ,else-exp)
(define-values (pred-v pred-sto π as facets) (eval-exp pred-exp Γ σ))
(define-values (result-v final-sto more-π more-as more-facets)
(if pred-v
(eval-exp then-exp Γ pred-sto)
(eval-exp else-exp Γ pred-sto)))
(values result-v final-sto (trie-union π more-π) (append as more-as) (append facets more-facets))]
[`(send! ,exp)
(define-values (v new-sto π as facets) (eval-exp exp Γ σ))
(values (void) new-sto π (append as (list (message v))) facets)]
[`(set! ,id ,exp)
(define-values (v new-sto π as facets) (eval-exp exp Γ σ))
(define result-sto (update-sto new-sto id v))
(values (void) result-sto π as facets)]
[`(read ,id)
(define v (sto-fetch σ id))
(values v σ trie-empty (list) (list))]
[`(,primop ,exp ..1)
#:when (primop? primop)
(define-values (args sto π as facets)
(for/fold ([vs (list)]
[σ σ]
[π trie-empty]
[as (list)]
[facets (list)])
([e (in-list exp)])
(define-values (v new-sto more-π more-as more-facets) (eval-exp e Γ σ))
(values (cons v vs) new-sto (trie-union π more-π) (append as more-as) (append facets more-facets))))
(define v (apply-primop primop (reverse args)))
(values v sto π as facets)]
[x (values x σ trie-empty (list) (list))]))
(module+ test
;; sequencing result
(let-values ([(v s p as f) (eval-exp `(begin 1 2 3) mt-Γ mt-σ)])
(check-equal? v 3))
;; variable lookup
(let-values ([(v s p as f) (eval-exp 'x (list (binding 'x 'hello)
(binding 'y 'bye)
(binding 'x 'world))
mt-σ)])
(check-equal? v 'hello))
;; variable binding
(let-values ([(v s p as f) (eval-exp '(let (y 12) 'cake) mt-Γ mt-σ)])
(check-equal? v ''cake))
(let-values ([(v s p as f) (eval-exp '(let (y 12) y) mt-Γ mt-σ)])
(check-equal? v 12))
;; if
(let-values ([(v s p as f) (eval-exp '(if #f 5 6) mt-Γ mt-σ)])
(check-equal? v 6))
(let-values ([(v s p as f) (eval-exp '(if #t 5 6) mt-Γ mt-σ)])
(check-equal? v 5))
;; send!
(let-values ([(v s p as f) (eval-exp '(send! 5) mt-Γ mt-σ)])
(check-equal? as (list (message 5)))
(check-true (void? v)))
;; set!
(let-values ([(v s p as f) (eval-exp '(set! x 12) mt-Γ (make-store '(x . 'hello)))])
(check-true (void? v))
(check-equal? (hash-ref s 'x) 12))
(let-values ([(v s p 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
(let-values ([(v s p as f) (eval-exp '(read x) mt-Γ (make-store '(y . 5) '(x . 'hello)))])
(check-equal? v ''hello))
(let-values ([(v s p as f) (eval-exp '(+ (- 5 1) (/ 4 (if (not #t) 1 2))) mt-Γ mt-σ)])
(check-equal? v 6)))
;; 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
;; 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 Γ σ))]
[(? dollar-id? s)
(?!)]
['_ ?]
[exp
(define-values (v s p as fs) (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)]
[(? 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) Γ σ -> (Values σ π (Listof Action) (Listof FacetTree))
;; evaluate a sequence of expressions for effect
(define (eval-exp* exps Γ σ)
(for/fold ([sto σ]
[π trie-empty]
[as (list)]
[facets (list)])
([e (in-list exps)])
(define-values (v new-sto more-π more-as more-facets) (eval-exp e Γ σ))
(values new-sto (π-union π more-π) (append as more-as) (append facets more-facets))))
;; A Continue is (continue σ π (Listof Action) (Listof FacetTree))
(struct continue (sto scn as fs) #:transparent)
;; A Stop is (stop σ (Listof Action))
(struct stop (sto as) #:transparent)
;; run-facet : facet π σ Γ Event -> (U Continue Stop)
(define (run-facet f π-old σ Γ e)
(match-define `(react ,O ...) f)
(for/fold ([s (continue σ trie-empty (list) (list))])
([o (in-list O)])
(match s
[(stop _ _) s]
[(continue σ π as facets)
(match (run-endpoint o π-old σ Γ e)
[(stop σ as)
;; ok to discard previous as here I guess
(stop σ as)]
[(continue new-sto more-π more-as more-facets)
(continue new-sto (π-union π more-π) (append as more-as) (append facets more-facets))])])))
;; run-endpoint : O π σ Γ Event -> (U Continue Stop)
;; I guess can also return a facet tree
(define (run-endpoint O π-old σ Γ e)
(match O
[`(field ,_ ,_)
(continue σ trie-empty (list) (list))]
[`(on-start ,exp ...)
(continue σ trie-empty (list) (list))]
[`(assert ,exp)
(define-values (v new-sto π as facets) (eval-exp exp Γ σ))
(continue new-sto (π-union (assertion v) π) as facets)]
[`(stop-when ,E ,exps ...)
(define bindings (occurrences E e π-old Γ σ))
(cond
[(empty? bindings)
(continue σ (subscription E Γ σ) (list) (list))]
[else
(define-values (sto as)
(for/fold ([sto σ]
[as (list)])
([captures (in-list bindings)])
(define extended-env (append captures Γ))
;; seems like we should ignore new facets here?
(define-values (new-sto π more-as facets) (eval-exp* exps extended-env sto))
(values new-sto (append as more-as))))
(stop sto as)])]
[`(on ,E ,exps ...)
(define bindings (occurrences E e π-old Γ σ))
(cond
[(empty? bindings)
(continue σ (subscription E Γ σ) (list) (list))]
[else
(define-values (sto π as facets)
(for/fold ([sto σ]
[π trie-empty]
[as (list)]
[facets (list)])
([captures (in-list bindings)])
(define extended-env (append captures Γ))
(define-values (new-sto more-π more-as more-facets) (eval-exp* exps extended-env sto))
(values new-sto (π-union π more-π) (append as more-as) (append facets more-facets))))
(continue sto (subscription E Γ sto) as facets)])]))
;; an OK is (ok σ FacetTree π (ListofAction))
(struct ok (sto ft asserts 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 assertions as new-facets)
(define-values (final-sto final-as asserts new-children)
(for/fold ([sto new-sto]
[as as]
[assertions assertions]
[new-children (list)])
([ft (in-list (append children new-facets))])
(match (run-facets ft π sto e)
[(ok new-sto new-ft more-asserts more-as)
(values new-sto
(append as more-as)
(π-union assertions more-asserts)
;; n^2 but let's keep the order the same
(append new-children (list new-ft)))]
[more-as
(values sto
(append as more-as)
assertions
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) asserts final-as)]
[(stop _ as)
as]))
;; actor-behavior : ActorState Event -> Transition
;; leaf behavior function
(define (actor-behavior e s)
(cond
[e
(with-handlers ([exn:fail? (lambda (e) (quit #:exceptiuon e (list)))])
(match-define (actor-state π-old ft) s)
(match (run-facets ft π-old mt-σ e)
[(ok _ ft π as)
(define next-π (if (scn? e) (scn-trie e) π-old))
(transition (actor-state next-π ft)
(cons (scn π) as))]
[as
(quit as)]))]
[else #f]))
;; run : Program -> Syndicate
(define (run p)
(define boot-actions
(for/list ([boot (in-list p)])
(match-define-values (v _ π (list s) fs) (eval-exp boot mt-Γ mt-σ))
s))
(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))))
(actor (react (stop-when (asserted (observe (list 'deposit _)))
(send! (list 'deposit +100))
(send! (list 'deposit -30)))))))
(run bank-account)