IvoryScript user guide

1 Introduction

This user guide provides a comprehensive overview of IvoryScript, a functional programming language designed for dynamic data types, uniform data persistence and explicit control over evaluation.

It explains key concepts and practical usage, highlighting distinctive features compared to more conventional languages. Whilst this guide focuses on practical aspects of the language, a formal specification of the syntax and semantics is provided by the reference manual.

Many of the syntactic constructs ultimately transform into a much simpler core language. One particular feature of which is that all evaluation is explicit, so the meaning of reduction and evaluation operators and coercion for different syntactic terms is explained fully to enable a comprehensive understanding of the meaning of expression values.

Although the language is generally purely functional, mutable heap variables are supported for changes of application state.

Programming in the language involves scripts written in its grammar, which a compiler translates into either bytecode for an interpreter or C++ source code for further compilation by external tools.

As IvoryScript continues to evolve, this guide will be updated to reflect new features - such as derived classes etc.

2 Lexical overview

Scripts consist of ASCII characters that are are subdivided into tokens based on the following rules.

2.1 Comments

Two types of comments are supported:

  • Single-line comments start with -- and continue to the end of the line.

  • Nested comments start with {- and end with -}. Comment symbols within string codes do not introduce comments.

2.2 Whitespace and tokens

Whitespace characters (spaces, tabs, newlines) serve as separators between tokens like identifiers, keywords, literals and operators. Consecutive whitespace characters are treated as a single separator.

2.3 Names

A name consists of letters, digits and underscores. It must start with a letter, and names are used in different contexts dependent on the case of the first letter. e.g. x, Ptr and temp_123 etc.

2.4 Keywords

Lowercase words like

class, inline, let, module and type etc.

are reserved keywords with special meaning and so unavailable for use as general identifiers. For a full list see.

2.5 Special symbols

Special symbols, such as ;, (), [], :=, ->, ::, !, ^ and #, have specific meaning as described in the language syntax. For a full list see.

2.6 Numeric literals

Numeric literals can be either integer or floating-point:

  • Integer literals consist of digits (e.g. 6144). Hexadecimal form if there is a 0x or 0X prefix (e.g. 0x23FF).

  • Floating-point literals consist of digits, a decimal point and may include an exponent (e.g. 3.14e2).

2.7 Character and string literals

Character literals are single characters enclosed in single quotes, and they support 'C' style escape sequences (e.g. '\n' for newline). For a full list see.

String literals are sequences of characters enclosed in double quotes and also support the same escape sequences (e.g. 'Hello, world\n').

3 Script forms: Order and Module

IvoryScript supports two primary script forms: Order and Module.

3.1 Order script

An Order script is typically associated with an application, such as a session in the console. It comprises a sequence of expressions that are executed in order. Each step in a sequence is typically either an action or a value to be shown.

An example of an Order script:

tan (radians 45);
sqrt 2;
let {
   x :: Int = 10;
   y :: Int = x * 2;
   z :: Int = y + 5
} in
   z
      

For the last, the variables x, y and z are assigned sequentially, and then the value of z is shown.

3.2 Module script

Module scripts are designed to define reusable names associated with types, classes, functions etc. Modules serve as libraries or packages that can be imported and used elsewhere in an application. Although the primary use is for declarations and definitions that are initialised in definition order, initialisation can cause side effects - although this is not something to be generally recommended.

An example of a Module script:

module MathOperations where {

square :: Int -> Int;
square x = x * x;

cube :: Int -> Int;
cube x = x * x * x;

sideEffect :: Int = {show "Module initialised"; 42}

}
      

which defines reusable functions, but also includes a side effect that is executed when the module is initialised.

4 Namespaces

Names may occur in four distinct namespaces.

  • Type namespace

  • Class namespace

  • Module namespace

  • Value namespace

4.1 Naming convention

In IvoryScript, Type, Class, and Module names must begin with a capital letter. In contrast, names in the Value namespace can begin with either an uppercase or lowercase letter.

A name can appear in multiple namespaces. For example, Point can be a type constructor in the Type namespace and a data constructor in the Value namespace:

