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.
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.
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.
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
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.
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 |
The following special symbols have meaning as described in other sections.
; |
[ |
] |
( |
) |
{ |
} |
\ |
:= |
@= |
<- |
! |
(!) |
, |
_ |
:: |
¬:: |
& |
| |
-> |
<< |
>> |
¬ |
#& |
#| |
#^ |
#¬ |
< |
<= |
= |
¬= |
>= |
> |
. |
: |
:+ |
++ |
+ |
- |
* |
/ |
^ |
(^) |
¬^ |
# |
#:: |
@ |
=> |
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.
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.
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.
A script is either an order or a module, providing a distinction between sequential expressions and top-level declarations and definitions.
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>[;]]
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.
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.
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.
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.
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.
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.
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.
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.
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.
The primitive data types are:
An expression denoting no value has type Void
. There are
no values of type Void
and the type has no data
constructors.
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.
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.
Integers are represented by the Int
data type. The range
is implementation dependent, but a minimum representation of 32 bits is
expected.
Floating point numbers are represented by the data types
Float
and Double
. They correspond,
respectively to the C/C++ float and double types.
Characters are represented by the Char
data type. The
character code is implementation dependent.
Strings of characters are represented by the String
data
type.
Object references are represented by the Ref
data
type.
Function types denote a mapping from one type to another and take the form:
<type1> ->
<type2>
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>] )
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.
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.
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.
The syntax and meaning of the various expression forms is described in this section.
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.
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>
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.
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.
The infix application of a binary operator <op> generally has the form: <expression> <op> <expression>
There are two exceptions to this:
.
has two special forms:
<expression>.<Name> or
<expression>.<data constructor>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 |
Property addition or destructive update is carried out by the assignment operator
|
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:
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.
|
There are two forms of conditional expression:
if expr 1 then expr 2 else expr 3 if expr 1 then expr 2If 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.
|
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.
A block expression has the following form:
{
<sequence> }
where a sequence is
<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.
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.
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
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.
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.
_
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.
Tuple patterns match and deconstruct tuple values. There are two forms: value and pointer
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.
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.
Data constructor patterns match on values returned by data constructors. There are three forms: value, concrete, and concrete pointer.
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.
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.
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.
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.
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>
signedId
dyadicOp: 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>
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: (+
)
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> [;] ... }
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>)
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>
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>
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
.
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.
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).
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 !.