Cover

Preface

Like some of you, I programmed sequentially for years, using the occasional thread here and there. I mostly enjoyed the performance gains as processors became faster and faster. Then, processors stopped becoming faster. Instead, processors started becoming more parallel by adding more cores. I became painfully aware that my programming style was not the future. Moreover, programming applications imperatively with shared state, critical sections, and semaphores had been declared an abject failure. I needed to learn a new programming model.

Moore's Law

The Problem With Threads

A colleague introduced me to an early version of Scala. Programming in Scala opened my eyes to a new world of concurrent actors, monads, and multi-paradigm programming. I worked hard and became confident in this new environment, but the applications I produced were suspect. Instead of forming a business solution, my programs formed a concurrency solution. Although I liked the sophistication of this new environment, the results just felt wrong. Writing application code, especially for business applications, should be easy, and the results should express the solution clearly without requiring special knowledge.

While battling the concurrent programming experience, other trends affected my opinions around concurrent programming, such as cloud computing, IT and OT convergence, low-code development, and software composition using web services. Along the way, I studied "Concepts, Techniques, and Models in Computer Programming," paying particular attention to a programming model named Declarative Dataflow that greatly simplified concurrent programming. Declarative dataflow introduced a new construct named the dataflow variable, making concurrent programming extremely simple. I began imagining how this dataflow construct could streamline and accelerate the development of modern cloud applications, low-code platforms, and software composition.

In my first experiment with declarative dataflow, I created a dynamic programming language named Ozy that ran as a companion language on the JVM. The experiment was especially fruitful as it exposed a weakness in declarative dataflow--dataflow variables cannot be easily shared with other programming languages--and motivated a novel solution.

Ozy: A General Orchestration Container

In 2020, I found myself locked down because of COVID-19, staring at a blank screen with an epiphany for a new programming language. The central idea was an actor construct fusing the message-passing protocol with a hidden implementation of declarative dataflow. Unlike typical actor systems, programs would not be formed as state machines, and unlike declarative dataflow, programs would interoperate without knowing dataflow variables. The name Torq came to me as I imagined a programming language with the power to service millions of requests incrementally and fairly, moving massive amounts of data without stalling.

Torq is the culmination of a personal journey to realize a new programming experience. It consists of a language and a patented programming model named Actorflow. The language is dynamic with optional type annotations, and although interpreted, Torq can be faster than compiled languages by utilizing multiple processors more efficiently. The Actorflow model gives Torq encapsulation power, where actor messages are the inputs and outputs of a hidden dataflow machine.

This book presents Torq by example, where each example builds on previous examples. I hope you enjoy it.

--Glenn Osborne

Introduction

Welcome to the Torq Programming Language for Java developers.

Big Data analytics, artificial intelligence, and IT/OT convergence are pressuring organizations to be data-centric. The urgent need to manage explosive data growth with meaning, where billions of files and records are distributed over thousands of computers, is a significant challenge.

Torq addresses these challenges by simplifying data-centric programming at scale with a novel programming model. This allows programmers to focus on the application problem rather than the programming problem, leading to efficiencies that lower costs.

The Torq programming model is designed to improve scalability, responsiveness, and faster time-to-market in three problem areas:

  1. Data contextualization - enhancing live data meaningfully before it is stored
  2. Enterprise convergence - creating meaningful APIs across departments and divisions
  3. Situational applications - composing low-cost, low-code, highly-valued applications quickly
Data contextualization
Modern data fuels analytics and artificial intelligence. To make data specifically meaningful, it needs preprocessing, contextualization, and additional features before reaching storage. Torq enhances existing dataflows efficiently with related data to help reveal insights that would otherwise go unseen.
Enterprise convergence
Enterprise workflows require applications spanning multiple departments and divisions. Initiatives, such as the "Unified Name Space," aim to tear down traditional silos. Torq provides a dynamic platform to unify the enterprise with composable services that integrate departments and divisions.
Situational applications
Recent advancements have enabled solutions that were out of reach only a few years ago, creating a massive demand for new solutions. Low-code platforms help citizen developers reduce software backlogs. Torq is a truly low-code platform that scales and executes efficiently.

Torq is a dynamic, gradually typed, concurrent programming language with novel ease-of-use and efficiency.

Concurrent programming is hard, really hard

What makes concurrent programming so hard that it would justify a new programming language like Torq?

Mainstream programming suffers an incurable problem: variables alone cannot refer to shared memory values from multiple threads. Shared memory values require multiple threads to explicitly synchronize access, and as Lee (2006) formalized, programming over shared memory with threads is a failure. To solve this problem, the industry has adopted models other than shared memory for concurrency, such as message passing, functional programming, and borrow checking. In each of these models, programming is highly structured to avoid shared memory and unsafe access. Unfortunately, these models tend to create complex and tangled code. In the degenerative case, they organize code into concurrency solutions instead of application solutions.

A programming model does exist where multiple threads can share memory without explicit synchronization. Borrowed from logic programming, Declarative Dataflow (Van-Roy, P., & Haridi, S., 2004) computes over single-assignment variables called dataflow variables. A dataflow variable is initially unbound, but once bound, it is immutable. Threads that produce information bind dataflow variables, and threads that consume information suspend until dataflow variables are bound. This dataflow rule describes a producer-consumer interaction that implicitly synchronizes access to shared memory, resulting in a natural style void of technical concerns unrelated to the problem. Unfortunately, adopting declarative dataflow is highly disruptive because it requires runtime architectures to redefine the fundamental semantics of the memory variable for all processes.

Actorflow

Torq is a dynamic programming language based on Actorflow, a patented programming model that fuses message-passing actors with a hidden implementation of declarative dataflow. Concurrently executing actors only communicate by sending immutable messages. Requests and responses are correlated with private dataflow variables, bound indirectly by a controller. Actors that send requests may suspend, waiting for a variable bound by a response. Actors that receive messages may resume when a received message binds a waiting variable. This request-response interaction provides synchronization without sharing variables, giving us a naturally sequential programming style. Moreover, we can compose programs using a mix of libraries from other programming languages. All variables, dataflow or otherwise, are hidden.

Consider the following program written as a Torq actor. ConcurrentMath calculates the number 7 using three concurrent child actors to supply the operands in the expression 1 + 2 * 3. This example is an unsafe race condition in mainstream languages. However, in Torq, this sequential-looking but concurrently executing code will always calculate 7 because of the dataflow rule defined previously. Notice that Torq honors operator precedence without explicit synchronization.

actor ConcurrentMath() in
    actor Number(n) in
        handle ask 'get' in
            n
        end
    end
    var n1 = spawn(Number.cfg(1)),
        n2 = spawn(Number.cfg(2)),
        n3 = spawn(Number.cfg(3))
    handle ask 'calculate' in
        n1.ask('get') + n2.ask('get') * n3.ask('get')
    end
end

Concurrent Construction

Torq facilitates a concurrent style of programming not possible in mainstream languages. Consider the next example as a slightly modified version of our previous example. Instead of calculating concurrently, we construct concurrently. The concurrent math calculation x + y * z from our first example is replaced with a concurrent data construction [x, y, z], where x, y, and z stand for n1.ask('get'), n2.ask('get'), and n3.ask('get'), respectively.

actor ConcurrentMathTuple() in
    actor Number(n) in
        handle ask 'get' in
            n
        end
    end
    var n1 = spawn(Number.cfg(1)),
        n2 = spawn(Number.cfg(2)),
        n3 = spawn(Number.cfg(3))
    handle ask 'calculate' in
        [n1.ask('get'), n2.ask('get'), n3.ask('get')]
    end
end

Dataflow variables make concurrent construction possible. Instead of using futures (functions and callbacks), like other programming languages, Torq uses dataflow variables to construct partial data that is complete when concurrent tasks are complete. In essence, futures wait for logic, but Torq waits for data. Implicit synchronization provided by dataflow variables is not the same as the syntactic sugar provided by async-await for futures (sometimes called promises). In essence, Torq is non-blocking-concurrent whereas async-await is non-blocking-sequential.

Streaming Data

