Skip to content
Leonardo Laguna Ruiz edited this page Aug 23, 2017 · 12 revisions

Comments

// Line Comment

/* Block Comment */

/* /* Nested Comment */ */

Literals and builtin types

// Unit (or empty value)
val u : unit  = ();

// Integers
val x : int = 0;
val y : int = 1;
val z : int = 3942;

// Real (floating or fixed point)
val a : real = 1.0;
val b : real = 2.0;
val c : real = 3.1416;

// Booleans
val k : bool = true;
val l : bool = false;

Numbers without the decimal dot are integers and cannot be mixed with real numbers.

Casting values

Conversion between int and real needs to be explicit.

val x : int = 1 + int(1.7);
val y : real = 1.0 + real(1);

Operators

Operators require that both arguments are the same type.

Arithmetic (for int and real types)

- Addition       : '+'
- Subtraction    : '-'
- Multiplication : '*'
- Division       : '/'
- Modulo         : '%'

e.g.

val x = 1 + 2;
val z = -1.0; // Unary minus
val y = 3.8 * 56.0;

Logic operators (for bool type)

- And : '&&'
- Or  : '||'
- Not : 'not' // 'not' is a function

e.g.

val l = not(1.0 > 3.0) || false;

Relational operators (for int and real types)

- Equal            : '=='
- Unequal          : '<>'
- Less than        : '<'
- Greater than     : '>'
- Less than Eq.    : '<='
- Greater than Eq. : '>='

Expressions

If-expressions

If expressions require an else branch.

val x = if y > 0.0 then 3.0 else 4.0; 

Tuples

val x,y,z = 1,2,3;
val (x,y,z) : (int,int,int) = 1,2,3;
val point : (real,real,real) = 0.0, 1.0, 2.0;

Arrays

Array types are in the form array(type,size) e.g. array(int,3) it's an array of 3 integers.

val x = [1,2,4];
val y : array(real,3) = [1.0, 2.0, 4.0];

Arrays can also be declared with [], but this will only define the size of the array. In order to infer the type the array has to be used.

val z[3]; // at this point we know that the array is of size 3
z[0] = 1; // now we know that the types of the array are int

Function calls

Function calls are special in Vult since they create implicit context. These context can be anonymous or named.

val x = foo();     // anonymous context
val y = bob:foo(); // context with name 'bob'
val z = bob:foo(); // this call is performed in the context 'bob' 

Statements

Variable declarations

When declaring a variable you can either provide a type or let the type inference decide on the type.

val x = 0;          // The type is inferred to be 'int'
val y : real = 1.0; // The type is specified to be 'real'

Memory variables

The value of variable declarations is lost when the function returns. On the contrary, the value of memory variables in stored in the function context.

// mem variables can reference themselves and they are always initialized to zero
mem x = x + 1;

// you can specify the type as well
mem y : real = 0.0;

Assignments

Once a variable is declared you can change it's value with =. The type of a variable cannot be changed.

val x = 0;
x = 1;

// Assigning tuples

a,b,c = 0,1,2;

If you want to ignore a value you have to assign it to _.

val x,_ = foo(); // 'foo' returns a tuple

IMPORTANT: calling a function that does not returns a value needs to be assigned to _ as follows.

_ = bar();

If-statements

If-statements may not have else branch and if the body is a single statement curly braces are not required.

// simple if-statement
if(x > 0)
   return 0;

if(y > 0) {
   y = 0;
   x = 1;
}
else {
   y = 1;
   x = 2;
}

Loops

There is a single kind of loop: while. A while loop can be used to get a for loop. For example:

fun foo() {
   val i = 0;
   val acc = 0;
   while(i < 10) {
      acc = acc + i;
      i = i + 1;
   }
}

Function declarations and Return

Functions are defined with the keyword fun and return values with return.

fun add(a, b) {
   return a+b;
}

Functions may include type definitions.

fun add(a:int, b:int) : int {
   return a+b;
}

Functions that do not return any value have type unit.

fun foo() : unit {
}

Functions sharing context

When functions declare memory variables that need to be accessed from other functions they need to be linked with the word and. For example, when making a counter you may need a function to reset it.

fun counter() {
   mem x = x + 1;
   return x; 
}
and reset() {
   x = 0;
}

fun test() {
   val a = c:counter(); // a = 1
   val b = c:counter(); // b = 2
   _ = c:reset();       // counter goes back to zero
   val d = c:counter(); // d = 1

}

