Expressions

Expressions are a description of the computation of a value, given some environment. Expressions appear inside source code, and are evaluated at runtime. The environment in which an expression is evaluated is the environment of its surrounding scope, which is computed at runtime.

This section outlines the syntax and semantics of expressions in Mars.

Atoms

Atoms are the most basic expressions. They are comprised of single tokens, and do not recurse.

atom ::=  int_literal_expr | variable | constructor

Integer literals

Integer literal expressions are usually just formed from integer literal tokens. However, a character literal token also forms an integer literal expression:

int_literal_expr ::=  int_literal | char_literal

Because Mars has no “character” data type, a char_literal is simply interpreted as an integer, with the byte value of the character. Thus, 'a' is equivalent to 97, and '\x8c' to 140.

The type of an integer literal expression is always Int. It evaluates to the constant value of the integer literal.

Variable references

An identifier refers to a variable or constructor.

variable    ::=  lower_identifier
constructor ::=  upper_identifier

Statically, the variable must exist in the expression’s scope. If the identifier matches the name of a local variable or argument of the current procedure, then it refers to that variable, regardless of whether there is a global variable of the same name (hence, local variables shadow globals), and the expression has the type of that variable.

If the identifier matches the name of a global variable or constructor, then it refers to that variable, and the expression has the type of that global.

If the identifier does not match a local or global variable, it is a compiler error, and the program MUST be rejected.

The dynamic semantics of a variable reference expression are to evaluate to the current value of the variable, at the point at which the expression is evaluated. Since Mars is a strict language, that point is the exact point in the code at which the expression appears, so even though local variables may change value, the evaluation of such expressions is quite deterministic.

Note

A constructor reference is just a special type of variable reference. Constructors are variables too, only syntactically distinguished by having an uppercase letter. They are semantically equivalent.

Primary expressions

primary         ::=  atom
                     | array_literal_expr
                     | apply_expr
                     | field_reference
                     | "(" expression ")"
expression      ::=  primary
                     | field_replace
expression_list ::=  expression ("," expression)*

Array literals

An array literal expression allows an array value to be created and initialised with a single expression. The value of an array literal does not need to be constant, as it may contain nested expressions which are evaluated at runtime.

Just as a character literal forms an integer expression, so does a string literal form an array of integers. Hence, a string literal is just special syntax for an array literal.

array_literal_expr ::=  array_literal | string_literal
array_literal      ::=  "[" [expression_list] "]"

An array literal consists of a list of zero or more sub-expressions. The sub-expressions may be of any type, but the types of all sub-expressions must unify, or the compiler MUST reject the program (with a type error). The type of the array literal expression is Array(a), where a is the unified type of all of the sub-expressions.

The dynamic semantics of an array literal expression are to construct an array value with as many elements as there are sub-expressions. The value of each element is the evaluation of its corresponding sub-expression.

A string literal consists of zero or more byte values. The type of a string literal expression is Array(Int). The evaluation of a string literal is an array value, with one Int element per character, with each element evaluating to the corresponding character’s byte value. Thus, "hello" is equivalent to ['h', 'e', 'l', 'l', 'o'], which is equivalent to [104, 101, 108, 108, 111].

Function application

Function application expressions allow arguments to be passed to functions, and functions to be evaluated:

apply_expr ::=  primary "(" [expression_list] ["," "..."] ")"

Primarily, function application has the form f(v1, v2, ..., vn). This is known as full function application (due to the lack of a trailing “...” token). f must have type (t1, t2, ..., tn) -> t, or the expression is a type error, and the compiler MUST reject the program. Each value vi must have type ti, where 1 <= i <= n. This expression has type t.

The evaluation of this expression first causes f and each vi to be evaluated. This expression evaluates to the result of the full application of function f to all actual parameters v1 ... vn (see procedure evaluation). All side-effects involved in the function’s evaluation occur at the time of this expression’s evaluation.

Warning

