_______ _______ __ _ ______ |______ | |______ | \ | | ____ | |_____ |______ | \_| |_____| User Manual Version: 12 I. Introduction This document describes an implementation of the "FLENG" programming language as described in [1]. FLENG is a very minimal and low-level committed-choice concurrent logic language descending from Concurrent Prolog. The implementation described here provides a FLENG compiler that generates assembly language for x86-64, ARM and RISCV machines on UNIX or BSD systems. Additionally a front end for "Flat Guarded Horn Clauses" (FGHC)[2] and "Strand"[5] is provided, which are slightly higher level and more convenient languages. II. Usage The compiler is invoked by using the "fleng" driver script: fleng [-whkdcepnt] [--version] [CCOPT ...] [-m MODULE] [-I DIRECTORY] FILENAME [-o OUTFILE] [OBJECTS ...] The script takes FLENG, Strand or FGHC source files and generates assembler, binary object files or executables. The following command line options are supported: --version Print version and exit. -w Disable compiler warnings. -k Keep intermediate files. -c Compile to object file, do not create an executable. -p Generate profiling information. -n Show name of each clause group as it is compiled. -t Print internal compiler information about known types and inlining optimizations. -cflags Output C compiler options suitable for compiling C code interfacing to FGHC/FLENG. -libs Output libraries needed to be linked to compiled FGHC/FLENG code. -link FILENAME Use entry-point information previously generated using the "-e" option for faster cross-module calls. -e Generate entry-point information instead of a compiled target file, for use with "-link". -m MODULE Specifies the name of the compiled module. If no module name is given the module name defaults to "" (empty), which designates a main module. -I DIRECTORY Prepend list of directories searched when locating files accessed via the "include" declaration. The default include directory is ".". -o OUTFILE Produce OUTFILE and exit. Depending on the file extension, the compilation process will stop after generating a FLENG source file (.fl or .fleng) an assembler file (.s), a binary object file (.o) or an executable (any other extension). As default output filename the basename of the source file is given and an executable file is created. -d Enable debug information, which shows slightly more information in debug logs and enables some additional error checking. -h Show usage information. Binary object files can be linked together to produce executables consisting of multiple compiled modules. The "fleng" driver accepts *.o and *.a files on the command line and links them to any generated executable. To compile an executable program, one main module must be given when linking. If the source file name has an .fl or .fleng extension, then the "fleng" driver assumes it contains FLENG code. In all other cases the file is considered to be an FGHC or Strand source file. The compiler options are also documented in the fleng(1) manual page. Generated wrapper code using the "foreign/1" declaration is detected and compiled automatically when binary files are produced. Separate compilation is straightforward by compiling library modules into object files that can then be linked with the main module. All compiled executables accept a number of run-time command line options to enable debugging output and override internal settings: [+HELP] [+VERSION] [+LOG ] [-LOGX ] [+HEAP ] [+GOALS ] [+TIMESLICE ] [+LOGFILE ] [+THREADS ] [+STATS ] [+STAT_TICKS ] [+HEAPSTATS ] [+PROFILE ] [--] ... +VERSION Show version with which this executable was built. +LOG Enable logging for all threads specified by , where bit 1 corresponds to thread 1 and so on. +LOGX Enable explicit logging with "log/1" for all threads specified by , where bit 1 corresponds to thread 1 and so on. +HEAP Set default heap size for each thread in bytes. The default size is 5000000 bytes. The value may be suffixed by "k", "m" or "g". +GOALS Set the default number of goals a thread can have active at the same time. The default number is 100000. The value may be suffixed by "k", "m" or "g". +LOGFILE Specifies the location of the log file where debug information is written to, if logging is enabled for one or more threads. By default logging output is written to standard error. +THREADS Specifies the number of threads to be used and defaults to 1. +TIMESLICE Sets the number of goal-invocations after which a process yields, is added to the active process queue and another active process is scheduled. +STATS Write statistics to the log file for all threads selected by the . The statistics are given in textual format and have the following format: ## G Q S C THREAD gives the ordinal number of the thread, starting at 1. TICKS is the number of internal clock ticks of the thread. ACTIVE is the number of active goals. SUSPENDED is the number of inactive, suspended threads. CELLS is the number of used cells in the heap. +STAT_TICKS Sets the number of internal clock ticks, after which statistics are written for a thread. The default is 250. Whenever a new process is scheduled, the internal clock is increased. +HEAPSTATS Enable heap statistics, similar to "+STATS". The format is as follows: #$ T L V F P TUPLES, LISTS, VARS, FLOATS and PORTS give the number of heap cells storing objects of the corresponding type at the time the statistics where written. Note that tuple arguments are stored in a list, so TUPLES gives the number of heap cells holding tuple heads. +OCCURS_CHECK Enable "occurs check" to enable possibly circular variable unifications. This has a significant performance impact and is by default disabled. +PROFILE When the executable has been compiled with the "-p" option, write a dump with profiling information to the given file after execution completed and the program terminated without errors. The generated file will contain a breakdown of how much time was spent in all user processes. +HELP Shows run-time option usage information. -- Disables any further run-time option processing. The run-time options are also documented in the fleng(7) manual page. FLENG and FGHC programs usually spawn many processes that run in parallel. This implementation can utilize native threads to partition process pools into separately executing entities to take advantage of multi-core CPUs. III. The FLENG Language For a full desription of FLENG, see [1]. This implementation provides a superset including additional computation operators for numerical calculations. A very short description of the language is given here. Some familiarity with Prolog is helpful to understand the syntax and concepts used, but Prolog has quite a different evaluation model and one is not required to be proficient in it to learn FLENG, which is in a sense considerably simpler. A FLENG program consists of a set of "process definitions", where each definition is a set of clauses of the same name and arity: process_name(Argument1, ...) :- Goal1, ... . Arguments are "matched" to the actual argument given when the process is created. Goals are new processes to be invoked when the clause is selected. When a process is invoked, one clause of its definition that matches the given arguments is selected and executed, by spawning a new process for each goal in the body, for example: % write each element of the given list: walk([]) :- true. walk([_|Y]) :- walk(Y). main :- walk([1, 2, 3]). Each clause must have a body containing a list of goals, separated by comma (","). Note that all body goals are truly executed in parallel, not in the order in which they are specified. If no clause matches the arguments, then the process "fails", resulting in a run-time error. Argument variable bindings take place when clause heads are matched and variables are distinguished by starting with an uppercase letter or the "_" character. A variable in a clause head matches whatever is passed as argument to clause invocation. Logic variables passed as arguments on the other hand can only be bound by explicit unification in the body. Matching a non-variable argument with an unbound variable passed by the caller, the process selection is suspended until the variable has been bound. Here an example: try([Y]) :- true. main :- try(X), wait(X). wait(X) :- X = 123. Here "try" will suspend until the unification of "X" with 123 is performed. Once a logic variable is bound, all goals suspended on the variable will be made active and are scheduled to be executed at some stage, after which they may continue or suspend on other variables. The variable named "_" is always unique, multiple occurrences of "_" represent different anonymous variables. These are normally used as "don't care" patterns when matching. Once the head of a clause is fully matched, the clause "commits" and the body goals are spawned. All other alternative clauses will be ignored, there is no "backtracking" as in Prolog. One specific feature of FLENG is the "!" operator. When used in the head of a clause, a variable prefixed with "!" will suspend until the variable is bound, regardless of whether the argument is subsequently used or not: try1([X]) :- true. try2([!X]) :- true. main :- try([Z]). try(Y) :- try1(Y), try2(Y). "try1" will match and complete, but "try2" will suspend forever, since it waits for the first element of the given list to be bound to some value. In FLENG the same argument variable may not appear more than once in a clause head. A FLENG program terminates once there are no more active goals. If there are still suspended goals, then a deadlock has occurred and the program aborts with an error message. The only built-in primitives in FLENG are "compute/{3,4}", "unify/3", "true/0", "call/1", "=/2" and "foreign_call/1", described later in this manual. There are further undocumented primitives which are used for translated FGHC code. Declarations define additional properties of the compiled module and have the following form: -declaration(Argument, ...). Currently supported declarations in FLENG are "include/1", "initialization/1", "arguments/1", "exports/1", "module/1", "foreign/1" and "uses/1", which are described below. Some additional declarations exist for internal use but are not documented here. Declarations apply to the whole source file and are processed before all other clauses, regardless of where the declaration is located in the source file. FLENG is intentionally minimal to make implementing it easier. For user programming it is recommended to use the FGHC front end described below. IV. The FGHC Language "FGHC" or "Flat Guarded Horn Clauses" is another dialect of Concurrent Prolog, descended from "GHC" which is described in [2]. It provides clause guards, arithmetic expression evaluation and some other syntactically convenient features. See also [3] for a general overview over the many concurrent logic language dialects and how they relate to each other. FGHC is "flat", meaning that only built-in guard-expressions are allowed in the guard part of a clause, no user defined process definitions can be used as guards. "Strand" can be considered a slightly restricted subset of FGHC, so code written in Strand is compiled as FGHC and can take advantage of FGHC features like general body unfication. An FGHC clause has the following general format: process_name(Argument1, ...) :- Guard1, ... | Goal1, ... . The "Guard1, ... |" part may be omitted. If no guards and no goals exist, the the clause may be written as process_name(Argument1, ...). If guards exist but no goals need to be evaluated, use the following form: process_name(Argument1, ...) :- Guard1, ... | true. A guard is one of a set of predefined guard expressions that are executed to determine if the clause commits or not. If a guard expression fails, then another clause is selected until one clause commits or the whole process invocation fails. Both FLENG and FGHC support the following data types: signed integers 123 0xF00F 0'A floating point numbers 1.23 strings (atoms) abc 'one\ntwo' lists [1, 2, 3] [head|tail] "abc" tuples foo(bar, baz) {x, y, z} variables Abc _123 _ ports modules The atom "[]" designates the empty list. Anonymous variables ("_") as in Prolog are supported. Character lists enclosed in double quotes are represented as a list of UNICODE code points. "Ports" are identity-preserving stream containers, as described in [4], Circular data structures are not allowed. This is currently only superficially checked at run-time. Tuples of the form "{...}' may not be empty. Note that "abc(def)" is equivalent to "{abc, def}". Care must be taken when having multiple clauses that overlap in the arguments they match: if multiple clauses match at the invocation of the associated process definition, then it is unspecified which of the candidate clauses will "fire" and be committed. You can narrow the set of matching clauses by using guards if you want to avoid this non-determinism: big(N, F) :- N > 1000 | F = yes. big(_, F) :- F = no. In an invocation of "big(1001)" the second clause may match, since matching is non-deterministic. Add another guard or "otherwise" to remove this ambiguity: big(N, F) :- N > 1000 | F = yes. big(_, F) :- N =< 1000 | F = no. % alternatively: "otherwise/0" Declarations as in FLENG are supported. FGHC also allows using the "!" annotation. The restriction that the same variable may not occur more than once in a clause head is lifted and equivalent to binding a fresh variable and adding a "==/2" guard matching both variables. Prolog's single line ("% ...") and block ("/* ... */") comments are supported in FGHC/Strand and FLENG. The main differences between FGHC and Prolog can be summarized as follows: - FGHC is a committed-choice language and has no backtracking: once a clause head matches and the guards succeed, all remaining solutions are abandoned. - All goals of a clause are executed in parallel, in an arbitrary order. - FGHC has no general unification - clause heads and guards only match (and suspend if unbound variables are matched with non-variable arguments), variables are assigned exclusively by "=/2", "unify/3", "assign/3", "is/2" and primitives that unify results with argument variables. - Matching a non-variable with an unbound variable or using it in a guard other than "unknown/1" suspends the current process until the variable has been bound. For a nore in-depth description of concurrent logic languages, the "Strand" book [5] is highly recommended as it explains the concepts and techniques in detail and gives many examples (The book is available online, see the "References" section for details). Strand is very similar to FGHC, but only provides assignment instead of unification. FGHC is similar to "KL1"[6], which was a low level language used in Japan's Fifth Generation Computing project (FGCS)[9]. This implementation of FGHC also supports paired arguments, an extension from KL1, that simplifies handling argument pairs. A preprocessing stage transforms clause heads and goals of a certain form in a manner described as follows: (this description was taken from the "KLIC" manual) The head and goals in body parts of a clause can have argument pairs specified by a single variable name attached to the head or goals by a hyphen character. We call such a pseudo variable an "argument pair name": p(X,Y)-Pair :- q(X)-Pair, s(Z)-Pair, r(Pair,Y), t(Z)-Pair. The pseudo-variable "Pair" is an argument pair name. Such a clause is interpreted the same as the following clause: p(X,Y,P0,P) :- q(X,P0,P1), s(Z,P1,P2), r(P2,Y), t(Z,P2,P). Occurrences of argument pair names attached to the head or goals by a hyphen character are interpreted as a pair of two different variables added at the end of the argument lists. In what follows, we call the two variables generated from an paired argument an "expanded pair". The second of an expanded pair of a goal is the same as the first of the expanded pair of the next goal with the same argument pair name. In the example above, "P1" appearing as the third argument of the goal of "q/3" also appears as the second argument of "s/3", as originally they both have the same argument pair name "Pair". The first of an expanded pair in the head will be the same as the first of the expanded pair in the first goal in the clause with the same argument pair name. The second of an expanded pair in the head will be the same as the second of the expanded pair in the last goal with the same argument pair name. In the above example, the first of the expanded pair "P0" in the head appears again as the second argument of the first goal calling "q/3", and "P", the second of the expanded pair in the head, appears again as the third argument of the last goal of "t/3". If the argument pair name appears only in the head, two variables of the expanded pair are unified in the body. For example, a clause: p(X)-Y :- q(X). is expanded into the following. p(X,Y0,Y) :- Y0=Y, q(X). An argument pair name may appear at a usual argument position rather than being attached to the head or goals, as does the first argument of the goal for "r/2" in the above example. In such a case, it is expanded to a single variable. This variable is the same as the second of the last expanded pair and is also the same as the first of the next expanded pair. Thus, in the above example, "Pair" appearing as the first argument of "r/2" is expanded into "P2", which is the same as the third argument of "s/3" and the second argument of "t/3". Arbitrarily many argument pair names can be specified for a head or a goal. For example, a clause such as: p-X-Y :- q-X, r-Y, s-Y-X. is interpreted as follows: p(X0,X,Y0,Y) :- q(X0,X1), r(Y0,Y1), s(Y1,Y,X1,X). Sometimes, specifying normal arguments after some argument pair names is desirable. This can be done by connecting them with a plus ("+") character. For example: p-X+Y :- q-X+35, r(Y), s+Y-X. is interpreted as follows: p(X0,X,Y) :- q(X0,X1,35), r(Y), s(Y,X1,X). Note that the expansion rules for paired arguments described above are position sensitive for goals. However, this does not mean that the execution order of body goals are constrained anyhow. Also note that the argument pair notation is no more than macro expansion of clauses. One predicate may have clauses some of which written in the argument pair notation and others in the usual notation. V. Builtin Primitives and Support Libraries This sections describes the available primitive operations available in FGHC code. These primitives may be implemented as calls to the run-time system, as calls to external C libraries, or as library predicates written in FGHC or FLENG. Arguments with the suffix "^" will be unified with a result value, arguments with the suffix "?" are expected to be non-variable ("ground") values. If a primitive is said to "signal an error", then the program will report an error message and terminate. Most library primitives "force" their arguments, if required, by suspending the calling process if an argument is an unbound variable. Notable exceptions are "compute/{3, 4}" and "unify/3". Note that if primitives require arguments of type "string", they expect an (interned) atom (abc, 'a string'), not a character list ("a list of characters"). If a primitive accepts both a string or a character list in an argument position, it specifically says so. * Primitives by group: Control ,/2 call/1 call/2 call/3 apply/2 run/2 true/0 when/2 &/2 ->/2 ;/2 Unification and assignment =/2 :=/2 assign/2 assign/3 unify/3 unify_with_occurs_check/3 deref/1 deref/2 Conversion number_to_list/3 number_to_list/4 real_to_list/2 string_to_real/2 string_to_integer/2 string_to_integer/3 string_to_list/3 string_to_byte_list/3 list_to_string/2 list_to_tuple/2 list_to_number/2 list_to_number/3 list_to_real/2 tuple_to_list/3 utf_encode/2 utf_encode/3 utf_decode/3 Lists length/2 Files close_file/2 close_file/3 delete_file/1 delete_file/2 rmdir/1 rmdir/2 open_file/3 file_exists/2 file_type/2 file_size/2 chmod/2 chmod/3 Input/Output listen/2, write/1 write/2 writeln/1 writeln/2 write_file/3 write_file/4 read_file/4 Numeric computations compute/3 compute/4 is/2 Program environment command_line/1 halt/0 halt/1 getpid/1 current_seconds/1 signal/3 clock/3 timer/3 cancel_timer/1 cancel_timer/2 restart_timer/2 restart_timer/3 randomize/1 randomize/2 error/1 log/1 log/2 environment/1 statistics/1 heap_statistics/1 Threads threads/1 self/1 idle_thread/1 @/2 Ports open_port/2 send/2 send/3 Modules get_module/2 all_modules/1 current_module/1 module_exports/2 module_name/2 Streams merger/2 Tuples put_arg/3 put_arg/4 get_arg/3 get_arg/4 make_tuple/2 Global Variables global/2 get_global/2 get_global/3 put_global/2 put_global/3 Ordering compare/3 Foreign Functions foreign_call/1 Paired Arguments +=/2 -=/2 *=/2 /=/2 =>/2 <=/2 <==/2 Guards ==/2 =\=/2 =:=/2 \=:=/2 >/2 =/2 =/2 @=/2 @= GOAL1 GUARD -> GOAL1; GOAL2 Executes GOAL1 if the guard expression GUARD evaluates to true or executes GOAL2, otherwise. If GOAL2 is not given, it defaults to "true". GOAL1 & GOAL2 Execute GOAL1 in a new task (see also "call/3") and then, once GOAL1 and all child processes created by GOAL1 have successfully executed, execute GOAL2. Note that "&/2" binds stronger than ",/2". Operationally, "X & Y" is equivalent to "(call(X, S), when(S, Y))". X = Y Unifies X with Y by recursively comparing the values in X and Y. Unbound variables will be bound. In FLENG a failed unification will silently be ignored. Note tha variables in X or Y may be partially bound when the unification fails. In FGHC a failed unification will signal an error. X := Y Equivalent to "assign(Y, X, _)". S <= M Equivalent to "S0 = [M|S1]" where S is a paired argument. M => S Equivalent to "[M|S0] = S1" where S is a paired argument. S <== X Equivalent to "S1 = X" where S is a paired argument. The previous value is lost and the next occurrence of S will mean X instead. S += E S -= E S *= E S /= E Equivalent to "S1 is S0 + E" ("-" ...), where S is a paired argument. all_modules(LIST^) Unifies LIST with a list of all linked modules. apply(GOAL?, LIST?) Appends the arguments in LIST to GOAL, which should be a string or a tuple, and invokes it with "call/1". GOAL must not be an invocation of a primitive operation. assign(VAR^, VAL?) assign(VAR^, VAL?, DONE^) Assigns VAL to the variable VAR, which must be unbound and unifies DONE with the empty list, when the assignment is completed. VAR must be currently unbound or contain a value identical to VAL or an error is signalled. call(GOAL?) call(GOAL?, STATUS^) call(GOAL?, ENVIRONMENT?, STATUS^) Invokes the process specified by GOAL which must be a tuple or string. If GOAL is not a variable, then it may refer to a primitive operation (a suitable user-defined wrapper clause is synthesized by the compiler), but if it is a variable, then it must refer to a user defined process. GOAL may refer to a module-qualified term and must be exported from the module in which it is defined. "call/2" and "call/3" execute GOAL in a new group of processes called a "task". Every process created in GOAL belongs to the same task and once all processes in that group have terminated (including child tasks), unifies STATUS with the empty list. IF ENVIRONMENT is given, it is provided as a parameter associated with the task group and can be accessed by the "environment/1" primitive, otherwise the new task inherits the same environment as the current task (if any). Tasks may span multiple threads, a task is considered completed when all processes of the task and it's child tasks in all threads have terminated. cancel_timer(ID?) cancel_timer(ID?, RESULT^) Cancels and removes the timer identifier by ID, previously created by calling "timer/3" or "clock/3" and unifies RESULT with the "true" when the operation is complete. The variable associated with the timer is assigned the string "false" or an empty list, depending on whether the timer is single-shot or periodic. If no timer with the given ID is currently active, then this operation does nothing and unifies RESULT with "false" (if given). chdir(DIRECTORY?) chdir(DIRECTORY?, DONE^) Changes the current directory to DIRECTORY, which should be a string or a character list. When the operation is complete, DONE is unified with the empty list. chmod(FILENAME?, MODE?) chmod(FILENAME?, MODE?, DONE^) Changes file permission bits of the file with the given name to MODE, which must be an integer and unifies DONE with the empty list when the operation is completed. In case of an error, DONE is unified with a tuple of the form "error(ERRNO)", where ERRNO is the UNIX error code. clock(MS?, VAR^, ID^) Establishes a periodic timer and assigns an integer identifier to ID that can be used to cancel the timer. Every MS milliseconds an integer indicating the number of expirations since the last will be assigned to the stream in VAR. close_file(FILE?, DONE^) Closes the open file with the descriptor FILE and unifies DONE with the empty list when the operation is completed. command_line(LIST^) Unifies LIST with a list of strings holding the command line arguments to the currently executing program, not including the program name and any run-time options. compare(X?, Y?, RESULT^) Unifies RESULT with -1, 0 or 1, depending on whether X is ordered below, equal or above Y, as compared with '@>'/2 and '@<'/2. compute(OP?, ARG?, RESULT^) compute(OP?, ARG1?, ARG2?, RESULT^) Performs a unary or binary numeric computation. OP must be a string and may be one of the following: Unary Binary Result + - x Sum - - x Difference * - x Product / - x Quotient mod - x Remainder and - x Bitwise AND or - x Bitwise OR xor - x Bitwise XOR shl - x Shift bits left shr - x Arithmetic shift bits right = - x Equality comparison > - x Is ARG1 numerically greater than ARG2 < - x Is ARG1 numerically less than ARG2 min - x Return lesser of the two arguments max - x Return greater of the two arguments sametype - x Have ARG1 an ARG2 the same type ** - x Exponentiation sqrt x - Square root sin x - Sine (radians) cos x - Cosine (radians) tan x - Tangent atan x - Arc tangent abs x - Absolute value sign x - Sign (returns integer) rnd x - Returns random integer real_integer_part x - Extract integer part of float real_fractional_part x - Extract fractional part of float integer x - Convert float to integer real x - Convert integer to float truncate x - Truncate float floor x - Round float to next lower integer round x - Round float ceiling x - Round float to next higher integer +, -, * and / produce real results if one of the arguments is a real number. "abs" returns a result of the same type as the argument. "**" always produces a real result. "and", "or", "xor", "rnd", "shl" and "shr" require integer arguments. With the exception of "=", "sametype" the arguments must be bound to numbers. Note that arguments to "compute" are not forced if unbound. Note that integer over- and underflow is not detected and will result in significant bits being silently truncated. "sametype" will not force unbound arguments (the type of the argument is considered "var" in that case). See "is/2" for a more convenient way to perform arithmetic computations. current_module(MODULE^) Assigns the module containing this expression to MODULE. current_seconds(SECONDS^) Unifies SECONDS with the current UNIX timestamp in seconds since 00:00, Jan. 1, 1970. delete_file(FILENAME?) delete_file(FILENAME?, DONE^) Deletes the file with the name FILENAME and unifies DONE with the empty list when the operation is completed. Note that no error is signaled if FILENAME does not exist. deref(OBJECT?) deref(OBJECT?, DONE^) Recursively derefences variables in OBJECT until the term is fully ground, possibly suspending. Once all variables inside OBJECT are bound, DONE is unified with the empty list. environment(ENV^) Unifies ENV with the environment slot of the current task, or the empty list, if the current process does not belong to a task group. error(MESSAGE?) Writes MESSAGE to standard error and terminates the program with exit code 1. Message may have any type and will be written as is, even if it contains unbound variables. file_exists(FILENAME?, FLAG^) If FILENAME designates an existing file or directory, assign "true" to FLAG, otherwise "false". Signals an error if the system call fails due to other causes. file_size(FILENAME?, BYTES^) Unifies BYTES with the number of bytes in the file named by the string or character list FILENAME. If an error occurs, BYTES is unified with a term of the form "error(ERRNO)" where ERRNO designates the error code. file_type(FILENAME?, TYPE^) Unifies TYPE with the string "file", "directory", "link", "fifo" or "socket" depending on what type of file FILENAME refers to. If an error occurs, TYPE is unified with a term of the form "error(ERRNO)" where ERRNO designates the error code. FILENAME may be a string or a character list. foreign_call(TERM?) Invokes a builtin or user primitive specified by TERM. TERM must be a tuple. See below for more information about the foreign function interface. get_arg(INDEX?, TUPLE?, ITEM^) get_arg(INDEX?, TUPLE?, ITEM^, DONE^) Unifies ITEM with the INDEXth element of TUPLE, assigning the empty list to DONE, if given. Indexing starts at 1. Signals an error if INDEX is out of range. get_global(NAME?, VAL^) get_global(NAME?, VAL^, DEFAULT^) Retrieves the global variable named by the string NAME and unifies its value with VAL. If no global variable under this name is defined, an error is signaled if DEFAULT is not given. If DEFAULT is given and the global variable has not yet been set, VAL is unified with DEFAULT. Global variables are thread-local. get_module(NAME?, MODULE^) Assigns the module with the given name to MODULE. If no module with this name is linked, an error is signalled. getcwd(DIR^) Unifies DIR with a character list holding the name of the current directory. getenv(NAME?, VAL^) Unifies VAL with a character list holding the value of the environment variable of the given name, which should be a string or character list. If no variable with that name is defined, VAL is unisifed with the empty list. getpid(PID^) Unifies PID with the UNIX process ID of the currently executing program. global(NAME?, VAL?) If a global variable with the given name exists, its current value is unified with VAL. Otherwise the global variable will be created, set to a fresh unbound variable which will then be unified with VAL. halt halt(EXITCODE?) Terminates the program with the given exit code, which must be an integer and defaults to 0_. heap_statistics(TUPLE^) Unifies TUPLE with a tuple of 5 elements containing the number of bytes currently used in the threads's heap holding variables, tuples, list, floats and port objects. idle_thread(PEER^) Unifies PEER with the number of a random idle thread (other than the current). If no other thread is currently idle, PEER is unified with "false". Note that threads listening for input or other events like signals are not considered idle. kill(SIGNAL?, PID?) kill(SIGNAL?, PID?, DONE^) Sends SIGNAL (which should be an integer or a string containing the Linux/BSD uppercase signal name) to the process with the ID PID using the kill(2) system call and unifies DONE with the empty list when the operation is completed. If the system call fails, DONE will be unified with a tuple of the form "error(ERRNO)" where ERRNO designates the error code. RESULT^ is EXPRESSION Evaluates the numerical expression and unifies RESULT with the result of the computation. EXPRESSION must be an arithmetic expression and may not contain unbound variables. Available operations are: X + Y X - Y X * Y X / Y Usual arithmetic operators X \\ Y Integer remainder +X Identity -X Negation \X Bitwise NOT sqrt(X) integer(X) Truncate and convert to integer real(X) Convert integer to float X /\ Y Bitwise AND X \/ Y Bitwise OR X >< Y Bitwise XOR X << Y X >> Y Shift X by Y X ** Y Exponentiation of X by Y rnd Pseudo random float betwwen 0 and 1 rnd(X) Random integer between 0 and X-1 floor(X) ceiling(X) round(X) Rounding operations sin(X) cos(X) tan(X) atan(X) Trigonometry log(X) Natural logarithm exp(X) Exponential value abs(X) Absolute value sign(X) Sign real_integer_part(X) real_fractional_part(X) Deconstruct floating-point value max(X, Y) min(X, Y) Return greater or lesser argument Random numbers returned by "rnd/0" are uniformly distributed, "rnd/1" uses a slightly faster method and may not be uniformly dsitributed. Note that variables holding valid arithmetic expressions (as in ISO Prolog) are not supported. Signals an error if any argument is not numeric. length(OBJECT?, LEN^) Unifies LEN with the length of OBJECT, which may be a string, a list or a tuple. list_to_number(LIST?, NUMBER^) list_to_number(LIST?, BASE?, NUMBER^) Unifies NUMBER with the number consisting of the characters in LIST, which must specify a valid integer or floating point number. If LIST designates an integer, BASE determines in what numeric base the conversion should occur. list_to_real(LIST?, REAL^) Similar to "list_to_number/2", but implicitly converts the result into a floating-point number. list_to_string(LIST?, STRING^) Unifies STRING with the string holding the UNICODE code points in LIST. list_to_tuple(LIST?, TUPLE^) Unifies TUPLE with the string holding the elements of LIST. If LIST is the empty list, TUPLE will be unified with the empty list as well (there are no empty tuples). listen(FILE?, VAR^) Registers the file descriptor FILE with the internal polling loop and unifies VAR with the empty list once data can be read from the file. To listen again, invoke "listen" again for the same file. If an error occurs, then VAR will be unified with a tuple of the form "error(ERRNO)", where ERRNO is the UNIX error code. log(MESSAGE?) log(MESSAGE?, DONE^) Dereferences MESSAGE fully and writes it into the log file. After the message is written, DONE is unified with the empty list. make_tuple(LENGTH?, TUPLE^) Unifies TUPLE with a fresh tuple of the specified length, the resulting tuple is populated with unbound variables. Signals an error if LENGTH is not between 0 and 127. Creating a tuple of length 0 will unify TUPLE with the empty list. merger(INSTREAM?, OUTSTREAM^) Takes elements from INSTREAM and writes them to OUTSTREAM. Element of the form "merge(STREAM?)" in INSTREAM result in merging the elements from STREAM into OUTSTREAM. Items are added to the output stream in an arbitrary order, but retain the order from the respectively merged streams. Merged streams may again receive "merge(STREAM)" messages. Once all streams are terminated with the empty list, OUTSTREAM is terminated as well. module_exports(MODULE?, LIST^) Unifies LIST with the list of exported process definitions contained in MODULE. Each element of the list will be a tuple of the form '/'(NAME, ARITY). module_name(MODULE?, NAME^) Assigns astring holding the name of MODULE to NAME. number_to_list(NUMBER?, LIST^, TAIL?) number_to_list(NUMBER?, BASE?, LIST^, TAIL?) Converts the integer or real NUMBER into a list of character codes terminated by TAIL and unifies it with LIST. open_file(NAME?, MODE?, FILE^) Creates or opens a file with the name NAME in the given MODE, which may be one of the strings (not character lists!) "r" (read), "w"(write) or "a" (append). The file descriptor will be unified with FILE. If the system call fails, FILE will be unified with a tuple of the form "error(ERRNO)", where ERRNO is the UNIX error code. open_port(PORT^, LIST^) Creates a "port" object and unifies it with PORT. LIST is unified with a "stream" receiving objects send to PORT using the "send" primitive. When the port is not referenced anymore, the stream is closed by assigning the empty list to its tail. put_arg(INDEX?, TUPLE?, ITEM?) put_arg(INDEX?, TUPLE?, ITEM?, DONE^) Unifies the INDEXth element of TUPLE with ITEM, assigning the empty list to DONE, if given. Indexing starts at 1. Signals an error if INDEX is out of range. put_global(NAME?, VAL^) put_global(NAME?, VAL^, DONE^) Assigns VAL to the global variable named by the string NAME and unifies DONE with the empty list when the assignment is completed. Global variables are thread-local. randomize(SEED?) randomize(SEED?, DONE^) Initializes the internal pseudo random number generator. SEED should be a string or a byte list. The buffer holding the random state has a size of 16 machine words. If the byte sequence given by SEED has a smaller size, then initialization "wraps around" to the start of SEED again. Once the random number generator has been initialized, DONE is unified with the empty list. On startup the random number generator is seeded with the system time. readlink(FILENAME?, VAL^) Unifies VAL with a character list holding the contents of the symbolic link FILENAME. If an error occurs, VAL is unified with a term of the form "error(ERRNO)" where ERRNO designates the error code. read_file(FILE?, COUNT?, LIST^, TAIL?) Reads COUNT bytes from the file designated by the file descriptor FILE and unifies LIST with the read data, terminated by TAIL. If an error occurs, then LIST will be unified with a tuple of the form "error(ERRNO)", where ERRNO is the UNIX error code. real_to_list(REAL?, LIST^) Converts the floating-point number REAL to a list of character codes. restart_timer(ID?, MS?) restart_timer(ID?, MS?, RESULT^) Modifies the timeout for the timer identified by ID and previously created by calling "timer/3" or "clock/3" and unifies RESULT with "true" when the operation is complete. MS specifies the new timeout (possibly periodic when ID designates a periodic timer). If no timer with the given ID is currently active, then this operation does nothing and unifies RESULT with "false" (if given). rmdir(FILENAME?) rmdir(FILENAME?, DONE^) Deletes the directory with the name FILENAME and unifies DONE with the empty list when the operation is completed. Note that no error is signaled if FILENAME does not exist. In case the operation fails DONE is unified with a tuple of the form "error(ERRNO)", where ERRNO is the UNIX error code. run(MODULE?, TERM?) Invokes TERM similar to "call/1" in the module represented by MODULE, which may be a module object or a name referring to a linked module. self(ID^) Unifies ID with the thread number of the thread that is executing the current clause. send(PORT?, OBJECT?) send(PORT?, OBJECT?, DONE^) Sends OBJECT (which may be any data objecv whatsoever) to PORT and unifies DONE with the empty list, when given. signal(SIGNAL?, VAR^) signal(SIGNAL?, VAR^, DONE^) Installs a signal handler for the signal with the name given by the string SIGNAL, which should be an integer or a valid uppercase Linux/BSD signal name. If the signal is raised, or was raised since the program started, the number of times the signal was raised since is added as an integer to the stream in VAR. Handling of the signal can not be disabled, once the signal handler was installed. statistics(TUPLE^) Unifies TUPLE with a tuple of 4 elements containing the number of goals, active goals, suspended goals and used heap-cells. string_to_real(STRING?, REAL^) Converts the atom STRING to a floating-point number. If STRING can not be converted, then an error is signaled. string_to_integer(STRING?, INTEGER^) string_to_integer(STRING?, BASE?, INTEGER^) Converts the atom STRING to an integer, interpreting the characters in STRING in BASE. BASE defaults to 10. If STRING can not be converted, then an error is signaled. string_to_byte_list(STRING?, LIST^, TAIL?) Converts STRING to a list of bytes, terminated by TAIL. string_to_list(STRING?, LIST^, TAIL?) Converts STRING to a list of UNICODE code points, terminated by TAIL. threads(NUMBER^) Unifies NUMBER with the number of threads, as given to the "+THREADS" run-time option on startup. timer(MS?, VAR^, ID^) Establishes a single-shot timer that unifies VAR with the empty list after MS milliseconds have passed. ID is unified immediately with an integer identifying this timer and can be used to cancel the timer before it has expired. true Does nothing. tuple_to_list(TUPLE?, LIST^, TAIL?) Unifies LIST with the elements of TUPLE, terminated by TAIL. unify(X?, Y?, RESULT^) Unifies X with Y and finally unifies RESULT with the string "true" or "false", depending on whether the unification succeeded or failed. Variables bound during the unification will be restored to their unbound state if the full unification should fail. Note that this primitive receives the result variable in the third argument position, which differs from the description given in [1]. Also note that "remote" variable bindings in other threads are not restored on failure and still trigger inter-thread messages that negotiate the final value. unify_with_occurs_check(X?, Y?, RESULT^) Similar to "unify/3", but fails in case a binding would make a variable directly or indirectly refer to itself. This primitive always performs the check, regardless of the "+OCCURS_CHECK" runtime option. utf_decode(LIST?, OUTLIST^) utf_decode(LIST?, CHAR^, REST^) Converts bytes in LIST to UNICODE code points. The former primitive writes characters into the stream OUTLIST until LIST is exhausted and closes OUTLIST by unifying it with the empty list. The latter primitive converts a single character and unifies REST with the remaining elements of LIST. utf_encode(CHAR?, LIST^, TAIL?) utf_encode(INLIST?, OUTLIST^) Convert UNICODE code points into a stream of bytes. The first primitive takes a character code and unifies LIST with the list of bytes required to encode CHAR, terminated by TAIL. The second primitive receives a stream of code points and produces a stream of bytes until the end of INLIST is reached," after which OUTLIST is unified with the empty list. when(VAR?, GOAL) Executes GOAL, which must not be an unbounded variable, once VAR is bound. write(OBJECT?) write(OBJECT?, DONE^) Fully dereferences OBJECT and writes it to standard output. After writing, DONE is unified with the empty list, if given. write_file(FILE?, DATA?, REST^) write_file(FILE?, DATA?, COUNT?, REST^) Writes COUNT bytes of DATA to the file designated by the file descriptor FILE and unifies REST with the empty list or any yet unwritten data. DATA may be a string or a byte list. If COUNT is not given, writes DATA completely. When an error occurs, REST will be unified with a tuple of the form "error(ERRNO)", where ERRNO is the UNIX error code. writeln(OBJECT?) writeln(OBJECT?, DONE^) Fully dereferences OBJECT and writes it to standard output, followed by a newline character. After writing, DONE is unified with the empty list, if given. * Alphabetical list of guard expressions: X == Y Succeeds if X matches Y. Note that matching never binds logic variables. If an argument variable in a clause head occurs several times, then subsequent occurrences imply an argument match equivalent to using "==/2", e.g. foo(X, [X]) :- ... is the same as foo(X, [Y]) :- X == Y | ... X =\= Y Succeeds if X does not match Y. X =:= Y Succeeds if X is numerically the same as Y, X and Y may be numerical expressions as in "is/2". X \=:= Y Succeeds if X is numerically different to Y. X and Y may be numerical expressions. X > Y Succeeds if X is numerically greater than Y. X and Y may be numerical expressions. X < Y Succeeds if X is numerically less than Y. X and Y may be numerical expressions. X >= Y Succeeds if X is numerically greater than or equal to Y. X and Y may be numerical expressions. X =< Y Succeeds if X is numerically less than or equal to to Y. X and Y may be numerical expressions. X @> Y Succeeds if X is ordered above Y. Implements a general ordering relation where integers < reals < strings < lists < tuples < ports < modules Integers and reals are compared numerically, lists element by element, strings lexicographically, tuples by size and, if equal, element by element. All other objects are sorted by some arbitrary but stable order. X @< Y Succeeds if X is ordered below Y X @>= Y Succeeds if X is ordered above or equal to Y. X @=< Y Succeeds if X is ordered below or equal to Y. atomic(X) Succeeds if X is a number or a string. data(X) Suspends if X is an unbound variable. This is identical to qualifying a head variable with "!". idle Suspends the clause until the current thread us "idle", that is, when no active processes are scheduled for execution. Once idle, the process will continue and subsequent matching of an "idle" guard will wait for the next idle cycle. Note that threads listening for input or other events like signals are not considered idle. integer(X) Succeeds if X is an integer. known(X) Succeeds if X is a non-variable object or a bound variable, the guard does not suspend in the case that X is bound. list(X) Succeeds if X is a non-empty list. module(X) Succeeds if X is a module object. number(X) Succeeds if X is an integer or a floating point number. otherwise Always succeeds. A clause with this guard will only be matched if all textually previous clauses of the same process definition failed to match. port(X) Succeeds if X is a port object, real(X) Succeeds if X is a floating point number. remote(X) Succeeds if X is a variable or port object located in a different thread than the current one. string(X) Succeeds if X is a string. Note that this includes the empty list ("[]"). tuple(X) Succeeds if X is a tuple. unknown(X) Succeeds if X is an unbound variable, the guard does not suspend in that case. * Alphabetical list of declarations: arguments(STRING) Adds additional run-time arguments to be used when running the compiled executable. This declaration is ignored in modules other than the main ('') module. The arguments can ge given as a string or character list and are effectively prepended to any arguments that are given when the executable is invoked. foreign(SPECLIST) Creates stubs and wrappers for external native code. SPECLIST should be a list of foreign-call specifications or strings and generates FLENG stubs that call generated C wrapper code which is produced in a file named ".c", where "" is the name of the currently compiled module. If no module is declared, the file will be named "foreign.c". See below for a description of the interpretation of the specification format. exports(EXPORTLIST) EXPORTLIST should be a list of "/" specifications of predicates that should be visible to external modules. If this declaration is not given, then by default all predicates are exported. include(FILENAMES) Includes the contents of the files given by the string or list of strings in FILENAME at the current toplevel position in the compiled file. The file-extension defaults to ".ghc" or ".st" when compiling GHC or Strand and to ".fl" otherwise. initialization(PROCESSLIST) Declares that on startup, the processes given will be spawned. PROCESSLIST should be a string or a list of strings naming predicates with zero arity, which must be defined, but not necessarily exported. machine(TOPOLOGY) Provided for Strand compatibility. Only "ring" and "torus" topologies are allowed. This declaration has no effect. mode(CLAUSE) Declares the predicate defined by CLAUSE to have a mode. CLAUSE should specify a predicate term "(, ...)", where "" is either the symbol "?" or "^". The compiler will transform arguments to the predicate marked as "^" into an explicit output-argument unification, e.g. -mode(wheels(?, ^)). wheels(car, 4). is equivalent to wheels(car, X) :- X = 4. Output arguments (marked as "^") may not be used in guards. module(NAME) Declares the name of the current module, unless the file was compiled with the "-m" command line option. If not given and no module name was specified during the invocation of the compiler, the module name defaults to the empty ("") module, which is the program's main module. struct(TUPLE) Generates code to easily extract and modify tuple components. TUPLE should be a named tuple of length > 1. A struct declaration of the form -struct(point(x, y)). produces code similar to (but slightly more efficient than): point_x(point(X, _), Y) :- Y = X. point_y(point(_, X), Y) :- Y = X. point_x(X, point(_, B), Y) :- Y = point(X, B). point_y(X, point(A, _), Y) :- Y = point(A, X). Note that the update predicates take the new value as the first argument. uses(MODULELIST) Declares that the modules should be loaded and process definitions be made available for use using the "MODULE:GOAL" notation. MODULELIST may be a string or a list of strings. In FGHC, declarations for modules called directly using the "MODULE:GOAL" notation are added automatically and so not required. * Precedence and associativity of infix operators: The precedence and associativity of numerical operators follows the usual ISO Prolog syntax: Operator Associativity + - \ fy Highest precedence ** xfx * / \\ << >> yfx + - /\ \/ >< yfx : xfy == =\= =:= \=:= @< @> @=< @>= > < >= =< @ <= => <== += -= *= /= xfx = := is xfx & xfy , xfy -> xfy ; xfy | xfy :- xfx Lowest precedence VI. Programming Tips * Sequencing process execution If processes must run sequentially, there are three methods for ensuring non-parallel execution, with different run-time costs and partially different semantics. The cheapest method is to assign a value to a dedicated unbound variable and force the argument in a clause head to be bound: seq :- first(A), second(A). first(A) :- ..., A = []. % "[]" used by convention second(A) :- data(A) | ... . % alternatively: "second(!_)" The second option is to use "when/2": seq :- first(A), when(A, second). first(A) :- ..., A = []. second :- ... . "when/2" is basically equivalent to the first method, but creates a compiler-generated intermediate predicate that performs the dereferencing (and possible suspension) of the assigned variable. The third method is to use "&/2", which runs the first goal as a child task and invokes the second goal when the task and all processes spawned by the task have completed: seq :- first & second. Note that if "first" starts a recursive process, then "second" will not be executed until the process finishes. The example is mostly equivalent to the following: seq :- call(first, A), when(A, second). Task-creation and maintenance will have a slight run-time cost, so performance critical code may try to avoid using tasks in this situation. * Module prefixes in uses of "call" and "apply" Dynamic calls assume the target predicates belongs to the module that contains the "call" or "apply" form, unless the invoked form is already prefixed with a module name using the ":/2" operator. Indirect calls and applications, which occur for example in library code (notably in the "app" library) do not have information about the calling module, so this cases the module defaults to "" (the empty module name) which is the main application module. * Runaway stream producers When processes that produce and consume streams run at different speeds, it may happen that the consumer is not able to process elements from the stream fast enough to avoid the build up of an ever larger number of input elements. As an example, consider the following code: io:read_lines(File, Lines), consume(Lines). If "consume" runs slower than the library code that reads in the stream of lines, a large file will create a stream holding most or all of the contents of the file, while the consumer processes only a small part of the data at a time, possibly filling up the available heap space. In this example, "io:read_lines_bounded" can be used, but the problem exists in every situation where data is produced quicker than it can be consumed. In such situations the producer must be slowed down artifically, by using bounded buffers (where the consumer explicitly provides elements to the producer to be filled) or by using some other method of signalling to the producer that new data is required. Another situation exists when processes are created at a high rate, without requiring synchronization: loop :- process_user_input, loop. This will quickly exhaust the available goal space by spawning processes excessively, since the call to "loop/0" can proceed immediately, even though the intent is to read and process input from the user. The remedy is to explicitly wait for data to be read and processed fully before starting the next iteration of the loop, for example by using "when/2": loop :- process_user_input(Done), when(Done, loop). where "process_user_input/1" unifies "Done" with some value when it is ready for new input. Alternatively, you can use "&/2" and run the processing in a separate task, if possible background processes spawned in reaction to the input are ensured to be completed: loop :- process_user_input & loop. * Overlapping matches and "otherwise" The order in which goal arguments are matched with clause heads is not specified and may not follow strict left to right order or the order of clauses in the source code. Heads of clause definitions that overlap may cause unexpected behaviour. For example: start :- perform_work(A), foo(A). foo([A]) :- ... . foo(A) :- ... . Both clauses may match if "foo" is called with a one-element list, as the order of matching and unification is not defined. It is advisable to use the "otherwise/0" guard to identify fallback clauses that should be matched if all previous clauses are guaranteed to be failing. The FGHC compiler will detect overlapping clause heads in some cases (but not all) and issue a warning. Sometimes this overlapping is desired, for example when simultaneously consuming more than one stream: merge([X|S], S2) :- ... merge(S, [Y|S2]) :- ... Here we want to suspend on either stream and are sure that at least one stream will be instantiated. Still, an otherwise clause will not hurt and may also catch unexpected cases. * Variable matching in guards Unbound variables in guards will cause a process to suspend. This may be unintuitive for the user as unbound variables in clause heads do not cause a suspension: foo(A, B) :- B == x(_) | ... In the example, "A" will not cause a suspension, even if the argument passed to "foo/2" is unbound. But the match of "B" with will suspend on "B" (if it is unbound) _and_ will also suspend forever, because it will try to dereference the anonymous variable ("_"). This can be considered a bug. * Interprocess comunication ("detached ports") Message exchange between threads normally takes place via shared memory "message ports" where threads store and retrieve message data passed between different execution contexts. These ports can also be "detached" by placing them into files on disk that allow shared access by different processes. The "sys" library provides operations to detach a message port and to attach to a port already detached by a different process. Messages can only be sent to a detached port and must not contain unbound variables or references to "live" data that can not be meaningfully deserialized by the receiving process, like port objects. To perform bidirectional communication, both processes have to detach message ports and need to know the filename of respective peer's port file. Here is a simple example: % a -initialization(main). main :- sys:detach_message_port('/tmp/a', _). forwarded(Msg) :- fmt:format('a received: ~q\n', [Msg]). % b -initialization(main). main :- sys:attach('/tmp/a', _, Addr), command_line(Args), sys:transmit(Addr, Args, _). $ fleng a.ghc -o a.out $ fleng b.ghc -o b.out $ b.out hello world a received: [hello, world] Only the thread calling "sys:detach_message_port/2" will have its port detached, all other threads still use a process-local message port, but communication between threads with detached and undetached ports still works as normal. * Networking FLENG has no built-in networking facilities. It is recommended to run tools like socat(1) [7] as subprocesses, using the "proc" library module. * Cleaning up resources once they are no longer needed Especially when interfacing to foreign code it may be necessary to control when resources are being freed. A particularly useful idiom is to use a "port" object, which closes the associated stream once the port is not referenced anymore by a process: create_resource(P) :- open_port(P, S), spin(S, R). spin([], R) :- . Since ports are also useful as a message or command interface and ensure serialization of requests, they are generally useful to implement all accesses to specific resources. * Speeding up calls to other modules Cross-module invocations are performed dynamically, using a cache for saving the resolved target address in subsequent calls from the same site. To further speed up direct calls to other modules, you can generate a special "link" file holding labels designating the location of the external call target. Use it like this: % foo.ghc: -initialization(main). main :- bar:foo. % bar.ghc: -module(bar). foo :- writeln(foo). $ fleng -e bar.ghc # generates "bar.link" $ fleng -c -link bar.link bar.ghc # generates "bar.o" $ fleng foo.ghc -link bar.link bar.o $ ./a.out foo Note that compiling "bar.ghc" to an object module needs to see the link file so that additional globally visible entry-points are produced in the output file. VII. Debugging To analyse run-time errors, execution can be logged to standard error or a dedicated file using the "+LOG" run-time option. If a FLENG or FGHC file is compiled using the "-d" option, each entry into a clause is written to the log-file. Assignment, suspension and resuming is logged for modules compiled with the "-d" option. Logging can be enabled for specific threads by using a suitable bit-mask specifying the threads that should be logged. The log-file is never deleted and new information is appended at the end, so you can use "tail -f" to follow the log over mulitple runs. The "+LOGX" option only logs explicit calls to "log/1". Additionally, information about the overall status of the program is logged. If the "+STATS" or "+HEAPSTATS" run time options are given, statistics about memory use and process load are logged and can be subsequently filtered and analyzed using the "plot" utility included in the distribution which uses gnuplot(1) to show a graph of the collected statistics data like the number of active and suspended goals and an overall breakdown of heap usage. Note that logging has a severe performance impact due to file-system access and thread locking. Configuring the build with "--debug" adds debugging information to the C run time library and enables additional events to be logged. Code execution can be profiled by using the built-in statistical profiler. Compiling your programs and library modules with the "-p" option will instrument the code to provide profiling information. When you run a thus instrumented executable with the "+PROFILE " run-time option, then on successful execution timing information is written to the given file in textual format. "plot" takes a log file and extracts either heap statistics (when the "-heap" option is given) or one or more of "axis" names that should be plotted in a graph. Only the latest run will be plotted if the log file holds logging data from multiple runs. VIII. Data Representation and Interfacing to C Code Memory for all data objects is allocated from the "heap", which holds fixed size "cells". All data is made up from cells and each thread has its own private heap, data is (usually) never shared between threads. Unused memory is reclaimed using a reference counting scheme, where every store of a cell pointer increases the reference count and any release of a cell reduces the count. Once the count reaches zero, the cell is reclaimed and stored in the "freelist" from which new allocations take cells for further use. Cells have 3 fields: a "tag" and reference count cell, a head and a tail. The tag specifies what type of object the cell is representing, the head and tail point to other objects or contain integers. A datum is either a cell-pointer (of type "FL_CELL *), an integer of machine word size or a pointer to a string. Integers and strings are distinguished from pointers by having the lowest and next to lowest bit set, respectively, which means the cells are always located at a word size boundary. Integers are representable in the range of 63 (on 64 bit systems) or 31 bits (on 32 bit machines). The base type on the C level for all objects is "FL_VAL". Cells should never be modified destructively. Strings are "interned" and unique, a string with a given name exists only once in the entire process. Depending on type, the fields of a cell have various meanings: Head Tail List List head List tail Real IEEE double Unused* Variable Value+ Suspensions Tuple Head/name Arguments (list) Port Stream|source Unused Module Internal^ Unused Ref Source% Unused *: on 32 bit systems the head + tail fields contain the double +: points to the variable if unbound ^: internal pointer to module structure, with lowest bit set %: contains a pointer to a variable in another thread, with the lowest bit set A "ref" is a variable-like object referencing a variable in another thread. To call native code from FGHC or FLENG you can use the "foreign_call" form, which generates a call to an external function using the C ABI: foreign_call(my_func(Argument, ...)) will invoke an externally visible C function with the following prototype: void my_func_(FL_TCB *tcb, FL_VAL, ...)) "" is the number of arguments passed to the foreign function, at most 4 arguments can be passed. The "tcb" argument holds a pointer to a "thread control block" designating the thread context in which this foreign function is invoked. Normally, user code should not manipulate the reference counts of arguments or results, this is automatically handled by the compiler. Cell-pointers stored in external memory must have their reference count increased or may be invalidated when their reference count is decremented to zero in other code using the value. Note that while your foreign code executes, the runtime can not process other processes, the thread is effectively blocked until the foreign code body has finished and returns to the caller. Therefore it is important to minimize the execution time and perform polling of I/O and other resources with the means provided by the FLENG runtime system ("listen/2", timers and clocks, for example). Also, if foreign code requires data passed in arguments to be dereferenced (possibly by waiting until variables are bound), this should be done on the FGHC/FLENG side, see "deref/2". To simplify the creation of wrapper code, the "foreign/1" declaration can be used, allowing the automatic generation of FLENG stub predicates and associated C wrapper code. "foreign/1" receives a list of specifications (or a single specification) defining the name and argument and result types that a foreign function accepts: -foreign('#include '). -foreign([modf(real, -real, -real)]). -initialization(main). main :- modf(1.23, I, F), writeln([I, F]). A specification can either be a tuple describing a foreign function, a foreign struct specification, a foreign constant definition or an atom holding C code that is inserted verbatim into the generated wrapper code. A function specification gives the name and argument types that the function expects, where results and "output" arguments are preceded by a "-". The meaning of the types and their interpretation follow these rules: 1. If an argument list ends with an output ("-...") argument, then this argument is taken as the type of the function result. 2. If no output arguments are given, or if the final type is "-void", then the function is expected to have no return type. 3. Output arguments are passed as pointers to the foreign function (with the exception of the final return type). 4. Arguments of type "string", "list" and "tuple" are completely dereferenced before they are passed to the foreign function. 5. All arguments, except output arguments and arguments of type "any" are "forced", execution will block until they are bound. 6. If no result type is given and the function has no output arguments, an implicit final argument is added and unified with the empty list when the function returns. The wrapper predicate will have the same name as the name given in the function specification, unless the specification has the form =(...) In this case the FLENG predicate is named "". Specification types are mapped to C types according to the following table: Type C integer int short short long long char char real double also accepts integer as argument string char * charlist char * also accepts string as argument tuple FL_VAL list FL_VAL any FL_VAL pointer void * pointer(T) T * void - only allowed as return type Foreign structs are defined like thisL -foreign(struct(NAME(SLOT1:TYPE1, ...))). An example should make this clear: -foreign(struct(vector(-x:real, -y:real, -z:real, moved:integer)). The "-..." marks read-only slots, so the following predicates will be generated: vector_x(POINTER?, VALUE^) vector_y(POINTER?, VALUE^) vector_z(POINTER?, VALUE^) vector_moved(POINTER?, FLAG?, DONE^) vector_moved(POINTER?, FLAG^) Setter predicates take a third argument that is unified with the empty list when the operation is complete. The "=(...)" syntax is allowed here as well to allow using a specific name prefix for the stub predicates. Finally, foreign constant are defined as follows: -foreign(const(eof = 'EOF':integer)). This defines a predicate "eof(EOF^)" that assigns the C EOF code to the result variable "EOF". The " =" prefix can be omitted if the native name shall be used. IX. Bugs and Limitations * System limits: Maximal number of threads: 64 (32 on 32-bit platforms) Maximal number of local variables: 64 Maximal size of message sent to other threads: 4096 bytes Number of uninterrupted goal calls: 10 These settings can be changed using run time options: Default heap size per thread: 1000000 bytes Default timeslice: 10 Maximal number of active or suspended goals: 10000 Number of ticks between logging of statistics: 50 * Known bugs and shortcomings: - The generated code is should be more compact and more efficient. - There is currently not mechanism for catching errors, failed run-time errors result in a termination of the program. - Some attempt is made to prohibit the creation of circular data structures, but not all edge cases are likely to be addressed. X. Code Repository A git[8] repository hosting the sources can be found here: https://gitlab.com/b2495/fleng/ XI. Further Reading [1] "FLENG Prolog - The Language which turns Supercomputers into Parallel Prolog Machines" Martin Nilsson and Hidehiko Tanaka [2] "Guarded Horn Clauses" PhD Thesis, Kazunori Ueda [3] "The Deevolution of Concurrent Logic Programming Languages" Evan Tick [4] "Ports of Objects in Concurrent Logic Languages" https://people.kth.se/~johanmon/papers/port.pdf [5] "Strand: New Concepts for Parallel Programming", Ian Foster and Stephen Taylor, Prentice Hall, 1990 This book can also be downloaded here: http://www.call-with-current-continuation.org/files/strand-book.pdf [6] "Design of the Kernel Language for the Parallel Inference Machine", U. Kazunori et al., 1990 [7] http://www.dest-unreach.org/socat/ [8] https://git-scm.com [9] https://en.wikipedia.org/wiki/Fifth_generation_computer XII. License This software was written by Felix L. Winkelmann and has been released into the public domain. Some parts of the code are authored by other people, see the file "LICENSE" in the source distribution for more information. XIII. Contact Information In case you need help, have suggestions or ideas, please don't hesitate to contact the author at felix AT call-with-current-continuation DOT org Also visit the IRC channel "#fleng" on https://libera.chat/ if you have questions or need advice.