.|byExample

Acton is a general purpose programming language designed to be useful for a wide range of applications. It provides a persistent programming environment, alleviating the need for manually persisting program state and can run in a distributed mode for fault tolerance and scaling.

Acton by Example is a collection of runnable examples that illustrate various Acton concepts. Follow along by installing Acton locally.

Some previous experience with programming languages is probably helpful.

Hello World

We follow tradition and introduce Acton with the following minimal example

Source:

# This is a comment, which is ignored by the compiler.

# This is the main actor, which we will tell the compiler to use as the root
# actor by setting `--root main` when compiling the program
actor main(env):
    print("Hello World!")
    env.exit(0)

Compile and run:

actonc hello.act
./hello

Output:

Hello World!

Description

An executing Acton program consists of a collection of interacting actors. Here we have just a single actor, which has been given the name main and that acts as the root actor of our system. The root actor of a system takes a parameter env, which represents the execution environment. env has methods for accessing command line arguments and carries a reference to the capabilities of the surrounding world, WorldCap, for accessing the environment, e.g. reading from and writing to keyboard/screen and files, working with sockets etc. When the program is executed, the run time system creates the root actor, hands it the env and runs its initialization code, which here is just a single command, to print a message on screen.

The careful reader may ask why print is not a method of env. The answer is that we see print as such a ubiquitous function that it should be possible to use without having to thread an env parameter to all sub-units.

Shebang

While Acton is a compiled language and the actonc compiler produces an executable binary, script style execution is also possible through the use of a shebang line.

Source:

#!/usr/bin/env runacton

actor main(env):
    print("Hello World!")
    env.exit(0)

Ensure the executable bit is set and run your .act file directly:

chmod a+x hello.act
./hello.act

Output:

Hello Johan!

Program Arguments

Program arguments are available as the attribute argv on the env actor. env.argv is a list where the first element contains the name of the shell command and the second element being the first proper argument.

We can rewrite our program to print a user supplied name to greet rather than the world.

Source:

actor main(env):
    print("Hello " + env.argv[1] + "!")
    env.exit(0)

Compile and run, with argument:

actonc hello.act
./hello Johan

Output:

Hello Johan!

Acton Projects

Besides compiling individual .act files, it is possible to organize Acton source files into a an Acton Project, which simplifies many common tasks.

Use actonc to create a project called foo:

actonc new foo

Output:

Created project foo
Enter your new project directory with:
  cd foo
Compile:
  actonc build
Run:
  ./out/rel/bin/foo

Description

Use actonc build to build a project. The current working directory must be the project directory or a sub-directory to the project directory. actonc will discover all source files and compile them according to dependency order.

Add a main actor to any source file directly under src/ to produce an executable binary. For example, if src/hello.act contains a main actor, it will produce out/rel/bin/hello using main as the root actor.

Project directory structure

The directory structure of an Acton project follows a certain convention.

.
├── Acton.toml
├── build.sh
├── out
│   ├── dev
│   │   ├── bin
│   │   │   └── foo
│   │   └── lib
│   │       ├── foo.o
│   │       └── libActonProject.a
│   ├── rel
│   │   ├── bin
│   │   │   └── foo
│   │   └── lib
│   │       ├── foo.o
│   │       └── libActonProject.a
│   └── types
│       ├── foo.c
│       ├── foo.h
│       ├── foo.root.c
│       └── foo.ty
├── README.org
└── src
    └── foo.act

An Acton.toml file must be present in the project root, otherwise it is not considered a project.

src/ is used for all source files of the project. Use subdirectories to create hierarchies of modules, for example src/a/b.act is imported in an Acton program as import a.b.

Output goes into out/ with subdirectories out/rel/ and out/dev for release mode and development mode respectively. A project archive file (libActonProject.a) contains the compiled output of all modules, but is considered an internal implementation detail. Don't touch it.

Executable binaries go in out/dev/bin and out/rel/bin respectively. The name of the binary is the name of the module which contains the specified root actor. In the example above, the root actor is foo.main, i.e. actor main in the module foo and consequently, the executable name is foo.

Variable data types

Acton supports a plethora of primitive data types.

  • int integers, like 1, 2, 123512, -6542 or 1267650600228229401496703205376
    • int is arbitrary precision and can grow beyond machine word sizes
    • i16 is a fixed size signed 16 bit integer
    • i32 is a fixed size signed 32 bit integer
    • i64 is a fixed size signed 64 bit integer
    • u16 is a fixed size unsigned 16 bit integer
    • u32 is a fixed size unsigned 32 bit integer
    • u64 is a fixed size unsigned 64 bit integer
  • float 64 bit float, like 1.3 or -382.31
  • bool boolean, like True or False
  • str strings, like foo
    • strings support Unicode characters
  • lists like [1, 2, 3] or ["foo", "bar"]
  • dictionaries like {"foo": 1, "bar": 3}
  • tuples like (1, "foo")
  • sets like {"foo", "bar"}

