Nim/doc/spec.txt
2008-06-23 01:37:49 +02:00

1297 lines
48 KiB
Text

====================
Nimrod Specification
====================
:Author: Andreas Rumpf
.. contents::
About this document
===================
This document describes the lexis, the syntax, and the semantics of Nimrod.
However, this is only a first draft. Some parts need to be more precise,
features may be added to the language, etc.
The language constructs are explained using an extended BNF, in
which ``(a)*`` means 0 or more ``a``'s, ``a+`` means 1 or more ``a``'s, and
``(a)?`` means an optional *a*; an alternative spelling for optional parts is
``[a]``. The ``|`` symbol is used to mark alternatives
and has the lowest precedence. Parentheses may be used to group elements.
Non-terminals are in lowercase, terminal symbols (including keywords) are in
UPPERCASE. An example::
if_stmt ::= IF expr COLON stmts (ELIF expr COLON stmts)* [ELSE stmts]
Definitions
===========
The following defintions are the same as their counterparts in the
specification of the Modula-3 programming language.
A Nimrod program specifies a computation that acts on a sequence of digital
components called `locations`. A variable is a set of locations that
represents a mathematical value according to a convention determined by the
variable's *type*. If a value can be represented by some variable of type
``T``, then we say that the value is a *member* of ``T`` and ``T`` *contains*
the value.
An *identifier* is a symbol declared as a name for a variable, type,
procedure, etc. The region of the program over which a declaration applies is
called the *scope* of the declaration. Scopes can be nested. The meaning of an
identifier is determined by the smallest enclosing scope in which the
identifier is declared.
An expression specifies a computation that produces a value or variable.
Expressions that produce variables are called `designators`. A designator
can denote either a variable or the value of that variable, depending on
the context. Some designators are *readonly*, which means that they cannot
be used in contexts that might change the value of the variable. A
designator that is not readonly is called *writable*. Expressions whose
values can be determined statically are called *constant expressions*;
they are never designators.
A `static error` is an error that the implementation must detect before
program execution. Violations of the language definition are static
errors unless they are explicitly classified as runtime errors.
A `checked runtime error` is an error that the implementation must detect
and report at runtime. The method for reporting such errors is via *raising
exceptions*. However, an implementation may provide a means to disable these
runtime checks. See the section *pragmas* for details.
An `unchecked runtime error` is an error that is not guaranteed to be
detected, and can cause the subsequent behavior of the computation to
be arbitrary. Unchecked runtime errors cannot occur if only `safe`
language features are used.
Lexical Analysis
================
Indentation
-----------
Nimrod's standard grammar describes an `indentation sensitive` language.
This means that all the control structures are recognized by the indentation.
Indentation consists only of spaces; tabulators are not allowed.
The terminals ``IND`` (indentation), ``DED`` (dedentation) and ``SAD``
(same indentation) are generated by the scanner, denoting an indentation.
Using tabulators for the indentation is not allowed.
These terminals are only generated for *logical lines*, i.e. not for an empty
line and not for a line with only whitespace or comments.
The parser and the scanner communicate over a stack which indentation terminal
should be generated: The stack consists of integers counting the spaces. The
stack is initialized with a zero on its top. The scanner reads from the stack:
If the current indentation token consists of more spaces than the entry at the
top of the stack, a ``IND`` token is generated, else if it consists of the same
number of spaces, a ``SAD`` token is generated. If it consists of fewer spaces,
a ``DED`` token is generated for any item on the stack that is greater than the
current. These items are then popped from the stack by the scanner. At the end
of the file, a ``DED`` token is generated for each number remaining on the
stack that is larger than zero.
Because the grammar contains some optional ``IND`` tokens, the scanner cannot
push new indentation levels. This has to be done by the parser. The symbol
``IND_PUSH`` indicates that the ``IND`` token should be pushed onto the stack
by the parser.
An Example how this works::
if_stmt ::= IF expr COLON stmts (ELIF expr COLON stmts)* (ELSE stmts)?
stmts ::= IND_PUSH (stmt [SAD])+ DED | stmt [SAD]
if expr0:
stmt1 # would be valid because, SAD is not generated any longer!
if expr0:
stmt1 # scanner: IND; parser pushes 2 onto the stack
if expr3: stmt5 # DED; ... SAD eaten by stmt5
else: stmt6 #
if expr4: stmt7
if expr:
stmt1 # scanner: IND; the parser pushes 2 onto the stack
stmt2 # scanner: SAD (because indentation is 2)
elif expr2: # scanner: DED (because indentation is 0)
stmts3 # scanner: IND; the parser pushes 2 onto the stack
else: # scanner: DED;
stmt4 # scanner: IND; the parser pushes 2 onto the stack
# scanner generates 1 DED, because end of file and 1 item on stack
Identifiers & Keywords
----------------------
`Identifiers` in Nimrod can be any string of letters, digits
and underscores, beginning with a letter. Two immediate following
underscores ``__`` are not allowed::
letter ::= 'A'..'Z' | 'a'..'z'
digit ::= '0'..'9'
IDENTIFIER ::= letter ( ['_'] letter | digit )*
The following `keywords` are reserved and cannot be used as identifiers::
${keywords}
Some keywords are unused; they are reserved for future developments of the
language.
Nimrod is a `style-insensitive` language. This means that it is not
case-sensitive and even underscores are ignored:
**type** is a reserved word, and so is **TYPE** or **T_Y_P_E**. The idea behind
this is, that this allows programmers to use their own prefered spelling style.
Editors can show the identifiers as configured.
Literal strings
---------------
`Literal strings` can be delimited by matching double quotes, and can contain
the following `escape sequences`:
================== ==================================
Escape sequence Meaning
================== ==================================
``\n`` `newline`
``\r`` `carriage return`
``\l`` `line feed`
``\f`` `form feed`
``\t`` `tabulator`
``\v`` `vertical tabulator`
``\\`` `backslash`
``\"`` `quotation mark`
``\'`` `apostrophe`
``\y`` `character with number y`
``\a`` `alert`
``\b`` `backspace`
``\e`` `escape` `[ESC]`
================== ==================================
Strings in Nimrod may contain any 8-bit value, except embedded zeros,
which are not allowed for compability with `C`.
Literal strings can also be delimited by three double squotes
``"""`` ... ``"""``.
Literals in this form may run for several lines, may contain ``"`` and do not
interpret any escape sequences.
For convenience, when the opening ``"""`` is immediately
followed by a newline, the newline is not included in the string.
`Raw string literals` are preceded with the letter ``r`` (or ``R``)
and are delimited by matching double quotes (just like ordinary string
literals) and do not interpret the escape sequences.
Literal characters
------------------
Character literals are enclosed in single quotes ``''`` and can contain the
same escape sequences as strings - with one exception: ``\n`` is not allowed
as it may be wider than one character (often it is the pair CR/LF for example).
Numerical constants
-------------------
`Numerical constants` are of a single type and have the form::
hexdigit ::= digit | 'A'..'F' | 'a'..'f'
octdigit ::= '0'..'7'
bindigit ::= '0'..'1'
INT_LIT ::= digit ( ['_'] digit )*
| '0' ('x' | 'X' ) hexdigit ( ['_'] hexdigit )*
| '0o' octdigit ( ['_'] octdigit )*
| '0' ('b' | 'B' ) bindigit ( ['_'] bindigit )*
INT8_LIT ::= INT_LIT '\'' ('i' | 'I' ) '8'
INT16_LIT ::= INT_LIT '\'' ('i' | 'I' ) '16'
INT32_LIT ::= INT_LIT '\'' ('i' | 'I' ) '32'
INT64_LIT ::= INT_LIT '\'' ('i' | 'I' ) '64'
exponent ::= ('e' | 'E' ) ['+' | '-'] digit ( ['_'] digit )*
FLOAT_LIT ::= digit (['_'] digit)* ('.' (['_'] digit)* [exponent] |exponent)
FLOAT32_LIT ::= ( FLOAT_LIT | INT_LIT ) '\'' ('f' | 'F') '32'
FLOAT64_LIT ::= ( FLOAT_LIT | INT_LIT ) '\'' ('f' | 'F') '64'
As can be seen in the productions, numerical constants can contain unterscores
for readability. Integer and floating point literals may be given in decimal (no
prefix), binary (prefix ``0b``), octal (prefix ``0o``) and
hexadecimal (prefix ``0x``) notation.
There exists a literal for each numerical type that are
defined. The suffix starting with an apostophe ('\'') is called a
`type suffix`. Literals without a type prefix are of the type ``int``, unless
the literal contains a dot or an ``E`` in which case it is of type ``float``.
The following table specifies type suffixes:
================= =========================
Type Suffix Resulting type of literal
================= =========================
``'i8`` int8
``'i16`` int16
``'i32`` int32
``'i64`` int64
``'f32`` float32
``'f64`` float64
================= =========================
Floating point literals may also be in binary, octal or hexadecimal
notation:
``0B0_10001110100_0000101001000111101011101111111011000101001101001001'f64``
is approximately 1.72826e35 according to the IEEE floating point standard.
Comments
--------
`Comments` start anywhere outside a string with the hash character ``#``.
Comments run until the end of the line. Comments are tokens; they are only
allowed at certain places in the input file as they belong to the syntax. This
is essential for performing correct source-to-source transformations or
documentation generators.
Other tokens
------------
The following strings denote other tokens::
( ) { } [ ] , ; [. .] {. .} (. .)
: = ^ .. `
``..`` takes precedence over other tokens that contain a dot: ``{..}`` are the
three tokens ``{``, ``..``, ``}`` and not the two tokens ``{.``, ``.}``.
In Nimrod one can define his own operators. An `operator` is any
combination of the following characters that are not listed above::
+ - * / < >
= @ $ ~ & %
! ? ^ . |
These keywords are also operators:
``and or not xor shl shr div mod in notin is isnot``.
Syntax
======
This section lists Nimrod's standard syntax in ENBF. How the parser receives
indentation tokens is already described in the Lexical Analysis section.
Nimrod allows user-definable operators.
Binary operators have 8 different levels of precedence. For user-defined
operators, the precedence depends on the first character the operator consists
of. All binary operators are left-associative.
================ ============================================== ================== ===============
Precedence level Operators First characters Terminal symbol
================ ============================================== ================== ===============
7 (highest) ``$`` OP7
6 ``* / div mod shl shr %`` ``* % \ /`` OP6
5 ``+ -`` ``+ ~ |`` OP5
4 ``&`` ``&`` OP4
3 ``== <= < >= > != in not_in is isnot`` ``= < > !`` OP3
2 ``and`` OP2
1 ``or xor`` OP1
0 (lowest) ``? @ ^ ` : .`` OP0
================ ============================================== ================== ===============
The grammar's start symbol is ``module``. The grammar is LL(1) and therefore
not ambigious.
.. include:: grammar.txt
:literal:
Semantics
=========
Constants
---------
Constants are symbols which are bound to a value. The constant's value
cannot change. The compiler must be able to evaluate the expression in a
constant declaration at compile time. This means that most of the functions in
the runtime library cannot be used in a constant declaration.
Operators such as ``+, -, *, /, not, and, or, div, mod`` and the procedures
``addr, ord, chr, sizeof, trunc, round, frac, odd, abs`` can be used, however.
An implementation may restrict the usage of ``addr`` in constant expressions.
Types
-----
All expressions have a type which is known at compile time. Thus Nimrod is
statically typed. One can declare new types, which is in
essence defining an identifier that can be used to denote this custom type
when declaring variables further in the source code.
These are the major type classes:
* ordinal types (consist of integer, bool, character, enumeration
(and subranges thereof) types)
* floating point types
* string type
* structured types
* reference (pointer) type
* procedural type
Ordinal types
~~~~~~~~~~~~~
Ordinal types have the following characteristics:
- Ordinal types are countable and ordered. This property allows
the operation of functions as Inc, Ord, Dec on ordinal types to be defined.
- Ordinal values have a smallest possible value. Trying to count farther
down than the smallest value gives a checked runtime or static error.
- Ordinal values have a largest possible value. Trying to count farther
than the largest value gives a checked runtime or static error.
Signed integers, bool, characters and enumeration types (and subrange of these
types) belong to ordinal types. Unsigned integer types are special in the way
that over- and underflows generate no errors, but wrap around.
Pre-defined numerical types
~~~~~~~~~~~~~~~~~~~~~~~~~~~
These integer types are pre-defined:
``int``
the generic signed integer type; its size is platform dependant
(a compiler should choose the processor's fastest integer type)
this type should be used in general. An integer literal that has no type
suffix is of this type.
intXX
an implementation may define additional signed integer types
of XX bits using this naming scheme (example: int16 is a 16 bit wide integer).
The current implementation supports ``int8``, ``int16``, ``int32``, ``int64``.
Literals of these types have the suffix 'iXX.
There are no unsigned integer types, only *unsigned operations* that treat their
arguments as unsigned. Unsigned operations all wrap around; they may not lead to
over- or underflow errors.
The following floating point types are pre-defined:
``float``
the generic floating point type; its size is platform dependant
(a compiler should choose the processor's fastest floating point type)
this type should be used in general
floatXX
an implementation may define additional floating point types of XX bits using
this naming scheme (example: float64 is a 64 bit wide float). The current
implementation supports ``float32`` and ``float64``. Literals of these types
have the suffix 'fXX.
Automatic type conversion in expressions where different kinds
of integer types are used is performed. However, if the type conversion
loses information, the `EConvertError` exception is raised. An implementation
may detect certain cases of the convert error at compile time.
Automatic type conversion in expressions with different kinds
of floating point types is performed: The smaller type is
converted to the larger. Arithmetic performed on floating point types
follows the IEEE standard.
Boolean type
~~~~~~~~~~~~
The boolean type is named ``bool`` in Nimrod and can be one of the two
pre-defined values ``true`` and ``false``. Conditions in while,
if, elif, when statements need to be of type bool.
This condition should hold::
ord(false) == 0 and ord(true) == 1
The operators ``not, and, or, xor`` are defined for the bool type.
The ``and`` and ``or`` operators perform short-cut evaluation.
Example::
while p != nil and p.name != "xyz":
# p.name is not evaluated if p == nil
p = p.next
The size of the bool type is implementation-dependant, typically it is
one byte.
Character type
~~~~~~~~~~~~~~
The character type is named ``char`` in Nimrod and uses the platform's
native encoding. Thus on nearly every platform its size is one byte due to
the popular UTF-8 encoding.
Character literals are enclosed in single quotes ``''``.
.. Note:: For platform-independant character handling is the ``encoding``
standard module.
Enumeration types
~~~~~~~~~~~~~~~~~
Enumeration types define a new type whose values consist only of the ones
specified.
The values are ordered by the order in enum's declaration. Example::
type
TDirection = enum
north, east, south, west
Now the following holds::
ord(north) == 0
ord(east) == 1
ord(south) == 2
ord(west) == 3
Thus, north < east < south < west. The comparison operators can be used
with enumeration types.
An implemenation should store enumeration types with the minimal number of
bytes required for the particular enum, unless efficiency would be affected by
doing so.
For better interfacing to other programming languages, the fields of enum
types can be assigned an explicit ordinal value. However, the ordinal values
have to be in ascending order appropriately. A field whose ordinal value that
is not explicitly given, gets the value of the previous field + 1.
An explicit ordered enum can have *wholes*::
type
TTokenType = enum
a = 2, b = 4, c = 89 # wholes are valid
However, it is then not an ordinal anymore, so it is not possible to use these
enums as an index type for arrays. The procedures ``inc``, ``dec``, ``succ``
and ``pred`` are not available for them.
Subrange types
~~~~~~~~~~~~~~
A subrange type is a range of values from an ordinal type (the host type).
To define a subrange type, one must specify it's limiting values: the highest
and lowest value of the type::
type
TSubrange = range[0..5]
``TSubrange`` is a subrange of an integer which can only hold the values 0
to 5. Assigning an other value to a variable of type ``TSubrange`` is a
checked runtime error (or static error if it can be statically
determined). Assignments from the base type to one of its subrange types
(and vice versa) are allowed.
An implemenation should give it the same size as its base type.
String type
~~~~~~~~~~~
All string literals are of the type string. A string in Nimrod is very
similar to a sequence of characters. However, strings in Nimrod both are
zero-terminated and have a length field. One can retrieve the length with the
builtin ``length`` procedure; the length never counts the terminating zero.
The assignment operator for strings always copies the string.
Strings are compared by their lexicographical order. All comparison operators
are available. String can be indexed like arrays (lower bound is 0). Unlike
arrays, they can be used in case statements::
case paramStr(i)
of "-v": incl(options, optVerbose)
of "-h", "-?": incl(options, optHelp)
else: write(stdout, "invalid command line option!\n")
Structured types
~~~~~~~~~~~~~~~~
A variable of a structured type can hold multiple values at the same time.
Stuctured types can be nested to unlimited levels. Arrays, sequences, records,
objects and sets belong to the structured types.
Array type
~~~~~~~~~~
Arrays are a homogenous type, meaning that each element in the array has the
same type. Arrays always have a fixed length which is specified at compile time
(except for open arrays). They can be indexed by any ordinal type. A parameter
may leave out the index type in the declaration making it an
*open array*. An open array ``A`` is always indexed by integers from 0 to
``length(A)-1``.
A sequence may be passed to a parameter that is of type *open array*, but
not to a multi-dimensional open array, because it is impossible to do so in an
efficient manner.
An array expression may be constructed by the array constructor ``[]``.
A constructed array is assignment compatible to a sequence.
Example::
type
TIntArray = array[0..5, int]
var
x: TIntArray
x = [1, 2, 3, 4, 5, 6] # this is the array constructor
The lower bound of an array may be received by the built-in proc
``low()``, the higher bound by ``high()``. The length may be
received by ``length()``.
Arrays are always bounds checked (at compile-time or at runtime). An
implementation may provide a means to disable these checks.
Sequence type
~~~~~~~~~~~~~
Sequences are similar to arrays but of dynamic length which may change
during runtime (like strings). A sequence ``S`` is always indexed by integers
from 0 to ``length(S)-1`` and its bounds are checked. Sequences can also be
constructed by the array constructor ``[]``.
Record and object types
~~~~~~~~~~~~~~~~~~~~~~~
A variable of a record or object type is a heterogenous storage container.
A record or object defines various named *fields* of a type. The assignment
operator for records and objects always copies the whole record/object. The
constructor ``[]`` can be used to initialize records/objects. A field may
be given a default value. Fields with default values do not have to be listed
in a record construction, all other fields have to be listed.
::
type
TPerson = record # type representing a person
name: string # a person consists of a name
age: int = 30 # and an age which default value is 30
var
person: TPerson
person = (name: "Peter") # person.age is its default value (30)
An implementation may align or even reorder the fields for best access
performance. The alignment may be specified with the `align`
pragma. If an alignment is specified the compiler shall not reorder the fields.
The difference between records and objects is that objects allow inheritance.
Objects have access to their type at runtime, so that the ``is`` operator
can be used to determine the object's type. Assignment from an object to its
parents' object leads to a static or runtime error (the
`EInvalidObjectAssignment` exception is raised).
::
type
TPerson = object
name: string
age: int
TStudent = object of TPerson # a student is a person
id: int # with an id field
var
student: TStudent
person: TPerson
student = (name: "Peter", age: 89, id: 3)
person = (name: "Mary", age: 17)
assert(student is TStudent) # is true
person = student # this is an error; person has no storage for id.
Set type
~~~~~~~~
The `set type` models the mathematical notion of a set. The set's
basetype can only be an ordinal type. The reason is that sets are implemented
as bit vectors. Sets are designed for high performance computing.
.. Note:: The sets module can be used for sets of other types.
Sets can be constructed via the set constructor: ``{}`` is the empty set. The
empty set is type combatible with any special set type. The constructor
can also be used to include elements (and ranges of elements) in the set::
{'a'..'z', '0'..'9'} # This constructs a set that conains the
# letters from 'a' to 'z' and the digits
# from '0' to '9'
These operations are supported by sets:
================== ==========================================================
operation meaning
================== ==========================================================
A + B union of two sets
A * B intersection of two sets
A - B difference of two sets (A without B's elements)
A == B set equality
A <= B subset relation (A is subset of B or equal to B)
A < B strong subset relation (A is a real subset of B)
e in A set membership (A contains element e)
A >< B symmetric set difference (= (A - B) + (B - A))
card(A) the cardinality of A (number of elements in A)
incl(A, elem) same as A = A + {elem}, but may be faster
excl(A, elem) same as A = A - {elem}, but may be faster
================== ==========================================================
Reference type
~~~~~~~~~~~~~~
References (similiar to `pointers` in other programming languages) are a way to
introduce many-to-one relationships. This means different references can point
to and modify the same location in memory. References should be used sparingly
in a program. They are only needed for constructing graphs.
Nimrod distinguishes between *traced* and *untraced* references. Untraced
references are also called `pointers`. The difference between them is that
traced references are garbage collected, untraced are not. Thus untraced
references are *unsafe*. However for certain low-level operations (accessing
the hardware) untraced references are unavoidable.
Traced references are declared with the **ref** keyword, untraced references
are declared with the **ptr** keyword.
The ``^`` operator can be used to derefer a reference, the ``addr`` procedure
returns the address of an item. An address is always an untraced reference.
Thus the usage of ``addr`` is an *unsafe* feature.
The ``.`` (access a record field operator) and ``[]`` (array/string/sequence
index operator) operators perform implicit dereferencing operations for
reference types::
type
PNode = ref TNode
TNode = record
le, ri: PNode
data: int
var
n: PNode
new(n)
n.data = 9 # no need to write n^.data
As can be seen by the example, reference types are the only types that can be
used in *implicit forward declarations*: TNode may be used before it is
defined, because only a refence to it is needed.
To allocate a new traced object, the built-in procedure ``new`` has to be used.
To deal with untraced memory, the procedures ``alloc``, ``dealloc`` and
``realloc`` can be used. The documentation of the system module contains
further information.
Special care has to be taken if an untraced object contains traced objects like
traced references, strings or sequences: In order to free everything properly,
the built-in procedure ``finalize`` has to be called before freeing the
untraced memory manually!
.. XXX finalizers for traced objects
Procedural type
~~~~~~~~~~~~~~~
A procedural type is internally a pointer to procedure. Thus ``nil`` is an
allowed value for variables of a procedural type. Nimrod uses procedural types
to achieve `functional` programming techniques. Dynamic dispatch for OOP
constructs can also be implemented with procedural types.
Example::
type
TCallback = proc (x: int) {.cdecl.}
proc printItem(x: Int) = ...
proc forEach(c: TCallback) =
...
forEach(printItem) # this will NOT work because calling conventions differ
A subtle issue with procedural types is that the calling convention of the
procedure influences the type compability: Procedural types are only compatible
if they have the same calling convention.
Altough a Nimrod implementation may provide additional calling conventions
the following shall always exist:
``cdecl``
is used for interfacing with C; indicates that a proc shall
use the same calling convention as the C compiler.
``inline``
indicates that the caller of the proc should not call it,
but rather inline its code in place for improved efficiency. Note that
this is only a hint for the compiler: It may completely ignore it and
it may inline procedures that are not marked as ``inline``.
``closure``
indicates that the procedure expects a context, a `closure` that needs
to be passed to the procedure.
Statements
----------
Nimrod uses the common statement/expression paradigma: Statements do not
produce a value in contrast to expressions. Call expressions are statements.
If the called procedure returns a value, it is not a valid statement
as statements do not produce values. To evaluate an expression for
side-effects and throwing its value away, one can use the ``discard``
statement.
Statements are separated into `simple statements` and `complex statements`.
Simple statements are statements that cannot contain other statements, like
assignments, calls or the ``return`` statement; complex statements can
contain other statements. To avoid the `dangling else problem`, complex
statements always have to be intended::
XXX
Discard statement
~~~~~~~~~~~~~~~~~
Syntax::
discard_stmt ::= discard expr
The `discard` statement evaluates its expression for side-effects and throws
the expression's resulting value away. If the expression has no side-effects,
this shall generate at least a warning from the compiler.
Var statement
~~~~~~~~~~~~~
Syntax::
varlist ::= identlist [asgn_opr expr doc]
var_section ::= var varlist
| var INDENT(x > I[-1]) PUSH(x) varlist
{ INDENT(x) varlist } POP
`Var` statements simply declare new local and global variables and may
initialize them. A comma seperated list of variables can be used to specify
variables of the same type::
var
a: int = 0
x, y, z: int
However, an initializer is not allowed for such a list as its semantic
would be ambigious in some cases. If an initializer is given the type
can be omitted: The variable is of the same type as the initializing
expression.
If statement
~~~~~~~~~~~~
Syntax::
if_stmt ::= PUSH(x = I[-1]) if expr ":" stmts
{ INDENT(x) elif expr ":" stmts }
[ INDENT(x) else ":" stmts ]
POP
The `if` statement is a simple way to make a branch in the control flow:
The expression after the keyword ``if`` is evaluated, if it is true
the corresponding statements after the ``:`` are executed. Otherwise
the expression after the ``elif`` is evaluated (if there is an
``elif`` branch), if it is true the corresponding statements after
the ``:`` are executed. This goes on until the last ``elif``. If all
conditions fail, the ``else`` part is executed. If there is no ``else``
part, execution continues with the statement after the ``if`` statement.
Case statement
~~~~~~~~~~~~~~
Syntax::
case_stmt ::= PUSH(I[-1]) case expr
INDENT(x>=I[-1]) of vallist ":" stmts
{ INDENT(x) of vallist ":" stmts }
{ INDENT(x) elif expr ":" stmts }
[ INDENT(x) else ":" stmts ]
POP
The `case` statement is similar to the if statement, but it represents
a multi-branch selection. The expression after the keyword ``case`` is
evaluated and if its value is in a *vallist* the corresponding statements
(after the ``of`` keyword) are executed. If the value is no given *vallist*
the ``else`` part is executed. If there is no ``else`` part and not all
possible values that ``expr`` can hold occur in a ``vallist``, a static
error shall be given. This holds only for expressions of ordinal types.
If the expression is not of an ordinal type, and no ``else`` part is
given, control just passes after the ``case`` statement.
To suppress the static error in the ordinal case the programmer needs
to write an ``else`` part with a ``nil`` statement.
When statement
~~~~~~~~~~~~~~
Syntax::
when_stmt ::= PUSH(x=I[-1]) when expr ":" stmts
{ INDENT(x) elif expr ":" stmts }
[ INDENT(x) else ":" stmts ]
POP
The `when` statement is almost identical to the ``if`` statement with some
exceptions:
* Each ``expr`` has to be a constant expression of type ``bool``.
* The statements do not open a new scope if they introduce new identifiers.
* The statements that belong to the expression that evaluated to true are
translated by the compiler, the other statements are not checked for
syntax or semantics at all! This holds also for any ``expr`` coming
after the expression that evaluated to true.
The ``when`` statement enables conditional compilation techniques. As
a special syntatic extension, the ``when `` construct is also available
within ``record`` or ``object`` definitions.
Raise statement
~~~~~~~~~~~~~~~
Syntax::
raise_stmt ::= raise [qualified_identifier [comma expr]]
Apart from built-in operations like array indexing, memory allocation, etc.
the ``raise`` statement is the only way to raise an exception. The
identifier has to be the name of a previously declared exception. A
comma followed by an expression may follow; the expression must be of type
``string`` or ``cstring``; this is an error message that can be extracted
with the `getCurrentExceptionMsg` procedure in the module ``system``.
If no exception name is given, the current exception is `re-raised`. The
`ENoExceptionToReraise` exception is raised if there is no exception to
re-raise. It follows that the ``raise`` statement *always* raises an
exception.
Try statement
~~~~~~~~~~~~~
Syntax::
try_stmt ::= PUSH(x=I[-1]) try ":" stmts
{ INDENT(x) except exceptlist ":" stmts }
[ INDENT(x) except ":" stmts ]
[ INDENT(x) finally ":" stmts ]
POP
The statements after the ``try`` are executed in sequential order unless
an exception ``e`` is raised. If the exception type of ``e`` matches any
of the list ``exceptlist`` the corresponding statements are executed.
The statements following the ``except`` clauses are called
`exception handlers`.
The empty ``except`` clause is executed if there is an exception that is
in no list. It is similiar to an ``else`` clause in ``if`` statements.
If there is a ``finally`` clause given, it is always executed after the
exception handlers.
The exception is *consumed* in an exception handler. However, an
exception handler may raise another exception. If the exception is not
handled, it is propagated through the call stack. This means that often
the rest of the procedure - that is not within a ``finally`` clause -
is not executed (if an exception occurs).
Block statement
~~~~~~~~~~~~~~~
Syntax::
block_stmt ::= block [IDENTIFIER] ":" stmts
The block statement is a means to group statements to a (named) `block`.
Inside the block, the ``break`` statement is allowed to leave the block
immediately. A ``break`` statement can contain a name of a surrounding
block to specify which block is to leave.
Break statement
~~~~~~~~~~~~~~~
Syntax::
break_stmt ::= break [IDENTIFIER]
The break statement is used to leave a block immediately. If ``IDENTIFIER``
is given, it is the name of the enclosing block that is to leave. If it is
absent, the innermost block is leaved.
While statement
~~~~~~~~~~~~~~~
Syntax::
while_stmt ::= WHILE expr COLON stmts
The `while` statement is executed until the ``expr`` evaluates to false.
Endless loops are no error. ``while`` statements open an `implicit block`,
so that they can be aborted by a ``break`` statement.
For statement & iterators
~~~~~~~~~~~~~~~~~~~~~~~~~
Syntax::
for_stmt ::= PUSH(x=I[-1]) for exprlist in expr [".." expr] ":" stmts
POP
The `for` statement is an abstract mechanism to iterate over the elements
of a container. It relies on an *iterator* to do so. Like ``while``
statements, ``for`` statements open an `implicit block`, so that they
can be aborted by a ``break`` statement.
XXX
Assembler statement
~~~~~~~~~~~~~~~~~~~
Syntax::
asm_stmt ::= asm [CHAR_LITERAL] STRING_LITERAL
The direct embedding of assembler code into Nimrod code is supported by the
unsafe ``asm`` statement. Identifiers in the assembler code that refer to
Nimrod identifiers shall be enclosed in a special character which can be
specified right after the ``asm`` keyword. The default special character is
``'!'``. An implementation does not need to support the assembler statement,
giving a static error if it encounters one.
Modules
-------
Nimrod supports splitting a program into pieces by a module concept. Modules
make separate compilation possible. Each module needs to be in its own file.
Modules consist of an interface and an implementation section. The interface
section lists the symbols that can be imported from other modules. Thus modules
enable `information hiding`. The interface section may not contain any
code that is executable. This means that only the headers of procedures can
appear in the interface. A module may gain access to symbols of another module
by the `import` statement. Recursive module dependancies are allowed, but
slightly subtle.
The algorithm for compiling modules is:
- Compile the whole module as usual, following import statements recursively
- if we have a cycle only import the already parsed symbols (in the interface
of course); if an unknown identifier occurs then abort
This is best illustrated by an example::
# Module A
interface
type
T1 = int
import B # the compiler starts parsing B
implementation
proc main() =
var i = p(3) # works because B has been parsed completely here
main()
# Module B
interface
import A # A is not parsed here! Only the already known symbols
# of A are imported here.
proc p(x: A.T1): A.T1 # this works because the compiler has already
# added T1 to A's interface symbol table
implementation
proc p(x: A.T1): A.T1 = return x + 1
Scope rules
-----------
Identifiers are valid from the point of their declaration until the end of
the block in which the declaration occurred. The range where the identifier
is known is the `scope` of the identifier. The exact scope of an identifier
depends on the way it was declared.
Block scope
~~~~~~~~~~~
The *scope* of a variable declared in the declaration part of a block
is valid from the point of declaration until the end of the block. If a
block contains a second block, in which the identifier is redeclared,
then inside this block, the second declaration will be valid. Upon
leaving the inner block, the first declaration is valid again. An
identifier cannot be redefined in the same block, except if valid for
procedure or iterator overloading purposes.
Record or object scope
~~~~~~~~~~~~~~~~~~~~~~
The field identifiers inside a record or object definition are valid in the
following places:
* To the end of the record definition
* Field designators of a variable of the given record type.
* In all descendent types of the object type.
Module scope
~~~~~~~~~~~~
All identifiers in the interface part of a module are valid from the point of
declaration, until the end of the module. Furthermore, the identifiers are
known in other modules that import the module. Identifiers from indirectly
dependent modules are *not* available. Identifiers declared in the
implementation part of a module are valid from the point of declaration to
the end of the module. The ``system`` module is automatically imported in
all other modules.
If a module imports an identifier by two different modules,
each occurance of the identifier has to be qualified, unless it is an
overloaded procedure or iterator in which case the overloading
resolution takes place::
# Module A
interface
var x: string
# Module B
interface
var x: int
# Module C
import A, B, io
write(stdout, x) # error: x is ambigious
write(sdtout, A.x) # no error: qualifier used
Messages
========
A Nimrod compiler has to emit different kinds of messages: `hint`,
`warning`, and `error` messages. `errors` have to be emitted if the compiler
encounters any static errors. If and when the other two message kinds are
emitted is not specified, unless a message is requested with the
``hint`` or ``warning`` pragma.
Pragmas
=======
Pragmas are Nimrod's method to give the compiler additional information/
commands without introducing a massive number of new keywords. Pragmas are
processed on the fly during parsing. Pragmas are always enclosed in the
special ``{.`` and ``.}`` curly brackets. There are a number of pragmas
that a compiler has to process; a compiler may define additional pragmas
not specified here.
define pragma
-------------
The `define` pragma defines a conditional symbol. This symbol may only be
used in other pragmas and in the ``defined`` expression and not in ordinary
Nimrod source code. The conditional symbols go into a special symbol table.
The compiler shall define the target processor and the target operating
system as conditional symbols. See `Annex A <XXX>`_ for a list of specified
processors and operating systems. The Syntax of the define pragma is::
define_pragma ::= curlydot_le "define" colon IDENTIFIER curlydot_ri
undef pragma
------------
The `undef` pragma the counterpart to the define pragma. It undefines a
conditional symbol. Syntax::
undef_pragma ::= curlydot_le "undef" colon IDENTIFIER curlydot_ri
error pragma
------------
The `error` pragma is used to make the compiler output an error message with
the given content. Compilation may abort after an error (or not). Syntax::
error_pragma ::= curlydot_le "error" colon STRING_LITERAL curlydot_ri
fatal pragma
------------
The `fatal` pragma is used to make the compiler output an error message with
the given content. In contrast to the ``error`` pragma, compilation
is guaranteed to be aborted by this pragma. Syntax::
fatal_pragma ::= curlydot_le "fatal" colon STRING_LITERAL curlydot_ri
warning pragma
--------------
The `warning` pragma is used to make the compiler output a warning message with
the given content. Compilation continues after the warning. Syntax::
warning_pragma ::= curlydot_le "warning" colon STRING_LITERAL curlydot_ri
hint pragma
-----------
The `hint` pragma is used to make the compiler output a hint message with
the given content. Compilation continues after the hint. Syntax::
hint_pragma ::= curlydot_le "hint" colon STRING_LITERAL curlydot_ri
compilation option pragmas
--------------------------
The listed pragmas here can be used to override the code generation options
for a section of code.
::
"{." pragma: val {pragma: val} ".}"
An implementation should provide at least the following possible options (it can
add various others). If an implementation does not recognize the option, a
warning shall be given to the user.
=============== =============== ============================================
pragma allowed values description
=============== =============== ============================================
checks on|off Turns the code generation for all runtime
checks on or off.
bound_checks on|off Turns the code generation for array bound
checks on or off.
overflow_checks on|off Turns the code generation for over- or
underflow checks on or off.
nil_checks on|off Turns the code generation for nil pointer
checks on or off.
assertions on|off Turns the code generation for assertions
on or off.
warnings on|off Turns the warning messages of the compiler
on or off.
hints on|off Turns the hint messages of the compiler
on or off.
optimization none|speed|size Optimize the code for speed or size, or
disable optimization. For non-optimizing
compilers this option has no effect.
Neverless they must parse it properly.
callconv cdecl|... Specifies the default calling convention for
all procedures (and procedure types) that
follow.
=============== =============== ============================================
Example::
{.checks: off, optimization: speed.}
# compile without runtime checks and optimize for speed
push and pop pragmas
--------------------
The push/pop pragmas are very similar to the option directive,
but are used to override the settings temporarily. Example::
{.push checks: off.}
# compile this section without runtime checks as it is
# speed critical
# ... some code ...
{.pop.} # restore old settings
Annex A: List of conditional symbols
====================================
``posix`` is defined on any POSIX compatible operating system.
+----------------------------+-----------------+
| Operating System | Symbols |
+============================+=================+
| AIX | aix, posix |
+----------------------------+-----------------+
| Compaq Tru64 UNIX | tru64, posix |
+----------------------------+-----------------+
| Digital UNIX | tru64, posix |
+----------------------------+-----------------+
| OSF/1 | tru64, posix |
+----------------------------+-----------------+
| FreeBSD | freebsd, |
| | posix, |
| | bsd |
+----------------------------+-----------------+
| GNU/Linux | linux, |
| | posix |
+----------------------------+-----------------+
| HP-UX | hpux, |
| | posix |
+----------------------------+-----------------+
| Irix | irix, |
| | posix |
+----------------------------+-----------------+
| MacOS X | macosx, |
| | posix |
+----------------------------+-----------------+
| NetBSD | netbsd, |
| | posix, |
| | bsd |
+----------------------------+-----------------+
| OpenBSD | openbsd, |
| | posix, |
| | bsd |
+----------------------------+-----------------+
| Solaris | solaris, |
| | posix |
+----------------------------+-----------------+
| Windows (all variants) | windows |
+----------------------------+-----------------+
+----------------------------+-----------------+
| Processor | Symbols |
+============================+=================+
| Compaq Alpha | alpha |
+----------------------------+-----------------+
| HP Precision Architecture | hppa |
+----------------------------+-----------------+
| INTEL x86 | x86 |
+----------------------------+-----------------+
| AMD/INTEL x86 64bit | x86_64, |
| | amd64 |
+----------------------------+-----------------+
| MIPS RISC | mips |
+----------------------------+-----------------+
| IBM Power PC | powerpc |
+----------------------------+-----------------+
| SPARC | sparc |
+----------------------------+-----------------+
| MicroSPARC | sparc |
+----------------------------+-----------------+
| UltraSPARC | sparc |
+----------------------------+-----------------+
On targets for 16, 32 or 64 bit processors the symbols ``cpu16``, ``cpu32``
or ``cpu64`` shall be defined respectively. On little endian machines the
symbol ``little_endian`` and on big endian ones the symbol ``big_endian``
are defined.