type Point a = Point a a               -- 'Point' as both type constructor and data constructor
let p :: Point Double = Point 1.0 1.0  -- 'Point' constructs a value of type Point
      

In this case, Point is a type constructor when defining the type and a data constructor when creating a value.

4.2 Type namespace

The Type namespace holds type constructor names, including built-in types (Int, Double) and custom types.

type Point a = ...
      

Here, Point is introduced as a type constructor in the Type namespace.

4.3 Class namespace

The Class namespace contains type classes, which define sets of functions or operations that can be implemented for various types.

class Point a where
  move :: a -> a
      

In this case, Point is a type class in the Class namespace that defines a move function.

4.4 Module namespace

The Module namespace holds module names, which group related functions, types, and values. Modules allow for code organization and reusability.

module Point where
  origin = Point 0 0
      

Here, Point is the name of a module in the Module namespace.

4.5 Value namespace

The Value namespace holds variables, functions, constants, and data constructors—names representing values or operations on them.

let p = Point 1.0 1.0
      

In this example, Point is a data constructor in the Value namespace, used to create a value of type Point.

4.6 Summary

Namespace Contains Example
Type Types, type constructors Int, Point
Class Type classes Eq, Point
Module Module names MathOps, Point
Value Variables, functions, constants, data constructors x, Point

5 Data types and signatures

5.1 Introduction

IvoryScript’s type system is designed to maintain clarity and safety, ensuring that values are used consistently with their declared types. This chapter introduces the core types in IvoryScript, including primitive types, type signatures, function types, and the essential Ptr a. Types such as List and Bool are not part of the core language but are expected to be present in the prelude or list modules.

5.2 Primitive types

IvoryScript supports several primitive types that serve as the foundation for more complex structures. These include:

  • Int: Represents integer numbers.

                   
    let x = 42  -- x is inferred as Exp Int
    let y = !x  -- y is of type Int
            
                
  • Double: Represents floating-point numbers.

                   
    let z = 3.14  -- z is inferred as Exp Double
    let w = !z    -- w is of type Double
            
                
  • Char: Represents a single character.

                   
    let letter = 'a'  -- letter is inferred as Exp Char
    let strictLetter = !letter  -- strictLetter is of type Char
            
                
  • String: Represents a sequence of characters (a list of Char).

                   
    let greeting = "Hello, World!"  -- greeting is inferred as Exp String
    let strictGreeting = !greeting  -- strictGreeting is of type String
            
                

Primitive types are inferred as lazy expressions (Exp a) by default. When a value is forced with ! or #, it changes to its strict type a.

5.3 Name and Type

IvoryScript introduces two critical built-in types:

  • Name: Typically, name values result from a literal, such as #Mary, or from the special binding syntax, e.g., Mary:"Had a little lamb". For the latter, a pattern Bind name _ matches the name.

  • Type: Type values can be created from a literal type signature, such as let typeOfVar = #::Int, and are also the return value of the typeOf function, which provides reflection capabilities.

These types are essential for working with more dynamic aspects of IvoryScript, allowing you to reference and manipulate names and types directly.

5.4 Type signatures

Type signatures in IvoryScript explicitly declare the types of values and functions. These signatures ensure correctness and make the function's input and output types clear.

The general form of a type signature is:

         
name :: Type
    
      

For example, a function that adds two integers might have the following type signature:

         
add :: Int -> Int -> Int
    
      

This indicates that add takes two arguments of type Int and returns an Int. For lazy values, the signature might be:

         
add :: Exp Int -> Exp Int -> Exp Int
    
      

5.5 Function type signatures

Function types are a core part of IvoryScript’s type system. They define how functions take arguments and return results.

The arrow (->) separates argument types from the return type:

         
multiply :: Int -> Int -> Int
    
      

This shows that multiply takes two Int values and returns an Int. For functions that operate on lazy values:

         
multiply :: Exp Int -> Exp Int -> Exp Int
    
      

Function types can also be higher-order, where a function returns another function:

         
compose :: (b -> c) -> (a -> b) -> (a -> c)
    
      