In mainstream programming, vendors provide reactive stream libraries "certified" with an official TCK (Technology Compatibility Kit). In Torq, reactive streams are a natural part of the language. A publisher is simply an actor that can respond multiple times to a single request.

The IntPublisher can be configured with a range of integers and an increment.

actor IntPublisher(first, last, incr) in
    import system[ArrayList, Cell]
    import system.Procs.respond
    var next_int = Cell.new(first)
    handle ask 'request'#{'count': n} in
        func calculate_to() in
            var to = @next_int + (n - 1) * incr
            if to < last then to else last end
        end
        var response = ArrayList.new()
        var to = calculate_to()
        while @next_int <= to do
            response.add(@next_int)
            next_int := @next_int + incr
        end
        if response.size() > 0 then
            respond(response.to_tuple())
        end
        if @next_int <= last then
            eof#{'more': true}
        else
            eof#{'more': false}
        end
    end
end

The SumOddIntsStream example spawns and iterates an IntPublisher, summing the integers not divisible by two.

actor SumOddIntsStream() in
    import system[Cell, Stream, ValueIter]
    import examples.IntPublisher
    handle ask 'sum'#{'first': first, 'last': last} in
        var sum = Cell.new(0)
        var int_publisher = spawn(IntPublisher.cfg(first, last, 1))
        var int_stream = Stream.new(int_publisher, 'request'#{'count': 3})
        for i in ValueIter.new(int_stream) do
            if i % 2 != 0 then sum := @sum + i end
        end
        @sum
    end
end

The MergeIntStreams example spawns two configurations of IntPublisher, one for even numbers and one for odd numbers. The even stream publishes 3 integers at a time and the odd stream publishes 2 integers at a time. After spawning the streams, the example performs an asynchronous, concurrent, non-blocking, merge sort while honoring backpressure.

actor MergeIntStreams() in
    import system[ArrayList, Cell, Stream, ValueIter]
    import examples.IntPublisher
    handle ask 'merge' in
        var odd_iter = ValueIter.new(Stream.new(spawn(IntPublisher.cfg(1, 10, 2)), 'request'#{'count': 3})),
            even_iter = ValueIter.new(Stream.new(spawn(IntPublisher.cfg(2, 10, 2)), 'request'#{'count': 2}))
        var answer = ArrayList.new()
        var odd_next = Cell.new(odd_iter()),
            even_next = Cell.new(even_iter())
        while @odd_next != eof && @even_next != eof do
            if (@odd_next < @even_next) then
                answer.add(@odd_next)
                odd_next := odd_iter()
            else
                answer.add(@even_next)
                even_next := even_iter()
            end
        end
        while @odd_next != eof do
            answer.add(@odd_next)
            odd_next := odd_iter()
        end
        while @even_next != eof do
            answer.add(@even_next)
            even_next := even_iter()
        end
        answer.to_tuple()
    end
end

These kinds of asynchronous, concurrent, non-blocking, reactive streams cannot be created using mainstream languages and libraries that rely on async-await. The mainstream approach is non-blocking, sequential programming, which is insufficient. Reactive streams require non-blocking, concurrent programming.

"Asynchronous, Concurrent, Non-blocking, Backpressure"

  • Asynchronous: actors don't necessarily wait on other actors
  • Concurrent: actors progress independently and opportunistically
  • Non-blocking: operating system threads are released while waiting
  • Backpressure: publishers cannot produce more than requested

Recall that concurrent is the potential to run in parallel.

Higher concurrency and throughput

The Torq effect is more than just a natural programming style. The implicit synchronization afforded by dataflow variables can increase concurrency and throughput by reducing synchronization barriers. Consider the following comparison of a simple use-case written in Torq versus Java.

TODO: Insert the Torq versus Java throughput example--the simple use-case that retrieves a customer order, product, and contact history.

Getting started

In this chapter, we provide the following sections:

Installation

Install Java

Torq for Java requires JDK 17+. Use your favorite JDK provider or one of the following:

Install Torq JARs

Torq consists of four core JARs, one optional server JAR, and one examples JAR. The core jars are 100% Java with no external dependencies. The server JAR provides dynamic API support and depends on Jetty 12. The examples JAR depends on all the other JARs.

Core JARs

<dependency>
    <groupId>org.torqlang</groupId>
    <artifactId>torqlang-local</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
    <groupId>org.torqlang</groupId>
    <artifactId>torqlang-lang</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
    <groupId>org.torqlang</groupId>
    <artifactId>torqlang-klvm</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
    <groupId>org.torqlang</groupId>
    <artifactId>torqlang-util</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>

Server JAR

<dependency>
    <groupId>org.torqlang</groupId>
    <artifactId>torqlang-server</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>

Examples JAR

<dependency>
    <groupId>org.torqlang</groupId>
    <artifactId>torqlang-examples</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>

Install Torq JARs using Maven

To use Maven, you need to:

  1. Configure your Maven settings file settings.xml
    1. Add a GitHub profile
    2. Add your GitHub identity and personal access token
  2. Configure your project POM file pom.xml
    1. Add the Torq GitHub repository

For detailed guidance, see GitHub, Learn more about Maven.

Configure settings.xml

Add a GitHub profile

Add a GitHub profile to your Maven settings. Replace OWNER with torq-lang and REPOSITORY with torq-jv.

<profile>
    <id>github</id>
    <repositories>
        <repository>
            <id>central</id>
            <url>https://repo1.maven.org/maven2</url>
        </repository>
        <repository>
            <id>github</id>
            <url>https://maven.pkg.github.com/OWNER/REPOSITORY</url>
            <snapshots>
                <enabled>true</enabled>
            </snapshots>
        </repository>
    </repositories>
</profile>

Add your GitHub identity and personal access token

Add a GitHub server to your list of servers. Replace USERNAME with your GitHub user ID and replace TOKEN with your GitHub personal access token.

<servers>
    <server>
        <id>github</id>
        <username>USERNAME</username>
        <password>TOKEN</password>
    </server>
</servers>

Configure pom.xml

Your POM file must contain two declarations:

  1. A repository declaration
  2. A dependency for each Torq JAR being used
<project>

    <!-- Declare the GitHub repository for Torq -->
    <repositories>
        <repository>
            <id>github</id>
            <url>https://maven.pkg.github.com/torq-lang/torq-jv</url>
        </repository>
    </repositories>

    <!-- Declare a dependency for each Torq JAR used -->
    <dependencies>
        <dependency>
           <groupId>org.torqlang</groupId>
           <artifactId>torqlang-server</artifactId>
           <version>1.0-SNAPSHOT</version>
        </dependency>       
        <dependency>
            <groupId>org.torqlang</groupId>
            <artifactId>torqlang-local</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>org.torqlang</groupId>
            <artifactId>torqlang-lang</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>org.torqlang</groupId>
            <artifactId>torqlang-util</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>org.torqlang</groupId>
            <artifactId>torqlang-klvm</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>org.torqlang</groupId>
            <artifactId>torqlang-examples</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
    </dependencies>

</project>

Hello, World!

In this section, we learn how to create and run simple Torq programs.

HelloWorld

The fundamental unit of concurrency in Torq is the actor. In our diagram below, RequestClient and HelloWorld are actors that run concurrently with the potential to run in parallel. RequestClient sends a 'hello' message to HelloWorld, and in response, HelloWorld sends a 'Hello, World!' message to RequestClient. Note the sold line arrow. The labels are read clockwise, so that we say 'hello' is sent from RequestClient to HelloWorld and 'Hello, World!' is sent from HelloWorld to RequestClient. The open arrow points toward the recipient of the request and the closed arrow points toward the recipient of the response, the terminus of our interaction.

HelloWorld Diagram

Programming HelloWorld

HelloWorld is defined beginning with the actor keyword. Following the actor keyword is the name HelloWorld. An actor can accept parameters that affect how it is configured. In our case, there are no parameters. Next, the actor requires that a body be defined between enclosing in and end keywords. The HelloWorld actor contains just one statement in its body, an ask handler, specified as handle ask 'hello' in ... end.

actor HelloWorld() in
    handle ask 'hello' in
        'Hello, World!'
    end
end

Our ask handler is programmed to receive 'hello' messages one at a time. If additional 'hello' messages are received while one is being processed, they are queued until HelloWorld is ready to receive another. Like our actor, our ask handler requires that a body be defined between enclosing in and end keywords. Our ask handler contains a single expression in its body, the 'Hello, World!' string literal. By convention, an ask handler returns the last expression as its result.

Running HelloWorld

Torq is a dynamic language run on the JVM. In this section, we place our source into a Java String literal. With our source defined, we build and execute our program from Java using a fluent builder tool.

public static final String SOURCE = """
    actor HelloWorld() in
        handle ask 'hello' in
            'Hello, World!'
        end
    end""";

public static void perform() throws Exception {

    // Build and spawn HelloWorld. After spawning, HelloWorld is waiting to
    // receive the 'hello' message. The spawn method returns an actorRef used
    // subsequently to send it messages.
    ActorRef actorRef = Actor.builder()
        .spawn(SOURCE);

    // Send the 'hello' message to the actor reference returned above. Wait a
    // maximum of 100 milliseconds for a response.
    Object response = RequestClient.builder()
        .sendAndAwaitResponse(actorRef, Str.of("hello"), 100, TimeUnit.MILLISECONDS);
    checkExpectedResponse(Str.of("Hello, World!"), response);
}

Click org.torqlang.examples.HelloWorld.java to see the full source code on GitHub.

HelloWorldWithGoodbye

In our HelloWorldWithGoodbye diagram, we extend HelloWorld with a 'goodbye' handler. We use two instances of RequestClient to send two different requests to just one instance of HellWorldWithGoodbye. This demonstrates a long-running actor servicing different requests from multiple clients.

HelloWorldWithGoodbye Diagram

Programming HelloWorldWithGoodbye

actor HelloWorld() in
    handle ask 'hello' in
        'Hello, World!'
    end
    handle ask 'goodbye' in
        'Goodbye, World!'
    end
end

Running HelloWorldWithGoodbye

public static final String SOURCE = """
    actor HelloWorld() in
        handle ask 'hello' in
            'Hello, World!'
        end
        handle ask 'goodbye' in
            'Goodbye, World!'
        end
    end""";

public static void perform() throws Exception {

    // Build and spawn HelloWorld. After spawning, HelloWorld is waiting to
    // receive the 'hello' or 'goodbye' message. The spawn method returns an
    // actorRef used subsequently to send it messages.
    ActorRef actorRef = Actor.builder()
        .spawn(SOURCE);

    // Send the first message -- 'hello'
    Object response = RequestClient.builder()
        .sendAndAwaitResponse(actorRef, Str.of("hello"), 100, TimeUnit.MILLISECONDS);
    checkExpectedResponse(Str.of("Hello, World!"), response);

    // Send the second message -- 'goodbye'
    response = RequestClient.builder()
        .sendAndAwaitResponse(actorRef, Str.of("goodbye"), 100, TimeUnit.MILLISECONDS);
    checkExpectedResponse(Str.of("Goodbye, World!"), response);
}

Summary

In this section, we learned:

  1. How to program concurrent actors
  2. How to run an actor program from Java
  3. How to use a concurrent actor to service multiple requests

Data Types

  • Values
    • Numbers
      • Dec
      • Flt
      • Int
    • Literals
      • Bool
      • Null
      • Str
      • Token
  • Records
    • Syntax
      • Labels
      • Features
    • Selecting using .
    • Selecting using []
    • Feature order

Functions

Factorial

Factorial

Programming Factorial

actor Factorial() in
    func fact(x) in
        func fact_cps(n, k) in
            if n < 2 then k
            else fact_cps(n - 1, n * k) end
        end
        fact_cps(x, 1)
    end
    handle ask x in
        fact(x)
    end
end

Running Factorial

public static final String SOURCE = """
    actor Factorial() in
        func fact(x) in
            func fact_cps(n, k) in
                if n < 2 then k
                else fact_cps(n - 1, n * k) end
            end
            fact_cps(x, 1)
        end
        handle ask x in
            fact(x)
        end
    end""";

public static void perform() throws Exception {

    ActorRef actorRef = Actor.builder()
        .spawn(SOURCE);

    Object response = RequestClient.builder()
        .sendAndAwaitResponse(actorRef, Int64.of(10), 100, TimeUnit.MILLISECONDS);

    checkExpectedResponse(Int64.of(3628800), response);
}

Child Actors

ConcurrentMath

Concurrent Math Diagram

Programming ConcurrentMath

actor ConcurrentMath() in
    actor Number(n) in
        handle ask 'get' in
            n
        end
    end
    var n1 = spawn(Number.cfg(1)),
        n2 = spawn(Number.cfg(2)),
        n3 = spawn(Number.cfg(3))
    handle ask 'calculate' in
        n1.ask('get') + n2.ask('get') * n3.ask('get')
    end
end

Running ConcurrentMath

public static final String SOURCE = """
    actor ConcurrentMath() in
        actor Number(n) in
            handle ask 'get' in
                n
            end
        end
        var n1 = spawn(Number.cfg(1)),
            n2 = spawn(Number.cfg(2)),
            n3 = spawn(Number.cfg(3))
        handle ask 'calculate' in
            n1.ask('get') + n2.ask('get') * n3.ask('get')
        end
    end""";

public static void perform() throws Exception {

    ActorRef actorRef = Actor.builder()
        .spawn(SOURCE);

    Object response = RequestClient.builder()
        .sendAndAwaitResponse(actorRef, Str.of("calculate"),
            100, TimeUnit.MILLISECONDS);
    checkExpectedResponse(Int32.of(7), response);
}

ConcurrentMath with increment

Concurrent Math Diagram

Programming ConcurrentMath with increment

actor ConcurrentMath() in
    import system.Cell
    actor Number(n) in
        var value = Cell.new(n)
        handle ask 'get' in
            @value
        end
        handle tell 'incr' in
            value := @value + 1
        end
    end
    var n1 = spawn(Number.cfg(0)),
        n2 = spawn(Number.cfg(0)),
        n3 = spawn(Number.cfg(0))
    handle ask 'calculate' in
        n1.tell('incr')
        n2.tell('incr'); n2.tell('incr')
        n3.tell('incr'); n3.tell('incr'); n3.tell('incr')
        n1.ask('get') + n2.ask('get') * n3.ask('get')
    end
end

Running ConcurrentMath with increment

public static final String SOURCE = """
    actor ConcurrentMath() in
        import system.Cell
        actor Number(n) in
            var value = Cell.new(n)
            handle ask 'get' in
                @value
            end
            handle tell 'incr' in
                value := @value + 1
            end
        end
        var n1 = spawn(Number.cfg(0)),
            n2 = spawn(Number.cfg(0)),
            n3 = spawn(Number.cfg(0))
        handle ask 'calculate' in
            n1.tell('incr')
            n2.tell('incr'); n2.tell('incr')
            n3.tell('incr'); n3.tell('incr'); n3.tell('incr')
            n1.ask('get') + n2.ask('get') * n3.ask('get')
        end
    end""";

public static void perform() throws Exception {

    ActorRef actorRef = Actor.builder()
        .spawn(SOURCE);

    // 1 + 2 * 3
    Object response = RequestClient.builder().sendAndAwaitResponse(actorRef,
        Str.of("calculate"), 100, TimeUnit.MILLISECONDS);
    checkExpectedResponse(Int32.of(7), response);

    // 2 + 4 * 6
    response = RequestClient.builder().sendAndAwaitResponse(actorRef,
        Str.of("calculate"), 100, TimeUnit.MILLISECONDS);
    checkExpectedResponse(Int32.of(26), response);

    // 3 + 6 * 9
    response = RequestClient.builder().sendAndAwaitResponse(actorRef,
        Str.of("calculate"), 100, TimeUnit.MILLISECONDS);
    checkExpectedResponse(Int32.of(57), response);
}

Error Handling

Error Messages

  • Reference standard JSON schema for error handling
  • Align Torq error messages with JSON standard
  • Document the techniques for return standard error messages

Failed Values

  • Explain and demonstrate how FailedValues work
  • Explain how FailedValue is mapped to standard JSON errors

Native Actors

In this chapter, we provide the following sections:

Northwind Database

In this section, we implement the Northwind database as a through-cache server using a directory as a backing storage. The goal is to create a realistic, self-contained data source that not only serves as an example of using native actors but can also be used in follow-on examples and benchmarking.

Read-Through Caching

A read-through cache acts as an intermediary between an application and a database. When an application reads from the cache, the cache checks if the data is already in memory. If the data is there, the cache returns it directly to the application. If the data is not in memory, the cache loads it from the database, stores it in memory, and then returns it to the application.

Write-Through Caching

A write-through cache acts as an intermediary between an application and a database. When an application writes to the cache, the cache writes the data to memory first and then immediately writes that cached data through to the database. Processing suspends until the data is successfully written to the database.

NorthwindDB

The Northwind database uses 4 native actors (A, B, C, and D), 1 shared memory cache (E), and one file system directory (F).

TODO: Read/Write actions in the NorthwindDb diagram need to be corrected to match the actual protocol consisting of Find/Create/Update/Delete vernacular.

Northwind DB Diagram

Native Performance

In this section, we evaluate the performance of our NorthwindDb actor design.

The ultimate goal in scalability is linear scalability, where doubling the number of cores doubles the amount of work your program completes per unit of time, perfect scalability. In practice, this is never achieved (research Amdahl's Law). Moreover, multicore processors can perform worse if applications ignore their effects on CPU caches.

To move our designs toward linear scalability, we must understand how CPUs work best and how to avoid problems created by using threads:

  • Threads are heavyweight requiring lots of memory and CPU cycles
  • Waiting threads are almost as expensive as active threads
  • I/O reads and writes must wait somewhere without blocking threads

In this section we document how:

  1. Decreasing thread counts may increase throughput for CPU-bound actors
  2. Increasing thread counts may increase throughput for I/O-bound actors

Performance summary

Read-only NorthwindDb

ReadersSimulated LatencyMillis per readThroughput per second
200.000442,257,829.57

Executors and Thread Pools

We use executors to partition our actors into CPU-bound or I/O-bound thread pools. CPU-bound actors do not block threads while waiting and need only a few threads to maintain throughput. I/O-bound actors, on the other hand, block threads while they wait for storage or network I/O to complete. As a consequence, they need more threads to maintain throughput.

The common practice for allocating a thread pool is to allocate n + 1 threads where n is the number of available hardware threads. Brian Goetz, one of the authors of "Java Concurrency in Practice," suggests a formula that uses more threads for I/O bound processes: thread_pool_size = available_hw_threads * target_utilization * (1 + wait_time / compute_time)

The formula above uses two variables to calculate the base allocation of threads for a thread pool available_hw_threads * target_utilization. Next, the formula calculates the ratio wait_time / compute_time and uses it to increase the resulting thread pool size. It increases the pool size dramatically if we are I/O-bound.

Consider a CPU-bound ratio of 1 I/O second for every 10 compute seconds, resulting in a one-tenth increase 1 / 10. Now, consider an I/O-bound ratio of 10 I/O seconds for every 1 compute second, resulting in a 10 times increase 10 / 1. If we have 32 hardware threads, but only want to utilize 25% of them per thread pool, we get:

  • CPU-bound thread pool: thread_pool_size = 32 * .25 * (1 + 1/10) = 8.8 or 9 if we round up
  • I/O-bound thread pool: thread_pool_size = 32 * .25 * (1 + 10/1) = 88

As we can see, this formula allocates far more threads for I/O bound actors. Our example used the simple ratios 1 / 10 and 10 / 1, respectively. In practice, actual pool sizes will vary as wait_time / compute_time varies.

CPU Executors

TODO: Discuss the AffinityExecutor used to produce 0.00044ms NorthwindDb read times. Demonstrate how few threads produced higher throughput by reducing cache misses.

NOTE: The following cache misses are for 10 iterations. Replace these examples with single iterations.

With 3 total threads:

1,435,452,059      cache-misses:u
RunNorthwindDb
  Total time: 1,439 millis
  Total reads: 3,100,000
  Millis per read: 0.00046
  Reads per second: 2,154,273.80

With 32 total threads:

5,697,015,968      cache-misses:u
RunNorthwindDb
  Total time: 4,997 millis
  Total reads: 3,100,000
  Millis per read: 0.00161
  Reads per second: 620,372.22

I/O Executors

TODO: Insert an example that leverages more threads to reduce tail latency. Use NorthwindDb readLatencyInNanos parameter to simulate I/O read latency and its effect on concurrency.

Comparative Performance

The non-Torq numbers below are, in most instances, best-case numbers. See the underlying references for details. In particular, the DynamoDB numbers are server-side latency numbers that do not include connection and download overhead.

0.000000222ms (or 0.222 nanoseconds) -- 1 CPU cycle
Calculated for a 4.5Ghz processor. Faster CPU clocks perform faster calculations.
0.000000667ms to 0.000000888ms (or 0.667 to 0.888 nanoseconds) -- read from L1 CPU cache
Based on a 4.5Ghz processor.
0.00000222ms to 0.000002664ms (or 2.22 to 2.66 nanoseconds) -- read from L2 CPU cache
Based on a 4.5Ghz processor.
0.00000666ms to 0.00001554 (or 6.66 to 15.54 nanoseconds) -- read from L3 CPU cache
Based on a 4.5Ghz processor.
0.0000222ms to 0.0000333ms (or 22.2 to 33.3 nanoseconds) -- read from RAM
Based on a 4.5Ghz processor.
0.00007ms (70 nanoseconds) -- read 1 Northwind customer from a list of 29 customers
Based on a synchronous, blocking, straight-line Java process using a 4.5GHz processor. Demonstrates nothing is faster than a straight-line process. However, a straight-line process does not support thousands of concurrent users maintaining a database.
0.00044ms (440 nanoseconds) -- read 1 Northwind customer using NorthwindDb actor
Performed asynchronously with 2 underlying NorthwindReader actors with affinity scheduling using a 4.5GHz processor.
0.0172ms (or 17.2 microseconds) -- send 1,518 byte ethernet packet
Based on a 1GbE and 5 microsecond switch latency. Where network_latency = (packet_size_bits / bit_rate) + switch_delay.
0.5ms to 1.0ms -- DynamoDB DAX (server-side latency)
"Amazon DynamoDB Accelerator (DAX) is a fully managed, highly available caching service built for Amazon DynamoDB. DAX delivers up to a 10 times performance improvement—from milliseconds to microseconds"
2.0ms -- Solid State Drive
Micron 7450 NVMe -- "The Micron 7450 SSD addresses QoS needs with industry-leading, 99.9999% mixed-workload read latencies under 2 milliseconds (ms) while still delivering hundreds of thousands of IOPS."
5.0ms to 10.0ms -- DynamoDB (server-side latency)
"For example, DynamoDB delivers consistent single-digit millisecond performance for a shopping cart use case, whether you've 10 or 100 million users."

References:

Evaluation output

Timings

RunNorthwindJava

RunNorthwindJava
  Total time: 13 millis
  Total reads: 200,000
  Millis per read: 0.00007
  Reads per second: 15,384,615.38

NorthwindDB

1 writer and 2 readers (taskset -c 0-2):

RunNorthwindDb
  Total time: 1,373 millis
  Total reads: 3,100,000
  Millis per read: 0.00044
  Reads per second: 2,257,829.57

Hardware used

Listing produced using lscpu:

Architecture:             x86_64
  CPU op-mode(s):         32-bit, 64-bit
  Address sizes:          48 bits physical, 48 bits virtual
  Byte Order:             Little Endian
CPU(s):                   32
  On-line CPU(s) list:    0-31
Vendor ID:                AuthenticAMD
  Model name:             AMD Ryzen 9 7950X 16-Core Processor
    CPU family:           25
    Model:                97
    Thread(s) per core:   2
    Core(s) per socket:   16
    Socket(s):            1
    Stepping:             2
    CPU(s) scaling MHz:   25%
    CPU max MHz:          5881.0000
    CPU min MHz:          545.0000

Native Packs

Torq is a dynamic companion language that runs within the JVM and integrates easily with its runtime. A Torq Pack is a construct for declaring, bundling, and exporting items for integration. A pack contains classes, objects, and methods and makes them available for import.

ArrayListPack

As an example object, consider the builtin Torq type named ArrayList that corresponds to the well-known Java ArrayList. The constructs in the pack are named by convention:

  • The type name is ArrayList
  • The pack name is ArrayListPack
  • The class name is ArrayListCls
  • The object name is ArrayListObj
  • Object methods begin with obj, such as objSize
  • Class methods begin with cls, such as clsNew

The ArrayList implementation is found in the Java file org.torqlang.local.ArrayListPack.

TimerPack

As an example actor, consider the builtin actor named Timer:

  • The type name is Timer
  • The pack name is TimerPack
  • The actor record is TimerPack.TIMER_ACTOR

The Timer implementation is found in the Java file org.torqlang.local.TimerPack.

NorthwindDbPack

In this section, we create a native pack as a wrapper for the NorthwindDb actor created previously.

Building an API Server

In this chapter, we provide the following sections:

Northwind API

API Performance

Building a GraphQL-JSON server

GraphQL-JSON

Northwind GraphQL-JSON

API and GraphQL-JSON Performance

This section evaluates a mixed workload server, demonstrating the performance effects of serving API and GraphQL loads at scale.

Configurators

Actors are not simply constructed. Instead, they are configured, and later, the configuration is spawned. The evolution from source statement to spawned actor can be described in four steps:

  1. Generate -- compile the source to kernel code that creates actor records
  2. Construct -- execute the kernel code to create a configurator
  3. Configure -- invoke the configurator to create a configuration
  4. Spawn -- spawn a concurrent process using the configuration

Generate and Construct

HelloWorld

In this section, we compile the following HelloWorld source into kernel code that, when executed, constructs an actor record containing a configurator.

01  actor HelloWorld(init_name) in
02      import system.Cell
03      var my_name = Cell.new(init_name)
04      handle tell {'name': name} in
05          my_name := name
06      end
07      handle ask 'hello' in
08          'Hello, World! My name is ' + @my_name + '.'
09      end
10  end

Compiling HelloWorld generates the following kernel code with code omitted in certain places to keep the example concise. Omissions are noted using <<...>>.

01  local $actor_cfgtr in
02      $create_actor_cfgtr(proc (init_name, $r) in // free vars: $import, $respond
03          local Cell, my_name, $v0, $v6 in
04              $import('system', ['Cell'])
05              $select_apply(Cell, ['new'], init_name, my_name)
06              $create_proc(proc ($m) <<...>> in end, $v0)
07              $create_proc(proc ($m) <<...>> in end, $v6)
08              $create_tuple('handlers'#[$v0, $v6], $r)
09          end
10      end, $actor_cfgtr)
11      $create_rec('HelloWorld'#{'cfg': $actor_cfgtr}, HelloWorld)
12  end

When executed, the kernel code creates the actor record 'HelloWorld'#{'cfg': $actor_cfgtr} at line 11. All actor records conform to the pattern 'ACTORNAME'#{'cfg': $actor_cfgtr} where ACTORNAME is the name used in the source code and $actor_cfgtr is a configurator compiled from the actor body source.

Next, we discuss how configurators are used to produce configurations.

Configure and Spawn

Configuring and spawning an actor involves two special Java classes: ActorCfgtr and ActorCfg.

The configurator discussed previously is an instance of ActorCfgtr, and when invoked, produces an instance of ActorCfg. The essence of both Java classes are shown below.

public class ActorCfgtr implements Proc {
    private final Closure handlersCtor;

    public ActorCfgtr(Closure handlersCtor) {
        this.handlersCtor = handlersCtor;
    }

    @Override
    public final void apply(List<CompleteOrIdent> ys, Env env, Machine machine) throws WaitException {
        List<Complete> resArgs = new ArrayList<>(ys.size());
        for (int i = 0; i < ys.size() - 1; i++) {
            CompleteOrIdent y = ys.get(i);
            Complete yRes = y.resolveValue(env).checkComplete();
            resArgs.add(yRes);
        }
        CompleteOrIdent target = ys.get(ys.size() - 1);
        ValueOrVar targetRes = target.resolveValueOrVar(env);
        ActorCfg actorCfg = new ActorCfg(resArgs, handlersCtor);
        targetRes.bindToValue(actorCfg, null);
    }
}
public final class ActorCfg implements Obj {

    private final List<Complete> args;
    private final Closure handlersCtor;

    public ActorCfg(List<Complete> args, Closure handlersCtor) {
        this.args = args;
        this.handlersCtor = handlersCtor;
    }
}

PerformHelloWorld

In this section, we compile PerformHelloWorld and discuss how a parent actor configures and spawns its nested actor HelloWorld discussed previously.

01  actor PerformHelloWorld() in
02      actor HelloWorld(init_name) in
03          import system.Cell
04          var my_name = Cell.new(init_name)
05          handle tell {'name': name} in
06              my_name := name
07          end
08          handle ask 'hello' in
09              'Hello, World! My name is ' + @my_name + '.'
10          end
11      end
12      handle ask 'perform' in
13          var hello_world = spawn(HelloWorld.cfg('Bob'))
14          var hello_bob = hello_world.ask('hello')
15          hello_world.tell({'name': 'Bobby'})
16          var hello_bobby = hello_world.ask('hello')
17          [hello_bob, hello_bobby]
18      end
19  end

Compiling PerformHelloWorld generates the following kernel code containing our nested actor HelloWorld. Again, omissions are noted using <<...>>. Note the two invocations of $create_actor_cfgtr at lines 02 and 04, which create configurators for PerformHelloWorld and HelloWorld, respectively.

01  local $actor_cfgtr in
02      $create_actor_cfgtr(proc ($r) in // free vars: $import, $respond, $spawn
03          local HelloWorld, $actor_cfgtr, $v9, $v15 in
04              $create_actor_cfgtr(proc (init_name, $r) in // free vars: $import, $respond
05                  local Cell, my_name, $v0, $v6 in
06                      $import('system', ['Cell'])
07                      $select_apply(Cell, ['new'], init_name, my_name)
08                      $create_proc(proc ($m) in <<...>> end, $v0)
09                      $create_proc(proc ($m) in <<...>> end, $v6)
10                      $create_tuple('handlers'#[$v0, $v6], $r)
11                  end
12              end, $actor_cfgtr)
13              $create_rec('HelloWorld'#{'cfg': $actor_cfgtr}, HelloWorld)
14              $create_proc(proc ($m) in // free vars: $respond, $spawn, HelloWorld
15                  local $else in
16                      $create_proc(proc () in <<...>> end, $else)
17                      case $m of 'perform' then
18                          local $v12, hello_world, hello_bob, hello_bobby in
19                              local $v13 in
20                                  $select_apply(HelloWorld, ['cfg'], 'Bob', $v13)
21                                  $spawn($v13, hello_world)
22                              end
23                              $select_apply(hello_world, ['ask'], 'hello', hello_bob)
24                              local $v14 in
25                                  $bind({'name': 'Bobby'}, $v14)
26                                  $select_apply(hello_world, ['tell'], $v14)
27                              end
28                              $select_apply(hello_world, ['ask'], 'hello', hello_bobby)
29                              $create_tuple([hello_bob, hello_bobby], $v12)
30                              $respond($v12)
31                          end
32                      else
33                          $else()
34                      end
35                  end
36              end, $v9)
37              $create_proc(proc ($m) in <<...>> end, $v15)
38              $create_tuple('handlers'#[$v9, $v15], $r)
39          end
40      end, $actor_cfgtr)
41      $create_rec('PerformHelloWorld'#{'cfg': $actor_cfgtr}, PerformHelloWorld)
42  end

The nested HelloWorld actor is defined between lines 04 and 13. At run time, PerformHelloWorld configures and spawns a HelloWorld child as part of its handle ask 'perform' in <<...>> end kernel code defined between lines 19 and 22. The HelloWorld configurator (an instance of ActorCfgtr) is invoked at line 20 to instantiate an ActorCfg and bind it to variable $v13. Subsequently, the $spawn instruction launches a concurrent actor using the ActorCfg bound to $v13. The result of the $spawn instruction is an ActorRef bound to the hello_world variable. Later, the hello_world variable is used to send messages to the new child actor.

Native Configurators

In this section, we present a timer example that uses a native actor. The Torq program below configures and spawns a timer at line 05 as part of stream expression.

01 actor IterateTimerTicks() in
02     import system[Cell, Stream, Timer, ValueIter]
03     handle ask 'iterate' in
04         var tick_count = Cell.new(0)
05         var timer_stream = Stream.new(spawn(Timer.cfg(1, 'microseconds')),
06             'request'#{'ticks': 5})
07         for tick in ValueIter.new(timer_stream) do
08             tick_count := @tick_count + 1
09         end
10         @tick_count
11     end
12 end

The Java program below runs the example from above and validates that the stream generated five timer ticks.

ActorRef actorRef = Actor.builder()
    .setAddress(Address.create(getClass().getName() + "Actor"))
    .setSource(source)
    .spawn()
    .actorRef();

Object response = RequestClient.builder()
    .setAddress(Address.create("IterateTimerTicksClient"))
    .send(actorRef, Str.of("iterate"))
    .awaitResponse(100, TimeUnit.MILLISECONDS);

assertEquals(Int32.of(5), response);

The timer implementation is provided as a TimerPack.

final class TimerPack {

    public static final Ident TIMER_IDENT = Ident.create("Timer");
    private static final int TIMER_CFGTR_ARG_COUNT = 3;
    private static final CompleteProc TIMER_CFGTR = TimerPack::timerCfgtr;
    public static final CompleteRec TIMER_ACTOR = createTimerActor();

    private static CompleteRec createTimerActor() {
        return CompleteRec.singleton(Actor.CFG, TIMER_CFGTR);
    }

    private static void timerCfgtr(List<CompleteOrIdent> ys, Env env, Machine machine) throws WaitException {
        if (ys.size() != TIMER_CFGTR_ARG_COUNT) {
            throw new InvalidArgCountError(TIMER_CFGTR_ARG_COUNT, ys, "timerCfgtr");
        }
        Num period = (Num) ys.get(0).resolveValue(env);
        Str timeUnit = (Str) ys.get(1).resolveValue(env);
        TimerCfg config = new TimerCfg(period, timeUnit);
        ys.get(2).resolveValueOrVar(env).bindToValue(config, null);
    }

    private static final class Timer extends AbstractActor {
        // <<...>>
    }

    private static final class TimerCfg extends OpaqueValue implements NativeActorCfg {
        final Num periodNum;
        final Str timeUnitStr;

        TimerCfg(Num periodNum, Str timeUnitStr) {
            this.periodNum = periodNum;
            this.timeUnitStr = timeUnitStr;
        }

        @Override
        public final ActorRef spawn(Address address, ActorSystem system, boolean trace) {
            return new Timer(address, system, trace, periodNum, timeUnitStr);
        }
    }

}

Note the following TimerPack elements and how they correlate to compiled Torq:

  1. TIMER_ACTOR is a Torq record, which is the same structure produced when a source statement actor <<...>> end is compiled.
  2. TIMER_CFGTR is a configurator and part of the TIMER_ACTOR record.
  3. TimerCfg is an instance of NativeActorConfig

At run time, LocalActor recognizes the difference between Torq and Java configurations and spawns appropriately.

ActorRefObj childRefObj;
if (config instanceof ActorCfg actorCfg) {
    childRefObj = spawnActorCfg(actorCfg);
} else {
    childRefObj = spawnNativeActorCfg((NativeActorCfg) config);
}

Spawning an actor from Java

The previous sections describe how Torq and native actors are spawned from Torq. Here, we briefly discuss how to spawn actors from Java and obtain an instance of ActorRef to send the actor messages.

To configure an instance of a Torq actor, begin with a fluent builder instance using ActorBuilder.builder(). Actor builders are used throughout this book to run examples.

To configure an instance of a native actor, you must use the elements it provides. For example, the Timer actor above is held privately in the TimerPack, so you must access its configurator from its actor record.

Appendix

In the appendix, we provide the following sections:

Appendix A: Basic Types

Torq is a gradual, nominative, and parametric type system.

The Torq gradual type system is a work in progress.

Scalar and composite types

- Value
    - Comp
        - Obj
        - Rec
            - Tuple
            - Array
    - Literal
        - Bool
        - Eof
        - Null
        - Str
        - Token
    - Num
        - Dec128
        - Flt64
            - Flt32
        - Int64
            - Int32
                - Char
    - Meth
        - Func
        - Proc

Actor, label and feature types

- Value
    - Comp
        - Obj
            - ActorCfg
    - Feature
        - Int32
        - Literal
    - Meth
        - Func
            - ActorCfgtr

Other types

The dynamic type

Every type is compatible with Any.

Consider the notion of strong arrows as described here: https://elixir-lang.org/blog/2023/09/20/strong-arrows-gradual-typing/

The FailedValue type

A failed value is a pseudo value used to propagate errors across actor boundaries. Any value may contain in its place a FailedValue. Accessing a failed value raises an exception that is ultimately returned to requesters or logged.

Appendix B. Builtin Packs

ArrayList

A wrapper for Java ArrayList that can contain zero or more values.

Class Methods

ArrayList.new() -> ArrayList

Create an empty ArrayList using the underlying Java ArrayList() constructor.

ArrayList.new(values::Tuple) -> ArrayList

Create an ArrayList using the Java ArrayList(field_count) constructor, where field_count is the values size. Add each value in values to the newly created list.

After creation, an ArrayList can contain a mix of bound and unbound dataflow variables.

Object Methods

array_list.add(element::Value)

Add element to the list using the corresponding Java add(element) method.

array_list.clear()

Invoke the corresponding Java clear() method.

array_list.size() -> Int32

Invoke the corresponding Java size() method and return its size as an Int32.

array_list.to_tuple() -> Tuple

Create and return a Tuple where each field corresponds in index and value to the elements of array_list.

Iterator Sourcing

  • ValueIter

Cell

A mutable container with a single value.

Class Methods

Cell.new(initial::Value) -> Cell

Create a Cell with an initial value initial.


FeatureIter

If a type is a FeatIter source, it can be used as an argument in FeatIter.new(iter_source) expressions.


FieldIter

A FieldIter is a generator. It defines no parameters and each time it is called, it returns either the next field or EOF. Once an iterator returns EOF it will always return EOF.

If a type is a FieldIter source, it can be used as an argument in FieldIter.new(iter_source) expressions.

Class Methods

FieldIter.new(record::Rec) -> func () -> ([Feat, Value] | Eof)

Fields are returned in feature cardinality order:

  1. Integers in ascending order
  2. Strings in lexicographic order
  3. Booleans in order with false before true
  4. Nulls
  5. Tokens in id order

HashMap

A wrapper for Java HashMap that can contain zero or more mappings.

Class Methods

HashMap.new() -> HashMap

Create an empty HashMap using the underlying Java HashMap() constructor.

Object Methods

hash_map.get(key::Value) -> Value

Suspend until key becomes bound. Return the mapped value at key.

hash_map.put(key::Value, value::Value)

Suspend until key becomes bound. Invoke the underlying put method using key and value arguments.

Iterator Sourcing

  • FeatIter
  • FieldIter
  • ValueIter

LocalDate

A wrapper for Java LocalDate.

Class Methods

LocalDate.new(date::Str) -> LocalDate

Create a LocalDate instance using the underlying java.time.LocalDate#parse(CharSequence text) method.


RangeIter

A RangeIter is a generator. It defines no parameters and each time it is called, it returns either the next number or EOF. Once an iterator returns EOF it will always return EOF.

Class Methods

RangeIter.new(from::Int32, to::Int32) -> func () -> (Int32 | Eof)

The range includes from but excludes to.


Rec

Class Methods

Rec.assign(from::Rec, to::Rec) -> Rec

Create a new record as if it was initialized with the to fields, and then was assigned from fields in a way that new fields are added and existing fields are replaced.

var from = { b: 4, c: 5 }
var to = { a: 1, b: 2 }
var result = Rec.assign(from, to)
// result = { a: 1, b: 4, c: 5 }

Rec.size(rec::Rec) -> Int32

Return the field count in rec.


Str

Instance Methods

str.substring(start::Int32) -> Str

str.substring(start::Int32, stop::Int32) -> Str


Stream

Class Methods

Stream.new(actor::Actor, request_message::Value) -> func () -> Rec

Iterator Sourcing

  • ValueIter

StringBuilder

TBD

Timer

A Timer generates a stream of ticks as <period 1>, <tick 1>, ..., <period n>, <tick n> where each <period n> is a delay and each <tick 1> is a response.

A timer can be configured with a period and a time unit.

var timer_cfg = Timer.cfg(1, 'seconds')

Subsequently, a timer is spawned as a publisher.

var timer_pub = spawn(timer_cfg)

Once spawned, a timer can be used as a stream. The following example iterates over 5 one-second timer ticks.

var timer_pub = spawn(Timer.cfg(1, 'seconds'))
var tick_count = Cell.new(0)
var timer_stream = Stream.new(timer_pub, 'request'#{'ticks': 5})
for tick in Iter.new(timer_stream) do
    tick_count := @tick_count + 1
end
@tick_count

A timer can only be used by one requester (subscriber) at a time.

Configurators

Timer.cfg(period::Int32, time_unit::Str) -> ActorCfg

Configure a timer with the given delay period and its time unit.

Protocol

'request'#{'ticks': Int32} -> StreamSource<Int32>

Respond with the requested number of ticks where each tick is preceded by the delay period defined when the timer was configured.


Token

A Token is an unforgeable value. Typically, tokens are used in-process to secure services. A requester that has been authenticated or authorized is issued a token. Later, the requester must present the token as proof.

Class Methods

Token.new() -> Token

Create a new token.


ValueIter

If a type is a ValueIter source, it can be used as an argument in ValueIter.new(iter_source) expressions.

Appendix C: Linux Commands

This appendix contains linux commands used in this book.

List CPU

List CPU information. In this instance, we listed the CPU information for one of our workstations running an AMD Ryzen 9 CPU.

lscpu
Architecture:             x86_64
  CPU op-mode(s):         32-bit, 64-bit
  Address sizes:          48 bits physical, 48 bits virtual
  Byte Order:             Little Endian
CPU(s):                   32
  On-line CPU(s) list:    0-31
Vendor ID:                AuthenticAMD
  Model name:             AMD Ryzen 9 7950X 16-Core Processor
    CPU family:           25
    Model:                97
    Thread(s) per core:   2
    Core(s) per socket:   16
    Socket(s):            1
    Stepping:             2
    CPU(s) scaling MHz:   25%
    CPU max MHz:          5881.0000
    CPU min MHz:          545.0000

Run on select CPU Cores

Run a Java program constrained to a select set of hardware threads. In this instance, we run RunNorthwindDb using cores 0, 1, and 2.

taskset -c 0-2 java -XX:+UseZGC -p ~/workspace/torq_jv_runtime -m org.torqlang.examples/org.torqlang.examples.RunNorthwindDb

Display runtime stats

Show cache misses:

perf stat -e cache-misses COMMAND

Show cache misses while running RunNorthwindDb using 3 cores:

perf stat -e cache-misses taskset -c 0-2 java -XX:+UseZGC -p ~/workspace/torq_jv_runtime -m org.torqlang.examples/org.torqlang.examples.RunNorthwindDb

Appendix D: Grammar

Operators and Symbols

Priority

  • 1 = Highest Priority
  • 9 = Lowest Priority
OperatorDescription of OperatorPriorityAssociativity
@Get cell value1Right to left
.Select2Left to right
[]Select2Left to right
()Apply2Left to right
!Logical NOT3Right to left
Negate3Right to left
%Remainder4Left to right
/Divide4Left to right
*Multiply4Left to right
Subtraction5Left to right
+Addition5Left to right
>Greater than6Left to right
<Less than6Left to right
>=Greater than or equal6Left to right
<=Less than or equal6Left to right
==Equal to6Left to right
!=Not equal to6Left to right
&&Logical AND7Left to right
||Logical OR8Left to right
=Assign value (unify)9Right to left
:=Set cell value9Right to left

The partial arity symbol:

  • ... -- partial arity

Keywords

  • act
  • actor
  • as (weak)
  • ask (weak)
  • begin
  • break
  • case
  • catch
  • continue
  • do
  • else
  • elseif
  • end
  • eof
  • false
  • finally
  • for
  • func
  • handle (weak)
  • if
  • import
  • in
  • local
  • null
  • of
  • proc
  • return
  • self
  • skip
  • spawn
  • tell (weak)
  • then
  • throw
  • true
  • try
  • var
  • when
  • while

ANTLR BNF

grammar torqlang;

// Although this grammar may be used to create parsers, it was derived for
// documentation purposes from the hand-written Torqlang lexer and parser.

//******************//
//   PARSER RULES   //
//******************//

program: stmt_or_expr EOF;

stmt_or_expr: meta? assign ';'*;

meta: 'meta' '#' (meta_rec | meta_tuple);

meta_rec: '{' meta_field (',' meta_field)* '}';

meta_tuple: '[' meta_value (',' meta_value)* ']';

meta_field: STR_LITERAL ':' meta_value;

meta_value: meta_rec | meta_tuple | bool |
            STR_LITERAL | INT_LITERAL;

assign: or ('=' or | ':=' or)?;

or: and ('||' and)*;

and: relational ('&&' relational)*;

relational: sum (relational_oper sum)*;

relational_oper: '<' | '>' | '==' | '!=' | '<=' | '>=';

sum: product (sum_oper product)*;

sum_oper: '+' | '-';

product: unary (product_oper unary)*;

product_oper: '*' | '/' | '%';

unary: select_or_apply | (negate_or_not unary)+;

negate_or_not: '-' | '!';

select_or_apply: access (('.' access) | ('[' stmt_or_expr ']') |
                 ('(' arg_list? ')'))*;

access: '@' access | construct;

construct: keyword | ident | value;

keyword: act | actor | begin | 'break' | case | 'continue' |
         for | func | group | if | import_ | local | proc |
         throw | return | 'self' | 'skip' | spawn | try | var |
         while;

act: 'act' stmt_or_expr+ 'end';

actor: 'actor' ident? '(' pat_list? ')' 'in'
       (stmt_or_expr | message_handler)+ 'end';

message_handler: 'handle' (tell_handler | ask_handler);

tell_handler: 'tell' pat ('when' stmt_or_expr)?
              'in' stmt_or_expr+ 'end';

ask_handler: 'ask' pat ('when' stmt_or_expr)? return_type_anno?
             'in' stmt_or_expr+ 'end';

begin: 'begin' stmt_or_expr+ 'end';

case: 'case' stmt_or_expr
      ('of' pat ('when' stmt_or_expr)? 'then' stmt_or_expr+)+
      ('else' stmt_or_expr+)? 'end';

for: 'for' pat 'in' stmt_or_expr 'do' stmt_or_expr+ 'end';

func: 'func' ident? '(' pat_list? ')' return_type_anno?
      'in' stmt_or_expr+ 'end';

group: '(' stmt_or_expr+ ')';

if: 'if' stmt_or_expr 'then' stmt_or_expr+
    ('elseif' stmt_or_expr 'then' stmt_or_expr+)*
    ('else' stmt_or_expr+)? 'end';

// `import` is already an ANTLR4 keyword, therefore we create our keyword
// with a trailing underscore
import_: 'import' ident ('.' ident)* ('[' import_alias (',' import_alias)* ']')?;

import_alias: ident ('as' ident)?;

local: 'local' var_decl (',' var_decl)*
       'in' stmt_or_expr+ 'end';

var_decl: pat ('=' stmt_or_expr)?;

proc: 'proc' ident? '(' pat_list? ')'
      'in' stmt_or_expr+ 'end';

throw: 'throw' stmt_or_expr;

return: 'return' stmt_or_expr?;

spawn: 'spawn' '(' arg_list ')';

try: 'try' stmt_or_expr+ ('catch' pat 'then' stmt_or_expr+)*
     ('finally' stmt_or_expr+)? 'end';

var: 'var' var_decl (',' var_decl)*;

while: 'while' stmt_or_expr 'do' stmt_or_expr+ 'end';

arg_list: stmt_or_expr (',' stmt_or_expr)*;

pat_list: pat (',' pat)*;

pat: rec_pat | tuple_pat |
     (label_pat ('#' (rec_pat | tuple_pat))?) |
     INT_LITERAL | (ident var_type_anno?);

label_pat: ('~' ident) | bool | STR_LITERAL | 'eof' | 'null';

rec_pat: '{' (field_pat (',' field_pat)* (',' '...')?)? '}';

tuple_pat: '[' (pat (',' pat)* (',' '...')?)? ']';

field_pat: (feat_pat ':')? pat;

feat_pat: ('~' ident) | bool | INT_LITERAL | STR_LITERAL |
          'eof' | 'null';

value: rec_value | tuple_value |
       (label_value ('#' (rec_value | tuple_value))?) |
       INT_LITERAL | CHAR_LITERAL | FLT_LITERAL | DEC_LITERAL;

label_value: ident | bool | STR_LITERAL | 'eof' | 'null';

rec_value: '{' (field_value (',' field_value)*)? '}';

tuple_value: '[' (value (',' pat)*)? ']';

field_value: (feat_value ':')? stmt_or_expr;

feat_value: ident | bool | INT_LITERAL | STR_LITERAL |
            'eof' | 'null';

var_type_anno: '::' ident;

return_type_anno: '->' ident;

bool: 'true' | 'false';

ident: IDENT | 'ask' | 'tell';

//*****************//
//   LEXER RULES   //
//*****************//

IDENT: ((ALPHA | '_') (ALPHA_NUMERIC | '_')*) |
       '`' (~('`' | '\\') | ESC_SEQ)+ '`';

CHAR_LITERAL: '&' (~'\\' | ESC_SEQ);

STR_LITERAL: STR_SINGLE_QUOTED | STR_DOUBLE_QUOTED;
STR_SINGLE_QUOTED: '\'' (~('\'' | '\\') | ESC_SEQ)* '\'';
STR_DOUBLE_QUOTED: '"' (~('"' | '\\') | ESC_SEQ)* '"';

INT_LITERAL: DIGIT+ [lL]? |
             ('0x' | '0X') HEX_DIGIT+ [lL]?;

FLT_LITERAL: DIGIT+ '.' DIGIT+
             ([eE] ('+' | '-')? DIGIT+)? [fFdD]?;

DEC_LITERAL: DIGIT+ ('.' DIGIT+)? [mM]?;

//
// Define lexical rules for comments and whitespace
//

LINE_COMMENT : '//' .*? '\r'? '\n' -> skip;
COMMENT : '/*' .*? '*/' -> skip;
WS: [ \r\n\t\f\b]+ -> skip;

//
// Define reusable lexical patterns
//

fragment ALPHA_NUMERIC: ALPHA | DIGIT;

fragment ALPHA: UPPER_CASE_ALPHA | LOWER_CASE_ALPHA;
fragment UPPER_CASE_ALPHA: [A-Z];
fragment LOWER_CASE_ALPHA: [a-z];

fragment DIGIT: [0-9];
fragment NZ_DIGIT: [1-9];
fragment HEX_DIGIT: (DIGIT | [a-f] | [A-F]);

fragment ESC_SEQ: '\\' ([rntfb'"`\\] |
                  'u' HEX_DIGIT HEX_DIGIT HEX_DIGIT HEX_DIGIT);

Future Changes

Lang and KLVM names for statements

Torq derives its kernel language from the Oz Computation Model where a semantic statement is a key concept. With that in mind, Torq defines Stmt as a semantic statement in agreement with the Oz model. The language parser uses Sntc for statements to avoid confusion. The Torq implementation for Rust is changing and Torq for Java will change in the future to match it.

  • The KLVM package will use Instr instead of Stmt
  • The language package will use Stmt instead of Sntc
  • The language package will use Sntc instead of Lang

The language package type hierarchy gets rearranged.

Before:

  • Lang
    • Sntc
    • Expr

After:

  • Sntc
    • Stmt
    • Expr

Narrow feature integers from Int64 to Int32

In consideration of value types (Valhalla) and Torq for Rust, we need to narrow the feature domain to the very reasonable range of 0 to 2,147,483,647.

Import multiple components from same package

Change to be more consistent with other language syntax.

Before:

import system[ArrayList as JavaArrayList, Cell, ValueIter]

After:

import system.[ArrayList as JavaArrayList, Cell, ValueIter]

Interactive Debugger

Current thinking is to provide an instrumented version of the KLVM "Machine" class.

Gradual Typing

Type systems generally fall into two categories: static type systems, where every program expression must have a computed type before execution, and dynamic type systems, where nothing is known about types until execution, when the actual values are available.

Gradual typing lies between static typing and dynamic typing. Some variables and expressions are given types and correctness is checked at compile time (static typing). Other expressions are left untyped and type errors are reported at runtime (dynamic typing).

Torq is predisposed to gradual type checking as it already allows type annotations:

  • Invariant variable type: x::Int32
  • Invariant return type: func x(x::Int32) -> Int32

Current thinking:

  • Untyped variables default to Any
  • Type checks such as Int32 == Any succeed at compile time but may fail at run time
  • Type checks such as Int32 == Bool fail at compile time

Actor Protocols

Gradual typing enables explicit protocol definitions.

Parametric Types

Allow type parameters on type declarations and type instantiations. For example, allow ArrayList<Int32>.new().

Do we use <T> like most languages or [T] like Scala?

References

Bravo, M., Li, Z., Van Roy, P., & Meiklejohn, C. (2014, September). Derflow: distributed deterministic dataflow programming for Erlang. In Proceedings of the Thirteenth ACM SIGPLAN workshop on Erlang (pp. 51-60).

Crichton, W., Gray, G., & Krishnamurthi, S. (2023). A Grounded Conceptual Model for Ownership Types in Rust. arXiv preprint arXiv:2309.04134.

Doeraene, S., & Van Roy, P. (2013, July). A new concurrency model for Scala based on a declarative dataflow core. In Proceedings of the 4th Workshop on Scala (pp. 1-10).

Hewitt, C. (2010). Actor model of computation: scalable robust information systems. arXiv preprint arXiv:1008.1459.

Kunicki, Jacek (2019). How (Not) to use reactive Streams in Java 9+. https://www.youtube.com/watch?v=zf_0ydYb3JQ.

Lee, E. A. (2006). The problem with threads. Computer, 39(5), 33-42.

Leger, P., Fukuda, H., & Figueroa, I. (2021). Continuations and Aspects to Tame Callback Hell on the Web. Journal of Universal Computer Science, 27(9), 955-978.

Thompson, C. (2023). How Rust went from a side project to the world’s most-loved programming language. s Interneta, https://www.technologyreview.com/2023/02/14/1067869/rust-worlds-fastest-growing-programming-language, 14.

Van-Roy, P., & Haridi, S. (2004). Concepts, techniques, and models of computer programming. MIT press.

Van Roy, P., Haridi, S., Schulte, C., & Smolka, G. (2020). A history of the Oz multiparadigm language. Proceedings of the ACM on Programming Languages, 4(HOPL), 1-56.

Wadler, P. (1992, February). The essence of functional programming. In Proceedings of the 19th ACM SIGPLAN-SIGACT symposium on Principles of programming languages (pp. 1-14).