In Acton, mutable state can only be held by actors. Global definitions in modules are constant. Assigning to the same name in an actor will shadow the global variable.

Source:

foo = 3    # this is a global constant and cannot be changed

def printfoo():
    print("global foo:", foo)  # this will print the global foo

actor main(env):
    # this sets a local variable with the name foo, shadowing the global constant foo
    foo = 4
    print("local foo, shadowing the global foo:", foo)
    printfoo()

    a = u16(1234)
    print("u16:", a)

    env.exit(0)

Output:

local foo, shadowing the global foo: 4
global foo: 3
u16: 1234

Scalars

Source:

actor main(env):
    i = 42       # an integer
    f = 13.37    # a float
    
    print(i)
    print(f)

    s = "Hello"  # a string
    print(s)
    # a slice of a string
    print(s[0:1])
    
    b = True     # a boolean
    print(b)
    
    env.exit(0)

Compile and run:

actonc scalars.act
./scalars

Output:

42
13.37
Hello
H
True

float

Source:

from math import pi

actor main(env):
    # round to 2 decimals
    a = round(pi, 2)

    # print 4 digits of pi
    print("%.4f" % pi)

    env.exit(0)

Output:

3.1416

Lists

Source:

actor main(env):
    l = ["foo", "foo"]
    l.append("bar")
    l.insert(0, "Firsty")
    l.append("banana")

    # Indexing starts at 0
    print("First item: " + l[0])
    # Negative index counts from the end
    print("Last item : " + l[-1])

    # Note how we need to explicitly cast the list to str before printing
    print("List items: " + str(l))
    # We can take a slice of a list with [start:stop] where the start index is
    # inclusive but the stop index is exclusive, just like in Python.
    # A slice of a list is also a list, so cast to str.
    print("A slice   : " + str(l[2:4]))
    
    print("Pop first item:", l.pop(0))
    print("Pop last item:", l.pop(-1))
    print("List items:", str(l))
    
    # Extend a list by adding another list to it
    l.extend(["apple", "orange"])
    print("List items:", str(l))
    
    unsorted_list = [9, 5, 123, 14, 1]
    sl = sorted(unsorted_list)
    print("Sorted list", str(sl))

    # Reverse a list inplace
    l.reverse()
    print("Reversed:", l)
    
    # Get a shallow copy of the list
    l2 = l.copy()
    print("Copy:", l2)
    
    # Clear list
    l.clear()
    print("List after clear:", str(l))

    env.exit(0)

Compile and run:

actonc lists.act
./lists

Output:

First item: Firsty
Last item : banana
List items: ['Firsty', 'foo', 'foo', 'bar', 'banana']
A slice   : ['foo', 'bar']
Pop first item: Firsty
Pop last item: banana
List items: ['foo', 'foo', 'bar']
List items: ['foo', 'foo', 'bar', 'apple', 'orange']
Sorted list [1, 5, 9, 14, 123]
Reversed: ['orange', 'apple', 'bar', 'foo', 'foo']
Copy: ['orange', 'apple', 'bar', 'foo', 'foo']
List after clear: []
  • All items in a list must be of the same type
    • It is not allowed to mix, like ["foo", 1] leads to a compiler error

Dictionaries

Source:

actor main(env):
    d = {"foo": 1, "bar": 2}
    d["cookie"] = 3

    print("Dict: " + str(d))
    print("len : " + str(len(d)))
    print("item foo: " + str(d["foo"]))
    print("item foo: " + str(d.get("foo", "default")))
    print("get default value when key nonexistent: " + str(d.get("foobar", "DEF_val")))
    
    d["bar"] = 42
    del d["foo"]
    print("Dict: " + str(d))
    
    print("Dict keys: " + str(list(d.keys())))
    print("Dict values: " + str(list(d.values())))

    print("Dict items:")
    for k, v in d.items():
        print("  dict key " + k + "=" + str(v))

    print("Pop last item:", d.popitem())
    print("Dict after .popitem():", d)

    env.exit(0)

Compile and run:

actonc dicts.act
./dicts

Output:

Dict: {"foo":1, "bar":2, "cookie":3}
len : 3
item foo: 1
item foo: 1
get default value when key nonexistent: DEF_val
Dict: {"bar":42, "cookie":3}
Dict keys: ["bar", "cookie"]
Dict values: [42, 3]
Dict items:
  dict key bar=42
  dict key cookie=3
Pop last item: ("cookie", 3)
Dict after .popitem(): {"bar":42}

Tuples

Source:

actor main(env):
    # Items in a tuple can be of different types
    t = ("foo", 42)
    # Fields are accessed by their index and using field / attribute selection style:
    print(t)
    print(t.0)
    print(t.1)
    
    # Tuples can use named fields
    nt = (a="bar", b=1337)
    print(nt)
    print(nt.a)
    print(nt.b)
    
    r = foo()
    if r.b:
        print(r.c)

    env.exit(0)
    
def foo() -> (a: str, b: bool, c: int):
    """A function that returns a tuple with fields name a and b
    """
    return (a = "hello", b=True, c=123)

