IvoryScript reference manual

1 Introduction

IvoryScript is the scripting language for programming the Ivory System. It is essentially functional with a simplified syntax based on the Haskell programming language.

At the outermost level, a script may take one of two forms:

  • order: A sequence of expressions intended for direct execution.

  • module: A collection of related declarations and definitions.

2 Lexical description

A script conforming to the IvoryScript syntax is a sequence of ASCII characters which may be separated into lines by the newline (LF) character. Lexically, it is subdivided into a sequence of tokens according to the following rules.

2.1 Comments

  • Single line: Introduced by the character pair --, all succeeding characters up to the next newline are skipped.

  • Nested: Introduced by the character pair {-, the comment nesting level for all succeeding characters up to and including the closing character pair -} is incremented. The comment nesting level for a script is initially zero, so all characters associated with a nesting level greater than zero are skipped.

Note: Including any of the character pairs --, {- and -} within a string literal does not introduce a comment.

2.2 Tokens

Whitespace characters not included in a comment, string or character literal are the space, tab and newline characters. Multiple consecutive whitespace characters are treated as a single separator between identifiers, keywords and operators to avoid ambiguity

2.3 Name

A name token is a sequence of case significant letters, digits and underscore. The first character of which is restricted to a letter and there is a distinction as to whether it is upper or lower case.

2.4 Keywords

The following words are reserved in the syntax, and are unavailable for use as names:

and case class constant def
do else if in inline
instance let matching mod module
not object of or otherwise
persistent private public subordinate then
this transient type undef variable
where with
Ptr

2.5 Special symbols

The following special symbols have meaning as described in other sections.

; [ ] ( ) { } \ := @= <- ! (!) , _
:: ¬:: & | -> << >> ¬ #& #| #^ < <= =
¬= >= > . : :+ ++ + - * / ^ (^) ¬^ #
#:: @ =>

2.6 Numeric literals

Numeric literal tokens may be integer or floating point.

  • Integer literal: A sequence of one or more digits. Alternatively, if preceded by 0x, or 0X, each digit is hexadecimal and the sequence may also include a, or A to f, or F with values 10 to 15.

  • Floating point literal: A sequence of one or more digits comprising the integer part following by a decimal point and sequence of one or more digits comprising the fraction part. An optional signed integer exponent prefixed by e, or E may be appended, in which case the decimal point and fraction part are optional.

2.7 Character literals

A character literal consists of a character or character escape sequence enclosed in a pair of single quotes '. The subset of characters which are not included directly, but represented by an escape sequence, is as follows:

warning sound BEL \a
backspace BS \b
escape ESC \e
form feed FF \f
line feed/newline LF \n
carriage return CR \r
horizontal tab HT \t
vertical tab VT \v
backslash \ \\
single quote ' \'
double quote " \"
question mark ? \?

The escape \integer may also be used to specify a charatcer given its numeric encoding, where integer may be decimal or hexadecimal as described for an integer literal.

2.8 String literals

A string literal consists of a sequence of characters enclosed in a pair of double quotes ". As for character literals, a subset of characters is not included directly, but by an escape sequence and numeric encoding may also be used.

3 Order and Module

A script is either an order or a module, providing a distinction between sequential expressions and top-level declarations and definitions.

3.1 Order

An order is a sequence of expressions, each terminated with a semicolon (;). The last semicolon is optional.

The syntax for an order is:

<expr1>[;] [<expr2>[;] ... <exprn>[;]]

3.1.1 Order transformation

A program transformation to each Order expression is applied as follows:

(REDUCE, showWithNewLine (COERCE <expr>))

For a sequence of more than one expression, the transformation is:

(REDUCE, SEQ ( transformexpr <expr1>) ( transformexpr <expr2>))

For longer sequences, this pattern extends:

(REDUCE, SEQ (REDUCE, SEQ ( transformexpr <expr1>) ( transformexpr <expr2>)) ( transformexpr <expr3>))

This transformation continues for all expressions in the sequence. Each expression must either be coercible to an instance of show or to Exp Void.

3.2 Module

A module is a named collection of declarations and definitions. A module is introduced using the keyword module, followed by the module's name, and an optional body of declarations and definitions enclosed by the keyword where.

The syntax for a module is:

