run () run (() | () | ()) _ x!y stands for x y x?y=P stands for x(y).P in PICT also the syncronization signal: [] and "concrete" values run ( print!"peering" | print!"absorbing" | print!"translating" ) peering absorbing translating print is NOT a command, but the name of a channel enabling to communicate to the "hidden" process that prints run ( x?[] = print!"Got it!" | x![] ) Got it! run x?[] = (print!"Got it!" | x![]) run ( x!y | x?z = z!u | y?w = print!"Got it!" ) Got it! run ( x!y | x?z = a!z | a?w = print!"Got it!" ) Got it! a channel is a value tuple of values are values [v1...vn] x, [x y], and [x [[y x]] y [] x] are then values The "signal" is then simply the empty tuple For the time being strings are values only for the print channel In the version of pi-calculus only channels were values x!v we can receive values by means of patter matching (like inputs for haskell functions) pattern matching produces, like in haskell, bindings for variables Exercise: - define in haskell the data type of pict values - define the data type of patterns - define the data types of bindings - define the pattern matching operation other useful patterns _ (wildcard) x@p (layered pattern) run ( new ch: ^[String String] ( ch!["Hello" "world"] | ch?a@[b c] = ( print!b | print!c ) ) ) Hello world >>> PICT is typed!!! =============== Languages such as C, C++ and Java created a bad reputation to static type systems. There, in order to do something useful, you had to constantly “cast” values from one type to another. We recommend you to ignore those examples. You should rather look at good examples such as typing in ML or Ocaml. Typing belongs to computer science. ================ PICT is statically typed primitive types: type | example value _________________________ Bool | true Int | 50 Char | ’c’ String | "The Pict programming language" The type system (statically) guarantees that patterns matches values in communications Variable declarations: val i = 50 val b = true val message = "Hello" types are inferred example value | its type ______________________________________ [] | [] [5] | [Int] [5 10 15] | [Int Int Int] [5 "foo"] | [Int String] [[48 8 41] [17 6 46]] | [[Int Int Int] [Int Int Int]] type X = T - higher-order polymorphism, - subtyping, - records, - recursive types, - type-inference mechanism. CHANNELS New channel names are introduced by the new declaration: new x:^T Each channel may be used to send and receive values of exactly one type. If this type is T, then the type of the channel is ^T, read “channel of T” or “channel carrying T.” (Not the only possible approach: channels carrying any sort of values. Types can be used to ensure the right sequence (protocol) Session Types.) new x:^Int new y:^Int run x!y Expected type does not match actual since ^Int is not a subtype of Int since ^Int and Int do not match. Example of well typed program: new x:^Int new y:^Int new z:^^Int run ( x!50 | y!100 | z!x | z!y ) The keywords new, run, type all introduce declaration clauses A Pict program is a sequence of declaration clauses. the scope of variables introduced in each clause is all of the following clauses. new x:^Int run ( x?a = () | x!5) new y:^[Int Int] run ( y?[a b] = () | y![5 6] ) Private declarations: _____________________ Any process expression e can be preceded by a sequence d of private declarations d: (d e) run ( new x:^[] ( x![] | x?[] = () ) ) run ( new x:^[] run x![] x?[] = () ) alpha-conversion for bound channel names: run ( ( new x:^[] ( x![] | x?[] = () ) ) | ( new x:^[] ( x![] | x?[] = () ) ) ) scope extrusion: run ( new x:^[] ( z!x | x?[] = print!"Continuing..." ) ) replication: in pi-calculus infinite behaviours provided by means of replication operrator !P (*P) in PICT, use of def construct def hailer [] = print!"Hail!" so, it is like replication restricted to input-prefixed processes process above equivalent to new hailer : ^[] run ( hailer?[] = print!"Hail!" | hailer?[] = print!"Hail!" | hailer?[] = print!"Hail!" | ... ) so hailer channel visible in the body of def and also after the def construct def hailer [] = print!"Hail!" run ( hailer![] | hailer![] | hailer![] | hailer![] | hailer![] ) Hail! Hail! Hail! Hail! Hail! recursive processes: def hailer n:Int = if (== n 0) then () else ( print!"Hail!" | hailer!(dec n) ) run hailer!5 Hail! Hail! Hail! Hail! Hail! mutually recursive processes: using def .. and .. and .. def ping n:Int = if (== n 0) then () else ( print!"ping" | pong!(dec n) ) and pong n:Int = if (== n 0) then () else ( print!"pong" | ping!(dec n) ) run ping!6 ping pong ping pong ping pong conditional processes: run if true then print!"branch1" else print!"branch2" run if false then print!"branch3" else print!"branch4" branch1 branch4 run ( if true then print!"branch1" else print!"branch2" | if false then print!"branch3" else print!"branch4" ) branch1 branch4 Version of "if" also for expressions (in the functional sense) Running the compiler pict prog.pi typechecking unwanted behaviours (other than pattern matching failure at run time) by means of Input an Output channels It is possible to declare a channel to be used ONLY for input or output Example: consumer-producer program def consumer channel:^Int = channel?i = ( printi!i | consumer!channel ) def producer [channel:^Int i:Int] = if (== i 0) then () else ( channel!i | producer![channel (dec i)] ) new ch:^Int run consumer!ch run producer![ch 4] 4 3 2 1 The producer, once received ch, could use it also to receive integers Violation of POLA!!! (PrincipleOfLeastAuthority) - - - - So we rewrite the program as def consumer channel:?Int = channel?i = ( printi!i | consumer!channel ) def producer [channel:!Int i:Int] = if (== i 0) then () else ( channel!i | producer![channel (dec i)] ) new ch:^Int run consumer!ch run producer![ch 4] 4 3 2 1 How can typecheck new ch:^Int ^^^^^^^ with def consumer channel:?Int = ... ? ^^^^^^^^^^^^ Subtyping is used by typececker!!!! ^Int < ?Int ^Int < !Int Exercises: -- describe the subsumption rule used by PICT type system -- define a formal system for subtype judgments of PICT types S < T Another refinement of channel types: Responsive Output channels sending on a channel of a "def" can be implemented essentially as a branch In fact, for channels created by def clauses: (1) There is always a receiver available (2) all communications received by the same receiver Sending on other channels more difficult to implement Types and Type system used to statically know which outputs will communicate with definitions channels created by def have type /T new x : ^[/Bool] def d b:Bool = if b then print!"True" else print!"False" run x![d] run x?[a] = a!false False from the type of x a!false knows that it is communicating with a def. So, types as sort of directives to the compiler ..... Useful... but I do not like it! /T < !T new y : ^[!Bool] run y![d] run y?[a] = a!false False An allowed cast from !T to /T The primitive channel "pr" pr : /[String /[]] Using pr: def d [] = print!"done" run pr!["pr... " d] pr... done new c : ^[] run pr!["pr... " c] run c?[] = print!"done" example.pi:2.20: Expected type does not match actual since ^[] is not a subtype of /[] since ^[] and /[] do not match allowed cast: new c : ^[] run pr!["pr... " (rchan c)] run c?[] = print!"done" pr... done In a programmaing language based on the notion of process, basic operations like addition, multiplication etc. are basic processes So, +, * etc. are channels through which we communicate with such basic processes + : /[Int Int /Int] Example: run +![10 20 printi] 30 run ( new r:^Int ( +![10 20 (rchan r)] | r?sum = *![sum 30 printi] ) ) 900 run ( new r1:^Int new r2:^Int ( +![10 20 (rchan r1)] | +![30 40 (rchan r2)] | r1?sum1 = r2?sum2 = *![sum1 sum2 printi] ) ) 2100 PICT has syntactic sugar that enables us to write such programs easier!!! Example: factorial (no syntactic sugar) run ( def fact [n:Int r:!Int] = ( new br:^Bool ( ==![n 0 (rchan br)] | br?b = if b then r!1 else ( new nr:^Int ( -![n 1 (rchan nr)] | nr?nMinus1 = (new fr:^Int ( fact![nMinus1 fr] | fr?f = *![f n (rchan r)] ) ) ) ) ) fact![5 printi] ) 120 Exercise: Fibonacci Characters and Strings ----------------------- print defined in terms of the lower-level predefined channel pr: def print s:String = ( def r[] = () pr![s r] ) ============================= A usual practice in programming language implementations: Define a Core Language and then define the whole language in terms of the core one. Before the actual translation, derived forms are translated into the core language. Motivation? programs easier to write and read. Complex values Simple values in the core language: (channel) variables basic values and channels (+,*,..) record values they are entities that can be passed along channels and partecipate in pattern matching PICT extends the core language with Complex values: expressions of the form (d v) to be used in any place a simple value is expected x!(new n:T n) x![23 (new x:A x) (new y:B y)] Notice that, since the name of a channel is a value we can also have something like (d v)!(d' v') We need to formally define the "meaning" of complex values -------- (1) first define a "continuation-passing" style interpretation of complex values, [[v → c ]] (2) then define a traslation for process expressions containing complex values Let v be a complex value and c a channel. We define [[v → c ]] denotes a process that evaluates v and sends the resulting simple value along c The interpretation [[-]] defined by induction on the syntax of value expressions [[x → c]] = c!x [[k → c]] = c!k [[(new x:T v) → c]] = (new x:T [[v → c]]) [[(def x1 a1 ... and xn an v) → c]] = (def x1 [[a1]] ... and xn [[an]] [[v → c]]) [[(run e v) → c]] = ([[e]] | [[v → c]]) [[if:T v then v1 else v2 → c]] = (def d x:Bool = if x then [[v1 → c]] else [[v2 → c]] [[v → d]]) SEE TEXT for records [l1 fv1...ln fvn] has type [l1FT1...lnFTn] _____________________________________________________ [[[l1fv1...lnfvn] → c]] = [[ | l1fv1...lnfvn → c]] [[l1fv1...lnfvn | → c]] = c![l1fv1...lnfvn] vi has type Ti ___________________________________________________________________________ [[l1fv1... | livi...lnfvn → c]] = (def ci xi:Ti = [[l1fv1...lixi | li+1fvi+1...lnfvn → c]] [[vi → ci]]) We can now translate process expression containing complex values: [[v1!v2]] = (def c1 x1:T1 = (def c2 x2:T2 = x1!x2 [[v2 → c2]]) [[v1 → c1]]) where T1 and T2 are the types calculated by the type-checker for v1 and v2 [[v?p=e]] = (def c x:T = x?p=[[e]] [[v → c]]) where T is the type calculated by the type-checker for v [[if v then e1 else e2]] = (def c x:Bool = if x then [[e1]] else [[e2]] [[v → c]]) Exercise: which approach to typing for PICT (a' la Curry, or Church) Value Declarations __________________ A construct to bind values to names: (val x = (new n:T [n n]) e) binds x to the result of evaluating (new n:T [n n]) and then executes e [[(val p = v e)]] = (def c p = [[e]] [[v → c]]) Since declarations can also appear in value expressions, we also need to add a clause to the definition of CPS-conversion for values: [[(val p = v1 v2) → c]] = (def d p = [[v2 → c]] [[v1 → d]]) Application ___________ We extend the syntax with (v v1...vn) which enables to look at def channels as functions def double [s:String r:/String] = +$![s s r] now, instead of double!["soothe" print] we can write print!(double "soothe") soothesoothe by interpreting (v v1...vn) as follows [[(v v1...vn) → c]] = [[ v![v1...vn c] ]] (we can also use labelled values l1 fv1 ... ) Let us see what is the result of the translation of print!(double "soothe") [[print!(double "soothe")]] = (def c1 x1:/String = (def c2 x2:String = x1!x2 [[(double "soothe") → c2]]) [[print → c1]]) = (def c1 x1:/String = (def c2 x2:String = x1!x2 [[ double!["soothe" c2] ]]) c1!print) = (for simplicity's sake we translate [[ double!["soothe" c2] ]] directly in double!["soothe" c2] . We can assume the translation of Core PICT expressions or processes to be the identity) (def c1 x1:/String = (def c2 x2:String = x1!x2 double!["soothe" c2]) c1!print) To convince ourselves that the above process does exactly what double!["soothe" print] does, let us execute it (mind that there is the def of double that precedes all) (def c1 x1:/String = (def c2 x2:String = x1!x2 double!["soothe" c2]) c1!print) --> (def c1 x1:/String = (def c2 x2:String = x1!x2 double!["soothe" c2]) (def c2 x2:String = print!x2 double!["soothe" c2]) ) --> (def c1 x1:/String = (def c2 x2:String = x1!x2 double!["soothe" c2]) (def c2 x2:String = print!x2 +$!["soothe" "soothe" c2])) the string appending process will then execute the process c2!"soothesoothe" whose execution will cause the execution of print!"soothesoothe" che provoca la stampa di soothesoothe Exercise: translate the following in core pict x![(* (+ 2 3) (+ 4 5))] Exercise: rewrite previous exercises (like fact or fibonacci) using application syntax (and also the function definition syntax as defined below) The defs that can be looked at as functions have the form def f [a1:A1 a2:A2 a3:A3 r:/T] = r!v (the v can be looked at as the "result" of the function that is given through the channel r) A special syntax for defs that can be looked at as function definitions instead of def f [a1:A1 a2:A2 a3:A3 r:/T] = r!v we can write def f (a1:A1 a2:A2 a3:A3):T = v Anonymous process declarations (def x p = e x) example of use: (def x p = e x)!v So, anonymous function can be defined by means of the following derived form \p=e stands for (is translated into) (def x p = e x) Exercise: we can write in PICT the following process new y: ^Int y!((\x=(+ x 1)) 4) Show, using the translation), that the effect of executing the above process is exactly that of executing y!5 Example: using the "for" process. "for" process defined in the standard library def for [min:Int max:Int f:![Int /[]] done:/[]] = ( def loop x:Int = if (<= x max) then ( new c : ^[] ( f![x (rchan c)] | c?[] = loop!(+ x 1) ) ) else done![] loop!min ) we can use "for" as in the following process run ( new done : ^[] ( for![ 1 4 \[x c] = (printi!x | c![]) (rchan done) ] | done?[] = print!"Done!") ) 1 2 3 4 Done! Another use of anonymous functions: definition of a record whose fields are two anonymous functions that take simply a syncronization signal as output. val r = [ one = \[] = print!"Low hangs the moon" two = \[] = print!"O it is lagging" ] run (r.one![] | r.two![]) O it is lagging Low hangs the moon Records can be used to implement distributed objects. Sequencing run ( val [] = (pr "the ") val [] = (pr "musical ") val [] = (pr "shuttle") () ) the musical shuttle See the (translation) semantics to understand its meaning “invoke an operation, wait for a signal as a result, and continue” appears so frequently that it is worth providing some convenient syntax. sequential declaration: v; is translated into val [] = v