Compile and run:

actonc sets.act
./sets

Output:

('foo', 42)
foo
42
('bar', 1337)
bar
1337
123
  • fields in a tuple can be of different types
  • tuples have a fixed fields
  • tuples with named fields is like an anonymous data class, i.e. the data type itself has no name
  • tuples with named fields can be used like a simple record type

Sets

Source:

actor main(env):
    # set syntax is similar to dicts using {} but without keys
    s = {"foo", "bar"}
    print("Set content:", s)
    if "foo" in s:
        print("'foo' is in the set")
    if "a" not in s:
        print("'a' is not in the set")
    # Adding an item that is already in the set does nothing
    s.add("foo")
    print("Set without duplicate 'foo':", s)
    s.add("a")
    print("Set after adding 'a':", s)
    if "a" in s:
        print("'a' is in the set now")
    print("Entries in set:", len(s))
    s.discard("foo")
    print("Set after discarding 'foo':", s)

    # Use set() to create an empty set. {} means an empty dict!
    empty_set = set()

    env.exit(0)

Compile and run:

actonc sets.act
./sets

Output:

Set content: {'bar', 'foo'}
'foo' is in the set
'a' is not in the set
Set without duplicate 'foo': {'bar', 'foo'}
Set after adding 'a': {'bar', 'a', 'foo'}
'a' is in the set now
Entries in set: 3
Set after discarding 'foo': {'bar', 'a'}

Functions

Functions are declared using the def keyword.

Use return foo to return variable foo. If no return keyword is used or a lone return without argument is given, the function will return None.

Source:

def multiply(a, b):
    print("Multiplying", a, "with", b)
    return a*b
    
actor main(env):
    result = multiply(3, 4)
    print("Result:", result)
    env.exit(0)

Output:

Multiplying 3 with 4
Result: 12

Actor methods

Actor methods are declared under an actor using the def keyword.

An actor method runs in the context of the actor and can access its private state. As Actors are sequential processes, calling other methods on the local actor or any function is going to be run sequentially.

Calling an actor method on the local actor can be done simply by calling it by its name, without any prefix such as self..

All actor methods are public. Call a method on another actor by calling actor_name.method_name(). Calling methods on other actors can be done synchronously or asynchronously.

Source:


def multiply(a, b):
    print("Multiplying", a, "with", b)
    return a*b
    
actor main(env):
    var secret = 42

    def compute(a):
        print("Computing result based on our secret", secret)
        res = multiply(a, secret)
        return res

    result = compute(3)
    print("Result:", result)
    env.exit(0)

Output:

Computing result based on our secret 42
Multiplying 3 with 42
Result: 126

Higher order functions

Acton supports higher order functions which means you can pass a function as an argument to another function.

Source:

def multiply_with_3(a):
    print("Multiplying with 3")
    return 3*a

def multiply_with_42(a):
    print("Multiplying with 42")
    return 42*a

def compute(a, fun):
    """Compute value from a using function fun"""
    return fun(a)
    
actor main(env):
    print( compute(7, multiply_with_3) )
    print( compute(7, multiply_with_42) )
    env.exit(0)

Output:

Multiplying with 3
21
Multiplying with 42
294

Actors

Actors is a key concept in Acton. Each actor is a small sequential process with its own private state. Actors communicate with each other through messages, in practice by calling methods on other actors or reading their attributes.

Source:

# An actor definition
actor Act(name):

    # Top level code in an actor runs when initializing an actor instance, like
    # __init__() in Python.
    print("Starting up actor " + name)
    
    def hello():
        # We can directly access actor arguments, like `name`
        print("Hello world from " + name)

actor main(env):
    # Create an actor instance a of Act
    a = Act("FOO")
    # Call the actor method hello
    await async a.hello()

    env.exit(0)

Compile and run:

actonc actors.act
./actors

Output:

Starting up actor FOO
Hello world from FOO

Root Actor

Like C has a main() function, Acton has a root actor. To compile a binary executable, there must be a root actor. Per default, if a source (.act) file contains an actor named main, it will be used as the root actor but it can also be specified using the --root argument. While the convention is to call the root actor main, you are free to name it anything.

Given this Acton program:

actor main(env):
    print("Hello World!")
    env.exit(0)

The following actonc commands will all produce the same output.

actonc hello.act
actonc hello.act --root main
actonc hello.act --root hello.main

The first invocation relies on the default rule of using an actor called main. The second invocation explicitly specifies that we want to use main as the root actor while the third uses a qualified name which includes both the actor name (main) as well as the module name (hello). Using qualified names can be particularly useful when building executable binaries in projects.

A normal Acton program consists of many actors that are structured in a hierarchical tree. The root actor is at the root of the tree and is responsible for starting all other actors directly or indirectly. The Acton Run Time System (RTS) will bootstrap the root actor.

rootAB12345

Any executable Acton program must have a root actor defined. Acton libraries (that are included in an another Acton program), do not need a root actor.

Lifetime