module <module_name> where <declaration or definition>

or

module <module_name> where { <declaration or definition>; ... [;] }

A module can contain a single declaration or definition, or a sequence of declarations or definitions enclosed in curly brackets. Each item in the sequence is terminated by a semicolon, and the final semicolon is optional.

Modules provide a way to group related functionality under a single namespace.

4 Namespaces

IvoryScript supports four distinct namespaces, similar to those found in Haskell, to prevent naming conflicts and provide clarity in program structure. Each namespace serves a specific purpose and is used in different contexts within the language.

4.1 Type namespace

The type namespace holds type names. These include both concrete types (e.g., Int, String) and user-defined types introduced via data or newtype declarations. Type constructors and type synonyms also belong to this namespace.

4.2 Class namespace

The class namespace contains names for type classes, which define sets of functions that can be implemented for various types. Names declared with class reside here.

4.3 Module namespace

The module namespace includes all module names. Modules can be imported and referenced using their names, allowing the reuse and organization of code across different parts of a program. This namespace prevents conflicts between module names and other entities.

4.4 Value namespace

The value namespace contains names for variables, functions, and constants. These names represent values that are computed during program execution. All functions, variables, and constants declared in a module or let binding reside in this namespace.

Each of these namespaces serves a distinct role, and IvoryScript ensures that names from different namespaces do not clash. Identifiers within one namespace cannot conflict with those from another namespace.

5 Data types

Many syntactic forms are associated with a value and, in common with other languages, data types (often referred to as just types) distinguish particular sets of values.

5.1 Type signatures

Types are specified syntactically by type expressions, which will be referred to as type signatures, formed from the following elements:
  • Type Constructor: An identifier beginning with an upper case letter.

  • Type variable: An identifier beginning with an upper case letter.

  • Arrow ->: A functional mapping from one type to another.

    Although most commonly serving as a dyadic operator associating to the right, it is also used (mainly for system purposes) as a prefix operator for a nullary function (otherwise known as a thunk).

  • Type application. <type signaturefn> <type signaturearg>

    In this case, the function type signature may be either:
    • A type constructor introducing a parameterised type.

      An arrow.

    • A type

  • Tuple types. An ordered composite sequence of types.

    ( <type1> , <type2> [ , <typen>] )

  • List. Non primitive sum and product types.

  • Binding. Non primitive sum and product types.

There are five kinds of data types:

  • Primitive: These comprise the types of primitive atomic values from which all others are derived.

  • Function: The family of types associated with functional mappings of values of one type to another.

  • Denotional or expression: Types of unreduced/unevaluated expressions.

    Exp a

    Given a type a , Exp a is the type of the expression which reduces to a value of type a in a single reduction step.

    Note that a may also be a denotional type, e.g.

    Exp (Exp a)

    denotes a value of type a requiring two reduction steps.

  • Tuple types. An ordered composite sequence of types.

  • Algebraic types. Non primitive sum and product types.

Syntactically the type * is a synonym for a partially built-in type: Any, which is the superset of all types except Void. It essentially wraps a value, so that its type can be determined dynamically.

5.2 Primitive data types

The primitive data types are:

5.2.1 Void

An expression denoting no value has type Void. There are no values of type Void and the type has no data constructors.

5.2.2 Name

The purpose of the Name data type is to support both space and time efficient late name binding. Values of type Name are typically used for identification.

Names are typically implemented as short integers and most name comparisons can be carried out with a simple integer comparison.

5.2.3 Type

The Type data type represents a type as a value for dynamic programming.

Types are typically also implemented as short integers and most type comparisons can be carried out with a simple integer comparison.

5.2.4 Int

Integers are represented by the Int data type. The range is implementation dependent, but a minimum representation of 32 bits is expected.

5.2.5 Float and Double

Floating point numbers are represented by the data types Float and Double. They correspond, respectively to the C/C++ float and double types.

5.2.6 Char

Characters are represented by the Char data type. The character code is implementation dependent.

5.2.7 String

Strings of characters are represented by the String data type.

5.2.8 Ref

Object references are represented by the Ref data type.

5.3 Function or Arrow Types

Function types denote a mapping from one type to another and take the form:

<type1> -> <type2>

5.4 Tuple types

