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 it continues to evolve, this guide will be updated to reflect new features; such as derived classes etc.
Scripts consist of ASCII characters that are are subdivided into tokens based on the following rules.
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.
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.
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.
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.
Special symbols, such as ;
, ()
,
[]
, :=
, ->
, ::
,
!
, ^
and #
, have specific meaning
as described in the language syntax. For a full list see.
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
).
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'
).
IvoryScript supports two primary script forms: Order and Module.
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.
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}
}
This module defines reusable functions, but it also includes a side effect that is executed when the module is initialised.
Names may occur in four distinct namespaces.
Type namespace
Class namespace
Module namespace
Value namespace
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 = Point Double Double -- 'Point' as both type constructor and data constructor
let p :: Point = 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.
The Type namespace holds type constructor names, including
built-in types (Int
, Double
) and user-defined
types.
type Point = Point Double Double
Here, Point
is introduced as a type constructor in the
Type 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.
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.
The Value namespace holds variables, functions, constants, and data constructors—names representing values or operations on them.
let p :: Point = 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
.
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 |
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.
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
.
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.
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
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.
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.
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.
The Ptr a
type is an essential 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.
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.
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.
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.
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.
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.
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.
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.
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.
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
Let
expressions allow local calculations and function
definitions that do not affect the global state of the program.
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.
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.
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.
Expressions involving user-defined types allow for rich data manipulation. For instance, a custom data type can be used in pattern matching and function application.
Example:
data Maybe a = Nothing | Just a;
let {
describeMaybe m = case m of {
Nothing -> "Nothing";
Just x -> "Just " + show x
}
} in
describeMaybe (Just 42) -- Matches the 'Just' case
User-defined types are central to IvoryScript’s flexibility in structuring data.
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.
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.
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.
Casting between strict types, 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.
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
.
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
.
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.
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.
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.
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.
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.
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.
IvoryScript provides the polymorphic constructor Ptr a
for working with pointer values. These pointers allow references to
values of any type a
, primarily for operations that involve
genuine state changes, such as interacting with mutable data
structures.
Although it is possible to use Ptr a
to implement
structures like loops that modify state, this approach is generally
inefficient compared to tail-recursive functional solutions. In
IvoryScript, recursion is typically more efficient and maintains
functional purity.
let table = HashTable 100
hashTableAdd table "key" "value"
let value = hashTableGet table "key"
The use of Ptr a
should be limited to scenarios where
state mutation is necessary. In most cases, a recursive approach is
preferred for control flow, as it is functionally pure and
efficient.
In IvoryScript, tail recursion is a technique used to optimize recursive functions by ensuring that the recursive call is the final operation in the function. If a function’s return value is a direct result of a recursive call without any additional operations, it is considered tail recursive.
Tail recursion allows the compiler or runtime to reuse the current function’s stack frame for the next recursive call, preventing the need to create new stack frames for each call. This results in efficient recursion with constant memory usage, similar to iterative loops.
let rec 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 followed by an addition. The function cannot
return directly from the recursive call; it must complete the addition
first.
To make the function tail recursive, the operation must be included in the recursive call. This is often done by introducing an accumulator:
let rec sum_tail n acc =
if n == 0 then acc else sum_tail (n - 1) (acc + n)
In this version, the recursive call is the last operation in the
function, making it tail recursive. The accumulator acc
carries the result forward, allowing the function to be optimized and
avoid new stack frames.
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 rec 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 rec 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.
In IvoryScript, inline polymorphic functions require special handling to achieve tail recursion. Because inline polymorphic functions are inlined directly, a tail-recursive function may need to delegate to a non-inline helper function. This helper function 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.
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.
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 optimize 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.
The Mark_GC
class in IvoryScript specifies how custom
types are marked during garbage collection. It defines the essential
method mark_GC
, which must be implemented for each custom
type.
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 nested or
referenced 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.
Although every custom type currently requires an explicit
Mark_GC
instance, future developments are expected to allow
automatic derivation of the mark_GC
function based on the
data constructor's representation function. This would simplify the
implementation process, enabling IvoryScript to automatically generate
the necessary marking logic for most types without manual
intervention.
This enhancement would further streamline the garbage collection process and reduce the need for repetitive code in custom type definitions.
IvoryScript’s design is particularly suited for embedded systems operating under constraints on memory, processing power, and communication bandwidth. This chapter explains how IvoryScript can minimize resource usage by separating the compilation process, using remote bytecode interpretation, and supporting dynamic linking to the runtime system. It also explores how applications can evolve dynamically while remaining available for inspection and modification, enabling flexible deployment in various constrained environments, including unmanned systems like drones or spacecraft.
In embedded systems where resources are constrained, such as in drones, vehicles, or satellites, the IvoryScript 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. These scripts are verified, checked for correctness, and compiled into bytecode. 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 embedded system, such as a drone or remote sensor platform. The lightweight interpreter on the device executes the bytecode, dynamically linking it to the runtime system as needed. There is no need to reboot or restart processes to execute new Order scripts, allowing updates to happen seamlessly in real-time.
IvoryScript’s ability to dynamically link new bytecode to the runtime system allows the application to evolve over time without a full reboot or recompilation. This feature is particularly useful in environments where requirements may change mid-operation, and new functionalities need to be introduced dynamically.
Modular Runtime System: The runtime system on the embedded device can accept new bytecode modules, linking them into the application as needed. This modular approach allows for incremental updates without disrupting existing functionality.
Small Memory Footprint: Only necessary components are loaded into memory, ensuring that resources are conserved. Unused parts of the system can be removed, keeping the memory footprint minimal.
Remote Inspection and Modification: IvoryScript supports runtime object inspection, enabling developers to examine and modify objects remotely. This is invaluable for adjusting configurations or troubleshooting during deployment, allowing the system to be monitored and evolved in real-time.
IvoryScript enables applications to adapt and evolve during operation, which is particularly beneficial for systems deployed in inaccessible locations or with long operational lifetimes. The system can be updated dynamically as new operational requirements arise, all while minimizing communication bandwidth and avoiding re-deployment.
On-Demand Updates: As operational conditions change or unexpected events occur, updated functionality can be transmitted in bytecode form, dynamically linked into the runtime system. This allows the application to adjust without interrupting ongoing operations.
Object State Evolution: Throughout operation, the state of application objects—such as system configurations or sensor data—can be inspected and modified remotely. For example, sensor calibration data can be updated as conditions change, ensuring optimal system performance without requiring system-wide changes.
Efficient Bandwidth Use: Bytecode is compact, minimizing the data needed for updates and allowing the system to evolve even in environments with limited communication bandwidth, such as remote drones or sensor networks.
IvoryScript’s capabilities are particularly well-suited for a range of embedded applications, especially those in constrained environments like drones, autonomous vehicles, or other remote systems.
Mission Updates in Real Time: IvoryScript enables real-time system adaptations by transmitting updated scripts that allow drones or other autonomous systems to switch tasks or adapt to new operational phases without a full system reset.
System Adaptation to New Conditions: In the event of unforeseen conditions (e.g., equipment malfunctions or environmental changes), new scripts can be sent to update operational behavior in real-time, such as adjusting power management routines or navigation protocols.
Remote Troubleshooting and Modification: The ability to inspect and modify running systems remotely provides flexibility to handle software issues during operation, potentially extending the operational lifetime of drones, remote sensors, or other systems.
IvoryScript’s ability to separate compilation from execution, combined with its dynamic linking and runtime inspection features, makes it an ideal language for embedded applications with limited resources. By relying on remote bytecode interpretation and dynamic system evolution, IvoryScript allows for ongoing adaptation while keeping memory and bandwidth demands low. This flexibility ensures that systems can evolve over time and remain operational in various resource-constrained environments, whether on drones, sensors, or other embedded platforms.