VeLa - AAVSO/VStar GitHub Wiki

Introduction

VeLa (VStar expression Language) is a general purpose programming language that, within VStar, can be used to:

  • specify a numeric expression wherever a number can be used as input;
  • create complex observation filter and search expressions;
  • specify model functions;
  • transform observations.

For examples of how VeLa is used in VStar, see Contextual examples.

VeLa is a simple functional language with:

  • integer, real, string, boolean, list, function types
  • variables that take on the type of the value to which they are first bound via the <- (assignment) operator (e.g. n <- 1)
  • named constants via the is operator (e.g. π is 3.1415926)
  • case insensitive, Unicode keyword, variable, and function names
  • statically typed, recursive, higher order, named or anonymous functions that may be overloaded by parameter type
  • selection (when) and iteration (while)
  • numeric operators: + - * / ^
  • string operators: + in
  • logical operators: not, and, or
  • relational operators: = <> > < >= <=
  • list operators: in
  • regular expressions (Java syntax, see Summary of regular-expression constructs) for use with =~ (approximately equal) operator
  • collection of useful intrinsic functions (see list at end)

VeLa is an interpreted, dynamically typed, domain specific language primarily intended for use within VStar.

Some other languages are too verbose or too weakly typed for the intended purpose or for the developer's taste. A disadvantage of VeLa is that it is not an existing, well-known language. An advantage is control over evolution, implementation, and integration with VStar.

VeLa code is translated from text into a more efficient internal representation before execution.

This document corresponds to the VeLa language for the latest official release of VStar.

Types

VeLa supports values of the following types, with examples:

  • integer, e.g. 42, -42
    • Integer values are signed 64-bit (long) numbers
  • real, e.g. 4.2, 0.42e1
    • Note that localised real numbers can be used in VeLa, so that if the decimal point in the current locale is a comma, this can be used instead of ., e.g. 4,2 vs 4.2.
  • boolean, true or false
  • string, e.g. "42", "(∃x)P(x)"
  • function
    • e.g. two anonymous (unnamed) function expressions that when applied to 3 yield 3^3 or 27:
      • function(n:integer) : integer { n^3 } (3), or with λ or Λ:
      • λ(n:integer) : integer { n^3 } (3))
    • see also the Functions section below
  • list
    • e.g. [41 42 43], [1 2 "many"], [2 3 5 λ(x:integer):integer{x*x}], [1 "✊" "✋" "✅" "❓" 6]
      • note the absence of commas between list elements
      • lists are heterogenous, i.e. each element may be of any type

There is also a special type called any which is a placeholder for one or more values of any type. Its use is restricted to a small number of functions (help, print, println at time of writing) which can take one or more values of any type. See also https://github.com/AAVSO/VStar/issues/199. A small number of intrinsic functions also take parameters or return values of type object. Neither object nor any should be considered to be first class types in the language, neither may currently be used explicitly in VeLa code, and one or both may be deprecated in the future.

Sequences

A VeLa program is sequence of expressions and named bindings (variable, constant, function).

Expressions

VeLa expressions consist of values of these types combined in various ways using operators, functions, selection, and iteration constructs.

Operators

  • integer and real number operators: + - * / ^
    • The first four correspond to the familiar ones: addition, subtraction, multiplication, division
    • The last is exponentiation, e.g. 2^3 which is 8.
  • string operators: + in
    • An example of in is: "42" in "the meaning of life is 42", which will return true
  • logical operators: not, and, or
  • relational operators: = <> > < >= <=
  • list operators: in + - * / ^
    • An example of in is: "42" in "the_meaning_of_life is 42", which will return true.
    • + - * / ^ can be applied to lists or to a scalar and a list, e.g.
      • [1 2 3] + [4 5 6] gives [5 7 9]
      • 2 * [4 5 6] or [4 5 6] * 2 gives [8 10 12]
      • "a " + ["cube" "sphere" "cylinder"] gives ["a cube" "a sphere" "a cylinder"]
  • approximately equal to: =~

Functions

A named function starts with an identifier, followed by zero or more typed parameters, between parentheses, separated by spaces, and then an optional return type. The body of the function comes next. The function can be preceded by a help comment that can later be accessed via the help function, e.g. help(find). The last expression computed is returned as the value of a function.

This dry, abstract description is best illustrated by an example:

cube(n:real) : real {
    n*n*n
}

This code defines a function called cube that takes a real number, computes its cube, and returns it.

Functions must specify the types of their parameters and if a value is to be returned, the return type must also be specified.

Spaces, not commas, separate multiple parameters, e.g.

apply_f_to_list(f:function xs:list) : list {
  map(f xs)
}

