3 Kernel Esterel Reference
(require esterel/kernel) | package: esterel-lib |
The esterel/kernel and esterel/full libraries provide all of the names documented here; the esterel/full library provides additional functionality.
3.1 Running Esterel Code
syntax
(esterel maybe-pre expr ...)
maybe-pre =
| #:pre pre-count-expr
If present, the value of pre-count-expr is expected to be a natural number. It is a limit on the history that’s saved for signals in previous instants. It defaults to 0.
procedure
(react! r [#:emit signals])
→
(hash/dc [s signal?] [v (s) (if (signal-combine s) any/c boolean?)] #:immutable #t #:kind 'flat) r : esterel?
signals :
(listof (or/c (and/c signal? (not/c signal-combine)) (cons/c (and/c signal? signal-combine) any/c))) = '()
If signals are supplied, they are emitted at the start of the instant; valued signals must be paired with values.
The result has the values of signals that were emitted. Additionally if a signal is not a valued signal and the computation depends on it not being present (e.g., if it is passed to present?), it is included in resulting hash, mapped to #f.
If the code is not constructive, an exception (that is recognized by exn:fail:not-constructive?) is raised.
> (define-signal S1 S2) > (react! (esterel (emit S1))) '#hash((#<signal: S1> . #t))
> (react! (esterel (if (present? S1) (void) (emit S2)))) '#hash((#<signal: S2> . #t) (#<signal: S1> . #f))
> (react! (esterel (if (present? S1) (void) (emit S2))) #:emit (list S1)) '#hash((#<signal: S1> . #t))
> (react! (esterel (if (present? S1) (emit S1) (void)))) react!: the program is not constructive
the signal blocking progress can be emitted
signal: #<signal: S1>
procedure
(in-esterel?) → boolean?
procedure
v : any/c
syntax
(debug-when-must e1 e2 ...)
This can be used to debug Esterel in Racket programs. Sometimes, code inside esterel is run as part of an exploration to determine if signal might be emitted and, in that case, we do not know that that code must run. In such situations, effectful code (such as printf) can run multiple times, leading to confusing behavior. Wrapping such debugging IO operations in a debug-when-must form can help to understand an Esterel in Racket program.
> (define-signal S1)
> (react! (esterel (if (present? S1) (debug-when-must (printf "hi!\n")) (debug-when-must (printf"bye!\n"))))) bye!
'#hash((#<signal: S1> . #f))
3.2 Signals
syntax
(with-signal (signal ...) body-expr ...+)
signal = signal-id maybe-combine maybe-combine =
| #:combine combine-expr | #:init init-expr #:combine combine-expr | #:memoryless #:init init-expr #:combine combine-expr | #:single
Each signal suffixed with #:combine is a value-carrying signal, and those without are not. Multiple emissions of the signal are combined using the result of combine-expr, a binary function that is assumed to be associative and commutative. If init-expr is provided, then the value of the signal if it is never emitted is the value of init-expr. Once the signal is emitted, however, the value of init-expr is discarded.
If #:memoryless is supplied, the signal’s value is not carried forward from previous instants, but instead restarts with the value of init-expr. If #:memoryless is not supplied, and the signal is not emitted in the current instant, then its value is the value it had in the previous instant (if it had one).
If the signal is followed by #:single, it is also a valued signal, but it may be emitted at most once in each instant and it takes that value.
If with-signal is invoked from within esterel, then the signals may not be emitted once the last body-expr is evaluated (it will result in an error from emit if they are).
The result of the with-signal expression is the result of the last expression. If with-signal is used in the dynamic extent of esterel, the last body-expr is not in tail position with respect to the with-signal, but otherwise it is.
> (react! (esterel (with-signal (s1 s2) (unless (present? s2) (emit s1))))) '#hash((#<signal: s2 (1)> . #f) (#<signal: s1 (0)> . #t))
> (react! (esterel (with-signal (s1 s2 #:combine + s3 s4 #:combine *) (emit s1) (emit s2 22) (emit s2 33)))) '#hash((#<signal: s1 (0)> . #t) (#<signal: s2 (1)> . 55))
> (react! (esterel (with-signal (s1 #:combine + s2 #:init 11 #:combine + s3 #:combine + s4 #:init 22 #:combine *) (emit s1 (signal-value s2 #:can (set s3 s4))) (emit s4 33) (emit s3 (signal-value s4 #:can (set))))))
'#hash((#<signal: s4 (3)> . 33)
(#<signal: s3 (2)> . 33)
(#<signal: s1 (0)> . 11))
syntax
(define-signal signal ...)
The signals that define-signal creates have indefinite extent (i.e., the signal will not become dead unlike the signals created by with-signal), but define-signal can be used only at the module top-level or at the interactive top-level.
procedure
(make-global-signal name [ #:combine combine] #:init init [ #:memoryless memoryless]) → signal? name : string? combine : #f = (or/c #f (procedure-arity-includes/c 2)) init : any/c memoryless : #f = boolean?
Use make-global-signal when the number of signals is not known ahead of time or it is not convenient to write a sequence of define-signal definitions.
> (define sigs (for/hash ([i (in-range 10)]) (values i (make-global-signal (~a i))))) > sigs
'#hash((0 . #<signal: 0>)
(1 . #<signal: 1>)
(2 . #<signal: 2>)
(3 . #<signal: 3>)
(4 . #<signal: 4>)
(5 . #<signal: 5>)
(6 . #<signal: 6>)
(7 . #<signal: 7>)
(8 . #<signal: 8>)
(9 . #<signal: 9>))
> (with-signal (s1) (signal? s1)) #t
> (signal? "not a signal, but a string") #f
procedure
(signal-name s) → (and/c string? immutable?)
s : signal?
> (define-signal S) > (signal-name S) "S"
procedure
(signal-index s) → (or/c #f natural?)
s : signal?
> (define-signal O) > (signal-index O) #f
> (define r (esterel (loop (with-signal (S) (if (present? S) (emit O) (void)) (pause) (emit S)))))
> (for/set ([(k v) (in-hash (react! r))]) (list (signal-name k) (signal-index k) v)) (set '("S" 0 #f))
> (for/set ([(k v) (in-hash (react! r))]) (list (signal-name k) (signal-index k) v)) (set '("S" 1 #f) '("S" 0 #t))
> (for/set ([(k v) (in-hash (react! r))]) (list (signal-name k) (signal-index k) v)) (set '("S" 1 #t) '("S" 2 #f))
procedure
(signal-combine s) → (or/c #f (-> any/c any/c any/c))
s : signal?
> (with-signal (s1 s2 #:combine +) (values (signal-combine s1) (signal-combine s2)))
#f
#<procedure:+>
If pre is larger than zero, returns whether or not s was present in previous instants. If pre is larger than the value of the pre-count-expr passed to esterel, an error is raised.
> (define-signal S) > (define-signal O1) > (define-signal O2) > (react! (esterel (if (present? S) (emit O1) (emit O2)))) '#hash((#<signal: O2> . #t) (#<signal: S> . #f))
> (define r (esterel #:pre 1 (emit S) (pause) (if (present? S #:pre 1) (emit O1) (emit O2)))) > (react! r) '#hash((#<signal: S> . #t))
> (react! r) '#hash((#<signal: O1> . #t))
procedure
(signal-value s [#:pre n] #:can can) → any/c
s : signal? n : natural? = 0 can : (setof signal?)
If n is larger than zero, then signal-value returns the value of s is the nth previous instant. If n is larger than the value of the pre-count-expr passed to esterel, an error is raised. If the value has never been emitted and the signals declaration did not have an #:init clause, an error is raised.
The can argument indicates which signals can be emitted by the remaining computation and it must be supplied if if n is 0. That is, if it is possible that some signal can be emitted in the current instant after signal-value returns, then that signal must be in the set can.
> (define-signal S1 #:combine + S2 #:combine + O1 #:combine + O2 #:combine + O3 #:combine +)
> (define r (esterel #:pre 1 (emit S1 2) (emit S1 3) (emit S2 0) (pause) (emit S2 6) (emit O1 (signal-value S1 #:can (set O2 O3))) (emit O2 (signal-value S2 #:pre 1)) (emit O3 (signal-value S2 #:can (set))))) > (react! r) '#hash((#<signal: S2> . 0) (#<signal: S1> . 5))
> (react! r)
'#hash((#<signal: S2> . 6)
(#<signal: O2> . 0)
(#<signal: O3> . 6)
(#<signal: O1> . 5))
3.3 Control Operations
syntax
(par expr ...)
syntax
(suspend body-expr when-expr)
In the second instant in this example, S1 is not emitted because that thread is suspended. In the third instant, however it is, because S2 is not emitted.
> (define-signal S1 S2)
> (define r (esterel (par (begin (pause) (emit S2) (pause)) (suspend (let loop () (pause) (emit S1) (loop)) (present? S2))))) > (react! r) '#hash()
> (react! r) '#hash((#<signal: S2> . #t))
> (react! r) '#hash((#<signal: S2> . #f) (#<signal: S1> . #t))
> (react! r) '#hash((#<signal: S2> . #f) (#<signal: S1> . #t))
> (define-signal O #:init (set) #:combine set-union S2)
> (define r (esterel #:pre 1 (suspend (with-signal (S1) (let loop () (emit S1) (pause) (emit O (present? S1 #:pre 1)) (loop))) (present? S2)))) > (react! r) '#hash((#<signal: S1 (0)> . #t))
> (react! r #:emit (list S2)) '#hash((#<signal: S2> . #t))
> (react! r) '#hash((#<signal: S1 (0)> . #t) (#<signal: S2> . #f) (#<signal: O> . #t))
In the second instant, S1 is not emitted but, because its declaration is suspended, the present test on the previous instant’s version of S1 is true and thus the result of react! has O mapped to #t.
syntax
(with-trap trap-id body-expr ...)
See also with-trap.
syntax
(exec esterel-id ([id id-expr] ...) exec-expr ... maybe-kill-exprs maybe-suspend-exprs maybe-resume-exprs)
maybe-kill-exprs =
| #:kill kill-expr ... maybe-suspend-exprs =
| #:suspend suspend-expr ... maybe-resume-exprs =
| #:resume resume-expr ...
The id-expr expressions are evaluated when the exec is evaluated and they are bound to the ids, whose scope spans the exec-exprs, kill-exprs, suspend-exprs, and the resume-exprs.
The suspend-exprs and resume-exprs are evaluated each instant that the exec is suspended or resumed, with the caveat that, if the exec is suspended for multiple instants, the suspend-exprs are evaluated only during the first instant when the expr is suspended. Similarly, when the exec is resumed, the resume-exprs are evaluated only once, until the next time the exec is resumed (following some future suspension).
If the exec is terminated (because a parallel thread exit-traps to an enclosing with-trap) the kill-exprs are evaluated.
If they are evaluated, the kill-exprs, suspend-exprs, and resume-exprs are evaluated on the same thread as each other, and that thread is different from the thread evaluating the exec-exprs. Also, the kill-exprs, suspend-exprs, and resume-exprs must not raise errors or otherwise fail to return normally, as the overall esterel that they are running in will not function properly in that situation.
The intention of exec is to offer a controlled way to connect the synchronous computation (inside esterel) to the asynchronous computation that’s going on outside it. As such, the idea is that the exec-exprs may run some asynchronous computation, such as connecting to a website or waiting for a timeout and, when that completes, trigger another reaction with the results of the asynchronous computation communicated to the synchronous world via values on signals. That is, the last thing that the exec-exprss should do is trigger a reaction. But, that reaction should probably be triggered on the same thread that triggered the original reaction (or, at least, in a way that guarantees that only one reaction is running at a time). The syncronization required to establish that is left to the user of exec. One possibility, if a GUI is involved, is to use queue-callback in the exec-exprs, keeping every reaction on an eventspace’s handler thread.