Full function application expressions are the only kind of expression which may exhibit side-effects. The order in which these side-effects take place is undefined, except that all side-effects produced by one statement occur before its successor statement. Side-effects include input/output and destructive update of data structures.

An extended form f(v1, v2, ..., vm, "...") is known as partial function application (it has a trailing “...” token). f must have type (t1, t2, ..., tn) -> t, where m <= n, or the expression is a type error, and the compiler MUST reject the program. Each value vi must have type ti, where 1 <= i <= m. This expression has type (tm+1, tm+2, ..., tn) -> t.

The evaluation of this expression first causes f and each vi to be evaluated, in order from left to right (from v1 to vm). This expression evaluates to the result of the partial application of function f to all actual parameters v1 ... vm (see procedure evaluation).

Field references

A field reference retrieves the value of a named field of a user-defined type:

field_reference ::=  primary "." lower_identifier

Recall that user-defined types consist of one or more constructors, each with zero or more parameters, each with an optional name. The same name may appear in multiple constructors, but must have the same type in each.

For a field reference of the form obj.field, obj MUST have a user-defined type, and MUST have at least one constructor with a parameter named field, or the compiler must reject the program. The type of the field reference is the type of the parameter named field in any constructor of the type of obj.

The evaluation of the expression causes obj to be evaluated first. If the constructor used to construct obj has a parameter named field, then the value of the field reference is the value of the parameter field. If the constructor used to construct obj does not have a parameter named field, then a runtime error occurs, terminating the program.

For example, the List type in the prelude has two constructors: Cons with parameters head and tail, and Nil, with no parameters. Hence:

?> x = Cons(1, Nil)
?> x.head
1
?> x.tail
Nil
?> Nil.head
Runtime Error: List instance has no field 'head'

Due to the possibility of runtime errors, it is recommended that field references be used only on types with a single constructor, or fields which appear in all constructors of a type. For other types, the more powerful switch statement should be used. The switch statement can also be used to access fields without names.

Field replace expressions

A field replace expression is used to non-destructively modify a named field of a user-defined type:

field_replace ::=  primary "." lower_identifier ":=" expression

Note

The := operator is right-associative, and has lower precedence than field references or function application. Hence, x.foo := y.bar := z is equivalent to x.foo := (y.bar := z). Updating multiple fields simultaneously therefore requires parentheses, for example, (x.foo := y).bar := z.

A field replace expression of the form obj.field := e produces a new value which is the same as obj but with field field replaced with the value of e. The original obj is not modified. The same compile-time and runtime rules as field reference expressions apply. The expression e MUST have the same type as the type of the parameter named field in any constructor of the type of obj.

The type of the field replace expression is the type of obj. The evaluation of the expression causes obj to be evaluated first. If the constructor used to construct obj has a parameter named field, then the value of the field replace is a value with the same constructor as obj, and all parameters the same as obj, except the parameter named field having the value of e. If the constructor used to construct obj does not have a parameter named field, then a runtime error occurs, terminating the program.

Note that obj is evaluated before e. If obj does not have an appropriate constructor, it is unspecified whether e is evaluated or not.

For example:

?> x = Cons(1, Nil)
?> x.head := 2
Cons(2, Nil)
?> x.tail := Cons(2, Nil)
Cons(1, Cons(2, Nil))
?> x
Cons(1, Nil)
?> Nil.head := 1
Runtime Error: List instance has no field 'head'

As with any other expression, field replace expressions do not update the variable obj. Therefore, it is common to perform an “update” as follows:

?> x = x.head := 2
?> x
Cons(2, Nil)

Note

Unlike conventional languages, this will not modify any aliases of x (it is literally an assignment of a new value to the variable named x). This can help avoid confusing bugs, and make your software more reliable overall (indeed, it is the reason behind Mars being a declarative language), but it can be confusing if you are used to conventional languages.

See also: Field update statements.

Table Of Contents

Previous topic

Procedures

Next topic

Statements

This Page