The VeLa interpreter checks the types of variables and functions at run-time. Unlike some dynamically typed languages, a function will not run at all if the types of its parameter list (so-called "formal parameters") are not compatible with the actual parameters passed to it.

Functions may be overloaded, having the same name but different parameter or return types, e.g.

f(n:integer):integer{
  n^4
}

f(n:real):real{
  n^4
}

If called with an integer parameter, the first function will be invoked. If called with a real parameter, the second function will be invoked, e.g. f(2.0) will return 16 as will f(2). The difference in the value returned can be seen with the help function, e.g. help(f(2.0):

REAL : 16

If the actual parameter does not match any known form of the function, e.g. f("2"), an error such as this will follow:

Invalid parameters for function "F":
 F(N:INTEGER) : INTEGER
 F(N:REAL) : REAL

Their higher-order nature means that functions are also expressions, so can be bound to variables or named constants, passed as function parameters, and returned from functions, i.e. they are higher-order functions (HOFs).

This example defines a multiplication function which is used by the function n! to compute factorial by reducing a sequence from 1 to n:

mul(n:integer m:integer) : integer {n*m}
n!(n:integer) : integer { reduce(mul seq(1 n 1) 1) }

e.g.

n!(10)
3628800

An anonymous function can be introduced with the function keyword or the λ (lambda) symbol, e.g.

find(λ(n:integer):boolean{n > 10} [4 6 10 21 8 42])

This returns 3, the index of 21 in the list to which the find function applies an anonymous function that takes an integer and returns true if that number is greater is 10, terminating the execution of find.

Names

Variable, constant, function, and parameter names must start with an underscore, letter, or Unicode character in the range hexadecimal 80 to FFFF, and can be followed by multiple letters, underscores, digits, symbols (? ! & % # $) or Unicode characters in the range hexadecimal 80 to FFFF. Unicode characters can be copied and pasted into VeLa code, as shown in this and other sections.

For example, here is a named constant called "⭕":

⭕ is "red circle (i.e. ⭕)"

Bindings

A binding creates a variable, e.g.

x <- 42

or a named constant:

x is 42

that takes on the type of the first value it is bound to. In these examples, the type is integer.

A named constant binding cannot be changed. The type of a variable binding cannot be changed and should be preferred unless variability is actually required.

Selection

if and when constructs allow choices to be made, different branches of code to be taken or expressions evaluated.

if

The form of an if expression is if boolean-expression then consequent1 else consequent2 with else consequent2 being optional. If boolean-expression is true, consequent1 is the result of the whole if expression, otherwise consequent2 results.

Below is an example of its use in a function (recursive factorial: n!).

n!(n:integer) : integer {
    if n = 0 then 1 else n*n!(n-1)
}

If the consequent is a binding instead of an expression, no result is returned from an if expression, and if is treated as a statement.

when

The when statement is a selection construct that checks a series of Boolean expressions one by one, returning the value of the first matching expression. The form of a when expression is when boolean-expression1 -> consequent1 boolean-expression2 -> consequent2 .. boolean-expressionN -> consequentN).

Here is an example of a function that takes a real value, t, and uses a when expression to return the value of an expression of the form mt + b (a linear equation).

f(t:real) : real {
  when
    t < 2451482.87956 -> -0.056438243321*t + 138360.8422308304
    t >= 2451482.87956 and t < 2451499.19244 -> -0.007211142945*t + 17681.44844775249
    t >= 2451499.19244 and t < 2451519.71482 -> 0.008330270959*t + -20418.315186923635
    t >= 2451519.71482 and t < 2451541.82028 -> 0.027192077131*t + -66658.4048747983
    true  -> 0.038448083037*t + -94252.97408292542
}

This is similar to the cond (conditional) construct in the LISP language.

Iteration

while

Loops in VeLa are often unnecessary due to functions that operation on lists such as find, map, for, but where general purpose iteration is required, the while loop can be used.

The form of a while expression is while boolean-expression body. Here is an example:

i <- 1
while i <= 10 {
  println(format("%d^3 = %d" [i i^3]))
  i <- i + 1
}

Running this gives:

1^3 = 1
2^3 = 8
3^3 = 27
4^3 = 64
5^3 = 125
6^3 = 216
7^3 = 343
8^3 = 512
9^3 = 729
10^3 = 1000

The same effect could be achieved via the for procedure which applies a function (procedure in this case since nothing is returned) to each element of a list, in this case the integer sequence 1..10. The advantage here is that there is no variable increment necessary.

for(
  λ(i:integer){
    println(format("%d^3 = %d" [i i^3]))
  }
  seq(1 10 1)
)

If a value is returned from the function passed to for, the last value returned will be the final output of the for function, e.g.

sum <- 0
for(
  λ(i:integer):integer{
    sum <- sum + i
    sum
  }
  seq(1 100 1)
)

will return the sum of values from 1 to 100 inclusive:

5050

This is a little clumsy though, and reduce would be a better choice here, e.g.

reduce(λ(a:integer b:integer):integer{a+b}
       seq(1 100 1)
       0)

Finally, a general purpose while construct is not strictly necessary since it could be implemented as a recursive function, e.g. as a procedure called while_:

while_(condition:function body:function) {
  if condition() then { body() while_(condition body) }
}

condition():boolean { i <= 10 }

body() { 
  println(format("%d^3 = %d" [i i^3]))
  i <- i+1
}

i <- 1
while_(condition body)

But see also https://github.com/AAVSO/VStar/issues/364 for why this is not yet possible in VeLa.

Grammar

Here is the full VeLa grammar (PDF).

Contextual examples

Numeric expression in text entry

Any text entry box in VStar that accepts a numeric value can instead receive a numeric VeLa expression.

Expressions such as 2457529.5-365.25*5 or today()-365.25*5 could be used as the minimum JD when loading a dataset from AID.

Observation filter expression

(obscode in ["CTIA" "SAH"] and band = "Johnson V" and uncertainty <= 0.01) 
OR 
(band = "Visual" and obscode = "BDJB")

This Boolean expression defines an observation filter in which:

  • the observer code is CTIA or SAH and the band is Johnson V and the uncertainty is less than or equal to 0.01 or
  • the band is Visual and the observer code is BDJB (the lead VStar developer)

Load eta Aquilae with a JD range of 2457529.5-365.25*5 to 2458259.5 and try this filter via VeLa Filter... in the View menu.

A VeLa observation filter has access to the properties of the currently loaded observations, such as time/JD, magnitude, observer code, band, and any properties peculiar to a particular observation source.

These expressions may also be used in the Observation List to restrict the set of observations, create selection filters, and exclude observations.

In addition, observation source dialogs allow a VeLa expression to be used to filter observations as they are loaded, rather than afterwards.

Model function

Applying DCDFT with Period Range to the eta Aql filtered observations above with a period range of 1 to 10 and a resolution of 0.01 will yield a period search top hit of 7.18.

This function defines a Fourier model obtained from a fundamental frequency of ~0.1393 (a period of 7.18) and two harmonics:

f(t:real) : real {
  3.8893863
  +0.1148971 * cos(2*PI*0.1392758*(t-2457506))-0.312022 * sin(2*PI*0.1392758*(t-2457506))
  +0.0432131 * cos(2*PI*0.2785515*(t-2457506))-0.1196311 * sin(2*PI*0.2785515*(t-2457506))
  +0.0370884 * cos(2*PI*0.4178273*(t-2457506))-0.0013007 * sin(2*PI*0.4178273*(t-2457506))
}

VeLa in plug-ins

VeLa is used in conjunction with some plug-ins, for example:

  • The VeLa observation transformation plugin requires a function to be defined to determine what transformation to apply to each observation.
  • VeLa model functions are used in the VeLa model creator and VeLa observation source plug-ins.
  • VeLa filters can be used to subset the observations loaded from an observation source.

See VStar Plug-in Details for more.

Scatter Plot via Scripting API scatter function

Here is an example of how to use the scatter API function with VeLa:

# This is a comment.
# y <- x^3

x is seq(1.0 1000.0 1.0)
y is map(function(n:real):real{n^3} x)

scatter("Cubes" "x" "x^3" x y)

After entering this code and clicking Run in the Tools -> VeLa... dialog, a new Cubes tab will be added with a plot of the cubes of x from 1 to 1000.

Running VeLa Code

VeLa code can be entered into a dialog (Tools -> VeLa) to test it before use in one of the contexts outlined above, e.g.

fact(n : integer) : integer {
    when n = 0 -> 1
         true -> n*fact(n-1)
}

map(fact [1 2 3 4 5 6 7 8 9 10])

Intrinsic named constants

π 
pi
e

Intrinsic functions

A range of in-built functions are available in VeLa and categorised below.

The help function can be used to find out at least the parameter types and return type of a function. In some cases, a help comment is included, e.g.

help(ord chr)

gives:

ORD(character:STRING) : INTEGER
Returns an ordinal value given a single character string.

CHR(ordinalValue:INTEGER) : STRING
Returns a single character string given an ordinal value.

The next section summarises the available functions but help can be used to find more information in some cases.

The function intrinsics can be used to get a complete list of available intrinsic (vs user-defined) VeLa functions. The following code outputs one function per line:

for(println intrinsics())
⚠️ **GitHub.com Fallback** ⚠️