_______ _______ __ _ ______ _ _ _ ____ ____ ____ |______ | |______ | \ | | ____ |\/| | |___ |--< [__] | |_____ |______ | \_| |_____| User Manual Version: 2 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 z80 CPUs running on the CP/M operating system, for Commodore 64 systems and for the "Uxn" virtual machine[13]. 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. MicroFLENG is derived from the "FLENG" project [10], which provides a much more featureful implementation of FGHC/Strand/FLENG for UNIX machines. II. Usage The compiler is invoked by using the "fleng" driver script: -fleng [-Vwhkdc] [CCOPT ...] [-I DIRECTORY] FILENAME [-o OUTFILE] [OBJECTS ...] where TARGET is the target specification given when configuring the system. The script takes FLENG, Strand or FGHC source files and generates assembler, binary object files or executables. The following command line options are supported: -V Print version and exit. -w Disable compiler warnings. -k Keep intermediate files. -c Compile to binary file, do not create an executable. -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 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 Show some internal compilation information. -h Show usage information. If the source file name has an .fl or .fleng extension, then the "fleng" driver script assumes it contains FLENG code. In all other cases the file is considered to be an FGHC or Strand source file. This is a whole-program compiler, separate compilation of source code is not supported. The compiler generates by default executable files, unless the "-o" option was used to force compilation after one of the intermediate stages. The resulting files can be executed with the appropriate emulator. Using, for example, the "cpm" CP/M emulator[11]: $ cpm-fleng hello.ghc $ cpm --exec hello For C64 programs, one can use "vice"[12] to create and run a disk image: $ c64-fleng hello.ghc $ c1541 -format diskname,id d64 hello.d64 -attach hello.d64 -write \ hello.prg hello $ x64sc hello.d64 And for the "uxn" virtual machine: $ uxn-fleng hello.ghc $ uxncli hello.rom 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 recommended to understand the concepts used. 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", "=/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 program and have the following form: -declaration(Argument, ...). Currently supported declarations in FLENG are "include/1", "initialization/1" and "foreign/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 unification. 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 strings (atoms) abc 'one\ntwo' lists [1, 2, 3] [head|tail] "abc" tuples foo(bar, baz) {x, y, z} variables Abc _123 _ 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 8-bit characters. Circular data structures are not allowed. This is not checked at run-time. Tuples of the form "{...}' may not be empty. Note that "abc(def)" is equivalent to "{abc, def}" and "{abc}" is not the same as "abs" Only integers of 16 bit width are supported, with a range of -32768 to 32767. 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. 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 true/0 when/2 ->/2 ;/2 Unification and assignment =/2 :=/2 assign/2 assign/3 unify/3 deref/2 Conversion number_to_list/3 string_to_list/3 list_to_tuple/2 list_to_number/2 tuple_to_list/3 Lists append/2 append/3 member/3 length/2 reverse/2 reverse/3 Numeric computations compute/3 compute/4 is/2 Program environment error/1 Random numbers randomize/1 randomize/2 random/1 Streams merger/2 Tuples put_arg/3 put_arg/4 get_arg/3 get_arg/4 make_tuple/2 Ordering compare/3 Input/Output print/1 print/2 println/1 println/2 nl/0 nl/1 Foreign Functions foreign_call/1 Memory access peek/2 peek/3 poke/2 poke/3 Paired Arguments +=/2 -=/2 *=/2 /=/2 =>/2 <=/2 <==/2 Control flow label/2 jump/2 foreach/3 maplist/3 filter/3 partition/4 foldl/4 sequence/4 any/3 every/3 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". 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" ("-" ...) any(GOAL*, LIST?, RESULT^) Invokes GOAL for each element of LIST, by adding the element and an unbound variable as additional arguments. If GOAL assigns a non-false result to its second argument, then RESULT will be assigned "true" and the operation finishes. If GOAL returned false for all elements if LIST, then "false" will be assigned to RESULT. GOAL must be an literal string or tuple naming a predicate in the current program. append(LIST1?, LIST2?, RLIST^) append(LISTS?, RLIST^) Appends the elements of LIST1 and LIST2 or of the lists in LISTS and unifies RLIST with the resulting list append_file(NAME?, ADDRESS?, LENGTH?, RESULT^) [uxn] Appends to the first file device, with given filename, destination address and length. RESULT holds the "success" value after the file operation is complete. 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. bdos(FUNCTION?, PARAMETER?, RESULT^) [CP/M] Invoke a CP/M BDOS system call with function code FUNCTION in C register and PARAMETER in DE. RESULT will be assigned the 8 bit result from the B register. 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 sqrt x - Square root abs x - Absolute value sign x - Sign (returns integer) With the exception of "=" and "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. datetime(TYPE?, VALUE^) [uxn] Assigns date/time information to VALUE, depending on TYPE, which should be one of the strings "year", "month", "day", "hour", "minute", "second", "dotw", "doty" or "osdst". delete_file(NAME?, DONE^) [uxn] Deletes the file with the given name and assigns the empty list to DONE. 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. draw_pixel(X?, Y?, COLOR?, DONE^) [uxn] Draws a pixel at the given coordinates and assigns the empty list to DONE. draw_sprite(X?, Y?, COLOR?, ADDRESS?, DONE^) [uxn] Draws a sprite at the given coordinates and assigns the empty list to DONE. COLOR should give the sprite-drawing mode as used by the screen device. draw_string(X?, Y?, STRING?, FONT?, COLOR?, DONE^) [uxn] Draws 8x8 character tiles for STRING of the font stored at address FONT. STRING may be a string or a character list. Assigns the empty list to DONE when the operation is complete. error(STRING?) Writes STRING (which must be a string) to the console and terminates the program. every(GOAL*, LIST?, RESULT^) Invokes GOAL for each element of LIST, by adding the element and an unbound variable as additional arguments. If GOAL assigns "false" to its second argument, then RESULT will be assigned "false" as well and the operation finishes. If GOAL returned a non-false value for all elements if LIST, then "true" will be assigned to RESULT. GOAL must be an literal string or tuple naming a predicate in the current program. file_information(NAME?, INFO^) [uxn] Performs a "stat" operation and reads the file information string, assigning it a list of character codes to INFO. filter(GOAL*, LIST?, RESULT^) Invokes GOAL for each element of LIST, by adding the element and an unbound variable as additional arguments and unifies RESULT with the list of those elements for which GOAL assigned a non-false result to its second argument. GOAL must be an literal string or tuple naming a predicate in the current program. foldl(GOAL*, LIST?, INIT?, RESULT^) Invokes GOAL for each element of LIST, by adding the result of the previous invocation of GOAL (or INIT) and an unbound variable as additional arguments and unifies RESULT with the final result. GOAL must be an literal string or tuple naming a predicate in the current program. foreach(GOAL*, LIST?, DONE^) Invokes GOAL for each element of LIST, by adding the element as an additional argument and unifies DONE with the empty list when GOAL has been called for all list elements. GOAL must be an literal string or tuple naming a predicate in the current program. 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_button(BUTTON^) [uxn] Assigns the current button state of the controller device to BUTTON. get_key(KEY^) [uxn] Assigns the current key state of the controller device to KEY. get_mouse(X^, Y^) get_mouse_scroll(X^, Y^) [uxn] Reads the current mouse position or scroll values to X and Y. get_mouse_button(BUTTON^) [uxn] Assigns the current button state of the mouse device to BUTTON. get_screen_size(WIDTH^, HEIGHT^) [uxn] Assigns the current screen device size to WIDTH and HEIGHT, respectively. get_vector(DEVICE?, STREAM^) [uxn] Retrieves the currently defined stream for the given device and assigns it to STREAM. If no vector is currently set, assign the empty list. halt Terminates the program. 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 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 address_of(L) Return address of assembly language label L Note that variables holding valid arithmetic expressions (as in ISO Prolog) are not supported. Signals an error if any argument is not numeric. jump(ADDR?, ARGS?) Invokes the predicate designated by ADDR (which should previously have been determined using "label/2") with the arguments in the LIST ARGS. label(NAME/ARITY?, ADDR^) Assigns the address of the predicate with the given name and arity (which must be defined in the current program) to ADDR. The predicate can then be invoked indirectly by using "jump/2". 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^) 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. 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). load(FILENAME?) [uxn] Chain-loads the rom-file designated by FILENAME and starts execution at 0x0100. The old contents of memory will be overwritten. If loading fails, an error is signalled. 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. member(ITEM?, LIST?, FLAG^) Asssigns the string "true" or "false" to FLAG, depending on whether ITEM matches any element in 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. number_to_list(NUMBER?, LIST^, TAIL?) Converts the integer NUMBER into a list of character codes terminated by TAIL and unifies it with LIST. nl nl(DONE^) Inserts a line-break on the console. DONE is unified with the empty list when the operation is complete. partition(GOAL*, LIST?, TRESULT^, FRESULT^) Invokes GOAL for each element of LIST, by adding the element and an unbound variable as additional arguments and unifies TRESULT with the list of those elements for which GOAL assigned a non-false result to its second argument and FRESULT to all other elements. GOAL must be an literal string or tuple naming a predicate in the current program. peek(ADDRESS?, LIST^) peek(ADDRESS?, COUNT?, LIST^) Fetches COUNT consecutive bytes from memory at ADDRESS and unifies the resulting list with LIST, or assign consecutive bytes at ADDRESS into the variables given by LIST. play_audio(ADSR?, VOLUME?, PITCH?, DONE^) [uxn] Plays an audio sample with the given parameters and assigns the empty list to DONE. See the varvara documentation[14] for details. poke(ADDRESS?, ARG?) poke(ADDRESS?, ARG?, DONE^) Stores the bytes in the list or string ARG (or ARG itself, if it is an integer) in memory at ADDRESS and unifies DONE with the empty list when the operation is complete. 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. print(X?) print(X?, DONE^) Write X to the console, which may be a character list or a string or a number. If a number, then it is assumed to represent a character code. DONE is unified with the empty list when the operation is complete. println(X?) println(X?, DONE^) Combines print + nl. random(NUM^) Assigns a random 14-bit integer to NUM. randomize(SEED?) randomize(SEED?, DONE^) Initializes the internal pseudo random number generator. SEED should be an integer. Once the random number generator has been initialized, DONE is unified with the empty list. On startup the random number generator is seeded with some unknown but fixed value. read(STREAM^) [uxn] Reads data from the console device indefinitely and stores the character codes in STREAM. read_file(NAME?, ADDRESS?, LENGTH?, RESULT^) [uxn] Reads from the first file device, with given filename, destination address and length. RESULT holds the "success" value after the file operation is complete. reset_vector(DEVICE?) reset_vector(DEVICE?, DONE^) [uxn] Clears the currently set vector stream for the specified device and assigns the empty list to DONE, when given. reverse(LIST?, RESULT^) reverse(LIST?, RESULT^, TAIL?) Reverses the elements of LIST and unifies it with RESULT, optionally with tail TAIL. sequence(GOAL*, COUNT?, RESULT^, TAIL?) Invokes GOAL for all integer from 1 to COUNT, by adding the number and an unbound variable as additional arguments and unifies RESULT with a list of the values which GOAL assigned to its second argument. GOAL must be an literal string or tuple naming a predicate in the current program. set_colors(RED?, GREEN?, BLUE?, DONE^) [uxn] Define screen colors with the integers given and assigns the empty list to DONE. set_filename(NAME?, DONE^) [uxn] Stores the name given by the list or string NAME in memory and sets the filename slot of the first file device to its address. Assigns the empty list to DONE when the operation is complete. set_position(X?, Y?, DONE^) [uxn] Sets the screen device position to X/Y and assigns the empty list to DONE. set_sample(ADDRESS?, LENGTH?, DONE^) [uxn] Sets the sample address and length of the audio device and assigns the empty list to DONE. set_screen_auto(AUTO?, DONE^) [uxn] Sets the autoincrement for drawing in the screen device to AUTO and assigns the empty list to DONE. set_screen_size(WIDTH?, HEIGHT?, DONE^) [uxn] Overrides the screen device size and assigns the empty list to DONE. set_vector(DEVICE?, STREAM^) set_audio_vector(STREAM^) set_screen_vector(STREAM^) [uxn] Set the vector for the given DEVICE to STREAM. Any invocation of the vector will extend the stream with an increasing integer. Note that vectors are only invoked when no processes are running and no idle guards are currently active. "set_audio_vector" and "set_screen_vector" are shorthands for the respective varvara device. stop [uxn] sets the screen vector and performs a BRK, waiting indefinitely. string_to_list(STRING?, LIST^, TAIL?) Converts STRING to a list of UNICODE code points, terminated by TAIL. 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. Note that this primitive receives the result variable in the third argument position, which differs from the description given in [1]. when(VAR?, GOAL) Executes GOAL, which must not be an unbounded variable, once VAR is bound. write_file(NAME?, ADDRESS?, LENGTH?, RESULT^) [uxn] Writes to the first file device, with given filename, destination address and length. RESULT holds the "success" value after the file operation is complete. * 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 < strings < lists < tuples 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 system is "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. 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. 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. 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: foreign(FILENAME) Includes an assembler file with the given name in the generated output. Care should be taken to avoid conflicts with labels generated by the compiler, e.g. by using a distinguishing prefix. include(FILENAME) Includes the contents of FILENAME at the current toplevel position in the compiled file. 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. * 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 * 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. 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. * 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. VII. Data Representation and Interfacing to Native Code Memory for all data objects is allocated from the "heap", which holds fixed size "cells". Unused cells are 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. The memory is partitioned into a "low" and "high" area, the lower part from addresses 0x0000 to 0x7fff is for code and strings, and the higher part from addresses 0x8000 up is available for the heap, minus a certain area required by the operating system. FFFF +--------------------------+ | Used by operating system | (Also available for heap on Uxn) D000 +--------------------------+ | Heap | 8000 +--------------------------+ | Goal buffer | 5700 +--------------------------+ | Suspension stack | 5600 +--------------------------+ | Goal queue | 4e00 +--------------------------+ | Compiled code space | +--------------------------+ | Runtime code space | (Around 5 to 6Kb) BASE +--------------------------+ ($100 on CP/M + Uxn, $801 on C64) | Used by Operating system | 0000 +--------------------------+ On the C64, the BASIC ROM is disabled and the corresponding RAM bank is used to hold part of the heap. 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, an integer of machine word size or a pointer to a string. Integers and strings are distinguished from pointers by having the lowest bit set, which means the cells are always located at a word size boundary and strings at odd addresses. Moreover, strings exist in low memory, so testing the highest address bit is sufficient to distinguish string addresses from other data. Immediate integers can represent 14 bit signed quantities and have the highest bit set, a separate "boxed" cell representation for integers can hold signed 16-bit quantitiews. Strings are "interned" and unique and can not be created at run time. Depending on type, the fields of a cell have various meanings: Head Tail List List head List tail Variable Value* Suspension list Tuple Head/name Argument list Integer Magnitude Unused *: points to the variable itself if unbound The tag field holds bits designating the type of the cell: tag = _TLIVAAAcccccccc Bits 0-7 contain the reference count of the cell, and bits 8-15 hold the type: T = 1: tuple ("AAA" is arity / length - 1) L = 1: list I = 1: integer V = 1: var To call native code from FGHC or FLENG you can use the "foreign_call" form, which generates a call to an external function. foreign_call(my_func(Argument, ...)) will invoke an assembler routine with the name "my_func_", where "" is the number of arguments passed to the foreign function, at most 4 arguments can be specified. Arguments are passed in specific global locations. To return values from a foreign call, assign them to argument variables using the internal assignment primitive. Arguments to foreign calls and important registers are passed in special registers and/or memory locations, depending on target: PARAM0 - PARAM3: 16-bit locations holding foreign call arguments #1 to #4. A: main value register. X0: secondary value register. Target A X0 PARAM0 - PARAM3 z80 hl bc 6502 $fb, $fc $fd, $fe $36, $37 - $3c, $3d uxn TOS 0004 000e - 0014 The most important runtime routines can be called using the CPUs "call" instruction ("jsr" on 6502), or as macros (uxn). Noteworthy definitions: fl_assign Takes a value in A and assigns it to the variable stored in X0 ("iy" register on Z80), increasing the reference-count, if necessary. On Uxn value and variable are provided on the stack. fl_deref Dereferences variable in A, if necessary. fl_getint Takes an immediate or boxed integer in A and converts it into a signed 16-bit quantity in the same location. fl_mkint Takes a signed 16-bit integer in A and converts it into the internal representation. Further support routines and control registers are defined in ".inc", also consult "rt0.s" for more detailed information regarding the runtime system. Assembler files can be included using the "foreign" declaration: -foreign('include.asm'). which includes the given file verbatim in the generated assembler code for the currently compiled source file. Normally, user code should not manipulate the reference counts of arguments or results, this is automatically handled by the compiler. Note that while your foreign code executes, the runtime can not process other processes, execution 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. 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". VIII. Bugs and Limitations * System limitations: - Maximum number of cells: 3328 (5365 on Uxn) - Maximum number of variables in a clause: 32 - Maximum number of active goals: 1024 - Maximum length of tuples: 7 - Maximum number of arguments in "foreign_call/1" forms: 4 * Known bugs and shortcomings: - The generated code is os too big and too slow. - Access to CP/M and KERNAL functionality is currently missing. - There is currently no 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. IX. Code Repository A git[8] repository hosting the sources can be found here: https://gitlab.com/b2495/fleng/-/tree/microfleng 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 [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 [10] https://gitlab.com/b2495/fleng/ [11] https://github.com/jhallen/cpm [12] https://vice-emu.sourceforge.io [13] https://100r.co/site/uxn.html [14] https://wiki.xxiivv.com/site/varvara.html XI. 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. XII. 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.