Tuple types allow for multiple values of different types to be grouped into a single compound value. A tuple type declaration consists of a list of two or more component types enclosed within parentheses.

( <type1> , <type2> [ , <typen>] )

5.5 Custom data types

IvoryScript allows the definition of custom data types using algebraic data types. These types are constructed with multiple constructors, which may have associated data fields.

The syntax for defining custom data types is:

            type TypeName typeVariables = ConstructorName [type1] [type2] | ConstructorName2 [type1] [type2]
         

Where:

  • TypeName is the name of the custom data type.

  • typeVariables are optional type parameters, which allow the data type to be generic.

  • ConstructorName and ConstructorName2 are the names of the constructors, each optionally followed by fields of specified types.

Each constructor may have zero or more fields, and fields can be of any valid type.

6 Semantics and the valOf Operator

6.1 The valOf Operator

The valOf operator is introduced as an abstract operator to formalize the semantics of graph reduction in IvoryScript. It provides a uniform mechanism for interpreting expressions in the language. For each expression form, the interpretation of valOf will be defined in the following chapter, describing how the value of the expression is derived through reduction.

Each expression, whether strict or lazy, has an associated value. For strict expressions, valOf represents the immediate, fully established value. For lazy expressions, valOf is simply the identity function, preserving the unevaluated form of the expression.

This operator ensures that expressions in IvoryScript can be formalized in a clear and consistent manner, with no implicit reductions. Only explicit operations, such as (REDUCE, <expr>), can trigger further steps of reduction when required.

6.2 Coercion and context

Coercion automatically adjusts an expression when its type does not match the expected context. Whenever an expression e fails to unify with its required type, IvoryScript applies coercion, transforming e into a form that matches the context. This transformation is managed by the coerce function, which casts the expression to the expected type and prepares it for further reductions.

The formal transformation is specified as:

<expr> → REDUCE(cast <expr>)

This process ensures that expressions remain consistent with their contexts while maintaining the distinction between strict and lazy values, in line with the explicit evaluation model of IvoryScript.

7 Expressions

The syntax and meaning of the various expression forms is described in this section.

7.1 Name expressions

Identifiers beginning with a lower case letter refer to pattern variables and named associations in let expressions.

Identifiers beginning with an upper case letter refer to data constructors.

7.2 Constant expressions

A constant expression includes number, character and string literals. Each of which may be prefixed by # to indicate that its value is strict. In addition, strict name and type literals are:

# <name> (note the distinction here between a name as a literal value and an identifer)

and

#::<type signature>

7.3 Function Applications

A function application has the form:

<exprfun> <exprarg>

Association is to the left, so the parentheses may be omited in (f x) y.

If a function has n arguments, then it said to have arity n. For a function of arity n (where n > 1) function application with m arguments, where m < n, denotes a new function with arity n - m. This is also known as currying and it allows for partial function applications.

Partial data constructor application is permitted.

7.4 Lambda Abstractions

A lambda abstraction has the form:

\ <patt1> ... <pattn> -> <expr>

It denotes a unnamed function of arity n. Currently, the only kind of permitted pattern is an optionally signed variable.

7.5 Operator Applications

The infix application of a binary operator <op> generally has the form: <expression> <op> <expression>

There are two exceptions to this:

  • The select operator . has two special forms: <expression>.<Name> or <expression>.<data constructor>
  • The binding constructor: <Name>:<Expr>

The prefix notation is used for unary operators such as -, ¬: <op> <expr>

There is an exception to this:

The select operator . has two special forms: .<name> or .<data Constructor>

Semantically, operator applications are transformed to a standard function or type method application as specified in the following table:

Symbol Form Associativity Transformation Notes
. unary right (.) Root <name> or (.) dataCon <expr> Root Root select method or Root object construction
¬ unary right (¬) <expr> Boolean negation
:: unary right (-) <type> Type constant
. binary left (.) <expr> <name> Select by name
- unary right (-) <expr> Numeric negation
^ binary left (^) <expr1> <expr2> Numeric exponentiation
* binary left (*) <expr1> <expr2> Numeric multiplication
/ binary left (/) <expr1> <expr2> Numeric division
mod binary left (modlt;expr1> <expr2> Numeric modulus
+ binary left (+) <expr1> <expr2> Numeric addition
- binary left (-) <expr1> <expr2> Numeric subtraction
< binary left (<) < expr1> <expr2> Less than
<= binary left (<=) <expr1> <expr2> Less than or equal to
= binary left (=) <expr1> <expr2> Equal to
¬= binary left (¬=) <expr1> <expr2> Not equal to
>= binary left (>=) <expr1> <expr2> Greater than or equal to
> binary left (>) <expr1> <expr2> Greater than
¬ or not binary right (¬) <expr> Logical NOT
& and binary left (&) <expr1> <expr2> Logical AND
| or binary left (|) <expr1> <expr2> Logical OR
unary right (#¬) <expr> Bitwise one's complement
<< binary left (<<) <expr1> <expr2> Bitwise left shift
>> binary left (>>) <expr1> <expr2> Bitwise right shift
#& binary left (#&) <expr1> <expr2> Bitwise AND
#^ binary left (#^) <expr1> <expr2> Bitwise Exclusive OR
#| binary left (#|) <expr1> <expr2> Bitwise Inclusive OR
: binary right Bind <name> (Any <expr>) Name association
:+ binary right Cons <name> <expr> List construction
:= binary right See Assignment Assignment operator
-> binary right Function type operator
:: binary left See Type Constraint Type constraint

7.5.1 Assignment

Property addition or destructive update is carried out by the assignment operator

<expr1> .<name> := <expr2> => set <expr1> #<name> <expr2>

Type Constraint

Type constraints may be used to restrict values to sub-types of their primary type. Commonly used to constrain results of type * and object references (Ref), they take the form:

<expr> :: <type>

Any expression may be constrained, but it must be consistent with the type of its context. The compiler will either statically check that the constraint is valid or insert a run-time type check, which may result in FAIL.

<expr> :: <type> => let !u = <expr> in let v::<type> = if typeOf u = ::<type> then u else FAIL in v
where u and v are new variables

7.6 Conditional Expressions

There are two forms of conditional expression:

if expr 1 then expr 2 else expr 3 if expr 1 then expr 2

If expr1 is True, the value of the conditional expression is expr2 . Otherwise, the value is either expr3 or Void for the second form.

The type of e 1 must be Bool; See case for more explanation about the type of a conditional expression.

if <expr1> then <expr2> else expr3> => case <expr1> of {
 
True ->   <expr2> ;
False ->   <expr3> }
if <expr1> then <expr2> => case <expr1> of {
 
True ->   <expr2> }

7.7 Case expressions

A case expression has the following form:

            case <expr>
            {
            <alt1>;
            <alt2>;
            ...
            <altn>;
            <default>;
            }
         

where <alt1> has the form:

            <pati> -> <exprj>
            or
            <pati>, <pati + i> ... <pati + m> -> <exprj>
         

and <default>

            otherwise -> <exprdef>
         

Semantically, the value of<expr> (which must be a strict type) is matched against the sequence of alternatives: <alt1> ... <altn> as described in patterns.

The matches are tried sequentially, from top to bottom. The value of the corresponding alternative body for the first successful match is the value of the case, in the environment of the case expression extended by the bindings created during the matching of that alternative and by the declarations associated with that alternative. If no match succeeds, the result is unspecified. The last alternative can start with the keyword otherwise, which will always be successful.

7.8 Blocks

A block expression has the following form:

{ <sequence> }

where a sequence is

<expr1> ... ; <expr2> ... ; <exprn>

{ <expr> } => <expr>
{ <expr1> ; <expr2> ; <exprn> } => seq <expr1> ({ <expr2> ; <exprn> }))

seq x y is a standard function where the evaluation of x (required to be either statically or dynamically determined to be Void) is performed before the value of y is returned.

7.9 Let expressions

Let expressions have the form:

let <declaration or definition body> in <expr>

The value is <expr> where the set of mutually recursive definitions has scope both in it and the right-hand side of the definitions.

8 Patterns

Values matched against patterns are used to select case alternatives. A match is said either to succeed or fail as described below for the various pattern forms:

  • Literal

  • Variable

  • WildCard

  • Tuple

  • Data constructor

8.1 Literal

A literal pattern is a constant of one of the following types:

Name, Type, Int, Char, String

The value is matched both by data type and equality.

8.2 Variable

A variable pattern is a lower case name (optionally with a type signature).

Provided that the value and pattern have the same type, the match always succeeds and the variable name is bound to the value with scope in the corresponding alternative expression.

For a variable pattern, the value is not required to be strict.

8.3 Wildcard

_

A wildcard pattern is a single underscore. It is similar to a variable pattern in that (if the value and pattern have the same type) the match always succeeds, but without introducing a locally bound variable in the corresponding alternative expression.

8.4 Tuple

Tuple patterns match and deconstruct tuple values. There are two forms: value and pointer

8.5 Tuple value

A tuple value data pattern matches directly on a strict tuple value and its components.

( <pat1> , <pat2> [ , <patn>] )

Each component of the tuple value is matched against the corresponding component pattern.

8.6 Tuple pointer

A tuple pointer pattern matches a strict pointer to tuple.

@( <pat1> , <pat2> [ , <patn>] )

This pattern provides pointers to the components of the tuple, rather than the values themselves. Its main purpose is to allow for selective updates to individual components.

8.7 Data constructor

Data constructor patterns match on values returned by data constructors. There are three forms: value, concrete, and concrete pointer.

8.7.1 Value data constructor pattern

A value data constructor pattern matches directly on a value created by a specific data constructor.

The general form of such a pattern is:

<data constructor> <patterns>

where the pattern list can either be empty or composed of multiple sub-patterns, such as:

<pattern1> ... ; <pattern2> ... ; <patternn>

with n being the same number of components (arity) of the data constructor.

Provided that the value matches the data constructor, pattern matching is performed sequentially from left to right. This involves, if applicable, a match against the value returned by the related (by position) matching function of the data constructor.

If all sub-patterns match, the pattern as a whole succeeds.

8.7.2 Concrete data constructor pattern

The concrete data constructor pattern matches the representation of a value returned by the associated data constructor function.

#<data constructor> <pattern>

The pattern match succeeds if the represented value matches the pattern.

8.7.3 Concrete data constructor pointer pattern

The concrete data constructor pointer pattern matches against a pointer to the return value of the associated data constructor function.

@ <data constructor> <pattern>

The pattern match succeeds if the a pointer to the represented value matches the pattern.

The purpose is similar to a tuple pointer pattern, and is often used in conjuction: i.e. to allow for structured data updates.

The previous two forms of pattern are intended mainly for use in trusted system modules. Because they could limit abstraction, there might be future restrictions placed on their use. At a miminum, this is likely be the same module as the associated data constructor definition.

9 Basic declarations and definitions

Basic declarations and definitions serve to associate types and values with names.

They apply to:
  • let expressions

  • Global names and constants as well as class and instance methods. One exception is that a class method may be an object constructor.

9.1 Signed Identifier (signedId)

A signedId represents an identifier that may include an optional type signature or can be a prefix operator. It can take the form of a standard identifier, a prefix operator, or an operator enclosed in parentheses.

The forms for signedId are:

  • Standard identifier: A typical identifier that refers to value.

    <idName>

  • prefixOP: When used as a prefix, the operator can function as an identifier. Examples include arithmetic operators such as + and -.

    <prefixOp>

  • signedIddyadicOp: Operators can also be used in function-like syntax by enclosing them in parentheses.

    (<prefixOp>)

  • Optional Type Signature: A signedId may be associated with a type signature using the :: symbol.

    <signedId> :: <typeSig>

9.2 Prefix Operator (prefixOp)

A prefixOp in IvoryScript is an operator that is used before its operands. Prefix operators are typically symbols used for arithmetic or other binary operations, but they can be treated as identifiers when enclosed in parentheses or used in certain functional contexts.

The forms for prefixOp include:

  • Basic Prefix Operator: +, -, *, /

  • Operator in Parentheses: (+)

9.3 Declarations and definitions body

A body of declarations or definitions can consist of either a single declaration/definition or a list enclosed in curly brackets. or multiple declarations or definitions, the entries in a list are separated by semicolons, with the final semicolon being optional.

The syntax for a body is:

<declOrDefn> { <declOrDefn> [;] ... }

9.4 Declaration

A declaration introduces a name but does not provide a specific value or implementation. It is represented by a signedId and may optionally include a type signature or a prefix operator.

The syntax for a declaration is:

<signedId> <signedId> :: <typeSig> (<prefixOp>)

9.5 Definition

A definition binds a name to a value or function. It may include patterns and an expression following an equal sign.

The syntax for a definition is:

<signedId> <patterns> = <retExpr> (<prefixOp>) <patterns> = <retExpr>

9.6 Primitive Declaration

A primitive declaration is prefixed with the keyword primitive and is treated as a built-in entity. It can also include a standard declaration.

The syntax for a primitive declaration is:

primitive <decl>

9.7 Inline Definition

An inline definition is introduced by the keyword inline and indicates that the definition should be expanded at the point of use. It can also be a standard definition without the inline keyword.

The syntax for an inline definition is:

inline <defn>

This summary outlines the structure of basic declarations and definitions, covering modules, let expressions, and method declarations or definitions for classes and instances. The handling of object constructors as class methods works because constructors return a value of type Ref.

10 Module declarations and definitions

10.1 Type Definition

A type definition introduces a new type along with potential parameters. The form is:

         type <typeCon> <typeVar1> ... <typeVarn> = <dataDecl1> | ... | <dataDeclm>
      

This defines a type <typeCon> with optional parameters <typeVari> and corresponding data declarations <dataDecli> .

Data constructors define the structure of values for a type. The form is:

         <dataCon> <aTypeSig1> ... <aTypeSign>
      

This declares a data constructor <dataCon> with the argument types <aTypeSigi> .

Data constructors can be defined along with optional deconstructor functions. The form is:

         <dataCon> <param1> ... <paramn> -> <returnType>
      

matching { <deconstructorPattern> -> <expr> }

This defines the constructor <dataCon> and its behavior when pattern-matched via the deconstructors.

10.2 Class Definition

A class groups types that share common behaviors. The form is:

         class <bracketedClassList> => <className> <typeVar1> ... <typeVarn> where {
         <classDeclOrDefn1>;
         ...
         <classDeclOrDefnm>;
         }
      

The bracketedClassList consists of one or more classes, possibly enclosed in parentheses and separated by commas. Its form is:

         <class1>, ..., <classn>
      

or

         (<class1>), ..., (<classn>)
      

This list is optional and specifies constraints on the type variables for the class.

The body of the class contains method declarations or definitions. Each method has the form:

         <methodName> : <signature>
      

or

         inline <methodName> = <expr>
      

The methods define the behavior expected from instances of the class. These can include declarations (decl), inline definitions (inlineDefn), or object definitions (objectDefn).

10.3 Instance Definition

An instance defines how a specific type implements the behavior of a class. The form is:

         instance <classId> <typeSigCSList> <typeQuals> where {
         <instanceDeclOrDefn1>;
         ...
         <instanceDeclOrDefnm>;
         }
      

The where clause in an instance definition may include an optional body that provides method implementations. The syntax is:

         where {
         <instanceDeclOrDefn1>;
         ...
         <instanceDeclOrDefnm>;
         }
      

If no body is provided, the instance declaration or definition is considered empty.

The body of an instance may contain declarations, inline definitions, data constructor declarations, or inline data constructor definitions. The form is:

         <decl>
      

or

         inline <methodName> = <expr>
      

or

         <dataConDecl>
      

or

         <inlineDataConDefn>
      

An instance may include type qualifiers that impose constraints or relationships between types. These are defined as:

         | <dyadicTypeQual>
         | <monadicTypeQual>
         | <instanceTypeQual>
         | (<typeQual>)
      

A dyadic type qualifier defines binary relations or logical operators between types:

         <typeSig> <predRelOp> <typeSig>
      

or

         <typeQual> <predAndOp> <typeQual>
      

or

         <typeQual> <predOrOp> <typeQual>
      

A monadic type qualifier applies a unary operation to a type, such as strictness:

         ! <typeVar>
      

or negation:

         <predNotOp> <typeQual>
      

An instance type qualifier is used to indicate that one class instance depends on another:

         instance <classId> <typeSigCSList>
      

Predicate operators can be used in type qualifiers to define relationships between types:

  • Relational operators:

                   Equality (=) or inequality (≠).
                
  • Logical operators:

                   Conjunction (∧) or disjunction (∨).
                
  • Negation operator:

                   ¬ or !.