The main function in most imperative and functional programming languages start at the top and when they reach the end of the function, the whole program exits. Actors do not exit on their own. Once an actor has been created, it runs the initialization code contained in the body of the actor, after which it will wait for incoming messages in the form of actor methods calls.

This means that a simple program like this modified helloworld (the env.exit() call has been removed) will run indefinitely.

Source:

actor main(env):
    print("Hello world!")

Compile and run:

actonc noexit.act

Output:

$ ./noexit
<you will never get your prompt back>

Actor Attributes & Constants

Actors typically contain some private state. We define variable attributes at the top level in the actor using the var keyword and can then access them from any method within the local actor. Note how self. is not needed. Private variables are not visible from other actors.

Source:

actor Act():
    var something = 40  # private actor variable attribute
    fixed = 1234        # public constant
    
    def hello():
        # We can access local actor variable attributes directly, no need for 
        # self.something or similar
        something += 2
        print("Hello, I'm Act & value of 'something' is: " + str(something))

actor main(env):
    actor1 = Act()
    await async actor1.hello()
    print("Externally visible constant: ", actor1.fixed)
    # This would give an error, try uncommenting it
    # print(actor1.something)

    env.exit(0)

Compile and run:

actonc attrs.act

Output:

Hello, I'm Act & value of 'something' is: 42
Externally visible constant:  1234

Without the var keyword, an actor attribute is a constant. As constants are not mutable, it is safe to make it visible to other actors and it can be accessed like an attribute on an object.

Actor concurrency

Multiple actors run concurrently. In this example we can see how the two actors Foo and Bar run concurrently. The main actor is also running concurrently, although it doesn't do anything beyond creating the Foo and Bar actors and exiting after some time.

Source:

actor Counter(name):
    var counter = 0

    def periodic():
        print("I am " + name + " and I have counted to " + str(counter))
        counter += 1

        # 'after 1' tells the run time system to schedule the specified
        # function, in this case periodic(), i.e. ourselves, after 1 second
        after 1: periodic()

    # First invocation of periodic()
    periodic()


actor main(env):
    # Create two instances of the Counter actor, each with a unique name
    foo = Counter("Foo")
    bar = Counter("Bar")
    
    def exit():
        env.exit(0)

    # exit the whole program after 10 seconds
    after 10: exit()

Compile and run:

actonc concurrency.act
./concurrency

Output:

I am Foo and I have counted to 0
I am Bar and I have counted to 0
I am Foo and I have counted to 1
I am Bar and I have counted to 1
I am Foo and I have counted to 2
I am Bar and I have counted to 2
I am Foo and I have counted to 3
I am Bar and I have counted to 3
I am Foo and I have counted to 4
I am Bar and I have counted to 4
I am Bar and I have counted to 5
I am Foo and I have counted to 5
I am Bar and I have counted to 6
I am Foo and I have counted to 6
I am Bar and I have counted to 7
I am Foo and I have counted to 7
I am Foo and I have counted to 8
I am Bar and I have counted to 8
I am Foo and I have counted to 9
I am Bar and I have counted to 9

Sync Method calls

While async is good for performance it makes it somewhat convoluted, forcing use of callbacks, to just return a value. Acton makes it possible to call other actors in a synchronous fashion for ease of use.

A method is called synchronously when the return value is used.

Source:

import acton.rts

actor DeepT():
    def compute():
        # some heavy computation going on
        acton.rts.sleep(1)
        return 42

actor main(env):
    d1 = DeepT()

    answer = d1.compute()
    print("The answer is", answer)

    env.exit(0)

Compile and run:

actonc sync.act

Output:

The answer is 42

The call flow can be illustrated like this. We can see how the execution of main is suspended while it is waiting for the return value from actor d1.

maind1

While synchronous is bad because we block waiting for someone else, we are only ever going to wait for another actor to run its method. There is never any wait for I/O or other indefinite waiting, only blocking wait for computation within the Acton system. This is achieved by the lack of blocking calls for I/O, thus even if there is a chain of actors waiting for each other

Async Method calls

As actors are sequential programs and can only do one thing at a time, it is important not to spend time waiting in a blocking fashion. Acton leverages asynchronous style programming to allow actors to react and run only when necessary. Async is at the core of Acton!

A method is called asynchronously when the return value is not used.

Source:


def nsieve(n: int):
    """Sieve of Erathostenes to find primes up to n
    """
    count = 0
    flags = [True] * n
    for i in range(2, n, 1):
        if flags[i]:
            count += 1
            for j in range(i, n, i):
                flags[j] = False
    return count

actor Simon(idx):
    def say(msg, n):
        # Simon likes to compute primes and will tell you how many there are under a given number
        count = nsieve(n)
        print("Simon%d says: %s.... oh and there are %d primes under %d" % (idx, msg, count, n))

actor main(env):
    s1 = Simon(1)
    s2 = Simon(2)

    s1.say("foo", 1000000)
    s2.say("bar", 5)

    def exit():
        env.exit(0)
    after 0.2: exit()

Compile and run:

actonc async.act

