💾 Archived View for schinkel.bevuta.com › microfleng › MANUAL captured on 2024-06-16 at 12:47:16.
View Raw
More Information
⬅️ Previous capture (2023-11-04)
-=-=-=-=-=-=-
_______ _______ __ _ ______
_ _ _ ____ ____ ____ |______ | |______ | \ | | ____
|\/| | |___ |--< [__] | |_____ |______ | \_| |_____|
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:
<TARGET>-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.
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
@>/2 @</2 @>=/2 @=</2
atomic/1
data/1
number/1 list/1 string/1 tuple/1
idle/0
known/1 unknown/1
otherwise/0
Declarations
include/1
initialization/1
foreign/1
mode/1
provide/2
machine/1
CP/M specific
bdos/3
Uxn/varvara specific
set_vector/2 get_vector/2
reset_vector/1 reset_vector/2
set_colors/3 set_position/3 set_filename/2
read/1
draw_pixel/4
draw_sprite/5 draw_sprite/7
draw_string/6
set_screen_size/3 get_screen_size/2
set_screen_auto/2
get_button/1 get_mouse_button/1 get_mouse_scroll/2
read_file/4 write_file/4 append_file/4
delete_file/2
file_information/2
datetime/2
set_sample/3
play_audio/4
set_screen_vector/1 set_audio_vector/1
stop/0
load/1
A number of additional libraries are provided with the system that
are written in FGHC and are automatically available (if platform-specific)
or can be accessed by using the "include" declaration (portable):
Platform-specific:
z80c Architecture-specific code
m65c
uxn
cpm Access to some basic CP/M system functions
c64 ' Access to some fundamental KERNAL operations
varvara Access to the "varvara" virtual computer[14]
Portable:
lib Base library
fmt Formatted output
map Balanced AVL trees
sort Sorting
set Lists as sets
list List processing operations
scan Basic parsers for character sequences
The platform-specific libraries are included by default, the compiler
makes sure that unused definitions to not consume any code space.
For the portable libraries, consult the comments in the relevant library
files for information on how to use the definitions in these files.
Platform-specific predicates for accessing OS functionality are
described below.
- Alphabetic list of primitives:
GOAL1, GOAL2
Execute goals in parallel. The goals may not be variables.
GUARD -> 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 "<name>(<mode>, ...)", where "<mode>" 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
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_<N>", where
"<N>" 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
"<ARCH>.inc", also consult "rt0<ARCH>.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
- 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.