Here, compose is a function that takes two functions as arguments and returns a new function.

5.6 Lazy and strict types

IvoryScript distinguishes between lazy and strict types:

  • Exp a: Represents a lazy expression of type a. Lazy expressions are not immediately evaluated and only reduced when explicitly forced.

                   
    let x = 42  -- x is of type Exp Int
    let y = #42 -- y is of type Int
            
                
  • a: Represents a strict, fully evaluated type.

Values are typically inferred as Exp a unless explicitly forced to be strict using the ! or # operators. This allows IvoryScript to manage evaluation efficiently.

5.7 Tuple types

Tuples group multiple values of potentially different types into a single structure. They are enclosed in parentheses, with values separated by commas.

         
let point = (3, 4)  -- point is of type Exp (Int, Int)
    
      

Strict tuples can be defined as well:

         
let strictPoint = !(point)  -- strictPoint is of type (Int, Int)
    
      

Type signatures for functions that use tuples follow this pattern:

         
first :: Exp (a, b) -> Exp a
    
      

This signature shows that the first function takes a lazy tuple and returns the first element lazily.

5.8 Ptr types

The Ptr a type is an essential polymorphic primitive type in IvoryScript. It represents a pointer to a value of type a and is primarily used for referencing memory or interacting with lower-level aspects of the runtime system. The use of Ptr a is primarily for operations that involve state changes or for efficient memory access in embedded or performance-critical applications.

  • Ptr a: Represents a pointer to a value of type a.

                   
    let ptr = Ptr someValue  -- ptr is of type Exp (Ptr a)
            
                

The primary intention of Ptr a is for managing state changes or mutable references within an application. Although Ptr a can be used imperatively, doing so is generally less efficient than a pure, tail-recursive functional approach.

Pointers allow interaction with application state in a controlled manner:

         
let p = Ptr 10  -- p is of type Ptr Int
    
      

While Ptr a provides a way to handle mutable state, its use should be limited to genuine scenarios requiring changes to application state. Pure functional recursion often achieves similar results in a more efficient and declarative manner.

5.9 Conclusion

IvoryScript’s type system introduces essential types such as Name, Type, and Ptr a, alongside primitive types like Int and Char. The distinction between lazy (Exp a) and strict (a) types ensures that evaluations are performed only when necessary, allowing for efficient management of resources. Function types further provide a foundation for defining behavior, whether working with lazy or strict values.

6 Values, reduction and evaluation

6.1 Strict and lazy values

Everything in IvoryScript is treated as a value, including expressions that require further reduction or evaluation. Values can be strict or lazy, depending on their declaration.

A strict value is fully evaluated when used, while a lazy value represents an expression that can undergo further reduction when needed.

There is nothing special about functions returning lazy values. Coercion often transforms lazy values into their appropriate forms without requiring explicit reduction or evaluation.

6.2 Coercion and function types

The coercion mechanism automatically adjusts values to fit the context in which they are used, reducing the need for explicit use of (!) or !. However, because of this flexibility, it is essential to declare a function’s type, particularly for recursive functions.

The example below demonstrates how IvoryScript represents lazy and nested expressions:

         
typeOf (take 100 (!primes))
-- Output: Exp (Exp [Int])
    
      

In this case, Exp (Exp [Int]) indicates that the result of take 100 operates on a value that itself still requires further evaluation, even after a single reduction step. The outer Exp refers to the need for additional reductions to retrieve the result, while the inner Exp [Int] signifies that the list of integers has yet to be fully evaluated. Thus, Exp represents the delayed or pending computation that will eventually yield the final value.

This is quite different from many other languages in common use, where lazy values or deferred evaluations are less explicit or hidden from the developer. In IvoryScript, this flexibility requires managing types carefully, especially with recursive functions or lazy expressions that may need multiple steps of reduction before becoming usable values. Declaring types such as Exp explicitly allows control over how and when evaluation occurs, providing the benefit of lazy computation while maintaining clarity about the process.

7 Expression forms

7.1 Introduction