Output:

Simon2 says: bar.... oh and there are 2 primes under 5
Simon1 says: foo.... oh and there are 78498 primes under 1000000

A method call like s1.say("foo", 100000) does not use the return value of and is thus called asynchronously. We ask s1 to compute primes under 1000000 while s2 only gets to compute primes up to 5 which will invariably run faster. Thus, s2 despite being called after s1, will print out its result before s1. The s1 and s2 actors are called asynchronously and are executed concurrently and in parallel.

The call flow can be illustrated like this. We can see how main asynchronously calls s1 and s2 that will be scheduled to run concurrently. The run time system (RTS) will run s1.say() and s2.say() in parallel if there are 2 worker threads available. Per default, there are as many worker threads as available CPU threads.

mains2s1after2

In addition we see how the call to after 2 schedules the main actor to run again after 2 seconds, specifically it will run the main.exit() method, which in turn exists the whole program.

Control flow

A crucial part in any imperative program is to control the flow of execution. Acton supports a number of different constructs for this:

Control flow in an async actor world

The basic control flow of most programming languages involve a starting point, like a main function, which is run from top to bottom, after which the program implicitly terminates. The basic objective is to feed instructions to the CPU and this goal remains through increasing levels of abstractions. Acton is different. Once created, an actor will simply remain indefinitely, waiting for incoming messages in the form of actor method calls. See Actor Lifetime.

A mental model of actors

Actors in an Acton program form a vast web of interconnected actors. Some actors are on the edge of the Acton realm, bordering to the external world where they may be doing I/O with external entities, through files, sockets or other means. All I/O is callback based and thus event driven and reactive. When there is an event, an actors reacts, perhaps initiating calls to other actors. A ripple runs across the web of actors, each reacting to incoming messages and acting accordingly.

if / elif / else

Acton supports the if / elif / else construct - the corner stone of programming control flow.

The conditionals evaluated by if / elif / else are expressions.

Source:


def whatnum(n):

    if n < 0:
        print(n, "is negative")
    elif n > 0:
        print(n, "is positive")
    else:
        print(n, "is zero")


def inrange(n):
    if n < 10 and n > 5:
        print(n, "is between 5 and 10")
    else:
        print(n, "is outside of the range 5-10")

actor main(env):

    whatnum(5)
    whatnum(1337)
    whatnum(-7)
    whatnum(0)
    
    inrange(3)
    inrange(-7)
    inrange(7)

    env.exit(0)

Compile and run:

actonc if_else.act

Note that the output is random and you could get a different result.

Output:

5 is positive
1337 is positive
-7 is negative
0 is zero
3 is outside of the range 5-10
-7 is outside of the range 5-10
7 is between 5 and 10

for

Iteration is a core concept in programming and for loops are perhaps the most well known.

Acton supports the for in construct to iterate through an Iterator.

Source:


actor main(env):

    for n in range(1, 100, 1):
        if n % 15 == 0:
            print("fizzbuzz")
        elif n % 3 == 0:
            print("fizz")
        elif n % 5 == 0:
            print("buzz")
        else:
            print(n)

    env.exit(0)

Compile and run:

actonc while.act

Note that the output is random and you could get a different result.

Output:

1
2
fizz
4
buzz
fizz
7
8
fizz
buzz
11
fizz
13
14
fizzbuzz
16
17
fizz
19
buzz
fizz
22
23
fizz
buzz
26
fizz
28
29
fizzbuzz
31
32
fizz
34
buzz
fizz
37
38
fizz
buzz
41
fizz
43
44
fizzbuzz
46
47
fizz
49
buzz
fizz
52
53
fizz
buzz
56
fizz
58
59
fizzbuzz
61
62
fizz
64
buzz
fizz
67
68
fizz
buzz
71
fizz
73
74
fizzbuzz
76
77
fizz
79
buzz
fizz
82
83
fizz
buzz
86
fizz
88
89
fizzbuzz
91
92
fizz
94
buzz
fizz
97
98
fizz

while

The while construct can be used to run a loop while a condition is true.

Source:

import random

def throw_dice():
    number = random.randint(1,6)
    print("Dice:", number)
    return number

actor main(env):
    var luck = True

    while luck:
        if throw_dice() == 4:
            # ran out of luck, nobody likes a 4
            luck = False

    env.exit(0)

Compile and run:

actonc while.act

Note that the output is random and you could get a different result.

Output:

Dice: 3
Dice: 1
Dice: 5
Dice: 2
Dice: 4

after and sleep

In many languages, it is fairly common to use sleep() for things like timeouts, pacing and similar. In Acton, and more generally in async actor based languages, sleeping is frowned upon and often not even available.

The idiomatic control pattern in Acton is using after, like after 42.1337: foo(). This tells the run time system (RTS) to schedule the execution of the foo() function after 42.1337 seconds. Meanwhile, other methods on the actor can be invoked.

Source:

import time

"""Pace the sending of messages to once a second
"""

actor Receiver():
    def recv(msg):
        print("At " + str(time.now()) + ", I received a message:", msg)

