Arrp Language Guide

This document walks through the syntactical elements of an Arrp program, and describes their semantics.

See also the Formal syntax specification. This document provides links into the formal specification whenever a syntactical element is introduced.

Table of contents

Modules

A module is the smallest unit of compilation, residing in a file or provided to the compiler through standard input.

A module consists of different kinds of declarations:

See Grammar: module.

module example;

import lib;

input iota :: int;

external eta :: int -> real64;

gamma(x) = x + 1;

main :: real64;
main = lib.alpha(1) + eta(2) - gamma(iota);

Default module name

The module name declaration is optional. If it is missing, the module gets the default name "m".

Imported modules

A module import declaration causes the imported module to be compiled, and the global names in that module become available in the current module.

The imported module must reside in a file named "module-name.arrp". The file must either be adjacent to the location of the importing module, or in one of the import directories given as an option to the compiler.

Using names from imported modules

The names in the imported module are available in the importing module using qualified identifiers (see Grammar: qualified-id):

Input and output

The output of an Arrp program is the value of the global name "main", which must be present.

Inputs must be explicitly declared, along with their type (see Grammar: inputs).

In the following example, two inputs are declared: an integer named "a" and an array named "b" with 64-bit floating point elements and two dimensions - the first infinite and the second of size 10:

input a :: int;
input b :: [~,10]real64;

How input and output data is exchanged with the environment depends on the target form of compilation. See also the page on the C++ target.

Values and functions

Value declarations define values and functions and bind them to names:

pi = 3.14159265359;

area_of_circle(r) = pi * r * r;

area_of_right_triangle(a, b) = a * b / 2;

a = area_of_right_triangle(10, 5);

See Grammar: value-decl

The scope of globally declared names is the entire module. They are mutually recursive.

Local declarations

Names with scope local to an expression can be declared using the "let" and "where" expressions:

f(x) = let y = x * x in sin(y) + cos(y);

g(x) = sin(y) + cos(y) where y = x * x;

See Grammar: let-expr, Grammar: where-expr

Function application and polymorphism

A function is applied when followed by arguments in parenthesis (see Grammar: func-application).

Partial application is supported: a function applied to less arguments than it has parameters results in another function with the remaining parameters.

All functions are polymorphic: they can be applied to arguments of any type consistent with the expression of the function.

Lambda functions

An unnamed function (a "lambda abstraction") can be defined as part of another expression:

f(g) = g(1,2) + g(3,4);

a = f(\x, y -> x * y);

See Grammar: lambda.

Type annotations

Occasionally, you may want to provide an explicit type declaration for a name:

y :: real64;
y = 1 + f(x);

See Grammar: type-decl.

The type of a name can not be changed using a type declaration - the declared type must match the type of the name's expression.

Recursion

Recursion is allowed for the purpose of recursive arrays. However, recursive use of functions is not supported. Some common uses of functional recursion can be achieved with recursive arrays. See Recursive arrays.

Due to a current limitation, the Arrp compiler requires a type annotation for at least one name in any recursive set of names.

External functions

An external function is declared with a name and a type, and it allows the name to be used as any other function, except that it is not polymorphic:

external transform :: [100]real64 -> [50]real64;
x = [100: i -> sin(i/100)];
y = transform(x);

See Grammar: external-decl.

Precisely how an external function is executed depends on the target form of compilation. See also the page on the C++ target.

Arrays

Arrays are a special kind of functions that map integers to values of some primitive type. Each formal parameter of such a function also called a dimension, and an actual parameter (argument) is also called an index. A multi-parameter function is therefore a multi-dimensional array.

An array dimension ranges from 0 up to (and excluding) some number called array size. However, the size of one dimension can be infinite. Such an array is called a signal.

We represent a sequence of indices or array sizes for multiple dimensions with a tuple: <x,y,...>.

Array definition

An array can be defined with an expression that specifies the array size and a list of mappings from indices to values (see Grammar: array-func):

a = [5,10:
    x,0 -> 0;
    x,y -> x/y;
];

This is a 2-dimensional array with size <5,10>, and it maps all indices where the second dimension equals 0 to the value 0, and all other indices of form <x,y> to the value x/y.

Signals

A signal (an infinite array) is defined by using ~ in place of the array size:

s = [~,5: t,i -> sin(t*i)];

Alternatively, an infinite array size is assumed when no size is given. Mind that only a single infinite dimension is allowed:

s = [t -> sin(t)];

Index patterns and guards

Each index-value mapping begins with an index pattern (see Grammar: index-mapping). Each pattern element is either a literal index value for which the pattern is valid, or a name of a variable representing any index value which can be used in the mapped expression.

A mapping is only effective for indices not matched by any pattern of the preceding mappings. Any index within the size of the array must be matched by at least one pattern.

An index mapping can be further split by index guards (see Grammar: index-mapping). An index guard is a boolean expression on array index variables. The mapped expression is effective only for indices where the expression is true. Guarded expressions must be followed by an expression without a guard that is effective for all indices not matched by any guard.

The following signal - defined using guards - has every third element equal to 1, and all others equal to 0:

s = [t | t % 3 == 0 -> 1 | 0];

A guarded mapping is only effective for indices matched by its pattern, and not matched by guards and patterns of any preceding mappings.

