The Flow Caml System (version 1.00): Documentation and user's manual
Vincent Simonet
July, 2003
The Flow Caml System (version 1.00): Documentation and user's manual
Vincent Simonet
July, 2003
Abstract: Flow Caml is an extension of the Objective Caml language with a type system tracing information flow. Its purpose is basically to allow to write ``real'' programs and to automatically check that they obey some confidentiality or integrity policy. In Flow Caml, standard ML types are annotated with security levels chosen in a user-definable lattice. Each annotation gives an approximation of the information that the described expression may convey. Because it has full type inference, the system verifies, without requiring source code annotations, that every information flow caused by the analyzed program is legal with regard to the security policy specified by the programmer.
flowcaml
command from the shell. Under the interactive
toplevel, the user types Flow Caml phrases, terminated by ;;
,
in response to the #
prompt. The system type-checks them on the
fly and prints the inferred type scheme.
let x = 1;;
x : 'a int |
x
to the integer constant 1
. The toplevel answers that
this constant has type 'aint
. In Flow Caml, the type
constructor int
takes one argument, which is a security
level belonging to an arbitrary lattice. These annotations allow
the system to trace information flow. In the above example, the
security level is a variable, 'a
; as every variable appearing
free in a type, it is implicitly universally quantified. Basically,
this means that outside of any context, the constant 1
may have
any security level.
let x1 : !alice int = 42;;
val x1 : !alice int
let x2 : !bob int = 53;;
val x2 : !bob int
let x3 : !charlie int = 11;;
val x3 : !charlie int |
!alice
, !bob
or !charlie
(Any
alphanumeric identifier preceded by a !
is a suitable constant
security level.) Initially, these security levels are incomparable
points in the lattice: this means that the principals they represent
cannot exchange any information. We will further on see how to allow
some (see section 2.6).
x1 + x1;;
- : !alice int
x1 + x2;;
- : [> !alice, !bob] int
x1 * x2 * x3;;
- : [> !alice, !bob, !charlie] int |
x1
, so
its security level is !alice
. The sum x1 +x2
is liable
to leak information about x1
and x2
. Then, its security
level must be greater than those of x1
and x2
:
[> !alice, !bob]
stands for any level which is greater than or
equal to !alice
and !bob
. This can be read as the
``symbolic union'' of these two principals. Similarly, the
security level of the last expression must be greater than or equal to
!alice
, !bob
and !charlie
.
let succ = function x -> x + 1;;
val succ : 'a int -> 'a int |
'a int -> 'aint
. This type means
that the function succ
takes as argument one integer of some
level 'a
and returns another integer whose security level is
exactly the same: indeed the result of this function carries
information about its input. Because its type is polymorphic w.r.t.
the security level of the integer argument, you can apply succ
on arguments of different levels:
succ x1;;
- : !alice int
succ x2;;
- : !bob int |
succ
for every level it is
used with.
let half = function x -> x lsr 1;;
val half : 'a int -> 'a int |
half
is exactly the same as that of
succ
: it reflects that the result produced by half
reveals
some information about its input. However, this leak is only partial,
because it is, for instance, not possible to completely retrieve
x1
from the result of halfx1
. In some situations, this
may yield typings which are surprising at first sight:
let return_zero = function x -> x * 0;;
val return_zero : 'a int -> 'a int |
return_zero
always returns zero! Roughly speaking, this
is because for the system, the result of a product always leaks
information about the two factors, whatever they are. Obviously, if
you rewrite the function as follows:
let return_zero' = function x -> 0;;
val return_zero' : 'a int -> 'b int |
'b
) is not related to that of the input ('a
),
reflecting the absence of information flow from the latter to the former.
return_zero x1;;
- : !alice int
return_zero' x1;;
- : 'a int |
int
, the
corresponding type constructors carry one security level:
let y0 = true;;
- : 'a bool
let z0 = 'a';;
val z0 : 'a char |
let pi = 3.14159265359;;
val pi : 'a float
let pi' = 4.0 *. atan 1.0;;
val pi' : 'a float |
string
) mutable ones (type charray
). One
may wonder why we distinguish them, since in Objective Caml every
string is mutable---even if it is not used as such---and everything
works well. This design choice is motivated because, in type systems
tracing information flow, mutable values require some particular care
which increases the complexity of types (we will discuss this point in
section 2.3) whereas, in many situations,
strings are used without in place modification. Hence, providing a
distinct type for such cases allows better typings.
let s = "Flow Caml";;
val s : 'a string |
string
and it has one argument which is a
security level. It naturally describes all information attached to
the string. The module String
provides functions for manipulating
immutable strings.'a
, 'b
, etc. we have encountered in
type schemes up to now stand for levels of the security
lattice. In Flow Caml, polymorphism applies naturally on whole types
(as in Objective Caml) too. For instance, define the identity
function:
let id x = x;;
val id : 'a -> 'a |
'a
stands for a type. Indeed, in
Flow Caml, a variable appearing in a typing can be of different kinds:
it may stand either for a level or for a type (in
section 2.4, we will see it also may
denote a row). What is more, kinds are not given explicitly
by the system (and the programmer does not have to give them when he enters
a type expression) because they always can be deduced from the
context. The type scheme inferred for id
does not involve any
security annotation: it simply says that the function takes an
argument of some type 'a
and produces a result of the same type.
For instance, if you specialize the identity function so that it
applies only to integers, it will have the type
'b int -> 'bint
.list
type constructor has two arguments
(while in Objective Caml it has only one). Thus, in the type
('a, 'b)list
, 'a
is a type variable
which gives the type of the elements of the list; 'b
is a
level variable describing the information attached to the
structure of the list. This corresponds for instance to the
information leaked by testing whether the list is empty.
let l1 = [1; 2; 3; 4];;
val l1 : ('a int, 'b) list
let l2 = [x1; x2];;
val l2 : ([> !alice; !bob] int, 'b) list |
let is_empty = function [] -> true | _ :: _ -> false ;;
val is_empty: ('a, 'b) list -> 'b bool |
'b
does not depend on the type of the list's
elements, 'a
, but is the same as the level of the input list, because
the function reveals information only about its structure of the list.
Functions manipulating lists are often recursive, but this does not
raise any particular difficulty concerning typing:
let rec length = function [] -> 0 | _ :: tl -> 1 + length tl ;;
val length: ('a, 'b) list -> 'b int |
length
is similar to that of
is_empty
: the length of the list contains some information about
its structure, but not about its elements. On the contrary, a function
testing whether the integer 0
appears in a list reveals
information about both the structure of the list and its elements,
hence its type:
let rec mem0 = function [] -> false | hd :: tl -> hd = 0 || mem0 tl ;;
val mem0: ('a int, 'a) list -> 'a bool |
List
of the standard library provides usual functions
operating on lists, including the following examples:
let rec rev_append l1 l2 = match l1 with [] -> l2 | hd :: tl -> rev_append tl (hd :: l2) ;;
val rev_append: ('a, 'b) list -> ('a, 'b) list -> ('a, 'b) list
let rev l = rev_append l [];;
val rev: ('a, 'b) list -> ('a, 'b) list |
None
(the empty option) or Somev
, where v
is
another value, the content of the option. The type
option
behaves similarly to that of lists. It has two arguments
too: in ('a, 'b)option
, 'a
is the type of the content of
the option while 'b
is the security level attached to the option
itself, describing the information attached to the knowledge of its
form. This is illustrated by the following functions:
let is_none = function None -> true | Some _ -> false ;;
val is_none: ('a, 'b) option -> 'b bool |
is_none
tests whether an option is None
, by a
simple pattern matching. Thus, the security level of the obtained
integer is exactly that of the option: the test is likely to leak
information only about the form of the argument.
let default = function None -> 0 | Some x -> x ;;
val default: ('a int, 'a) option -> 'a int |
default
matches an integer option. If it is
None
, it returns the default value 0
, and otherwise the
content of the option itself. Thus, the result produced by an
application of default
carries information about both the form of
the option and its content.x1
, ..., xn
are values whose respective types are
t1
, ..., tn
then (x1, ..., xn)
is a tuple of
type t1 * ... *tn
. For instance:
let pair0 = (0, true);;
val pair0 : 'a int * 'a bool
let triple0 = (0, 1, 'a');;
val triple0 = 'a int * 'a int * 'a char |
!alice
and the second !bob
:
let pair1 = x1, x2;;
val pair0 : !alice int * !bob int |
f1
takes one integer x
as argument and
returns a pair formed of its successor and its sum with the global
constant x1
defined above:
let f1 x = (x + 1, x + x1);;
val f1 : 'a int -> 'a int * 'b int with 'a < 'b and !alice < 'b |
'a
and 'b
. The first one, 'a
, is the security level
of the function's argument. Naturally, it is also that of the first
component of the pair returned by the function. The second integer
returned by the function is labeled by the variable 'b
. This
security level is related to 'a
by the first inequality
appearing after the keyword with
: 'a < 'b
tells us that
'b
must be greater than or equal to 'a
(note that the
character <
output by your terminal stands, in Flow Caml, for
the mathematical symbol £). In what concerns information flow,
this inequality reflects the fact that the integer labeled by 'b
depends on the one labeled 'a
; in other words that there is a
flow from the latter to the former. The other constraint,
!alice < 'b
requires 'b
to be greater than or equal to
the constant !alice
. It says that there is a possible flow from
data (namely x1
) coming from the external source symbolized by
the constant !alice
(the principal Alice) to the
second output of the function.
f1 0;;
- : 'a int * !alice int
f1 x1;;
- : !alice int * !alice int
f1 x2;;
- : !bob int * [> !alice, !bob] int |
f1
means that every instance of 'a int -> 'a int * 'bint
for some 'a
and 'b
which satisfy the inequalities
'a < 'b
and !alice < 'b
is a valid type for the function. This statement cannot be expressed
as precisely in a unification-based type system. Indeed, in such a
framework, every <
must be read as =
, i.e. the variables
'a
and 'b
must be unified with the constant !alice
.
Thus we would obtain the following judgment:
val f1 : !alice int -> !alice int * !alice int |
f1
to the integer 0
would yield a result of type
!aliceint
(instead of 'aint
), while the expression
f1x2
would be ill-typed. The same observation can be made with
the following function, f2
, which takes three integer arguments
and computes the sums of each pair of them:
let f2 x y z = (x + y, y + z, x + z) ;;
val f2 : 'a int -> 'b int -> 'c int -> 'd int * 'e int * 'f int with 'a < 'd, 'f and 'b < 'd, 'e and 'c < 'e, 'f |
'a < 'd, 'f
(which is a shorthand for
'a < 'd and 'a < 'f
) traces the information flow from the first
argument, x
to the first and third components of the result,
x +y
and x +z
respectively. The next two constraints
deal similarly with the second and third arguments of the function,
respectively. Obviously, the system performs some arbitrary choice
when it typesets a list of constraints. For instance, f2
's scheme
may equivalently be written:
val f2 : 'a int -> 'b int -> 'c int -> 'd int * 'e int * 'f int with 'a, 'b < 'd and 'b, 'c < 'e and 'a, 'c < 'f |
x1
, x2
and x3
, the constraints allow to compute the respective levels
of the resulting integer:
f2 x1 x2 x3;;
- : [> !alice, !bob] int * [> !bob, !charlie] int * [> !alice, !charlie] int |
f2
would have a much more restrictive typing
val f2 : 'a int -> 'a int -> 'a int -> 'a int * 'a int * 'a int |
<
, which is
said to be a subtyping order. In general terms, subtyping
consists of a partial order on types and a subsumption rule that
allows every expression which has a given type to be used with any
greater type, i.e. if an expression e has some type t and t is
a subtype of t' (t < t') then e also has
type t'. In Flow Caml, subtyping is structural and defined by
lifting the order between security levels throughout the structure of
types: two comparable types must have the same ``structure'' and only
their annotations may differ. For this purpose, every type
constructor (such as int
, list
or ->
) has a
signature which gives the variance (and the kind) of each of
its argument. A variance is either +
(covariant),
-
(contravariant) or =
(invariant). The
signature of a type constructor can be displayed in the toplevel
thanks to the directive #lookup_type
:
#lookup_type "int";;
type (#'a:level) int |
'a
) of int
is a level
and is covariant. (The #
symbol is a distinguished form of
+
, whose role will be explained in
section 2.2.2. For the time being, you can
simply read it as if it were +
.) This defines the subtyping
order on integer types: given two security levels 'a
and
'b
, 'a int < 'bint
holds if and only if
'a < 'b
. Similarly, the two arguments of list
are
also covariant:
#lookup_type "list";;
type (+'a:type, #'b:level) list = ... |
('a1, 'b1) list < ('a2, 'b2)list
is equivalent
to 'a1 < 'a2 and 'b1 < 'b2
. As a result, subtyping constraints
involving two type structures can be decomposed recursively: for
instance ('a1 int, 'b1) list < ('a2 int, 'b2)list
produces
'a1 int < 'a2int
and
'b1 < 'b2
and then
'a1 < 'a2 and 'b1 < 'b2
.f3
takes three arguments and build three lists of two
elements each:
let f3 x y z = ([x; y], [y; z], [x; z]) ;;
val f3 : 'a -> 'b -> 'c -> ('d, 'e) list * ('f, 'g) list * ('h, 'i) list with 'a < 'd, 'h and 'b < 'd, 'f and 'c < 'f, 'h |
f2
. Each constraint relates the type of one input to those of
the result: thus, the type of the first argument, 'a
is
``injected'' in those of the elements of the first and third lists,
reflecting the dependency. However, it is worth noting that here, the
variables 'a
, 'b
, 'c
, 'd
, 'e
and
'f
are types, not levels.->
we have encountered in the
previous examples has the following signature:
type (-'a:type) -> (+'b:type) |
'a1 -> 'b1 £ 'a2 -> 'b2
holds
if and only if 'b1 £ 'b2
and 'a2 £ 'a1
.+
. In the Flow Caml standard library, it is declared
with the following scheme:
val ( + ) : 'a int -> 'a int -> 'a int |
x1 + x2;;
- : [> !alice, !bob] int |
x1
has the type !aliceint
. By
subsumption, it can be freely used with any greater type, e.g.
[> !alice, !bob]int
. (The system is able to perform the
coercion itself when needed, no explicit annotation is therefore required.)
Similarly, x2
has type
!bobint
but it can also be used as a value of type
[> !alice, !bob]int
. It follows that the expression
x1 +x2
is well-typed and produces a value of type
[> !alice, !bob]int
. Generalizing this process, one may
naturally propose another type scheme for ( +)
, which
explicitly includes the subsumption mechanism:
val ( + ) : 'a1 int -> 'a2 int -> 'a3 int with 'a1, 'a2 < 'a3 |
'a
can be
replaced by a fresh variable 'b
with the constraint
'b < 'a
(resp. 'a < 'b
). Applying this principle to the
scheme
val ( + ) : 'a int -> 'a int -> 'a int |
val ( + ) : 'a1 int -> 'a2 int -> 'a3 int with 'a1 < 'a and 'a2 < 'a and 'a < 'a3 |
<
,
val ( + ) : 'a1 int -> 'a2 int -> 'a3 int with 'a1 < 'a3 and 'a2 < 'a3 |
if
...
then
... else
..., which has the same semantics
as that of Objective Caml, as well as polymorphic comparison
primitives. As explained above, the type of boolean values carries
one security level:
let y1 : !alice bool = false;;
val y1 : !alice bool
let y2 : !bob bool = false;;
val y2 : !bob bool |
then
and else
. Hence, the type of the
former must be a super-type of the latter. For instance, in the simple
case where a conditional produces integers, this means that the
security level of the whole expression must be the union of those of
the two branches:
if y0 then x1 else x2;;
- : [> !alice, !bob] int |
x1
has type !aliceint
, x2
has type !bobint
, so the whole expression has type
[> !alice, !bob]int
. The value produced by a conditional
also carries information about the result of the test. Hence, in
order to take in account this possible information flow, the security
level of the latter must guard the type of the former, this
means that its security level(s) must be greater than or equal to that
of the condition:
if y1 then 1 else 0;;
- : !alice int |
y1
, has level
!alice
; hence the result of the whole expression must have this
level too. Similarly, if a conditional evaluates to a tuple, the type
of each of its components must be guarded by the level attached to the
test:
if y1 then (x1, (true, 'a')) else (x2, (false, 'b'));;
- : [> !alice, !bob] int * (!alice bool * !alice char) |
int_of_bool
simply
converts a boolean into an integer:
let int_of_bool x = if x then 1 else 0 ;;
val int_of_bool : 'a bool -> 'a int |
let choose y1 y0 x = if x then y1 else y0 ;;
val choose : 'a -> 'a -> 'b bool -> 'a with 'b < level('a) |
choose
clearly depends on the value of
the boolean x
, and hence must be guarded by its security level,
'b
. However, it has the type of the two other arguments of the
function, y0
and y1
, but this may be arbitrary. That is
the reason why this example involves a new form of constraint,
'b < level('a)
. (In [PS03], this is written
'b
'a
.) Let us first
remark that in this constraint 'b
and 'a
are variables of
different kinds: 'b
stands for a security level while 'a
is a type. This constraint can be viewed as an inequality
delayed until the structure of the type 'a
is known: roughly
speaking, it means that the topmost security level(s) of the type
'a
must be greater than or equal to 'b
. For instance, if
you instantiate 'a
by 'a1int
, the constraint is simply
decomposed as 'b < 'a1
, but, if you instantiate 'a
by
'a1 int * 'a2bool
, it produces 'b < 'a1 and 'b < 'a2
. To
illustrate how this decomposition mechanism works, one can consider
some partial applications of the function:
let choose1 y = choose 1 0 y;;
val choose : 'a bool -> 'a int
let choose2 y = choose (1, 1) (0, 0) y;;
val choose : 'a bool -> 'a int * 'a int |
'a
is instantiated by
'a1int
and the constraint 'b < level('a1 int)
is
decomposed as 'b < 'a1
. This yields the scheme
'b bool -> 'a1 int with 'b < 'a1 |
'a bool -> 'aint
. In the second
example, 'a
is instantiated by 'a1 int * 'a2int
. The
constraint becomes successively
'b < level('a1 int * 'a2 int) 'b < level('a1 int) and 'b < level('a2 int) 'b < 'a1 and 'b < 'a2 |
'a bool -> 'a int * 'aint
. Similarly, we can also consider
lists:
let choose3 y = choose [] [1;2] y;;
val choose3 : 'a bool -> ('b, 'a) list |
'a
is instantiated
by ('a1, 'a2)list
, which yields the constraint
'b < level(('a1, 'a2) list)
that is decomposed into
'b < 'a2
. level
must be decomposed? This information is retrieved from signatures: the
arguments on which level
applies are those which are marked as
``guarded'' by a sharp symbol (#
):
#lookup_type "int";;
type (#'a:level) int
#lookup_type "list";;
type (+'a:type, #'b:level) list = ... |
level
applies on the single
argument of int
while it considers only the second one of
list
. It is worth noting that #
is a distinguished form
of +
, that means that guarded arguments are always covariant.=
or <=
. These operators can
be used to compare data structures of any type, so, in Objective Caml,
they have the following type
'a -> 'a -> bool |
'b
.
Moreover, because, the result produced by the operator is liable to carry
information about the two compared values, 'b
must be related to
the security levels which describe them, i.e. those that appear
within the type 'a
. For instance, specialized
versions of the equality for integers, pairs of integers, lists of
integers and lists of pairs of integers should have the following type
schemes:
val eq_int : 'a int -> 'a int -> 'b bool with 'a < 'b val eq_int_pair : ('a1 int * 'a2 int) -> ('a1 int * 'a2 int) -> 'b bool with 'a1, 'a2 < 'b val eq_int_list : ('a1 int, 'a2) list -> 'b bool with 'a1, 'a2 < 'b val eq_int_pair_list : ('a1 int * 'a2 int, 'a3) list -> 'b bool with 'a1, 'a2, 'a3 < 'b |
val eq_int_ref : ('a1 int, 'a2) ref -> ('a1 int, 'a2) ref -> 'b with 'a1, 'a2 < 'b |
'a2 < 'b
---and their contents, hence
'a1 < 'b
.'b
that labels
the boolean produced by the comparison must be greater than
or equal to every security level that appears in the type of
the arguments. This reflects how comparison applies recursively on
data-structures. Thus, in order to give a principal type to these
polymorphic operators, we need an additional form of
constraint, content('a) < 'b
where 'a
is a type and
'b
is a level. (In [PS03], this is
written 'b
'a
.) This
constraint requires every security annotation of the type 'a
to
be less than or equal to the security level 'b
. For instance,
content('a1 int * 'a2 int) < 'b
is equivalent to
'a1 < 'b and 'a2 < 'b
while content('a1 ref, 'a2) < 'b
stands for
'a1 < 'b
and
'a2 < 'b
. This definition mimics the
behavior of generic comparison operators which traverse data structures
recursively. Then, in Flow Caml, =
and <=
have the
following type:
val ( = ) : 'a -> 'a -> 'b bool when content('a) < 'b
val ( <= ) : 'a -> 'a -> 'b bool when content('a) < 'b |
mem
which
searches whether an element is amember of a list:
let rec mem x = function [] -> false | hd :: tl -> (x = hd) || mem x tl ;;
val mem : 'a -> ('a, 'b) list -> 'b bool with content('a) < 'b |
let rec insert x = function [] -> [x] | hd :: tl -> (min hd x) :: insert (max hd x) tl ;;
val insert : 'a -> ('a, 'b) list -> ('c, 'b) list with 'a < 'c and content('a) < level('c)
let rec sort = function [] -> [] | hd :: tl -> insert hd (sort tl) ;;
val sort : ('a, 'b) list -> ('c, 'b) list with 'a < 'c and content('a) < level('c) |
f1
and
f2
are two functions, (f1 = f2)
either returns true
(if the two functions have the same memory address) or raises an
exception in all other cases. Such an expression seems to have a very
limited interest and is not really used because it largely depends on
the implementation: for instance let f = fun x -> x in (f = f)
returns true
while (fun x -> x) = (fun x -> x)
raises an
exception. However, the Caml type system has no way to prevent such
calls from arising. The SML [MTHM97] dialect of ML
addresses these issues by introducing ``eq'' types, and hence
refuses at compile time any application of a comparison primitive to
values which (are likely to) contain closures. The same approach is
followed in Flow Caml, where non-eq types are marked by the
keywork noneq
in their definition, and the constraint
content('a) < 'b
cannot be satisfied if 'a
is a
non-eq type. Hence, the following piece of code yields a type
error:
(fun x -> x) = (fun x -> x);;
Magic generic primitives cannot be applied on expressions of type ~a -> ~a |
let skel x y = if x = y then (); x ;;
val skel : 'a -> 'b -> 'a with 'a ~ 'b |
skel xy
tests whether x
equals y
, then returns
x
. In the case where the test succeeds, the function skel
does nothing particular, but it should for instance be possible to
replace ()
by an expression which performs side-effects, as we
will do in section 2.3.2. However, the
current function is sufficient to illustrate the need of
same-skeleton constraints. skel
, x
and y
, will be required to be of the same type, in
order to allow comparing them: skel
's principal type scheme would
be 'a -> 'a -> 'a
. However, in Flow Caml, thanks to subtyping,
it is no longer necessary to require them to have exactly the same
type: indeed, they may have different security annotations, e.g. be
two integers of different security levels. Formally, if
x
has type 'a
and y
has type 'b
, it is
sufficient to require the existence of a super-type 'c
of
'a
and 'b
(i.e. such that 'a < 'c
and
'b < 'c
). This is what expresses the ~
constraint.
Indeed, the above type scheme is equivalent to:
val skel : 'a -> 'b -> 'a with 'a < 'c and 'b < 'c |
'c
is an extra type
variable. It is easy to check that such a 'c
exists if and only if
'a
and 'b
are two types of the same shape or
skeleton i.e. differ only by their non-invariant security
annotations. ~
predicate is transitive and
associative (it is indeed the symmetric, transitive closure of <
),
so that same-skeleton constraints which involve a common variable can
be merged, as in the following example:
let skel3 x y z = if x = y or y = z then (); x ;;
val skel3 : 'a -> 'b -> 'c -> 'a with 'a ~ 'b ~ 'c |
let pred x = x + 1;;
val pred : 'a int -> 'a int
let pred_or_succ y = if y then pred else succ;;
val pred_or_succ : 'a bool -> 'b int -{|| 'a}-> 'b int |
val pred_or_succ : 'a bool -> ('b int -{|| 'a}-> 'b int) |
pred
or succ
an application of pred_or_succ
returns
naturally leaks information about the boolean given as argument. In
order to reflect this information flow, the type assigned to the
function returned by pred_or_succ
comprises an additional
security level, 'a
, printed inside the arrow symbol: it intends
to describe how much information is attached to the knowledge of the
function. For instance, an application of pred_or_succ
with a
boolean of level !alice
yields a function whose identity has
level !alice
too:
pred_or_succ y1;;
- : 'a int -{|| !alice}-> 'a int |
(pred_or_succ y1)
to some
integer, the result must be guarded by the level !alice
, because
it allows determining whether the function was pred
or succ
and hence the boolean y1
.
(pred_or_succ y1) 0;;
- : !alice int
(pred_or_succ y1) x2;;
- : [> !alice, !bob] int |
'a -{'b | 'c | 'd}-> 'e |
'a
and 'e
are the types of the argument
expected by the function and the result it produces, respectively.
Furthermore, 'b
and 'd
are levels. The former is a
lower bound on the side effects performed by the function (it will be
introduced in section 2.3) while the latter
represents information about the function's identity, as explained
above. Lastly, 'c
is a row describing the exceptions
the function may raise (we will detail its usage in
section 2.4). However, in order to
improve readability, Flow Caml does not print annotations on arrows
that carry no information, i.e. that are universally quantified and
unconstrained type variables. For instance 'a -> 'b
is a
shorthand for 'a -{'c | 'd | 'e}-> 'b
(where 'c
, 'd
and 'e
are fresh variables), while 'a -{|| 'b}-> 'c
stands
for 'a -{'d | 'e | 'b}-> 'c
(where 'd
and 'e
are
fresh). -graph
option, or---at any time---by entering the following
directive in the toplevel:
#open_graph;; |
|
![]() |
succ
's type scheme, the dashed arrow from
the green bullet to the red one symbolizes an inequality whose left-
(resp. right-) hand-side is the security annotation symbolized by the
green (resp. red) bullet. Then, the drawing must be read as the
following scheme
val succ : 'a int -> 'b int with 'a < 'b |
'a int -> 'aint
. Let us now
show how type variables are graphically represented.
|
![]() |
skel
's
type scheme, the boxes labeled ~a
stand for a skeleton
class: each occurrence of ~a
must be read as a different type
variable a
1
, a
2
, ..., a
n
, with the
constraint a
1
~ a
2
~ ... ~ a
n
. For instance,
~a -> ~a -> ~a
represents
'a1 -> 'a2 -> 'a3 with 'a1 ~ 'a2 ~ 'a3 |
'a1 < 'a3
.
|
![]() |
|
![]() |
content('a) < 'b
, 'a < level('b)
and
level('a) < content('b)
, are drawn. All of them are
represented by a dashed arrow from (the box/bullet which stands for)
'a
to (the box/bullet which stands for) 'b
. No confusion
can arise thanks to kinding as illustrated by the following table:kind of 'a |
kind of 'b |
meaning of a dashed arrow from 'a to 'b |
|||||
level |
level |
'a |
< | 'b |
|||
level |
type |
'a |
< | level('b) |
|||
type |
level |
content('a) |
< | 'b |
|||
type |
type |
content('a) |
< | level('b) |
|||
|
![]() |
|
![]() |
choose
, the dashed arrow symbolizes the
constraint 'b < level('a)
, while in that of ( =)
it
stands for content('a) < 'b
.while
and for
loops.
r := not y r := if y then false else true if y then r := false else r := true r := true; if y then r := false |
r
, storing in it the negation of the boolean
y
. Hence, this produces some information flow from y
to
r
. However, depending on the cases, it is of a different
nature. In the two first examples, the flow is said to be
direct: a value depending from y
is computed and then
stored in r
; this is very similar to what we have encountered up
to now. On the contrary, in the last two expressions, the value in
every right-hand-side of the :=
operator does not involve
y
: it is even given explicitly in the source code. However,
the reference's update is performed in a branch of the program whose
execution is conditioned by the value of y
. In this situation,
we say there is an indirect flow form y
to r
. The
last example calls for an additional comment: in the case where the
boolean y
is false
, the reference r
is never updated
in a context conditioned by y
. However, the information flow
from the latter to the former still exists: it is indeed possible to
leak information through the absence of a certain effect.
(This last example shows that it would be very difficult to
detect information flow at run time.)ref
, has two
arguments:
#lookup_type "ref";;
type (='a:type, +'b:level) ref = ... |
ref
is a security
level, which is guarded and covariant. It describes how much
information is attached to the identity of the reference, in
other words its memory address.r1
and r2
whose contents are declared to be
booleans of levels !alice
and !bob
, respectively.
let r1 : (!alice bool, 'a) ref = ref true;;
val r1 : (!alice bool, 'a) ref
let r2 : (!bob bool, 'a) ref = ref true;;
val r2 : (!bob bool, 'a) ref |
r1
has type
!alicebool
. This means it may receive any boolean whose
security level is less than or equal to !alice
, i.e. a boolean
Alice is allowed to read. The boolean y1
(defined in
section 2.1) has level
!alice
. Hence it can legally be stored in r1
:
r1 := y1;;
- : unit |
unit
. Because there is only one value of this type, the
constant ()
, the value of a unit
expression yields no
information. At a result, the unit
type constructor does not
carry any security annotation. On the contrary, the boolean y2
has been declared with the level !bob
. Because information flow
from !bob
to !alice
is not allowed (see
section 2.6), assigning it to
r1
raises a typing error:
r1 := y2;;
This expression generates the following information flow: from !bob to !alice which is not legal. |
r1
can be updated in a context whose
execution depends on y1
but not y2
:
if y1 then r1 := false else r1 := true;;
- : unit
if y2 then r1 := false else r1 := true;;
This expression generates the following information flow: from !bob to !alice which is not legal. |
r1
naturally yields a boolean of
level !alice
:
!r1;;
- : !alice bool |
if y1 (* y1 has type !alice bool *) then ... (* this branch is typechecked at level !alice *) else if y2 (* y2 has type !bob bool *) then ... (* this branch is typechecked at level [> !alice, !bob] *) else ... (* this branch is typechecked at level [> !alice, !bob] *) |
ref
appears when a reference is used as first class value, e.g. if it is
the result of some function. For instance, let
us define a version of the function choose
specialized for
references by a type constraint:
let choose_ref y r1 r0 : (_, _) ref = if y then r1 else r0 ;;
val choose_ref : 'a bool -> ('b, 'a) ref -> ('b, 'a) ref -> ('b, 'a) ref |
y
. Such an
observation can be performed, for instance, by updating its content.r1
to false
:
let reset_r1 () = r1 := false ;;
val reset_r1 : unit -{!alice ||}-> unit |
!alice
. This is reflected by the
annotation !alice
printed ``inside'' the arrow symbol of the
above type: this security level is a lower bound on the effects
performed by the function and an upper bound on the contexts where it
can be applied. In many cases, it is a variable related to (parts of)
the type of the function's argument:
let reset r = r := false ;;
val reset : ('a bool, 'a) ref -{'a ||}-> unit |
reset
takes a reference as argument and sets its
content to false
. The type system constrains the level of the
content of the reference to be equal to or greater than (1) the level
attached to the reference's identity and (2) the level attached to the
context where the function is applied.y
. This is reflected in the inferred scheme
by the fact that all of them are annotated by the same security level,
'a
.length
, in imperative style:
let length' list = let counter = ref 0 in let rec loop = function [] -> () | _ :: tl -> incr counter; loop tl in loop list; !counter ;;
val length' : ('a, 'b) list -{'b ||}-> 'b int |
length
's type:
val length: ('a, 'b) list -> 'b int |
length'
, the result's security level must be greater than
or equal to the function's pc parameter. However, the
difference is only superficial; it can be checked that both types in
fact have the same expressive power.array
)
carries two arguments:
[|0; 1; 2|];;
- : ('a int, 'b) array |
ref
: the former is the type of the content of the cells of the
array, and the latter is a security level, related to the array
identity. A slight novelty is that this comprises information
attached to the length of the array. Indeed, the function returning
the length of an array has the following type:
Array.length;;
- : ('a, 'b) array -> 'b int |
[|'a'; 'b'; 'c'|];;
- : ('a char, 'b) array
"abc";;
- : ('a, 'b) charray |
charray
expects two
security levels as arguments. The first one describes information
attached to the characters stored in the string while the second one
is related to the identity of the string (including its length).exception
construct and signaled with the raise
operator:
exception X;;
exception X
exception Y;;
exception Y
raise X;;
- : 'a |
X
in the above example) is not a value, and hence
cannot be bound to a variable or passed as argument to a function
(while in Objective Caml, it is a legal value of type exn
).
Similarly, in Objective Caml, raise
is a regular function which
accepts an arbitrary argument (of type exn
), but, in Flow Caml,
it is a built-in construct which requires the name of the raised
exception to be statically specified. For instance, the following
Objective Caml piece of code cannot be written in Flow Caml:
let f x = raise (if x then X else Y) ;; |
let f x = if x then raise X else raise Y ;; |
try ...finally
and try ...propagate
(see section 2.4.3).X: 'a; Y: 'b; 'c
stands for the row which maps
the exception name X
to 'a
, Y
to 'b
and whose
other entries are given by 'c
. Here, 'a
and 'b
are
levels while 'c
is a row variable of domain
X,Y
: it stands for a row ranging over all exception names except
X
and Y
. The order in which fields appear is not
significant: the above row is equal to Y: 'b; X: 'a; 'c
. Row
variables can appear in constraints: the subtyping order is extended
point-wise to rows. Indeed, if 'c1
and 'c2
are two row
variables of the same co-domain, the constraint 'c1 < 'c2
means
that every entry of 'c1
must be less than or equal to the
corresponding one in 'c2
. Hence, constraints involving expanded row
terms may be decomposed: X: 'a1; Y: 'b1; 'c1 < X: 'a2; Y: 'b2; 'c2
is equivalent to 'a1 < 'a2 and 'b1 < 'b2 and 'c1 < 'c2
.
Lastly, for the sake of conciseness, when it prints a type scheme,
Flow Caml omits unconstrained universally quantified row variables:
for instance, A: 'a; Y: 'b
stands for A: 'a; Y: 'b; 'c
where 'c
is a fresh row variable.X
:
let raise_X () = raise X ;;
val raise_X : unit -{'a | X: 'a |}-> 'b |
X: 'a
, tells that the given function may raise an
exception of name X
: catching this exception leaks information
about the context where the function is called, so the security level
associated to X
is constrained to be at least that of the
context where the function is applied (which appears as usual in first
place in the arrow). In the following example,
let raise_X' y = if y then raise X ;;
val raise_X' : 'a bool -{'a | X: 'a |}-> unit |
X
gives information about both the
context where raise_X'
has been applied and the boolean
argument given to the function. Thus, the annotation associated to
the entry X
in the row of this function must be greater than or
equal to the security levels of both.
let raise_X_or_Y x y = if x then raise X; if y then raise Y ;;
val raise_X_or_y : 'a bool -> 'b bool -{'a | X: 'a; Y: 'b |}-> unit with 'a < 'b |
X
yields information
only about the first argument, x
; while handling Y
about
both.X
if it is zero and returns false
otherwise:
let test_zero x = if x = 0 then raise X; false ;;
val test_zero: 'a int -> {'a | X: 'a |}-> 'b bool |
false
. However,
this function can reveal information about its argument through its
effect. This is reflected by the security level associated to
the exception X
in its type: it must be greater than or equal to
the levels of the context where the function is applied and the integer
argument. try ...with
construct.
try test_zero x1 with X -> true ;;
- : !alice bool |
test_zerox1
is liable to raise an exception
X
with the level !alice
, which will be catched by the
handler try ... with X->
. Thus, the value produced by the
whole construct must be guarded by the level of the handled
exception, i.e. !alice
. Let us embed this piece of code in a
function:
let f5 x = try test_zero x with X -> true ;;
- : 'a int -{'a ||}-> 'a bool |
f
carries information about its argument, but also about the context
where the function is called although it does not. However, we
witness the same phenomenon as for side effects: once again this is
only a superficial difference with the typing obtained for the
function written in a direct style:
let f6 x = if x = 0 then true else false ;;
val f6 : 'a int -> 'a bool |
with
part is actually a kind of pattern-matching on
exception names (however, this is not a regular pattern matching since
exceptions are not values). In particular, one try ..with
construct can catch several exceptions names (or even all of them
using the _
pattern), as illustrated by the following example:
let f7 x y = try raise_X_or_Y x y; 0 with X -> 1 | Y -> 2 ;;
val f7 : 'a bool -> 'a bool -{'a ||}-> 'a int |
Division_by_zero
when its second argument is zero, as
reflected by its type:
val ( / ) : 'a int -> 'b int -{'c | Division_by_zero: 'c |}-> 'a int with 'b < 'c, 'a |
Division_by_zero
.
This reflects that this operator does not need to match its first
argument before raising the exception.
let f8 x y = (if x = 0 then raise X else x) + (if y = 0 then raise Y else y) ;;
val f8 : 'a int -> 'b int -{'c | X: 'd; Y: 'e |}-> 'f int with 'b < 'd, 'e, 'f and 'c < 'd, 'e and 'a < 'd, 'f |
y =0
is
considered before x =0
, so the exception X
carries
information about x
and y
while Y
only about
y
. Assuming a left-to-right evaluation order, one would obtain
the following scheme, where the roles of the variables 'a
and
'b
are exchanged:
'a int -> 'b int -{'c | X: 'd; Y: 'e |}-> 'f int with 'b < 'd, 'f and 'c < 'd, 'e and 'a < 'd, 'e, 'f |
f8
would be much less informative
about the function:
'a int -> 'a int -{'b | X: 'b; Y: 'b |}-> 'a int with 'a < 'b |
test_zero x1;;
- : 'a bool Current evaluation context has level !alice |
x1
is zero, evaluating this toplevel phrase
causes the program to terminate. Hence, if this does not happen and
execution continues, the remaining expressions receive some
information about x1
when they are evaluated. Therefore, they
must be type-checked in a context augmented with the security level of
the information carried by x1
, i.e. !alice
. This point
is expressed by the second line output by the system. Thus, all
side-effects performed afterward by the program must affect data of
levels greater than or equal to !alice
. For instance, the
reference r1
(whose content has level !alice
) can be
updated while r2
(whose content has level !bob
) cannot:
r1 := false;;
- : unit
r2 := false;;
This expression is executed in a context of level !alice but has an effect at level !bob. This yields the following information flow: from !alice to !bob which is not legal. |
test_zero
with x2
, the level of
the toplevel context is increased by !bob
:
test_zero x2;;
- : 'a bool Current evaluation context has level !bob, !alice |
r1
can no longer be updated,
because information flow from !alice
to !bob
is not
allowed (i.e. !alice
is not inferior to !bob
).
#reset_context;;
Level of evaluation context reset |
;
operator. Let us for
instance consider the following function:
let f9 x r = if x = 0 then raise X; r := false ;;
val f9 : 'a int -> ('b bool, 'b) ref -{'a | X: 'a |}-> unit with 'a < 'b |
f9
takes two arguments: an integer x
and a boolean
reference r
. If the integer is 0
then the exception
X
is raised, and the following statement is not performed.
Otherwise, execution continues and the reference r
is set to
false
. We now explain the typing inferred by the system, which
reflects the two possible observable effects of the function. First
of all, it may raise the exception X
. The security level
attached to this effect, 'a
, must be greater than or equal to
that attached to the context where the function is applied and that of
the integer argument, because the exception raising is conditioned by
a test on x
. The second effect that the function is liable to
have is the update of the reference r
. Observing it
gives information naturally about the context where f
has been
called, but also reveals whether the exception X
has been raised,
and, as a consequence, about the integer x
. That is the reason
why, the security level of the content of the reference, 'b
,
must be greater than the levels of the context where the expression is
applied and the first argument, as reflected by the constraint
'a < 'b
.f9
into the following, the inferred type scheme
is similar:
let f10 x r = try if x <> 0 then raise X; () with X -> r := false ;;
val f10 : 'a int -> ('a bool, 'a) ref -{'a ||}-> unit |
r :=false
is increased by the
security level associated to X
in the expression between
try
and with
.try ...with
, Flow Caml features
two other ways of handling expressions. Two reasons have motivated
their introduction: firstly, they partially counterbalance the loss of
expressiveness resulting of our decision to make exception names
second-class citizens; secondly, they allow a more precise typing
(w.r.t. information flow) of common idioms.with
part of a try ...with
may be terminated by the keyword propagate
. In this case, the
exception trapped by the handler is re-raised at the end of its
execution. For instance:
try e with X | Y -> e'; propagate |
e
. If it raises X
or Y
then e'
is
executed and, then, the trapped exception, X
or Y
, is
raised again. In Objective Caml, this can be implemented by binding
the exception to an identifier:
try e with X | Y as x -> e'; raise x |
e
. In particular, two different levels may be associated to
X
and Y
.try ...finally
construct of Flow Caml is a translation of
the Java's construct for the Caml language. Indeed
try e1 finally e2 |
e1
, which yields either a regular value or an
exception. In both cases, e2
is executed, and the
result produced by e1
is returned. Once again, this can be
encoded in regular Caml (without even using exception values):
try let r = e1 in e2; r with exn -> e2; raise exn |
e2
does not raise any exception). However, using the
dedicated construct try
...
finally
allows better typings
(w.r.t. information flow): this makes explicit that the expression
e2
is always executed (whether e1
raises an
exception or not). Thus, the type system is able to take this in
account and type-checks e2
in a context whose level is not
altered by those of the exceptions possibly raised by e1
,
whereas, in the proposed encoding, it is. For instance the
following piece of code is accepted by the type system:
try if y1 then raise X finally r2 := false ;; |
try if y1 then raise X; r2 := false; with _ -> r2 := false; propagate ;; |
exception Error of int;; |
ErrorAlice
which is parameterized
by an integer of level !alice
:
exception ErrorAlice of !alice int;; |
!alice
as argument:
raise (ErrorAlice 0);;
raise (ErrorAlice x1);; |
ErrorAlice
with an argument whose level is, for
instance, !bob
. Obviously, a workaround may consist in defining
another exception name:
raise (ErrorAlice x2);;
This expression generates the following information flow: from !bob to !alice which is not legal.
exception ErrorBob of !bob int ;;
raise (ErrorBob x2);; |
exception Error : 'a of 'a int;; |
let error code = raise (Error code) ;;
val error: 'a int -{'a | Error: 'a |}-> 'b |
Error
in the row
of this function combines two pieces of information: first, the
security level of the context where the exception is raised and, that
of the integer argument. Merging these two annotations into a
single one is relatively ad hoc; however, this allows keeping
concise typings, and works well with most common usage
of exceptions with arguments. It should be possible to provide a more
flexible mechanism for parameterizing types of exceptions arguments,
for instance by allowing several security levels as arguments, which
will also appear in rows. However, this would increase the complexity
of the system, as well as the verbosity of function types.Out_of_memory
and Stack_overflow
which are respectively raised by the garbage
collector when there is insufficient memory to complete the
computation and the bytecode interpreter when the evaluation stack
reaches its maximal size. Indeed, analyzing them with Flow Caml would
be of little sense, because, in absence of sophisticated memory and
stack analyzes, one must assume them to be possibly raised at almost
every point of the program. That is the reason why they are not
provided in Flow Caml library: thus, they cannot be catched by
programs and become fatal errors.
#reset_context;; |
type
declaration. First and foremost, this allows to define new
data structures using records and variants. The mechanism used to
define types in Flow Caml is similar to that of Objective Caml.
However, type declarations involve additional information, in
order to deal with the extra features of the type system related to
the security analysis.
type 'a cardinal = North | West | South | East # 'a ;;
type (#'a:level) cardinal = North | West | South | East # 'a |
cardinal
(which is one of the four symbolic constants listed in
the declaration) is described by one security level, similarly to the
built-in enumerated types, such as integers or characters. Indeed, the
type constructor cardinal
has one argument which is a security
level. In the above definition, this argument, 'a
, is declared
to be the information level related to the sum by the clause
# 'a
.#'a:level
means that 'a
is
a parameter of kind level
, is covariant and must be guarded.
let p0 = North;;
val p0 : 'a cardinal
let p1 : !alice cardinal = North;;
val p1 : !alice cardinal
let p2 = if y2 then North else South;;
val p2 : !bob cardinal |
rotate
, which takes as argument a
cardinal point and returns its successor in the clockwise order:
let rotate = function North -> East | West -> North | South -> West | East -> South ;;
val rotate: 'a cardinal -> 'a cardinal |
type ('a, 'b) option = None | Some of 'a # 'b ;;
type (+'a:type, #'b:level) option = None | Some of 'a # 'b |
('a, 'b)option
: it is either the constant None
or the
constructor Some
with some argument of type 'a
. The
fourth line of the declaration, # 'b
tells that 'b
is the
security level attached to the knowledge of the form of the option,
i.e. whether it is None
or Some
. (Let us recall that,
in the second case, information carried by Some
's argument
is reflected by the security levels appearing in the type 'a
itself.)option
: +'a:type
means
that the first argument is covariant and is a type; while
#'b:level
means that the second argument is a level, and is
covariant and guarded.list
is naturally recursive; but this
has no particular consequence and the declaration is therefore similar
to the previous one:
type ('a, 'b) list = [] | :: of 'a * ('a, 'b) list # 'b ;;
type (+'a:type, #'b:level) list = [] | (::) of 'a * ('a, 'b) list # 'b |
type ('a, 'b) tree = Leaf | Node of ('a, 'b) tree * 'a * ('a, 'b) tree # 'b ;;
type (+'a:type, #'b:level) tree = Leaf | Node of ('a, 'b) tree * 'a * ('a, 'b) tree # 'b |
let rec height = function Leaf -> 0 | Node (tl, _, tr) -> max (height tl) (height tr) ;;
val height: ('a, 'b) tree -> 'b int |
Leaf
and
Node
) but not on the values stored inside the nodes.
type int_tree = ILeaf | INode of int_tree * int * int_tree ;; |
tree
given above:
type ('a, 'b) int_tree = ILeaf | INode of ('a, 'b) int_tree * 'a int * ('a, 'b) int_tree # 'b ;;
type (+'a:level, #'b:level) int_tree = ILeaf | INode of ('a, 'b) int_tree * 'a int * ('a, 'b) int_tree # 'b ;; |
'a
and 'b
: the former describes information
attached to the integers stored in the tree while is related to the
structure of the tree. In fact, the type ('a, 'b)int_tree
is
isomorphic to ('a int, 'b)tree
. This allows distinguishing the
knowledge of the structure of a tree from that of its labels.
To illustrate this point, let us define two functions: size
which calculates the number of nodes of a tree and sum
which
calculates the sum of its labels:
let rec size = function ILeaf -> 0 | INode (tl, x, tr) -> size tl + 1 + size tr ;;
val size : ('a, 'b) int_tree -> 'b int
let rec sum = function ILeaf -> 0 | INode (tl, x, tr) -> sum tl + x + sum tr ;;
val sum : ('a, 'a) int_tree -> 'a int |
sum
, the security level of the returned integer must be greater
than or equal to the two ones of the tree.
type 'a int_tree1 = ILeaf1 | INode1 of 'a int_tree1 * 'a int * 'a int_tree1 # 'a ;;
type (#'a:level) int_tree1 = ILeaf1 | INode1 of 'a int_tree1 * 'a int * 'a int_tree1 # 'a ;; |
let rec size1 = function ILeaf1 -> 0 | INode1 (tl, x, tr) -> size1 tl + 1 + size1 tr ;;
val size1 : 'a int_tree1 -> 'a int
let rec sum1 = function ILeaf1 -> 0 | INode1 (tl, x, tr) -> sum1 tl + x + sum1 tr ;;
val sum1 : 'a int_tree1 -> 'a int |
size1
gives a less precise description of
the behavior of the function w.r.t. information flow than that of
size
: it does not reflect that the size of a tree does not
depend on the value of its labels, as reflected by these computations:
size (INode (ILeaf, x1, ILeaf));;
- : 'a int
size1 (INode1 (ILeaf1, x1, ILeaf1));;
- : !alice int |
type 'a vector = { x: 'a int; y: 'a int } ;;
type (#'a:level) vector = { x: 'a int; y: 'a int } |
vector
has one argument which is the common level of the two
integers it is made of. This argument is covariant and guarded. As
for tuples, there is no particular security level attached to the
record structure itself, since it is not really observable in the
language.
let v = { x = x1; y = x2 };;
val v : [> !alice, !bob] vector |
let add_vector v1 v2 = { x = v1.x + v2.x; y = v1.y + v2.y } ;;
val add_vector: 'a vector -> 'a vector -> 'a vector
let rot_vector v = { x = - v.y; y = v.x } ;;
val rot_vector: 'a vector -> 'a vector |
vector
with security annotations is
somehow arbitrary. Indeed, it is also possible to distinguish the
information carried by each of its components and hence have two
security levels:
type ('a, 'b) vector2 = { x2: 'a int; y2: 'b int } ;;
type (#'a:level, #'b:level) vector = { x2: 'a int; y2: 'b int } |
let add_vector2 v1 v2 = { x2 = v1.x2 + v2.x2; y2 = v1.y2 + v2.y2 } ;;
val add_vector2: ('a, 'b) vector -> ('a, 'b) vector -> ('a, 'b) vector |
let rot_vector2 v = { x2 = - v.y2; y2 = v.x2 } ;;
val rot_vector2: ('a, 'b) vector2 -> ('b, 'a) vector2 |
rot_vector2
clearly shows that the function performs some permutation of the two
components of the vector.mutable
keyword:
type ('a, 'b) mvector = { mutable mx: 'a int; mutable my: 'a int } # 'b ;;
type (='a:level, #'b:level) mvector = { mutable mx : 'a int; mutable my : 'a int; } # 'b |
'a
is
invariant, as reflected in the signature by the =
.
Secondly, a record involving some mutable field is no longer a simple
tuple: the information it carries is not entirely contained by its
components because its identity (i.e. its address in memory) can be
observed in the language. Hence, its type must carry an additional
security level which tells how much information is attached to the knowledge
of its identity. In our example, this role is played by the argument
'b
, which is specified by the clause # 'b
at the end of
the definition. To illustrate the use of such a datatype, let us
define the function rot_mvector
which rotates in place a vector:
let rot_mvector v = let x = v.mx in v.mx <- v.my; v.my <- x ;;
val rot_mvector : ('a, 'a) mvector -{'a ||}-> unit |
type ('a, 'b) ref = { mutable contents: 'a } # 'b ;;
type (='a:type, #'b:level) ref = { mutable contents: 'a } # 'b |
ref
, :=
and
!
on references are regular functions which can be implemented
from the record representation of references:
let ref x = { contents = x } ;;
val ref : 'a -> ('a, _) ref |
let (:=) r x = r.contents <- x ;;
val ( := ) : ('a, 'b) ref -> 'a -{'b ||}-> unit with 'b < level('a) |
let ( ! ) r = r.contents ;;
val ( ! ) : ('a, 'b) ref -> 'c with 'b < level('c) and 'a < 'c |
!alice
,
!bob
and !charlie
, respectively. However, they remained
relatively abstract, because we just declared a series of values to have
these levels---thanks to some type constraint---but we did not
say how a program can really interact with them.!stdin
and !stdout
, respectively. A
program can interact with them using the usual functions of the
standard library. For instance, print_int
outputs an integer on
the standard output:
print_int;;
- : !stdout int -{!stdout ||}-> unit |
!stdout
. To print the integer 1
, one writes:
print_int 1;;
- : unit |
1
has type 'aint
for
every 'a
; hence one can instantiate 'a < !stdout
and the call
to the function is possible. However, printing the integer x1
(which comes from the principal Alice and hence has the security level
!alice
) is not, in the default security policy, legal:
print_int x1;;
This expression generates the following information flow: from !alice to !stdout which is not legal. |
!alice < !stdout
. This is not satisfied in the default security
policy which is the empty one: it never allows any
communication from one principal to another. It can be
refined using declarations introduced by the keyword flow
:
flow !alice < !stdout;; |
!alice
less than
or equal to !stdout
. In other words, this allows information
flow from the principal represented by !alice
(Alice) to that of
!stdout
(the standard output). These declarations are naturally
``transitive''. For instance, if one declares:
flow !bob < !alice;; |
print_int x2;;
- : unit |
!stdin
intends to represent the
standard input in the type system. For instance, the function
read_line
has the following type:
read_line;;
- : unit -{[< !stdout, !stdin] | End_of_file: !stdin |}-> !stdin string |
read_line
``flushes standard output, then reads characters from standard
input until a newline character is encountered [and] returns the
string of all characters read, without the newline character at the
end''. Thus, invoking read_line
affects both the standard
input and output, which explains the first annotation in the arrow of
its type. Furthermore, if the user sent the ``end-of-file'' sequence
(e.g. by typing ^D
), the function raises the exception
End_of_file
, hence the second annotation on the arrow. Lastly,
a string obtained by reading on the standard input must have the level
!stdin
:
let s1 = read_line ();;
val s1 : !stdin string Current evaluation context has level !stdin |
flow !stdin < !stdout;; |
echo
which ``pipes'' the standard input to the standard output:
let echo () = try while true do let s = read_line () in print_string s done with End_of_file -> ();;
val echo : unit -{[< !stdout, !stdin] ||}-> unit |
struct ...end
construct, and is usually given a name with the
module
binding. For instance, one may define a structure
implementing sets of integers (with binary trees):
module IntSet = struct type 'a t = Empty | Node of 'a t * 'a int * 'a t # 'a let empty = Empty let rec add x = function Empty -> Node (Empty, x, Empty) | Node (l, y, r) -> if x < y then Node (add x l, y, r) else Node (l, y, add x r) let rec mem x = function Empty -> false | Node (l, y, r) -> (x = y) || mem x (if x < y then l else r) end;;
module IntSet : sig type (#'a:level) t = Empty | Node of 'a t * 'a int * 'a t # 'a val empty : 'a t val add : 'a int -> 'a t -> 'a t val mem : 'a int -> 'a t -> 'a bool end |
t
), and three values: the empty set,
empty
, and two functions operating on sets, add
(to add an
integer to a set) and mem
(to test whether an integer belongs to a
set). The system outputs the signature of the structure, which
is a list of its components with their declaration. Outside the
structure, its components can be referred to using the ``dot
notation'', that is, identifiers qualified by a structure name. For
instance, IntSet.add
refers to the function add
of this
structure.
IntSet.add x1 (IntSet.add x2 IntSet.empty);;
- : [> !alice, !bob] IntSet.t |
module type INTSET = sig type (#'a:level) t val empty: 'a t val add: 'a int -> 'a t -> 'a t val mem: 'a int -> 'a t -> 'a bool end;;
module AbstractIntSet = (IntSet : INTSET);;
module AbstractIntSet : INTSET |
compare
defining a total order between them:
module type ORDERED_TYPE = sig type (#'a:level) t val compare : 'a t -> 'a t -> 'a int end;; |
compare xy
is expected to return 0
if
x
is equal to y
, a negative integer if x
is less
than y
and a positive integer otherwise.) In this signature,
the type of the elements, t
, is parameterized by one security
level which describes all the information leaked by a comparison
(as reflected by the type of compare
). However, this does not
prevent to instantiate it with more complex data types, which are
originally parameterized by several security levels:
module IntList : ORDERED_TYPE = struct type 'a t = ('a int, 'a) list let rec compare l1 l2 = match l1, l2 with [], [] -> 0 | [], _ :: _ -> -1 | _ :: _, [] -> 1 | hd1 :: tl1, hd2 :: tl2 -> let c = Pervasives.compare hd1 hd2 in if c = 0 then compare tl1 tl2 else c end;;
module IntList : sig type (#'a:level) t val compare : 'a t -> 'a t -> 'a int end |
Elt
as argument which must have the
signature ORDERED_TYPE
:
module Set (Elt: ORDERED_TYPE) = struct type 'a element = 'a Elt.t type 'a t = Empty | Node of 'a t * 'a element * 'a t # 'a let empty = Empty let rec add x = function Empty -> Node (Empty, x, Empty) | Node (l, y, r) -> if Elt.compare x y < 0 then Node (add x l, y, r) else Node (l, y, add x r) let rec mem x = function Empty -> false | Node (l, y, r) -> let c = Elt.compare x y in (c = 0) || mem x (if c < 0 then l else r) end;;
module Set : functor (Elt : ORDERED_TYPE) -> sig type (#'a:level) element = 'a Elt.t type (#'a:level) t = Empty | Node of 'a t * 'a Elt.t * 'a t # 'a val empty : 'a t val add : 'a Elt.t -> 'a t -> 'a t val mem : 'a Elt.t -> 'a t -> 'a bool end |
IntSet
example, it would be good style to hide the
actual implementation of the type of sets. This can be achieved by
restricting Set
by a suitable functor signature. Firstly, let
us define the type of a module implementing a set structure:
module type SET = sig type (#'a:level) element type (#'a:level) t val empty: 'a t val add: 'a element -> 'a t -> 'a t val mem: 'a element -> 'a t -> 'a bool end;; |
Set
functor takes
a structure of signature ORDERED_TYPE
and returns one of
signature SET
, so it may be declared with the following type:
module Set (Elt: ORDERED_TYPE) : (SET with type 'a element = 'a Elt.t) = struct ... end |
with type 'a element = 'a Elt.t
has the same purpose as in Objective Caml: it points out the fact that
the sets contain elements of type Elt.t
, i.e. that the
functions add
and mem
can be applied with arguments of
this type. To conclude with this example, one can retrieve our first
implementation of integer sets, the module IntSet
, as an
instance of the functor Set
:
module IntSet' = Set (struct type 'a t = 'a int let compare = Pervasives.compare end);;
module IntSet' : sig type 'a element = 'a int type 'a t val empty : 'a t val add : 'a element -> 'a t -> 'a t val mem : 'a element -> 'a t -> 'a bool end |
module type IN = sig level Data level Prompt val read : unit -{[< Prompt] ||}-> Data string end;; |
Data
is the
security level of data read on the input channel; and Prompt
represents the information leaked on the channel when one starts
listening on it. At the time being, nothing is known about these
levels, so they remain ``abstract''. The function read
is
intended to read one line on the underlying channel. It naturally produces a
string whose level is Data
(let us remark that, in this model,
reading can never fail). An implementation of this signature using
the standard input would be as follows:
module Stdin = struct level Data = !stdin level Prompt less than !stdin, !stdout let read () = try read_line () with End_of_file -> `` end;;
module Stdin : sig level Data = !stdin level Prompt less than !stdout, !stdin val read : unit -{[< !stdout, !stdin] ||}-> !stdin string end |
!stdin
,
so Data
is declared to equal to it in Stdin
. Invoking
read_line
affects the standard input and the standard output
(because it is flushed), so Prompt
must be less than or equal to
!stdin
and !stdout
. Then, the module Stdin
implements the signature IN
, which may be immediately verified by
a type constraint:
module AbstractStdin = (Stdin : IN);;
module AbstractStdin : IN |
module type OUT = sig level Data val print : Data string -{Data ||}-> unit end;; |
Data
which represents the information which may be sent on the channel.
(We do not consider the possibility of receiving information
from an output channel, for instance because of a buffer overflow.) The
module Stdout
implements this signature for the standard output:
module Stdout = struct level Data = !stdout let print = print_endline end;;
module Stdout : sig level Data = !stdout val print : !stdout string -{!stdout ||}-> unit end;; |
copy
whose purpose is simply to read one line on the input
channel and print it on the output channel. However, it is not enough
to require the two structures parameterizing the functor to have the
respective signature INPUT
and OUTPUT
: indeed, the
function copy
implemented by the functor generates an
information flow from the channel represented by the former to that of
the latter. Hence the security level Data
of the input channel,
must be declared to be less than or equal to that of the output
channel.
module Copier (I : IN) (O : OUT with level Data greater than I.Data) = struct let copy () = O.print (I.read ()) end;;
module Copier : functor (I : IN) -> functor (O : sig level Data greater than I.Data val print : Data string -{Data ||}-> unit end) -> sig val copy : unit -{[< O.Data, I.Prompt] ||}-> unit end |
withlevel
appearing in
the type of the second argument of the function. Its semantics is
similar to that of withtype
or withmodule
in Objective
Caml: it refines the definition of the level Data
in the
signature of the module O
. The clause greater than I.Data
declares that this security level must be that of a principal allowed to
``receive'' information from the channel implemented by the
structure I
whose level is I.Data
.
module Copier' (O : OUT) (I : IN with level Data less than O.Data) = struct let copy () = O.print (I.read ()) end;;
module Copier' : functor (O : OUT) -> functor (I : sig level Data less than O.Data level Prompt val read : unit -{Prompt ||}-> Data string end) -> sig val copy : unit -{[< I.Prompt, O.Data] ||}-> unit end |
source
in I
must be allowed to send information
to O.Data
.Copier
dedicated to the standard input and output. This
requires to allow information flow from the former to the latter,
which can be done by the toplevel declaration:
flow !stdin < !stdout;; |
Stdin.Data
is less than or
equal to Stdout.Data
, so Stdin
and Stdout
are
legal arguments for Copier
:
module StdCopier = Copier (Stdin) (Stdout);;
module StdCopier : sig val copy : unit -{[< Stdout.Data, Stdin.Prompt] ||}-> unit end |
let
definitions and their evaluation may have
side-effects or raise exceptions. It is worth noting that an
exception which escapes the scope of a top-level let
definition
cannot be trapped further, so it terminates the program.let
definition but also two lists of security levels, or
bounds written from ... to...
where each ...
stands for
a list of security levels. The lower bound (appearing after
the from
keyword) describes the side-effects performed by the
evaluation of the definition, roughly speaking it includes the
security level(s) of data structures the definition
may affect. The upper bound (appearing after the to
keyword) tells how much information is attached to the exceptions
the definition may raise. This process is generalized to the
whole module language by associating to every definition and module
expression a pair of bounds. Because they have no computational
content, the bounds of external
, type
, level
,
exception
, moduletype
, open
and include
definitions are always empty. The bounds of a module
definition
are obtained by considering recursively the module expression which
appears in the right-hand-side. struct def
1
... def
n
end
consists in evaluating successively each of the definitions, then the
bounds associated to the whole module expression are naturally
obtained by merging those of the definitions def
1
to
def
n
. Moreover, while considering this sequence of
definitions, def
i
is evaluated if and only if none of
def
1
to def
i-1
raised an exception. As a result,
to prevent any illegal information flow, the upper bounds of the
former must be less than or equal to the lower bound of the latter.
module type S = sig val x : 'a int end;; module F (X: S) = struct let _ = r1 := X.x; if X.x = x2 then raise Exit end;;
module F : functor (X : S) -{!alice | !bob}-> sig end |
F
may
generate a side-effect on cells of level !alice
and may raise an
exception at level !bob
. Then, an application of F
inserts !bob
in the evaluation context's security level:
module F0 = F (struct let x = 1 end);;
Current evaluation context has level !bob |
F
if the
evaluation context's security level is less than or equal to
!alice
, which is no longer the case after a first application of
F
:
module F1 = F (struct let x = 1 end);;
This expression is executed in a context of level !bob but has an effect at level !alice. This yields the following information flow: from !bob to !alice which is not legal. |
flowcamlc
allows to type-check them, and also
translates them into regular Objective Caml source code files, so that
they can be compiled using the compilers ocamlc
or
ocamlopt
, yielding a standard executable./etc/passwd
registers the list of logins, with, for each of them, a password and
some administrative information such as a numeric id, the user's home
directory and shell. Besides, the file /etc/shadow
associates
to every login a password stored in a encrypted form (with some optional
aging information), which is used in place of that in
/etc/passwd
. Our program aims at synchronizing these two files,
i.e. generating an entry in /etc/shadow
for every account
which is listed only in /etc/passwd
. In the forthcoming
subsections, we will explain step by step how the source code is
organized, how it is verified and compiled thanks to the Flow Caml
system. The type system will allow us to check that running the
program cannot reveal to the user which invokes the command any
information about the passwords stored in the two files.A
comprises one or two files,
among:
.fml
, which contains a
sequence of definitions, analogous to the inside of a
struct...end
construct;
.fmli
, which contains a
sequence of specifications, analogous to the inside of a
sig...end
construct.
flow
declaration and optional affects
and raises
statements, whose
respective purposes will be explained in the
sections 2.8.2
and 2.8.3.) Both files define a structure
named A
(same name as the base name of the two files, with the
first letter capitalized), as if the following definition was entered
at top-level:
module A : sig (* specifications of file a.fmli *) end = struct (* definitions of file a.fml *) end;; |
flowcamlc
, following for each unit A
one of the
three above schemes:
A
defined in files
a.fmli and a.fml is described in
figure 2.1. First, the Flow Caml
interface a.fmli is fed to flowcamlc
, which
checks its well-formedness and produces a compiled version of it,
a.fcmi. It also translates the interface file
into a regular Objective Caml one, namely a.mli.
Second, the implementation a.fml can be type-checked by
flowcamlc
. The compiler computes the most general interface
for the implementation, and checks it fulfills the declared one
(i.e. that stored in a.fcmi). Furthermore, the source
code of the unit in a.fml is translated into a Objective
Caml implementation file, a.ml. Then, a.mli
and a.ml can be compiled with ocamlc
to produce a
compiled interface a.cmi and a bytecode object file
a.cmo.
![]()
Figure 2.1: Compilation scheme of a unit defined in a.fmli and a.fml
A
by providing only an implementation file a.fml
but no interface file. This yields the compilation scheme of
figure 2.2: the implementation
a.fml can be directly passed through flowcamlc
and the interface computed by type inference is stored itself in
a.fcmi.
![]()
Figure 2.2: Compilation scheme of a unit defined in a.fml
![]()
Figure 2.3: Compilation scheme of a unit defined in a.fmli and a.ml
Passwd
and Shadow
are low-level modules which implement functions for
accessing the /etc/passwd
and /etc/shadows
files:
their implementations are directly written in Objective Caml (files
passwd.ml
and shadow.ml
), and only interfaces are
provided in Flow Caml (files passwd.fmli
and
shadow.fmli
). These interfaces assign security levels to the
information manipulated by the units: data stored in
/etc/passwd
has the level !passwd_file
, except the
passwords, which have level !password
. Similarly,
information from /etc/shadow
receives the level !shadow_file
and !shadow_password
. The unit Verbose
provides a verbose
mode: if the user runs the program with the -v
option, then the
execution is traced on the standard output. The body of the
program is in Main
. These last two units are fully implemented
in Flow Caml: implementation (verbose.fml
and
main.fml
) and interface (verbose.fmli
and
main.fmli
) files are provided for each of them.flow
declarations allow specifying the security policy by
setting inequalities between principals. We have seen that the
toplevel system allows the programmer to refine the security policy
incrementally, by entering new flow
declarations which remain
valid until the end of the interactive session.flowcamlc
, every
compilation unit must come with its own security policy, i.e. a
flow
declaration which specifies sufficient assumptions on
principals for its source code to be well-typed. This declaration
must be provided at the beginning of the implementation and interface
files. For instance, the compilation unit Verbose
of our example
begins with the following declaration:
flow !arg < !stderr, !stdout |
!stderr
and !stdout
represent the standard error and the standard output of the program,
respectively. !arg
is the security level of the command-line
arguments. The declaration is a shorthand for
flow !arg < !stderr and !arg < !stdout |
!arg
<
!stdout
and !arg
<
!stderr
. When a compilation unit includes
no flow
declaration---as Passwd
and Shadow
in the
example---this simply means it is well-typed in every security
policy.flow
declaration in every compilation unit of a program is of main
importance for modularity of programming and re-usability of code, in
the context of separate compilation. Indeed, this allows for instance
having libraries (such as the standard one) used in programs which
have different security policies. Otherwise, one would have to
write or compile a specialized version of these libraries for each
program which expects a different policy.flowcamlpol
, to compute
the (minimal) security policy under which a program is (checked to be)
safe. The usage of flowcamlpol
is---to some extent---similar
to that of a linker of object files: it expects as argument the
name of the compiled interfaces of the program's units, in the same
order as the corresponding object files will be linked. For our
example, one must run the command:
flowcamlpol passwd.fcmi shadow.fcmi debug.fcmi main.fcmi |
|
![]() |
-graph
option. It shows some interesting properties of the
information flow graph of the program, which have been established
automatically by the type system. For instance, the
standard output is not related to sensitive data stored in the
/etc/passwd
and /etc/shadow
files, i.e. the user
passwords.
print_string Sys.argv.(0);; |
!arg < !stdout
is
enforced. However, the existence of a ``minimal'' solution to this
problem is no longer ensured when considering the module language.
Indeed, typing module expressions requires comparing type
schemes, i.e. verifying that a given scheme is an instance of another
one, which cannot yield principal flow
declarations. For
instance, the comparison of [< !alice, !bob]int
with
!charlieint
requires the least upper bound of the levels
!alice
and !bob
to be less than or equal to
!charlie
, which is not expressible in a flow
statement.
ocamlc -o passwd2shadow passwd.cmo shadow.cmo verbose.cmo main.cmo |
passwd2shadow
, the
definitions of Passwd
, Shadow
, Verbose
and
Main
are successively evaluated, until an uncaught exception is
raised or the end is reached. Then, when gaining control, each unit
acquires the information that the previous ones normally terminated.
So, in order to trace this possible information flow, one has to
consider the bounds of the underlying structures, as if the
program where defined in a single unit such as:
module Passwd = struct ... end module Shadow = struct ... end module Verbose = struct ... end module Main = struct ... end |
affects
and raises
statements, respectively , which appears
between the flow
declaration and the signature (as usual,
omitted bounds are supposed to be empty). For instance, the interface
of Verbose
declares the following bounds
affects !arg raises !arg |
Verbose
has side-effect of level
!arg
and can raise an exception at this level. This
statement appears only in the interface: when type-checking the
implementation, the bounds are inferred from the source code and
compared to those provided in the interface. Unit
1
,
..., Unit
n
to produce an executable, one has to check
that, for every i, the upper bounds of Unit
1
to
Unit
i-1
are less than or equal to the lower bound of
Unit
i
, which is in fact achieved by the flowcamlpol
command, at the same time it computes the security policy:
flowcamlpol passwd.fcmi shadow.fcmi debug.fcmi main.fcmi |
passwd.fmli
passwd.ml
shadow.fmli
shadow.ml
verbose.fmli
verbose.fml
main.fml
like this
).
Non-terminal symbols are set in italic font
(like that). Square brackets
[...] denote
optional components. Curly brackets <...> denotes
zero, one or several repetitions of the enclosed components. Curly
bracket with a trailing plus sign <...>+ denote one or
several repetitions of the enclosed components. Parentheses
(...) denote grouping. ident | ::= | (letter | _ ) <letter | 0 ...9 | _ | ' > |
letter | ::= | A ...Z | a ...z |
infix-symbol | ::= | (= | < | > | @ | ^ | | | & | + | - |
* | / | $ | % ) <operator-char> |
prefix-symbol | ::= | (! | ? | ~ ) <operator-char> |
operator-char | ::= | ! | $ | % | & | + | - | . | / | :
< | = | > | ? | @ | ^ | | | ~ |
`
) and immutable ones (string-literal),
between double quotes ("
). It is worth noting that escape
sequences are the same for both. In particular, \`
is not a
valid escape sequence, even between `
, so one has to write
\096
.integer-literal | ::= | [- ] <0 ...9 >+ |
| | [- ] (0x | 0X ) <0 ...9 | A ...F | a ...f >+ |
|
| | [- ] (0o | 0O ) <0 ...7 >+ |
|
| | [- ] (0b | 0B ) <0 ...1 >+ |
|
float-literal | ::= | [- ] <0 ...9 >+ [. <0 ...9 >]
[(e | E ) [+ | - ] <0 ...9 >+] |
char-literal | ::= | ' regular-char ' |
| | ' escape-sequence ' |
|
escape-sequence | ::= | \ (\ | " | ' | n | t | b | t ) |
| | \ (0 ...9 ) (0 ...9 ) (0 ...9 ) |
|
string-literal | ::= | " <string-character> " |
charray-literal | ::= | ` <charray-character> ` |
string-character | ::= | regular-char-string |
| | escape-sequence | |
charray-character | ::= | regular-char-charray |
| | escape-sequence |
and as assert asr begin class closed constraint do done downto else end exception external false for fun function functor if in include inherit land lazy let lor lsl lsr lxor match method mod module mutable new of open or parser private rec sig struct then to true try type val virtual when while with affects content finally flow greater less level noneq propagate raise raises row thanThe following character sequences are also keywords:
# & ' ( ) * , -> ? ?? . .. .( .[ : :: := ; ;; <- = [ [| [< {< ] |] >] >} _ ` { | } ~ -{ }-> ={ }=>
· Value names: | value-name | ::= | lowercase-ident | ( operator-name ) |
|
operator-name | ::= | prefix-symbol | infix-symbol | ||
| | * | = | or | & | := |
|||
· Value constructors: | constr-name | ::= | capitalized-ident | false | true | [] | () | :: |
|
· Record fields: | field-name | ::= | lowercase-ident | |
· Type constructors: | typeconstr-name | ::= | lowercase-ident | |
· Level names: | level-name | ::= | uppercase-ident | |
· Principals: | principal | ::= | ! lowercase-ident |
|
· Exception names: | exception-name | ::= | capitalized-ident | |
· Module name: | module-name | ::= | capitalized-ident | |
· Module type names: | modtype-name | ::= | ident |
.
name, where prefix designates a module
and name is the name of an object defined in that module. The
first component of the path, prefix is either a simple module name
or an access path name1.
name2..., in case the
defining module is itself nested inside other modules. For referring
to type constructors, levels, exceptions (in type expressions) or
module types, the prefix can also contain simple functor
applications (as in the syntactic class ext-module-path), in case
the defining module is the result of a functor application.value-path | ::= | value-name | module-path . lowercase-ident |
constr | ::= | constr-name | module-path . capitalized-ident |
typeconstr | ::= | typeconstr-name | extended-module-path . lowercase-ident |
level | ::= | level-name | extended-module-path . capitalized-ident |
exception | ::= | exception-name | extended-module-path . capitalized-ident |
field | ::= | field-name | module-path . lowercase-ident |
module-path | ::= | module-name | module-path . capitalized-ident |
ext-module-path | ::= | module-name |
| | ext-module-path . capitalized-ident |
|
| | ext-module-path ( ext-module-path ) |
|
modtype-path | ::= | modtype-name | module-path . ident |
<
, although it is not strict). The lattice must include
principals (principal) and levels introduced by level
declarations (level), which form ``(constant) security levels'':flow
declarations (see section 3.3.3),
<
security-level2 holds.level
definitions.level-definition | ::= | level level-name level-repr |
level-repr | ::= | [greater than security-level-list]
[less than security-level-list] |
| | = security-level |
|
security-level-list | ::= | security-level <, security-level> |
level
keyword. It consists
in a capitalized identifier followed by two optional sets of
assumptions or bounds. The identifier is the name of the level
being defined. The assumptions relate this new level with existing
ones:
greater
than
security-level1 ,
... ,
security-leveln is provided, then the new
level is made greater than or equal to security-leveli, for
all i.
less
than
security-level1 ,
... ,
security-leveln is provided, then the new
level is made less than or equal to security-leveli, for all i.
=
security-level is a shorthand for
greater
than
security-level less
than
security-level.
The definition cannot introduce new relationships about levels listed in
these assumptions: every level which appears in the former must be
known to be less than or equal to each of those appearing in the latter.row
[
exception1,
...,
exceptionn]
is a mapping from exceptions distinct of
exception1, ..., exceptionn to security
levels.typexpr | ::= | ' ident |
| | ( typeexpr ) |
|
| | typexpr -{ typexpr | typexpr | typexpr
}-> typexpr |
|
| | typexpr <* typexpr>+ |
|
| | typeconstr | |
| | typeexpr typeconstr | |
| | ( typexpr <, typexpr> ) typeconstr |
|
| | exception : typexpr ; typexpr |
'
ident. A
type variable can be used with any kind (i.e. there is a not a
distinguished name-space for every kind of variables); however in a
given context (i.e. a type scheme, a data-type definition or a type
constraint), every occurrence of a given type variable must be used
with the same kind. In type definitions, type variables are names for
the type parameters. In type schemes, they are implicitly
universally quantified.-{
typexpr2 |
typexpr3 |
typexpr4}->
typexpr5. typexpr1 and
typexpr5 have the kind type
; the former is the type of the
argument of the function and the latter that of its result.
typexpr2 and typexpr4 are levels (of kind level
). The
former is the security level of the context (generally written
pc in the literature) where the
function is called while the latter is the annotation attached to the
function's identity. Lastly, typexpr3 is the row (of kind
row []
) describing the exceptions raised by the function.*
... *
typexprn is the type of tuples whose elements belong
to types typexpr1, ..., typexprn respectively.(
typeexpr <,
typeexpr> )
)
arguments. Every type constructor is supposed to have a
signature which gives the number of parameters it expects and
their respective kind, see section 3.2.7.:
typexpr1 ;
typexpr2 stands for the row whose entry at index
exception is typexpr1 and whose other entries are given
by the row typexpr2.type-scheme | ::= | typexpr [with constraint <and constraint>] |
constraint | ::= | left-hand-side <, left-hand-side> <
right-hand-side <, right-hand-side> |
| | typexpr <~ typexpr>+ |
|
left-hand-side | ::= | typexpr | content ( typexpr) |
right-hand-side | ::= | typexpr | level ( typexpr) |
<
, or
a same-skeleton constraint, i.e. a ~
-separated list of type
expressions.'a
with the two constraints
security-level <
'a
and
'a
<
security-level.
[<
security-level1 ,
... ,
security-leveln]
is a shorthand for a fresh level
variable 'a
with the constraint 'a
<
security-level1 ,
... ,
security-leveln.
Similarly, [>
security-level1 ,
... ,
security-leveln]
is a shorthand for a fresh level
variable 'a
with the constraint security-level1
,
... ,
security-leveln <
'a
. Lastly,
[<
security-level1 ,
... ,
security-leveln |>
security-leveln+ 1 ,
... ,
security-leveln+ k]
is a shorthand for a fresh
level variable 'a
with the constraints
security-level1 ,
... ,
security-leveln
<
'a
and
'a
<
security-leveln+ 1 ,
... ,
security-leveln+ k.
_
stands for an anonymous fresh variable of any kind.
-{|
typexpr1 |
typexpr2
}->
is a shorthand for -{
'a
|
typexpr1 |
typexpr2 }->
where 'a
is
a fresh variable. Furthermore, an arrow whose three annotations are
omitted, -{ | | | }->
, can be written ->
.
type-definition | ::= | type typedef <and typedef> |
typedef | ::= | [noneq ] [type-params] typeconstr-name [= typexpr]
[type-repr] |
type-params | ::= | type-param | ( type-param <, type-param> ) |
type-param | ::= | [+ | - | = | # ] ' ident
[: (level | type row [ [exception-list] ] )] |
type-repr | ::= | constr-decl <| constr-decl> [# ' ident] |
| | { field-decl <; field-decl> } [# ' ident] |
|
constr-decl | ::= | constr-name | constr-name of typexpr |
field-decl | ::= | [mutable ] field-name : typexpr |
type
keyword, and consist in
one or several simple definitions, possibly mutually recursive,
separated by the and
keyword. Each simple definition defines one
type constructor. A simple definition consists in a lowercase
identifier, possibly preceded by a noneq
flag and one or several
type parameters, and followed by an optional type equation, and then
an optional type representation. The identifier is the name of the
type constructor being defined.'
ident or a list of type variables (
'
ident1
,
... ,
identn)
for type constructors with several
parameters. Each parameter may be annotated by its variance and its
kind. In the case where the type definition introduces an abstract
type (i.e. no type equation is provided), these annotations are
mandatory and reproduced in the signature of the type constructor. In
other cases, the signature computed for the type constructor is the
minimal one which fits the type equation, the type representation and
these annotations.=
typexpr makes the
defined type equivalent to the type expression typexpr on the
right of the =
sign: one can be substituted for the other during
typing. If no type equation is given, a new type is generated which
is incompatible with any other type.=
constr-decl <|
constr-decl>
[#
'
ident] describes a variant type. The optional
annotation [#
'
ident] declares the security level
attached to variant values. It must be one of the parameters of the
type constructor and it is required if the variant type comprises
several constructors.
=
{
field-decl <;
field-decl > }
describes a record type. The optional
annotation [#
'
ident] declares the security level
attached to records values. It must be one of the parameter of the
type constructor and it is required if the record type comprises one
or several mutable fields.
exntypexpr | ::= | ' ident |
| | security-level | |
| | ( typeexpr ) |
|
| | exntypexpr -{ exntypexpr | exntypexpr | exntypexpr
}-> exntypexpr |
|
| | exntypexpr <* exntypexpr>+ |
|
| | typeconstr | |
| | typeexpr typeconstr | |
| | ( exntypexpr <, exntypexpr> ) typeconstr |
|
| | exception : exntypexpr ; exntypexpr |
|
exception-definition | ::= | exception exception-name [exception-argument]
[= exception-name] |
exception-argument | ::= | [: ' ident] of exntypexpr |
'
ident which can appear in the type expression. The type
expression exntypexpr gives the type of the exception's argument,
it is a type expression which may involve constant security levels.constant | ::= | integer-literal |
| | float-literal | |
| | char-literal | |
| | string-literal | |
| | charray-literal | |
| | constr |
pattern | ::= | value-name |
| | _ |
|
| | pattern as value-name |
|
| | ( pattern ) |
|
| | ( pattern : type-scheme ) |
|
| | pattern | pattern |
|
| | constr pattern | |
| | { field = pattern <; field = pattern> } |
|
| | [ pattern <; pattern> ] |
|
| | pattern :: pattern |
|
| | [| pattern <; pattern> |] |
expr | ::= | value-path |
| | constant | |
| | ( expr ) |
|
| | begin expr end |
|
| | ( expr : type-scheme ) |
|
| | expr , expr <, expr> |
|
| | ncconstr expr | |
| | expr :: expr |
|
| | [ expr <; expr> ] |
|
| | [| expr <; expr> |] |
|
| | { field = expr <; field = expr> } |
|
| | { expr with
field = expr <; field = expr> } |
|
| | expr <expr>+ | |
| | prefix-symbol expr | |
| | expr (infix-symbol | * | = | or | & ) expr |
|
| | expr . field |
|
| | expr . field <- expr |
|
| | expr .( expr ) |
|
| | expr .( expr ) <- expr |
|
| | expr .[ expr ] |
|
| | expr .[ expr ] <- expr |
|
| | if expr then expr [else expr] |
|
| | while expr do expr done |
|
| | for ident = expr (to | downto ) expr
do expr done |
|
| | expr ; expr |
|
| | match expr with pattern-matching |
|
| | function pattern-matching |
|
| | fun multiple-matching |
|
| | raise (exception | ( exception expr ) ) |
|
| | try expr with [| ] handler <| handler> |
|
| | try expr finally expr |
|
| | let [rec ] let-binding <and let-binding> in expr |
|
pattern-matching | ::= | [| ] pattern [when expr] -> expr <|
pattern [when expr] -> expr> |
multiple-matching | ::= | <pattern>+ [when expr] -> expr |
let-binding | ::= | pattern [: type-scheme] = expr |
| | value-name <pattern>+ [: type-scheme] = expr |
|
handler | ::= | exception-pattern -> expr [propagate ] |
raise
is no longer a regular function but a construct of the
language. Two exceptions handlers are provided: the expression
try |
|
expr | |
with |
|
pattern1 -> expr1 [propagate ] |
|
... | |
| patternn -> exprn [propagate ] |
propagate
, the
trapped exception is propagated (in this case, exceptions raised by
expri are not thrown), otherwise the value produced by the
evaluation of expri becomes that of the whole
try
expression. If none of the patterns matches
the exception raised by expr, the exception is raised again,
thereby transparently ``passing through'' the try
construct.try
expr1 finally
expr2 evaluates the expression expr1. This
produces a result which is either a value of a raised exception. In
both cases, the expression expr2 is evaluated and its result
(value or exception) discarded. Finally, the result produced by
expr1 becomes the result of the whole expression.module-type | ::= | modtype-path |
| | sig <specification [;; ]> end |
|
| | functor ( module-name : module-type )
functor-arrow module-type |
|
| | module-type with mod-constraint <and mod-constraint> |
|
| | ( module-type ) |
|
specification | ::= | val value-name : type-scheme |
| | external value-name : type-scheme =
external-declaration |
|
| | type-definition | |
| | level-definition | |
| | exception-definition | |
| | module module-name module-args : module-type |
|
| | module type modtype-name = [module-type] |
|
| | open module-path |
|
| | include module-type |
|
module-args | ::= | <( module-name : module-type ) > |
mod-constraint | ::= | type [type-parameters] typeconstr = typexpr |
| | level level level-repr |
|
| | module module-path = ext-module-path |
|
functor-arrow | ::= | -> | -{ [security-level-list] | [security-level-list] }-> |
exception
) may mention an equality between exceptions
(as in structures). This is made necessary because exceptions
appears in types for values.
module-expr | ::= | module-path |
| | struct <definition [;; ]> end |
|
| | functor ( module-name : module-type ) ->
module-expr |
|
| | module-expr ( module-expr ) |
|
| | ( module-expr ) |
|
| | ( module-expr : module-type ) |
|
definition | ::= | let [rec ] let-binding <and let-binding> |
| | external value-name : type-scheme =
external-declaration |
|
| | type-definition | |
| | level-definition | |
| | exception-definition | |
| | module module-name module-args
= module-expr |
|
| | module type modtype-name = module-type |
|
| | open module-path |
|
| | include module-expr |
interface | ::= | flows-declaration [affects security-level-list]
[raises security-level-list]
<specification> |
implementation | ::= | flows-declaration <specification> |
flows-declaration | ::= | flow principal-list < principal-list
<and principal-list < principal-list> |
principal-list | ::= | principal <, principal> |
flow
declarations and a specification. The flow
declarations define
the partial order <
between principals which is used throughout
the type-checking of the specification: it is the smallest one which
satisfies all the listed inequalities. The specification is the body
of a structure which lists the values, types, levels, exceptions,
modules and module types implemented by the unit.flow
declarations, the affects
and raises
statements and a
specification. The flow
declarations define the partial order <
between principals which has been used throughout the type-checking of
the unit: in short, it provides a description of the possible
information flow generated by the code of the unit. The inequality
between principals provided in the interface of a unit must imply
(possibly by transitivity) all those mentioned in the implementation
of the unit. Lastly, the statements affects
and raises
mentions respectively the lower and upper bounds of the module
expression underlying the compilation unit. They are omitted when the
bound is empty.flowcaml
, permits interactive
use of the Flow Caml system through a read--type-check loop. In this
mode, the system repeatedly reads Flow Caml phrases from the input,
type-checks them and outputs the inferred type, if any. The system
prints a #
(sharp) prompt before reading each phrase.;;
(a double-semicolon). The toplevel input has the following syntax.toplevel-input | ::= | <toplevel-phrase> ;; |
| | flows-declaration ;; |
|
| | # ident [directive-argument];; |
|
toplevel-phrase | ::= | definition |
| | expr | |
directive-argument | ::= | string-literal | integer-literal | value-path | level | typeconstr | exception |
struct ...end
module expressions. It can also consist in a flow
declaration, which extend the current security policy, or in a
toplevel directive, starting with #
(the sharp sign). These
directives control the behavior of the toplevel; they are listed
below in section 4.1.3.-graph
command line option or the directive
#open_graph
. For a description of the graphical output of type
schemes, see section 2.2.6 of the
tutorial.flowcaml
command.
-graph
). By default this is obtained from the environment
variable DISPLAY
.-display
and -geometry
. -I
are searched after the current directory, in
the order in which they were given on the command line, but before
the standard library directory.C/c | enable/disable use of colors for displaying polarities of type variables (on VT100 compatible terminals). | |
0/1/2 | Set which universally quantified and unconstrained type
variables are hidden. In mode 0 , all are hidden or
replaced by _ , in mode 1 , only those which appear on
functions arrows are hidden, in mode 2 , all are displayed. |
|
H/h | enable/disable the hiding of universally quantified and
unconstrained type variables (h implies g ). |
|
V/v | enable/disable printing of the polarities of type variables
(with the symbols + , - and = ) |
Cv1
.
-pprint
above for a
description of available flags).flowcaml
command.
flowcamlc
. To describe in a few words its working, let us say
that it reads Flow Caml files as input, type-checks their content and
produces regular Objective Caml code as output. These may be later
compiled using the standard ocamlc
or ocamlopt
compilers
to obtain executables. flowcamlc
command has a command-line interface similar to
that of the Objective Caml compilers. It accepts several types of
arguments:
.fmli
are taken to be source files
for compilation unit interfaces. From the
file x.fmli, the flowcamlc
compiler produces
a compiled interface in the file x.fcmi and an
Objective Caml compilation unit interface in the file
x.mli..fml
are taken to be source files for
compilation unit implementations. From the file
x.fml, the flowcamlc
compiler produces a
Objective Caml compilation unit implementation in the file
x.ml.flowcamlc
command:
.fml
file). This can be useful
to check the types inferred by the compiler. Also, since the output
follows the syntax of interface files, it can help in writing an
explicit interface (.fmli
file) for a file: just redirect the
standard output of the compiler to a .fmli
file, and edit
that file to remove all declarations of unexported names.-I
are searched after the current directory, in
the order in which they were given on the command line, but before
the standard library directory.flowcaml
for a
description of available flags.flowcamlpol
tool aims at verifying that a series of
compilation units can be linked together in order to produce a secure
program. It moreover computes the minimal security policy under which
the program can be safely run (which is the ``union'' of the security
policies declared in the compilation units). flowcamlpol
command takes as argument the list of the
compiled interface files of the units which form the program, in the
order they should be passed to the linker. For instance, for a
program made of the compilation units unit
1
, unit
2
, ...,
unit
n
, which is linked with the command
ocamlc unit
1
.cmo unit
2
.cmo ... unit
n
.cmo
, one have to run:
flowcamlpol unit1.fcmi unit2.fcmi ... unitn.fcmi |
-graph
option.) flowcamlpol
also checks that the
bounds of the compilation units fit together, i.e. that for every
i, the lower bound of unit
i
is greater than or equal to the
upper bounds of unit$_1$
, ..., unit
i-1
, in the printed
security policy.flowcamlpol
is given in
sections 2.8.2
and 2.8.3 of the tutorial.flowcamlpol
command.
-graph
). By default this is obtained from the environment
variable DISPLAY
.-display
and
-geometry
.flowcamldep
command scans a set of Flow Caml source files
(.fml and .fmli files) for references to external
compilation units, and outputs dependency lines in a format suitable
for the make
utility. This ensures that make
will
compile the source file in the correct order, and recompile those files
that need to when a source file is modified.flowcamldep *.fmli *.fml > Depend.flowcamlwhere
*.fmli *.fml
expands to all source files in the current
directory and Depend.flowcaml
is the file that should contain the
dependencies. (See below for a typical Makefile.)flowcamldep
generates only the dependencies
needed to the Flow Caml compiler, that is those relating .fmli
and .fml
files to .mli
and .ml
. To compile the
files generated by Flow Caml with one of the Objective Caml compilers,
you will need to run ocamldep
on the intermediate files
.mli
and .mli
.flowcamldep
:
foo.fml
mentions an external compilation unit Bar
, a
dependency on that unit's interface bar.mli
is generated only
if the source for bar
is found in the current directory or in
one of the directories specified with -I
. Otherwise,
Bar
is assumed to be a module from the standard library, and
no dependencies are generated.# Compilers OCAMLC=ocamlc -I +flowcamlrun OCAMLOPT=ocamlopt -I +flowcamlrun FLOWCAMLC=flowcamlc OCAMLDEP=ocamldep FLOWCAMLDEP=flowcamldep # The list of object files for the program OBJECTS=mod1.cmo mod2.cmo mod3.cmo # To check the security policy pol: $(OBJECTS.cmo=.fcmi) flowcamlpol $^ # To build the program prog: $(OBJECTS) $(OCAMLC) -o $@ flowcamlrun.cma $(OBJECTS) # Common rules .SUFFIXES: .ml .mli .fml .fmli .cmi .cmo .cmx .fcmi .ml.cmo: $(OCAMLC) -c $< .mli.cmi: $(OCAMLC) -c $< .ml.cmx: $(OCAMLOPT) -c $< .fmli.mli: $(FLOWCAMLC) -c $< .fml.ml: $(FLOWCAMLC) -c $< # Clean up rm -f prog rm -f *.cm[iox] *.fcmi for i in *.mli; do \ if test -f `basename $$i .mli`.fmli; then rm -f $$i; fi \ done for i in *.ml; do \ if test -f `basename $$i .ml`.fml; then rm -f $$i; fi \ done # Dependencies depend-flowcaml: $(FLOWCAMLDEP) *.fml *.fmli > Depend.flowcaml depend-ocaml: $(OCAMLDEP) *.ml *.mli > Depend.ocaml # Dependencies depend-flowcaml: $(FLOWCAMLDEP) *.fml *.fmli > Depend.flowcaml depend-ocaml: $(patsubst %.fml,%.ml,$(wildcard *.fml))\ $(patsubst %.fml,%.ml,$(wildcard *.fml)) $(OCAMLDEP) *.ml *.mli > Depend.ocaml include Depend.flowcaml include Depend.ocaml
Pervasives
module is
automatically ``opened'' when a compilation starts or when the
toplevel is launched.'a
.
'a
.
Array.get a n
returns the element number n
of array a
.
The first element has number 0.
The last element has number Array.length a - 1
.n
is outside the range
0 to (Array.length a - 1)
.
You can also write a.(n)
instead of Array.get a n
.
Array.set a n x
modifies array a
in place, replacing
element number n
with x
.n
is outside the range
0 to Array.length a - 1
.
You can also write a.(n) <- x
instead of Array.set a n x
.
Array.make n x
returns a fresh array of length n
,
initialized with x
.
All the elements of this new array are initially
physically equal to x
(in the sense of the ==
predicate).
Consequently, if x
is mutable, it is shared among all elements
of the array, and modifying x
through one of the array entries
will modify all other entries at the same time.n < 0
or n > Sys.max_array_length
.
If the value of x
is a floating-point number, then the maximum
size is divided by 2.
Array.create
is an alias for Array.make
.
Array.init n f
returns a fresh array of length n
,
with element number i
initialized to the result of f i
.
In other terms, Array.init n f
tabulates the results of f
applied to the integers 0
to n-1
.
Array.make_matrix dimx dimy e
returns a two-dimensional array
(an array of arrays) with first dimension dimx
and
second dimension dimy
. All the elements of this new matrix
are initially physically equal to e
.
The element (x,y
) of a matrix m
is accessed
with the notation m.(x).(y)
.dimx
or dimy
is less than 1 or
greater than Sys.max_array_length
.
If the value of e
is a floating-point number, then the maximum
size is only Sys.max_array_length / 2
.
Array.create_matrix
is an alias for Array.make_matrix
.
Array.append v1 v2
returns a fresh array containing the
concatenation of the arrays v1
and v2
.
Array.append
, but concatenates a list of arrays.
Array.sub a start len
returns a fresh array of length len
,
containing the elements number start
to start + len - 1
of array a
.start
and len
do not
designate a valid subarray of a
; that is, if
start < 0
, or len < 0
, or start + len > Array.length a
.
Array.copy a
returns a copy of a
, that is, a fresh array
containing the same elements as a
.
Array.fill a ofs len x
modifies the array a
in place,
storing x
in elements number ofs
to ofs + len - 1
.ofs
and len
do not
designate a valid subarray of a
.
Array.blit v1 o1 v2 o2 len
copies len
elements
from array v1
, starting at element number o1
, to array v2
,
starting at element number o2
. It works correctly even if
v1
and v2
are the same array, and the source and
destination chunks overlap.o1
and len
do not
designate a valid subarray of v1
, or if o2
and len
do not
designate a valid subarray of v2
.
Array.to_list a
returns the list of all the elements of a
.
Array.of_list l
returns a fresh array containing the elements
of l
.
Array.iter f a
applies function f
in turn to all
the elements of a
. It is equivalent to
f a.(0); f a.(1); ...; f a.(Array.length a - 1); ()
.
Array.map f a
applies function f
to all the elements of a
,
and builds an array with the results returned by f
:
[| f a.(0); f a.(1); ...; f a.(Array.length a - 1) |]
.
Array.iter
, but the
function is applied to the index of the element as first argument,
and the element itself as second argument.
Array.map
, but the
function is applied to the index of the element as first argument,
and the element itself as second argument.
Array.fold_left f x a
computes
f (... (f (f x a.(0)) a.(1)) ...) a.(n-1)
,
where n
is the length of the array a
.
Array.fold_right f a x
computes
f a.(0) (f a.(1) ( ... (f a.(n-1) x) ...))
,
where n
is the length of the array a
.
create n
returns a fresh buffer, initially empty.
The n
parameter is the initial size of the internal string
that holds the buffer contents. That string is automatically
reallocated when more than n
characters are stored in the buffer,
but shrinks back to n
characters when reset
is called.
For best performance, n
should be of the same order of magnitude
as the number of characters that are expected to be stored in
the buffer (for instance, 80 for a buffer that holds one output
line). Nothing bad will happen if the buffer grows beyond that
limit, however. In doubt, take n = 16
for instance.
If n
is not between 1 and Sys.max_string_length
, it will
be clipped to that interval.
n
that was allocated by Buffer.create
n
.
For long-lived buffers that may have grown a lot, reset
allows
faster reclamation of the space used by the buffer.
add_char b c
appends the character c
at the end of the buffer b
.
add_substring b s ofs len
takes len
characters from offset
ofs
in string s
and appends them at the end of the buffer b
.
add_string b s
appends the string s
at the end of the buffer b
.
add_buffer b1 b2
appends the current contents of buffer b2
at the end of buffer b1
. b2
is not modified.
Invalid_argument "Char.chr"
if the argument is
outside the range 0--255.
Charray.get s n
returns character number n
in string s
.
The first character is character number 0.
The last character is character number Charray.length s - 1
.n
is outside the range
0 to (Charray.length s - 1)
.
You can also write s.[n]
instead of Charray.get s n
.
Charray.set s n c
modifies string s
in place,
replacing the character number n
by c
.n
is outside the range
0 to (Charray.length s - 1)
.
You can also write s.[n] <- c
instead of Charray.set s n c
.
Charray.make n c
returns a fresh string of length n
,
filled with the character c
.
Terminate the program if n < 0
or n >
Sys.max_string_length
.
Charray.sub s start len
returns a fresh string of length len
,
containing the characters number start
to start + len - 1
of string s
.start
and len
do not
designate a valid substring of s
; that is, if start < 0
,
or len < 0
, or start + len >
Charray.length
s
.
Charray.fill s start len c
modifies string s
in place,
replacing the characters number start
to start + len - 1
by c
.start
and len
do not
designate a valid substring of s
.
Charray.blit src srcoff dst dstoff len
copies len
characters
from string src
, starting at character number srcoff
, to
string dst
, starting at character number dstoff
. It works
correctly even if src
and dst
are the same string,
and the source and destination chunks overlap.srcoff
and len
do not
designate a valid substring of src
, or if dstoff
and len
do not designate a valid substring of dst
.
Charray.concat sep sl
concatenates the list of strings sl
,
inserting the separator string sep
between each.
Charray.iter f s
applies function f
in turn to all
the characters of s
. It is equivalent to
f s.(0); f s.(1); ...; f s.(Charray.length s - 1); ()
.
Charray.index s c
returns the position of the leftmost
occurrence of character c
in string s
.
Raise Not_found
if c
does not occur in s
.
Charray.rindex s c
returns the position of the rightmost
occurrence of character c
in string s
.
Raise Not_found
if c
does not occur in s
.
Charray.index
, but start
searching at the character position given as second argument.
Charray.index s c
is equivalent to Charray.index_from s 0 c
.
Charray.rindex
, but start
searching at the character position given as second argument.Charray.rindex s c
is equivalent to
Charray.rindex_from s (Charray.length s - 1) c
.
Charray.contains s c
tests if character c
appears in the string s
.
Charray.contains_from s start c
tests if character c
appears in the substring of s
starting from start
to the end
of s
.
Terminate the program if start
is not a valid index of s
.
Charray.rcontains_from s stop c
tests if character c
appears in the substring of s
starting from the beginning
of s
to index stop
.stop
is not a valid index of s
.
float
).
re
is the real part and im
the
imaginary part.
0
.
1
.
i
.
x + i.y
, returns x - i.y
.
1/z
).
x + i.y
is such that x > 0
or
x = 0
and y >= 0
.
This function has a discontinuity along the negative real axis.
x + i.y
, returns x^2 + y^2
.
x + i.y
, returns sqrt(x^2 + y^2)
.
-pi
to pi
. This function has a discontinuity along the
negative real axis.
polar norm arg
returns the complex having norm norm
and argument arg
.
exp z
returns e
to the z
power.
e
).
pow z1 z2
returns z1
to the z2
power.
.
in Unix).
..
in Unix).
concat dir file
returns a file name that designates file
file
in directory dir
.
true
if the file name is relative to the current
directory, false
if it is absolute (i.e. in Unix, starts
with /
).
true
if the file name is relative and does not start
with an explicit reference to the current directory (./
or
../
in Unix), false
if it starts with an explicit reference
to the root directory or the current directory.
check_suffix name suff
returns true
if the filename name
ends with the suffix suff
.
chop_suffix name suff
removes the suffix suff
from
the filename name
. The behavior is undefined if name
does not
end with the suffix suff
.
.xyz
for instance.Invalid_argument
if the given name does not contain
a period.
concat (dirname name) (basename name)
returns a file name
which is equivalent to name
.
Filename.basename
.
Fmarshal.to_string v flags
returns a string containing
the representation of v
as a sequence of bytes.
Fmarshal.from_string buff ofs
unmarshals a structured value
stored in stg
.
'a
to type 'b
.
Hashtbl.create n
creates a new, empty hash table, with
initial size n
. For best results, n
should be on the
order of the expected number of elements that will be in
the table. The table grows as needed, so n
is just an
initial guess.
Hashtbl.add tbl x y
adds a binding of x
to y
in table tbl
.
Previous bindings for x
are not removed, but simply
hidden. That is, after performing Hashtbl.remove
tbl x
,
the previous binding for x
, if any, is restored.
(Same behavior as with association lists.)
Hashtbl.find tbl x
returns the current binding of x
in tbl
,
or raises Not_found
if no such binding exists.
Hashtbl.find_all tbl x
returns the list of all data
associated with x
in tbl
.
The current binding is returned first, then the previous
bindings, in reverse order of introduction in the table.
Hashtbl.mem tbl x
checks if x
is bound in tbl
.
Hashtbl.remove tbl x
removes the current binding of x
in tbl
,
restoring the previous binding if it exists.
It does nothing if x
is not bound in tbl
.
Hashtbl.replace tbl x y
replaces the current binding of x
in tbl
by a binding of x
to y
. If x
is unbound in tbl
,
a binding of x
to y
is added to tbl
.
This is functionally equivalent to Hashtbl.remove
tbl x
followed by Hashtbl.add
tbl x y
.
Hashtbl.iter f tbl
applies f
to all bindings in table tbl
.
f
receives the key as first argument, and the associated value
as second argument. The order in which the bindings are passed to
f
is unspecified. Each binding is presented exactly once
to f
.
Hashtbl.fold f tbl init
computes
(f kN dN ... (f k1 d1 init)...)
,
where k1
...
kN
are the keys of all bindings in tbl
,
and d1 ... dN
are the associated values.
The order in which the bindings are passed to
f
is unspecified. Each binding is presented exactly once
to f
.
Hashtbl.Make
.
equal
, then they have identical hash values
as computed by hash
.
Examples: suitable (equal
, hash
) pairs for arbitrary key
types include
((=)
, Hashtbl.hash
) for comparing objects by structure, and
((==)
, Hashtbl.hash
) for comparing objects by addresses
(e.g. for mutable or cyclic keys).
Hashtbl.Make
.
H
instead of generic
equality and hashing.
Hashtbl.hash x
associates a positive integer to any value of
any type. It is guaranteed that
if x = y
, then hash x = hash y
.
Moreover, hash
always terminates, even on cyclic
structures.
Hashtbl.hash_param n m x
computes a hash value for x
, with the
same properties as for hash
. The two extra parameters n
and
m
give more precise control over hashing. Hashing performs a
depth-first, right-to-left traversal of the structure x
, stopping
after n
meaningful nodes were encountered, or m
nodes,
meaningful or not, were encountered. Meaningful nodes are: integers;
floating-point numbers; strings; characters; booleans; and constant
constructors. Larger values of m
and n
means that more
nodes are taken into account to compute the final hash
value, and therefore collisions are less likely to happen.
However, hashing takes longer. The parameters m
and n
govern the tradeoff between accuracy and speed.
int32
of signed 32-bit integers. Unlike the built-in int
type,
the type int32
is guaranteed to be exactly 32-bit wide on all
platforms. All arithmetic operations over int32
are taken
modulo 232.int32
occupy more memory
space than values of type int
, and arithmetic operations on
int32
are generally slower than those on int
. Use int32
only when the application requires exact 32-bit arithmetic.
Division_by_zero
if the second
argument is zero. This division rounds the real quotient of
its arguments towards zero, as specified for Pervasives.(/)
.
Int32.succ x
is Int32.add x Int32.one
.
Int32.pred x
is Int32.sub x Int32.one
.
Int32.shift_left x y
shifts x
to the left by y
bits.
The result is unspecified if y < 0
or y >= 32
.
Int32.shift_right x y
shifts x
to the right by y
bits.
This is an arithmetic shift: the sign bit of x
is replicated
and inserted in the vacated bits.
The result is unspecified if y < 0
or y >= 32
.
Int32.shift_right_logical x y
shifts x
to the right by y
bits.
This is a logical shift: zeroes are inserted in the vacated bits
regardless of the sign of x
.
The result is unspecified if y < 0
or y >= 32
.
int
) to a 32-bit integer (type int32
).
int32
) to an
integer (type int
). On 32-bit platforms, the 32-bit integer
is taken modulo 231, i.e. the high-order bit is lost
during the conversion. On 64-bit platforms, the conversion
is exact.
Int32.min_int
, Int32.max_int
].
0x
, 0o
or 0b
respectively.
Raise Failure "int_of_string"
if the given string is not
a valid representation of an integer.
Pervasives.compare
. Along with the type t
, this function compare
allows the module Int32
to be passed as argument to the functors
Set.Make
and Map.Make
.
int64
of
signed 64-bit integers. Unlike the built-in int
type,
the type int64
is guaranteed to be exactly 64-bit wide on all
platforms. All arithmetic operations over int64
are taken
modulo 264 int64
occupy more memory
space than values of type int
, and arithmetic operations on
int64
are generally slower than those on int
. Use int64
only when the application requires exact 64-bit arithmetic. Division_by_zero
if the second
argument is zero. This division rounds the real quotient of
its arguments towards zero, as specified for Pervasives.(/)
.
Int64.succ x
is Int64.add x Int64.one
.
Int64.pred x
is Int64.sub x Int64.one
.
Int64.shift_left x y
shifts x
to the left by y
bits.
The result is unspecified if y < 0
or y >= 64
.
Int64.shift_right x y
shifts x
to the right by y
bits.
This is an arithmetic shift: the sign bit of x
is replicated
and inserted in the vacated bits.
The result is unspecified if y < 0
or y >= 64
.
Int64.shift_right_logical x y
shifts x
to the right by y
bits.
This is a logical shift: zeroes are inserted in the vacated bits
regardless of the sign of x
.
The result is unspecified if y < 0
or y >= 64
.
int
) to a 64-bit integer (type int64
).
int64
) to an
integer (type int
). On 64-bit platforms, the 64-bit integer
is taken modulo 263, i.e. the high-order bit is lost
during the conversion. On 32-bit platforms, the 64-bit integer
is taken modulo 231, i.e. the top 33 bits are lost
during the conversion.
Int64.min_int
, Int64.max_int
].
int32
)
to a 64-bit integer (type int64
).
int64
) to a
32-bit integer (type int32
). The 64-bit integer
is taken modulo 232, i.e. the top 32 bits are lost
during the conversion.
nativeint
)
to a 64-bit integer (type int64
).
int64
) to a
native integer. On 32-bit platforms, the 64-bit integer
is taken modulo 232. On 64-bit platforms,
the conversion is exact.
0x
, 0o
or 0b
respectively.
Raise Failure "int_of_string"
if the given string is not
a valid representation of an integer.
int64
.
Pervasives.compare
. Along with the type t
, this function compare
allows the module Int64
to be passed as argument to the functors
Set.Make
and Map.Make
.
Failure "hd"
if the list is empty.
Failure "tl"
if the list is empty.
Failure "nth"
if the list is too short.
@
.
Not tail-recursive (length of the first argument). The @
operator is not tail-recursive either.
List.rev_append l1 l2
reverses l1
and concatenates it to l2
.List.rev
l1 @ l2
, but rev_append
is
tail-recursive and more efficient.
List.iter f [a1; ...; an]
applies function f
in turn to
a1; ...; an
. It is equivalent to
begin f a1; f a2; ...; f an; () end
.
List.map f [a1; ...; an]
applies function f
to a1, ..., an
,
and builds the list [f a1; ...; f an]
with the results returned by f
. Not tail-recursive.
List.rev_map f l
gives the same result as
List.rev
(
List.map
f l)
, but is tail-recursive and
more efficient.
List.fold_left f a [b1; ...; bn]
is
f (... (f (f a b1) b2) ...) bn
.
List.fold_right f [a1; ...; an] b
is
f a1 (f a2 (... (f an b) ...))
. Not tail-recursive.
List.iter2 f [a1; ...; an] [b1; ...; bn]
calls in turn
f a1 b1; ...; f an bn
.
Raise Invalid_argument
if the two lists have
different lengths.
for_all p [a1; ...; an]
checks if all elements of the list
satisfy the predicate p
. That is, it returns
(p a1) && (p a2) && ... && (p an)
.
exists p [a1; ...; an]
checks if at least one element of
the list satisfies the predicate p
. That is, it returns
(p a1) || (p a2) || ... || (p an)
.
mem a l
is true if and only if a
is equal
to an element of l
.
List.mem
, but uses physical equality instead of structural
equality to compare list elements.
find p l
returns the first element of the list l
that satisfies the predicate p
.
Raise Not_found
if there is no value that satisfies p
in the
list l
.
filter p l
returns all the elements of the list l
that satisfy the predicate p
. The order of the elements
in the input list is preserved.
find_all
is another name for List.filter
.
partition p l
returns a pair of lists (l1, l2)
, where
l1
is the list of all the elements of l
that
satisfy the predicate p
, and l2
is the list of all the
elements of l
that do not satisfy p
.
The order of the elements in the input list is preserved.
assoc a l
returns the value associated with key a
in the list of
pairs l
. assoc a [ ...; (a,b); ...] = b
if (a,b)
is the leftmost binding of a
in list l
.
Raise Not_found
if there is no value associated with a
in the
list l
.
List.assoc
, but simply return true if a binding exists,
and false if no bindings exist for the given key.
remove_assoc a l
returns the list of
pairs l
without the first pair with key a
, if any.
Not tail-recursive.
split [(a1,b1); ...; (an,bn)]
is ([a1; ...; an], [b1; ...; bn])
.
Not tail-recursive.compare
function is a suitable comparison function.
The resulting list is sorted in increasing order.
List.sort
is guaranteed to run in constant heap space
(in addition to the size of the result list) and logarithmic
stack space.List.stable_sort
.List.sort
, but the sorting algorithm is stable.Map.Make
.
f
such that
f e1 e2
is zero if the keys e1
and e2
are equal,
f e1 e2
is strictly negative if e1
is smaller than e2
,
and f e1 e2
is strictly positive if e1
is greater than e2
.
Example: a suitable ordering function is
the generic structural comparison function Pervasives.compare
.
Map.Make
.
key
to type 'a
.
add x y m
returns a map containing the same bindings as
m
, plus a binding of x
to y
. If x
was already bound
in m
, its previous binding disappears.
find x m
returns the current binding of x
in m
,
or raises Not_found
if no such binding exists.
remove x m
returns a map containing the same bindings as
m
, except for x
which is unbound in the returned map.
mem x m
returns true
if m
contains a binding for x
,
and false
otherwise.
iter f m
applies f
to all bindings in map m
.
f
receives the key as first argument, and the associated value
as second argument. The order in which the bindings are passed to
f
is unspecified. Only current bindings are presented to f
:
bindings hidden by more recent bindings are not passed to f
.
map f m
returns a map with same domain as m
, where the
associated value a
of all bindings of m
has been
replaced by the result of the application of f
to a
.
The order in which the associated values are passed to f
is unspecified.
Map.S.map
, but the function receives as arguments both the
key and the associated value for each binding of the map.
fold f m a
computes (f kN dN ... (f k1 d1 a)...)
,
where k1 ... kN
are the keys of all bindings in m
,
and d1 ... dN
are the associated data.
The order in which the bindings are presented to f
is
unspecified.
nativeint
of
signed 32-bit integers (on 32-bit platforms) or
signed 64-bit integers (on 64-bit platforms).
This integer type has exactly the same width as that of a long
integer type in the C compiler. All arithmetic operations over
nativeint
are taken modulo 232 or 264 depending
on the word size of the architecture.nativeint
occupy more memory
space than values of type int
, and arithmetic operations on
nativeint
are generally slower than those on int
. Use nativeint
only when the application requires the extra bit of precision
over the int
type. Division_by_zero
if the second
argument is zero. This division rounds the real quotient of
its arguments towards zero, as specified for Pervasives.(/)
.
Nativeint.succ x
is Nativeint.add x Nativeint.one
.
Nativeint.pred x
is Nativeint.sub x Nativeint.one
.
32
on a 32-bit platform and to 64
on a 64-bit platform.
Nativeint.shift_left x y
shifts x
to the left by y
bits.
The result is unspecified if y < 0
or y >= bitsize
,
where bitsize
is 32
on a 32-bit platform and
64
on a 64-bit platform.
Nativeint.shift_right x y
shifts x
to the right by y
bits.
This is an arithmetic shift: the sign bit of x
is replicated
and inserted in the vacated bits.
The result is unspecified if y < 0
or y >= bitsize
.
Nativeint.shift_right_logical x y
shifts x
to the right
by y
bits.
This is a logical shift: zeroes are inserted in the vacated bits
regardless of the sign of x
.
The result is unspecified if y < 0
or y >= bitsize
.
int
) to a native integer
(type nativeint
).
nativeint
) to an
integer (type int
). The high-order bit is lost during
the conversion.
Nativeint.min_int
, Nativeint.max_int
].
int32
)
to a native integer.
int32
). On 64-bit platforms,
the 64-bit native integer is taken modulo 232,
i.e. the top 32 bits are lost. On 32-bit platforms,
the conversion is exact.
0x
, 0o
or 0b
respectively.
Raise Failure "int_of_string"
if the given string is not
a valid representation of an integer.
Pervasives.compare
. Along with the type t
, this function compare
allows the module Nativeint
to be passed as argument to the functors
Set.Make
and Map.Make
.
Pervasives
.Exit
exception is not raised by any library function. It is
provided for use in your programs.
Invalid_argument
with the given string.
Failure
with the given string.
e1 = e2
tests for structural equality of e1
and e2
.
Mutable structures (e.g. references and arrays) are equal
if and only if their current contents are structurally equal,
even if the two mutable objects are not the same physical object.
Equality between cyclic data structures may not terminate.
Pervasives.=
.
Pervasives.>=
.
Pervasives.>=
.
Pervasives.>=
.
(=)
. As in the case
of (=)
, mutable structures are compared by contents.
Comparison between cyclic structures may not terminate.
compare x y
returns 0
if x=y
, a negative integer if
x<y
, and a positive integer if x>y
. The same restrictions
as for =
apply. compare
can be used as the comparison function
required by the Set
and Map
modules.
e1 == e2
tests for physical equality of e1
and e2
.
On integers and characters, it is the same as structural
equality. On mutable structures, e1 == e2
is true if and only if
physical modification of e1
also affects e2
.
On non-mutable structures, the behavior of (==)
is
implementation-dependent, except that e1 == e2
implies
e1 = e2
.
Pervasives.==
.
e1 && e2
, e1
is evaluated first, and if it returns false
,
e2
is not evaluated at all.
Pervasives.&&
should be used instead.
Pervasives.or
.
e1 || e2
, e1
is evaluated first, and if it returns true
,
e2
is not evaluated at all.
-e
instead of ~-e
.
succ x
is x+1
.
pred x
is x-1
.
Division_by_zero
if the second argument is 0.
Integer division rounds the real quotient of its arguments towards zero.
More precisely, if x >= 0
and y > 0
, x / y
is the greatest integer
less than or equal to the real quotient of x
by y
. Moreover,
(-x) / y = x / (-y) = -(x / y)
.
y
is not zero, the result
of x mod y
satisfies the following properties:
x = (x / y) * y + x mod y
and
abs(x mod y) < abs(y)
.
If y = 0
, x mod y
raises Division_by_zero
.
Notice that x mod y
is negative if x < 0
.
n lsl m
shifts n
to the left by m
bits.
The result is unspecified if m < 0
or m >= bitsize
,
where bitsize
is 32
on a 32-bit platform and
64
on a 64-bit platform.
n lsr m
shifts n
to the right by m
bits.
This is a logical shift: zeroes are inserted regardless of
the sign of n
.
The result is unspecified if m < 0
or m >= bitsize
.
n asr m
shifts n
to the right by m
bits.
This is an arithmetic shift: the sign bit of n
is replicated.
The result is unspecified if m < 0
or m >= bitsize
.
infinity
for 1.0 /. 0.0
,
neg_infinity
for -1.0 /. 0.0
, and nan
(``not a number'')
for 0.0 /. 0.0
. These special numbers then propagate through
floating-point computations as expected: for instance,
1.0 /. infinity
is 0.0
, and any operation with nan
as
argument returns nan
as result. -.e
instead of ~-.e
.
Pervasives.atan2
.
Pervasives.atan2
.
Pervasives.atan2
.
Pervasives.atan2
.
Pervasives.atan2
.
Pervasives.atan2
.
Pervasives.tanh
.
Pervasives.tanh
.
Pervasives.floor
.
floor f
returns the greatest integer value less than or
equal to f
.
ceil f
returns the least integer value greater than or
equal to f
.
mod_float a b
returns the remainder of a
with respect to
b
. The returned value is a -. n *. b
, where n
is the quotient a /. b
rounded towards zero to an integer.
frexp f
returns the pair of the significant
and the exponent of f
. When f
is zero, the
significant x
and the exponent n
of f
are equal to
zero. When f
is non-zero, they are defined by
f = x *. 2 ** n
and 0.5 <= x < 1.0
.
ldexp x n
returns x *. 2 ** n
.
modf f
returns the pair of the fractional and integral
part of f
.
Pervasives.float_of_int
.
Pervasives.int_of_float
.
0.0 /. 0.0
. Stands for
``not a number''.
float
.
float
.
x
such that 1.0 +. x <> 1.0
.
Pervasives.classify_float
function.
String
(immutable strings) and Charray
(mutable strings).Char
.Invalid_argument "char_of_int"
if the argument is
outside the range 0--255.
()
.
For instance, ignore(f x)
discards the result of
the side-effecting function f
. It is equivalent to
f x; ()
, except that the latter may generate a
compiler warning; writing ignore(f x)
instead
avoids the warning.
Invalid_argument "bool_of_string"
if the string is not
"true"
or "false"
.
0x
, 0o
or 0b
respectively.
Raise Failure "int_of_string"
if the given string is not
a valid representation of an integer.
Failure "float_of_string"
if the given string is not a valid representation of a float.
List
. Failure "int_of_string"
if the line read is not a valid representation of an integer.
'a
.
!r
returns the current contents of reference r
.
Equivalent to fun r -> r.contents
.
r := a
stores the value of a
in reference r
.
Equivalent to fun r v -> r.contents <- v
.
fun r -> r := succ !r
.
fun r -> r := pred !r
.
Pervasives.stdout
and
Pervasives.stderr
,
and terminate the process, returning the given status code
to the operating system (usually 0 to indicate no errors,
and a small positive integer to indicate failure.)
An implicit exit 0
is performed each time a program
terminates normally (but not if it terminates because of
an uncaught exception).
'a
.
Queue.take
or Queue.peek
is applied to an empty queue.
add x q
adds the element x
at the end of the queue q
.
take q
removes and returns the first element in queue q
,
or raises Empty
if the queue is empty.
peek q
returns the first element in queue q
, without removing
it from the queue, or raises Empty
if the queue is empty.
iter f q
applies f
in turn to all elements of q
,
from the least recently entered to the most recently entered.
The queue itself is unchanged.
Random.init
but takes more data as seed.
Random.int bound
returns a random integer between 0 (inclusive)
and bound
(exclusive). bound
must be more than 0 and less
than 230.
Random.float bound
returns a random floating-point number
between 0 (inclusive) and bound
(exclusive). If bound
is
negative, the result is negative. If bound
is 0, the result
is 0.
Random.bool ()
returns true
or false
with probability 0.5 each.
Random.get_state
.
'a
.
Stack.pop
or Stack.top
is applied to an empty stack.
push x s
adds the element x
at the top of stack s
.
pop s
removes and returns the topmost element in stack s
,
or raises Empty
if the stack is empty.
top s
returns the topmost element in stack s
,
or raises Empty
if the stack is empty.
iter f s
applies f
in turn to all elements of s
,
from the element at the top of the stack to the element at the
bottom of the stack. The stack itself is unchanged.
String.get s n
returns character number n
in string s
.
The first character is character number 0.
The last character is character number String.length s - 1
.
Terminate the program if n
is outside the range
0 to (String.length s - 1)
.
You can also write s.[n]
instead of String.get s n
.
String.make n c
returns a fresh string of length n
,
filled with the character c
.
Terminate the program if n < 0
or n >
Sys.max_string_length
.
String.sub s start len
returns a fresh string of length len
,
containing the characters number start
to start + len - 1
of string s
.start
and len
do not
designate a valid substring of s
; that is, if start < 0
,
or len < 0
, or start + len >
String.length
s
.
String.concat sep sl
concatenates the list of strings sl
,
inserting the separator string sep
between each.
String.iter f s
applies function f
in turn to all
the characters of s
. It is equivalent to
f s.(0); f s.(1); ...; f s.(String.length s - 1); ()
.
String.index s c
returns the position of the leftmost
occurrence of character c
in string s
.
Raise Not_found
if c
does not occur in s
.
String.rindex s c
returns the position of the rightmost
occurrence of character c
in string s
.
Raise Not_found
if c
does not occur in s
.
String.index
, but start
searching at the character position given as second argument.
String.index s c
is equivalent to String.index_from s 0 c
.
String.rindex
, but start
searching at the character position given as second argument.String.rindex s c
is equivalent to
String.rindex_from s (String.length s - 1) c
.
String.contains s c
tests if character c
appears in the string s
.
String.contains_from s start c
tests if character c
appears in the substring of s
starting from start
to the end
of s
.
Terminate the program if start
is not a valid index of s
.
String.rcontains_from s stop c
tests if character c
appears in the substring of s
starting from the beginning
of s
to index stop
.
Terminate the program if stop
is not a valid index of s
.
Not_found
if the variable is unbound.
"Unix"
(for all Unix versions, including Linux and Mac OS X),
- "Win32"
(for MS-Windows, OCaml compiled with MSVC++ or Mingw),
- "Cygwin"
(for MS-Windows, OCaml compiled with Cygwin),
- "MacOS"
(for MacOS 9).
ocaml_version
is the version of Objective Caml."major.minor[additional-info]"
Where major and minor are integers, and additional-info
is
a string that is empty or starts with a '+'.
This document was translated from LATEX by HEVEA.
5.1.1 Predefined types
These are predefined types :