actor main(env):
    var i = 0
    r = Receiver()

    def send_msg():
        # Send a message, increment i
        r.recv("Hello " + str(i))
        i += 1

        # ... and reschedule execution of ourselves in 1 second
        after 1: send_msg()
        
        # Exit after awhile
        if i > 4:
            env.exit(0)
        
    # Kick off the whole thing
    send_msg()

Compile and run:

actonc after_pace.md

Since the output includes time, you will naturally get a slightly different result if you run this.

Output:

At 2023-05-16T10:08:59.135806428+02, I received a message: Hello 0
At 2023-05-16T10:09:00.136484032+02, I received a message: Hello 1
At 2023-05-16T10:09:01.135585727+02, I received a message: Hello 2
At 2023-05-16T10:09:02.135695030+02, I received a message: Hello 3
At 2023-05-16T10:09:03.135811176+02, I received a message: Hello 4

There is in fact a sleep function in Acton, hidden away in the acton.rts module. Do NOT use it! It is intended only for debugging of the RTS itself and will probably disappear from the standard library before 1.0. Despite it, we consider the language to not have a sleep.

Actors should either be actively processing or at rest. Conceptually, a sleep is an active wait, in the sense that the RTS will just sit there waiting for the sleep to finish, it is blocked, while it really could process something else in between, like run a different actor method continuation. Similarly, the actor itself could have had other methods on it invoked instead of being actively blocked on a sleep. Being blocked is very bad, which is why all I/O is asynchronous in Acton and why there is no sleep.

sleep is evil, use after!

Types

Acton is a statically typed language. Unlike traditional languages like C or Java, where the type of every variable must be explicitly stated, we can write most Acton programs without types. The Acton compiler features a powerful type inferencer which will then infer the type.

This program does not have any explicit types specified.

Source:

def foo(a, b):
    if a > 4:
        print(len(b))

actor main(env):
    i1 = 1234      # inferred type: int
    s1 = "hello"   # inferred type: str
    foo(i1, s1)

    env.exit(0)

To see the inferred types of an Acton program, use the --sigs option of the compiler. As the name suggests, this will print out the type signatures for functions, actors and their attributes and methods in the specified Acton module.

Print type signatures with --sigs:

actonc types.act --sigs

Output:

#################################### sigs:

foo : [A(Ord, Number)] => (A, Collection[B]) -> None

actor main (Env):
    i1 : int
    s1 : str

Explicit types

It is possible to explicitly specify the types of variables or arguments in a function. The syntax is similar to type hints in Python.

Source:

# 'a: int' means the first argument `a` should be of type int
# 'b: str means the second argument `b` should be of type str
# The function returns nothing
def foo(a: int, b: str) -> None:
    print(a, b)
    
# A functions type signature can also be written on a separate line
bar : (int, str) -> None
def bar(a, b):
    print(a, b)

actor main(env):
    # i1 is explicitly specified as an integer while s1 is a str
    i1 : int = 1234
    s1 : str = "hello"
    foo(i1, s1)
    bar(i1, s1)

    # The literal value 1234 is an integer, so when we assign it to i2, the
    # compiler can easily infer that the type of i2 is int. Similarly for s2
    # since the literal value "hello" is clearly a string
    i2 = 1234
    s2 = "hello"
    foo(i2, s2)
    bar(i2, s2)
    
    env.exit(0)

Compile and run:

actonc types.act
./types

Output:

1234 hello
1234 hello
1234 hello
1234 hello

Try changing the type of i1 or s1 and you will find that the compiler complains that it cannot solve the type constraints of the program.

Security

Security is an important part of application development and is best considered throughout the entire design and development time of an application rather than as an bolted-on after-thought.

In Acton, the separation of actors offers the primary means of security. Access to actors (like being able to call their methods) requires a reference to the relevant actor. Anyone with a reference can access the actor in question. It is not possible to forge a reference.

This is similar to the object capability (OCAP) model.

Since there are no global variables, the only reachable state is local to an actor or reachable via a reference to another actor. This means you cannot reach something out of thin air. You have to be explicitly passed a reference to anything you need to access.

The security model based on capability references extends for accessing the world outside of the Acton system.

actor Foo():
    def foo():
        print("foofoo")
    
actor Bar():
    # Without a reference to f we cannot call its foo() function
    
actor main(env):
    f = Foo()
    f.foo()
    b = Bar()

Capabilities to access outside world

Any interesting program will need to interact with the outside world, like accessing the network or reading files. In C and many other languages, it is possible for any function at any time to simply make calls and access the external world, like read a file (maybe your private SSH key and send it over the network). Acton makes all such access to the outside world explicit through capability references.

In an Acton program, having a reference to an actor gives you the ability to do something with that actor. Without a reference, it is impossible to access an actor and it is not possible to forge a reference. This provides a simple and effective security model that also extends to accessing things outside of the Acton system, like files or remote hosts over the network.