An index guard must be a quasi-affine expression in index variables and constant integers. This means it can only contain:

Nested arrays

Array definitions can be nested, resulting in an array with combined dimensions:

a = [t -> [5: i -> sin(t*i)]];

The array "a" is a 2-dimensional array with size <t, 5>.

Array application

An array is applied when followed by indices in square brackets (see Grammar: array-application):

a = [5,10: x,y -> x+y];
b = a[3,4];

An array can be partially applied, just like an ordinary function, which results in an array with the remaining dimensions:

a = [5,10: x,y -> x+y];
b = a[3];

Recursive arrays

A recursive array is defined using the name bound to the array expression inside the expression itself, or using the keyword "this":

a :: [~]real64;
a = [0 -> 0; t -> a[t-1] * 0.8];
b = [0 -> 0; t -> this[t-1] * 0.8];

Multiple arrays can be mutually recursive:

a :: [~]real64;
a = [0 -> 0; t -> b[t-1] * 0.8];
b = [t -> a[t] + 1];

Array enumeration

An array can be defined by listing values of each of its elements (see Grammar: array-enum):

x = 100;
a = [4; x*3; [-1; -2]; x+2];

Array concatenation

Two arrays can be concatenated (see Grammar: array-concat):

a = [5: i -> i] ++ [10: i -> i^2];

Combined with recursion:

a :: [~]int;
a = [0; 1] ++ [t -> a[t] * 2];

Or shorter:

a :: [~]int;
a = [0; 1] ++ a*2;

Primitive values

Arrp supports the following primitive types:

Literal values can be created for the following types: boolean, int, real64, complex64 (see Grammar: literal).

An expression can be converted to other primitive types (see Type Conversion).

Built-in operations and functions

Built-in operations and functions operate on primitive types. They can also be applied to arrays, pointwise.

The following tables list relations between argument types and result types, and additional conditions on argument types. In addition to type names, the following forms have special meanings:

If the types of arguments do not satisfy the conditions, argument promotion is attempted. Only the least number of promotions to satisfy the conditions are done. If there are multiple satisfiable sequences of promotions with equal length, the expression is ambiguous and the program is ill-defined.

The following type promotions are attempted:

Unary and binary operators

expression result conditions comments
!bool bool
bool || bool bool
bool && bool bool
bool && bool bool
a == a bool
a != a bool
a < a bool a : simple-numeric
a <= a bool a : simple-numeric
a > a bool a : simple-numeric
a >= a bool a : simple-numeric
a + a bool a : numeric
a - a bool a : numeric
a * a bool a : numeric
a / a bool a : real or complex Floating point or complex division.
a // a bool a : simple-numeric Integer division.
int % int int Remainder.
a ^ a a a : simple-numeric Exponentiation.

Math functions

expression result conditions comments
exp(a) a a : real or complex
exp2(a) a a : int or real
log(a) a a : real or complex
log2(a) a a : real
log10(a) a a : real or complex
sqrt(a) a a : real or complex
sin(a) a a : real or complex
cos(a) a a : real or complex
tan(a) a a : real or complex
asin(a) a a : real or complex
acos(a) a a : real or complex
atan(a) a a : real or complex
ceil(a) a a : int or real
floor(a) a a : int or real
abs(a) a a : int or real
min(a,a) a a : int or real
max(a,a) a a : int or real

Complex number functions

expression result conditions comments
real(complex32) real32 Real part of complex number.
real(complex64) real64 Real part of complex number.
imag(complex32) real32 Imaginary part of complex number.
imag(complex64) real64 Imaginary part of complex number.

Type conversion functions

expression result conditions comments
int(a) int a : simple-numeric
real32(a) real32 a : simple-numeric
real64(a) real64 a : simple-numeric
complex32(a) complex32 a : numeric
complex64(a) complex64 a : numeric

Pointwise operations and broadcasting

Pointwise application of primitive operations

When a primitive operation is applied to arrays, it is performed pointwise on array elements, given that the array sizes are compatible:

a = [5: i -> i] + [5: i -> 10*i];

Broadcasting

Two array sizes are compatible if they are equal in all common dimensions. The result of the operation will have the size of the array with more dimensions.

If an argument has less dimensions than the result, the result at some index will use the value of the argument at an index equal in the common dimensions.

In addition, a primitive value is compatible with an array of any size, and will be used for all the result values.

For example, the array "a" can be multiplied with a constant value (b), with an array of equal size (c), or an array with more dimensions (d):

a = [t -> sin(t*0.2)];
b = a * 0.1;
c = a * [t -> sin(t*0.01)];
d = a * [t -> [10: i -> sin(t*0.1/i)]];

Broadcasting in indexing

An array or primitive value can be indexed with more indices than it has dimensions, resulting in the array value at an index with equal common dimensions.

For example, the function "f" can be applied to a primitive value (a), in which case k[t] is a constant expression. But it can also be applied to a signal (b) so that k[t] is variable:

f(k) = [t -> sin(t * k[t])];
a = f(0.1);
b = f([t -> sin(t * 0.01)]);

Broadcasting in array definition

Broadcasting also applies to individual mappings in an array definition. If the mapped values are arrays of different sizes, the result reuses values of arrays with smaller number of dimensions just like in a primitive operation:

a = [
  0 -> 1;
  t -> [10: i -> t + i]
];