This chapter provides an overview of the primary expression forms in IvoryScript. The goal is to offer a practical understanding of how different expressions are used, balancing between detailed reference and introductory examples.

7.2 Literal expressions

Literal expressions represent direct values in IvoryScript. They include numbers, strings, booleans, and more.

Example:

         
42         -- Integer literal
"hello"    -- String literal
True       -- Boolean literal
    
      

Literal expressions are straightforward and are used to represent fixed values that do not require further reduction.

7.3 Variable expressions

Variable expressions refer to names that have been previously defined in a scope. They are used to access values stored in variables.

Example:

         
let {
    x = 10;
    y = x * 2
} in
    y   -- Variable 'y' refers to the result of 'x * 2'
    
      

Variables in IvoryScript are immutable, meaning once a value is assigned, it cannot be changed. Variable expressions rely on earlier bindings within their scope.

7.4 Function application

Function application is the most common way of invoking functions in IvoryScript. It consists of calling a function with one or more arguments.

Example:

         
let {
    square x = x * x;
    result = square 5
} in
    result  -- Applies the function 'square' to the argument 5
    
      

Function application is reduced by passing the argument(s) to the function and reducing the function body with these arguments.

7.5 Lambda expressions

Lambda expressions represent anonymous functions in IvoryScript. They are defined using the syntax \x -> expression where x is the argument.

Example:

         
let {
    add = \x y -> x + y;
    result = add 3 7
} in
    result  -- Applies the lambda function to arguments 3 and 7
    
      

Lambda expressions are useful for defining functions inline, especially when the function is only needed in a limited scope.

7.6 Let expressions

The let expression is used to introduce local bindings. Variables defined in a let block are only visible within that block.

Example:

         
let {
    x = 5;
    y = x + 3
} in
    y   -- Returns 8, as 'x' and 'y' are local to the let block
    
      

7.6.1 Introduction to 'let'

The let construct in IvoryScript is a fundamental building block for defining local variables and functions. It provides a way to bind expressions to names within a particular scope, allowing for modularity and reusability in code. The let construct is versatile, supporting both sequential and mutually recursive definitions.

7.6.2 Basic 'let' Usage

The most common usage of let involves binding expressions to names. These bindings are local to the block in which they are defined, and they are evaluated in sequence. Here is a basic example:

               
let {
    x = 10;
    y = x * 2;
    z = y + 5
} in
   z
    
            

In this example, the variables x, y, and z are defined locally within the let block, and the result of the expression is the final value of z. The variables are evaluated in the order they are written, making let a simple and powerful tool for local definitions.

7.6.3 Let and Function Definitions

The let construct also supports function definitions within its scope. This allows for defining local functions that can operate on the values bound in the same let block. Here's an example of a let block defining a function:

               
let {
    add x y = x + y;
    result = add 5 10
} in
    result
    
            

In this case, the function add is defined locally within the let block, and is then used to calculate the value of result. This allows for encapsulating functionality within the scope of the let expression.

7.6.4 Let and Recursive Functions

The let construct in IvoryScript also supports recursive and mutually recursive function definitions. This enables defining functions that refer to themselves or to each other within the same let block. Here's an example of a mutually recursive function pair:

               
let {
    isEven n = if n == 0 then True else isOdd (n - 1);
    isOdd n = if n == 0 then False else isEven (n - 1)
} in
    isEven 4
    
            

Here, the functions isEven and isOdd call each other, making them mutually recursive. Both are defined within the same let block, allowing for their mutual recursion to be expressed cleanly.

7.6.5 Evaluation Order in 'let'

In IvoryScript, the evaluation order within a let block is strictly sequential. This means that each expression is evaluated in the order it is written, and the result is bound to the corresponding name. If a variable depends on a previous definition, that previous expression must have been fully evaluated before the dependent variable can be used. This ensures a clear and predictable evaluation flow, avoiding potential ambiguities in execution.

However, if a function is defined within the let block and it is not immediately invoked, the function itself is treated as a value that can be evaluated later. This introduces the concept of delayed evaluation, where the function's body is not executed until the function is called. This behavior is particularly important when dealing with recursive or mutually recursive functions, as discussed in the next section.

