Skip to content
Cameron Purdy edited this page Dec 8, 2023 · 14 revisions

Foreword

When is the last time that you learned a new language? I mean, legitimately and truly learned a new language, in detail, and in depth?

It's not an easy undertaking to learn a new language. Many of the words used to describe the new language will look and sound like words that you already know, but now each word seems to have a slightly different meaning, and some words will contain surprising twists that aren't entirely obvious at first. It may seem like there's an entirely different set of assumptions about what words mean, how things work, what styles should be embraced, and what programming approaches should be avoided.

It's this enormous chasm -- or maybe just this minor crack in the sidewalk -- that this guide will help you safely cross. We will work to make the unfamiliar seem simple and natural, and the new and different seem simultaneously obvious and necessary. That's a tall order, and if you think of anything along the way that can make the process simpler and clearer, please let us know on our discussion forum. The same goes for questions that you might have, or edits that we should incorporate.

And now, without further ado ...

Introduction to the Ecstasy Language

Ecstasy is a programming language that will seem very familiar in many ways to any programmer who knows Java, C#, or C++. Its syntax design is intentionally similar to these languages, with bits and pieces influenced by other languages in the same general family, including Kotlin, Ceylon, Scala, and even Python. We wanted the syntax to feel familiar, like a favorite well-worn glove. And we wanted it to be a joy to learn and use as a new language, both in its familiarity, and also in its thoughtful innovations.

To illustrate this sense of familiarity, we're pretty sure that you won't need a detailed explanation to grok the basics of this Ecstasy code:

Boolean done     = False;
Int     count    = 0;
String  greeting = "hello";

And while there may be a few details that raise questions, there is probably nothing baffling about the Ecstasy "Hello World" example, either:

module HelloWorld {
    void run() {
        @Inject Console console;
        console.print("Hello World!");
    }
}

Yet this simple familiarity also hides some fundamental differences in the design of the Ecstasy execution model. It is this model that we will briefly describe here, before we proceed on to the easy aspects of the language like syntax and structure. This fundamental model is important to lay out up front, because understanding it will make almost everything else seem obvious.

We're going to fly through these topics, laying out statements of fact without many supporting details. We promise that each of these topics will be examined in depth later in this language guide, but for now, try to accept these statements as axioms of the language -- even if these points temporarily raise more questions than they answer.

Containers

All execution of Ecstasy code occurs within an Ecstasy container; if Ecstasy code is running, it is running inside of an Ecstasy container.

An Ecstasy container has a type system; all Ecstasy code running in a container is defined by and is part of that type system.

Ecstasy containers are arranged hierarchically, like directories in a file system. But unlike a file system, it is not possible to navigate from a nested container up to its parent container; a parent container is invisible and inaccessible to any of the containers nested within it. Like directories, nesting is recursive, and can be arbitrarily deep.

Type systems are both closed and immutable. Closed means that there are no unknown or unresolved types; it is illegal for a type system to not be fully consistent with itself, and the type compositions in the type system are fully structurally verifiable by the rules of the XVM. Immutable means that new code cannot be introduced within a type system on the fly; once a container is created, its type system is immutable.

New type systems can be created on the fly, and new nested containers can be created on the fly using those new type systems. As a result, it is possible to safely introduce new code on the fly into a running system, in the form of a new container.

The design deliberately disallows each container from (1) accessing (or even knowing about) its parent container, and (ii) accessing its runtime environment, including the machine, the OS, the network, the local storage, and so on. Access to any necessary capabilities can be explicitly injected into the new container by the parent container. Capabilities are injected by interface, such that no other reflective details exist, so that even using programmatic reflection, only the members of the injected interface will be present on the injected object.

Ecstasy does not have a Foreign Function Interface (FFI); this was a careful and purposeful decision. No native code can exist inside of an Ecstasy container. Native code, including all OS capabilities, is always represented within a container via injected interfaces.

Security is the fundamental principle of the Ecstasy container model.

Type System

A type system is composed from modules.

Ecstasy is class based; all objects are of a class, and all classes come from modules.

In Ecstasy, everything is an object, and everything has a type.

The Ecstasy language provides a core "ecstasy" module, which is automatically present in every type system. This module contains all of the fundamental classes defined by the Ecstasy language, including integers, booleans, character strings, arrays, and so on. Even the most basic types in Ecstasy are classes, and even the most basic values are objects.

Immutability

Objects can be immutable. Immutable state cannot be modified.

An object can be instantiated immutable, or an object can be made immutable. Once an object is immutable, it cannot be made mutable again.

Whether or not an object is immutable is a property of that object. Object immutability is also indicated by the type system: An immutable object will be of an immutable type.

An immutable type requires each instance of that type to be immutable. (There is no corresponding concept of a mutable type that requires each instance of that type to be mutable.)

Services

Each service belongs to a container.

All execution of Ecstasy code occurs within an Ecstasy service; if Ecstasy code is running, it is running as part of an Ecstasy service inside of an Ecstasy container.

Each container is a service.

Each service is an object, and has a class.

Any object passed into a service from another service goes through a service boundary. The only objects that can go through a service boundary are (i) proxies to other services, and (ii) immutable objects.

When a call is made to a service proxy, all call arguments and subsequent return values are passed through the service boundary. The XVM can pass immutable objects through the service boundary either by-reference or by-value, precisely because the objects are immutable. Services on the other hand are always passed by-proxy through the service boundary. Calls made to a service proxy always pass through the service boundary to the service represented by the proxy.

With respect to all other services, each service executes asynchronously, concurrently, and when possible, in parallel.

A service represents a mutable domain of state. Mutable information can neither leave nor enter a service. All access to mutable state and all mutation of state occurs within the service that owns that state. To access or modify state that is owned by a different service, those operations occur by proxy.

Fibers

When a call is made to a service proxy, the calling service creates a future result, and the service that is being called creates a new fiber on which the incoming call will execute.

All execution of Ecstasy code occurs on a fiber within an Ecstasy service; if Ecstasy code is running, it is running on a fiber as part of an Ecstasy service inside of an Ecstasy container.

A call to a service proxy can be either synchronous or asynchronous; a synchronous call will block the calling fiber until a result is received from the called service.

Within any service, at most one fiber is permitted to be actively executing code at any one time; there is no parallel execution within a single service. Many fibers can exist concurrently within a service; when the currently executing fiber blocks or completes, another fiber within that service may begin executing.

When a fiber completes, its result (return values or exception) will complete the future that was created within the calling service. If the caller made a synchronous (i.e. blocking) call to the service proxy, then the fiber on the calling service that made the call will receive the result and will then be able to continue executing.

A normal result from a synchronous call to a service will transfer all returned value(s) to the calling fiber. An exceptional result from a synchronous call to a service will raise a corresponding exception on the calling fiber.

Moving on ...

That list of axioms may seem overwhelming at first, but the design will become increasingly obvious as each new concept is explained. Don't be afraid to come back and re-read this section from time to time as you work through the subsequent chapters.

Next: Creating your first module