💾 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.



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.



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.



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.



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.



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.



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



- 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.