{-
Extensible Denotational Semantics
This work is a generalization of
Extensible Denotational Language Specifications
Robert Cartwright, Matthias Felleisen
Theor. Aspects of Computer Software, 1994
http://citeseer.ist.psu.edu/69837.html
to be referred to as EDLS.
We implement the enhanced EDLS in Haskell and add delimited control.
To be precise, we implement the Base interpreter (whose sole
operations are Loop and Error) and the following extensions: CBV
lambda-calculus, Arithmetic, Storage, Control. The extensions can be
added to the Base interpreter in any order and in any combination.
Our implementation has the following advantages over EDLS:
-- support for delimited control
-- support for a local storage (including `thread-local' storage
and general delimited dynamic binding)
-- extensions do not have to be composed explicitly, and so the
composition order truly does not matter. In the original EDLS, one
had to write interpreter composition explicitly. In our approach, an
extension is pulled in automatically if the program includes the
corresponding syntactic forms. For example, if a program contains
storage cell creation and dereference operations, the Storage extension
will automatically be used to interpret the program.
Our main departure from EDLS is is the removal of the `central
authority'. There is no semantic `admin' function. Rather, admin is
part of the source language and can be used at any place in the
code. The `central authority' of EDLS must be an extensible function,
requiring meta-language facilities to implement (such as quite
non-standard Scheme modules). We do not have central
authority. Rather, we have bureaucracy: each specific effect handler
interprets its own effects as it can, throwing the rest `upstairs' for
higher-level bureaucrats to deal with. Extensibility arises
automatically.
We take the meaning of a program to be the union of Values and
(unfulfilled) Actions. If the meaning of the program is a (non-bottom)
value, the program is terminating. If the meaning of the program is an
Action -- the program finished with an error, such as an action to
access a non-existing storage cell, or shift without reset, or a
user-raised error.
Incidentally, EDLS stresses on p. 2 (the last full paragraph) that
their schema critically relies on the distinction between a complete
program and a nested program phrase. Our contribution (which is the
consequence of removing the central admin, making it first-class) is
eliminating such a distinction! EDLS says, at the very top of p. 3,
that the handle in the effect message ``is roughly a conventional
continuation.'' Because the admin of EDLS is `outside' of the program
(at the point of infinity, so to speak), its continuation indeed
appears undelimited. By making our `admin' a part of the source
language, we recognize the handle in the effect message for what it
is: a _delimited_ continuation.
As we see below, the Control aspect is orthogonal to the CBV
aspect. So, we can just as easily replace the CBV extension with the
CBN extension, which will give us the denotation of delimited control
in CBN.
We shall be using Haskell to express denotations, taking advantage of
the fact that Haskell values are `lifted'. See also the remark on p21
of EDLS: someone (ref. [4]) showed that sets and partial functions can
suffice...
Our program denotations are stable: they have the same form regardless
of a particular composition of modular interpreters. See Section 3.5
of EDLS for precise formulation (and the last paragraph of Section 3.4
for concise informal statement of stability). Also see the comparison
with the monads on p. 20: different monads assign numeral radically
different meanings, depending on the monad. But in EDLS, a numeral has
exactly the same denotation.
The meaning of an expression is also the same and stable, no matter
what the interpreter is: it is a function from Env to Computations
(the latter being the union of values and unfulfilled effects -- the
same union mentioned above).
However, by using handlers, some effect messages can be subtracted:
so, strictly speaking, the denotation of a fragment is V +
effect-message-issued - effect-messages handled. There seem to be a
connection with effect systems.
Additional notes on EDLS:
The abstract and p. 2 (especially the last full paragraph) are
so well-written!
p. 5, bottom: ``It [ref \x.x, where ref means what it does in ML]
is a closed expression that does not diverge, is not a value, and
cannot be reduced to a value without affecting the context. We refer
to such results as _effects_.''
Beginning of Section 3 and App A give a very good introduction
to the denotational semantics.
p. 6: ``Algebraically speaking, the interpreter is (roughly) a
homomorphism from syntax to semantics''
``A straightforward representation of an evaluation context is a
function from values to computations, which directly corresponds to
its operational usage as a function from syntactic values to
expressions.'' (p. 8). EDLS then notices that the `handle' has
the same type as the denotation of a context. Footnote 6 notes that
handler is roughly the composition combinator for functions from values
to computations [contexts!] and it superficially relates to [bind]
but does not satisfy all monad laws.
The last of EDLS Sec 3.5, p. 15: ``Informally speaking, this property
[stable denotations] asserts that in the framework of extensible
semantics, the addition of a programming construct corresponds to the
addition of a new ``dimension'' to the space of meaning. [The
equations, featuring injection and projection functions, make that
precise and clear.] The reader may want to contrast this general
statement with the numerous papers on the relationship between direct
and continuation semantics.''
The practical consequence of the lack of stability of denotations for
monads is that monadic transformers put layers upon layers of thunks,
which have to be carried over _in full_ from one sub-expression into
the other, even if no single subexpression uses all of the features.
The present code is not written in idiomatic Haskell and does not take
advantage of types at all. The ubiquitous projections from the
universal domain tantamount to ``dynamic typing.'' The code is
intentionally written to be close to the EDLS paper, emphasizing
denotational semantics (whose domains are untyped). One can certainly
do better, for example, employ user-defined datatypes for tagged
values, avoiding the ugly string-tagged VT.
-}
module ExtensibleDS where
import Maybe
------------------------------------------------------------------------
-- The base interpreter
-- The universal domain
-- Yes, it is untyped... But Denot semantics is like this anyway...
-- Bottom is the implicit member of this domain.
data Value = VI Int -- integers
| VS String -- strings
| VP Value Value -- pairs
| VF (Value -> Value) -- functions
| VT Tag Value -- tagged value
type Tag = String
type Action = Value -- only differently tagged
instance Show Value where
show (VI x) = show x
show (VS x) = show x
show (VT tag v) = "{"++show tag++" "++show v++"}"
show (VP x1 x2) = show (x1,x2)
show (VF _) = ""
-- Computations: just like in EDLS
-- The strictness of inV guarantees that inV bottom = bottom, as
-- on Fig. 3 of EDLS
-- Error is always a part of the computation
data Comp = InV !Value | InFX (Value -> Comp) Action
instance Show Comp where
show (InV v) = show v
show (InFX _ a) = "Effect: " ++ show a
-- Auxiliary functions
newtype V = V String deriving (Eq, Show) -- Variables
type Env v = V -> v
empty_env x = error $ "impossible: an open program? var " ++ show x
ext_env env x v y | x == y = v
ext_env env x v y = env y
-- Note that inV is essentially the identity `partial' continuation
inAC action = InFX InV action
-- The error `raise' action and its tag
tagErr = "Err"
raise err = inAC $ VT tagErr (VS err)
-- The universal handler of actions
-- It propagates the action request InFX up.
-- Since we do case-analysis on the first argument, v, the handler
-- is strict in that argument. That is, handler bottom ==> bottom,
-- as required by Fig. 3 of EDLS.
-- The last clause is essentially the denotation of a `control channel'
handler (InV v) k = k v
handler (InFX ka a) k = InFX (\v -> handler (ka v) k) a
-- Expressions and their interpretations
-- This is an open data type and an open function. Haskell typeclasses
-- are ideal for that.
-- This is the function \curlyM of EDLS
class Interpretor exp where
interpret :: exp -> Env Value -> Comp
-- The base language has only two expressions: omega and error
-- See Fig. 3 (p. 7) of EDLS
data BE_err = BE_err
data BE_omega = BE_omega
instance Interpretor BE_omega where
interpret _ _ = InV undefined
instance Interpretor BE_err where
interpret _ _ = raise "error action"
-- The meaning of a program
prog p = interpret p empty_env
-- ----------------------------------------------------------------------
-- Extension: Call-by-Value: see a part Fig. 4 of EDLS (or, Fig. 8)
-- We add closures (procedures) to the Base
type Proc = Value -> Comp
class PV u where -- and the corresponding injection/
inP :: Proc -> u -- projection functions
prP :: u -> Maybe (Proc)
type Var = V
data Lam e = Lam V e
data App e1 e2 = e1 :@ e2
infixl 1 :@
instance Interpretor V where
interpret v env = InV (env v)
instance Interpretor e => Interpretor (Lam e) where
interpret (Lam x e) env = InV (inP (\d -> interpret e (ext_env env x d)))
instance (Interpretor e1, Interpretor e2) => Interpretor (App e1 e2) where
interpret (e1 :@ e2) env =
handler (interpret e1 env) $
\f -> handler (interpret e2 env) $
\a -> maybe (raise "InP expected") (\g -> g a)
(prP f)
-- Create a few variables for use in examples
vx, vy, vr, vf :: Var
(vx,vy,vr,vf) = (V "x", V "y", V "r", V "f")
-- Test of the extension
-- Instantiate this extension on the top of the base language
-- Denotations of the new actions (inj/proj to/from the Universal Domain)
tagProc = "Proc"
tagCompV = "CompV"
tagCompE = "CompE"
-- inject and project computations
inComp :: Comp -> Value
inComp (InV v) = VT tagCompV v
inComp (InFX k a) = VT tagCompE (VP (VF (inComp . k)) a)
prComp :: Value -> Maybe Comp
prComp (VT tag v) | tag == tagCompV = Just (InV v)
prComp (VT tag v) | tag == tagCompE = pr v
where pr (VP (VF k) a) = Just $ InFX (fromJust . prComp . k) a
pr _ = Nothing
prComp _ = Nothing
instance PV Value where
inP f = VT tagProc (VF (inComp . f))
prP (VT tag (VF k)) | tag == tagProc = Just $ fromJust . prComp . k
prP _ = Nothing
test1 = Lam vx (vx :@ BE_err)
test1e = prog test1
-- {"Proc" }
------------------------------------------------------------------------
-- Extension: Arithmetic: see a part Fig. 4 of EDLS
class PN u where -- inj/proj for numbers
inN :: Int -> u
prN :: u -> Maybe Int
data IntC = IntC Int -- an integer constant
data Add1 = Add1
instance Interpretor IntC where
interpret (IntC v) _ = InV (inN v)
instance Interpretor Add1 where
interpret Add1 _ = InV (inP $
\v -> maybe (raise "InN expected")
(InV . inN . succ)
(prN v))
add1 x = Add1 :@ x
-- Test of the extension
-- Instantiate this extension on the top of the CBV evaluator
instance PN Value where
inN n = VI n
prN (VI n) = Just n
prN _ = Nothing
test2 = (Lam vx $ add1 vx) :@ (IntC 1)
test2e = prog test2
-- 2
------------------------------------------------------------------------
-- Extension: State: see Fig. 5 of EDLS
newtype Loc = Loc Int deriving (Eq, Show) -- locations
class PL u where
inL :: Loc -> u
prL :: u -> Maybe Loc
-- a better idea is Loc -> Maybe Value, so that when lookup fails,
-- we can re-throw that Deref or Set message. That would make the
-- storage extensible...
-- The first component of the pair is the allocation counter
type Sto = (Int, Loc -> Value)
tagSto = "Storage"
inSto :: Sto -> Value
inSto (cnt, st) = VT tagSto (VP (VI cnt) (VF (st . fromJust . prL)))
prSto :: Value -> Maybe Sto
prSto (VT tag (VP (VI cnt) (VF k))) | tag == tagSto = Just (cnt, k . inL)
prSto _ = Nothing
init_sto :: Sto
init_sto = (0,\l->undefined)
-- data StoAct v = InRef v | InDer Loc | InSet Loc v
-- new expressions
data Ref e = Ref e
data Deref e = Deref e
data Set e1 e2 = Set e1 e2
-- Denotations of the new actions (inj/proj to/from the Universal Domain)
tagInRef = "Ref"
tagInDer = "Deref"
tagInSet = "Set"
inRef v = VT tagInRef v
prRef (VT tag v) | tag == tagInRef = Just v
prRef _ = Nothing
inDer v = VT tagInDer (inL v)
prDer (VT tag v) | tag == tagInDer = prL v
prDer _ = Nothing
inSet l v = VT tagInSet (VP (inL l) v)
prSet (VT tag (VP l v)) | tag == tagInSet = fmap (\l -> (l,v)) (prL l)
prSet _ = Nothing
instance Interpretor e => Interpretor (Ref e) where
interpret (Ref e) env = handler (interpret e env) $
\v -> inAC(inRef v)
instance Interpretor e => Interpretor (Deref e) where
interpret (Deref e) env = handler (interpret e env) $
\v -> maybe (raise "InL expected")
(inAC . inDer)
(prL v)
instance (Interpretor e1, Interpretor e2) => Interpretor (Set e1 e2) where
interpret (Set e1 e2) env = handler (interpret e1 env) $
\l -> handler (interpret e2 env) $
\v -> maybe (raise "InL expected")
(\l -> inAC (inSet l v))
(prL l)
-- Now we need a storage admin
-- It is treated as an expression in the source language!
data StoAdmin e = StoAdmin Sto e
instance Interpretor e => Interpretor (StoAdmin e) where
interpret (StoAdmin sto e) env = sto_handler sto (interpret e env) InV
sto_handler (cnt,st) (InFX k a) f | Just v <- prRef a =
let l = Loc cnt
sto = (cnt+1, \l' -> if l' == l then v else st l')
in sto_handler sto (k (inL l)) f
sto_handler sto@(cnt,st) (InFX k a) f | Just l <- prDer a =
sto_handler sto (k (st l)) f
sto_handler (cnt,st) (InFX k a) f | Just (l,v) <- prSet a =
sto_handler (cnt, \l' -> if l' == l then v else st l') (k (inL l)) f
sto_handler sto (InV v) f = f (VP v (inSto sto)) -- normal return
-- Propagate everything else
sto_handler sto (InFX k a) f = InFX (\v -> sto_handler sto (k v) f) a
tagLoc = "Loc"
instance PL Value where
inL (Loc l) = VT tagLoc (VI l)
prL (VT tag (VI l)) | tag == tagLoc = Just (Loc l)
prL _ = Nothing
-- Test of the extension
-- Note that we can use several local storages!
test3 = (Lam vr $ Lam vf $ vf :@ (Deref vr)) :@
(Ref (IntC 1)) :@ (Lam vx $ add1 vx)
-- we forgot the Admin..., so the program terminates with an effect
test31 = prog test3
-- Effect: {"Ref" 1}
test32 = prog (StoAdmin init_sto test3)
-- (2,{"Storage" (1,)})
test33 = prog (StoAdmin init_sto
((Lam vr $ (Lam (V "_") (Deref vr)) :@ (Deref (Set vr (IntC 2))))
:@ (Ref (IntC 1))))
-- (2,{"Storage" (1,)})
------------------------------------------------------------------------
-- Extension: Delimited continuation
-- That is new compared to EDLS
-- new effect message: Control ((Value->Comp)->Comp)
tagControl = "Control"
inControl :: (Value->Comp) -> Value
inControl f = VT tagControl (VF (inComp . f))
prControl :: Value -> Maybe (Value->Comp)
prControl (VT tag (VF f)) | tag == tagControl = Just (fromJust . prComp . f)
prControl _ = Nothing
data Control e = Control e
instance Interpretor e => Interpretor (Control e) where
interpret (Control e) env = handler (interpret e env) $
\a -> maybe (raise "InP expected")
(\f -> inAC(inControl f))
(prP a)
-- The administrator here is prompt, quite unsurprisingly
data ControlAdmin e = ControlAdmin e
instance Interpretor e => Interpretor (ControlAdmin e) where
interpret (ControlAdmin e) env = ctl_handler (interpret e env) InV
ctl_handler (InFX k a) f | Just e <- prControl a =
ctl_handler (e (inP k)) f
ctl_handler (InV v) f = f v -- normal return
-- Propagate everything else
ctl_handler (InFX k a) f = InFX (\v -> ctl_handler (k v) f) a
-- Ken's tests
control v e = Control (Lam (V v) e)
prompt e = ControlAdmin e
succ0 = (add1 (IntC 0))
-- if we forget Reset...
test41 = prog $ add1 (Control (Lam vf succ0))
-- Effect: {"Control" }
test42 = prog $ prompt (add1 (control "f" succ0))
-- 1
test43 = prog $ prompt (add1 (control "f" (vf :@ succ0)))
--2
test44 = prog $ prompt (add1 (add1 (control "f" (vf :@ succ0))))
-- 3
test45 = prog $ prompt (add1 (add1 (control "f"
(vf :@ (vf :@ succ0)))))
-- 5
test46 = prog $ prompt (add1 ((Lam vx (add1 vx)) :@
(control "f" (vf :@ (vf :@ succ0)))))
-- 5
test47 = prog $ prompt (add1 ((Lam vx (control "f" (IntC 0))) :@
(control "f" (vf :@ (vf :@ succ0)))))
-- 0
test48 = prog $ add1 (prompt ((Lam vx (control "f" (IntC 0))) :@
(control "f" (vf :@ (vf :@ succ0)))))
-- 1