Things outside of the actor world are represented by actors and to access such actors, a capability reference is required. For example, we can use TCPConnection to connect to a remote host over the network using TCP. The first argument is of the type TCPConnectCap, which is the capability of using a TCP socket to connect to a remote host. This is enforced by the Acton type system. Not having the correct capability reference will lead to a compilation error.

TCPConnectCap is part of a capability hierarchy, starting with the generic WorldCap and becoming further and further restricted:

WorldCap >> NetCap >> TCPCap >> TCPConnectCap

The root actor (typically main()) takes as its first argument a reference to Env, the environment actor. env.cap is WorldCap, the root capability for accessing the outside world.

import net

actor main(env):

    def on_connect(c):
        c.close()

    def on_receive(c, data):
        pass

    def on_error(c, msg):
        print("Client ERR", msg)

    connect_cap = net.TCPConnectCap(net.TCPCap(net.NetCap(env.cap)))
    client = net.TCPConnection(connect_cap, env.argv[1], int(env.argv[2]), on_connect, on_receive, on_error)

Capability based privilege restriction prevent some deeply nested part of a program, perhaps in a dependency to a dependency, to perform operations unknown to the application author. Access to capabilities must be explicitly handed out and a program can only perform operations based on the capabilities it has access to.

Restrict and delegate

Functions and methods taking a Cap argument normally takes the most restricted or refined capability. In the example with setting up a TCP connection, it is the TCPConnectCap capability we need, which is the most restricted.

Rather than handing over WorldCap to a function, consider what capabilities that function actually needs and only provide those. If a library asks for wider capabilities than it needs, do not use it.

Capability friendly interfaces

As a library author, you should only require precisely the capabilities that the library requires. Do not be lazy and require WorldCap. If the library offers multiple functionalities, for example logging to files or to a remote host, strive to make parts optional such that it the application developer and choose to only use a subset and only provide the capability required for that subset.

Standard library

Regular expressions

Source:

import re

actor main(env):
    m = re.match(r"(foo[a-z]+)", "bla bla foobar abc123")
    if m is not None:
        print("Got a match:", m.group[1])

    env.exit(0)

Output:

Got a match: foobar

Modules

Acton modules can be used to hierarchically structure programs by splitting code into smaller logical units (modules).

Import modules by using the import keyword. The module will be available with its fully qualified name.

Use from .. import to import a single function from a module.

Functions and modules can be aliased using the as keyword.

import time
import time as timmy
from time import now
from time import now as rightnow

actor main(env):
    time.now()         # using the fully qualified name
    timmy.now()        # using aliased module name
    now()              # using the directly imported function
    rightnow()         # using function alias

    env.exit(0)

Remember, all state in an Acton program must be held by actors. There can be no mutable variables in an Acton module, only constants! Similarly, there can be no global instantiation code in a module.

Compilation

Acton is a compiled language and as such, outputs binary executables. It is possible to influence the compilation process in various ways.

Optimized for native CPU features

The default target is somewhat conservative to ensure a reasonable amount of compatibility. On Linux, the default target is GNU Libc version 2.27 which makes it possible to run Acton programs on Ubuntu 18.04 and similar old operating systems. Similarly, a generic x86_64 CPU is assumed which means that newer extra CPU instruction sets are not used.

To compile an executable optimized for the local computer, use --target native. In many cases it can lead to a significant faster program, often running 30% to 100% faster.

Statically linked executables using musl for portability

On Linux, executable programs can be statically linked using the Musl C library, which maximizes portability as there are no runtime dependencies at all.

To compile an executable optimized for portability using musl on x86_64, use --target x86_64-linux-musl.

A default compiled program is dynamically linked with GNU libc & friends

$ actonc helloworld.act
Building file helloworld.act
  Compiling helloworld.act for release
   Finished compilation in   0.013 s
  Final compilation step
   Finished final compilation step in   0.224 s
$ ldd helloworld
        linux-vdso.so.1 (0x00007fff2975b000)
        libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007f11f472a000)
        libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f11f4725000)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f11f4544000)
        libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f11f453f000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f11f4827000)
$

A program linked statically towards Musl has no run time dependencies:

$ actonc helloworld.act --target x86_64-linux-musl
Building file helloworld.act
  Compiling helloworld.act for release
   Finished compilation in   0.013 s
  Final compilation step
   Finished final compilation step in   0.224 s
$ ldd helloworld
        not a dynamic executable
$

Although untested, static linking with musl should work on other CPU architectures.

MacOS does not support static compilation.

Cross-compilation

Acton supports cross-compilation, which means that it is possible to run develop on one computer, say a Linux computer with an x86-64 CPU but build an executable binary that can run on a MacOS computer.

Here's such an example. We can see how per default, the output is an ELF binary for x86-64. By setting the --target argument, actonc will instead produce an executable for a Mac.

$ actonc --quiet helloworld.act
$ file helloworld
helloworld: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.0.0, with debug_info, not stripped
$ actonc --quiet helloworld.act --target x86_64-macos-none
$ file helloworld
helloworld: Mach-O 64-bit x86_64 executable, flags:<NOUNDEFS|DYLDLINK|TWOLEVEL|PIE>

