Acton is a advanced general purpose programming language offering functional and object-oriented style of programming based on the actor-model and async I/O. Type safe and with capabilities based security, Acton is statically compiled for high performance and portability. In other words, pretty much perfect ;) We hope you enjoy it as much as we do. It's readily available to build anything from advanced "shell scripts" to low level databases.
Unique among programming languages, Acton offers orthogonal persistence, which means you don't have to think about how to persist data, or rather the state of your program, for durability. Acton will do it for you, using its fault-tolerant distribute database. Pretty damn cool!
Hello World
We follow tradition and introduce Acton with the following minimal example
Source:
# This is a comment, which is ignored by the compiler.
# An actor named 'main' is automatically discovered and recognized as the root
# actor. Any .act file with a main actor will be compiled into a binary
# executable and the main actor becomes the starting point.
actor main(env):
print("Hello World!")
env.exit(0)
Compile and run:
acton hello.act
./hello
Output:
Hello World!
Description
When an Acton program runs, it really consits of a collection of actors that interact with each other. In the above example, 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.
Installation
For Debian derivative distributions that use .dpkg and the APT ecosystem. Add the Acton APT repo and install from there:
wget -q -O - https://apt.acton-lang.io/acton.gpg | sudo apt-key add -
echo "deb [arch=amd64] http://apt.acton-lang.io/ stable main" | sudo tee /etc/apt/sources.list.d/acton.list
sudo apt-get update
sudo apt-get install -qy acton
Installation
Tip releases are built from the latest commit on the acton git repo main branch. They are built at least once a night, so can be thought of as nightlies but more up to date.
For Debian derivative distributions that use .dpkg and the APT ecosystem. Add the Acton APT tip repo and install from there:
wget -q -O - https://apt.acton-lang.io/acton.gpg | sudo apt-key add -
echo "deb [arch=amd64] http://aptip.acton-lang.io/ tip main" | sudo tee /etc/apt/sources.list.d/acton.list
sudo apt-get update
sudo apt-get install -qy acton
Shebang
While Acton is a compiled language and the acton
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 World!
Acton Projects
Besides compiling individual .act
files, it is possible to organize Acton code into an Acton Project, which is suitable once you have more than one .act
source code file.
Use acton
to create a new project called foo
:
acton new foo
Output:
Created project foo
Enter your new project directory with:
cd foo
Compile:
acton build
Run:
./out/bin/foo
Description
Use acton build
to build a project. The current working directory must be the project directory or a sub-directory to the project directory. acton
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/bin/hello
using main
as the root actor.
Running Tests
Writing tests is an integral part of writing software. In an Acton project, you can run all the tests by issuing acton test
:
foo@bar:~hello$ acton test
Tests - module hello:
foo: OK: 278 runs in 50.238ms
All 1 tests passed (23.491s)
foo@bar:~hello$
See the Testing section on how to write tests.
Variable data types
Acton supports a plethora of primitive data types.
int
integers, like1
,2
,123512
,-6542
or1267650600228229401496703205376
int
is arbitrary precision and can grow beyond machine word sizesi16
is a fixed size signed 16 bit integeri32
is a fixed size signed 32 bit integeri64
is a fixed size signed 64 bit integeru16
is a fixed size unsigned 16 bit integeru32
is a fixed size unsigned 32 bit integeru64
is a fixed size unsigned 64 bit integer
float
64 bit float, like1.3
or-382.31
bool
boolean, likeTrue
orFalse
str
strings, likefoo
- 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
Variable data types
Acton supports a plethora of primitive data types.
int
integers, like1
,2
,123512
,-6542
or1267650600228229401496703205376
int
is arbitrary precision and can grow beyond machine word sizesi16
is a fixed size signed 16 bit integeri32
is a fixed size signed 32 bit integeri64
is a fixed size signed 64 bit integeru16
is a fixed size unsigned 16 bit integeru32
is a fixed size unsigned 32 bit integeru64
is a fixed size unsigned 64 bit integer
float
64 bit float, like1.3
or-382.31
bool
boolean, likeTrue
orFalse
str
strings, likefoo
- 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
- It is not allowed to mix, like
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)
# Get a shallow copy of a dict. Note the use of .items() to get keys and values
new_d = dict(d.items())
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"}
# Although for an empty set, {} cannot be used as it means an empty dict
# (and it is impossible to distinguish between the two from syntax alone).
# Use set() to create an empty set.
empty_set = set()
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)
# Add all items from another set or some other iterable
s.update({"x", "y", "z"})
print(s)
env.exit(0)
Compile and run:
acton sets.act
./sets
Output:
Building project in /home/user/foo
Compiling example.act for release
Finished compilation in 0.032 s
Final compilation step
Finished final compilation step in 0.701 s
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'}
{'x', 'bar', 'a', 'y', 'z'}
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
Types
Every value in Acton is of a certain data type, which tells Acton what kind of data is being specified so it knows how to work with the data. Acton is a statically typed language, which means the type of all values must be known when we compile the program. Unlike many traditional languges like Java or C++, where types must be explicitly stated everywhere, we can write most Acton programs without types. The Acton compiler features a powerful type inferencer which will infer the types used in the program.
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
Acton implements the Hindley-Milner (HM) type system, which is common in languages with static types, like Haskell and Rust. Acton extends further from HM to support additional features.
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.
Generic Types
Sometimes we want to be able to work with any type. We call such types generic types. For example, the builtin list type in Acton can store any other type, so we say it is a list of A
, where A
is a generic type (from the specification in Acton builtins):
class list[A] (object):
...
def pop(self, n :?int=-1) -> A:
"""Pop and return an item from the list"
[A]
says A
is a generic type. And for example, the pop()
function on list returns an element of type A
.
class list[A] (object):
^^^ --- generic types are written here, between []
A is a generic type used in `class list`
def pop(self, n :?int) -> A:
^--- list.pop returns an item of type A
Classes and Objects
Acton supports the object-oriented paradigm, which means that it provides features to create classes and objects, which are instances of classes. Classes are a fundamental concept in an object-oriented world and they allow programmers to create their own data types with their own attributes and methods.
A class is defined using the class
keyword followed by the name of the class. The convention is to use CamelCase for class names.
class Circle(object):
radius: float
def __init__(self, radius):
self.radius = radius
def diameter(self):
return self.radius * 2
Attributes are variables that hold data for an object of a particular class and methods are functions that operate on that data. In the above example, radius
is an attribute of the Circle
class and diameter()
is a method that returns the diameter.
Class methods must have self
as the first argument, which refers to the object instance of the class that the method is called on.
Creating an object
A Class is like a blueprint and an object is an instance of such a blueprint. To create an object, or "instantiate", we use the "blueprint" (class), like so:
circle = Circle(3.14)
Here we create the object circle
from the class Circle
, passing the parameter 3.14
which will be used to set the radius
attribute of the object.
print(circle.diameter())
And here we print the diameter of the circle by calling the .diameter()
method.
Class Inheritance
Inheritance is a way of creating a new class from an existing class. The new class is called the derived class and the existing class from which we inherit is called the base class. The derived class inherits all the attributes and methods of the base class. The derived class can extend the base class by adding more attributes or methods. It is also possible to override methods to create more specific functionality.
We add the area()
to Circle
to get the area and realize that the unlike the diameter, area is common for all gemetric shapes. Thus we create a base class Shape
that defines the area()
method, but does not implement it since there is no generic way to compute the area for all shapes. Each concrete class, like Circle
and Square
, should implement area()
.
Source:
class Shape(object):
def area(self) -> float:
raise NotImplementedError("This method should be overridden by subclasses")
class Circle(Shape):
radius: float
def __init__(self, radius):
self.radius = radius
def diameter(self):
return self.radius * 2
def area(self):
return 3.14 * self.radius ** 2
class Square(Shape):
def __init__(self, side: float):
self.side = side
def area(self) -> float:
return self.side ** 2
actor main(env):
circle = Circle(3.14)
square = Square(3.14)
print(circle.area() + square.area())
env.exit(0)
Inheritance is one of the primary methods, if not the primary method, of structuring programs in the object oriented paradigm.
Shape
is an abstract class because it has no __init__
method.
Protocols
Protocols defines functionality in an abstract way that can be implemented by many classes.
Class and class inheritance is a good tool for structuring data and behavior. However, sometimes there is no clear parent / child relationship between classes, in which case using a protocol might be a more suitable choice.
Perhaps the simplest example is the Eq
protocol, that defines equality between two objects, from the Acton builtins. Classes do not need to have any common inheritance in order to support equality.
protocol Eq:
@staticmethod
__eq__ : (Self,Self) -> bool
The __eq__
method takes two arguments, itself and the other object to compare with and returns True
or False
. Both arguments are of the same type Self
, which means we can only compare two objects that are of the same type. It can be any type, as long as its the same type.
extension Circle (Eq):
def __eq__(self, other):
return self.radius == other.radius
Protocols allows us to express the requirement for a (generic) type to implement some particular functionality.
Let's say we have a function that compares two objects.
def comparator(a, b):
print("Things:", a, b)
print("Are they equal?", a == b)
We know that a
and b
must implement the Eq
protocol and they must be of the same type. Let's see what the compiler constraint solver says:
acton --sigs generic.act
== sigs: generic ================================
comparator : [A(Eq)] => (a: A, b: A) -> None
=================================================
The type has been inferred to a generic type A
that must implement the Eq
protocol, which is written as A(Eq)
. We can write this explicitly:
def comparator[A(Eq)](a: A, b: A):
print("Things:", a, b)
print("Are they equal?", a == b)
Protocol Method Dispatch and Method Resolution Order
Method dispatch and Method Resolution Order (MRO) is about picking a particular implementation of a method when there are multiple to pick from. For class inheritance, it is pretty straight forward, the method used will always be that of the actual class. For protocols, it is not as simple. Dispatch of methods for protocols is based on the observed type, which can be somewhat surprising.
Here are two classes, the base class Point
and the dervied class Point3D
(which inherits from Point
). Each implement the Eq
protocol.
class Point(object):
def __init__(self, x: int, y: int):
self.x = x
self.y = y
extension Point (Eq):
def __eq__(self, other):
return self.x == other.x and self.y == other.y
class Point3D(Point):
def __init__(self, x: int, y: int, z: int):
self.x = x
self.y = y
self.z = z
extension Point3D (Eq):
def __eq__(self, other):
return self.x == other.x and self.y == other.y and self.z == other.z
def comparator(a: Point, b: Point) -> bool:
return a == b
actor main(env):
# p1 and p2 are actually different in z. If seen as a 2D point, they appear
# equal since the x and y values of p1 and p2 are the same
p1 = Point3D(1, 2, 3)
p2 = Point3D(1, 2, 4)
# False, because we see p1 and p2 as Point3D objects and thus dispatch the Eq
# extension __eq__ method to Point3D which correctly compares x, y and z
print(p1 == p2)
# True, because comparator takes (Point, Point) and thus we see p1 and p2
# as Point objects and thus dispatch the Eq extension __eq__ method to Point
# which only compares x and y and thus returns True despite the z difference
print(comparator(p1, p2))
env.exit(0)
The comparator
function takes arguments of the base class Point
type, which means that when dispatching, they will use the Eq
protocol of Point
rather than that of Point3D
. Naive use typically dispatches to the protocol implementation of the derived class, which is what we intuitively want. It's only when we have forced the type to be observed, as here, that we end up with the "wrong" protocol implementation. Note that there are also cases where this is the desired behavior.
In this particular example, the natural and better solution is to write the comparator function as requiring the Eq type:
def generic_comparator[A(Eq)](a: A, b: A):
return a == b
Another common scenario is when the type has been forced by storing items in a list or dict, which in turn is typed as storing a base class.
actor main(env):
ref_point = Point3D(1, 2, 4)
p1 = Point3D(1, 2, 3)
p2 = Point3D(1, 2, 4)
my_points: list[Point] = [p1, p2]
for point in my_points:
if point == ref_point:
print("Found the reference (compared as Point)", point)
if isinstance(point, Point3D) and point == ref_point:
print("Found the reference (compared to Point3D)", point)
As shown in the above example, we can force the comparison to use the Eq
protocol implemention of Point3D
by using isinstance
to check and thus coerce the observed type of point
to be Point3D
.
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:
No dependencies found, building project
Building project in /tmp/tmp_nwgl0ik/example
Compiling example.act for release
Finished compilation in 0.015 s
Final compilation step
Finished final compilation step in 0.437 s
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.
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 exist as long as another actor has a reference to it and will idle, passively waiting for the next method call. Actors without references will be garbage collected. The root actor of a program will always exist even without other references.
This means that a simple program like this modified helloworld (the env.exit()
call has been removed) will run indefinitely. You need to deliberately tell the run time system to stop the actor world and exit, via env.exit()
, in order to exit the program.
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
.
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.
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.
Cleanup / Finalization
It is possible to run special code when an actor is about to be garbage collected. This should not be performed by normal actors, this is really only for I/O actors that interact with the environment and might need to also clean up in the environment when the actor is being garbage collected.
Define an actor action called __cleanup__
and the run time system will automatically install it to be run at garbage collection time. There is no hard guarantee when the __cleanup__
function will be called and it typically takes a couple of collection rounds to run all finalizers.
Source:
actor Foo():
action def __cleanup__():
print("Cleaning up after myself...")
actor main(env):
# create a bunch of actors and do not assign reference, so they should be collected
for i in range(20):
Foo()
# perform some busy work to eventually trigger the GC and thus finalization
# & schedule __cleanup__
a = 1
for i in range(99999):
a += i
# Delay exit a little bit to let RTS workers pick up the asynchronously
# scheduled __cleanup__ actions to run for the unreachable instances of the
# Foo actor
def _stop():
env.exit(0)
after 0.1: _stop()
Output:
Cleaning up after myself...
Cleaning up after myself...
Cleaning up after myself...
Cleaning up after myself...
Cleaning up after myself...
Cleaning up after myself...
Cleaning up after myself...
Cleaning up after myself...
Cleaning up after myself...
Cleaning up after myself...
Cleaning up after myself...
Cleaning up after myself...
Cleaning up after myself...
Cleaning up after myself...
Cleaning up after myself...
Cleaning up after myself...
Cleaning up after myself...
Cleaning up after myself...
Cleaning up after myself...
Cleaning up after myself...
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
!
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.
Environment
The environment of an Acton application is the outside world. Any useful application typically needs to interact with the environment in some way, like reading arguments or taking input from stdin and printing output.
Environment variables
It is possible to read, set and unset environment variables. The standard functions env.getenv
, env.setenv
and env.unsetenv
all assume str
input and output, which is a convenience based on the assumption that all data is encoded using UTF-8. POSIX systems really use binary encoding for both environment names and variables. To access the environment as bytes and handle decoding explicitly, use env.getenvb
, env.setenvb
and env.unsetenvb
.
Source:
actor main(env):
env_user = env.getenv("USER")
if env_user is not None:
print("User:", env_user)
env.setenv("FOO", "bar")
env.unsetenv("LANG")
foo_env = env.getenv("FOO")
if foo_env is not None:
print("FOO:", foo_env)
env.exit(0)
Output:
User: myuser
FOO: bar
Reading stdin input
Read input from stdin by installing a handler for stdin data. The returned data is str
actor main(env):
def interact(input):
print("Got some input:", input)
env.stdin_install(interact)
It is possible to specify the encoding and an on_error() callback which is invoked if there are problem with decoding the data. When encoding is not specified (default None
), an attempt is made to discover the encoding by reading the LANG
environment variable. If no encoding is discovered, the default is to use utf-8
.
actor main(env):
def interact(input):
print("Got some input:", input)
def on_stdin_error(err, data):
print("Some error with decoding the input data:", err)
print("Raw bytes data:", data)
env.stdin_install(on_stdin=interact, encoding="utf-8", on_error=on_stdin_error)
You can read the raw data in bytes
form by installing a bytes handler instead:
actor main(env):
def interact(bytes_input):
# Note how the input might contain parts (some bytes) of a multi-byte
# Unicode character in which case decoding will fail
print("Got some input:", bytes_input.decode())
env.stdin_install(on_stdin_bytes=interact)
This allows reading binary data and more explicit control over how to decode the data.
Interactive stdin
For interactive programs, like a text editor, input is not fed into the program line by line, rather the program can react on individual key strokes.
The default stdin mode is the canonical mode, which implies line buffering and that there are typically line editing capabilities offered that are implemented external to the Acton program. By setting stdin in non-canonical mode we can instead get the raw key strokes directly fed to us.
actor main(env):
def interact(input):
print("Got some input:", input)
# Set non-canonical mode, so we get each key stroke directly
env.set_stdin(canonical=False)
# Turn off terminal echo
env.set_stdin(echo=False)
env.stdin_install(interact)
We can also disable the echo mode with the echo option.
The Acton run time system will copy the stdin terminal settings on startup and restore them on exit, so you do not need to manually restore terminal echo for example.
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.
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
Testing
Testing your code is a really good idea! While Acton's type system allows interfaces to be precisely defined, it is imperative that the behavior of a function or actor is tested!
Test functions are automatically discovered by the compiler. Run tests with acton test
. Import the testing module and name your test functions starting with _test
. The type signature should match the test intended test category. Use the assertion functions available in the testing module. Here is a simple unit test:
Source:
import testing
def _test_simple():
testing.assertEqual(1, 1)
Run:
acton test
Output:
Building project in /home/user/foo
Compiling example.act for release
Finished compilation in 0.016 s
Final compilation step
Finished final compilation step in 0.437 s
Tests - module example:
simple: OK: 1523 runs in 50.001ms
All 1 tests passed (0.604s)
There are 4 kinds of tests
- unit tests
- small simple tests of pure functions
- synchronous actor tests
- involving one or more actors, returning results synchronously
- asynchronous actor tests
- involving actors but use asynchronous callbacks for return values
- environment tests
- similar to async actor test in that a callback is used for the return value
- these tests have access to the full environment via the
env
argument and can thus communicate with the outside world- this is a source of non-determinism so be mindful of this and try to avoid non-deterministic functions to the largest degree possible
When possible, strive to use unit tests rather than actor based tests and strive to avoid env tests.
Unit tests
Source:
import testing
def _test_simple():
foo = 3+4
testing.assertEqual(7, foo)
Run:
acton test
Output:
Building project in /home/user/foo
Compiling example.act for release
Finished compilation in 0.016 s
Final compilation step
Finished final compilation step in 0.442 s
Tests - module example:
simple: OK: 1565 runs in 50.079ms
All 1 tests passed (0.600s)
Unit tests are a good starting point for testing small units of your program. Pure functions are deterministic and are thus preferable for tests over non-deterministic tests using actors. You are limited in what you can do though, since all called functions must be pure.
The test discovery finds unit tests based on the name starting with _test_
and has a function signature of mut() -> None
or pure() -> None
.
Once effect analysis has been improved in the compiler to contain scope local effects, the test discovery will only consider pure
functions to be unit tests. See https://github.com/actonlang/acton/issues/1632
Sync actor tests
Source:
import logging
import testing
actor MathTester():
def add(a, b):
return a + b
actor SyncTester(log_handler):
def test():
m = MathTester()
testing.assertEqual(m.add(1, 2), 3, "1 + 2 = 3")
def _test_syncact(log_handler: logging.Handler) -> None:
"""A test using actors and synchronous control flow"""
# We make use of an actor as the central point for running our test logic.
# This _test_syncact function is just a wrapper picked up by the acton
# test framework runner
s = SyncTester(log_handler)
return s.test()
Run:
acton test
Output:
Building project in /home/user/foo
Compiling example.act for release
Finished compilation in 0.027 s
Final compilation step
Finished final compilation step in 0.526 s
Tests - module example:
syncact: OK: 1175 runs in 50.005ms
All 1 tests passed (0.655s)
Since the Acton RTS is multi-threaded and actors are scheduled concurrently on worker threads, using actors imply a degree of non-determinism and so unlike unit tests, which are completely deterministic, actors tests are fundamentally non-deterministic. You can still write deterministic tests as long as you pay attention to how you construct your test results.
For example, actor A might be scheduled before or after actor B so if the test relies on ordering of the output, it could fail or succeed intermittently. Interacting with the surrounding environment by reading files or communicating over the network introduces even more sources of non-determinism. Avoid it if you can.
The test discovery finds synchronous actor tests based on the name starting with _test_
and has a function signature of mut(logging.Handler) -> None
or pure(logging.Handler) -> None
.
Async actor tests
Source:
import logging
import testing
actor MathTester():
def add(a, b):
return a + b
actor AsyncTester(report_result, log_handler):
log = logging.Logger(log_handler)
def test():
log.info("AsyncTester.test()", None)
report_result(True, None)
def _test_asyncact1(report_result: action(?bool, ?Exception) -> None, log_handler: logging.Handler) -> None:
"""A test using actors and asynchronous control flow"""
# We make use of an actor as the central point for running our test logic.
# This _test_asyncact function is just a wrapper picked up by the acton
# test framework runner
s = AsyncTester(report_result, log_handler)
s.test()
Run:
acton test
Output:
Building project in /home/user/foo
Compiling example.act for release
Finished compilation in 0.028 s
Final compilation step
Finished final compilation step in 0.516 s
Tests - module example:
asyncact1: OK: 1171 runs in 56.181ms
All 1 tests passed (0.695s)
If a particular module is written to be called asynchronously, you will need to use asynchronous tests to test it.
The test discovery finds asynchronous actor tests based on the name starting with _test_
and has a function signature of proc(action(?bool, ?Exception) -> None, logging.Handler) -> None
.
Env tests
When you need to test functionality that accesses the environment, you need an env test. Do beware of errors related to test setup though, since you now depend on the external environment. TCP ports that you try to listen to might be already taken. Files that you assume exist might not be there.
Source:
import logging
import testing
actor EnvTester(report_result, env, log_handler):
log = logging.Logger(log_handler)
def test():
log.info("EnvTester.test()", None)
report_result(True, None)
def _test_envtest1(report_result: action(?bool, ?Exception) -> None, env: Env, log_handler: logging.Handler) -> None:
"""A test interacting with the environment"""
# We make use of an actor as the central point for running our test logic.
# This _test_envtest1 function is just a wrapper picked up by the acton
# test framework runner
s = EnvTester(report_result, env, log_handler)
s.test()
Run:
acton test
Output:
Building project in /home/user/foo
Compiling example.act for release
Finished compilation in 0.023 s
Final compilation step
Finished final compilation step in 0.484 s
Tests - module example:
envtest1: OK: 1213 runs in 50.135ms
All 1 tests passed (0.689s)
The test discovery finds env actor tests based on the name starting with _test_
and has a function signature of proc(action(?bool, ?Exception) -> None, Env, logging.Handler) -> None
.
Failures vs errors
Tests can have tree different outcomes; success, failure and error.
Success and failure are the two common cases where success is when the test meets the expected assertions and a failure is when it fails to meet a test assertion like testing.assertEqual(1, 2)
. We also distinguish a third case for test errors which is when a test does not run as expected, hitting an unexpected exception. This could indicate a design issue or that the test environment is not as expected.
All test assertions raise exceptions inheriting from AssertionError
which are considered test failures. Any other exception will be considered a test error.
For example, if a test attempts to retrieve https://dummyjson.com/products/1
and check that the returned JSON looks a certain way, it would be a test failure if the returned JSON does not match the expected value. If we try to connect with an invalid URL, like htp://
we would get a different exception and that would be considered a test error. It's probably a bad idea to try to connect to something on the Internet in a test, so avoid that and other sources of non-determinism when possible.
Unit tests
Source:
import random
import testing
def _test_failure():
testing.assertEqual(1, 2)
def _test_flaky():
i = random.randint(0, 2)
if i == 0:
return
elif i == 1:
testing.assertEqual(1, 2)
else:
raise ValueError("Random failure")
def _test_error() -> None:
# Now we could never use a unit test to fetch things from the Internet
# anyway, but it's just to show what the results look like
raise ValueError()
Run:
acton test
Output:
Building project in /home/user/foo
Compiling example.act for release
Finished compilation in 0.020 s
Final compilation step
Finished final compilation step in 0.482 s
Tests - module example:
error: ERR: 454 errors out of 454 runs in 52.733ms
ValueError:
flaky: FLAKY FAIL: 231 failures out of 471 runs in 52.819ms
testing.NotEqualError: Expected equal values but they are non-equal. A: 1 B: 2
failure: FAIL: 408 failures out of 408 runs in 52.837ms
testing.NotEqualError: Expected equal values but they are non-equal. A: 1 B: 2
1 error and 2 failure out of 3 tests (0.691s)
Unit tests are a good starting point for testing small units of your program. Pure functions are deterministic and are thus preferable for tests over non-deterministic tests using actors. You are limited in what you can do though, since all called functions must be pure.
Flaky tests
Flaky tests are those that have different outcomes during different runs, i.e. they are not deterministic. To combat these, acton test
will per default attempt to run tests multiple times to ensure that the result is the same. It runs as many test iterations as possible for at least 50ms. If a test is flaky, this will be displayed in the test output.
Source:
import random
import testing
def _test_flaky():
i = random.randint(0, 2)
if i == 0:
return
elif i == 1:
testing.assertEqual(1, 2)
else:
raise ValueError("Random failure")
Run:
acton test
Output:
Building project in /home/user/foo
Compiling example.act for release
Finished compilation in 0.017 s
Final compilation step
Finished final compilation step in 0.453 s
Tests - module example:
flaky: FLAKY FAIL: 565 failures out of 1140 runs in 50.043ms
testing.NotEqualError: Expected equal values but they are non-equal. A: 1 B: 2
1 out of 1 tests failed (0.625s)
Note how this test case is only made possible because the random module has incorrect effects. The type says it is pure while in reality, it is not. There is an issue to improve this by applying a proper effect to the random module, see https://github.com/actonlang/acton/issues/1729, after which this example needs to be rewritten.
Performance testing
It is also possible to run tests in a performance mode, which uses the same basic test definitions (so you can run your tests both as logic test and for performance purposes) but alters the way in which the tests are run. In performance mode, only a single test will be run at a time unlike the normal mode in which many tests are typically run concurrently.
To get good numbers in performance mode, it's good if test functions run for at least a couple of milliseconds. With very short tests, very small differences lead to very large percentage differences.
Source:
import testing
def _test_simple():
a = 0
for i in range(99999):
a += i
Run:
acton test perf
Output:
Building project in /home/user/foo
Compiling example.act for release
Finished compilation in 0.016 s
Final compilation step
Finished final compilation step in 0.451 s
Tests - module example:
simple: OK: 3.21ms Avg: 4.20ms 5.11ms 106 runs in 1005.261ms
All 1 tests passed (1.571s)
(note that the output is rather wide, scroll horizontally to see the full output)
Performance comparisons
When running in performance mode
you can record a snapshot of performance using acton test perf --record
. A perf_data
file is written to disk with the stored performance data. Subsequent test runs will read this file and show a comparison. The difference is displayed as a percentage increase or decrease in time.
Source:
import testing
def _test_simple():
a = 0
for i in range(99999):
a += i
Run:
acton test perf --record
acton test perf
Output:
Building project in /home/user/foo
Compiling example.act for release
Finished compilation in 0.017 s
Final compilation step
Finished final compilation step in 0.452 s
Tests - module example:
simple: OK: 3.25ms Avg: 4.16ms 7.38ms 122 runs in 1006.002ms
All 1 tests passed (1.565s)
Building project in /home/user/foo
Compiling example.act for release
Already up to date, in 0.000 s
Final compilation step
Finished final compilation step in 0.116 s
Tests - module example:
simple: OK: 3.23ms -0.50% Avg: 4.17ms +0.19% 6.35ms -13.91% 119 runs in 1001.375ms
All 1 tests passed (1.215s)
(note that the output is rather wide, scroll horizontally to see the full output)
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/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.
Package Management
acton
offers integrated package management to declare dependencies on other Acton packages and automatically download them from their sources on the Internet.
The guiding principle behind Actons package management is to strive for determinism, robustness and safety. This is primarily achieved by only resolving dependencies at design time. That is, it is the developer of a particular Acton package that determines its exact dependencies and not whomever might be downloading and building said package. The very identity of a package at a particular point in time, which can be thought of as the version of a package, is the hash of its content. This is the foundation of Actons package management. Each dependency has a hash of its content. A URL is just one place from which this particular version of a package can be downloaded. The hash of packages is determined and recorded at design time. Anyone pulling down and building dependencies will have the hash verified to ensure a deterministic build.
There is no central package repository, instead dependencies are defined as URLs from which the dependency package can be downloaded. This is typically a tar.gz file from GitHub, GitLab or similar source hosting site. Again, the very identity of a version of a package is the conten hash. The URL is only from where to get it.
Acton is statically compiled, all dependencies are fetched and included at compile time. There are no run time dependencies.
Add Dependency
Add a dependency to your project by using acton pkg add
and providing the URL to the project and a local reference name.
In this case we add the example foo
package as a dependency.
acton pkg add https://github.com/actonlang/foo/archive/refs/heads/main.zip foo
This will fetch the dependency and add it to the build.act.json
file of your local project, resulting in something like:
{
"dependencies": {
"foo": {
"url": "https://github.com/actonlang/foo/archive/refs/heads/main.zip",
"hash": "1220cd47344f8a1e7fe86741c7b0257a63567b4c17ad583bddf690eedd672032abdd"
}
},
"zig_dependencies": {}
}
It is possible to edit build.act.json
by hand, but adding dependencies, which requires filling in the 'hash' field requires computing the hash which is somewhat tricky.
The foo
package provides a single foo
module with a foo
function (that appropriately returns foo
). We can now access it from our main actor:
import foo
actor main(env):
print(foo.foo())
env.exit(0)
Local Dependencies
It is possible to use dependencies available via a local file system path by setting the path
attribute. Edit build.act.json
and add or modify an existing dependency. Set the path
attribute to a relative path on the local file system, e.g.:
{
"dependencies": {
"foo": {
"url": "https://github.com/actonlang/foo/archive/refs/heads/main.zip",
"hash": "1220cd47344f8a1e7fe86741c7b0257a63567b4c17ad583bddf690eedd672032abdd",
"path": "../foo"
},
"local_lib": {
"path": "deps/local_lib"
}
},
"zig_dependencies": {}
}
Setting a local path
will take priority over a remote url
. This can be useful to fork a library and make local modifications to it before submitting them back upstream.
Remove Dependency
You can remove a dependency from your project with acton pkg remove
:
acton pkg remove foo
Fetch Dependencies / Enable Airplane Mode
You can fetch all the dependencies of a project by using acton fetch
. It will download the dependencies specified in build.act.json
to the cache.
acton fetch
enables you to work offline (a.k.a airplane mode).
C / C++ / Zig dependencies
Much like dependencies on other Acton packages, an Acton project can depend on a Zig package which could be a C / C++ or Zig library, as long as it has a build.zig
file.
acton zig-pkg add URL NAME --artifact X --artifact Y
- list the libraries you want to link with as artifacts
acton zig-pkg remove NAME
acton zig-pkg add https://github.com/allyourcodebase/zlib/archive/refs/tags/1.3.1.tar.gz zlib --artifacts z
{
"dependencies": {},
"zig_dependencies": {
"zlib": {
"url": "https://github.com/allyourcodebase/zlib/archive/refs/tags/1.3.1.tar.gz",
"hash": "122034ab2a12adf8016ffa76e48b4be3245ffd305193edba4d83058adbcfa749c107",
"artifacts": [
"z"
]
}
}
}
Security and Trust (or the lack thereof)
Working with Zig / C / C++
Acton has C ABI compatibility which makes it trivial to call C functions and fairly simply to call Zig and C++ using C wrapping functions. If you want to integrate a library written in one of these languages, this page is for you.
Regardless of the foreign language used, we need to consider a few things:
- memory allocation, it must play well with the Acton GC
- You can allocate memory via classic
malloc
or via Acton GC mallocacton_malloc
- normal mallocacton_malloc_atomic
for allocations that are guaranteed to not contain pointers
- Better safe than sorry, use the GC-malloc when in doubt
- Always use Acton GC malloc for actor and class attributes and similar
- object and actor instances are garbage collected by the GC and there is no destructor function, so if you would have used class
malloc
there is no good place for thefree
- object and actor instances are garbage collected by the GC and there is no destructor function, so if you would have used class
- Within pure functions, you can use class
malloc
- be sure tofree
the allocations before return of the function, even for error paths
- You can allocate memory via classic
- thread safety, the Acton RTS is threaded and actors are concurrently executed by different threads
- in general, we strive to only keep data per actor and since an actor executes sequentially, we do not need thread safety measures like locks - just make sure you don't try to share data between actors "under the hood"
- libraries must not use global variables though
- asynchronous I/O, the Acton RTS performs asynchronous I/O and any other library that performs I/O need to conform to this model
Integrating a C library (zlib)
This is a guide to integrating C libraries in Acton code. We will use the zlib compression library, written in C, to build an Acton module that supports zlib compression and decompression.
We will only focus on the inflate
and deflate
functions in zlib. They are pure functions (meaning they only take some input and return some output, they do not have any side effects like writing to some shared state), that makes them easier to integrate than anything that does I/O. While zlib does expose functions to interact with files, we don't want to reimplement file related functionality since we already have this supported by the Acton stdlib.
Create new project
Let's start by making a new Acton project, let's call it acton-zlib
. New projects are created with an example "Hello world" app. Let's remove it and start from scratch.
acton new acton-zlib
cd acton-zlib
rm src/*
Acton's low level build system - the Zig build system
The Acton compiler parses .act source code, runs through all its compilation passes with type checking, CPS conversion, lambda lifting etc and finally produces C code. Internally, Acton then uses the Zig build system to compile the generated C code to libraries and finally binary executables.
To add a C library dependency, it first needs to be buildable using the Zig build system, which means that it needs a build.zig
file, the config file for the Zig build, somewhat similar to the CMakeLists.txt of CMake. Some projects have already adopted a build.zig
in the upstream repo, like PCRE2 and the Boehm-Demers-Weiser GC (both of which are used by Acton). In some cases, there are forks of projects with build.zig
added. Otherwise you will need to write one for yourself, which is usually simpler than it might first seem.
Add the zlib C library as a Zig dependency
In the case of zlib, there is already a repo available with a build.zig for zlib. Navigate to the Tags page, find 1.3.1
and the link to the source files, i.e. https://github.com/allyourcodebase/zlib/archive/refs/tags/1.3.1.tar.gz
.
Add it to our acton-zlib
project:
acton zig-pkg add https://github.com/allyourcodebase/zlib/archive/refs/tags/1.3.1.tar.gz zlib --artifact z
Note the --artifact z
which is provided to instruct which library to link with. Headers from the zlib library, like zlib.h
, will now become visible to C files in our project and the z
library will be linked in with our executables. The easiest way to discover what the artifacts are called is by inspecting the build.zig
file of the package. This particular zlib build.zig
starts like this:
const std = @import("std");
pub fn build(b: *std.Build) void {
const upstream = b.dependency("zlib", .{});
const lib = b.addStaticLibrary(.{
.name = "z",
.target = b.standardTargetOptions(.{}),
.optimize = b.standardOptimizeOption(.{}),
});
lib.linkLibC();
lib.addCSourceFiles(.{
.root = upstream.path(""),
.files = &.{
"adler32.c",
"crc32.c",
...
It is the .name
argument to addStaticLibrary
that tells us the name of the artifact. Zig packages might expose multiple such artifacts, as is the case for mbedtls.
acton zig-pkg add
will fetch the package from the provided URL and save the hash sum to build.act.json
, resulting in:
{
"dependencies": {},
"zig_dependencies": {
"zlib": {
"url": "https://github.com/allyourcodebase/zlib/archive/refs/tags/1.3.1.tar.gz",
"hash": "122034ab2a12adf8016ffa76e48b4be3245ffd305193edba4d83058adbcfa749c107",
"artifacts": [
"z"
]
}
}
}
Create zlib.act Acton module
Next up we need to create the Acton zlib
module. Open src/zlib.act
and add a compress and decompress function:
pure def compress(data: bytes) -> bytes:
NotImplemented
pure def decompress(data: bytes) -> bytes:
NotImplemented
The NotImplemented
statement tells the compiler that the implementation is not written in Acton but rather external. When there is a .ext.c
file, the compiler expects it to contain the implementations for the NotImplemented
functions. Also note the explicit types. Normally the Acton compiler can infer types, but since there is no Acton code here, only C code, there is nothing to infer from.
Now create src/zlib.ext.c
which is where we will do the actual implementation of these functions. We need to add a __ext_init__
function, which runs on module load by the Acton RTS, which must always exist. There is nothing to do in particular for zlib so let's just create an empty function, like so:
void zlibQ___ext_init__() {}
Next, we need to fill in the C functions that map to the Acton functions compress
and decompress
. By invoking acton build
we can get the compiler to generate a skeleton for these. We will also get a large error message, since there is no actual implementation:
user@host$ acton build
... some large error message
Ignore the error and instead check the content of out/types/zlib.c
and we will find the C functions we need, commented out:
#include "rts/common.h"
#include "out/types/zlib.h"
#include "src/zlib.ext.c"
B_bytes zlibQ_compress (B_bytes data);
/*
B_bytes zlibQ_compress (B_bytes data) {
// NotImplemented
}
*/
B_bytes zlibQ_decompress (B_bytes data);
/*
B_bytes zlibQ_decompress (B_bytes data) {
// NotImplemented
}
*/
int zlibQ_done$ = 0;
void zlibQ___init__ () {
if (zlibQ_done$) return;
zlibQ_done$ = 1;
zlibQ___ext_init__ ();
}
Copy the commented-out skeleton into our own src/zlib.ext.c
. Just in order to get something that compiles, let's just quickly let the functions return the input data. Since both input and output are bytes
, this should now compile (and work at run time).
B_bytes zlibQ_compress (B_bytes data) {
return data;
}
B_bytes zlibQ_decompress (B_bytes data) {
return data;
}
user@host:~/acton-zlib$ acton build
Building project in /Users/user/acton-zlib
Compiling zlib.act for release
Finished compilation in 0.005 s
Compiling test_zlib.act for release
Finished compilation in 0.019 s
Final compilation step
user@host:~/acton-zlib$
Add a test module
Before we implement the body of the compress and decompress functions, we can write a small test module which will tell us when we've succeeded. We use some pre-known test data (which we could get from another language implementation):
import testing
import zlib
def _test_roundtrip():
for x in range(100):
i = "hello".encode()
c = zlib.compress(i)
d = zlib.decompress(c)
testing.assertEqual(i, d)
def _test_compress():
for x in range(100):
i = "hello".encode()
c = zlib.compress(i)
testing.assertEqual(c, b'x\x9c\xcbH\xcd\xc9\xc9\x07')
def _test_decompress():
for x in range(1000):
c = b'x\x9c\xcbH\xcd\xc9\xc9\x07'
d = zlib.decompress(c)
testing.assertEqual(d, b'hello')
Note how we run a few test iterations to get slightly better timing measurements for performance testing. Run the test with acton test
:
user@host:~/acton-zlib$ acton test
Tests - module test_zlib:
decompress: FAIL: 195 runs in 50.728ms
testing.NotEqualError: Expected equal values but they are non-equal. A: b'x\x9c\xcbH\xcd\xc9\xc9\x07' B: b'hello'
compress: FAIL: 197 runs in 50.886ms
testing.NotEqualError: Expected equal values but they are non-equal. A: b'hello' B: b'x\x9c\xcbH\xcd\xc9\xc9\x07'
roundtrip: OK: 226 runs in 50.890ms
2 out of 3 tests failed (26.354s)
user@host:~/acton-zlib$
As expected, the roundtrip test goes through, since we just return the input data while the compress and decompress tests fail.
Implement the compress function
Now let's fill in the rest of the owl. Below is the body of the zlibQ_compress
function. The bulk of this code is not particularly interesting to this guide as it has more to do with standard C usage of zlib, but a few things are worth noting.
B_bytes zlibQ_compress(B_bytes data) {
if (data->nbytes == 0) {
return data;
}
// Prepare the zlib stream
int ret;
z_stream stream;
memset(&stream, 0, sizeof(stream));
ret = deflateInit(&stream, Z_DEFAULT_COMPRESSION);
if (ret != Z_OK) {
$RAISE((B_BaseException)$NEW(B_ValueError, to$str("Unable to compress data, init error: %d", ret)));
}
// Set the input data
stream.avail_in = data->nbytes;
stream.next_in = (Bytef*)data->str;
// Allocate the output buffer using Acton's malloc
size_t output_size = deflateBound(&stream, data->nbytes);
Bytef* output_buffer = (Bytef*)acton_malloc_atomic(output_size);
stream.avail_out = output_size;
stream.next_out = output_buffer;
// Perform the deflate operation
ret = deflate(&stream, Z_FINISH);
if (ret != Z_STREAM_END) {
$RAISE((B_BaseException)$NEW(B_ValueError, $FORMAT("Unable to compress data, error: %d", ret)));
}
// Clean up
deflateEnd(&stream);
return actBytesFromCStringNoCopy(output_buffer);
}
Memory management is always top of mind when writing C, as it the case here. We can allocate memory via the Acton GC-heap malloc or just plain malloc()
(the non-GC heap, to be explicit). Since zlibQ_compress
is pure, we have no state leaking out of the function other than via its return value. All return values must be allocated on the Acton GC heap, so we know we must use acton_malloc
for any value that we return. Any other local variables within the function can use classic malloc, as long as we make sure to explicitly free it up. For class or actor methods, any allocation for class or actor attributes must be performed using the Acton GC malloc, since there is no destructor or similar where a free can be inserted, so using classic malloc would be bound to leak. Also note that in this particular case, we know that the returned bytes value itself is not going to contain any pointers, so by using acton_malloc_atomic
we can get a chunk of memory that will not be internally scanned by the GC, which saves a bit of time and thus improves GC performance. If we allocate structs that do carry pointers, they must use the normal acton_malloc()
.
actBytesFromCStringNoCopy(output_buffer)
takes the buffer
(already allocated via acton_malloc_atomic()
) and wraps it up as a boxed value of the type B_bytes
that we return.
Also note how we convert Zlib errors to Acton exceptions where necessary.
Running the test, the compress
test now passes while roundtrip has stopped working (since decompress is not implemented yet):
user@host:~/acton-zlib$ acton test
Tests - module test_zlib:
decompress: FAIL: 158 runs in 50.175ms
testing.NotEqualError: Expected equal values but they are non-equal. A: b'x\x9c\xcbH\xcd\xc9\xc9\x07' B: b'hello'
compress: OK: 167 runs in 50.225ms
roundtrip: FAIL: 147 runs in 50.266ms
testing.NotEqualError: Expected equal values but they are non-equal. A: b'hello' B: b'x\x9c\xcbH\xcd\xc9\xc9\x07'
2 out of 3 tests failed (0.941s)
user@host:~/acton-zlib$
Implement the decompress function
Much like the compress function, the decompress function mostly relates to how zlib itself and its interface works. We use the same wrappers and transform errors to exceptions.
B_bytes zlibQ_decompress(B_bytes data) {
if (data->nbytes == 0) {
return data;
}
// Prepare the zlib stream
int ret;
z_stream stream;
memset(&stream, 0, sizeof(stream));
ret = inflateInit(&stream);
if (ret != Z_OK) {
$RAISE((B_BaseException)$NEW(B_ValueError, $FORMAT("Unable to decompress data, init error: %d", ret)));
}
// Set the input data
stream.avail_in = data->nbytes;
stream.next_in = (Bytef*)data->str;
// Allocate the output buffer using Acton's malloc
size_t output_size = 2 * data->nbytes; // Initial output buffer size
Bytef* output_buffer = (Bytef*)acton_malloc_atomic(output_size);
memset(output_buffer, 0, output_size);
stream.avail_out = output_size;
stream.next_out = output_buffer;
// Perform the inflate operation, increasing the output buffer size if needed
do {
ret = inflate(&stream, Z_NO_FLUSH);
if (ret == Z_BUF_ERROR) {
// Increase the output buffer size and continue decompressing
size_t new_output_size = output_size * 2;
output_buffer = (Bytef*)acton_realloc(output_buffer, new_output_size);
stream.avail_out = new_output_size - stream.total_out;
stream.next_out = output_buffer + stream.total_out;
} else if (ret != Z_OK) {
$RAISE((B_BaseException)$NEW(B_ValueError, $FORMAT("Unable to decompress data, error: %d", ret)));
}
} while (ret == Z_BUF_ERROR);
// Clean up
inflateEnd(&stream);
return actBytesFromCStringNoCopy(output_buffer);
}
Final test
user@host:~/acton-zlib$ acton test
Tests - module test_zlib:
decompress: OK: 42 runs in 51.065ms
compress: OK: 25 runs in 50.032ms
roundtrip: OK: 24 runs in 50.053ms
All 3 tests passed (0.738s)
user@host:~/acton-zlib$
And with that, we're done! A simple wrapper around zlib, which is also available on GitHub if you want to study it further.
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.