💾 Archived View for schinkel.bevuta.com › articles › PCN-tutorial.txt captured on 2024-06-16 at 12:30:11.
⬅️ Previous capture (2024-05-12)
-=-=-=-=-=-=-
P r o g r a m C o m p o s i t i o n N o t a t i o n (PCN) A T u t o r i a l by Felix L. Winkelmann I. Introduction This document is an attempt to describe "PCN"[1], a concurrent logic programming language developed at the Argonnne National Laboratory in 1990. PCN is a successor of a number of languages mixing concurrent and logic programming that provides a familiar syntax based on C but retains the essential features which make this particular style of programming interesting. The tutorial is based on "FLENG"[2], my implementation of a low-level language with the same name[10], which provides front-ends for PCN, "Flat Guarded Horn Clauses"[3] (FGHC) and "Strand"[4] and is available for UNIX-compatible operating systems. The FLENG system provides several extensions and a few minor differences to PCN as described in the available technical reports [5,6] for the original version. Concurrent logic programming is a fascinating paradigm, but, unfortunately lost in history, partly as a result of the fallout from the "AI winter". As modern computing systems struggle to find ways to use parallelism and concurrency to the extent now necessary to ensure high performance and to fully utilize available computing resources it may be worthwhile to look at concepts that have existed now for quite some time, yet have been forgotten, and which appear to promise a refreshing and easy to use method of writing correct and robust software systems in the presence of multiprocessing. II. History PCN is a member of a family of concurrent logic programming languages, an attempt to allow for more concurrency in languages based on PROLOG. One of the first was "Concurrent Prolog", followed by a plethora of descendants like "Guarded Horn Clauses" (GHC), "PARLOG", "Janus" and "Strand". The Japanese "Fifth Generation Computing Project"[7] was an ambitious undertaking to create advanced knowledge representation and research applications during the AI boom, where several experimental systems used "KL1"[8], another concurrent logic language based on (Flat) GHC. In the process of the development of these languages a trend in removing features of classic PROLOG can be noticed, with more emphasis on efficiently implementing concurrency[9]. For example, backtracking was removed and certain constraints were imposed to make the execution more streamlined and reduce implementation complexities. Most of the languages in this family are based on a number of common concepts: - "Committed choice" clause selection: once a solution is found, no backtracking is allowed. - Logic single-assignment variables. - "Flatness": clause-selection can only be constrained to a fixed set of built-in predicates.. - Fine-grained "AND" parallelism. With the exception of PCN, most languages retain the PROLOG syntax, while significantly changing the semantics. PCN uses a more traditional syntax based on C. Additionally, PCN can be considered a "composition language" for orchestrating existing sequential code in a concurrent environment, so the familiar syntax and ease of interfacing reduces the friction naturally imposed by mixing languages with different paradigms. PCN is still an imperative language but restricts side effects by design and convention, a necessary step when working with high levels of concurrency and parallelism. In what follows, no prior knowledge of PROLOG or logic programming is needed, the concepts originating in logic languages will be described in the context of PCN as they come up. Where the syntax or semantics of a particular feature differ from the original version, I describe it as implemented in [2]. Where examples are given, they are self-contained and can be directly executed by compiling and running the given piece of code. III. Basic Program Structure PCN largely follows C in terms of lexical syntax and comment format ("//" for single line comments, "/* ... */" for non-nested block comments). Programs are composed of procedures and functions, containing statements. Statements are separated by comma (",") or semicolon (";"). Execution starts in procedure "main": main() { writeln("Hello, world!") // write textual representation of } // some data object terminated by // newline If you have the FLENG system built and installed, you can compile and execute the example code like this: % cat hello.pcn main() { writeln("Hello, world!") } % fleng hello.pcn % ./a.out The keywords "char", "int", "double", "byte", "default", "idle", "over", "return", "foreign_call", "if", "else", "with", "let" and "function" are reserved and may not be used as variable or procedure names. IV. Values PCN handles values of data types usually found in dynamic programming languages: - Signed integers of machine-word size (31 or 63 bits, one bit used for marking). - Strings ("interned" text sequences also called atoms or symbols in other languages), enclosed in double quotes: "this is a string" - Characters, in the full UNICODE range, allowing the same escape sequences as strings. - Extended precision floating point numbers (64 bit IEEE doubles). - Lists of values, optionally with an explicit tail value: main() { writeln(["one", 2, 3.0]), writeln(["one" | [2 | [3.0 | []]]]) // writes exactly the same } - Tuples, which are fixed-size arrays or arbitrary values: main() { writeln({"one", 2, 3.0}) } - Arrays, fixed size homogenous numeric sequences of arbitrary length, with data types char (byte), short (16-bit integers), int (32-bit integers), long (32- or 64-bit integers, depending on platform) and floats (64-bit IEEE doubles). - Sub-arrays or "Views" are arrays sharing storage with other arrays or parts of existing arrays. Strings may not be longer than 255 bytes, supported escape sequences are: \t (tab), \n (newline), \", \xXX and \uXXXX for the UNICODE code points given in 2 or 4 hex digits. As string size is limited, larger text is usually handled in the form of "character lists", i.e. linked lists of UNICODE code points. This also has the advantage that normal list-processing operations transparently handle text encoding in this way. Also, for reasons explained later, working with text in this manner is less inefficient than it appears and provides ample opportunities to process text in parallel. Memory management in PCN automatic, unused data is garbage collected once it is no longer used. PCN is dynamically typed, only mutable variables and arrays have to be declared. Circular data structures are not supported. This simplifies the exchange of structured data between threads with separate heaps considerably. V. Variables Variables come in two flavors: "definitional" and "mutable". Mutable variables are scalars or arrays of one of the numeric types char, short, int, long and double. Mutable variables must be explicitly declared and can be changed (hence "mutable") during their lifetime. main() int x; // declares a mutable variable { x := 123, // assigns a value x := 456, // assigns another value writeln(x) // writes "456" } Mutable variables are initially bound to an unspecified value. Definitional variables on the other hand do not require a declaration and can hold values of every type but can only be assigned once. Unassigned definitional variables are first-class objects, they may be assigned to other definitional variables and stored in data structures. Unassigned (unbound) definitional variables can be considered "holes", to be filled with a value later. Referencing a definitional variable will automatically "dereference" any chain of links between unbound variables and access the final value. main() { y = x, // binds "y" to "x", even though "x" has no value, yet. x = 123, // binds definitional variable "x" // x = 456, <-- would be illegal (already bound) writeln(y) // writes "123" } When the concrete value of a definitional variable is required, either for some computation or comparison, then execution of the current process "suspends" until the variable is bound. main() { writeln(x), // results in "deadlock": x isn't bound yet x = 123 } The ability to work with unbound variables is a tremendous source of power and fundamental to the concepts used in PCN (and other concurrent logic languages) for communication with and the synchronization of concurrent processes. ":=" is the assignment operator and must have a mutable variable or array location at the left hand side. Definitions use the "=" operator and are only allowed for definitional variables. Certain restrictions apply about the use of mutable variables which will be described later. The special variable "_" is called the "anonymous" variable - every occurrence represents a fresh definitional unbound variable: main() { lst = [_, _, _], // a list of 3 unbound variables writeln(length(lst)) // writes "3" and does not suspend // as the elements are not needed } VI. Processes and Compositions The basic execution model of PCN programs can be described as a pool of currently active processes, where processes are taken from in some unspecified order and executed. Executing processes will start other processes which are themselves added to the pool. Processes that need access to definitional variables that are still unbound will be suspended. Once the pool is empty, the program has terminated, or, if processes are still suspended on unbound definitional variables, a "deadlock" is occurs as execution can not continue without some active process that may bind the variables processes are yet suspended on. In PCN code for processes is defined in procedures and functions and process-invocations are grouped inside "compositions". A procedure is defined by giving its name followed by a possibly empty list of parameter declarations and a composition, enclosed in "{...}", where the components of the composition are separated by "," or ";" (a final "," or ";" terminator is allowed but not required). main() { proc() } proc() { writeln("proc called") } A procedure is identified by its name and "arity" (the number of arguments it has declared) and multiple procedures of the same name may be defined if they have different arities. Compositions are statements and thus may be arbitrarily nested. Variable scope is always the whole procedure: variables are shared across all nested compositions inside a procedure definition. Three types of compositions exist: - Sequential composition: each statement in the composition is executed one after the other, in textual order: main() {; // <- sequential composition writeln(1), writeln(2) } The ";" may be omitted in a sequential composition. In the code examples here we will do so unless we want to emphasize the sequential nature of some code. - Parallel composition: each statement is executed in parallel: main() {|| write(x), // suspends until "x" is bound write(2), // may print "12" or "21" x = 1 } The order is completely undefined, but all statements will execute at some point, unless they are suspended. - "Choice" composition: execute statements depending on conditional guards: main() { x = 2 + 2, {? x == 4 -> writeln("ok"), default -> writeln("something's wrong") } } A choice composition contains a list of rules, separated by "," or ";". A rule consists of one or more conditions (called "guards"), separated by ",", an arrow ("->") and a statement to be executed when the conditions all succeed. Should multiple rules succeed, one of them will be arbitrarily picked and executed. The "default" condition always succeeds if no other rule does. If no "default" rule is given and no rule succeeds, execution continues normally and the composition is equivalent to an empty statement. If the body of a procedure consists of a choice composition that holds only a single rule then the braces can be omitted: main() { check(2, 2, 4) } check(a, b, c) a + b == c -> writeln("everything still ok") Functions are special procedures returning a value: main() { writeln(square(3)) } function square(x) { return(x * x) } Note that "return" does not change the control flow: it merely stores the result to be returned on function exit: main() { writeln(square(3)) } function square(x) { return(x * x), writeln("done") // still shown } This may seem unexpected but makes sense when the body of the function contains parallel compositions, which always treats the statements in the body as unordered. VII. Calls, Terms and Expressions The right-hand side of definitions and assignments and arguments to procedure calls can be expressions and are evaluated in the familiar manner, as long as referenced definitional variables are bound. An argument to a procedure or the right-hand side of a definition may also be a "term", which designates a data structure like a list or tuple. Nested elements of terms are _not_ evaluated themselves, but may refer to other variables: main() { x = 2 + 2, y = [x, 3 * 3], writeln(y) // writes the list "[4, *(3,3)]" // (this shows an alternative notation // for tuples, equivalent to {"'*", 3, 3}) } Procedure and function arguments are evaluated when they are expressions. As unbound variables are first-class values, passing such a variable effectively is "by-reference": main() { proc(2 + 2, x), writeln(x) // writes "5" } proc(a, b) { b = a + 1 } This also applies to mutable variables: main() int x; { proc(2 + 2, x), writeln(x) // writes "5" as well } proc(a, b) int b; // shares location of "x" in "main" above { b := a + 1 } Mutable variables that are not directly passed to procedures as arguments are always dereferenced, including arrays. An array passed to a procedure but not declared as an array in the callee will be "cloned" and an immutable copy being assigned to the parameter representing the argument: main() int a[10]; { a[ 0 ] := 123, clone(a), modify(a), writeln(a[ 0 ]) // writes "456" } clone(a) { // ... here "a" is immutable and does not share space with // the "a" in main } modify(a) int a[]; // declares parameter to be an array { a[ 0 ] := 456 // here "a" shares memory with "a" in // main and can be modified } The reason for these restrictions is that mutable variables (including arrays) must obey three rules to minimize time-dependent bugs in parallel compositions: Rule 1: A mutable variable can be shared by statements in a parallel composition only if no statement modifies the variable (note that these rules are only partially enforced by the compiler and run-time system). Rule 2: When a mutable variable occurs on the right-hand side of a definition statement, a "snapshort" (copy) of the current value of the mutable is taken, and the definition then proceeds as if a definitional value were involved. Rule 3: When a definitional variable occurs on the right-hand side of an assignment to a mutable variable, the assignment suspends until the variable has a value and then proceeds. PCN supports tail-call optimization: if the last expression in a sequential composition (that is not contained in another sequential composition) is not a quantification, it is automatically compiled as being in a parallel context and does not wait for the operation to complete (and thus does not maintain additional state or data). VIII. Guards and Control Structures Choice compositions are the only primitive method of performing conditional execution and the set of guards or conditions is fixed to one of the following: - Comparisons, binary relations as in C ("==", "!=", ">", "<", ">=", "=<"), where "==" and "!=" handle arbitrary terms and the rest allow numerical expressions. - Type tests ("number(x)", "string(x)", etc.) - Matching ("<variable> ?= <pattern>"), the variable must be definitional and the pattern is a term. Comparisons and tests evaluate their argument expressions and thus suspend if any definitional variable inside the guard is unbound: main() {|| even_odd(x), // suspends inside the choice below x = 2 // resumes suspended choice and writes "even" } even_odd(n) {? n & 1 == 0 -> writeln("even"), default -> writeln("odd") } Matching allows the destructuring of data objects like lists and tuples: main() { x = ["one", 2], {? x ?= [a, b] -> y = [b, a] }, writeln(y) // writes "[2, one]" } Matching suspends only when the pattern requires dereferencing an unbound definitional variable, but when it does, the whole composition is suspended. User-defined or primitive functions may not be used directly in guards, only if part of a comparison or test. Guard expressions must be "atomic" in that only a fixed set of expressions is allowed, for semantic reasons (using the jargon from concurrent logic languages, PCN is "flat"). For convenience, calls to primitive or user-defined functions are syntactically valid but are extracted from the actual choice composition and evaluated before the set of choices is entered. As any variables introduced by match guards are not yet bound during the execution of an embedded function-call, its arguments may not refer to those variables: : if(foo ?= {a, b}, some_function(a)) ... // <-- ILLEGAL : Any expression or term that is used as a guard that is not a type-test, comparison or match is treated as a comparison with "false", so if(some_test(...)) ... is equivalent to if(some_test(...) != "false") ... Similarly if(!some_test(...)) ... equals if(some_test(...) == "false") ... Some convenience forms are available as syntactic sugar that allow one to write choice compositions in a more traditional style. First, "let", for a choice-composition that consists of a single rule and which aborts with an error if the guard fails: main() { x = ["one", 2], let x ?= [a, b]; // note the use of ";" as "," would be ambiguous y = [b, a], // equivalent to // {? <guard> -> <statements>, default -> <error> } writeln(y) } Second, the familiar "if": main() { x = 2 + 2; if(x == 4) writeln("ok") // equivalent to else writeln("no!") // {? <guard> -> <if-statement>, default -> <else-statement> } } Iteration is performed with the "quantification" statement: main() { i over 1..10 :: writeln(i) // write numbers 1 to 10 } Whether the body statement is executed sequentially or in parallel depends on the type of the enclosing composition. The quantification variable ("i" in the example above) may be definitional (if undeclared) or mutable. In the former case, it is rebound during execution of the body and remains unbound in later statements following the quantification and can be re-used. If it is mutable, it is assigned the next value on every iteration and retains its value after the iteration completes. Iteration over lists is also supported: main() { x over [1, 2, 3] :: writeln(x) // write numbers 1 to 3 } In list-iteration, the quantification variable must always be definitional. Since PCN is tail-recursive, more complex iteration patterns are best expressed as recursive functions as the iteration runs in constant space: /* filter even numbers given as arguments on the command line */ main() { filter_even(command_line()) // obtain arguments } filter_even(args) {? args ?= [x|rest] -> { // destructure in head + tail n = string_to_integer(x); // arguments are strings if(n & 1 == 0) writeln(x); filter_even(rest) } // no match: do nothing, i.e. simply return } IX. Streams and Ports Now that we have concurrently running processes and automatic suspension when a definitional variable is unbound, we can easily synchronize processes: main() {|| work(done); // start some long-running task wait(done) // wait until it finished } work(done) { timer(1000, done, _) // primitive to set timer for 1000ms and assign // the empty list ("[]") to "done" when finished // the third argument (timer-ID) is not used } wait(done) {? data(done) -> writeln("ready") // data(): guard that succeeds when argument // is bound (and waits, if needed) } Combined with lists, we already have everything to establish communication channels between processes, by using a list with an unbound tail and successively bind the tail to a new list with another unbound tail and so on: main() {|| producer(10, stream); consumer(stream) } producer(n, out) {? n > 0 -> { out = [n|out2]; // create new stream element producer(n - 1, out2) }, default -> out = [] // close stream } consumer(in) {? in ?= [x|more] -> { writeln(x); // writes 10, 9, 8, ..., 1 consumer(more) } } Note how these two processes synchronize automatically: should the producer outrun the consumer, the list merely becomes longer. If the consumer executes faster than the producer it will suspend until the stream contains a new value in its head. We can make the consumer control the pace in which the producer generates values by reversing the role of who provides objects to consume - just use another stream to send unbound definitional variables to be filled by the producer: main() {|| producer(10, stream, [_|fill]); // provide initial slot for first item consumer(stream, fill) } producer(n, out, fill) {? fill ?= [place|more], n > 0 -> { place = n; // take element from placeholder stream and define it out = [place|out2]; // create new output-stream element producer(n - 1, out2, more) }, default -> out = [] } consumer(in, fill) {? in ?= [x|more] -> { writeln(x); // writes 10, 9, 8, ..., 1 fill = [_|fill2]; // create new slot and pass it to producer consumer(more, fill2) } } Streams of this sort are very useful not only as a communication device, they also serve as abstraction boundaries: a process can provide a "service", with requests sent to the process via a stream: main() {|| service(s, []); // start server s = [{"set", "waldo", 42}|s2]; // request setting a DB entry //... s2 = [{"get", "waldo", x}|s3]; // request reading an entry writeln(x); // writes "42" s3 = [] // close stream to terminate service } service(in, db) {? in ?= [{"set", key, val}|in2] -> { map:insert(key, val, db, db2); // call "insert" in the "map" library service(in2, db2) }, in ?= [{"get", key, val}|in2] -> { map:lookup(key, db, val); service(in2, db) } } Notice how we pass values ("set") and placeholders ("get") in the different requests. This is easy to extend and uses the built-in "map" lihrary which provides a balanced tree data structure wiht functional updates. We can easily intercept messages by using an intermediate process, here for logging: main() {|| service(s, []); logger(s): s = [{"set", "waldo", 42}|s2]; // request setting a DB entry //... s2 = [{"get", "waldo", n}|s3]; writeln(n); s3 = [] // close stream to terminate service } logger(in) {? in ?= [{op, key, _}|in2] -> { // use "fmt" library for formatted output fmt:format("log: ~a ~a\n", [op, key]); logger(in2) } } service(in, db) {? in ?= [{"set", key, val}|in2] -> { map:insert(key, val, db, db2); service(in2, db2) }, in ?= [{"get", key, val}|in2] -> { map:lookup(key, db, val); service(in2, db) } } Or perhaps we want to restrict access to certain data by using an intermediate stream: main() {|| service(s1, []); // [] represents an empty map filter(["waldo"], s, s1); s = [{"set", "waldo", 42}|s2]; // request setting a DB entry //... s2 = [{"get", "waldo", n}|s3]; writeln(n); s3 = [] // close stream to terminate service } filter(blacklist, in, out) {? in ?= [{"get", key, result}|in2] -> { list:member(key, blacklist, f); // check for list membership if(f) { result = "CONFIDENTIAL"; // request is not passed on filter(blacklist, in2, out) } else { out = [{"get", key, result}|out2]; filter(blacklist, in2, out2) } }, in == [] -> out = []; default -> let in ?= [req|in2] -> { out = [req|out2]; // pass on unchanged filter(blacklist, in2, out2) } } service(in, db) {? in ?= [{"set", key, val}|in2] -> { map:insert(key, val, db, db2); service(in2, db2) }, in ?= [{"get", key, val}|in2] -> { map:lookup(key, db, val); service(in2, db) } } Note how the original service process (stream consumer) never needs to be changed - we can connect software components by streams through which they communicate and add intermediate processes the perform various pre- and post-processing. Since lists and streams are structurally equivalent, using lists containing character codes makes them natural subjects for stream processing: /* convert input to uppercase */ main() {|| io:read_char_stream(0, in); // read characters from stdin upcase(in) } upcase(in) {? in ?= [c|in2] -> { if(c >= 'a', c <= 'z') c2 = c - 32 else c2 = c; fmt:format("~c", [c2]); upcase(in2) } } Using the "merger" primitive we can join multiple streams into one. "merger" takes an input-stream and copies its values into an output stream, but merges streams given by messages of the form {"merge", S} into the output. The values from different streams are arbitrarily interleaved but retain their order: /* count char-frequency of combined input files given on command line */ main() {|| merger(in, out); follow(command_line(), in); stats(out, []) } follow(files, in) {? files ?= [fname|next] -> { open_file(fname, "r", fd); // open file for reading io:read_char_stream(fd, chars); // read characters from file // as a stream in = [{"merge", chars}|in2]; follow(next, in2) }, files == [] -> in = [] // end input stream } stats(chars, map) {? chars ?= [c|chars2] -> { // notice how we already replace the entry by "new", which // is still unbound at this point map:replace(c, new, old, map, map2); if(old == []) new = 1 // indicates key in map doesn't exist else new = old + 1; stats(chars2, map2) }, chars == [] -> { map:map_to_list(map, lst); // converts map into sorted list of dump(lst) // {"-", KEY, VALUE} tuples } } dump(data) {? data ?= [{_, c, count}|more] -> { fmt:format("~c\t~d\n", [c, count]); dump(more) } } Streams are identified by their tail, the next slot to be filled with a new list cell holding a value and the new tail. This makes it awkward to store streams in data structures or global variables as they effectively have a new identity once extended with a fresh value. For this FLENG supports "ports", which are objects representing a stream: main() {|| open_port(p, s); // create port, associated with a stream put_global("port", p); // store port in thread-global variable consume(s); // process stream as usual work() } consume(s) {? s ?= [x|s2] -> { writeln(x); consume(s2) } } // we could of course just pass "p" as an argument, this code is // only intended to demonstrate storing a port reference somewhere work() { global("port", p); // suspends until variable is set send(p, "message"); // add value to stream put_global("port", []) // release, so it can be closed } Streams attached to ports are "closed" (terminated by the empty list), once the port is not longer referenced anywhere and can be garbage collected. IX. Using Threads PCN, like most implementations of concurrent logic languages is intended to run on multiple "nodes" or hardware processors, often on widely different platforms. The FLENG system utilizes operating system threads by allowing to spawn processes on another thread, running fully in parallel to the main thread. So far we have used concurrent processes running in a single OS thread. Each thread in the FLENG system has its own heap and process pool and doesn't share memory (with the exception of arrays, if desired) with any other thread. Values can be exchanged between processes running on different OS-level threads by passing definitional variables and binding them, which is transparently converted into messages following an internal value-exchange protocol. To launch a process on another thread, use the "at" ("@") notation: main() {|| spawn(x)@fwd; // invoke "spawn" on next thread in ring writeln(x) // suspends until "x" is bound by the other thread } spawn(x) { id = self(); // "self" gives thread ID fmt:format_chars("Hello, I'm #~d", [id], x) // formats into a char-list } When you compile this program, you can set the number of threads you require: % fleng hello2.pcn % ./a.out +THREADS 2 The right-hand side of the "@" operator may be an expression evaluating to an integer thread-ID or a string. The special identifiers "fwd", "bwd", "north", "south", "west" or "east" are also allowed and equivalent to their string form. Threads are addressed by a positive integer, the main thread has the ID "1". Threads can be addressed directly, via their ID or relatively, according to a topology in which all available threads are grouped. Currently "ring" and "torus" topologies are available, so "fwd" and "bwd" address the next or previous thread in a ring and "north", "east", "south", "west" address the four neighbours in a torus topology. The actual number of threads does not need to be reflected in the source code - the program above would still work on a single thread, since the next item in the ring would be the very same thread and invoking the current thread using the "@" notation is the same as calling the procedure without any annotation. Synchronization and communication between processes running on different threads is handled transparently, but note that data sent from one thread must be copied and re-created on the receiving side and so involves allocation and should be of limited size. Mutable variables are passed by value when given in a call decorated with "@", there locations can not be shared between threads. X. Modules and Separate Compilation PCN code can be structured into separately compiled modules, which are then linked together like normal C or C++ code. Say we have two files a.pcn: the main program main() { b:wave() } and b.pcn, a library module -module("b") // a "declaration" that names this compilation unit wave() { writeln("o/") } The files can be compiled and linked into a single executable: % fleng -c b.pcn # produces b.o % fleng a.pcn b.o % ./a.out Calls to procedures and functions in other modules need to be "qualified" with the module name and colon: "<name>:<procedure>(...)". Modules are resolved at the time of the first call, so mutually referencing modules are no problem. There can only be a single main module (internally named "", i.e. the empty string). A module can be declared by using the "module" declaration, as in the example above, or by giving the command line option ("-m") and a module name when compiling the file. A module always represents a single source file and there can not be more than one module in a file, nor can a module span more than one source file. Compiled modules can be combined into libraries just as in C or C++. There are no facilities for "importing" bindings from other modules, entities in modules other than the current one always require a qualificiation. There is also no mechanism to restrict what definitions are accessible from outside of the current module. All procedure and function definitions are exported and accessible from other modules that refer to them. A module is loaded when some other module declares it as "used". Usage in explicit calls to procedures of a module is an indicator for this and thus inferred automatically. But if a module is called indirectly (explained below) a "uses" declaration has to be added. One situation where this may occur is when the used module is intended to automatically start with some background task, which is done using the "initialization" declaration: it marks a zero-argument procedure to be called on thread startup. a.pcn: /* note: never stops, as the clock runs continuously */ -uses("b") main() { // ... } b.pcn: -module("b") -initialization("start") start() {|| clock(1000, clk, _); // produces a stream of integers spin(clk, 1) // in 1-second intervals } spin(clk, secs) let clk ?= [_|clk2] -> { fmt:format("seconds elapsed: ~d\n", [secs]); spin(clk2, secs + 1) } XI. Indirect Calls and Higher-Order Procedures Sometimes it is useful to be able to determine the target of a procedure call dynamically. PCN has "meta" calls for this purpose, where the module, procedure name (or both) is dynamically computed: /* convert input to uppercase or lowercase, depending on program-name so compile like this: % fleng a.pcn -o upcase % ln -s upcase downcase */ main(_, argv, _) // alternative signature: (argc, argv, status) {|| prg = argv[ 0 ]; // get first element of argv list (program name) path:basename(prg, name); // drop directory part + extension mode = list_to_string(name); // basename returns a character list io:read_char_stream(0, in); // read characters from stdin convert(in, mode) } convert(in, conv) {? in ?= [c|in2] -> { `conv`(c, c2); // call procedure with name given by "conv" fmt:format("~c", [c2]); convert(in2) } } upcase(c, result) { if(c >= 'a', c <= 'z') result = c - 32 else result = c } downcase(c, result) { if(c >= 'A', c <= 'Z') result = c + 32 else result = c } The part inside the `...` must be a definitional variable containing a string when in the module part. The procedure part of the call may be a string or a tuple holding the name (a string) and any number of default arguments preceding the arguments given in the call. If the meta-call is not qualified, the procedure part may also be a tuple of the form {":", MODULE, PROCEDURE}, where PROCEDURE may again be a string or tuple. main() { m = ""; // this module p = "foo"; p1 = {"foo", 123}; p2 = {"foo", 123, 456}; pm = {":", m, p1}; f = `(x) -> foo(123, x); `p`(123, 456); // all of these write "[123, 456]" `p1`(456); `p2`; `pm`(456); `f`(456) } foo(a, b) { writeln([a, b]) } The example above demonstrates "lambda" expressions, anonymous functions of the form `(<argument list>) -> <statement>. As `...` adds given default arguments in the call value, we effectively have "closures", that is, we can combine a piece of code with state, here also using the ``...`` notation to construct tuple representing calls conveniently: main() { adder = make_adder(123); writeln(`adder`(1)) } function make_adder(x) { return(``plus(x)``) // same as {":", "", {"plus", x}} } plus(x, y, z) { z = x + y } This allows the use of higher order procedures, procedures that call other procedures: /* write list of file given on command line that are directories */ main() { filter(``isdir``, command_line(), lst); writeln(lst) } isdir(x, r) { file_type(x, t); if(t == "directory") r = true else r = false } filter(func, lst, result) {? lst ?= [x|lst2] -> {|| `func`(x, r); if(r) result = [x|result2] else result = result2; filter(func, lst2, result2) }, default -> result = [] } If no module qualification is given, then the procedure to be called is assumed by built-in calling primitives like "call" and "apply" to be in the module that issues the call. Call targets defined using the ``...`` notation implicitly include this information, so it is preferrable to use this instead of constructing tuples by hand. XII. Interfacing with C/C++ Code To do anything useful, it must be possible to talk to external code like operating system APIs and third party libraries. This is relatively straightforward in PCN and allows it to be used as a language to "orchestrate" sequential native code using a higher level notation that is particularly well suited to coordinate concurrency and parallelism. Calls to undefined procedures which have no module-prefix are assumed to invoke a separately linked C function. Arguments are converted as necessary and are call by reference: main() { printf("hello, world.\n") // this is C's "printf" provided by libc } (This example may fail to link on some platforms, due to dynamic linking and calls via dynamic call stubs. An easy workaround is to use an intermediate function.) PCN passes the source code through the C preprocessor, so conditional compilation, inclusion, platform enquiries and macro definitions as in C are supported. During preprocessing a PCN source file, the macro "PCN" is defined. This allows expanding included files conditionally on whether they are currently processed by the PCN compiler or in a C program, which is handy when interfacing with foreign code that shares certain macro definitions. External calls are only allowed for procedures, not for functions inside expression. Arguments to C functions are converted depending on the type of the argument. All arguments are passed by reference, so mutable variables can be modified by the external code. Strings are passed as "char" pointers and are internally terminated by a zero byte, so that they match C's representation of strings. Arrays are passed as a pointers to the first element. Integers and floats that are passed as literals or as results of expressions are by reference but may not be modified as they refer to internal storage. Lists, tuples, ports and other data is passed as an undefined pointer and not usable on the C side. References to unbound definitional variables will suspend until all arguments are bound. Here we embed C code directly into the PCN source file. The code could also be stored in a separate file and compiled into object code, which would then have to be linked to the final program: /* format current time sing strftime(3) */ main() char buf[ 32 ]; // storage for buffer int len; { tm = current_seconds(); fmt_time(tm, "%F %R", buf, length(buf), len); array:get(0, len, buf, str); fmt:format("~s\n", [str]) } "C": #include <time.h> void fmt_time(long *t, char *fmt, char *buf, long *maxlen, int *len) { time_t t0 = *t; struct tm *ts = localtime(&t0); *len = strftime(buf, *maxlen, fmt, ts); } The "C" marks a section of C code and must be at the start of the source code line. "C++" is also a valid foreign code section marker. You can switch back to PCN code by using a "PCN" section marker. The "fleng" compiler driver will extract C/C++ code section and compile and link them automatically. As a further example, let's integrate Paul Heckbert's "Business Card Raytracer"[11] to take advantage of multiple threads: file "card.pcn": main() char buf[ 512 * 512 * 3 ]; { n = threads() - 1; // number of threads if(n == 0) error("invoke with +THREADS 2 or more"); {|| i over 2 .. threads() :: spawn(n, buf)@i }; write("P6 512 512 255 "); array:write(0, length(buf), buf, 1) // write out array to stdout } spawn(parts, buf) char buf[]; { step = 512 / parts; part = self() - 2; // "self" returns own thread-ID ts(buf, part, step) } file "card.cpp": #include <stdlib.h> #include <stdio.h> #include <math.h> typedef int i;typedef float f;struct v{f x,y,z;v operator+(v r){return v(x+r.x ,y+r.y,z+r.z);}v operator*(f r){ return v(x*r,y*r,z*r);}f operator%(v r){return x*r.x+y*r.y+z*r.z;}v(){}v operator^(v r){return v(y*r.z-z*r.y,z*r.x-x*r.z,x*r. y-y*r.x);}v(f a,f b,f c){x=a;y=b;z=c;}v operator!(){return*this*(1/sqrt(*this%* this));}};i G[]={247570,280596,280600,249748,18578,18577,231184,16,16 };f R(){return(f)rand()/RAND_MAX;}i T(v o,v d,f&t,v&n){t=1e9;i m=0;f p= -o.z/d.z; if(.01<p)t=p, n=v(0,0,1),m=1;for(i k=19;k--;)for(i j=9;j--;)if(G[j]& 1<<k){v p=o+v(-k,0,-j-4);f b=p%d,c=p%p-1,q=b*b-c;if(q>0){f s=-b-sqrt(q);if(s<t &&s>.01)t=s,n=!(p+d*t),m=2;}}return m;}v S(v o,v d){f t;v n;i m=T(o,d,t,n);if( !m)return v(.7,.6,1)*pow(1-d.z,4);v h=o+d*t,l=!(v(9+R(),9+R(),16)+h*-1),r=d+n* (n%d*-2);f b=l%n;if(b<0||T(h,l,t,n))b=0;f p=pow(l%r*(b>0),99);if(m&1){h=h*.2; return((i)(ceil(h.x)+ceil(h.y))&1?v(3,1,1):v(3,3,3))*(b*.2+.1);}return v(p,p,p )+S(h,r)*.5;}extern "C" void ts(char *buf,long *pt,long *st){ i sl=*pt * *st, off=512-sl;char *ptr=buf+sl*512*3;v g=!v(-6,-16,0),a=!(v(0,0, 1)^g)*.002,b=!(g^a)*.002,c=(a+b)*-256+g;for(i y=0;y<*st;++y)for(i x=512;x--;){v p (9,9,9);for(i r=64;r--;){v t=a*(R()-.5)*99+b*(R()-.5)*99;p=S(v(17,16,8)+t,!(t* -1+(a*(R()+x)+b*(off-y+R())+c)*16))*3.5+p;}*(ptr++)=(i)p.x;*(ptr++)=(i)p.y; *(ptr++)=(i)p.z;}} Compile and run like this: % fleng -O3 card.pcn card.cpp % ./a.out +THREADS 9 > aek.ppm # runs with 8 rendering threads XIII. Miscellaneous Features Due to the mostly side-effect free nature of code written in PCN, state is often updated in a functionally manner, by passing it along creating a new state from the old and so "threading" updated values through a chain of variables. Also, values are often accumulated, as for example when streams are built. To ease handling of paired accumulation arguments, this implementation of PCN provides a "variable pair" notation, similar to the argument pair notation used in KL1[8]. If the parameter list of a procedure definition contains a parameter prefixed by a colon (":abc"), then the parameter represents a pair of arguments, an input and an output argument that is practical for creating streams or passing on state. A colon-prefixed reference to such an argument pair in the argument list of a procedure call on the other hand will also be rewritten to a pair of arguments: /* average numbers given on command line */ main() { writeln(average(command_line())) } function average(lst) { average2(lst, 0, a1); return(a1 / length(lst)) } average2(lst, :a) lst ?= [n|lst2] -> { a = a + string_to_integer(n); // or: a += string_to_integer(n) average2(lst2, :a) } (This could of course be done using mutable variables, but once we want to parallelize code functional updates are the safer and often more correct method) The ":a" in "average2" in both occurences represents two arguments, the old (first) and the new (second) part of the argument pair. A definition of a paired variable stands for the new value, so the "a" in "a = a + n" is equivalent to "a_new = a_old + n". All other unqualified references to a paired variable refer to the old (current) value. PCN provides the "<op>=" operators known from C as syntactic sugar for updating pair variables. They are in all respects identical to the longer form "<identifier> = <identifier> <op> <expression>" and are only usable for definitional variables. There is no "default" clause in "average2" in the example above - in this case the new value defaults to the old, passing on the state unchanged, which is the default action, if a pair is not referenced at all, so: main() { foo(1, x); writeln(x) // simply writes "1" } foo(:_) {} Another example: main() {|| build(10, s, []); follow(s) // writes numbers "10" to "1" } follow(s) s ?= [x|s2] -> { writeln(x); follow(s2) } build(n, :s) {? n > 0 -> { s ++ n; build(n - 1, :s) } } Here we build a stream of integers. The use of the "++" operator is equivalent to "s_old = [n|s_new]": it constructs the stream value by value. This is the only operation that assigns of the first/input variable in a variable pair. Once "n" equals zero, the use of an unchanged argument pair will simply unify the input part of the pair with the output part, which is the empty list. "build" effectively injects values into a stream between the head (the first occurrence of "s") and the tail (the empty list) and once we are done we connect our current end of the stream with the tail, the stream ends and "follow" terminates. Lists and tuples can be used to hold structured data but a more convenient way is to use "structs", which are named tuples where fields can be accessed by name: -struct(point(x, y)) main() { // using the structure name as a function constructs a // new instance: p1 = point(100, 200); p2 = move(p1, 50, -10); writeln(p2) // writes "point(150, 190)" } function move(p, x, y) { return(point(p.x + x, p.y + y)) } Functional update is done with the "<instance>(.<field> = ...)" syntax: -struct(vector(x, y, len)) main() { p1 = vector(10, 20, 0); p2 = p1(.len = sqrt(p1.x * p1.x + p1.y * p1.y)); writeln(p2.len) / writes "22.360679774997" } Finally, the "<--" operator can be used to update a fields in structured data: -struct(point(x, y)) main() { p1 = point(100, 200); // create a {"point", 100, 200} tuple move(10, -20, p1, p2) } move(x, y, :p) { p.y <-- p.y - n } The "<--" operator also accepts lists and normal tuples by taking an index instead of a field name. XIV. Further Reading This completes this little tutorial on PCN. I hope I could interest you in the language and concepts used. Concurrent logic programming is a fascinating subject and addresses many common programming problems in a refreshing manner. I recommend reading the "Strand" Book[4] to learn more about various interesting techniques, which the book explains in an easy and often delightful manner. More information about the "FLENG" system can be found at my website[2], including reference documentation for the languages it supports and many libraries that provide extended functionality. If you have questions, suggestions or want to report errors, please go to the FLENG website where you will find contact information. XV. References [1] https://en.wikipedia.org/wiki/Program_Composition_Notation [2] http://www.call-with-current-continuation.org/fleng/fleng.html [3] "Guarded Horn Clauses" PhD Thesis, Kazunori Ueda [4] "Strand: New Concepts for Parallel Programming", Ian Foster and Stephen Taylor, Prentice Hall, 1990 Also available online: http://www.call-with-current-continuation.org/files/strand-book.pdf [5] "Parallel programming with PCN" Ian Foster and Steven Tuecke https://digital.library.unt.edu/ark:/67531/metadc1310344/ [6] "A Primer for Program Composition Notation." Chandy, K. Mani and Taylor, Stephen (1990), http://resolver.caltech.edu/CaltechCSTR:1990.cs-tr-90-10 [7] https://en.wikipedia.org/wiki/Fifth_generation_computer [8] "Design of the Kernel Language for the Parallel Inference Machine", U. Kazunori et al., 1990 [9] "The Deevolution of Concurrent Logic Programming Languages" Evan Tick [10] "FLENG Prolog - The Language which turns Supercomputers into Parallel Prolog Machines" Martin Nilsson and Hidehiko Tanaka [11] https://fabiensanglard.net/rayTracing_back_of_business_card/