D -> Cwhere D is the domain and C the codomain types. The binary combinator -> is left associative. Values of such a type are constructed by forming closures of function classes which are denoted by a function definition.
Closures bind to the instance of their environment denoted by lexical scoping together with control flow rules, and remain live at least while there is a live reference to them. The local part of a closure denoted by a function is called the stack frame for historical reasons, and the list of stack frames comprising that of the function and its ancestors is known as the display, also for historical reasons.
Closures are applicative objects which can be applied to a value of their domain type D, and which return a value of their codomain type C.
Neither the domain D nor codomain C of a function type may be void.
The special abbreviation:
{C}may be used to denote the function type:
unit -> Cand thus represents lazy evaluation of the function. For example:
{2}is the encoding of a boolean predicated on the environment.
Functions may not have side effects. See fundeffor details of defining function classes.
D --> CNote that C function closures certainly exist, however they bind only to their primitive static environment.
D -> 0 D -> voidwhere D may not be void. (void is an alias for 0 defined in the standard library).
Felix procedures values are constructed by forming the closure of a procedure class. Procedures are quite distinct from functions, since they may participate in synchronous multithreading, whereas functions may not. See syncfor details on synchronous threading. See procdeffor details of defining procedure classes.
1 unitwhere 'unit' is aliased to 1 in the standard library. The sole value of unit type is denoted:
()An n-ary tuple type, of components T0, T1, ... is denoted by:
T0 * T1 * T2 * ...and values are denoted:
v0, v1, v2, ...You should note that the chain operator * is not associative, although it is associative up to isomorphism.
Tuples support two destructors. The form
x.(0) x.(1) ...can be used with a constant expression denoting the projection. You can also use a tuple pattern in a match expression
match x with | ?v1,?v2, .. => .. endmatch
T ^ nmay be used for arrays of manifestly fixed length, where n is a literally given integer constant. Array values are the same as tuple values, since arrays are just special cases of tuples. However the special notation like:
[| 1,2,3 |]can be used for arrays, which results in a compile time error if the component values are not all the same type.
struct { a:int; b:string; c:double; }and values are constructed like:
struct { a=1; c= 9.2; b:string="Hello"; }Record types support a generic coercion operation which allows the formation of a new record with a subset of the fields of the argument record, the type of this record is a subtype. See record coercion.
Records can be accessed by the usual dot notation:
x.aor using pattern matches:
match x with | struct { a=?i; b=?s; } => ...Note that record patterns need only match a subtype of the record, that is, only some of the field names need be given.
T0 + T1 + T2 + ...and variant values are denoted by
(case 0 of T0 + T2 ...) v0 (case 1 of T0 + T2 ...) v1 (case 2 of T0 + T2 ...) v2 ...In addition, a sum of n units has the special name:
nwhere n is an non-negative integral constant. Three of these have standard names given by these typedefs:
typedef void = 0; typedef unit = 1; typedef bool = 2;These two variants have standard names:
case 0 of 2 // false case 1 of 2 // trueSums can be destroyed in pattern matches:
match x with | (case 1) ?a => ...Note that the sum type does not need to be named, since it is determined by the match argument type.
Note that sum type constructors can also be used as functions, like Haskell but unlike Ocaml. The compiler generates a wrapper function to form the closure automatically on such usage.
The standard function caseno can be used to obtain the zero origin ariant index:
var x : int = caseno (case 1 of 2); // x is 1
&TA pointer can be formed by addressing a var kind of variable, and dereferenced in the usual way:
var x : int = 1; var px : &int = &x; var y : int = *px; // y is now 1Pointers cannot dangle: it is safe to address a local variable. Pointers cannot be set to NULL in the abstract language, although they can become NULL by use of facilities in the C_hack module of the standard library. They cannot be incremented or added to.
t as vwhere v is an identifier in t. For example a list of int may be given by:
1 + int * ilist as ilistThe postfix fixpoint operator has a low precedence. This encoding is equivalent to:
typedef ilist = 1 + int * ilist;except that the former is an expression, allowing the type to be used in combinator form without defining a named alias.
struct X { a : int; b : long; };All structs are provided with constructors with the same name as the struct, and which take a tuple argument with the same components in order:
var x = X(1,2L);This construction is efficient: tuple and all struct types with the same sequence of components always have the members at the same offsets, and thus are representation compatible.
union X = | a of int | b of long ;and the C form:
union X { a : int; b : long; };These flavours are equivalent. However the ML form also supports constructors without arguments:
union X = | a of int | b of long | c ;Values of a union type are written like function applications, or constants:
var x1 : X = a 1; var x2 : X = b 1L; var x3 : X = c;Constructor names can be overloaded like ordinary functions. Unions can be decoded with pattern matches:
match x with | a ?x1 => .. | b ?x2 => .. | c => ..and the zero origin case number can be found with the caseno function:
var x : int = caseno (a ?1) ; // x is 0
enum X { a, b=7, c };can be used. In this form, an equal sign followed by an non-negative integral constant provides a way of fixing the case index. Case indicies are assigned as in C, starting at zero, and each component other than the first being assigned an index one higher than the previous one. Note duplicates and missing cases are both permitted.
class Y { val c : int; private var x : int; fun fetchc():int =>c+1; private fun fetchx():int =>x; proc setx(a:int) { x = a; } ctor (a:int): x(20000),c(1000) { x = a; } };Classes may contain, val, var, fun and proc definitions. Val's may be initialised in constructors but not otherwise modified. The special function introduced by the ctor keyword introduces a constructor, with C++ style member initialiser list.
Class members are all public by default. Var, val, proc and fun members can be declared private, in which case they can only be accessed within a method of the class.
Unlike structs, classes are always passed by reference, that is, a class value is actually a pointer.
Unlike C++, Felix supports method object closures:
var y <- new Y; val f = y.fetchc; // object closure print (f 1); endl; // value of y's c variable + 1A special new statement is used to construct classes with the form shown above. Constructors are overloaded in the usual way. As a special case one default constructor may be given and called without arguments, even though it really has a unit argument. Note that constructors exist in the scope containing a class definition and are not methods.
Methods exist within class scope and cannot be overloaded.
type myint = "int";Such definitions provide the user a way to introduce new primitive types into Felix. Such types are considered abstract. The C++ type must be allocable, default and copy constructible, copy assignable, and destructible, that is, they must be first class data types.
It is also possible to introduce non-first class types, with the adjective incomplete:
incomplete type myvoid = "void";Such types can only be used as the argument of some kind of pointer type constructor, or to instantiate a type variable in binding context for which it is suitable.
See Unknown Label:bindings for more information on bindings.
The following basic types are defined as bindings to the underlying C++ technology. They are:
Type | Role |
---|---|
Control::fthread | Thread of control |
Control::cont | Procedural Continuation |
Control::schannel[t] | Bidirectional Channel |
Control::ischannel[t] | Input Channel |
Control::oschannel[t] | Output Channel |
The following operations are used to manage these entities. ...
Felix name | Full C name |
---|---|
tiny | signed char |
utiny | unsigned char |
short | signed short int |
ushort | unsigned short int |
int | signed int |
uint | unsigned int |
long | signed long int |
ulong | unsigned long int |
vlong | signed long long |
uvlong | unsigned long long |
float | float |
double | double float |
ldouble | long double float |
The following aliases are also provided, in this case the C name may not be used:
Felix name | C name |
---|---|
size | size_t |
ptrdiff | ptrdiff_t |
intptr | intptr_t |
uintptr | uintptr_t |
intmax | intmax_t |
uintmax | uintmax_t |
The following aliases for exact integers are also provided, in this case the C name may not be used:
Felix name | C name |
---|---|
int8 | int8_t |
uint8 | uint8_t |
int16 | int8_t |
uint16 | uint16_t |
int32 | int32_t |
uint32 | uint32_t |
int64 | int64_t |
uint64 | uint64_t |
Felix guarantees all these integer aliases exist, even if the C99 counterparts do not.
Complex number are provided with one of the follwing bindings, dependent on the configuration.
Felix name | C name |
---|---|
complex | float _Complex |
dcomplex | double _Complex |
lcomplex | long double _Complex |
OR
Felix name | C++ name |
---|---|
complex | std::complex<float> |
dcomplex | std::complex<double> |
lcomplex | std::complex<long double> |
The choice between C99 and C++ complex types is implementation dependent and very unfortunate.
In all these cases, where a long version of a type is not provided by C or C++, such as long long and long double, a distinct replacement type is used instead, to ensure C++ level overloads are distinct.
[NOTE: THIS IS NOT IMPLEMENTED. The current system just uses an alias.]
Arithmetic types support the same operations as ISO C99. For the purpose of specifying bitwise operations on signed types, they use a full two's complement representation. Systems on machines with other representations must provide the appropriate glue logic.
Overflow of operations on signed types is undefined behaviour. Operations on unsigned types cannot overflow because it is defined as the modular residual of the underlying operation on integers.
Thus, operations on exact unsigned integral types are fully deterministic, and operations on signed integral types are also deterministic when it does not overflow.
Felix also guarantees 'long' is at least twice the length of 'short'.
[NOTE: THIS IS NOT IMPLEMENTED. Config check required.]
Felix name | C++ name | Notes |
---|---|---|
char | char | |
wchar | wchar_t | |
uchar | int32_t |
Felix name | Element Type | C++ name |
---|---|---|
string | char | std::basic_string<char> |
wstring | wchar | std::basic_string<wchar_t> |
ustring | uchar | std::basic_string<int32_t> |