You can notice that it is not necessary to declare the memory variable x in the function reset. In fact, mem variables can be declared anywhere in the function, for example:

fun foo(){
   mem x : int;
   mem y : real;
   mem z : array(real,3);
}
and bar() {
}

The code above is equivalent to the next:

fun foo(){
   mem x : int;
}
and bar() {
   mem y : real;
   mem z : array(real,3);
}

The memory variables x, y and z exists in the context of foo() and bar().

External functions

Functions declared as external, during code generation all calls are replaced to the given function. The types for the function need to be explicitly defined.

external foo(x:int) : int "actual_foo";

In the example above, a call like foo(0) becomes actual_foo(0).

Initial values

All variables (if the type can be inferred) are initialized to a default value (zero in the case of numeric values). If the variable is an array, every value will be initialized to it's default value. For example:

val a : int;           // No initial value given, therefore a = 0
val b : real;          // b = 0.0
val c : array(int,3);  // c = [0, 0, 0]
val d : array(real,3); // d = [0.0, 0.0, 0.0]
val e;                 // If the type is not inferred, it will produce an error

The same applies for mem variables. I you want to initialize mem variables to a different value than the default you can use the @[init] tag to define an initialization function (see section Custom initialization).

Builtin functions

Array access

Reading and writing an array can be done through the functions get and set.

val x = get(array, index); // similar to val x = array[index];
val _ = set(array, index, value); // similar to array[index] = value;

The size of arrays can be obtained with the function size.

val x = size(array);

Mathematical functions

The following mathematical operations are available for real numbers.

- abs   : absolute
- exp   : exponential
- sin   : sine
- cos   : cosine
- floor : floor
- tanh  : hyperbolic tangent
- tan   : tangent
- sqrt  : square root

Random numbers

- random : real type random number between 0.0 to 1.0
- irandom : integer type random number between 0 to 2^32 (use as irandom() % N to define a range between 0 to N)

Debug functions

- log : prints a value finishing with a new line (int, real, bool or string). 

Constants

The following constants are provided as functions.

- eps() : returns the minimal value that can be represented with fixed-point numbers.

Tags

Tags can be attached to functions to define specific behaviours e.g. custom initialisation or creation of tables.

Custom initialisation

By default all mem variables are initialised to zero. In some cases is necessary to initialise the variables to a different value. In order to do that, you can attach the tag @[init] to a function.

fun counter() {
   mem count = count + 1;
   return count;
}
and start() @[init] {
   count = 10;  // the first value of count will be 10
}

Creation of tables

When a function is expensive to compute or when you want to create wave tables you can use the @[table()] tag.

fun expensive(x) @[table(size=128, min=0.0, max=1.0)] {
   return exp(x * x) * tanh(x) / (x * x + 1);
}

To use the @[table()] tag you need to provide the size of the table and the range of the input. The function will be replaced by a new function performing second order interpolations among a number of segments equal to the size of the table.

For example, to create a wave table for the normalised sine function (range 0-1 instead of 0 to 2Pi) we can do it as follows:

fun sine_wave(x) @[table(size=128, min=0.0, max=1.0)] {
   return sine(2.0 * 3.1415 * x);
}

Tables can only be created for function taking as input one real argument and returning one real value.

Embedding WAV files

It's possible to take a WAV file and import it as a Vult array. This is useful when you want to embed wave tables or impulse response files.

To embed a file named wave.wav you need to add the tag @[wave()] to an external function as follows:

external mywave(channel:int, index:int) : real @[wave(channels=1, file="wave.wav")];

The function takes as arguments the channel and the sample number. In the @[wave()] tag you need to provide the number of channels of the file (as an integer number) and provide as a string the file name. The file will be searched in the implicit location (the directory of the current file) and among the extra include directories provided. The supported format is PCM 16 or 24 bits.

To use the embedded data you can call the external function as follows:

fun fun() {
   val channel = 0; // first channel
   val sample = 0;  // first sample
   return mywave(channel, sample);
}

The access to the data is circular, if you try to access a sample outside the range the mod operation is used to fix it.

Additionally, a function with the name <name>_samples() is declared that gives you the size of the array. For example, if you want to play the file in a loop:

external mywave(channel:int, index:int) : real @[wave(channels=1, file="wave.wav")];

fun index() {
   mem i = (i + 1) % mywave_samples(); // mywave_samples() is generated when embedding the wave file
   return i;
}

fun play() {
   return mywave(0, index());
}