Just joining us? We’re writing a compiler for the π-calculus in F#—Part I explains. Continuing from last time? Need a refresher? We finished the compiler and runtime library. Now it’s time to play!
Let’s see if this all works by creating a new C# project. Reference Pirt.dll and echo.dll:
using System.Threading;
class EchoClient
{
static void Main()
{
// create channel 'e'
Channel e = new Channel("e");
// start the echo program
new Thread(new ThreadStart(new Main(e).Run)).Start();
// call echo
Channel k = new Channel("k");
e.Send(k);
Channel f = k.Receive();
f.Send(new Channel("hello!"));
Channel r = new Channel("r");
f.Send(r);
System.Console.WriteLine(r.Receive().Name);
}
}
Let’s see if it runs:
$ csc /t:exe echoclient.cs /r:pirt.dll /r:echo.dll
Microsoft (R) Visual C# 2005 Compiler version 8.00.50727.42
for Microsoft (R) Windows (R) 2005 Framework version 2.0.50727
Copyright (C) Microsoft Corporation 2001-2005. All rights reserved.
$ echoclient
hello!
^C
$ _
Fairly tedious from C#, but it works!
Evaluating the Compiler
Some parts of the compiler are arguably fragile because they rely on other parts behaving just so. Here are some examples:
- The code generation for Repeat relies on Par reusing the existing thread for its second process. If Par changed to start two new threads, or reuse the existing thread for the first process instead of the second, Repeat would emit a branch instruction between methods which isn’t allowed in the CLR.
- How free variables are passed to processes is defined between Par and some code in gen_closure; the mapping of free variables to fields via constructor parameters is never made explicit. Both of these parts just know what to expect.
- gen_closures tears down the process × type ref mapping even as it is being created by gen_proc, which makes gen_closures rely on the fact that gen_proc never needs to look up type refs for a process it has already generated a closure for.
- gen_closure knows that gen_proc never uses more than two slots on the evaluation stack, and it uses a constant for the stack depth. The way gen_closure determines the number of local variables, from the extant bindings, is similar.
Although these all seem like egregious shortcomings, I believe there’s a point where making the coupling explicit (usually by extending the context and passing around data to connect two disconnected phases) is becomes more burdensome than remembering certain conventions exist between the two disparate pieces of code.
Variants of the π-Calculus
Messaging and channels make the π-calculus good for modeling distributed systems, but it is rather poor for implementing them! That’s because, in a distributed π system, two processes on different sides of the world can both say receive on channel x, and expect the contention to be resolved. Doing so at such a great distance is brittle or expensive. The join-calculus is similar process calculus with messages and channels, but receiving on a channel is limited to a particular scope, and in a distributed setting it follows intuition that messages must be sent to a well-defined location for the contention can be resolved locally. My research language and compiler, GPSL, is based on the join calculus. I recommend you read Cédric Fournet’s join calculus tutorial.
Another popular application of π-calculus is in modeling cryptographic protocols. The spi calculus uses type systems to ensure the secrecy of channel names.
There are even variants of the π-calculus, mostly for modeling biological systems, where we involve specific probabilities with Q and R getting y.
Further Reading
If you’re interested in learning more: Wikipedia has a brief overview of the π-calculus; Robin Milner’s Communicating and Mobile Systems: the π-calculus is a concise tutorial introduction and even extends to modeling object-orientation in π-calculus; Davide Sangiorgi’s The π-Calculus goes into detail about typed π-calculus, more detail about object encoding, and a π-calculus language Pict.
(* Fslex generated parsers follow the same pattern as OCamllex *)
(* and Mossmllex generated parsers, and do not update line number *)
(* information automatically, partly because the knowledge of when *)
(* a newline has occured is best placed in the lexer rules. *)
(* Thus the following boiler-plate code is very useful *)
{pos with pos_lnum = lnum+1; pos_bol = bol }
( inc_lnum (lexeme_end lexbuf) (lexeme_end_p lexbuf))
let newline = ('\n' | '\r' '\n')
| newline { newline lexbuf; token lexbuf }
| ['a'-'z']['a'-'z' '0'-'9']* { ID(lexeme lexbuf) }
%token LPAREN RPAREN LANGLE RANGLE INERT SEQ PAR REP NEW EOF
| Proc1 PAR Proc { Par ($1, $3) }
| ID LPAREN ID RPAREN SEQ Proc1 { Recv ($1, $3, $6) }
| ID LANGLE ID RANGLE SEQ Proc1 { Send ($1, $3, $6) }
| LPAREN NEW ID RPAREN SEQ Proc1 { New ($3, $6) }
open Microsoft.AbstractIL.BinaryWriter
open Microsoft.AbstractIL.Internal.Bytes
open Microsoft.AbstractIL.Internal.Nums
open Microsoft.AbstractIL.Internal.Zset
let var_order (x: id) (y: id) =
union (free_vars p) (free_vars q)
add ch (remove v (free_vars p))
failwith "free_vars: free_vars of branch"
(* Scope of the pic runtime assembly *)
(* the channel type in the runtime library *)
mk_nongeneric_boxed_typ (mk_tref (runtime_scope, "Channel"))
mk_ctor_mspec_for_typ (channel_typ, [typ_String])
mk_nongeneric_instance_mspec_in_typ (channel_typ, "Send", [channel_typ], Type_void)
mk_nongeneric_instance_mspec_in_typ (channel_typ, "Receive", [], channel_typ)
mk_tref (mscorlib_scoref, "System.Threading.ThreadStart")
mk_nongeneric_boxed_typ tref_ThreadStart
mk_ctor_mspec_for_typ (typ_ThreadStart, [typ_Object; typ_IntPtr])
mk_tref (mscorlib_scoref, "System.Threading.Thread")
mk_nongeneric_boxed_typ tref_Thread
mk_ctor_mspec_for_typ (typ_Thread, [typ_ThreadStart])
mk_nongeneric_instance_mspec_in_typ (typ_Thread, "Start", [], Type_void)
type storage_context = (id * storage) list
type closure_context = (proc * type_ref) list
type context = storage_context * closure_context
match List.assoc var (fst ctx) with
failwith ("undeclared variable '" ^ var ^ "' in scope variables: " ^ any_to_string (fst ctx))
List.filter is_local (fst ctx)
let n = int_to_u16 (List.length (locals_of_context ctx)) in
((var, Local n) :: fst ctx, snd ctx)
Par (desugar_proc p, desugar_proc q)
Par (desugar_proc (Repeat p), desugar_proc (Repeat q))
failwith "desugar_proc: desugar_proc of branch"
(* we need to keep running desugar_proc until it reaches a fixed
let rec gen_proc p entry_label ctx =
let q_entry = generate_code_label "" in
let q_code, ctx' = gen_proc q q_entry ctx in
join_code send_code q_code, ctx'
let q_entry = generate_code_label "" in
let store, ctx' = mk_store ctx v in
let q_code, ctx'' = gen_proc q q_entry ctx' in
join_code recv_code q_code, ctx''
let p_entry = generate_code_label "" in
let store, ctx' = mk_store ctx v in
([I_ldstr (string_as_unicode_bytes v);
mk_normal_newobj channel_ctor_mspec] @
let p_code, ctx'' = gen_proc p p_entry ctx' in
join_code new_code p_code, ctx''
let p_tref = mk_tref (ScopeRef_local,
generate_code_label "Closure") in
let ctx' = (fst ctx, snd ctx @ [p, p_tref]) in
let vs = elements (free_vars p) in
let p_ctor_mspec = mk_ctor_mspec_for_nongeneric_boxed_tref
(p_tref, List.map (fun _ -> channel_typ) vs) in
let q_entry = generate_code_label "" in
(List.flatten (List.map (mk_load ctx') vs) @
[mk_normal_newobj p_ctor_mspec;
mk_normal_newobj mspec_ThreadStart_ctor;
mk_normal_newobj mspec_Thread_ctor;
mk_normal_callvirt mspec_Thread_Start;
let q_code, ctx'' = gen_proc q q_entry ctx' in
join_code p_code q_code, ctx''
let loop_entry = generate_code_label "" in
failwith "gen_proc: gen_proc of non-canonical process"
failwith "gen_proc: encountered repeat-branch"
let p' = Send (ch, v, Par (q, Branch entry_label)) in
let p' = Recv (ch, v, Par (q, Branch entry_label)) in
let p' = New (v, Par (q, Branch entry_label)) in
bblockInstrs = [| I_br label |] },
let gen_closure p tref (tdefs, trefs) =
let typ = mk_nongeneric_boxed_typ tref in
(* fields for captured variables *)
let vs = elements (free_vars p) in
let fss = List.map (fun v -> mk_fspec_in_typ (typ, v, channel_typ)) vs in
let store = List.map2 (fun v f -> v, Field f) vs fss in
let fds = List.map (fun v -> mk_instance_fdef (v, channel_typ, None, MemAccess_private)) vs in
let ctor_params = List.map (fun v -> mk_param (Some v, channel_typ)) vs in
let ctor_entry = generate_code_label "" in
let ctor_code = nonbranching_instrs_then_ret ctor_entry (List.flatten
mk_normal_call (mk_nongeneric_instance_mspec_in_typ (typ_Object, ".ctor", [], Type_void))] ::
let ctor_body = mk_ilmbody (true, [], 2, ctor_code, None) in
let ctor_mdef = mk_ctor (MemAccess_public, ctor_params, MethodBody_il ctor_body) in
let run_entry = generate_code_label "" in
let run_code, ctx = gen_proc p run_entry (store, trefs) in
List.map (fun _ -> mk_local channel_typ) (locals_of_context ctx),
let run_mdef = mk_instance_mdef ("Run", MemAccess_public, [], Type_void, MethodBody_il run_body) in
tdEncoding = TypeEncoding_autochar;
tdMethodDefs = mk_mdefs [ctor_mdef; run_mdef];
tdSecurityDecls = mk_security_decls [];
tdInitSemantics = TypeInit_beforefield;
tdProperties = mk_properties [];
tdCustomAttrs = mk_custom_attrs [] } in
f (gen_closure q tref (tdefs, trefs'))
f ([], [p, mk_tref (ScopeRef_local, "Main")])
if Array.length Sys.argv <> 2 || not (File.Exists(Sys.argv.(1)))
let filename = Sys.argv.(1) in
using (new StreamReader(filename)) (fun input ->
let lexbuf = Lexing.from_stream_reader input in
try Pars.start Lex.token lexbuf
let pos = Lexing.lexbuf_curr_p lexbuf in
printf "%s (%d, %d): parse error: %s"
(pos.pos_cnum - pos.pos_bol + 1)
let tds = gen_closures !proc in
let assem_name = Path.GetFileNameWithoutExtension(filename) in
let modul_name = assem_name ^ ".dll" in
let output_filename = Path.Combine(Path.GetDirectoryName(filename), modul_name) in
let modul = mk_simple_mainmod assem_name modul_name true (mk_tdefs tds) in
write_binary output_filename None None None modul
The runtime library, channel.cs:
private volatile Channel channel;
private readonly AutoResetEvent sending;
private readonly AutoResetEvent receiving;
this.sending = new AutoResetEvent(false);
this.receiving = new AutoResetEvent(false);
public void Send(Channel channel)
Channel result = this.channel;
!e(k).(new f).k<f>.f(x).f(r).r<x>.0
A C# driver program, echoclient.cs:
new Thread(new ThreadStart(new Main(e).Run)).Start();
f.Send(new Channel("hello!"));
System.Console.WriteLine(r.Receive().Name);
Comments:
what a great find! thanks for posting this. i can't wait to get my hands dirty. — cor.byt