NAME
prog_unit -- unit to execute (small) C-program
PROTOTYPE
unitptr prog_unit( int iDimX, int iDimY, int iDimZ, int iDimT, char *pcFmt, char *pcProg, unitptr uHost)
ARGUMENTS
- int iDimX
- dimension for X-array
- int iDimY
- dimension for Y-array
- int iDimZ
- dimension for Z-array
- int iDimT
- dimension for T-array
- char *pcFmt
- option string, allowing to predefine up to four array variables and optionally make these visible as input or output fields of dimensions specified with the first four arguments iDimX..iDimT. The first letter pcFmt[0] of pcFmt determines whether array X is input ( pcFmt[0]='i'), output ( pcFmt[0]='o') or only available for internal use ( pcFmt[0]= any other char). Similarly for the next three letters and the arrays Y, Z and T. Likewise, letters '[' and ']' map the corresponding array to input or output fields of the type string.
- char *pcProg
- string holding program source code
- unitptr uHost
- host unit
RETURN VALUE:
A pointer to the created unit or NULL in the case of an error.
SYNOPSIS:
Creates a unit that executes a C-program whose source text is given
as a string pcProg. Supported syntax is a subset of C, with the
only data types const, float, int, struct and one-dimensional float,
int and char and struct arrays, but a facility to introduce
from shared libraries
additional, user-defined data types that appear as classes.
All globally declared variables and all local static variables
will retain their values between invocations of the prog_unit.
The non-standard keywords INP or OUT in place of static
front of a declaration of one or more variables makes these
'visible' as input or output fields of the created NST unit
(for details, cf. below).
Additionally, up to four arrays with fixed names X[], Y[],
Z[] and T[] and dimensions given by iDimX..iDimT can be
predefined through the interface of the prog_unit constructor function
(cf. description of argument parameter pcFmt).
There is some support for pointer variables and dynamically
allocated memory; pointers declared after an INP or OUT
keyword give rise to dynamic input or output arrays (cf. below).
Supported control constructs are the usual if, for, do,
while and switch constructs. As a non-standard extension,
a switch may use a string value (in this case, all case values
must be string constants too). Functions with return values
float int or void and argument parameters float, int,
struct, float*, int* or char* can be declared and defined,
including automatic, static and const local variables.
The usual math functions and operations, as well as printf
and scanf are predefined. Additionally, a strlen() and a
strdup() function on char or float arrays and a
+ operator that is interpreted as concatenation when occuring
between char arrays facilitates string handling.
The special predefined functions exec_opnds(), adapt_opnds() and
exec_container_opnds(), adapt_container_opnds() allow to execute or adapt an
interactively selectable number of successor units (``operands'')
of the prog_unit and/or its container unit, resp.
The predefined goto labels NST_INIT, NST_RESET,
NST_EXEC and NST_ADAPT may occur among the global program
statements and will then
serve as entry points when the corresponding NST operations
are applied to the prog_unit.
The non-standard assignment operator ":=" allows to
evaluate matrix algebra expressions (cf. below).
It is also used to copy class objects (while '=' only
re-references).
A #import command allows to extend the set of predefined
function with additional functions from shared libraries.
For example, the string.so and file.so libraries provide
most string and i/o function calls familiar from
string.h and stdio.h (including creating and accessing
files).
The class keyword allows to access user-defined classes
implemented in a shared library in a similar way as
for a #import'ed library. See Section Classes below.
The #debug and #break directives support some
debugging options (see DEBUGGING). The #implicit
directive allows to restrict the otherwise implicit
definition of scalar variable symbols.
The keyword public in front of a parameterless void
function makes it an externally callable 'method' and
turns the created prog_unit into a class container
with a separate method subunit for each public
function defined in this way. The interface of each
method subunit will then be given by the subset
of INP and OUT declarations within its local scope.
All remaining INP/OUT declarations will define the
interface of the enclosing class container.
Instead of a main() function, the corresponding
program statements are expected to directly
follow after all definitions of global variables
and functions (which must appear in this order)
have been made.
A throw() function is provided for error handling.
DESCRIPTION:
Expects pcProg to contain the code of a C-program that will be
executed at each subsequent exec_unit - call.
There is a simple way to dynamically extend the set of predefined
functions by self-written functions (see below).
There is the possibility to execute all or parts of the code also
in response to adapt_unit and ctrl_unit calls, and to execute or
adapt other units from within the C-program of the prog_unit
(see below).
This unit is provided to allow the interactive
specification of specialized NST-units from within Neo, using a subset of
C.
SUPPORTED SYNTAX:
Supported is a subset of the C-language plus a non-standard
extension for evaluating matrix expressions.
Primitive Data Types:
There are the primitive data types float (which is the default
and need not be declared), int (which must be declared)
and one-dimensional float, int, char or byte arrays
which also must be declared (and optionally initialized).
Additionally, constant values can be declared with the
keyword const; in this case, their type (.float or int)
will be inferred from the given expression, e.g.
const n=10/4, f=10./2;
will define n as a constant integer of value 2, and
f as a constant float of value 5.
Currently, int values are limited to a useful range
of about -1.6E7..+1.6E7 (if larger values occur, no
warning will be issued, but arithmetics becomes
unreliable, since most computations on int's are
done via floats, which outside the given range
become spaced too far apart to represent consecutive
integers).
Char and byte arrays are interchangeable for most purposes;
the main differences are (i) if they are made to appear as
NST input or output pins, char arrays are mapped to strings,
while byte arrays are mapped to byte vector pins, and
(ii) byte arrays are saved fully in hexadecimal format,
while char arrays are saved as strings and only up to
the first null byte.
Additionally, there are float*, int*, char* and byte* pointers
(with an analogous minor difference between char* and byte*)
and class data types (cf. below).
Declarations must appear at the beginning of their
respective scope (either a function body or the entire program).
Example:
const n=2, pi=4*atan(1);
static float a[2*n], b[n]={-0.5,0.5}, c[]={pi/2,pi,3*pi/2};
float *p,r;
Global variable and function declarations may be interspersed,
but each declaration is only visible below its first occurrence.
All variables that are declared as static will become
automatically saved with the prog unit, so that their values
become restored when the prog unit is later loaded
This feature becomes suppressed, however, if a static variable
is explicitly initialized (such as b[] and c[] above).
Global variables not declared static still retain their
values between invocations of the prog unit (but not
between load/save's).
Strings are supported either by using char or byte arrays,
or by using a float array to hold each letter as a float
value (deprecated - only for backward compatibility with
earlier versions).
Corresponding initialization
and elementwise char value assignments are supported, e.g.
static char s[]="hello world", mystr[4]="abc";
s[0] = 'H'; s[6] = '\n';
Such strings are always limited to a maximal number of chars
given by the array dimension minus one. See also Section Pointers.
DEPRECATED : there are up to four special predefined arrays
X[], Y[], Z[] and T[] for accessing values from input
or writing values to output fields of the prog_unit instance
(which of X,Y,Z,T is mapped to inputs or outputs is described below).
For backward compatibility, there are also four predefined macros X()..T()
of the same name to access the array elements.
There are the predefined constants NX, NY, NZ and NT
that provide the dimensions of the four predefined arrays.
Further predefined constants are M_PI for pi and NaN.
Finally, __LINE__ and __NAME__ provide the current line number
and the name string of the NST unit.
Struct Data Types:
The primitive data types can be used to define (non-recursive)
structures:
struct mytype {
int i=4711, j;
float af[] = {1,2,3};
char *string = "hello";
} u,v[100];
struct mytype x, y[12];
...
This will define two variables u and x and two
arrays v[100] and y[12] of structs. Not that,
unlike classes (and in line with C usage) declared
struct variables (including the elements of arrays)
are always instantiated.
Currently, struct instances are always static.
Copy and member assignments follow the syntax
u := x; // now u independent copy!
u.af[2] = 1234; // access an element
Reference assignments, such as
u = x; // will u only let reference x
are also possible, however, they require
that the struct has no members that are
arrays (i.e., the mytype struct would not work
here).
Although seldom needed, the name of the struct can
be used as a (parameterless) constructor function:
x = mytype();
for (i=0; i<100; i++) v[i] = mytype();
Struct data types can be declared as INP and OUT
parameters (giving rise to a composite field with
a pin of corresponding type for each member variable)
INP struct mytype xx;
OUT struct mytype yy;
and as argument parameters for subroutines
(again by reference access implied):
void foo(int i, struct mytype z) {
z.string += " world";
z.i = i;
}
...
foo(34,u);
// now u.i = 34 and u.string = "hello world"
However, inside a subroutine a referenced struct argument
cannot be re-allocated. Currently, there are also no
struct return values.
Structs can also have class members (cf. section
Classes below).
Finally, structs can serve as type arguments for
template classes (cf. below). In this way, suitable
template classes permit an organization of struct
instances into lists, trees, graphs etc. (this avoids
the need for recursive structs with struct pointer
elements and encapsulates the (often complex) handling
and maintenance of such structures in the template
class).
Input and Output fields:
The keyword static may be replaced by either of the
special keywords INP and OUT. In this case, the
declared variables again will be static, but they
will additionally become accessible as input or output
fields of the prog_unit.
Arrays of types float[], int[] and byte[]
give rise to packed float int or byte fields of corresponding
dimension. Arrays of type char[] will give string
field of corresponding dimension.
Float or int variables that appear
consecutively within the same declaration are 'glued'
together into a single scalar float field (also for
ints!) with one pin per variable. A float array can
also be mapped into a scalar field by specifying
instead of float the nonstandard keyword scalar
after the INP or OUT.
Composite Fields can be formed by enclosing several
declarations after a INP or OUT in curly brackets,
e.g.
OUT { float x,y; char ac[5]; float af[10]; }
will make a composite output field with 4 pins:
two scalar pins x,y, one text pin ac, and a
float vector pin af of 10 elements (all elements
are taken as static, i.e., will be saved unless
initialized, cf. LOAD/SAVE below).
A C++ style comment in the same line as the INP or
OUT declaration will be accessible as an info text
under Neo.
Synchronization of Input and Output fields:
The values of input and output fields are synchronized
only when the prog_unit starts or ends execution, or
when an exec_opnds() or adapt_opnds() call begins
or ends (in the latter case, only the fields of the
calling unit are synchronized). Sychronization
means, that changes from outside the prog_unit become
visible inside and vice versa. Sometimes (e.g., when the
called operands access pins of the prog unit that do not
belong to the calling unit (which may be a method subunit),
or when an #import library function is called that itself calls
external units) an extra, immediate synchronization of all
pins may be required. This can be achieved with a call of the
predefined parameterless function sync_ports().
Dynamic Arrays:
Pointer variables (cf. below) after an INP or OUT
keyword give rise to an input or output field for
a float or int vector of dynamically changeable dimension,
e.g.:
INP float *p; OUT int *q;
Here, INP pointers cannot be initialized (they just
provide access to the input vector and follow its
dimension; an exception is the special empty
initialization p=, which only serves to
exclude p from being saved to file, cf. below),
while OUT pointers can be treated as
usual. Likewise, the type specifier byte* yields
a dynamic array of bytes and a corresponding
NST dynamic bytes output pin (currently, the
char* will yield a dynamic float output field;
this should not be used, since it may change in
future versions).
String Inputs and String Outputs:
After a INP or OUT declarator it is also possible to
use the type specifier char or byte instead of float
or int.
This has the effect to make the declared variables
visible as NST fields of type string (for char)
and of type byte (for byte) in the interface
of the prog_unit. The string will be always clipped
to a maximal length of array dimension minus 1
(scalars are treated as arrays of dimension 1 and
are thus not useful in this context).
Functions:
Functions can be defined with return types void,
float or int. The parameter list can be void,
or it can be any combination of float, int, float*,
int*, char* or struct arguments. Char* arguments
expect a pointer or an array that must have char or
byte elements (.i.e, no float array is allowed).
Float* arguments admit arrays or pointers of both
element types, but in the case of the char element
type there will be some overhead due to allocation
of a temporary buffer to pass a suitably converted
copy of the char array, whose contents will be written
back when the function returns. Therefore, for large
arrays or time critical calls one should always pass
the matching type.
Local variables can be declared in the usual way, including
const and static var. Static local variables can be
made visible in the interface of the NST unit, using
INP and OUT instead of static, as described above.
To allow mutually recursive function calls, preliminary
function declarations, such as
float myfun(float x);
can be made before the full definition of the function
occurs.
Implicit Variables:
If a function introduces implicitly defined variables
in its body, the scope of the implicit definition
is limited to the function, unless there is a global
definition of the same name before the function.
This is a subtlety that is unknown in standard C,
since no implicit definitions are allowed there.
Classes:
The keyword class permits to define static instances
of classes that are implemented in shared libraries
(for a description, see Section Implementing Classes below).
Classes admit linear arrays, e.g., if foo is a class
with a constructor that expects two integer arguments,
the following definition
class foo x, y[10], z(3,4);
will define x to be an uninstantiated foo reference,
y to be an array of 10 such references, and z to
be a reference that is initialized to a foo instance
created by the foo constructor called with parameters (3,4).
(if there is a (void) constructor: x() will
create an instance, x only a reference).
Constructors in class declarations (e.g., z(3,4) above)
must not have input variables among their arguments.
E.g. the following would be illegal:
INP int iDim;
class foo z(iDim,4);
The reason for this restriction is that the input values
of a prog_unit are copied into the associated variables
only much later (namely at execution time), so that the
above code would look grossly misleading (the constructor
would always see the value iDim=0).
Membership functions are then invoked in the usual way, e.g.
...
y[3] = z; // make y[3] reference instance z
y[4] = foo(1,2); // create y[4] as new instance
u = z.method(4.56); // invoke method of z
u = y[3].method(4.56); // the same via y[3]
y[3] := z; // make y[3] a copy of instance z
...
Accessing members of an uninstantiated class variable
or using it as a function argument will cause a
runtime error.
Class instances can occur as members of structs:
struct mix {
int i;
class foo obj;
} x,y;
Currently, such members are initially uninstantiated
(replacing obj with the full constructor obj(args..)
would not be accepted); they also cannot be accessed
with the "naive" syntax
x.obj.method(4.56); // does not work
Instead, one has to use an auxiliary variable
class foo p;
...
p = x.obj;
// now access x.obj via p:
p = foo(1,2); // instantiation
p.method(4.56); // invoke foo-method on obj
To make class variables participate in load/restore,
declare them with an explicit static keyword, e.g.
static class foo x, y[10], z(3,4);
instead of the above. This requires that the class
has a RESTORE method defined (the older LOAD
method permits no uninstantiated variables).
Class arrays can be dynamically resized when declared
without dimension:
static class foo z[]; // has initially 0 elements
z = resize(20);
printf("%d\n",dimof(z));
will create an array with 20 uninstantiated elements
and print out the new dimension. Later resizes that
make the array grow will leave already instantiated
elements untouched. When an array is shrunk,
unused instantiated elements will be freed if they
are not referenced by other variables.
The statement
x = NULL;
puts x back into the uninstantiated state (when x was
the last reference to an object, the object will become
freed too, otherwise it is kept alive for the remaining
references).
The instantiation of a class variable w can be checked
with the ! operator: !w is true (value=1), when w
is uninstantiated, false (0) otherwise, and !!w is
the logical complement (simply using w will not work,
since it cannot be interpreted as a number).
Template Classes:
Classes can be implemented to offer member functions
that work on choosable data types. Such classes are called
template classes and can, e.g., offer structured storage
and access of general objects. When creating instances
from a template class, the necessary type parameters are
passed in acute brackets:
class foo <thistype,thattype> u(4711);
...
(assuming that foo denotes a template class that expects
two type parameters, which here have been chosen as
class thistype and class thattype (note that the
class keyword is implied within the <..> brackets;
currently, allowable template types are only (non-template-)
classes or struct types.
For details about the implementation of template
classes, see the Neo Tutorial.
For a template class that does not use any of its template
types in the constructor function(s), the <...> can always
be omitted, or some of the type names be specified as asterisks '*'.
This will restrict the callable member functions to those
that do not involve any of the unspecified types.
Public Functions and Class Containers:
Parameterless functions of return type void may be declared
public. In the presence of one or more public functions,
the created prog_unit will become a NST class container, with
one method subunit for each public function. The interface
of a method subunit will consist of the subset of static variables
of its associated public function that were declared with
INP and OUT keywords (these variables will be withdrawn from
the interface of the enclosing class container). The name of
the method subunit will be the name of its associated public
function (truncated to a maximum length). Public functions
can call other functions (public or not), and they share
all global variables.
Note that this offers a flexible and convenient
way for a rapid prototyping of NST class containers,
in particular, if the
public functions are used as wrappers of compiled
C-functions introduced with the #import directive.
A call of a public function involves only a synchronization
of the input and output pins that belong to the public function.
To also synchronize the pins of the main prog unit,
call the routine sync_ports().
Implicit Declarations:
By default, scalar float variables need no explicit declaration.
If an undefined identifier occurs within the scope of
a function, it will be implicitly declared as an
automatic float variable that is local to the function.
If it occurs globally, it will be implicitly declared
as a global static float variable.
The possibility of implicit declarations introduces
some subtleties unknown in standard C:
// start of program here!
foo1(void) { i=5; printf("%g\n",i/2); }
foo2(void) { i=7; printf("%g\n",i/2); }
foo1(); foo2();
will print the values 2.5 and 3.5, since in each routine
i is an implicitly defined variable of type float
that is confined to the scope of its definition, i.e.,
the respective enclosing function. If the above
is changed to
// start of program here!
foo1(void) { i=5; printf("%g\n",i/2); }
int i;
foo2(void) { i=7; printf("%g\n",i/2); }
foo1(); foo2();
the output will now yield 2.5 and 3, since foo2
(but not foo1()!) now falls into the scope of
the newly defined global int variable, so that
the division now is treated differently (the prog
unit's printf function is nice enough to accept
also integer arguments for its %g format).
If foo2() is changed to use the explicit
declaration float i=7; the old behavior is
restored.
If such things appear too dangerous, the #noimplicit
directive at the head of the program allows to re-instate
the standard C policy to require declarations for all
variables. A directive #implicit \"ijk" offers the
compromise to allow only single-letter variable
names from the given set of letters.
Operations:
Supported unary operators are ++ and -- (both as post- and prefix),
supported binary operators are:
= + - * / % += -= *= /= < > <= > != and == with
the same rules for forming expressions and assignments
as familiar from C. Not supported is the comma-operator,
the ()?: - construct and any operators
involving the tilde, | or &. As a result, boolean expressions are restricted
to a single comparison of two operands, or to a single operand
(use brackets to process more complicated boolean expressions).
The non-standard assignment operator := allows to evaluate
matrix expressions (cf. below). The '+', if between
pointer variables or string constants, is interpreted as
string concatenation, e.g. p = p + "hello".
Control Constructs:
Supported control constructs are while() for(;;) (including
break and continue ) and if() - statements
(including an optional else ), unsupported are switch and
do.
Comments:
C-style and C++ style comments can be used. A C++-style
comment that follows an INP or OUT declaration on the same line
is automatically used to make for Neo an info-text for the
corresponding input or output field.
Built-in Functions:
Supported functions are the usual float valued math functions
(.sqrt, log, log10, exp, rint, floor, ceil) and the
trigonometric functions ( sin, cos, tan, sinh, cosh, tanh )
together with their inverses. The function pow
is replaced by the binary operator ^. The function abs returns
the absolute value of its argument, but keeping the type
of the argument in the result. In addition,
the printf and sprintf functions are supported.
A few additional functions are provided:
erf and erfc (error and complementary error function),
fermi(x)=1/(1+exp(-x)), step(x)=(x>0)?1:0,
noneg(x)=(x>0)?x:0, rnd(x) returns random
number from uniform distribution in interval [0..x], srand(x)
sets seed value for rnd). grnd(x) returns gaussian random numbers
with variance x and finally isnan(x) which is 1 if x is
the non-value NaN, 0 otherwise.
The int valued functions dimof(a), bgnof(a)
and endof(a) expect an array or class array parameter
a as their argument and return its
dimension and the value of the start and the
end (i.e., the highest allowable index value plus one) of the
index range of a so that
for (i=bgnof(a); i<endof(a); i++)
will correctly iterate over the index range of array a. This
is important when iterating over elements of arrays that have
been passed as parameters to functions: their index range
may appear shifted, due to calls such as f(a+5) etc.)
If no shift is present, bgnof(a)=0 and endof(a)=dimof(a).
Additionally, the special function dim_from_mask(msk,d)
where msk is a string and d a nonnegative integer dimension value
computes the dimension of a subvector created according to
msk from a subset of elements of a d dimensional vector
(the subvector itself is not created by this routine; it provides
only the dimension calculation). For the format of the mask
string msk see the manual page of class extract
Predefined functions with array arguments:
A few functions admit array arguments and interprete them as
strings: strlen(s) returns the length of the string,
system(s) issues s as a system command (cf. section SYSTEM CALLS).
printf(fmt,...) and sprintf(buf,fmt,...) are analogous
to their C counterparts, but with an extension to print arrays
(see Printing of Array Elements below).
The call strdup(s,n) returns
a string that is a copy of the first n
(or all, if n is absent) characters of s.
An overloaded throw() function throws an exception and is
provided for exception handling (cf. section EXCEPTION HANDLING below)
All remaining built-in functions take a single
scalar argument. The set of built-in functions can be extended by
user-defined functions with the #import directive for accessing functions
in a shared library (see below).
If a syntax error is detected, the returned handle
will be NULL, and an error message that (approximately)
locates the error will be printed.
RESERVED WORDS:
The following may not be used as variable identifiers:
abs, acos, acosh, adapt_opnd, adapt_opnds, adapt_container_opnds,
adapt_unit, asin, asinh, atan, atanh, bgnof, break, byte, case,
catch, char, ceil, class,
continue, const, cos, cosh, ctrl_opnd, ctrl_unit, dimof, dim_from_mask,
endof, else, erf, erfc, errno, errtype, exp, exec_opnds,
exec_container_opnds,
exec_unit, fermi, float, floor, for, int, if, init_opnd, init_unit,
int, iterator, log, log10, isnan, main, M_PI, NaN,
new noneg, printf, public,
reset_opnd, reset_unit, resize return, scalar, sgn, sin, sinh,
sprintf, sqrt, static, step, strdup, strlen, struct, system, tan, tanh,
throw, while, INP, OUT, NX, NY, NZ, NT, NULL, PI,
SHARED, X, Y, Z, T.
Further reserved C-keywords may be added to this list in the
future. It is thus recommendable to avoid C keywords for
variable identifiers or functions in advance.
STRING AND ARRAY HANDLING:
Array elements
and string characters can only be accessed by indexing. However,
functions that expect array parameters may be passed offset
"values", provided that the array identifier comes first, e.g.
char a[] = "hello world";
pi = 3.14;
printf("%s\n",a+pi-1);
will print "llo world", but replacing s+pi-1 by, e.g.,
pi+s-1 or (s+pi-1) would not be accepted. If a is an array
of dimension d, and n>=0, the usage of a+n as an array
parameter will be treated as an offset array with the shifted
index range -n..d-n. If such arrays are passed to
#import-ed functions, their upper dimension will be
passed as d-(int)n. In this case, negative shifts n
should be avoided, since no information of the lower
index bound is passed to #import-ed functions.
Note also, when a is an array and
not a pointer, a new value can not be set with s="new text".
Use
sprintf(a,"%s","new text");
instead. If a is too small, any assignments will be suitably
truncated. Finally, if v is a float variable, &v will be
accepted as an array with a single element of value v.
Redundant combinations such as &v[0], however, are not supported.
If a float array is used as a string, the first float element that,
if rounded to an int, is 0, determines its end. If there is no such
element, the length will array dimension. prog_unit functions need
not have extra parameters for passing array dimensions: such
information is always implicitly passed to their
C implementation function (cf. below).
The '+' between pointers (and, if not in first position,
also arrays) that have been declared as
char* performs concatenation, e.g,
if s is a pointer variable holding a string,
the statement
s = strdup(s,n) + p + strdup(s+n);
inserts a string p after the n-th character into s.
NOTE: Unlike in C, initialization of automatic
variables is restricted to constant values. This
restriction does not apply to pointers (cf. below).
Printing of Array Elements:
The predefined printf(fmt,...) and sprintf(buf,fmt,...)
functions provide a convenient extension to print array elements:
Each conversion specifier may be preceded by the non-standard
length-modifier v, optionally followed by a decimal integer d.
The corresponding argument is then expected to be an array
whose elements will then all be printed, applying for each the
current format directive. If a non-zero decimal integer d was
given, after each block of d array elements an additional
newline character will be inserted. If no field width was
specified, an additional space character will be inserted
between otherwise adjacent array elements. Example:
printf("A vector: %5.2v3f---%vd\n",afVec,afVec);
will print
A vector: 1.00 2.00
3.00 4.00
5.00 6.00
7.00---1 2 3 4 5 6 7
when afVec is a vector holding the first seven integers.
When the array argument is NULL, nothing will be contributed
to the output by the associated conversion specifier.
Dimension Revision Directive:
For #import-ed functions, a dimension
cast allows to pass an array (or pointer)
parameter with an apparently (for the called function)
revised dimension:
static float a[]={1,2,3,4,5}, i=1;
...
myfun((DIM 3) a, (DIM i+1)(a+1))
will make myfun see the arrays 1,2,3 and
2,3 of dimensions 3 and 2, resp. (there is
no analogous directive for functions that
are defined in the prog unit source code).
Any cast with DIM specified larger than endof(a)
will be without effect. A cast to dim 0 or to a
negative dim will be equivalent to passing a
NULL pointer.
POINTERS:
Pointers can be declared in the usual way, e.g.
static float *p=NULL,*q="abc";
There is no pointer arithmetics; pointers are just provided
to simulate arrays with dynamic dimension:
p = new(d); // allocation of d elements
p = resize(n); // resize to n elements
resize p,q,r = n; // resize several pointers
p = p + "hello"; // string concatenation
p += "hello"; // the same effect
p = a + i; // ref to array el a[i]
p = (DIM 2) a+i; // dto., but make .p appear as
// if only 2-dimensional
p = vecfun(42); // allocation with result
of vector valued function
After the first statement, p can be used like an array
variable with d elements. The second statement resizes
this 'array' to n elements, keeping the first min(n,d)
values intact. The third statement uses a more compact
notation to achieve the same for several pointer arguments
in one statement. The fourth statement concatenates two strings
(with a char pointer var p interpreted as a null-terminated string)
and resizes the result to the required length. The fifth
statement achieves the same effect, and the sixth
statement requires a to be an array variable.
In this case, p will have dim(p)=dim(a)-i and
an allowable indexing range of
bgnof(p)=0..endof(p)=dim(p). The dimension cast
in the next statement allows to make p appear
with a smaller dimension (this is useful, e.g.,
in conjunction with matrix algebra expressions).
The last statement
allocates p to the result of a vector-valued
#import function. If the return type of vecfun
is char*, it can also be used in string concatenations,
such as
p = p + vecfun(42);
There is a subtle but important difference in the
interpretation of arrays and pointers after a '=':
an array introduces always a reference (such as the
last statement above) and may only be followed by an
address offset, but a char or byte pointer always
introduces a string assignment or a string concatenation
and may only be followed by further char/byte
pointers, arrays or string
constants. After a '+=', we always have string
concatenation, and there is no difference if an
char/byte array or a pointer follows.
(there is no analogous definition for the action
of + or += between float arrays).
There is no need to care
about freeing memory: when the number of references to
a memory block drops to 0, it is automatically freed.
If p is the only reference to a block,
p = NULL;
will enforce its freeing (.p=resize(0) or p=new(0)
are equivalent). The special pointer value NULL
may also be passed in place of an array argument
to a function.
A quick way to
copy n elements in the midst of an array into
a pointer variable is p=new(n); p=a+i; p=resize(n);
Alternatively, p = (DIM n) a+i makes p appear
as a pointer pointing to a n-dimensional array with
first element a[i], with no copying involved.
No other types of pointer assignments are allowed, i.e.,
there is no pointer arithmetics such as in C, not
even assignments such as p=q+i when q is a pointer
(and not an array). However, pointers
can be passed as float* or char* arguments to a function
(in this case, shifts, such as f(p+i) are allowed).
Within the scope of the function, a passed pointer
argument appears as an array (and no longer as
a pointer!) of corresponding dimension. As a
consequence, pointer assignments such as the above
are no longer allowed inside the function.
Pointer Initialization:
While static pointers are initialized only once at
the beginning (and thus require constant initializers),
automatic pointers within a function
are newly allocated at each invocation (and deallocated
when the function returns). Example:
float fac(float i) {
float *p=new(10+i);
if (i==1) return 1;
else return i*fac(i-1);
}
The call fac(3) will recursively allocate three
memory blocks of 13, 12 and 11 elements, resp.,
each referenced by the instance of the pointer variable
p within its current scope. Upon return of the function
invocations, the three blocks will then be freed in
the reverse order of their creation.
Pointers defined as INP cannot be initialized
(the only exception is the empty initialization
INP float *p=; to suppress load/save of their
data (cf. below)). Conceptually, they
inherit their elements from the (dynamic!) input
field that they represent. Pointers defined as OUT
can be initialized as usual, making the dimension
of the corresponding (dynamic!) output field vary
accordingly.
Initialization of a pointer also allows to control
whether it participates in load/save operations
(cf. LOAD AND SAVE below).
Initialization of a pointer p can be checked with the
value of dimof(p) or with the same syntax as for
objects: !!p is true (non-zero) when p has been
initialized, false (0) otherwise (and vice versa for !p).
EXCEPTION HANDLING:
The predefined function
throw(char *pcErrType, char *pcFmt, ..)
is provided to throw an exception. The first argument
pcErrType should be a string of zero or more
colon-separated identifiers
pcErrType = "ident1:ident2:...identk"
and is used to specify a type for the exception.
The remaining arguments are parsed as in a printf
statement to associate with the exception an optional
error text.
The call will deposit pcErrType and the error text
in the two predefined global char arrays errtype[]
and errtext[] and starts the search for an exception
handler.
Exception handlers can be defined within the prog_unit
in the form of catch block chains
catch(Arg1) { ... statements ... }
catch(Arg2) { ... statements ... }
...
catch(Argk) { ... statements ... }
In contrast to the familiar C++ construct, here each
Argi is a constant string of the same syntax as
pcErrType that is matched against
the pcErrType of the current exception. The first catch
block whose Argi matches some initial portion of
the current pcErrType will be executed to handle the
exception, after which processing will continue after the
end of the catch chain. If no handler in the nearest
following catch chain matches, the exception will be
propagated one calling level up and the search will
recursively continue from there, until either a matching
handler is found, or the top level is reached. In this
case, propagation continues into the NST execution chain
(there can be handler units instead of catch blocks),
and if this fails, too, the exception finally appears
as a runtime error message at the top level.
A catch block that has started handling can still
pass control to subsequent catch
block candidate by raising itself an exception (in
particular, a parameterless call of throw() will pass
on the current exception, with errtype and errtext
unchanged), or it can be left at any point
with a break statement occuring at block scope
(i.e., not nested in a for or while body).
For throwing exceptions from within an #import function
or class, see Section PASSING EXCEPTIONS FROM EXTENSION
LIBRARIES.
In contrast to C++, there is no try block construct
to restrict the code that can throw an exception.
The prog unit builtin functions may throw exceptions of
one of the following pcErrTypes:
1. exceptions signalling bad argument parameters:
"badarg:type"
"badarg:value"
"badarg:array"
"badarg:array:type"
"badarg:value:none"
"badarg:array:dim"
"badarg:object"
2. exceptions signalling an attempt at a bad operation:
"badop"
"badop:div0"
"badop:index"
"badop:array"
"badop:array:dim"
"badop:array:type"
"badop:object"
3. exceptions signalling a detected bad result outcome:
"badres:array"
"badres:singular"
PROG UNITS AS NST EXCEPTION HANDLERS:
When the entire main portion of a prog_unit consists
only of catch blocks, the unit becomes a NST exception
handler. A NST exception handler does no longer react
to exec and adapt. Instead, it plays the role of a
catch block at the NST unit level, i.e., one can arrange
such units or chains of them to catch exceptions propagated
from other units (the search following analogous rules as
within a C++ program, with the units playing the role of
(possibly nested) statement blocks). The exceptions to
which a prog_unit will respond are then precisily those
that are defined by its catch blocks that make up main.
MATRIX ALGEBRA EXTENSIONS:
The non-standard vector assignment operator ":="
allows to assign to an array variable a vector or matrix
expression. For example, let A and B be float arrays of
suitable dimension, then we may compute expressions
such as
A := 2*(A^2+2*B+1)/B;
Here, the operators '*', '/' and '\\' between arrays
are interpreted as matrix multiplication, right and
left matrix division, resp. (i.e., A/B=A*Inv(B) and
A\\B=Inv(A)*B), in any other cases they are the usual
scalar operations. Addition of a scalar and an array
is elementwise, i.e., the entire array becomes shifted
(therefore, 1 is not to be confused with the identity
matrix!).
HINT: A^0 (cf. below) yields an identity matrix
of the size of A (which must be a square matrix).
An assignment to a subarray can be made with the syntax
A[first..last] := someOtherArray;
Here, first may also be larger than last. For instance,
A[dimof(A)-1,0] := A;
reverses the array A.
NOTE: don't confuse vector expressions with string
addition:
char *pc="hello", *w="world", *r1, *r2;
r1 = pc + w; // concatenation
r2 := pc + w; // elementwise addition
Vector or matrix expressions may also contain
calls of array-valued #import library functions,
e.g., if eigval() is such a function:
A := 2*eigval(B) - 1;
(such calls include an automatic conversion of
return types, e.g., from byte to float, if
necessary).
As a special case, instead of A there may also
be a scalar variable. This is treated as if assigning
to an array of dimension 1 and is handy, e.g., for
computing scalar products or other matrix algebra
expressions with 1-dimensional results:
scalar := vec1*vec2;
Two further operators on arrays require a square
number of array elements and assume in this case
that their array operand is a square matrix:
The single apostrophe "'" will form the transpose,
the hat-operator "^" (followed by an integral
scalar exponent) will compute an integral matrix
power (this includes the matrix inverse and the
identity matrix as special cases). Example ( A
square matrix):
A := (A + A')^(-2);
NOTE: applying either of these two operators to
a rectangular matrix will lead to a meaningless
result and will go unnoticed, if the matrix
has a square number of elements (the reason for
this is that the prog_unit actually knows
nothing about matrix shapes, except in bilinear
products, where the correct matrix shape can be
inferred (cf. below)).
The arrangement of matrix elements in an array
is taken as row-wise, e.g.,
(a00,a01,a10,a11) for a 2x2 matrix.
Array variables can be replaced everywhere by
pointers. This admits, e.g., to cut out rows or
shorter pieces of arrays.
If a matrix division contains a rectangular
(non-square) divisor, it will be interpreted
as the corresponding (left or right) multiplication
with the pseudoinverse. This includes the least-square
solution of overdetermined linear equations as
a special case.
While all matrix algebra expressions can work
with float arrays, those that involve
higher-order operations on matrices (matrix
division, inversion and exponentiation, except
for the trivial exponents 0 and 1)
will produce a run-time error when used
with int or byte operands (scalar scaling, addition
multiplication, transpose and the special matrix
functions described below are ok for all three
data types).
Syntax for Matrix Algebra Expressions:
Handling of linear algebra expressions is based on
interpreting linear arrays as matrices and on deriving
their matrix shape from the linear dimensions of
the arrays, when they occur in matrix products or
divisions with two factors. Therefore,
all matrix expressions are restricted to sums of terms
each of which contains at most two matrix factors
(if there is only a single matrix factor, there is no
way to infer its matrix shape, but we also don't need
it, since matrix addition is just elementwise,
regardless of matrix shape). Note also that a factor may
be an arbitrary power of a square shaped matrix,
i.e., products such as A^5*(A+B)^(-2) are perfectly
possible.
There are also two special restrictions on the use
of brackets in matrix expressions:
- 1.:
- a bracket must not contain any matrix products
or divisions, except those that can be written
as powers of a square matrix (e.g., (A*A+1)
is forbidden, but (A^2+1) is allowed).
- 2.:
- If a bracket contains a matrix expression,
the expression must start with an array
(i.e., instead of (2-3*A) we must write
(A*(-3)+2) or -(A*3-2)).
Both restrictions are for reasons of simplified
parsing and are not really limiting; they only
enforce that more complex matrix expressions (with
products or divisions of more than two factors)
must be built up in several steps from simpler
(at most bilinear) expressions.
Cross Product:
Any 3-dimensional product of two float vectors that are
themselves 3-dimensional float vectors is interpreted
as the cross product (since it cannot be the usual matrix or
scalar product in this case). Thus, the right hand side of
y := a*b + 4*c*(d-2);
is evaluated as "a x b + 4 c x (d-(2,2,2))" in this case.
Special Matrix Functions:
First of all, any one-argument
float functions (such as sin, cos, but also
self-defined functions or #import'ed functions) can
in a vector expression be applied to a vector
argument. In this case, they will be applied
elementwise, yielding a result array of the
same dimension as the argument array.
E.g., the update of a simple neural network
with weight matrix W and activity vector v
is the single line
v := fermi(W*v);
Additionally, there are three special functions to access
rows, columns or subrectangles of a matrix A:
- col(A,j,n):
- j-th column of A; n must equal
the column dimension of A
- row(A,i,n):
- i-th row of A; n must equal
the column dimension of A
- rect(A,x,y,dx,dy,n):
- rectangular submatrix
of dx columns and dy rows,
starting at column x and row y.
Again, n is A's column dimension.
These functions are only recognized within matrix
assignments, and they may appear even as the
left value of a ":=" assignment operator.
Example:
row(A,i,n) := row(A,j,n);
This copies the contents of row j of matrix A
into into its row i (overlaps in the case of
rectangles are permitted). If part of a specified
rect-angle lies outside of a matrix, the elements
in this part will be returned as zero.
In contrast, the col() and row() functions
use wraparound.
Imported Matrix Functions:
Further vector or matrix functions can be implemented
as #import library functions (however, such functions
may not be used as left values). If the element type
of the assigned array does not match, there will be
an automatic conversion.
Note the following difference between ":=" and "="
assignments (assuming a function float *foo()):
float *vec;
...
vec := foo(); // (1)
vec = foo(); // (2)
Assignment (1) is a vector assignment that does no
allocation for vec, i.e., in this case vec must
already have the same (or a larger) dimension as
the return value of foo() (or the ::= operator
is used to enforce the assignment even in case of
a dimension mismatch, cf. below). Assignment (2)
is actually a redirection of the pointer vec
to the array that is the return
value of foo(). In this case, vec automatically
assumes the correct dimension and need not be
allocated before. In (1), vec could also be
replaced by an array, in (2) not.
Currently, calls of vector valued functions, such
as foo(), cannot be directly used as function
parameters (use an assignment to an auxiliary
pointer for this).
Vector norm computation:
Additionally, |vec|[p] (where p is a numeric expression
that evaluates to a non-negative value) returns for p>0
the p norm of vec, while for p=0 the arithmetic mean
of all elements of vec is returned. Here, vec is a
vector expression restricted in the same way as for a
bracket in a matrix expression (such as A-2*B,
i.e., vec must start with an array identifier and have
no matrix products, cf. 1. and 2. above).
If the part [p] is
absent, the default p=2 (i.e., the euclidean norm) is
assumed. For instance, to compute the standard deviation
of all elements of a vector A, one may use
stdev = | A - |A|[0] | / sqrt(dimof(A));
Handling of Dimension Mismatches:
If a sum of vectors contains vectors that are
longer than the result dimension, the excess elements
will be ignored. Similarly, if one or several vectors
are too short, the sum will be formed, but some
elements in the result may not be set by any summand(s).
In both cases, run-time warnings are issued that
can be suppressed by specifying the assignment
with "::=" instead of the usual ":=".
An exception to this treatment are scalar summands;
they are considered as shorthands for arrays
of the required result dimension, but with all elements
equal.
The result dimension of a vector assignment
is the dimension of the array on the left hand
side. If the left hand side is a scalar variable,
it is treated as if it were an array with a single
element, e.g., to compute a scalar product f of two
vectors v1 and v2, one can use
f := v1*v2;
Within a bracket, the result dimension of a vector sum
is determined by the dimension of the leftmost array
in the bracket.
In matrix products and divisions, all dimensions
must match precisely, otherwise the operation
cannot be carried out and a run-time
error is issued.
Depending on the number of array elements of their
factors, different matrix products in a sum
may become interpreted as matrices of different
shape (but with the same number of elements).
In this case, their sum will still be computed
by elementwise (vector-) addition, although the
result of course cannot any longer be interpreted
as a matrix sum.
EXECUTION OF CREATED UNIT:
Will carry out the statements in pcProg. Initialization
for static variables (arrays and const values) will
only be carried out once before any other operation).
Communication with other units is
achieved by making some or all of the special arrays
X[], Y[], Z[] and T[] or additional static variables
visible as input or output fields
of the created NST unit (this requires an entry of 'i' or 'o'
at the corresponding character positions of pcFmt or
the use of INP and OUT instead of static )
The predefined goto label EXEC can be used to specify an entry
point for execution that differs from the first statement.
This is achieved by inserting the sequence
`EXEC:'' before the desired entry point.
ADAPTATION OF CREATED UNIT:
An adapt_unit call has no effect on the unit, unless the program
text contains the sequence
`ADAPT:''. In this case, it will specify an entry point
that is used for each adapt_unit call (similar as explained
for execution, see above).
LOAD AND SAVE:
All explicitly defined static variables that are not
explicitly initialized in their declaration are saved and
loaded under the NST save/load methods. Thus, initialization
in a declaration can be used to selectively exclude variables
(such as, e.g., a big array)
from (possibly time-consuming) save/load operations (even
a void initializer list, such as for a[n]= suffices
(this can also be applied to class instances)).
Also, omitting the keyword static will exclude a
variable from load/save, unless it is declared as
INP or OUT (these always imply static).
If a variable is declared as INP, it is also considered
as being initialized. Further, the up to four predefined
arrays X..T are never included in the saved/loaded set (if it
is desired to include them, they must be declared
explicitly in the program instead).
The matching of loaded to existing variables is by
their name and the name of the function in which they
occur. Thus, introduction of new or reordering of the
old variables will not do any harm (but redimensioning
will!).
Load and Save for Pointers:
Dynamic memory referenced by static pointers is handled
analogously: if the pointer has not been explicitly
initialized in its declaration, its currently referenced
memory block will be included in a save, if it does not
coincide with the memory of an array (i.e., it is required
to by truly dynamic). A similar rule applies for loads:
if the file contains data for a pointer variable p that
has not been explicitly initialized, an implicit
p=new(d) operation will be carried out such that p
can accomodate the data, then the load will be made.
An exception are pointers that are INP variables: their
dimension will not be changed under a load operation, and
any surplus data will just be ignored.
To benefit from loading of pointer data, of course, requires to make
processing of a pointer dependent on whether it
references any elements or not. While queries
on the 'value' of a pointer are not permitted,
both !p and !!p can be queried and are logical indicators
that p references no elements or has been initialized,
resp.
CONTROL MODES:
In addition to the EXEC and ADAPT goto labels, there are
two further goto labels INIT and RESET that define similar entry
points used when the NST_INIT or NST_RESET message is sent.
Imported class objects (cf. below) can define a CTRL_classname method
that will become invoked for each class instance and for each ctrl
call sent to the prog_unit.
Note:
For backward compatibility, the abovementioned goto labels
may be preceded by a (redundant) keyword case.
PROG_UNIT AS AN OPERATOR UNIT:
The presence of a call to the parameterless void function
exec_opnds() or adapt_opnds() makes the prog_unit
into an operator unit.
Each call of exec_opnds() is translated into an exec call
for each of the prog_unit's operands (the number of
operand units can be interactively adjusted in Neo,
or set with nst_operands_of() at the programming level).
If the exec_opnds() call appears within the scope of
a public function, the operator status will instead
be given to the corresponding method subunit (or its
referencing use_method or use_named unit).
Note that this happens only, if the exec_opnds()
occurs explicitly in the public function and not
hidden inside another function call.
In particular for iterators implemented in this way,
there usually will be nested references to the same
method subunit. To avoid side-effects, it is recommendable
to use for the referencing use_method unit the
BY_REDIRECTION policy for transporting of output
values.
Similar rules apply for the adapt_opnds() call. The
only difference is that here adaptation occurs in right-to
left order, i.e. proceeds from the rightmost, last operand
to the first operand unit.
PROG_UNIT AS A VIRTUAL OPERATOR UNIT:
The exec_container_opnds() call behaves
analogously as an exec_opnds() call, but executes as
its operands the successors of the container that embeds
the prog_unit (i.e., the prog_unit becomes a
virtual operator). The exec_container_opnds() call has the
restriction that it may not occur inside a public function.
NEO note: If there are several prog_units
with exec_container_opnds() calls inside the same container,
Neo will automatically restrict all exec_container_opnds() calls
to use the same (interactively set) number of operands.
CALLING SELECTED NST UNITS:
Additionally, there exist the older exec_opnd(i),
adapt_opnd(i), init_opnd(i), reset_opnd(i)
and ctrl_opnd(mode,i) functions. These allow to execute, adapt
init or reset a fixed (the i -th) successor unit
of the prog_unit. Instead of the the numeric position
i the argument can also be a string value that specifies
the referenced unit by its name.
The ctrl_opnd function has an additional mode argument.
This is not a normal argument; it must be a token that
is either a const int value or one of the NST ctrl mode
identifiers (upper or lowercase, with or without the
NST_ prefix).
While exec_opnds() and adapt_opnds() allows only
to execute a consecutive (but interactively adjustable)
number of operands, the other five functions allow to
select arbitrary, but fixed (no interactive adjustments
are possible) operand positions. Also, in this case
there is no visual indication of the selected units
in Neo.
A value of i=0 results in a recursive call of the prog-unit
itself (note, however, that all global variables are implicitly
taken as static, i.e., no copies for each new calling level
are made). Negative values of i result in calls to predecessor
units. If they occur within a public function, counting
of successors is with respect to the position of the
calling use_method unit instance.
Note:
This unit allows also to write into its float input pins
(not into its string input pins) and thus
gives a means to change the outputs of units connected to
these inputs.
Example:
Assume a unit u with iDimInp=.iDimOut and
pcFmt="io--" has been created
and for pcProg, the following code fragment
printf("Iteration %3.f:\n",count++);
for (j=0; j<NX; j++) {
Y[j] += X[j];
if (Y[j)>100) Y[j]=100;
printf("Y(%3.f) = %7.2f\n",j,Y[j]);
}
return;
INIT:
count=0;
RESET:
for (j=0; j<NX; Y[j++]=0);
was specified. Then u will be a unit that at
each exec_unit call
increments its outputs X_out[] by the corresponding current
inputs X_in[], however, imposing an upper limit of 100 on
the result. It will also print some information.
The output values will be reset to zero
whenever a call ctrl_unit(m,u) with m=NST_RESET
or m=NST_INIT is made, in the latter case, the global
iteration count count (note that variable values are preserved
between calls!) will also be set to zero.
DEBUGGING:
If the beginning of the program contains the token #debug,
any run-time error messages will show the portion
of the source code where the error occurred (setting the
#debug option causes no runtime penalty; only some extra
memory to keep the source code text will be allocated).
Additionally, the following four function calls are
provided (these can also be obtained from #importing
the predefined import library debug.so, which is actually
what the #debug directive does).
- print_lines(int iYes)
- turn on/off printing of
executed programming lines
- print_instructions(int iYes)
- turn on/off
printing of executed instructions
- print_stack(int iYes)
- turn on/off printing of
instruction stack pointer
The token #break may be placed among the source
source statements to insert a break point. When it is
encountered and the #debug option has been set,
program execution stops and a few commands
can be used to inspect variables. For a list of available
commands, enter '?', followed by return. To continue,
enter 'q'. Without a prior #debug option, the
#break directives have no effect.
PERFORMANCE ISSUES:
Execution of prog_unit code is about 10-12 times slower
than compiled C code. On a 100 Mhz Pentium, a function call
costs about 15us (each parameter adds a few us). There is
a significant optimization of for loops that are of the form
for (statement; i < expr; i++) ...
(here, i can be any variable name, and instead of '<'
there can also be '<=','>' or '>=', with i++ replaced
by i-- in the latter two cases). Therefore, time
critical loops should preferably be written in that form.
(note that a general loop can always be transformed into
the specified form by a suitable choice of expr!).
SYSTEM CALLS:
The function system(char *cmd) allows to invoke system
calls. However, as a safety measure, the prog_unit will
carry out system commands only when the following three
conditions are fulfilled: (i) the home directory contains
a permission file named .nst_permit_system_calls,
(ii) this file is write protected, and (iii) it contains
a line that specifies a pattern that matches the desired
command cmd (file globbing wildcards may be used to
specify the pattern). Example: if .nst_permit_system_calls
contains the two lines
ls
wc *
system("ls") and system("wc myfile") will be carried
out, but not system("ls mydir"), since only the first
two commands match a line in .nst_permit_system_calls.
(currently, even trailing or interspersed blanks [but
not leading blanks] are significant, thus not even
system("ls ") would be carried out [but don't rely on
this in the future]).
Lines starting with a hash symbol are considered as comments.
A .nst_permit_system_calls file with a line containing
a single "*" will allow any system command. THIS CAN
BE VERY DANGEROUS!
EXTENDING THE SET OF BUILT-IN FUNCTIONS:
The set of predefined functions can be dynamically extended
with zero or more directives of the form
#import sharedLibName
(for backward compatibility, there is also the older command
#loadlib sharedLibName.so that requires to specify the
(operating system dependent) file suffix). The token
sharedLibName may optionally be enclosed in apostrophes
(apostrophes become necessary when sharedLibName contains
characters, such as '+' or '/', that would otherwise cause
the parser to split it into multiple tokens).
Each #import directive must precede the program text
and each sharedLibName must specify
a shared object containing definitions of the to-be-added
functions (if no path is given, sharedLibName is searched in
LD_LIBRARY_PATH). In case of name conflicts when importing
from several shared libraries, a suffix can be specified
to restrict the imported definitions from each library,
e.g. (note the necessity of apostrophes in the first
#import due to the '*' in the name)
#import "string.str*"
#import string.islower
will import from shared library string.so all definitions
that start with "str" plus the
single routine named "islower" (the usual Unix file globbing
wildcard rules apply; note also that the file suffix ".so"
of the shared library is not specified when using the
#import statement). No such control is possible with
the older #loadlib directive (which, however, may be
useful to import from shared libraries that contain one
or more wildcard characters in their base name).
Directives #verbose and #silent
can be used to activate/deactivate a listing of the imported
function definitions.
A source file for adding three functions min(x,y), fun(x),
and quad_sum(x,y,z,t) (all argument and result types are restricted
to the type float) would have to contain the following definitions:
char *FUNCTIONS[] = {"min<2>","fun<1>","quad_sum<4>",(char*)0};
float min(float x, float y) { ... }
float fun(float x) { ... }
float quad_sum(float x, float y, float z, float t) { ... }
Here, the identifier *FUNCTIONS[] and the specification
of the arities in the format "<number>" are a fixed convention,
the identifiers can be freely chosen, the arities must be in
the range 0..10. The FUNCTIONS array MUST be terminated
with a (char*)0.
For a better name space separation, the symbol "FUNCTIONS" may
also be chosen as "FUNCTIONS_xxxx", where "xxxx" denotes the
base name of the specified shared library (which then must be
a legal C identifier). This is the now recommended practice
(the prog_unit will first search for the more specific
symbol FUNCTIONS_xxxx, and if not present, will try FUNCTIONS
as a fallback).
In order to pass more (and a possibly varying number) of
parameters to a function func, and to use also arrays as
parameters in order to pass back result values, and to
define package-specific constants, the following additional
specification formats can be used in the FUNCTIONS[] array:
- "ident"
- (to be distinguished from "indent<0>" for
a parameterless function) introduce
a constant ident in the name space of the
prog_unit, taking its value to be the
value of a float variable of the same name
that must be exported by the library (the prog_unit
will use the value of this variable at the
time of its (the prog_unit's) creation.
Any later changes will be ignored).
- "func(D)"
- define for the prog_unit a function func()
with a parameter list specified by the string
D. D is a usual C-style parameter list
declarations without parameter names.
The only allowed type identifiers are
byte*, char*, int, int*, float and
float* (Example: foo(int*,float,char*)).
Additionally, the last parameter may be followed
by three dots to indicate a variable number
of parameters. If there is no intervening comma
between the last type specifier and the three dots,
this indicates a variable (possibly 0) number
of parameters of the last specified type. If there is an
intervening comma, this indicates a variable number
of float or read-only char* parameters (as
explained for the '-'. old-style specifier).
The corresponding library function will be
passed two arrays:
piDim[0..iArg-1] dimension of each argument
ppfArg[0..iArg-1] adress of each argument
In the case of a variable number of parameters, these two
arguments will be preceded by a third argument of type int
specifying the total number (fixed and variable) parameters
with which the function call was made.
In the case of a function with a fixed number of zero arguments,
the expected prototype will be parameterless (Note that zero
arguments in the variadic case are still passed as a triple
(int,int*,void**) of arguments!).
Function pointer arguments:
An #import function can be passed up to 10 function pointers.
Function pointer arguments are defined with the usual C-syntax,
e.g.
FUNCTIONS[] = {
...
"myfun(int, float (*)(char*,float), void (*)(void))",
...
}
will define an #import function with three arguments. The
first argument is an int value, the next argument is a function
expecting a char* and a float argument and returning a float
value. The last argument is a parameterless function that also
returns nothing. The implementation of myfun will receive
in its void **ppvArg[] argument three pointers: one to the
int adress of the first argument, and the next two to the
two functions that were passed by the caller.
Special Note 1: function parameters with array or float arguments
(e.g., (*)(float*, float)) are passed to the implementation
not with their original signature (i.e., (float*,float)),
but with an "augmented signature" in which each float argument
is replaced by a double argument, and each float*,
int* or byte* argument is preceded by an additionally
inserted int argument (i.e., the above would be passed
as (int,float*,double), but at the prog unit level the
expected function still remains a (*)(float*,float)).
The implementation is expected to pass in the additional
int parameter the actual dimension
of the following array element (or 0, it it is NULL).
An exception are char* parameters: unlike byte*,
these are not augmented. Instead, the implementation is
expected to only pass null-terminated strings in such arguments.
The dimension is then always taken to be the string length+1.
Special Note 2: the passed function pointers can only be
used for calling the function during the lifetime of the
#import function call. For implementation reasons,
any other operations, e.g., storage for later use or comparisons,
will not produce the expected result. For instance, even when the
caller passes two identical function pointers,
the implementation may receive different adress values
(although calling the functions will perform the desired
operation).
Passed functions can only use int, float, float*,
int*, char* and byte* arguments (with char* args
assumed to be only used with null-terminated strings whose
dimension is then inferred as length+1).
The passed functions can either be #import library functions
or functions defined in the source code of the prog_unit,
as well as some fixed built-ins, such as sin or cos.
However, element functions of class instances are currently
not permitted (Hint: wrap them in a prog unit source function,
this function then can be passed).
When a function parameter is the name of an overloaded function,
the desired signature must be specified explicitly by
appending a type list, e.g.,
foo(4711,f(int,char*)); // a call, not a declaration!
foo2(f(float*)); // use of another version of f()
(assuming that f exists in the two indicated variants).
Old style function declarations:
For backward compatibility, there is also an old-style
declaration that is somewhat more cryptic, but shorter:
- "func<?>"
- define for the prog_unit a function func()
with a variable number of float parameters.
The corresponding library function func must
be declared as func(int iArg,float *pfArg),
with iArg passing the number of parameters found
in the prog_unit call, and pfArg[0..iArg-1]
providing their values.
- "func<S>"
- define for the prog_unit a function func()
with a parameter list specified by a string S
(for details, see below). The corresponding
library function will be passed two arrays.
piDim[0..iArg-1] dimension of each argument
ppfArg[0..iArg-1] adress of each argument
In the case of an <S> type specification, the number iArg
of arguments in this case is given by the
letters of S: each letter from the set '.','*' and '$'
denotes a separate argument. A final letter '+' or '-'
denotes a possibly varying number of further arguments
(in this case, iArg will be explicitly passed to the C
as an additional parameter preceding piDim[] and ppfArg[])
Argument types for an old-style declaration are:
- "."
- specifies a single float argument. ppfArg[iArgPos]
will be the adress of a copy of its value (i.e., the
original value cannot be changed), piDim[iArgPos]
will be 0.
- ";"
- specifies a single int argument. ppfArg[iArgPos]
will be the adress of a copy of its value (which will
be passed as an int; the original value cannot be changed),
piDim[iArgPos] will be 0.
- "$"
- specifies a string argument. ppfArg[iArgPos] will
point to the adress of a null-terminated char* string
which holds a copy of the original string (i.e.,
again access is readonly). piDim[iArgPos] will
hold the size of the string (including its null-byte)
and is, therefore, non-zero.
- "*"
- specifies a float array argument. ppfArg[iArgPos]
will point to the address of the original array,
i.e., elements can be read and written.
piDim[iArgPos] will hold the dimension of the array.
A parameter &Ident will be treated as an array
of one element.
- "%"
- specifies a byte array argument. ppfArg[iArgPos]
will point to the address of the original byte array,
i.e., elements can be read and written.
piDim[iArgPos] will hold the dimension of the array.
- "&"
- specifies a int array argument.
ppfArg[iArgPos] will point to the address of the
original int array, i.e., elements can be read and written.
piDim[iArgPos] will hold the dimension of the array.
- "+"
- may occur only as the last letter of S and indicates
a variable (including zero) number of additional, trailing
float array argument parameters, each with dimension and
address passed as decribed for '*'.
- "-"
- may occur only as the last letter of S and indicates
a variable (including 0) number of additional, trailing
single float or string array argument parameters
(.ie, types '$' or '.'). They can be distinguished
by the value of piDim[iArgPos] (0 in conjunction with
a non-NULL ppfArg[iArgPos] indicates that a float
was provided; all other cases indicate that an array
was provided. IMPORTANT NOTE: the called routine always gets
passed a float array, even if the caller provided
a char string).
- "="
- may occur only as the last letter
of S and indicates a variable (including 0) number of
additional, trailing int argument parameters.
- "!"
- may occur only as the last letter
of S and indicates a variable (including 0) number of
additional, trailing int array argument parameters,
each with dimension and address passed as decribed for '%'.
- ":"
- may occur only as the last letter of S and indicates
a variable (including 0) number of additional, trailing
byte array argument parameters, each with dimension and
address passed as decribed for '%'.
For all arguments that expect an array parameter, the reserved
value NULL may be given. This will be treated in the same way
as if an uninitialized pointer had been passed, i.e, the dimension
will appear as 0, and ppfArg[iArgPos] will be NULL.
Using these definitions, it is easy to extend
the prog_unit with self-written libraries. Moreover, it is often
very little overhead to include a suitable FUNCTIONS[] line
and a few wrapper functions to make part of the functionality
in existing NST shared libraries also accessible via function
calls from the prog_unit.
PASSING EXCEPTIONS FROM EXTENSION LIBRARIES:
At the implementation level of an #import function
or class (cf. section .) the function
void prog_throw(char *pcErrType, char *pcFmt, ..)
is available to throw an exception, setting the predefined
prog_unit variable errtype to pcErrType and evaluating its
remaining arguments like a printf statement to write errtext.
The call will cause an immediate return to the prog_unit
calling level to the position of the next catch block that
follows the #import function call (ascending from
any nested function calls as necessary).
DEFINING BUILT-IN EXTENSION LIBRARIES:
By calling the function nst_def_builtin_lib(name) one can
register a number names as built-in libraries. Any #import
directives for these are then resolved by looking up their
symbols directly in the main program (i.e., no attempt to
locate a separate shared library file is made). Neo will
automatically register the names of its compiled folders
in this way (therefore, "stdr" is always among the built-in
libraries).
To avoid name conflicts, the look-up symbols "FUNCTIONS"
and "CLASSES" must be replaced for a built-in library
"name" by "FUNCTIONS_name" and "CLASSES_name", resp.
This is also recommended for shared libraries (the loadlib
mechanism searches any loadlib name first for the specific
symbols FUNCTIONS_name and CLASSES_name and only then
uses the general FUNCTIONS and CLASSES symbols as a
fallback).
Type Conversions:
If a float* argument of a loadlib function receives a
char* parameter (or vice versa), there will be an automatic
creation of a temporary conversion buffer, i.e., one need
not care about element size, but a type mismatch will be
punished by the additional overhead for buffer creation
and copying (for writeable array parameters, copying will
occur twice, first when the function starts and a second
time to write any changes back when the function returns).
Avoiding Name Conflicts:
When extending the prog_unit with shared libraries, it is important
to avoid name conflicts by loaded symbols, e.g., by choosing
some reasonably safe prefix string for each identifier.
To restrict the visibility of such prefixes to the C implementation
level (and to avoid having to use it also for the call names
of the functions within code processed by the prog_unit itself)
include the prefix strings before the call names in the FUNCTION
list (in this list, any identifier that has a colon ':' at its
end (and is without a <> -part) will be
interpreted as a prefix specifier (with the colon excluded)
for the following symbols).
Name conflicts can also occur within the prog_unit code.
Currently, all predefined symbols for the prog_unit are
either all lower case or all upper case letters. This convention
will be kept in future extensions, so that name conflicts
with future extensions can be avoided by using mixed character
symbol names for private extensions. Name conflicts between
shared libraries that shall be used simultaneously can
be avoided by restricting the #import'ed functions suitably
by specifying a pattern (cf. above).
Example:
char *FUNCTIONS[] = {"__:", /* define name prefix */
"I_see",
"mul_vec<.*>", sum<?>, strcpy<*$>,
(char*)0};
/* first entry specifies additional prefix "__" for all
referenced library names that follow.
*/
float __I_see = 42;
float __mul_vec(int *piDim, float **ppfArg) {
float fArg0 = ppfArg[0][0]; /* first arg is scalar */
float *pfArg1= ppfArg[1]; /* 2nd arg is array */
int i, iDimArg1 = piDim[1]; /* its dimension */
for (i=0; i<iDimArg1; i++) pfArg1[i] *= fArg0;
return 0; /* return value not used here */
}
float __sum(int iArgs, float *pfArgs) {
float fSum=0;
while (--iArgs >= 0) fSum += pfArgs[iArgs];
return fSum;
}
float __strcpy(int *piDim, float **ppfArg) {
int i, iDim = piDim[0];
float *pfDest = ppfArg[0];
char *pcStr = (char*) ppfArg[1];
int iLen = strlen(pcStr);
if (iDim>iLen-1) iDim=iLen-1; /* ensure limits */
for (i=0; i<iDim; i++) *pfDest++ = *pcStr++;
*pfDest = 0; /* terminating null */
return i;
}
implements a constant I_see, a function mul_vec(float,float*)
a function sum(float,..) and a function strcpy(float*,float*)
for the prog_unit.
mul_vec(a,v) multiplies a vector v by a factor a, returning
the result in v (note that the dimension of v is not
part of the parameter list, but it is available for the called
library function __mul_vec in the array piDim).
sum(f1..fn) returns the sum of its (variable) number n
of arguments f1..fn.
strcpy(f1,f2) copies f2, interpreted as a string, into f1.
Since we want f1 to become changed, we must use * for this
arg, however, we can use t for the read-only string f2.
Overloading of functions:
Within the same #loadlib package, one and the same function
name may be defined with several different function argument
lists (."overloading"). By using different prefixes, each
such definition can be connected with its own implementation
function. The corresponding (single-named) prog unit function
will then appear as an overloaded function, with the actual
implementation function selected by the function name and
the chosen argument parameters. Example:
*FUNCTIONS[] = {
"_0_:", "foo(void)",
"_1_:", "foo(int)",
"_2_:", "foo(float)",
NULL };
float _0_foo(void) {
... here implementation of foo(void) ...
}
float _1_foo(int *piDim, int **ppfArg) {
... here implementation of foo(int) ...
}
float _2_foo(int *piDim, float **ppfArg) {
... here implementation of foo(float) ...
}
Initialization and Clean-up for Extension Libraries:
If a self-defined extension library defines the special
parameterless prog unit function _init() (requiring an
entry _init<> in the FUNCTIONS line), the corresponding
C-function (which then will need a prefix in order not to
collide with the _init() function of a shared library;
the type of the _init() function has to be void (*)(void))
will be called whenever the extension library is imported
with a loadlib command from a prog_unit (i.e., each new
prog unit causes a separate call). This may be used to
make package-specific initializations for the calling
prog-unit instance. An example is the loading of further
shared libraries for which the convenience function
nst_prog_loadlib(pcName)
is provided. Its string argument specifies the same
string as would
come after a loadlib directive, and the integer return
value is the nr of sucessfully loaded definitions,
or 0 in case of failure.
Likewise, if a self-defined extension library defines
the special parameterless prog unit function _fini(),
the corresponding C-function (which must again
be declared as void (*)(void)) will be called whenever
the prog unit instance is removed (NOTE: this means
that the _fini() function of a loadlib may be called
many times! Beware of repeated free's etc.!!).
An example of the use of _fini() can be found in nst_file.c.
To distinguish functions calls from different prog unit
instances, the parameterless function
nst_prog_unit_id() is provided. If called within
the scope of a package function,
it returns an integer id that uniquely identifies the
prog unit that caused the call of a package function.
Each of the functions _init() and _fini() may alternatively
also be defined with one int parameter. If this variant is
chosen, the int parameter will pass the nst_prog_unit_id()
of the prog_unit that caused the call.
Shared Variables per Loadlib and Prog_unit Instance:
There is a simple way to share a user-defined
data structure among all function calls that are
made by the same prog_unit to a particular loadlib
(NOTE: a better way to achieve the same goal may be
to implement a class instead of a function, cf.
IMPLEMENTING CLASSES IN IMPORT LIBRARIES below).
To this end, allocate the data structure (e.g., in
the _init function called once for each prog_unit
instance) and then register its address pAdr and
the address pFree of a suitable destructor function
(.pFree=NULL will select free()) by a call of
nst_prog_def_userobj(pAdr,pFree)
This will pass ownership of the object at pAdr
to that prog_unit instance that invoked the call
into the loadlib during which the above registration
was made. A subsequent call
pAdr = nst_prog_userobj()
occurring during a loadlib invocation from a
prog_unit instance will then return the object
address registered previously by that instance
(or NULL, if no such registry was made or the
call was not invoked by prog_unit).
Repeated calls to nst_prog_def_userobj() to redefine
the current shared data object are legal and will
free the previous object instance and register the
new one. Note that a registered object belongs
to the calling prog_unit and may thus not be
explicitly freed explicitly. However, a call of
nst_prog_def_userobj(NULL,NULL) is legal way to
undefine the object and thereby to enforce its
destruction.
IMPLEMENTING CLASSES IN IMPORT LIBARIES:
By a slight extension of the scheme for defining
new functions, a #import may also define classes
that then become available as new data types in the
importing prog_unit. The following lines in a
#import library will define a class account with
three methods to deposit or withdraw an amount
and to query the current balance:
char *Accnt[]={"account<1>","deposit<1>",
"withdraw<1>","balance<>",
"FREE_account<>",NULL};
char **CLASSES[]={Accnt,..more classes..,NULL};
// the implementation of the functions:
typedef struct account_{
float fAmount; ... } account_t;
// the constructor:
account_t *account(float fInitialAmount) {
account_t *pData=calloc(1,sizeof(account_t));
pData->fAmount = fInitialAmount;
...
return pData;
}
// the methods:
float deposit(float fVal,account_t *pD){
pD->fAmount += fVal; return 0;
}
float withdraw(float fVal, account_t *pD) {..}
float balance(account_t *pD) {return pD->fAmount;}
void FREE_account(account_t *pD) {free(pD);}
All previously described ways to define more
complicated parameter lists and to define prefixes
against name collisions are available in the same
way. Also, overloaded functions can be implemented
as explained previously (even for the constructor
function). Note that only the CLASSES identifier is
fixed (note also the char**[] type), all other
identifiers can be freely chosen.
In the same way as for the "FUNCTIONS" symbol,
the symbol "CLASSES" may
also be chosen as "CLASSES_xxxx", where "xxxx" denotes the
base name of the specified shared library (which then must be
a legal C identifier). This is the now recommended practice
(the prog_unit will first search for the more specific
symbol CLASSES_xxxx, and if not present, will try CLASSES
as a fallback).
A major difference to functions definitions
is that each function (except the constructor)
gets passed an additional
handle to reference a class-specific data
object for each instance. The constructor function
is expected to return a handle to a suitably
initialized new instance of this object.
The constructor is the first function that is
defined in the function definition list of
the class, and its name defines also the name
of the class. The destructor for a constructor
xxxx is expected to be named FREE_xxxx<>.
(its parameter list is always taken as
(void*), no matter what is actually defined).
If no constructor is given, the free()
function will be used.
Additional argument type tokens:
- "@"
- indicates a parameter of an element
function that is a reference to an
instance of the class to which the
element function belongs (for each
such argument, the corresponding
C function will be passed a handle
to the data object of the passed
instance). The passed dimension
will be 1, the passing mechanism
is the same float** array as for
* or $ parameters.
- "#"
- a variable number of @ - arguments,
passing mechanism as above.
- "a".."z":
- another class argument, where
the expected class is indicated by
the letter and must be from the
same loadlib as the class of the
called element function. Letters
'a', 'b', ... 'z' denote the
first, second, ... 26-th class
definition in the CLASSES[] array
of the loadlib (the number of classes
useable in this way is limited to max.
26).
Functions can be defined with return types, e.g.
...
"float fun0(void)",
"float *fun1(void)",
"void fun2(float)",
"class fun3(int)",
Here, the second function declares an array-valued
function. Such functions must pass a corresponding
pointer to a newly allocated array whose ownership
must be given to the caller. If a function returns
an array, it must also return the allocation dimension
of the array in piDim[-1] (all other functions can
ignore piDim[-1]).
The last definition declares a member function that
is similar to a constructor. The implementation of this
function is expected to be of the form
void *fun3(int *piDim, void **ppvArg, void *pD) {..}
and should return a class handle to a newly created instance
(ownership passed to the caller).
It then can be used in assignments, such as
x = y.fun3(42);
which copies the created instance into class
variable x.
Special functions:
The following method names will be treated
in a special way (in all cases, xxxx denotes
the name and dataptr a reference to
the class in consideration):
- SAVE_xxxx<>
- indicates that class xxxx has a
data save function that must be implemented
as char *SAVE_xxxx(FILE*,dataptr) and
that should save the data object instance
passed with its last argument to the stream
passed with its first argument. A
return value of NULL should indicate
correct operation, everything else is
interpreted as an address of an error
message.
- RESTORE_xxxx<>
- indicates that class xxxx has a
data restore function that must be implemented
as char *RESTORE_xxxx(FILE *fp,dataptr *pPtr).
This function is expected to be able to read
the data record produced by SAVE_xxx and to
initialize the data object that is passed with
the last argument appropriately.
Note that the last parameter is not the pointer
pPtr to the data object but instead the adress
&pPtr of that pointer! The reason is that the
RESTORE function is expected to create an new data
instance when the address pPtr of the data object
is NULL (indicating that no instantiation has yet
occurred). In this case, the address of the newly
created instance must be returned in *pPtr.
In all other cases (i.e., pPtr != NULL), the
pointer value pPtr *must not be changed*,
however, any elements inside the data object
can be rebuilt in any way (as required by the
read data). The return value of the routine
is used to transmit an error status:
A return value of NULL should indicate
correct operation, everything else is
interpreted as a char* address of an error
message.
- LOAD_xxxx<>
- [deprecated, only for backward compatibility]
a matching data load function that
should be able to restore the state of the
class instance from a data record previousy
saved by the associated SAVE_xxxx function.
Signature is char*(FILE*,dataptr).
Unlike the RESTORE function, a LOAD function cannot
pass back a newly allocated instance (since the last
argument is a dataptr and not a dataptr*). Therefore,
classes that have a LOAD function do not permit
static declarations of uninstantiated class variables
or assignments that involve such variables, since
the resulting data files could not be read back by
the load function.
- DESCRIBE_xxxx<>
- indicates presence of a
function char *DESCRIBE_xxxx(dataptr)
that is expected to return the address to
a string holding some descriptive information
about the data instance referenced by the
passed dataptr (used, for instance, by
Neo to display pin info).
- CTRL_xxxx<>
- specifies a control call
handler for the instances of class xxxx.
The handler must be implemented with the
fixed signature void CTLR_xxxx(int mode, void *obj)
(i.e., the first parameter will pass the NST ctrl
mode, the last parameter the object handle).
- COPY_xxxx<>
- indicates presence of a function
dataptr COPY_xxxx(dataptr,dataptr) which
will copy a class instance referenced by the
second argument, optionally utilizing the
instance referenced by the first argument
(ownership of this instance is passed to the
COPY routine). The following requirements must be
met by the implementation: when the first
argument is is NULL, the return value must
be the address of a newly allocated copy;
when the second argument is NULL, the reaction
must be as if a reference to a default instance
had been passed as the second argument
(for more details, see the nst_typedef()
manpage). When a COPY_xxxx function has been
defined, two important things happen:
(i), instances of the class can be defined
as INP or OUT variables, giving rise to NST
pins of the newly defined class and allowing
transport of (access to) the corresponding
objects via NST wires. (ii) instances of the
class can be assigned to each other, e.g.,
a=b; (will invoke COPY_xxxx(a,b)).
For more comments on the implementation of
COPY_xxxx and user-defined NST datatypes
see the nst_typedef MANPAGE.
- COMPATIBLE_xxxx<>
- sometimes, even instances
from the same class may differ sufficiently
that one might want to forbid assignments
such as a=b or connections of respective
pins by wires. Such restrictions can be built
by specifying a int COMPATIBLE_xxxx(dataptr,
dataptr) function that compares two instances
and returns nonzero, when they are compatible,
and 0 otherwise. This will restrict wires
and assignments in a corresponding way.
- UPDATE_xxxx<>
- this entry must be present when
the class has array variables that shall be
dynamically resizeable from a loadlib routine.
In this case, the implementation has to
define a function pointer void (*UPDATE_xxxx)(void).
The corresponding function call UPDATE_xxxx()
has then to be issued by any loadlib function that
has resized the memory of one or several of the
class array variables (the function ptr will
have been set by NST to a notifier
function whose call triggers a re-registration
of the changed class variable addresses and
dimensions).
These functions may also not be overloaded.
When implementations for the SAVE/RESTORE_xxxx functions
are provided, the prog_unit will automatically save
and restore instances of a newly defined class
(suppression is possible with a =" assignment).
Element variables:
Parameterless entries in the function definition
list of a class are interpreted as identifiers for
element variables. For each such identifier
(e.g., val) the implementation is expected
to provide a function void *val(int*,dataptr)
that returns an address location where the value
of the variable can be stored. Usually, this
will be some location inside the data object
of the class instance (to which dataptr
passes a reference). Variables can either be
of type float (the default) or of type int.
The type can (for int must) be explicitly
specified by prefixing an argument type char:
- ";"
- specifies an int
- "."
- specifies a float (optional).
The int* parameter allows to return
a dimension. If
the return value is a positive number, the
variable will appear at the prog unit level
as an array variable of corresponding
dimension (a few unreasonably large values
may be reserved for special purposes).
The type of an array variable can be specified
by prepending to its name one of the following
characters:
- "*"
- specifies an array of floats.
- "&"
- specifies an array of int.
- "%"
- specifies an array of bytes.
- "$"
- specifies a null terminated string.
In this case, the implementation is
expected to always hold a null-terminated
string at the returned address, unless
the returned addres was NULL. The
variable will then appear like a dynamic
char pointer variable of corresponding,
variable length.
If you want a scalar, simply leave the int
at the passed int* address unchanged (no
matter what its value!).
The dataptr again passes the handle of the
associated data object (usually, the variable
will occupy some location(s) in that object).
Each associated function is called once when
the class instance has been created.
Element variables implemented in this way can
then be used also as left values in assignments,
e.g., if we extend the above definitions as
char *Accnt[]={...,"*vec","val",...}
...
typedef struct account_{
float fAmount;
float fVec[10];
float fVal;
} account_t;
float *vec(int *piDim, account_t *pD){
*piDim=10; return &(pD->fVec); }
float *val(int *piDim, account_t *pD){
return &(pD->fVal); }
...
we can assign and retrieve values in
the prog unit by statements like
static class account a(5);
...
a.vec[3] = 3.56;
a.val = 4 + a.vec[0];
...
If the constructor of a class has no parameters,
the class instances may be instantiated without
any parameter list, e.g.
static class foo x,y,z;
...
Note that classes have no _init and
_fini function. If a package needs this,
you can use a FUNCTIONS[] lines to
introduce a pair of such functions.
C-Style Interface Specification:
Instead of the hard-to-memorize single character
letter codes, one can also provide the interface
specification in a more explicit, C-style fashion,
using the bsic data type keywords int, float,
byte, char and class (the latter optionally
followed by a class name; otherwise,
the class of the calling function is implied).
E.g., instead of
":thisarray",
"thatfun<.&c!>",
"SAVE_fxy<>",
one may write
"byte* thisarray",
"thatfun(float,int*,class foo,int* ...)",
"SAVE_fxy()"
or also
"thatfun(float x, int *pi,class foo c,int* ..)",
Similar to C++, it is also possible to assign default
values to a number of trailing arguments (that then may
be omitted when calling the function). Currently, such
default assignments are limited to int and float
parameters; additionally the single default value
NULL or 0 is possible for float*, int*, and char*
parameters. Example declaration:
"foo(float x, int n=5, char *s=NULL, int *p=0)"
With C-Style interface definitions
it is also possible to define class element functions
that have as their arguments classes that are defined
in other #import packages.
Variable arguments may only be
indicated in the last position and the following
correspondences hold:
"float" --> "."
"float*" --> "*"
"int" --> ";"
"int*" --> "&"
"byte*" --> "%"
"char*" --> "$"
"class" --> "@"
"..." --> "-"
"int ..." --> "="
"float* ..." --> "+"
"int* ..." --> "!"
"byte* ..." --> ":"
"class ..." --> "#"
Note the absence of a comma in the vararg
types: the sequence "(float*, ..)" and
"(float* ..)" correspond to the different
interfaces "<*->" and "<+>"!
Note also that the interfaces requested by
<number> and <?> cannot be obtained in
this way. They only can be specified in the
old notation. Single character and C-style
notation may also be mixed within the same
FUNCTION or CLASS definition array
(although not within the same definition
string!).
Redimensioning of Array Element Variables:
If an element array such as vec shall be
redimensioned, we must notify the prog unit
about that event. To this end, the implementation
has to provide a function pointer
void (*UPDATE_xxxx)(void), that is to be
called whenever one of its routines has resized an array
variable such as vec[]. In the present example,
we might now use a different data struct
typedef struct account_{
float fAmount;
float *pfVec; // now dynamic!
float fVal;
} account_t;
initialize pfVec in the constructor
and provide a resize method for subsequent
changes. This could be done by extending
*Accnt[] with the two further entries
..,"resize<1>", "UPDATE_account<>",...
implemented as
void (*UPDATE_account)(void) = NULL;
float resize(float iNewDim, account_t *pD) {
pD->pfVec = realloc(pD->pfVec,
((int)iNewDim)*sizeof(float));
UPDATE_account();
return 0;
}
...
Note that the initially NULL UPDATE_account
function pointer has become set (by the prog
unit) to a suitable notifier function when the
loadlib was loaded.
Exception: when the RESTORE or LOAD method of a class needs
to resize any array element variables, it need not call the UPDATE
function. In this case, any updates are made automatically,
when the RESTORE method returns.
PREDEFINED EXTENSION LIBRARIES:
The currently available prog_unit
extension libraries include the following:
Loadable library on string operations:
- string.so
- the NST string library wraps for
the prog_unit a number of string functions, mostly
analogous to the familiar C functions:
Available routines include
strcmp(s1,s2), strncmp(s1,s2,n),
strcpy(to,from), strncpy(to,from), strcat(s1,s2),
strstr(s1,s2), strchr(s1,c), strrchr(s2,c),
strpbrk(s1,s2), memove(s1,s2,n), memset(s1,f,n),
tolower(c), toupper(c), isdigit(c), isalpha(c),
islower(c), isupper(c), atof(s), atoi(s),
strtol(s,&offset,base), strtof(s,&offset),
strtoken(tok,s,sepset) [cf. below]
with the main difference that the counterparts of
functions that in C return a pointer here instead
return the corresponding index offset to the first
char position of the first argument parameter,
or -1, if the C result would be NULL, e.g.
strstr("abcde","cd") = 2
strstr("abcde","CD") = -1
The function memove is analogous to memmove,
but the moved entities are always array elements,
i.e., floats.
To parse a string s with strtoken() into tokens
tok separated by chars from d:
strtoken(tok,s,d); ... use tok ...
while (strtoken(tok,"",d)) { ... use tok ...}
Loadable library on low-level file access:
- file.so
- provides some routines for low-level file
access:
fopen(fileid,m), fclose(fp), feof(fp), stdin,
stdout, stderr, EOF, ftell(fp), rewind(fp),
fseek(fp,i,w), SEEK_SET, SEEK_CUR, SEEK_END,
fflush(fp), fgetc(fp), ungetc(c,fp), fputc(c,fp),
fgets(s,n,fp), fputs(s,fp), skip_comments(fp,c),
fprintf(fp,fmt,..), fscanf(fp,fmt,..),
fprintf_vec(fp, fmt, pfX, split)
These routines/constants are essentially wrappers of
their C counterparts (except fprintf_vec) and analogously defined.
FILE pointer arguments or
return values in the prog_unit are just integer-valued
floats, the value 0 corresponds to the file pointer
NULL. When a unit is deleted, its opened files
are closed. The implemented
fscanf interpretes a non-standard directive %#
to skip over a comment block.
fprintf_vec() prints vector pfX with format ftm (e.g. "%g" or "%5.2f").
Elements are separated by one space or after each split items by a
newline (if split>0). If split != 0 the last line is terminated
by a newline, else no newline termination occurs.
Returns the number of printed elements, EOF=-1 on error.
fprintf_vec(stdout,"%g", Y,-1); // print Y vector elements ending w. "\n"
fprintf_vec(stdout,"%7.3f",Y, 4); // print Y vector elements in block of 4
Loadable library on Open-GL solid routines:
- solid.so
- the NST solid-library now contains a number
of wrappers and a FUNCTIONS[]-line to provide a direct
interface for the prog_unit to some GL-functions, as well
as to some NST-rendering routines built on top of GL.
The routines have an effect only when the prog_unit is
in the scope of a view3d unit, otherwise they return with
no further action. Available routines and constants include
glRotatef(a,x,y,z), glTranslatef(x,y,z), glScalef(x,y,z),
glMultMatrixf(M[16]), glPushMatrix(), glPopMatrix(),
glColor3f(r,g,b), glPolygonMode(w,m),
GL_POINTS, GL_LINES, GL_FILL, GL_FRONT, GL_BACK,
nstBox(dx,dy,dz), nstPyramid(dx,dy,dz), nstTetrahedron(h),
nstCircleSegments(n), nstCylinder(r,h), nstSphere(r),
nstArrowZ(len,r)
nstMaterial(m,r,g,b,ambient,diffuse,specular,emission),
nstLighting(on), nstPuma(th[6]),
nstDHTrafo(distance,twist,offset,theta),
nstPoints(V[3n]),nstLineStrip(V[3n]), nstLineLoop(V[3n]),
nstPolygon2(V[2n]),nstLineStrip2(V[2n]),nstLineLoop2(V[2n])
and mostly wrap their similarly named GL-counterparts or
follow similarly named NST-units in nst_solid.c (arguments
V[n] denote n-dimensional arrays).
Loadable library on vector operation utilities:
- vector.so
- Contains:
Routines for sorting indexing and handling sorted vector elements;
Routines for creating random sequences without doubles or drop outs;
Routines for histogramming;
Routines for setting a vector;
Routines for complex vectors;
Routines for statistic analysis;
Unitary vector routines;
Routines for shifting vectors and time series representations;
Miscellaneous
More info see extra manpage vector.so
qsort(afRes,afX) -- quicksort vector afX[].
index(afIndex, afX) -- compute index array from data vector.
percentindex(pfSorted, percent) -- returns interpolated value into sorted array
findindex(pfIndex, pfX, pfBook, highIdx) -- find and return closest indeces
remap(pfRes, pfSorted, drop0, dropEnd) -- remap sorted array by interpolation.
checkorder(pfX) -- check ordering of vector, return bit mask
permuteindex(afIndex) -- compute random index sequence afIndex[]
nextindex(afIndex) -- return next index in array and mark used
histo(pfCount, pfMidpoints, pfX) -- compute histogram of pfX[] elements.
binning(pfCount, pfMidpoints, pfX) -- bin and count vector elements pfX[]
histoline(pfLine, pfY, pfMidpoints) -- compute polyline for histogram.
zero(pfX) -- set vector to zero.
linspace(pfX, x1, x2) -- return vector pfX[n] of n equally spaced points
logspace(pfX, x1, x2) -- return vector of n logarithmically
rand(pfX,l,h) -- return random vector with uniform distribution between [l,h[.
randn(pfX,std) -- return random vector with normal distribution, width std
gauss(std) -- return single random variable with gauss distribution,
quantile(pfRes, pfX) -- compute required quantile levels.
quantile_i(pfRes, pfX) -- compute required quantile levels by interpolation.
mean(pfX) -- return mean value of vector pfX[]
adev(pfX) -- return average deviation from mean value of vector pfX[]
var(pfX) -- return variance of vector pfX[]
std(pfX) -- return standard deviation value of vector pfX[]
moments(pfRes, pfX) -- return several moments and quantiles levels of
norm0(pfX) -- return maximum norm of vector pfX[] (=max(|x_i|)).
norm1(pfX) -- return absolute norm of vector pfX[] (=sum(|x_i|)).
norm2(pfX) -- return Euclidean norm of vector pfX[] (== norm(pfX)).
sum(pfX) -- return sum of all elements of vector pfX[].
prod(pfX) -- return product of all elements of vector pfX[].
min(pfX) -- return minimal element of vector pfX[].
max(pfX) -- return maximal element of vector pfX[].
mini(pfX) -- return index of minimal element of vector pfX[].
maxi(pfX) -- return index of maximal element of vector pfX[].
sprod(pfX1, pfX2) -- return scalar (inner) product of vectors pfX1[] and pfX2[].
mprod(pfRik, pfMij, pfMjk, j) -- matrix product pfRik[]=pfRij[] * pfRjk[].
multadd(pfY, a, pfX, b) -- return vector Y=a*X+b and its norm.
multaddv(pfY, pfA, pfX, pfB) -- return vector Y=A.*X+B and its norm.
add(pfDest, pfX1, pfX2) -- return addition vector pfDest[i] = pfX1[]+pfX2[]
diff(pfDest, pfX1, pfX2) -- return subtraction vector pfDest[] = pfX1[]-pfX2[]
scale(pfDest, fScale, pfX) -- returns scaled vector pfDest[i] = fScale * pfX[i]
clip(pfDest, pfX, lowClipValue, highClipValue) -- returns vector pfDest[] cliped
threshold(pfRes,pfX,threshold) -- threshold operation returns vector with 0 or 1s
reverse(pfRes, pfX) -- reverses order of vector elements pfX. (pfRes==pfX ok)
transpose(pfNM, pfMN, n) -- transpose matrix with initially n columns.
switch_input(pfX, pfVectors, select) -- switch one select-ed section to pfX.
switch_output(pfVectors, pfX, select) -- copy pfX to one (select-ed) section
switch(pfDest, pfSrc, select) -- switch_input() or switch_output()
convolve(pfRes, pfX, pfKernel) -- kernel convolution, only full operations
convolve_0(pfRes, pfX, pfKernel, kernelRefPos) -- convolution w. zero padding
convolve_e(pfRes, pfX, pfKernel, kernelRefPos) -- convolution w. edge extention
convolve_c(pfRes, pfX, pfKernel, kernelRefPos) -- convolution w. cyclic extention
convolve_(pfRes, pfX, pfKernel, kernelRefPos, mode) -- switched convolution.
sparse_copy(pfRes,offRes,incRes, pfX,offX,incX, maxCopy) -- sparse copy.
push(pfX, x) -- shift vector pfX[] right, return last, insert x at pos 0.
pop(pfX, x) -- return pos 0, shift vector pfX[] left, insert x at end.
acov(pfACV, pfX) -- compute data auto covariance function.
acor(pfACV, pfX) -- compute data auto corelation function.
size(pfX) -- return length of vector.
popup(string) -- write string to a popup alert window
"scopy", sparse sparse_copy()
"cabs", complex absolute
"cangle", complex angle
"csquare", complex square
"eprod", elementwise product
"elog10", elementwise log base 10
"elog", elementwise log
"chirpwave",sin wave with drifting frequency
"chirpwave_linWL", same with linear drifting wavelength
"DEG2RAD", Const
"RAD2DEG", Const
"linreg", linear least square regresion
"linmedreg",linear robust median regression
"set", set
"cmp_ge", compare greater equal
"cmp_le", compare less equal
"cmp_eq", compare equal
"tic", read elapsed time/ms + reset
"toc", read time/ms
"ranks" mid-ranks of unordered set
Loadable library on spectral methods:
- spectral.so
- Contains: extremly fast real-complex FFT, window operations, power spectra.
See spectral.so.
FILE
/homes/jontrup/nst5/man/../o.sol2//../nstsrc/nst_prog.c