It is not only possible to compile for other operating systems, but also for other CPU architectures. For example, use --target aarch64-macos-any to produce a binary executable for an Apple M1/M2 CPU.

Prebuilt libraries

Acton ships with prebuilt libraries for the local platforms default target, i.e. if you install Acton on a x86-64 Linux machine, it will have libraries prebuilt for x86_64-linux-gnu.2.27. The default target uses these prebuilt libraries which results in a fast build:

$ actonc helloworld.act
Building file helloworld.act
  Compiling helloworld.act for release
   Finished compilation in   0.013 s
  Final compilation step
   Finished final compilation step in   0.224 s
$

When targeting something that is not the default target, the entire Acton system, including builtins, the run time system, the standard library and external library dependencies is built from source and can take a significant amount of time. The build process is highly parallelized and cached. For example, on an AMD 5950X with 16 cores / 32 threads, it takes around 7 seconds to do a complete rebuild for a small Acton program as can be seen here:

$ actonc helloworld.act --target aarch64-macos-none
Building file helloworld.act
  Compiling helloworld.act for release
   Finished compilation in   0.012 s
  Final compilation step
   Finished final compilation step in   6.847 s
$

Build cache

In an Acton project, there is a build cache, is is stored in a directory called build-cache in the project directory. The cache is always used for the project local files. If a non-default --target is being used, the built output of the Acton system is also stored in the cache, which means that it is only the first time around that it is slow. Any subsequent build is going to use the cache and run very fast. Like in this example, where the first invocation takes 6.120 seconds and the second one runs in 0.068 seconds.

$ actonc new hello
Created project hello
Enter your new project directory with:
  cd hello
Compile:
  actonc build
Run:
  ./out/rel/bin/hello

Initialized empty Git repository in /home/kll/hello/.git/
$ cd hello/
$ actonc build --target native
Building project in /home/kll/hello
  Compiling hello.act for release
   Finished compilation in   0.012 s
  Final compilation step
   Finished final compilation step in   6.120 s
$ actonc build --target native
Building project in /home/kll/hello
  Compiling hello.act for release
   Already up to date, in    0.000 s
  Final compilation step
   Finished final compilation step in   0.068 s
$

When compiling standalone .act files, there is no project and thus no persistent cache, so using a custom --target will always incur a penalty.

Run Time System

The Acton Run Time System is what sets up the environment in which an Acton program runs. It performs bootstrapping of the root actor. The worker threads that carry out actual execution of actor continuations are part of the RTS. It is the RTS that handles scheduling of actors and the timer queue. All I/O is handled between modules in the standard library in conjunction with the RTS.

Arguments

It is possible to configure the RTS through a number of arguments. All arguments to the RTS start with --rts-. Use --rts-help to see a list of all arguments:

$ actonc examples/helloworld.act
Building file examples/helloworld.act
  Compiling helloworld.act for release
   Finished compilation in   0.012 s
  Final compilation step
   Finished final compilation step in   0.198 s
$ examples/helloworld --rts-help
The Acton RTS reads and consumes the following options and arguments. All
other parameters are passed verbatim to the Acton application. Option
arguments can be passed either with --rts-option=ARG or --rts-option ARG

  --rts-debug                       RTS debug, requires program to be compiled with --dev
  --rts-ddb-host=HOST               DDB hostname
  --rts-ddb-port=PORT               DDB port [32000]
  --rts-ddb-replication=FACTOR      DDB replication factor [3]
  --rts-node-id=ID                  RTS node ID
  --rts-rack-id=RACK                RTS rack ID
  --rts-dc-id=DATACENTER            RTS datacenter ID
  --rts-host=RTSHOST                RTS hostname
  --rts-help                        Show this help
  --rts-mon-log-path=PATH           Path to RTS mon stats log
  --rts-mon-log-period=PERIOD       Periodicity of writing RTS mon stats log entry
  --rts-mon-on-exit                 Print RTS mon stats to stdout on exit
  --rts-mon-socket-path=PATH        Path to unix socket to expose RTS mon stats
  --rts-no-bt                       Disable automatic backtrace
  --rts-log-path=PATH               Path to RTS log
  --rts-log-stderr                  Log to stderr in addition to log file
  --rts-verbose                     Enable verbose RTS output
  --rts-wthreads=COUNT              Number of worker threads [#CPU cores]

$

Worker threads

Per default, the RTS starts as many worker threads as there are CPU threads available, although at least 4. This is optimized for server style workloads where it is presumed that the Acton program is the sole program consuming considerable resources. When there are 4 and more CPU threads available, the worker threads are pinned to each respective CPU thread.

It is possible to specify the number of worker threads with --rts-wthreads=COUNT.

Actor method continuations run to completion, which is why it is wise not to set this value too low. Per default a minimum of 4 threads are started even when there are fewer CPU threads available, which means the operating system will switch between the threads inducing context switching overhead.