7.6.6 Delayed Initialization of Closures in Mutually Recursive Functions

A significant challenge with the let construct arises when dealing with mutually recursive functions, especially in terms of the availability and initialisation of closures. When two or more functions are mutually recursive, the closure (the environment capturing the function's variables and references) for each function may not be fully initialised at the time the other functions try to reference it.

In practical terms, this means that while the names of mutually recursive functions are available within the same let block, their closures may be incomplete when one function tries to call the other. This delayed initialisation can result in runtime errors or undefined behavior if a function is invoked before its closure has been fully initialised.

For example, consider this mutually recursive pair of functions:

               
let {
    f x = if x == 0 then 1 else g (x - 1);
    g y = if y == 0 then 0 else f (y - 1)
} in
    f 5
    
            

In this example, f and g are mutually recursive, meaning they call each other. However, if the function g is invoked before its closure has been fully initialised (due to f calling it), it could result in incorrect behavior. To handle this, IvoryScript currently ensures that the names of these mutually recursive functions are available, but the full initialisation of their closures may be delayed until the functions are first invoked.

This is a key consideration when working with mutually recursive functions in IvoryScript. A common workaround is to structure recursive functions carefully, ensuring that any function that relies on a mutually recursive closure is invoked only after it has been fully initialised. Another approach is to split the mutually recursive functions across separate let blocks or modules, ensuring that each function's closure is fully constructed before mutual recursion occurs.

7.7 Conditional expressions

Conditional expressions are written using the if/then/else structure. IvoryScript evaluates the condition and returns the corresponding branch.

Example:

         
let {
    result = if x > 0 then "positive" else "non-positive"
} in
    result
    
      

The condition must reduce to a boolean, and the result depends on whether the condition holds true.

7.8 Case expressions

Case expressions allow for pattern matching against different values. They are useful for deconstructing data types and handling multiple cases explicitly.

Example:

         
let {
    describeList lst = case lst of {
        [] -> "Empty list";
        _ :+ _ -> "Non-empty list"
    }
} in
    describeList [1, 2, 3]  -- Matches the non-empty list case
    
      

Pattern matching with case expressions is flexible, allowing behavior to be defined for various structures of data.

7.9 Operator expressions

Operators like +, -, *, and == are used to perform arithmetic or logical operations.

Example:

         
let {
    sum = 1 + 2;
    isEqual = 5 == 5
} in
    (sum, isEqual)  -- Returns (3, True)
    
      

Operator expressions are syntactic sugar for function applications. For example, x + y is equivalent to add x y where add is the operator function.

7.10 Conclusion

The expression forms in IvoryScript provide the building blocks for defining logic, performing calculations, and manipulating data. From simple literals and variable bindings to more advanced constructs like pattern matching and user-defined types, expressions form the core of writing functional programs in IvoryScript.

8 Coercion and type casting

8.1 Introduction

In IvoryScript, coercion and type casting are essential mechanisms that ensure expressions conform to expected types, with a focus on small steps of graph reduction rather than full evaluation. Coercion is applied automatically when values are transferred between contexts, such as when passing function arguments or defining names.

When an expression requires type adaptation, IvoryScript represents it as:

         
(COERCE, expr)
    
      

During type checking, IvoryScript attempts to match the type of expr with the expected type. If the types align, the coercion is no longer needed, and the expression is used directly. Otherwise, the expression is transformed using a cast:

         
(!)(cast expr)
    
      

This transformation introduces the necessary graph reduction steps for cases where type adaptation is required, ensuring the expression can be used in the given context.

8.2 The General Cast Class

IvoryScript uses the Cast class as the general mechanism for handling all type conversions. Instances of the Cast class define how an expression of one type can be cast to another. While there are many types of casts, the most common involves graph reduction, as function application and all constants are lazy by default.

The instance that deals with reductions is defined as follows:

         
instance Cast (Exp a), a where {
    inline cast x = (!)(reduceCast x)
}
    
      

This instance specifically handles the conversion from an expression of type Exp a to type a. The reduceCast function applies graph reduction to x, ensuring that the expression is reduced to a value before it is used. The explicit ! operator signals that the expression should be reduced as part of this casting process. Given that function application and constants are lazy, this cast is frequently encountered in practice.

8.3 Strict type casts

Casting between values of different strict types (where appropriate), such as from Int to Double, is also handled by the general Cast class. However, for strict types, the transformation is direct and does not involve reduction. For example, casting from Int to Double is achieved using the following instance of StrictCast:

         
primitive fromIntDouble :: Int -> Double;

instance StrictCast Int, Double where {
    inline strictCast x = fromIntDouble x
}
    
      

In this case, the StrictCast instance uses a primitive function, fromIntDouble, to cast the value directly. This transformation occurs without coercion or reduction, as the cast involves only a straightforward type conversion between two strict types.

8.4 Transforming strict values to lazy expressions

IvoryScript also supports casting from strict values to lazy expressions, such as from a strict type a to Exp a. This conversion is handled using the general Cast class and is performed via instances of StrictCast that convert strict values into lazy expressions.

         
-- mkExpr :: a -> Expr a;
instance StrictCast a, Exp a | a ¬= Void where {
    inline strictCast x = mkExpr x
}
    
      

In this instance, the strict value is wrapped in a lazy expression of type Exp a using the mkExpr function. This cast is essential for supporting deferred computation and graph reduction in IvoryScript. The qualifier a¬= Void ensures that this transformation is valid for types that have representable values, excluding Void.

8.5 Summary

Coercion and type casting in IvoryScript are critical for managing type transformations while ensuring that expressions conform to expected types. The Cast class serves as the general mechanism for all conversions, and reduction is the most common type of cast due to the laziness of function application and constants:

  • The Cast class handles all type conversions, including strict type casts and transformations between expressions and values.

  • Instances of Cast (Exp a), a involve reduction using the reduceCast function, which reduces the expression step by step as required. This is the most frequently encountered cast.

  • Casting between strict types, such as from Int to Double, involves direct conversion without reduction.

  • Strict values can be transformed into lazy expressions, enabling deferred computation and graph reduction, through instances of StrictCast.

9 Tail recursion

9.1 How it works

If the result of a function is a direct function application, then the function call can be made simply by adjusting the stack appropiately and transferring control. This is referred to as a tail call and, if the call is recursive, then such a function is termed tail recursive.

Thus tail recursion allows the compiler to simply discard the current function’s stack frame, which results in efficient recursion with constant memory usage, similar to iterative loops.

Consider:

let sum n = 
  if n = 0 then 0 else n + sum (n - 1)
         

This function is not tail recursive because the recursive call sum (n - 1) is combined with the addition of n. The function cannot return directly from the recursive call; the addition must first be completed.

9.2 Example

To make the function tail recursive, the operation must be included in the recursive call. This is often done by introducing an accumulator:

let sum_tail n acc = 
  if n = 0 then acc else sum_tail (n - 1) (acc + n)

In this version, thesum_tail, making it tail recursive. The accumulator acc carries the result forward, allowing the function to be optimised and avoid the need for new stack frames.

9.3 Restructuring functions

To achieve tail recursion, a function may need to be restructured. The goal is to ensure that the recursive call is the final action in the function. This often involves introducing an additional parameter, such as an accumulator, to carry intermediate results. Here’s how a typical non-tail-recursive function might be restructured:

let factorial n = 
  if n = 0 then 1 else n * factorial (n - 1)

This function can be restructured into a tail-recursive version by using an accumulator:

let factorial_tail n acc = 
  if n = 0 then acc else factorial_tail (n - 1) (acc * n)

In this tail-recursive version, the recursive call is the final operation, and the intermediate result is carried through the accumulator.

9.4 Inline polymorphic functions

Inline polymorphic functions require special handling to achieve tail recursion. Because they are inlined directly, a tail-recursive function may need to delegate to a non-inline helper function which performs the actual recursion.

An example of this can be seen in the lengthList function, which computes the length of a list:

lengthList :: [a] -> Int;
inline lengthList l =
   let { lengthList_ :: [a] -> Int -> Int;
         lengthList_ l acc =
            case l of {
               [] ->        acc;
               _ :+ xs ->   lengthList_ ((!)xs) (acc + 1)
            }
   } in
      lengthList_ l 0;

The function lengthList is an inline polymorphic function, but the actual recursion occurs in the helper function lengthList_, which uses an accumulator to make the recursion tail recursive. The recursive call lengthList_ ((!)xs) (acc + 1) is the final operation, ensuring that the function remains efficient and tail-recursive while allowing the polymorphic behavior to be maintained.

9.5 Advantages

Tail recursion offers several key advantages:

  • Improved performance: Reusing the stack frame for each recursive call reduces the overhead of recursion and leads to faster execution.

  • Avoidance of stack overflow: Tail-recursive functions can handle deep recursion without risking stack overflow, making them more reliable for large data sets.

  • Memory efficiency: Since tail-recursive functions do not accumulate stack frames, they use constant memory, even for complex or deeply recursive operations.

9.6 Future steps

While tail recursion must currently be implemented manually in IvoryScript, future enhancements are planned to introduce program analysis that can automatically detect and transform functions into their tail-recursive equivalents. This would allow the compiler to optimise recursion without requiring explicit restructuring by the developer.

For now, restructuring functions for tail recursion requires manual refactoring to ensure the recursive call is the final action, using accumulators or helper functions as necessary.

10 Garbage collection

10.1 'Mark_GC' class

The Mark_GC class is provided to support garbage collection of expression, function and Ptr values. There must be an instance of Mark_GC for every type, although the essential method mark_GC need only be defined if the representation of any constructor includes one or more primitive expression, function or Ptr values, or a value of nested type similarly.

The mark_GC method ensures that all reachable objects of the specified type are correctly processed by the garbage collector. This method traverses the structure of the object, marking any heap allocated values, which is essential for preventing the reclamation of live objects during memory management.

class Mark_GC a where
    mark_GC :: a -> ()

instance Mark_GC MyType where
    mark_GC v = case v of
        MyType x y -> {
            mark_GC x;
            mark_GC y;
        }

In this example, the mark_GC function uses a case expression to deconstruct MyType, marking each of its fields to ensure all components are handled during garbage collection.

10.2 Future derivation

Although every custom type currently requires an explicit Mark_GC instance with an appropriate definition of mark_GC, future developments are expected to allow automatic derivation of mark_GC based on the data constructor(s) representation function(s). This simplification would enable the compiler automatically to generate the necessary marking logic for most types without manual intervention and reduce the need for repetitive code in custom type definitions.

11 Embedded applications with limited resources

11.1 Introduction

IvoryScript is designed to underpin the Ivory System declarative event-driven model, which can be specifically tailored for resource-constrained applications. By natively supporting multiple environments and persistence, it is particularly suited for embedded systems operating under constraints on memory and communication bandwidth. This chapter explains how resource usage can be minimised by separating the compilation process, using remote bytecode interpretation and dynamic linking to the runtime system. It also explores how diverse applications can evolve dynamically and run non-stop with remote management for inspection and modification, enabling flexible deployment and control of a wide range of mobile units including vehicles, drones and spacecraft etc.

11.2 Lightweight deployment

In embedded systems where resources are constrained, the compiler can be omitted entirely. This reduces the memory footprint and computational overhead, as the only necessary components are the bytecode interpreter and runtime system.

  • Local compilation and bytecode preparation: Order scripts are prepared on a local host with full compiler support. The resulting bytecode, which is compact and efficient, is then ready for transmission to the embedded device.

  • Remote bytecode transmission and interpretation: The compiled bytecode is transmitted to the remote platform and dynamically linked to the runtime system. The lightweight interpreter on the device then executes the bytecode with no need to reboot or restart processes to execute new Order scripts - allowing updates and inspection to happen seamlessly in real-time.

11.3 Benefits

Applications can be updated dynamically as new operational requirements arise, all while minimising communication bandwidth and avoiding re-deployment. This flexibility ensures that systems can evolve over time and remain in continuous operation.