Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Intro to Graphix

Graphix is a programming language using the dataflow paradigm. It is particularly well suited to building user interfaces, and interacting with resources in netidx. Dataflow languages like Graphix are "reactive", like React or Vue, except at the language level instead of just as a library. A Graphix program is compiled to a directed graph, operations (such as +) are graph nodes, edges represent paths data can take through the program. A simple expression like,

2 + 2

will compile to a graph like

         
const(2) ──> + <── const(2)
         

The semantics of simple examples like this aren't noticibly different from a normal programming language. However a more complex example such as,

let x = cast<i64>(net::subscribe("/foo")?)?;
print(x * 10)

compiles to a graph like

                                               const(10)
                                                   │
                                                   │
                                                   ▼
                                         
const("/foo") ──> net::subscribe ──> cast<i64> ──> * ──> print
                                         

Unlike the first example, the value of net::subscribe isn't a constant, it can change if the value published in netidx changes. If that happens the new value will flow through the graph and will be printed again. If the published value of "/foo" is initially 10, and then the value of "/foo" changes to 5 then the program will print.

100
50

It will keep running forever, if "/foo" changes again, it will print more output. This is a powerful way to think about programming, and it's especially well suited to building user interfaces and transforming data streams.

Dataflow but Otherwise Normal

Besides being a dataflow language Graphix tries hard to be a normal functional language that would feel familiar to anyone who knows Haskell, OCaml, F# or a similar ML derived language. Some of it's features are,

  • lexically scoped
  • expression oriented
  • strongly statically typed
  • type inference
  • structural type discipline
  • parametric polymorphism
  • algebraic data types
  • pattern matching
  • first class functions, and closures
  • late binding

Installing Graphix

To install the Graphix shell from source you need to install a rust build environment. See here for instructions on how to do that for your platform. Once you have that set up, you can just run

cargo install graphix-shell

That should build the graphix command and install it in your ~/.cargo/bin directory. Windows and Mac OS should work out of the box as long as you have the prerequisites for rust installed.

Linux Prerequisites

Debian/Ubuntu

You need to install

  • clang
  • libkrb5-dev

On Fedora

You need to install

  • clang-devel
  • krb5-devel

Netidx

Graphix uses netidx to import and export data streams. So it is recommended that you set up at least a machine local installation of netidx when installing Graphix. Otherwise separate Graphix processes won't be able to communicate with each other and it will be difficult to get any data into Graphix.

See here for details

If you don't want to set up netidx Graphix will still work, it just won't be able to use the net module to send anything outside the current process.

Getting Started

Welcome to Graphix! This tutorial will get you up and running in about 10-15 minutes. You'll learn how to use the interactive REPL, write your first expressions, and create a simple reactive program.

By the end of this guide, you'll understand the basics of Graphix's reactive dataflow model and be ready to explore the language in depth.

Prerequisites

Make sure you've already installed Graphix. You can verify your installation by running:

graphix --version

Starting the REPL

The Graphix shell provides an interactive Read-Eval-Print Loop (REPL) where you can experiment with the language. Start it by running graphix with no arguments:

graphix

You'll see a prompt that looks like this:

Welcome to the graphix shell
Press ctrl-c to cancel, ctrl-d to exit, and tab for help
〉

The REPL evaluates expressions and shows you both the type and the value. The output format is:

-: Type
value

Let's try it!

Your First Expressions

Arithmetic

Type some simple arithmetic at the prompt:

〉2 + 2
-: i64
4

The -: i64 line tells you the result is a 64-bit integer, and 4 is the value. You may be wondering why you don't get the 〉 prompt after running this expression. This is because, being a dataflow language, expressions are pipelines that can output more than one value, they will run until you stop them by hitting ctrl-c. Do this now to get the prompt back.

Since ctrl-c is used to stop the currently running pipeline, if you want to exit the REPL press ctrl-d.

Try a more complex expression:

〉10 * 5 + 3
-: i64
53
〉2.5 * 4.0
-: f64
10.0

Notice that integer arithmetic produces i64 (integer) results, while floating-point arithmetic produces f64 (float) results.

Strings

Strings are written in double quotes:

〉"Hello, Graphix!"
-: string
"Hello, Graphix!"

String Interpolation

Graphix supports string interpolation using square brackets. Any expression inside [...] in a string will be evaluated and inserted:

〉"The answer is [2 + 2]"
-: string
"The answer is 4"
〉"2 + 2 = [2 + 2], and 10 * 5 = [10 * 5]"
-: string
"2 + 2 = 4, and 10 * 5 = 50"

This is incredibly useful for building dynamic strings!

Variables with Let Binds

Use let to create named bindings:

〉let x = 42
〉x
-: i64
42
〉let name = "World"
〉"Hello, [name]!"
-: string
"Hello, World!"

You can reuse the same name to create a new binding (this is called shadowing):

〉let x = 10
〉let x = x + 5
〉x
-: i64
15

The second let x creates a new binding that references the previous value of x.

Functions

Functions in Graphix are first-class values. Create them with the lambda syntax |args| body:

〉let double = |x| x * 2
〉double(21)
-: i64
42

You can add type annotations if you want to be explicit:

〉let add = |x: i64, y: i64| x + y
〉add(10, 32)
-: i64
42

Functions can capture variables from their surrounding scope:

〉let multiplier = 3
〉let times_three = |x| x * multiplier
〉times_three(14)
-: i64
42

Creating Your First File

Now let's write a real Graphix program! Create a file called hello.gx with this content:

use tui;
use tui::text;

let count = 0;
let timer = time::timer(duration:1.s, true);
count <- timer ~ (count + 1);

text(&"Count: [count]")

This program demonstrates Graphix's reactive nature:

  • We start with count = 0
  • time::timer(duration:1.s, true) creates a timer that fires every second
  • The ~ operator samples the right side when the left side updates
  • count <- ... schedules an update to count for the next cycle
  • Every second, count increments and the text automatically updates
  • The last expression creates a text widget displaying the count

Running Your File

Run your program with:

graphix hello.gx

You'll see a terminal UI that displays the count increasing every second! The screen updates automatically because Graphix tracks dependencies and propagates changes through the dataflow graph.

To stop the program, press Ctrl+C.

A Simpler Example

If you want to see the reactive behavior without the TUI, try this simpler version (counter.gx):

let count = 0;
let timer = time::timer(duration:1.s, true);
count <- timer ~ (count + 1);
"Count: [count]"

Run it with graphix counter.gx and you'll see the count printed to the console every second.

Understanding the Output

In Graphix programs:

  • The last value is what determines what the shell displays
    • If it's a Tui type (like our text example), then it is rendered as a TUI
    • Otherwise, the value is printed to the console every time it updates
  • Use print or println for explicit output during execution
  • Programs run forever unless they explicitly exit - they're reactive graphs that respond to events

The Dataflow Model

The key insight: when timer updates, it triggers an update to count (via the <- connect operator), which triggers an update to the text widget. The entire chain reacts automatically. You describe what should happen, not when or how to update things.

This is very different from traditional imperative programming where you'd need loops and manual state management. In Graphix, you build a graph of dependencies and the runtime handles updates for you.

Try It Yourself

Experiment with these ideas:

  1. Modify the counter to count down instead of up
  2. Make it count by 2s or 10s instead of 1s
  3. Change the timer interval to 0.5 seconds
  4. Display multiple values that update independently
  5. Try arithmetic on the count (show doubled value, squared value, etc.)

Next Steps

Now that you've experienced Graphix's reactive nature, you're ready to dive deeper:

The best way to learn Graphix is to experiment! Keep the REPL open as you read through the documentation and try out the examples. Every code snippet in this book is designed to be runnable.

Core Language

This chapter introduces the core constructs that make Graphix work. If you're coming from imperative languages like Python, Java, or C, some of these concepts will feel familiar—but the reactive twist makes them work differently than you might expect.

Types: Strong but Flexible

Graphix has a powerful static type system, but you'll rarely write type annotations. The compiler infers types for you using structural typing—types are compared by their shape, not their name. A struct {x: i64, y: i64} is the same type whether you call it Point or Vector or don't name it at all.

The Fundamental Types section covers the built-in numeric types (i64, f64, u32, etc.), booleans, strings, durations, and more. You'll learn how arithmetic works across different numeric types, how string interpolation works with [...] brackets, and why division by zero doesn't crash your program.

Reading Type Signatures teaches you how to read the type annotations you'll see throughout the documentation. Function types like fn(Array<'a>, fn('a) -> 'b) -> Array<'b> tell you exactly what a function expects and returns, including what errors it might throw.

Binding Values and Building Blocks

In Graphix, you create bindings with let. Unlike variables in other languages, these bindings can update over time—they're more like pipes that different values flow through.

Let Binds explains how to create bindings, how shadowing works, and why every binding in Graphix is potentially reactive. When you write let x = 42, you're not just storing a value—you're creating a node in the dataflow graph.

Blocks shows how to group expressions with {...} to create scopes, hide intermediate bindings, and build up complex expressions. Blocks are expressions too—they evaluate to their last value.

Use lets you bring module names into scope so you can write map(arr, f) instead of array::map(arr, f). Simple, but essential as your programs grow.

Connect: The Heart of Reactivity

This is where Graphix becomes special. The <- operator (connect) is the only way to create cycles in your dataflow graph, and it's the key to writing reactive programs and loops.

Connect is the most important section in this chapter. When you write:

let count = 0;
count <- timer ~ (count + 1)

You're telling Graphix: "When timer updates, increment count for the next cycle." Connect schedules updates for the future, which is how you build everything from simple counters to complex state machines. It's also the only looping construct in Graphix—there's no for or while, just connect and select working together.

Select: Pattern Matching with Power

The select expression is Graphix's answer to switch, match, and if/else—but much more powerful. It lets you match on types, destructure complex data, and ensure at compile time that you've handled every case.

Select shows you how to:

  • Match on union types and ensure you handle all variants
  • Destructure arrays with slice patterns like [head, tail..]
  • Match structs with patterns like {x, y}
  • Guard patterns with conditions like n if n > 10
  • Build loops by combining select with connect

The compiler checks your select expressions exhaustively—if you forget a case, it won't compile. If you match a case that can never happen, it won't compile. This eliminates entire classes of bugs.

Error Handling: Exceptions, Done Right

Graphix has first-class error handling with try/catch and the ? operator. Errors are just values with the special Error<'a> type, and they're tracked through the type system.

Error Handling explains:

  • How ? throws errors to the nearest try/catch in dynamic scope
  • How error types are checked at compile time—you can't forget to handle an error type
  • How the $ operator silently swallows errors (use with caution!)
  • How error chains track the full context of where errors originated

Every error that gets raised with ? is wrapped in an ErrChain that captures the file, line, column, and full stack of previous errors. No more mystery exceptions.

How It All Fits Together

These constructs combine to create the Graphix programming model:

  1. You create bindings that hold values
  2. You build expressions that compute new values from old ones
  3. You use select to handle different cases and make decisions
  4. You use connect to update bindings when events occur
  5. The type system ensures everything is safe and correct
  6. Errors propagate cleanly through try/catch

The result is a language where you describe relationships between values, and the runtime automatically maintains those relationships as things change. A temperature value updates, and the Fahrenheit conversion updates automatically. A timer fires, and your counter increments. A network subscription delivers new data, and your UI reflects it instantly.

Fundamental Types

Graphix has a few fundamental data types, the Graphix shell is a good way to explore them by trying out small Graphix expressions. You can run the Graphix shell by invoking graphix with no arguments.

Numbers

i32, u32, i64, u64, f32, f64, and decimal are the fundamental numeric types in Graphix. Literals are written with their type prefixed, except for i64 and f64 which are written bare. for example, u32:3 is a u32 literal value.

decimal is an exact decimal representation for performing financial calculations without rounding or floating point approximation errors.

The basic arithmetic operations are implemented on all the number types with all the other number types.

OperationOperator
Add+
Subtract-
Multiply*
Divide/
Mod%

The compiler will let you do arithmetic on different types of numbers directly without casting, however the return type of the operation will be the set of all the types in the operation, representing that either type could be returned. If you try to pass this result to a function that wants a specific numeric type, it will fail at compile time.

〉1. + 1
-: [i64, f64]
2
〉let f = |x: i64| x * 10
〉f(1. + 1)
error: in expr

Caused by:
    0: at: line: 1, column: 3, in: (f64:1. + i64:1)
    1: at: line: 1, column: 3, in: (f64:1. + i64:1)
    2: type mismatch '_1046: i64 does not contain [i64, f64]

Division by zero is raised as an error to the nearest error handler (more on that later) and will be printed to stderr by the shell if it is never handled. Overflow and underflow are handled by wrapping,

〉0 / 0

thread 'tokio-runtime-worker' panicked at <rust runtime>:
attempt to divide by zero
-: i64
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
unhandled error: error:"in expr at line: 1, column: 1 attempt to divide by zero"
〉u32:0 - u32:1
-: u32
4294967295

The thread panic message is an artifact of how the overflow error is handled at runtime, it is safe to continue using the shell and runtime if such an error occurs. However the particular arith operation that caused the error will not update, which may cause problems depending on what your program is doing with it.

v32, z32, v64, z64

These number types are the same as the normal types except when they are sent over the wire via netidx (or written to a file) they use variable width encoding instead of normal encoding. The number of bytes used varies for 64 bit numbers to between 1 and 10. Small numbers (below 127) are encoded in 1 byte, larger number use more bytes. The type correspondence is,

CompactNormal
v32u32
z32i32
v64u64
z64i64

Number Sets

There are a few sets of number types that classify numbers into various kinds. Number being the most broad, it contains all the number types. Int contains only integers, Real contains only reals (decimal plus the two float types), SInt contains signed integers, UInt contains unsigned integers.

Bool

Boolean literals are written as true and false, and the name of the boolean type is bool.

Boolean expressions using &&, ||, and ! are supported. These operators only operate on bool. They can be grouped with parenthesis. For example,

〉true && false
-: bool
false
〉true || false
-: bool
true
〉!true
-: bool
false
〉!1
error: in expr

Caused by:
    0: at: line: 1, column: 2, in: i64:1
    1: type mismatch bool does not contain i64

Duration

A time duration. The type name is duration, and the literals are written as, duration:1.0s, duration:1.0ms, duration:1.0us, duration:1.0ns. Durations can be added, and can be multiplied and divided by scalars.

〉duration:1.0s + duration:1.0s
-: duration
2.s
〉duration:1.0s * 50
-: duration
50.s
〉duration:1.0s / 50
-: duration
0.02s

DateTime

A date and time in the UTC time zone. The type name is datetime and literals are written in RFC3339 format inside quotes. For example, datetime:"2020-01-01T00:00:00Z". You can add and subtract duration from datetime.

〉datetime:"2020-01-01T00:00:00Z" + duration:30.s
-: datetime
2020-01-01 00:00:30 UTC

You can enter datetime literals in local time and they will be converted to UTC. For example,

〉datetime:"2020-01-01T00:00:00-04:00"
-: datetime
2020-01-01 04:00:00 UTC

String

Strings in Graphix are UTF8 encoded text. The type name is string and the literal is written in quotes "this is a string". C style escape sequences are supported, "this is \" a string with a quote and a \n". Non printable characters such as newline will be escaped by default when strings are printed to the console, you can use print to print the raw string including control characters.

String Interpolation

String literals can contain expressions that will be evaluated and joined to the string, such expressions are surrounded by unescaped [] in the string. For example,

〉let row = 1
〉let column = 999
〉"/foo/bar/[row]/[column]"
-: string
"/foo/bar/1/999"

Values in an interpolation need not be strings, they will be cast to a string when they are used. You can write a literal [ or ] in a string by escaping it.

〉"this is a string with a \[ and a \] but it isn't an interpolation"
-: string
"this is a string with a [ and a ] but it isn't an interpolation"

Any

The Any type is a type that unifies with any other type, it corresponds to the underlying variant type that represents all values in Graphix (and netidx). It is not used very often, as it provides very few guarantees, however it has it's place. For example, Any is the type returned by net::subscribe, indicating that any valid netidx value can come from the network. Usually the first thing you do with an Any type is call cast to turn it into the type you expect (or an error), or use a select expression to match it's type (more on select later).

Null

Null is nothing, just like in many other languages. Unlike most other languages null is a type not a catch all. If the type of a value does not include null then it can't be null. The set ['a, null] (alias Option<'a>) is commonly used to represent things that will sometimes return null.

Array

Arrays are immutable, contiguous, and homogeneous. They are parameterized, Array<string> indicates an array of strings. Arrays are zero indexed a[0] is the first element. Array elements can be any type, including other arrays at arbitrary levels of nesting. There is a special array type that acts like Array<Any>, that represents the fundamental array type in the underlying value representation. Array literals are written like [x, y, z]. There are many functions in the array module of the standard library for working with arrays.

Array Slicing and Indexing

Graphix supports array subslicing, the syntax will be familiar to Rust programmers.

  • a[2..] a slice from index 2 to the end of the array
  • a[..4] a slice from the beginning of the array to index 3
  • a[1..3] a slice from index 1 to index 2
  • a[-1] the last element in the array
  • a[-2] the second to last element in the array

..= is not supported however, the second part of the slice will always be the exclusive bound. Literal numbers can always be replaced with a Graphix expression, e.g. a[i..j] is perfectly valid.

Mutability and Implementation

Arrays are not mutable, like all other Graphix values. All operations that "change" an array, actually create a new array leaving the old one unchanged. This is even true of the connect operator, which we will talk more about later.

There are a couple of important notes to understand about the implementation of Arrays.

  • Arrays are memory pooled, in almost all cases (besides really huge arrays) creating an array does not actually allocate any memory, it just reuses a previously used array that has since become unused. This makes using arrays a lot more efficient than you might expect.

  • Arrays are contiguous in memory. This means they are generally very memory efficient, each element is 2 machine words, and O(1) to access. However there are a few cases where this causes a problem, such as building up an array by appending one element at a time. This is sadly an O(n^2) operation on arrays. You may wish to use another data structure, such as a map (which would be O(n log(n)) if you need to build a large data structure this way.

  • Array slices are zero copy, and also pooled. They simply create a light weight view into the array. This means algorithms that progressively deconstruct an array by slicing are O(N) not O(N^2) and the constants are very fast.

Tuples

Tuples are written (x, y), they can be of arbitrary length, and each element may have a different type. Tuples may be indexed using numeric field indexes. Consider

let x = (1, 2, 3, 4);
x.0

Will print 1.

Map

Maps in Graphix are key-value data structures with O(log(N)) lookup, insert, and remove operations. Maps are parameterized by their key and value type, for example Map<string, i64> indicates a map with string keys and integer values. There are many functions for working with maps in the map standard library module

Map Literals

Maps can be constructed using the {key => value} syntax:

〉{"a" => 1, "b" => 2, "c" => 3}
-: Map<'_1893: string, '_1895: i64>
{"a" => 1, "b" => 2, "c" => 3}

Keys and values can be any Graphix type, for example here is a map where the key is a Map<string, i64>.

{{"foo" => 42} => "foo", {"bar" => 42} => "bar"}
-: Map<'_1919: Map<'_1915: string, '_1917: i64>, '_1921: string>
{{"bar" => 42} => "bar", {"foo" => 42} => "foo"}

Map Indexing

Maps can be indexed using the map{key} syntax to retrieve values:

〉let m = {"a" => 1, "b" => 2, "c" => 3}
〉m{"b"}
-: ['_1907: i64, Error<`MapKeyError(string)>]
2

If a key is not present in the map, indexing returns a MapKeyError:

〉m{"missing"}
-: ['_1907: i64, Error<`MapKeyError(string)>]
error:["MapKeyError", "map key \"missing\" not found"]

Mutability and Implementation

Like all Graphix values, maps are immutable. All operations that "change" a map actually create a new map, leaving the original unchanged. Maps are memory pooled and very efficient - creating new maps typically reuses existing memory rather than allocating new memory.

Maps maintain their key-value pairs in a balanced tree structure, ensuring O(log(N)) performance for all operations regardless of map size.

Error

Error is the built in error type. It carries a type parameter indicating the type of error, for example Error<MapKeyError(string)>is an error that carries a ``MapKeyError `` variant. You can access the inner error value using e.0 e.g.,

〉let e = error(`MapKeyError("no such key"))
〉e.0
-: `MapKeyError(string)
`MapKeyError("no such key")

More information about dealing with errors is available in the section on error handling.

Reading Type Signatures

Throughout this book and in the standard library documentation, you'll encounter function type signatures. This guide will help you understand what they mean. Don't try to memorize everything here - just use this as a reference when you encounter unfamiliar notation.

Basic Function Signatures

The simplest function signature looks like this:

val double: fn(i64) -> i64

This breaks down into:

  • fn(...) - this is a function
  • i64 - takes one parameter of type i64 (64-bit integer)
  • -> i64 - returns a value of type i64

Another example:

val concat: fn(string, string) -> string

Takes two strings, returns a string.

Type Parameters (Generics)

Type parameters (like generics in other languages) are written with a single quote followed by an identifier: 'a, 'b, 'e, etc.

Simple Type Parameters

val identity: fn('a) -> 'a

This means: "takes a value of any type 'a and returns a value of the same type 'a". The identity function could work with integers, strings, or any other type.

val first: fn(Array<'a>) -> 'a

This means: "takes an array of any type 'a and returns a single element of type 'a". If you pass an Array<string>, you get back a string. If you pass an Array<i64>, you get back an i64.

Multiple Type Parameters

val map: fn(Array<'a>, fn('a) -> 'b) -> Array<'b>

This function takes:

  • An array of type 'a
  • A function that transforms 'a into 'b
  • Returns an array of type 'b

The types 'a and 'b can be the same or different.

Optional Labeled Arguments

Arguments prefixed with ?# are optional and labeled:

val text: fn(?#style: Style, string) -> Widget

This function can be called in two ways:

text("Hello")                           // style uses default value
text(#style: my_style, "Hello")        // style is specified

Order Flexibility

Labeled arguments can be provided in any order, but must come before positional arguments:

val widget: fn(?#width: i64, ?#height: i64, string) -> Widget

// All of these are valid:
widget("text")
widget(#width: 100, "text")
widget(#height: 50, #width: 100, "text")
widget(#height: 50, "text")

Required Labeled Arguments

Arguments with # but no ? are required but labeled:

val input_handler: fn(
    #handle: fn(Event) -> Response,
    &Widget
) -> Widget

You must provide #handle, but it doesn't have to be in the first position. However, it must come before the unlabeled &Widget argument:

input_handler(#handle: my_handler, &my_widget)

Variadic Arguments

The @args notation means a function accepts any number of arguments:

val sum: fn(@args: i64) -> i64

You can call this with any number of integers:

sum(1, 2, 3)
sum(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

Variadic with Required Arguments

Sometimes a function requires at least one argument:

val max: fn('a, @args: 'a) -> 'a

The first 'a is required, then any number of additional arguments of the same type.

Reference Types

An ampersand & before a type means "reference to" rather than the value itself:

val text: fn(&string) -> Widget

This takes a reference to a string, not the string value directly. References are important for:

  1. Efficiency - avoid copying large data structures
  2. Reactivity - updating a referenced value triggers updates without rebuilding entire structures

Create a reference with & and dereference (get the value) with *:

let s = "Hello";
let r = &s;      // r is a reference to s
let v = *r;      // v is the value "Hello"

In function signatures, &T in a parameter position means the function expects a reference. In widget examples, you'll often see:

block(#title: &line("My Title"), &my_widget)

The &line(...) creates a reference to the line, and &my_widget is a reference to the widget.

For a deeper dive, see References.

Error Types (throws)

When a function can throw errors, the signature includes throws:

val divide: fn(i64, i64) -> i64 throws `DivideByZero

This function returns i64 if successful, but might throw a DivideByZero error.

Multiple Error Types

A function can throw multiple error types:

val parse_and_divide: fn(string, string) -> i64 throws [`ParseError, `DivideByZero]

Generic Error Types

Often error types are parameterized:

val filter: fn('a, fn('a) -> bool throws 'e) -> 'a throws 'e

This means: the filter function itself doesn't throw errors, but if the function you pass to it throws errors of type 'e, then filter will also throw those same errors.

Result Type

The Result type is a convenient way to represent success or error:

type Result<'r, 'e> = ['r, Error<'e>]

So a function signature like:

val parse: fn(string) -> Result<i64, `ParseError>

Returns either an i64 (success) or an Error<ParseError>` (failure).

See Error Handling for complete details on working with errors.

Set Types

Square brackets [...] denote a set type - the value can be any one of the types in the set:

val process: fn([i64, string]) -> string

This function accepts either an i64 or a string, and returns a string.

Optional Types (Nullable)

The pattern [T, null] means "T or nothing":

val find: fn(Array<string>, string) -> [string, null]

Returns a string if found, null if not found. This is aliased as Option<T>:

type Option<'a> = ['a, null]
val find: fn(Array<string>, string) -> Option<string>

Nested Sets

Types can nest arbitrarily:

val sum: fn(@args: [Number, Array<[Number, Array<Number>]>]) -> Number

This accepts numbers, arrays of numbers, or even arrays of (numbers or arrays of numbers). The flexibility allows you to call:

sum(1, 2, 3)
sum([1, 2], [3, 4])
sum(1, [2, 3], 4)

Function Constraints

Type variables in functions can have constraints:

let sum<'a: Number>(@args: 'a) -> 'a

This is subtly different from the sum examples earlier. This says, sum is a function that takes any number of arguments of the same type and returns the same type, and that type must be some kind of number.

Auto Parameters

The compiler often infers type variables (and constrains them) by itself during the type inference process.

if we compile a function with no type constraints, such as:

let f = |x, y| x + y

It's type will be something like:

val f: fn<
  '_2073: Error<ErrChain<`ArithError(string)>>, 
  '_2069: Number, 
  '_2067: Number, 
  '_2071: Number
>('_2067, '_2069) -> '_2071 throws '_2073

The compiler has inferred a bunch of properties here,

  • both arguments must be of type Number, that's what the constraints on '_2067: Number and _2069: Number mean.
  • both arguments need not be the same type, hence they are different type variables
  • the return type will also be a number, hence '_2071: Number, but it may not be the same type of number as either of the arguments.
  • the function may throw an arithmetic exception, hence the constraint on '_2073

In the shell this rather imposing type signature is made even more complex by the shell also telling you what type variables are currently bound to, or unbound if they aren't bound. So in the shell this pops out as,

〉f
-: fn<'_2073: unbound: Error<ErrChain<`ArithError(string)>>, '_2069: unbound: Number, '_2067: unbound: Number, '_2071: unbound: Number>('_2067: unbound, '_2069: unbound) -> '_2071: unbound throws '_2073: unbound

The constraint '_2069: unbound: Number is read as. _2069 is not currently bound to a type but is constrained to type Number. This is all useful information, even though it's intimidating at first it's worth putting in the work to learn to decipher it.

Putting It All Together

Let's decode some complex real-world signatures:

Example 1: TUI Table Widget

val table: fn(
    ?#header: &Row,
    ?#selected: &i64,
    ?#row_highlight_style: &Style,
    ?#highlight_symbol: &string,
    &Array<&Row>
) -> Widget

Breaking it down:

  • ?#header: &Row - optional labeled argument, reference to a Row
  • ?#selected: &i64 - optional labeled argument, reference to selected index
  • ?#row_highlight_style: &Style - optional labeled argument, reference to a Style
  • ?#highlight_symbol: &string - optional labeled argument, reference to symbol string
  • &Array<&Row> - required unlabeled argument, reference to array of row references
  • -> Widget - returns a Widget

All parameters are references because the table needs to react to changes without rebuilding.

Example 2: Filter Function

val filter: fn('a, fn('a) -> bool throws 'e) -> 'a throws 'e

Breaking it down:

  • 'a - a value of any type
  • fn('a) -> bool throws 'e - a predicate function that:
    • Takes the same type 'a
    • Returns bool
    • Might throw errors of type 'e
  • -> 'a - returns the same type as input
  • throws 'e - propagates any errors from the predicate

Example 3: Queue Function

val queue: fn(#clock: Any, 'a) -> 'a

Breaking it down:

  • #clock: Any - required labeled argument of any type, just used as an event source
  • 'a - a value of any type
  • -> 'a - returns values of the same type

Call it like: queue(#clock: my_timer, my_value)

Example 4: Array Map

val map: fn(Array<'a>, fn('a) -> 'b throws 'e) -> Array<'b> throws 'e

Breaking it down:

  • Array<'a> - array of any type 'a
  • fn('a) -> 'b throws 'e - transformation function that:
    • Takes type 'a (array element type)
    • Returns type 'b (result element type)
    • Might throw errors of type 'e
  • -> Array<'b> - returns array of transformed type
  • throws 'e - propagates errors from the transform function

Quick Reference Table

NotationMeaningExample
'a, 'b, 'eType parameter (generic)fn('a) -> 'a
'_23, '_24, '_25Inferred type parameter (generic)fn('_23) -> '_23
?#paramOptional labeled argumentfn(?#x: i64 = 0)
#paramRequired labeled argumentfn(#x: i64)
@argsVariadic (any number of args)fn(@args: i64)
&TReference to type Tfn(&string)
throws 'eCan throw errors of type 'efn() -> i64 throws 'e
[T, U]T or U (set/union type)[i64, null]
->Returnsfn(i64) -> string
Array<T>Array of TArray<string>
Map<K, V>Map with keys K, values VMap<string, i64>
Error<'e>Error containing type 'eError<\ParseError>`
Result<'r, 'e>Success 'r or Error 'eResult<i64, \Err>`
Option<'a>Value 'a or nullOption<string>

Tips for Reading Signatures

  1. Start with the basics - identify parameters and return type
  2. Look for type parameters - they tell you about genericity
  3. Check for optional/labeled args - they indicate flexibility in calling
  4. Note reference types - important for reactivity
  5. Watch for throws - you'll need error handling
  6. Don't panic at complexity - break it down piece by piece

Remember: you don't need to memorize these patterns. As you use Graphix, you'll naturally become familiar with common signatures. This guide is here whenever you need a reminder!

See Also

Let Binds

Let bindings introduce names that are visible in their scope after they are defined.

let x = 2 + 2 + x; // compile error x isn't defined yet
let y = x + 1 // ok

The same name can be used again in the same scope, it will shadow the previous value.

let x = 1;
let x = x + 1; // ok uses the previous definition
x == 2 // true

You can annotate the binding with a type, which will then be enforced at compile time. Sometimes this is necessary in order to help type inference.

let x: Number = 1; // note x will be of type Number even though it's an i64
let y: string = x + 1; // compile time type error

You can use patterns in let binds as long as they will always match.

let (x, y) = (3, "hello"); // binds x to 3 and y to "hello"
x == 3; // true
y == "hello" // true

You can mix type annotations with pattern matches

let (x, y): (i64, string) = (3, "hello")

You can assign documentation to a let bind using a /// comment. Documentation will be displayed in the shell when the user tab completes and will be made available by the lsp server.

// this is a normal comment
let x = 1;
/// this is documentation for y
let y = 2;

Connect

Connect, written x <- expr is where things get interesting in Graphix. The sharp eyed may have noticed that up until now there was no way to introduce a cycle in the graph. Connect is the only graph operator in Graphix, it allows you to connect one part of the graph to another by name, causing the output of the right side to flow to the name on the left side. Consider,

let x = "off"
x <- time::timer(duration:1.0s, false) ~ "on"
print(x)

This program will first print off, and after 1 second it will print on. Note the ~ operator means, when the expression on the left updates return the current value of the expression on the right (called the sample operator). The graph we created looks like,

const("off") ────────────────> "x" ──────> print
                                 ▲
                                 │
                                 │
           time::timer ──> sample
                            ▲
                            │
                            │
                      const("on")

We can also build an infinite loop with connect. This won't crash the program, and it won't stop other parts of the program from being evaluated,

let x = 0;
x <- x + 1;
print(x)

This program will print all the i64s from 0 to MAX and then will wrap around. It will print numbers forever. You might notice, and you might wonder, why does it start from zero, shouldn't it start from 1? After all we increment x BEFORE the print right? Well, no, not actually, it will start at 0, for the same reason this infinite loop won't lock up the program or cause other expressions not to be evaluated. Graphix programs are evaluated in cycles, a batch of updates from the network, timers, and other IO is processed into a set of all events that happened "now", then the parts of the program that care about those particular events are evaluated, and then the main loop goes back to waiting for events.

What connect does is it schedules an update to x for the next cycle, the current cycle proceeds as normal to it's conclusion as if the connect didn't happen yet, because it didn't. In the above case the event loop would never wait, because there is always work to do adding 1 to x, however it will still check for other events every cycle.

When combined with other operations, specifically select, connect becomes a powerful general looping construct, and is the only way to write a loop in Graphix. A quick example,

let count = {
  let x = 0;
  select x {
    n if n < 10 => x <- x + 1,
    _ => never() // never() never updates
  };
  x
};
count

This program creates a bind count that will update with the values 0 to 10. If you put it in a file test.gx and execute it using graphix test.gx it will print 0 to 10 and then wait.

$ graphix test.gx
0
1
2
3
4
5
6
7
8
9
10

Is Connect Mutation?

Connect causes let bound names to update, so it's kind of mutation. Kind of. A better way to think about it is that every let bound value is a pipe with multiple producers and multiple consumers. Connect adds a new producer to the pipe. The values being produced are immutable, an array [1, 2, 3] will always and forever be [1, 2, 3], but a new array [1, 2, 3, 4] might be pushed into the same pipe [1, 2, 3] came from, and that might make it appear that the array changed. The difference is, if you captured the original [1, 2, 3] and put it somewhere, a new [1, 2, 3, 4] arriving on the pipe can't change the original array.

Blocks

A block is a group of code between { and } that has it's own scope, and evaluates to the last value in the block. Expressions in a block are ; separated, meaning every expression except the last one must end in a ; and it is illegal for a block to have just one expression (it will not parse).

You can use blocks to hide intermediate variables from outer scopes, and to group code together in a logical way.

let important_thing = {
  let x = 0;
  let y = x + 1;
  43 - y
};

x; // compile error, x isn't in scope
y; // compile error, y isn't in scope
important_thing

This program won't compile because you can't reference y and x from outside the block scope. Blocks are valid anywhere an expression is valid, and they are just expressions. They will become very important when we introduce lambda expressions.

Use

Use allows you to bring names in modules into your current scope so they can be used without prefixing.

net::subscribe(...); // call subscribe in the net module
use net;
subscribe(...) // same function

Use is valid anywhere expressions are valid

let list = {
  use array;
  map([1, 2, 3, 4, 5], |x| x * 2)
};
list

will print [2, 4, 6, 8, 10]

let list = {
  use array;
  map([1, 2, 3, 4, 5], |x| x * 2)
};
map(list, |x| x * 2)

will not compile, e.g.

$ graphix test.gx
Error: in file "test.gx"

Caused by:
    at line: 5, column: 1 map not defined

Use shadows earlier declarations in it's scope. Consider,

let map = |a, f| "hello you called map!";
let list = {
  use array;
  map([1, 2, 3, 4, 5], |x| x * 2)
};
(list, map(list, |x| x * 2))

prints

$ graphix test.gx
([2, 4, 6, 8, 10], "hello you called map!")

Select

Select lets us create a graph node with multiple possible output paths that will choose one path for each value based on a set of conditions. Kind of like,

                     ┌─────────────────────> if foo > 0 => ...
                     │
                     │
ref(foo) ──> select ─┼─────────────────────> if foo < 0 => ...
                     │
                     │
                     └─────────────────────> otherwise => ...

is written as

select foo {
  n if n > 0 => ...,
  n if n < 0 => ...,
  n => ...
}

select takes an expression as an argument and then evaluates one or more "arms". Each arm consists of an optional type predicate, a destructuring pattern, and an optional guard clause. If the type predicate matches, the pattern matches, and the guard evaluates to true then the arm is "selected". Only one arm may be selected at a time, the arms are evaluated in lexical order, and first arm to be selected is chosen as the one and only selected arm.

The code on the right side of the selected arm is the only code that is evaluated by select, all other code is "asleep", it will not be evaluated until it is selected (and if it has netidx subscriptions or published values they will be unsubscribed and unpublished until it is selected again).

Matching Types

Consider we want to select from a value of type [Array<i64>, i64, null],

let x: [Array<i64>, i64, null] = null;
x <- time::timer(duration:1.s, false) ~ [1, 2, 3, 4, 5];
x <- time::timer(duration:2.s, false) ~ 7;
select x {
  Array<i64> as a => array::fold(a, 0, |s, x| s + x),
  i64 as n => n,
  null as _ => 42
}

This program will print 42, 15, 7 and then wait. The compiler will check that you have handled all the possible cases. If we remove the null case from this select we will get a compile error.

$ graphix test.gx
Error: in file "test.gx"

Caused by:
    missing match cases type mismatch [i64, Array<i64>] does not contain [[i64, null], Array<i64>]

If you read this carefully you can see that the compiler is building up a set of types that we did match, and checking that it contains the argument type. This goes both ways, a match case that could never match is also an error.

let x: [Array<i64>, i64, null] = null;
x <- time::timer(duration:1.s, false) ~ [1, 2, 3, 4, 5];
x <- time::timer(duration:2.s, false) ~ 7;
select x {
  Array<i64> as a => array::fold(a, 0, |s, x| s + x),
  i64 as n => n,
  f64 as n => cast<i64>(n)?,
  null as _ => 42
}

Here we've added an f64 match case, but the argument type can never contain an f64 so we will get a compile error.

$ graphix test.gx
Error: in file "test.gx"

Caused by:
    pattern f64 will never match null, unused match cases

The diagnostic message gives you an insight into the compiler's thinking. What it is saying is that, by the time it's gotten to looking at the f64 pattern, the only type left in the argument that hasn't already been matched is null, and since f64 doesn't unify with null it is sure this pattern can never match.

Guarded patterns can always not match because of the guard, so they do not subtract from the argument type set. You are required to match without a guard at some point. No analysis is done to determine if your guard covers the entire range of a type.

let x: [Array<i64>, i64, null] = null;
x <- time::timer(duration:1.s, false) ~ [1, 2, 3, 4, 5];
x <- time::timer(duration:2.s, false) ~ 7;
select x {
  Array<i64> as a => array::fold(a, 0, |s, x| s + x),
  i64 as n if n > 10 => n,
  null as _ => 42
}

This will fail with a missing match case because the i64 pattern is guarded and no unguarded pattern exists that matches i64.

$ graphix test.gx
Error: in file "test.gx"

Caused by:
    missing match cases type mismatch [null, Array<i64>] does not contain [[i64, null], Array<i64>]

This is the same error you would get if you omitted the i64 match case entirely.

Matching Structure

The type predicate is optional in a pattern, and the more commonly used pattern is structural. Graphix supports several kinds of structural matching,

  • array slices
  • tuples
  • structs
  • variants
  • literals, ignore

NB: In most contexts you can match the entire value as well as parts of it's structure by adding a v@ pattern before the pattern. You will see this in many of the examples.

Slice Patterns

Suppose we want to classify arrays that have at least two elements vs arrays that don't, and we want to return a variant with a triple of the first two elements and the rest of the array or `Short with the whole array.

let a = [1, 2, 3, 4];
a <- [1];
a <- [5, 6];
select a {
  [x, y, tl..] => `Ok((x, y, tl)),
  a => `Short(a)
}

This program will print,

$ graphix test.gx
`Ok((1, 2, [3, 4]))
`Short([1])
`Ok((5, 6, []))

The following kinds of slice patterns are supported,

  • whole slice, with binds, or literals, e.g. [1, x, 2, y] matches a 4 element array and binds it's 2nd and 4th element to x and y respectively.

  • head pattern, like the above program, e.g. [(x, y), ..] matches the first pair in an array of pairs and ignores the rest of the array, binding the pair elements to x and y. You can also name the remainder, as we saw, e.g. [(x, y), tl..] does the same thing, but binds the rest of the array to tl

  • tail pattern, just like the head pattern, but for the end of the array. e.g. [hd.., {foo, bar}] matches the last element of an array of structs with fields foo and bar, binding hd to the array minus the last element, and foo to field foo and bar to field bar.

Structure patterns (all of the different types) can be nested to any depth.

Tuple Patterns

Tuple patterns allow you to match tuples. Compared to slice patterns they are fairly simple. You must specify every field of the tuple, you can choose to bind it, or ignore it with _. e.g.

("I", "am", "a", "happy", "tuple", w, _, "patterns")

Struct Patterns

Struct patterns, like tuple patterns, are pretty simple.

  • { x, y } if you like the field names then there is no need to change them
  • { x: x_coord, y: y_coord } but if you need to use a different name you can
  • { x, .. } you don't have to write every field

Consider

let a = {x: 54, y: 23};
a <- {x: 21, y: 88};
a <- {x: 5, y: 42};
a <- {x: 23, y: 32};
select a {
  {x, y: _} if (x < 10) || (x > 50) => `VWall,
  {y, x: _} if (y < 10) || (y > 40)  => `HWall,
  {x, y} => `Ok(x, y)
}

does some 2d bounds checking, and will output

$ graphix test.gx
`VWall
`HWall
`VWall
`Ok(23, 32)

You might be tempted to replace y: _ with .. as it would be shorter. Unfortunately this will confuse the type checker, because the Graphix type system is structural saying {x, ..} without any other information could match ANY struct with a field called x. This is currently too much for the type checker to handle,

$ graphix test.gx
Error: in file "test.gx"

Caused by:
    pattern {x: '_1040} will never match {x: i64, y: i64}, unused match cases

The error is slightly confusing at first, until you understand that since we don't know the type of {x, ..} we don't think it will match the argument type, and therefore the match case is unused. This actually saves us a lot of trouble here, because the last match is exhaustive, if we didn't check for unused match cases this program would compile, but it wouldn't work. You can easily fix this by naming the type, and for larger structs it's often worth it if you only need a few fields.

type T = {x: i64, y: i64};
let a = {x: 54, y: 23};
a <- {x: 21, y: 88};
a <- {x: 5, y: 42};
a <- {x: 23, y: 32};
select a {
  T as {x, ..} if (x < 10) || (x > 50) => `VWall,
  T as {y, ..} if (y < 10) || (y > 40)  => `HWall,
  {x, y} => `Ok(x, y)
}

Here since we've included the type pattern T in our partial patterns the program compiles and runs.

$ graphix test.gx
`VWall
`HWall
`VWall
`Ok(23, 32)

Note that we never told the compiler that a is of type T. In fact T is just an alias for {x: i64, y: i64} which is the type of a. We could in fact write our patterns without the alias,

{x: i64, y: i64} as {x, ..} if (x < 10) || (x > 50) => `VWall

The type alias just makes the code less verbose without changing the semantics.

Variant Patterns

Variant patterns match variants. Consider,

let v: [`Bare, `Arg(i64), `MoreArg(string, i64)] = `Bare;
v <- `Arg(42);
v <- `MoreArg("hello world", 42);
select v {
  `Bare => "it's bare, no argument",
  `Arg(i) => "it has an arg [i]",
  x@ `MoreArg(s, n) => "it's big [x] with args \"[s]\" and [n]"
}

produces

$ graphix test.gx
"it's bare, no argument"
"it has an arg 42"
"it's big `MoreArg(\"hello world\", 42) with args \"hello world\" and 42"

Variant patterns enforce the same kinds of match case checking as all the other pattern types

let v: [`Bare, `Arg(i64), `MoreArg(string, i64)] = `Bare;
v <- `Arg(42);
v <- `MoreArg("hello world", 42);
select v {
  `Bare => "it's bare, no argument",
  `Arg(i) => "it has an arg [i]",
  x@ `MoreArg(s, n) => "it's big [x] with args \"[s]\" and [n]",
  `Wrong => "this won't compile"
}

yields

$ graphix test.gx
Error: in file "test.gx"

Caused by:
    pattern `Wrong will never match [`Arg(i64), `MoreArg(string, i64)], unused match cases

Literals, Ignore

You can match literals as well as bind variables, as you may have noticed, and the special pattern _ means match anything and don't bind it to a variable.

Missing Features

A significant missing feature from patterns vs other languages is support for multiple alternative patterns in one arm. I plan to add this at some point.

Select and Connect

Using select and connect together is one way to iterate in Graphix. Consider,

let a = [1, 2, 3, 4, 5];
let len = 0;
select a {
  [x, tl..] => {
    len <- len + 1;
    a <- tl
  },
  _ => len
}

produces

$ graphix test.gx
5

This is not normally how we would get the length of an array in Graphix, or even how we would do something with every element of an array (see array::map and array::fold), however it illustrates the power of select and connect together.

Error Handling

Errors in Graphix are represented by the Error<'a> type. A new instance of which can be created with the error function. e.g.

〉error(`Foo)
-: Error<'a: `Foo>
error:"Foo"

Try Catch and ?

While errors are normal values, and can be matched in select, they can also be thrown and handled like exceptions. The ? operator throws errors generated by the expression on it's left to the nearest try catch block in dynamic scope. for example,

〉let a = [1, 2, 3, 4]
〉try a[15]? catch(e) => println(e)
-: i64
error:[["cause", null], ["error", ["ArrayIndexError", "array index out of bounds"]], ["ori", [["parent", null], ["source", "Unspecified"], ["text", "try a[15]? catch(e) => println(e)"]]], ["pos", [["column", i32:5], ["line", i32:1]]]]

Catches the array index error and prints it's full context to stdout. Every error raised with ? is wrapped in an ErrChain struct, the full definition of which is,

type Pos = {
    line: i32,
    column: i32
};

type Source = [
    `File(string),
    `Netidx(string),
    `Internal(string),
    `Unspecified
];

type Ori = {
    parent: [Ori, null],
    source: Source,
    text: string
};

type ErrChain<'a> = {
    cause: [ErrChain<'a>, null],
    error: 'a,
    ori: Ori,
    pos: Pos
}

This gives the full context of where the error happened, and whether it was previously caught and reraised, giving the full history back to the first time it was ever raised.

The scope is dynamic, not lexical, mirroring exception systems that unwind the stack,

〉let div0 = try |x| x / 0 catch(e) => println(e ~ "never triggered")
〉try div0(0) catch(e) => println(e)
-: i64
error:[["cause", null], ["error", ["ArithError", "attempt to divide by zero"]], ["ori", [["parent", null], ["source", "Unspecified"], ["text", "let div0 = try |x| x / 0 catch(e) => println(e ~ \"never triggered\")"]]], ["pos", [["column", i32:20], ["line", i32:1]]]]

The catch surrounding the function call site, not the definition site, is the one triggered.

Try Catch Block Value

The try catch block always evaluates to the last value inside the try catch, never to the value of the catch block. An error being raised to try catch does not stop the execution of nodes in the try catch.

Checked Errors

Graphix function types are annotated by the type of error they might raise. In most cases this is automatic, but for some higher order functions it may be necessary to specify it explicitly. For example array map has type fn(Array<'a>, fn('a) -> 'b throws 'e) -> Array<'b> throws 'e indicating that while the map function itself does not throw any errors, it will throw any errors the function passed to it throws. This is all in the service of being able to statically check the type of thrown errors, for example,

let a = [0, 1, 2, 3];
try a[0]? + a[1]?
catch(e) => select (e.0).error {
    `ArithError(s) => println("arithmetic operation error [s]"),
    `ArrayIndexError(s) => println("array index error [s]")
}

There are two types of errors that can happen in this example, and the compiler knows that. If you were to omit one of them, then the example would not compile. Suppose we remove the pattern for ArrayIndexError, we would get,

Error: in file "test.gx"

Caused by:
    0: at: line: 3, column: 13, in: select (e.0).error {`ArithError(s) => ..
    1: missing match cases type mismatch `ArithError('_1897: string) does not contain '_1895: [`ArithError(string), `ArrayIndexError(string)]

You'll recognize that this is just the normal select exhaustiveness checking at work. Since errors are just normal types, the important point is the compiler knows the type of every error at compile time, everything else flows from there.

Unhandled Errors

By default when evaluating a file, the compiler will print a warning whenever an error raised by ? is not handled explicitly by a try catch block. Arithmetic errors such as overflow do not generate this warning by default. Using -W flags you can change the compilers behavior in this respect.

The $ Operator, aka Or Never

The $ operator goes in the same position as ?, and is best described as "or never". If the expression on it's left is a non error, then $ doesn't do anything, otherwise it returns nothing. This is a concise way of writing,

select might_fail(1, 2, 3) {
  error as _ => never(),
  v => v
}

can instead be written as,

might_fail(1, 2, 3)$

The $ operator should be used cautiously, as it silently ignores errors, however often it is useful.

Functions

Functions are first class values. They can be stored in variables, in data structures, and they can be passed around to other functions. Etc. They are defined with the syntax,

|arg0, arg1, ...| body

This is often combined with a let bind to make a named function.

let f = |x, y| x + y + 1

f is now bound to the lambda that adds it's two arguments and 1. You can also use structure patterns in function arguments as long as the pattern will always match.

let g =|(x, y), z| x + y + z

Type annotations can be used to constrain the argument types and the return type,

let g = |(x, y): (f64, f64), z: f64| -> f64 x + y + z

Functions are called with the following syntax.

f(1, 1)

Would return 3. If the function is stored in a data structure, then sometimes you need parenthesis to call it.

(s.f)(1, 1)

Would call the function f in the struct s.

Labeled and Optional Arguments

Functions can have labeled and also optional arguments. Labeled arguments need not be specified in order, and optional arguments don't need to be specified at all. When declaring a function you must specify the labeled and optional arguments before any non labeled arguments.

let f = |#lbl1, #lbl2, arg| ...

In this case lbl1 and 2 are not optional, but are labeled. You can call f with either labeled argument in either order. e.g. f(#lbl2, #lbl1, a).

let f = |#opt = null, a| ...

opt need not be specifed when f is called, if it isn't specified then it will be null. e.g. f(2) is a valid way to call f. You can also apply type constraints to labeled and optional arguments.

let f = |#opt: [i64, null] = null, a| ..

Specifies that opt can be either an i64 or null and by default it is null. The compiler implements subtyping for functions with optional arguments. For example if you write a function that takes a function with a labeled argument foo, you can pass any function that has a labeled argument foo, even if it also has other optional arguments. The non optional and non labeled arguments must match, of course. For example,

let f = |g: fn(#foo:i64, i64) -> i64, x: i64| g(#foo:x, x);
let g = |#foo:i64, #bar: i64 = 0, x: i64| foo + bar + x;
f(g, 42) // valid call

outputs

$ graphix test.gx
84

Lexical Closures

Functions can reference variables outside of their definition. These variables are captured by the function definition, and remain valid no matter where the closure is called. For example,

let f = {
  let v = cast<i64>(net::subscribe("/local/foo")$)$;
  |n| v + n
};
f(2)

f captures v and can use it even when it is called from a scope where v isn't visible. Closures allow functions to encapsulate data, just like an object in OOP.

Functions are First Class values

We can store a function in a structure, which can itself be stored in a data structure, a file, or even sent across the network to another instance of the same program. Here we build a struct that maintains a count, and a function to operate on the count, returning a new struct of the same type with a different count.

type T = { count: i64, f: fn(T) -> T };
let t = { count: 0, f: |t: T| {t with count: t.count + 1} };
(t.f)(t)

when run this example will output,

{count: 1, f: 158}

158 is the lambda id, it's the actual value that is stored to represent a function.

Late Binding

Functions are always late bound. Late binding means that the runtime actually figures out which function is going to be called at runtime, not compile time. At compile time we only know the type of the function we are going to call. This complicates the compiler significantly, but it is a powerful abstraction tool. For example we can create two structs of type T that each contain a different implementation of f, and we can use them interchangibly with any function that accepts a T. In this simple example we create one implementation of f that increments the count, and one that decrements it.

type T = { count: i64, f: fn(T) -> T };
let ts: Array<T> = [
  { count: 0, f: |t: T| {t with count: t.count + 1} },
  { count: 0, f: |t: T| {t with count: t.count - 1} }
];
let t = array::iter(ts);
(t.f)(t)

when run this example will output,

{count: 1, f: 158}
{count: -1, f: 159}

You can clearly see that f is bound to different functions by the runtime since the lambda ids (158 and 159) are different. While Graphix is not an object oriented language, you can use closures and late binding to achieve some of the same outcomes as OOP.

Polymorphism

While the compiler does a pretty good job of inferring the types of functions, sometimes you want to express a constraint that can't be inferred. Suppose we wanted to write a function that you can pass any type of number to, but it has to be the same type for both arguments, and the return type will be the same as the argument type. We can say that using type variables and constraints in our annotations.

〉let f = 'a: Number |x: 'a, y: 'a| -> 'a x + y
〉f
-: fn<'a: unbound: Number, '_2101: unbound: Error<ErrChain<`ArithError(string)>>>('a: unbound, 'a: unbound) -> 'a: unbound throws '_2101: unbound
160

In type annotations of lambda expressions,

  • The constraints come before the first |, separated by commas if there are multiple constrained type variables. e.g. 'a: Number
  • Each argument may optionally have a : Type after it, and this will set it's type, e.g. x: 'a
  • After the second | you can optionally include an -> Type which will set the return type of the function, e.g. -> 'a
  • After the return type, you can optionally specify a throws type, throws Type, which will set the type that is thrown by the function

When a function type is printed, the stuff between the fn<> are the type constraints, the syntax in this readout is a colon separated list of,

  • type variable name, for example '_2073
  • current value, or unbound if there is no current value
  • constraint type
fn<'a: unbound: Number, 
   '_2101: unbound: Error<ErrChain<`ArithError(string)>>>
('a: unbound, 'a: unbound) -> 'a: unbound throws '_2101: unbound

We can remove the (unbound) current values and it becomes easier to read,

fn<'a: Number,
   '_2101: Error<ErrChain<`ArithError(string)>>>
('a, 'a) -> 'a throws '_2101

We just have two variables now, 'a representing both argument types and the return type, and '_2101 representing the throws type. We can still call this f with any number type,

〉f(1.212, 2.0)
-: f64
3.2119999999999997

However notice that we get back the explicit type we passed in,

〉f(2, 2)
-: i64
4

In one case f64, in the other i64. We can't pass numbers of different types to the same call,

〉f(1, 1.2)
error: in expr

Caused by:
    0: at: line: 1, column: 6, in: f64:1.2
    1: type mismatch 'a: i64 does not contain f64

Here the compiler is saying that 'a is already initialized as i64 and i64 doesn't unify with f64.

Higher Order Functions

Since functions are first class, they can take other functions as arguments, and even return functions. These relationships can be often inferred automatically without issue, but sometimes annotations are required.

〉 let apply = |x: 'a, f: fn('a) -> 'b throws 'e| -> 'b throws 'e f(x)
〉 apply
-: fn<'e: unbound: _>('a: unbound, fn('a: unbound) -> 'b: unbound throws 'e: unbound) -> 'b: unbound throws 'e: unbound
163

Here we've specified a single argument apply, it takes an argument, and a function f, and calls f on the argument. Note that we've explicitly said that whatever type of error f throws, apply will throw as well. That was constrained by the compiler to _ meaning basically this could throw anything or also not throw at all, it just depends on f.

We can see a more practical example in the type of array::map (this implementation of which I will not repeat here), which is,

fn(Array<'a>, fn('a) -> 'b throws 'e) -> Array<'b> throws 'e

So map takes an array of 'a, and a function mapping 'a to 'b and possibly throwing 'e and returns an array of 'b possibly throwing 'e.

Implicit Polymorphism

All functions are polymorphic, even without annotations, argument and return types are inferred at each call site, and thus may differ from one site to another. Any internal constraints are calculated when the definition is compiled and are enforced at each call site. For example consider,

〉let f = |x, y| x + y
〉f
-: fn<'_2073: unbound: Error<ErrChain<`ArithError(string)>>, '_2069: unbound: Number, '_2067: unbound: Number, '_2071: unbound: Number>('_2067: unbound, '_2069: unbound) -> '_2071: unbound throws '_2073: unbound
159

The type is a bit of a mouthfull, lets format it a bit so it's easier to read.

fn<'_2073: unbound: Error<ErrChain<`ArithError(string)>>,
   '_2069: unbound: Number,
   '_2067: unbound: Number,
   '_2071: unbound: Number>
('_2067: unbound, '_2069: unbound) -> '_2071: unbound throws '_2073: unbound

Removing the unbounds,

fn<'_2073: Error<ErrChain<`ArithError(string)>>,
   '_2069: Number,
   '_2067: Number,
   '_2071: Number>
('_2067, '_2069) -> '_2071 throws '_2073

Here we can see that '_2067, '_2069, and '_2071 represent the two arguments and the return type of the function. They are all unbound, meaning that when the function is used they can have any type. They are also all constrained to Number, and this will be enforced when the function is called, it's arguments must be numbers and it will return a number. We learned this because internally the function uses +, which operates on numbers, this constraint was then propagated to the otherwise free variables representing the args and the return type.

So in plain English this says that the arguments to the function can by any type as long as it is a number, and the function will return some type which is a number. None of the three numbers need to be the same type of number.

Finally lets address throws '_2073. This states that the function may throw an error, and if it does it's type will be '_2073, which in this case is constrained to be

Error<ErrChain<`ArithError(string)>>.

This is what happens in the case of overflow, underflow, and other arithmetic errors. The throws clause of the type is used by the try catch(e) => ... expression to compute the type of e, which is just the union of all the throws types within the try catch.

We can indeed call f with different number types, and it works just fine,

〉f(1.0, 1)
-: Number
2

The type we get back really depends on the values we pass. For example,

〉f(1.1212, 1)
-: Number
2.1212

Wherever we use f the compiler will force us to handle every possible case in the Number type

Recursion

Functions can be recursive, however there is currently no tail call optimization, so you can easily exhaust available stack space. With that warning aside, lets write a recursive function to add up pairs of numbers in an array,

let rec add_pairs = 'a: Number |a: Array<'a>| -> Array<'a> select a {
  [e0, e1, tl..] => array::push_front(add_pairs(tl), e0 + e1),
  a => a
}

running this we see,

〉add_pairs([1, 2, 3, 4, 5])
-: Array<'a: i64>
[3, 7, 5]

Detailed Semantics

Considering the underlying execution model functions might be better described as "polymorphic graph templates", in that they allow you to specify a part of the graph once, and then use it multiple times with different types each time. Most of the time this difference in semantics doesn't matter. Most of the time. Consider,

let f = |x, y| x + y + y;
let n = cast<i64>(net::subscribe("/hev/stats/power")?)?;
f(n, 1)

What happens here? Does f get "called" every time n updates? Does it only work for the first n? Does it explode? Lets transform it like the compiler would in order to understand it better,

let f = |x, y| x + y + y;
let n = cast<i64>(net::subscribe("/hev/stats/power")?)?;
n + 1 + 1

The "arguments" to the function call were plugged into the holes in the graph template and then the whole template is copied to the call site, and from then on the graph runs as normal.

So when n updates, the call site will return n + 2, since 1 never updates we don't have to worry about it, however this same flow applies when multiple arguments could update. In this case we're just having a philosophical discussion about how call sites are implemented, however it DOES actually matter sometimes.

Where Function Semantics Matter

Lets revisit an earlier example where we used select and connect to find the length of an array. Suppose we want to generalize that into a function,

let len = |a: Array<'a>| {
  let sum = 0;
  select a {
    [x, tl..] => {
      sum <- sum + 1;
      a <- tl
    },
    _ => sum
  }
}

Here we have a function that takes an array with any element type and returns it's length. Brilliant, lets call it,

let a = [1, 2, 3, 4, 5];
len(a)

and when we run this we get,

$ graphix test.gx
5

That's the right answer. Are we done? Noooooooo. No we are not done. Lets see what happens if we do,

let a = [1, 2, 3, 4, 5];
a <- [1, 2, 3];
a <- [1, 2];
len(a)

this results in,

$ graphix test.gx
4

What!? That's not even wrong. That's just nonsense, what happened? The key to understanding this problem is that there is just one call site, which means we instantiated this little reusable bit of graph one time, just one time. That means there is just one sum, one a, basically just one graph. When we use connect to iterate we are using graph traversal cycles to do a new element of the array every cycle until we are done. It will take 5 cycles for the first array to be done, and that's the problem, because we update a with a whole new array in cycle 1 and again in cycle 2. That's why we get 4, it's determanistic, we will get 4 every time.

  • the first cycle we add 1 to sum and set the inner a to tl (it's not the same variable as the outer a, which is why the chaos isn't even greater). But the outer a also gets set to [1, 2, 3] and that overwrites the inner set because it happens after it (because that's just the way the runtime works).
  • the second cycle we add 1 to sum and set the inner a to [2, 3] and the outer a to [1, 2]
  • the third cycle we add 1 to sum and set the inner a to [2]
  • the 4th cycle we add 1 to sum and set a to []
  • the 5th cycle we update our return value with sum, which is now 4

We can only fix this be understanding that we're programming a graph. I tried to make Graphix as much like a normal language as possible, but this is where we depart from that possibility. The general idea is, we need to queue updates to the outer a until we're done processing the current one. For that we have a builtin called queue, here is the correct implementation

let len = |a: Array<'a>| {
  let clock = once(null);
  let q = queue(#clock, a);
  let sum = 0;
  select q {
    [x, tl..] => {
      sum <- sum + 1;
      q <- tl
    },
    _ => {
      clock <- null;
      sum <- 0;
      once(sum)
    }
  }
}

Every time clock updates queue will output something it has queued, or if it has nothing queued it will store that the next thing that arrives can go out immediatly. So the first a will immediatly pass through the queue, but anything after that will be held. Then the normal select loop will run, except it will look at q instead of a now, so that a can update without disturbing it. When we get to the terminating case, we update for next cycle clock with null and sum with 0 and we return once(sum). We return once(sum) instead of just sum because removing something from the queue takes one cycle, so it will be two cycles before we start on the next array, and in the mean time the existing array will still be empty, meaning the second select arm will still be selected, and sum is updating to 0 which we do not want to return. If we run this with the same set of examples we will get the correct answer,

$ graphix cycle_iter.gx
5
3
2

This comes up other places as well, for example whenever we have to deal with something that does IO, like calling an RPC, subscribing to values in netidx, etc.

User Defined Types

You can define your own data types in Graphix. This is useful for many tasks, such as enforcing interface invariants, and modeling data. As a data modeling language Graphix supports both structures, so called conjunctive types, where you are modeling data that always appears together, and variants, or so called disjunctive types, where a type can be one of many possible types drawn from a set. This contrasts with other languages, for example Python, which only support conjunctive types.

Structural Typing

In most languages types are dealt with by name, meaning that two structs with exactly the same fields are still different types if they have a different name. The obvious implication of this is that all types need to be given a name, and thus declared. Graphix works differently. Types in Graphix are structural, meaning that types that are structurally the same are the same type. In fact types in Graphix don't formally have names, there can be aliases for a large type to cut down on verbosity, but an alias is always resolved to the structural type when type checking. Because of this you don't need to declare types before using them.

Set Based Type System

The Graphix type system is based on set operations. For example, a function could declare that it can take either an i32 or an i64 as an argument by defining the set, [i32, i64] and annotating it's argument with this type.

let f = |a: [i32, i64]| ...

When this function is called, the type checker will check that the type of argument a is a subset of [i32, i64], and will produce a type error if it is not. Pretty much every operation the type checker performs in Graphix is one of, or a combination of, simple set operations contains, union, difference, etc.

The combination of structural typing, set based type operations, and aggressive type inference is meant to make Graphix feel like an untyped scripting language most of the time, but still catch a lot of mistakes at compile time, and make it possible to enforce interface contracts.

Structs

Structs allow you to define a type that groups any number of fields together as one data object. The fields are accessible by name anywhere in the program. For example,

{ foo: string, bar: i64 }

defines a struct with two fields, foo and bar. foo has type string and bar has type i64. We can assign a struct of this type to a variable, and pass it around just like any other data object. For example,

let s = { foo: "I am foo", bar: 42 }
println("the struct s is [s]")

will print

the struct s is {bar: 42, foo: "I am foo"}

Field References

Struct fields can be referenced with the .field notation. That is,

〉s.foo
-: string
"I am foo"

A more complex expression that results in a struct (such as a function call), must be placed in parenthesis before the .field. For example,

〉let f = || s
〉(f()).foo
-: string
"I am foo"

Mutability and Functional Update

Structs are not mutable, like everything else in Graphix. However There is a quick way create a new struct from an existing struct with only some fields changed. This is called functional struct update syntax. For example,

{ s with bar: 21 }

Will create a new struct with all the same fields as s except bar which will be set to 21. e.g.

〉{ s with bar: 21 }
-: {bar: i64, foo: string}
{bar: 21, foo: "I am foo"}

Notice that the type printed is the full type of the struct, this is because of structural typing.

Implementation

Structs are implemented as a sorted array of pairs, the field name being the first element of the pair, and the data value being the second. The array is sorted by the field name, and because of this it is not necessary to do any matching when the field is accessed at run time, the index of the field reference is pre computed at compile time, so field references are always O(1). The reason why the fields are stored at all is so they can be used on the network and in files without losing information. Because structs are array backed, they are also memory pooled, and so making a new struct does not usually allocate any memory, but instead reuses objects from the pool.

The cast operator can cast an unsorted array of pairs where the first element is a string to a struct type. For example,

〉cast<{foo: string, bar: i64}>([["foo", "I am foo"], ["bar", 42]])$
-: {bar: i64, foo: string}
{bar: 42, foo: "I am foo"}

Structs along with cast can be used to communicate complex values over the network as long as the two sides agree on what the type is supposed to be.

Variants

Variants allow you to define a case that belongs to a set of possible cases a value is allowed to be. For example we might categorize foods,

[`Vegetable, `Fruit, `Meat, `Grain, `Other(string)]

Here we've defined a set of variants that together cover all the cases we want to model. We can write a function that will only accept a member of this set,

let f = |food: [`Vegetable, `Fruit, `Meat, `Grain, `Other(string)]| ...

and the type checker will ensure that it is an error caught at compile time to pass any other type of value to this function. The most interesting variant in this set is probably

`Other(string)

Because it carries data with it. Variant cases can carry an zero or more values with them (types separated by commas, e.g. `Other(string, i64)). We can use pattern matching to extract these values at run time. Lets write the body of our food consuming function,

let f = |food: [`Vegetable, `Fruit, `Meat, `Grain, `Other(string)]| select food {
  `Vegetable => "it's a vegetable",
  `Fruit => "it's a fruit",
  `Meat => "it's meat",
  `Grain => "it's grain",
  `Other(s) => "it's a [s]"
};
f(`Other("foo"))

If we copy the above into a file and run it we will get,

$ graphix test.gx
"it's a foo"

In this example the type checker will ensure that,

  • every item in the set is matched by a non guarded arm of the select (see the section on select)
  • no extra items that can't exist in the set are matched
  • you can't pass anything to f that isn't in the set

Singleton variant cases like `Other(string) are actually a perfectly valid type in Graphix, although they are much more useful in sets. Once we start naming types (in a later section), they will become even more useful.

Tuples

Tuples are like structs where the field names are numbers, or like Arrays where every element can be a different type and the length is known at compile time. For example,

(string, i64, f64)

Is an example of a three tuple.

Field Accessors

You can access the fields of a tuple by their field number, e.g. .0, .1, .2, etc.

〉let t = (1, 2, 3)
〉t.0 == 1
-: bool
true

Tuple fields may also be bound in a pattern match in a let bind, a select arm, or a function argument. For example,

〉let (f0, f1, f2) = t
〉f0
-: i64
1

Named Types

You can name types to avoid having to type them more than once. Named types are just aliases for the full structure of the type they reference. The fully written out type is the same as the alias and visa versa. Lets go back to our foods example from the section on variants.

type FoodKind = [
  `Vegetable,
  `Fruit,
  `Meat,
  `Grain,
  `Other(string)
];

let v: FoodKind = `Vegetable;
let f = |food: FoodKind| ...

Aliases are very useful for more complex types that are used many times. Selective annotations can also help the type checker make sense of complex program structures.

In the next section you'll see that we can do a lot more with them.

Parametric Polymorphism

We can define type variables as part of the definition of named types and then use them in the type definition in order to create type aliases with type parameters. For example, suppose in our foods example we wanted to specify that `Other could carry a value other than a string,

type FoodKind<'a> = [
  `Vegetable,
  `Fruit,
  `Meat,
  `Grain,
  `Other('a)
];
let v: FoodKind<`Cookie> = `Other(`Cookie);
v

if we paste this program into a file and run it we get,

$ graphix test.gx
`Other(`Cookie)

We can even place constraints on the type that a type variable can take. For example,

type Point3<'a: Number> = {x: 'a, y: 'a, z: 'a};
let f = |p: Point3<'a>, x: 'a| {p with x: p.x + x};
f({x: 0., y: 1., z: 3.14}, 1.)

Running this program we get,

$ graphix test.gx
{x: 0, y: 1, z: 3.14}

However, consider,

type Point3<'a: Number> = {x: 'a, y: 'a, z: 'a};
let v: Point3<'a> = {x: "foo", y: "bar", z: "baz"};
v

Running this, we can see that 'a is indeed constrained, since we get

$ graphix test.gx
Error: in file "test.gx"

Caused by:
    0: at: line: 2, column: 21, in: { x: "foo", y: "bar", z: "baz" }
    1: type mismatch Point3<'a: [Int, Real]> does not contain {x: string, y: string, z: string}

Indicating that we can't construct a Point3 with the type parameter of string, because the constraint is violated.

Recursive Types

Type aliases can be used to define recursive types, and this is a very powerful modeling tool for repeating structure. If you want to see an advanced example look no further than the Tui type in graphix-shell. Tui's are a set of mutually recursive types that define the tree structure of a UI. For a less overwhelming example consider a classic,

type List<'a> = [
  `Cons('a, List<'a>),
  `Nil
]

This defines a singly linked list as a set of two variant cases. Either the list is empty (nil), or it is a cons cell with a 'a and a list, which itself could be either a cons cell or nil. If you've never heard the term "cons" and "nil" they come from lisp, the original functional programming language from the late 1950s. Anyway, lets define some functions to work on our new list type,

type List<'a> = [
  `Cons('a, List<'a>),
  `Nil
];

/// cons a new item on the head of the list
let cons = |l: List<'a>, v: 'a| -> List<'a> `Cons(v, l);

/// compute the length of the list
let len = |l: List<'a>| {
  let rec len_int = |l: List<'a>, n: i64| select l {
    `Cons(_, tl) => len_int(tl, n + 1),
    `Nil => n
  };
  len_int(l, 0)
};

/// map f over the list
let rec map = |l: List<'a>, f: fn('a) -> 'b| -> List<'b> select l {
  `Cons(v, tl) => `Cons(f(v), map(tl, f)),
  `Nil => `Nil
};

/// fold f over the list
let rec fold = |l: List<'a>, init: 'b, f: fn('b, 'a) -> 'b| -> 'b select l {
  `Cons(v, tl) => fold(tl, f(init, v), f),
  `Nil => init
}

You can probably see where functional programming gets it's (partly deserved) reputation for being elegant and simple. Lets try them out,

let l = cons(cons(cons(cons(`Nil, 1), 2), 3), 4);
l

running this we get,

$ graphix test.gx
`Cons(4, `Cons(3, `Cons(2, `Cons(1, `Nil))))

Lets try something more complex,

map(l, |x| x * x)

results in

$ graphix test.gx
`Cons(16, `Cons(9, `Cons(4, `Cons(1, `Nil))))

as expected. Finally lets sum the list with fold,

fold(l, 0, |acc, v| acc + v)

and as expected we get,

$ graphix test.gx
10

So with recursive types and recursive functions you can do some really powerful things. When you add these capabilities to the data flow nature of Graphix, it only multiplies the power even further.

References

A reference value is not the thing itself, but a reference to it, just like a pointer in C. This is kind of an odd thing to have in a very high level language like Graphix, but there are good reasons for it. Before we get into those lets see what one looks like.

〉let v = &1
〉v
-: &i64
727

The & in front of the 1 creates a reference. You can create a reference to any value. Note that the type isn't i64 anymore but &i64 indicating that v is a reference to an i64. Just like a function when printed the reference id is printed, not the value it refers to. We get the value that this reference 727 refers to with the deref operator *.

〉*v
-: i64
1

But Why

Now that we've got the basic semantics out of the way, what is this good for? Suppose we have a large struct, with many fields, or even a struct of structs of structs with a lot of data. And suppose every time that struct updates we do a bunch of work. This is exactly how UIs are built by the way, they are deeply nested tree of structs. Under the normal semantics of Graphix, if any field anywhere in our large tree of structs were to update, then we'd rebuild the entire object (or at least a substantial part of it), and any function that depended on it would have no way of knowing what changed, and thus would have to do whatever huge amount of work it is supposed to do all over again. Consider a constrained GUI type with just labels and boxes,

type Gui = [
  `Label(string),
  `Box(Array<Gui>)
]

So we can build labels in boxes, and we can nest the boxes, laying out the labels however we like (use your imagination). We have the same problem as the more abstract example above, if we were mapping this onto a stateful GUI library then every time a label text changed anywhere we'd have to destroy all the widgets we had created and rebuild the entire UI from scratch. We'd like to be able to just update the label text that changed, and we can, with a small change to the type.

type Gui = [
  `Label(&string),
  `Box(Array<Gui>)
]

Now, the string inside the label is a reference instead of the actual string. Since references are assigned an id at compile time, they never change, and so the layout of our GUI can never change just because a label text was updated. Whatever is actually building the GUI will only see an update to the root when the actual layout changes. To handle the labels it can just deref the string reference in each label, and when that updates it can update the text of the label, exactly what we wanted.

Connect Deref

Suppose we want to write a function that can update the value a passed in reference refers to, instead of the reference itself (which we can also do). We can do that with,

*r <- "new value"

Consider,

let f = |x: &i64| *x <- once(*x) + 1;
let v = 0;
f(&v);
println("[v]")

Running this program will output,

$ graphix test.gx
0
1

We were able to pass v into f by reference and it was able to update it, even though the original bind of v isn't even in a scope that f can see.

Modules

Graphix has a simple module system, with the minimal required functionality necessary to organize code into a project.

The module system should be considered a work in progress, more features may be added in the future. In particular there are a few unfinished parts,

  • no module renaming on use
  • no access control, everything in a module is currently public

These shortcomings may be fixed in a future release. Current features include,

  • module hierarchies
  • inline modules (defined in the same file)
  • modules defined in external files
  • modules defined in netidx
  • modules dynamically loadable at runtime

Inline Modules

In a single file, you can define modules inline. Expressions in modules are semi colon separated. expressions defined in a module may be referred to directly e.g. m::name or by bringing the module into scope with use m.

mod m {
  let hello = "hello world";
  let goodbye = "goodbye world"
};

"we say [m::hello] followed by [m::goodbye]"

running this we get,

$ graphix test.gx
"we say hello world followed by goodbye world"

External Modules

A simple external module may be defined in either a file or by a value in netidx. In the case of a file, the file name must end in .gx and the part before that is the name of the module. For example a file m.gx contains the expressions defining the module m. Expressions in the file do not need to be surrounded by a mod m { ... }, the name of the module is taken from the filename.

In netidx, a string value containing Graphix source is published, and the final name in the path is the name of the module, no .gx is required for netidx modules. For example we might publish /libs/graphix/strops, the name of the module would be strops, and the resolver expects it to be a string containing valid Graphix code, just like a file.

Here is a simple example with file modules,

$ ls
m.gx  test.gx

test.gx is the program that we will run, m.gx is a module it will load.

test.gx

mod m;

m::hello

m.gx

let hello = "hello world"

running this we get,

$ graphix test.gx
"hello world"

Module Load Path

The graphix shell reads the GRAPHIX_MODPATH environment variable at startup and appends it's contents to the built in list of module paths. The syntax is a comma separated list of paths. Paths that start with netidx: are netidx paths, otherwise file paths are expected. The comma separator can be escaped with \. For example,

GRAPHIX_MODPATH=netidx:/foo,/home/user/graphix-modules,/very/str\,ange/path

would add

  • netidx:/foo
  • /home/user/graphix-modules
  • /very/str,ange/path

to the Graphix module path

Default Module Path

By default the module resolve path has several entries,

  • the parent directory of the program file passed on the command line. e.g. if we are running /home/user/test.gx then Graphix will look for modules in /home/user

  • the Graphix init directory. This is a platform specific directory where you can put Graphix modules.

    • On Linux ~/.local/share/graphix
    • On Windows %APPDATA%\Roaming\graphix
    • On Mac OS ~/Library/Application Support/graphix

In REPL mode, which is when it's given no argument, the graphix command will try to load the module init. If no such module exists it will silently carry on. You can use this to load commonly used utilities in the repl automatically.

Modules in Netidx

We can publish the same code as the files example in netidx and use it in Graphix directly, but we have to run it in a slightly different way, first lets publish it,

$ printf \
  "/local/graphix/test|string|%s\n/local/graphix/m|string|%s" \
  "$(tr \n ' ' <test.gx)" "$(tr \n ' ' <m.gx)" \
  | netidx publisher

Graphix doesn't care about whitespaces like newline, so we can just translate them to spaces to avoid confusing the command line publisher. Lets see if we published successfully.

$ netidx subscriber /local/graphix/test
/local/graphix/test|string|"mod m;  m::hello"

Looks good, now lets run the code. In order to do this we need to add to the resolve path to tell the Graphix shell where it should look for modules. We also don't pass a .gx extension, so we are telling Graphix to look for a module named test in it's configured module paths and run that.

$ GRAPHIX_MODPATH=netidx:/local/graphix graphix test
"hello world"

Module Hierarchies

Module hierarchies can be created using directories, for example to create m::n you would create a directory m and in it a file called mod.gx and a file called n.gx

$ find .
.
./m
./m/mod.gx
./m/n.gx
./test.gx

test.gx is the root of the hierarchy

mod m;

m::n::hello

m/mod.gx is the root of module m

mod n

m/n.gx is the m::n module

let hello = "hello world"

if we run the program we get,

$ graphix test.gx
"hello world"

Module Hierarchies in Netidx

Module hierarchies in netidx work the same as in the file system except that mod.gx is never needed because in netidx a value can also be a container. So to replicate the above example we'd publish,

/lib/graphix/test <- the contents of test.gx
/lib/graphix/m    <- the contents of m/mod.gx
/lib/graphix/m/n  <- the contents of m/n.gx

Dynamic Modules

Graphix programs can dynamically load modules at runtime. The loaded code will be compiled, type checked, and the loader will return an error indicating any failure in that process. Because Graphix is a statically typed language we must know ahead of time what interface the dynamically loaded module will have. We do this by defining a module signature. We can also define what the dynamically loaded module is allowed to reference, in order to prevent it from just calling any function it likes (aka it's sandboxed). Lets dive right in with an example,

// the module source, which we will publish in netidx
let path = "/local/foo";
let source = "
    let add = |x| x + 1;
    let sub = |x| x - 1;
    let cfg = \[1, 2, 3, 4, 5\];
    let hidden = 42
";
net::publish(path, source)$;

// now load the module
let status = mod foo dynamic {
    sandbox whitelist [core];
    sig {
        val add: fn(i64) -> i64;
        val sub: fn(i64) -> i64;
        val cfg: Array<i64>
    };
    source cast<string>(net::subscribe(path)$)$
};
select status {
    error as e => never(dbg(e)),
    null as _ => foo::add(foo::cfg[0]$)
}

running this we get,

$ graphix test.gx
2

In the first part of this program we just publish a string containing the source code of the module we want to ultimately load. The second part is where it gets interesting, lets break it down.

mod foo dynamic declares a dynamically loaded module named foo. In the rest of our code we can refer (statically) to foo as if it was a normal module that we loaded at compile time. There are three sections required to define a dynamic module, they are required to be defined in order, sandbox, sig, and source,

  • a sandbox statement, of which there are three types
    • sandbox unrestricted; no sandboxing, the dynamic module can access anything in it's scope
    • sandbox whitelist [item0, item1, ...] the dynamic module may access ONLY the names explicitly listed. e.g. sandbox whitelist [core::array]; would allow the dynamic module to access only core::array and nothing else.
    • sandbox blacklist [item0, item1, ...] the dynamic module may access anything except the names listed. sandbox blacklist [super::secret::module]; everything except super secret module would be accessible
  • a sig statement is the type signature of the module. This is a special syntax for writing module type signatures. There are three possible statements,
    • a val statement defines a value and it's type, val add: fn(i64) -> i64 is an example of a val statement, it need not be a function it can be any type
    • a type statement defines a type in the loaded module, e.g. type T = { foo: string, bar: string } val statements that come after a type statement may use the defined type. The type statement is identical to the normal type statement in Graphix (so it can be polymorphic, recursive, etc).
    • a mod statement defines a sub module of the dynamically loaded module. A sub module must have a sig. mod m: sig { ... } defines a sub module.
  • a source statement defines where the source code for the dynamic module will come from. It's type must be a string.

The mod foo dynamic ... expression returns a value of type,

[null, Error<`DynamicLoadError(string)>]

The runtime will try to load the module every time the source updates. If it succeeds it will update with null, if it fails it will update with an error indicating what went wrong. Regardless of the outcome the previous loaded module, if any, will be deleted. If compilation succeeded the new module will be initialized, possibly causing values it exports to update.

Obviously the loaded module must match the type signature defined in the dynamic mod statement. However, the signature checking only cares that every item mentioned in the signature is present in the dynamic module and that the types match. If extra items are present in the dynamic module they will simply be ignored, and will be inaccessible to the loading program.

The Graphix Shell

The Graphix shell (graphix) is the primary way to interact with Graphix programs. It provides both an interactive REPL for experimentation and a runtime for executing Graphix scripts. This chapter explores the shell's behavior in depth, covering topics like output handling, module resolution, and project structure.

Running Modes

The shell operates in several distinct modes:

REPL Mode

When you run graphix with no arguments, it starts an interactive Read-Eval-Print Loop:

graphix

In REPL mode:

  • Input is read line by line from the user
  • Each line is compiled and executed immediately
  • Completion is available via the Tab key
  • The value and type of output expressions are
    • built into TUIs if they are of type Tui
    • printed to stdout if they are not
  • Ctrl+C cancels the currently running expression/tui
  • Ctrl+D exits the shell

REPL mode is designed for interactive exploration. It doesn't enable warnings by default to keep the experience lightweight.

Script Mode

When you pass a file path, directory path, or netidx url to graphix, it runs in script mode:

graphix ./myprogram.gx
graphix ./myapp # my app is a directory containing a main.gx
graphix netidx:/path/to/my/program

In script mode:

  • The entire program source is loaded, compiled, and executed
  • The value of the last expression is
    • built into a TUI if it is of type Tui
    • printed to stdout as it updates if it is not
  • Ctrl+C exits the program
  • Warnings are enabled by default (unused variables, unhandled errors)

Script mode is for running complete programs. The shell stays running to handle the reactive graph's ongoing updates.

Check Mode

Check mode compiles a program but doesn't execute it:

graphix --check ./myprogram

You can pass the same program sources to check mode as you can to script mode.

This is useful for:

  • Verifying syntax and types without running side effects
  • Integrating with editors and build tools
  • Quick validation during development

Understanding Output

One of the most important concepts to understand about the shell is its output behavior. Not all expressions produce output, and expressions that do produce output can update multiple times.

Output vs Non-Output Expressions

The shell only prints values from expressions that are considered "output expressions." The following expression types are not considered output and will not print anything:

  • Bindings: let x = 42 defines a variable but doesn't output
  • Lambdas: |x| x + 1 defines a function but doesn't output
  • Use statements: use stdlib::time imports a module but doesn't output
  • Connect operations: x <- y schedules updates but doesn't output
  • Module definitions: mod m { ... } defines a module but doesn't output
  • Type definitions: type Point = {x: f64, y: f64} defines a type but doesn't output

Everything else is considered an output expression:

  • Values: 42, "hello", true
  • Arithmetic: 2 + 2
  • Function calls: time::now()
  • Variable references: x
  • Struct/variant/tuple construction: {x: 10, y: 20}
  • Blocks with output expressions as their last value

This is why you can type let x = 42 in the REPL and not see any output - it's a binding, not an output expression.

Why Programs Keep Running

Graphix programs are reactive dataflow graphs. When you run an expression that produces output, that output can update over time as upstream values change. The shell keeps the program running to display these updates.

For example:

let count = 0;
let timer = time::timer(duration:1.s, true);
count <- timer ~ (count + 1);
count

The last line count is an output expression. Its value changes every second as the timer fires. The shell stays running, printing each new value.

To stop watching the output and return to the REPL prompt, press Ctrl+C. In script mode, Ctrl+C exits the entire program.

Non-Terminating Expressions

Most useful Graphix programs don't terminate naturally because they're reactive systems responding to events. The program runs until you explicitly stop it with Ctrl+C.

However, some expressions produce a single value and effectively "complete":

〉2 + 2
-: i64
4

Even though this printed its value immediately, the shell is still waiting for potential updates. Since 2 + 2 can never update, nothing more will happen, but you still need Ctrl+C to return to the prompt.

Script Output Behavior

When you run in script mode only the last top-level expression produces output. Consider this file:

let x = 10
let y = 20
x + y
print("Hello")
x * y

This file has multiple top-level expressions. The first two are bindings (no output). The third (x + y) is an output expression but not the last. The fourth calls print which has side effects but returns _ (bottom). The fifth and final expression (x * y) is the output expression that the shell will print.

When you run this file:

  • print("Hello") will print "Hello" as a side effect
  • The shell will print the value of x * y (200) as the program output

Special Output TUIs

When the type of the output expression is a Tui then instead of printing the expression to stdout the Graphix shell will switch to TUI mode and will render the output expression as a tui. For example,

〉let count = 0
〉count <- time::timer(1, true) ~ count + 1
〉tui::text::text(&"count is [count]")

won't print the expression returned by tui::text::text(&"count is [count]") to stdout, it will build a tui,

TUI Output

When you type Ctrl+C the shell will exit TUI mode and return to the normal shell mode. You can use this behavior to experiment with TUI widgets interactively.

Module Resolution

A crucial feature of the shell is its automatic module path configuration. Understanding how this works is essential for organizing larger projects.

Running a Local File

When you run a local file, the parent directory of that file is automatically added to the module search path:

graphix /home/user/myproject/src/main.gx

This automatically adds /home/user/myproject/src to the module path. Any .gx files in that directory can be loaded as modules.

For example, if you have:

/home/user/myproject/src/
  main.gx
  utils.gx
  math.gx

Then main.gx can use:

mod utils;
mod math;

utils::helper()

The shell will find utils.gx and math.gx because they're in the same directory.

Running a Local Directory

When you run a local directory, the shell looks for main.gx in that directory and executes it. The directory is also added to the module search path.

For example, with this structure:

/home/user/myproject/src/
  main.gx
  utils.gx
  math.gx

You can run:

graphix /home/user/myproject/src

This executes main.gx and adds /home/user/myproject/src to the module search path, so main.gx can load utils and math modules.

This is useful for organizing projects where you want both a runnable program (main.gx) and a library interface (mod.gx) for the same set of modules.

Running from Netidx

When you run a program from netidx, the netidx path is added to the module search path.

If you run:

graphix netidx:/my/graphix/modules/myprogram

The shell:

  1. subscribes to /my/graphix/modules/myprogram
  2. Loads and executes it
  3. Adds netidx:/my/graphix/modules/myprogram to the module search path

So if myprogram contains mod utils, the shell will look for netidx:/my/graphix/modules/myprogram/utils.

Module Search Path Priority

The complete module search path, in order of priority:

  1. File parent directory (if running a local file)
  2. Netidx path (if running from netidx)
  3. GRAPHIX_MODPATH entries (from the environment variable)
  4. Platform-specific init directory:
    • Linux: ~/.local/share/graphix
    • Windows: %APPDATA%\Roaming\graphix
    • macOS: ~/Library/Application Support/graphix

The shell searches these paths in order, returning the first match found.

The GRAPHIX_MODPATH Environment Variable

You can extend the module search path by setting GRAPHIX_MODPATH:

export GRAPHIX_MODPATH=netidx:/shared/modules,/home/user/graphix-lib
graphix myprogram.gx

The syntax is a comma-separated list of paths:

  • Paths starting with netidx: are netidx paths
  • Other paths are treated as filesystem paths
  • Escape literal commas in paths with \

Example:

GRAPHIX_MODPATH=netidx:/foo,/home/user/lib,/path/with\,comma

This adds:

  • netidx:/foo
  • /home/user/lib
  • /path/with,comma

Structuring Larger Projects

Understanding module resolution makes it straightforward to structure larger projects.

Single-Directory Projects

For small to medium projects, keep all .gx files in a single directory:

myproject/
  main.gx
  ui.gx
  logic.gx
  utils.gx

Run with:

graphix myproject/main.gx

The module resolution will automatically find the other .gx files in myproject/.

Hierarchical Projects

For larger projects, use directory hierarchies:

myproject/
  main.gx
  ui/
    mod.gx
    widgets.gx
    layout.gx
  logic/
    mod.gx
    handlers.gx
    state.gx

In this structure:

  • ui/mod.gx defines the ui module (loads submodules)
  • ui/widgets.gx defines the ui::widgets module
  • logic/mod.gx defines the logic module
  • logic/handlers.gx defines the logic::handlers module

From main.gx:

mod ui;
mod logic;

ui::widgets::button("Click me")

The shell will:

  1. Find ui/mod.gx for the ui module
  2. Find ui/widgets.gx when ui/mod.gx does mod widgets
  3. Similarly for the logic hierarchy

Shared Libraries

To share code across multiple projects, use the init directory or GRAPHIX_MODPATH:

Option 1: Init Directory

Place shared modules in your platform's init directory (e.g., ~/.local/share/graphix on Linux):

~/.local/share/graphix/
  common.gx
  mylib.gx

Any Graphix program can then use:

mod common;
mod mylib;

Option 2: GRAPHIX_MODPATH

Keep shared libraries elsewhere and point to them:

export GRAPHIX_MODPATH=/opt/graphix-libs
graphix myproject/main.gx

Option 3: Netidx

Publish shared modules to netidx for organization-wide sharing:

# Publish the library
netidx publisher /shared/graphix/mylib < mylib.gx

# Use it from any program
GRAPHIX_MODPATH=netidx:/shared/graphix graphix myprogram.gx

The Init Module

In REPL mode only, the shell automatically tries to load a module named init. If found, it's loaded before the REPL starts. If not found, the shell continues silently.

Create an init.gx file in your init directory to:

  • Define commonly used utilities
  • Set up default imports

Example ~/.local/share/graphix/init.gx:

// Commonly used stdlib modules
use time;
use str;
use array;

// Personal utilities
let debug = |x| { print("DEBUG: [x]"); x };
let clear = || print("\x1b[2J\x1b[H");

Now these are available immediately in any REPL session.

Command-Line Options

The graphix command supports several options for controlling its behavior.

Netidx Configuration

# Use a specific netidx config file
graphix --config /path/to/netidx.toml myprogram

# Specify the netidx authentication mechanism
graphix --auth krb5 netidx:/apps/myprogram

# Disable netidx entirely (internal-only mode)
graphix --no-netidx ./myapp

When netidx is disabled, networking functions work only within the same process.

Publisher Configuration

# Set the publisher bind address
graphix --bind 127.0.0.1:5000 ./myprogram.gx

# Set a timeout for slow subscribers
graphix --publish-timeout 30 myprogram

Module Resolution

# Set timeout for resolving netidx modules (seconds)
graphix --resolve-timeout 10 netidx:/apps/myprogram

# Skip loading the init module in REPL mode
graphix --no-init

Compiler Warnings

Control which warnings are enabled with the -W flag:

# Warn about unhandled error operators (?) - default in script mode
graphix -W unhandled ./myprogram

# Disable warning about unhandled errors
graphix -W no-unhandled myapp.gx

# Warn about unhandled arithmetic overflow
graphix -W unhandled-arith netidx:/apps/calculator

# Warn about unused variables - default in script mode
graphix -W unused ./myproject

# Disable unused variable warnings
graphix -W no-unused myprogram.gx

# Make all warnings into errors
graphix -W error ./myapp

Multiple warning flags can be combined:

graphix -W unused -W unhandled -W error myprogram

If you specify both a flag and its negation (e.g., unhandled and no-unhandled), the no- variant always wins.

Logging

Enable debug logging for troubleshooting:

RUST_LOG=debug graphix --log-dir /tmp/graphix-logs ./myprogram

Logs will be written to files in the specified directory.

Summary

The Graphix shell is designed around the reactive nature of Graphix programs:

  • Output expressions produce values that can update over time
  • Programs keep running to display ongoing updates
  • Ctrl+C stops the current expression (REPL) or exits (file mode)
  • Module resolution is automatic based on where you run from
  • Project structure can be flat or hierarchical
  • Shared code can live in the init directory, GRAPHIX_MODPATH, or netidx

Understanding these concepts will help you work efficiently with Graphix, whether you're experimenting in the REPL or building large applications.

The Standard Library

The Graphix standard library is split into several modules with different functions. The core module is always imported with an implicit use statement. The modules are documented in the sig format used by dynamic module signatures.

  • core fundamental functions and types
  • array functions for manipulating arrays
  • map functions for manipulating maps
  • str functions for manipulating strings
  • re regular expressions
  • rand random number generator
  • time timers and time functions
  • net netidx functions

The standard library is a work in progress.

Core

mod core: sig {
    type Sint = [ i32, z32, i64, z64 ];
    type Uint = [ u32, v32, u64, v64 ];
    type Int = [ Sint, Uint ];
    type Float = [ f32, f64 ];
    type Real = [ Float, decimal ];
    type Number = [ Int, Real ];
    type NotNull = [Number, string, error, array, datetime, duration];
    type Primitive = [NotNull, null];
    type PrimNoErr = [Number, string, array, datetime, duration, null];
    type Log = [`Trace, `Debug, `Info, `Warn, `Error, `Stdout, `Stderr];
    type Result<'r, 'e> = ['r, Error<'e>];
    type Option<'a> = ['a, null];

    type Pos = {
        line: i32,
        column: i32
    };

    type Source = [
        `File(string),
        `Netidx(string),
        `Internal(string),
        `Unspecified
    ];

    type Ori = {
        parent: [Ori, null],
        source: Source,
        text: string
    };

    type ErrChain<'a> = {
        cause: [ErrChain<'a>, null],
        error: 'a,
        ori: Ori,
        pos: Pos
    };

    /// return the first argument when all arguments are equal, otherwise return nothing
    val all: fn(@args: Any) -> Any;

    /// return true if all arguments are true, otherwise return false
    val and: fn(@args: bool) -> bool;

    /// return the number of times x has updated
    val count: fn(Any) -> i64;

    /// return the first argument divided by all subsequent arguments
    val divide: fn('a, @args:'a) -> 'a;

    /// return e only if e is an error
    val filter_err: fn(Result<'a, 'b>) -> Error<'b>;

    /// return v if f(v) is true, otherwise return nothing
    val filter: fn('a, fn('a) -> bool throws 'e) -> 'a throws 'e;

    /// return true if e is an error
    val is_err: fn(Any) -> bool;

    /// construct an error from the specified string
    val error: fn('a) -> Error<'a>;

    /// return the maximum value of any argument
    val max: fn('a, @args: 'a) -> 'a;

    /// return the mean of the passed in arguments
    val mean: fn([Number, Array<Number>], @args: [Number, Array<Number>]) -> Result<f64, `MeanError(string)>;

    /// return the minimum value of any argument
    val min: fn('a, @args:'a) -> 'a;

    /// return v only once, subsequent updates to v will be ignored
    /// and once will return nothing
    val once: fn('a) -> 'a;

    /// seq will update j - i times, starting at i and ending at j - 1
    val seq: fn(i64, i64) -> Result<i64, `SeqError(string)>;

    /// return true if any argument is true
    val or: fn(@args: bool) -> bool;

    /// return the product of all arguments
    val product: fn(@args: [Number, Array<[Number, Array<Number>]>]) -> Number;

    /// return the sum of all arguments
    val sum: fn(@args: [Number, Array<[Number, Array<Number>]>]) -> Number;

    /// when v updates return v if the new value is different from the previous value,
    /// otherwise return nothing.
    val uniq: fn('a) -> 'a;

    /// when v updates place it's value in an internal fifo queue. when clock updates
    /// return the oldest value from the fifo queue. If clock updates and the queue is
    /// empty, record the number of clock updates, and produce that number of
    /// values from the queue when they are available.
    val queue: fn(#clock:Any, 'a) -> 'a;

    /// hold the most recent value of v internally until clock updates. If v updates
    /// more than once before clock updates, older values of v will be discarded,
    /// only the most recent value will be retained. If clock updates when no v is held
    /// internally, record the number of times it updated, and pass that many v updates
    /// through immediately when they happen.
    val hold: fn(#clock:Any, 'a) -> 'a;

    /// ignore updates to any argument and never return anything
    val never: fn(@args: Any) -> _;

    /// when v updates, return it, but also print it along
    /// with the position of the expression to the specified sink
    val dbg: fn(?#dest:[`Stdout, `Stderr, Log], 'a) -> 'a;

    /// print a log message to stdout, stderr or the specified log level using the rust log
    /// crate. Unlike dbg, log does not also return the value.
    val log: fn(?#dest:Log, 'a) -> _;

    /// print a raw value to stdout, stderr or the specified log level using the rust log
    /// crate. Unlike dbg, log does not also return the value. Does not automatically insert
    /// a newline and does not add the source module/location.
    val print: fn(?#dest:Log, 'a) -> _;

    /// print a raw value to stdout, stderr or the specified log level using the rust log
    /// crate followed by a newline. Unlike dbg, log does not also return the value.
    val println: fn(?#dest:Log, 'a) -> _;

    /// Throttle v so it updates at most every #rate, where rate is a
    /// duration (default 0.5 seconds). Intermediate updates that push v
    /// over the #rate will be discarded. The most recent update will always
    /// be delivered. If the sequence, m0, m1, ..., mN, arrives simultaneously
    /// after a period of silence, first m0 will be delivered, then after the rate
    /// timer expires mN will be delivered, m1, ..., m(N-1) will be discarded.
    val throttle: fn(?#rate:duration, 'a) -> 'a;
}

Net

mod net: sig {
    type Table = { rows: Array<string>, columns: Array<(string, v64)> };
    type ArgSpec = { name: string, doc: string, default: Any };

    /// write the value to the specified path
    val write: fn(string, Any) -> Result<_, `WriteError(string)>;

    /// subscribe to the specified path
    val subscribe: fn(string) -> Result<Primitive, `SubscribeError(string)>;

    /// call the specified rpc
    val call: fn(string, Array<(string, Any)>) -> Result<Primitive, `RpcError(string)>;

    /// Publish an rpc. When the rpc is called f will be called with the arguments
    /// sent by the caller, and whatever f returns will be sent back to the caller.
    /// If f does not return, the caller will hang waiting for a reply.
    val rpc: fn(
        #path:string,
        #doc:string,
        #spec:Array<ArgSpec>,
        #f:fn(Array<(string, Any)>) -> Any throws 'e
    ) -> Result<_, `PublishRpcError(string)> throws 'e;

    /// list paths under the specified path. If #update is specified, then the list will
    /// be refreshed each time clock is triggered. If update is not specified, the list will
    /// be updated each second
    val list: fn(?#update:Any, string) -> Result<Array<string>, `ListError(string)>;

    /// list the table under the specified path. If #update is specified, then the table
    /// will be refreshed each time clock is triggered. If update is not specified, the table
    /// will be updated each second
    val list_table: fn(?#update:Any, string) -> Result<Table, `ListError(string)>;

    /// Publish the specified value at the specified path. Whenever the value updates,
    /// the new value will be sent to subscribers. If #on_write is specified, then if
    /// subscribers write to the value on_write will be called with the written value.
    /// on_write need not return anything.
    val publish: fn(?#on_write:fn(Any) -> _ throws 'e, string, Any) -> Result<_, `PublishError(string)> throws 'e;
}

Array

mod array: sig {
    type Direction = [
        `Ascending,
        `Descending
    ];

    /// filter returns a new array containing only elements where f returned true
    val filter: fn(Array<'a>, fn('a) -> bool throws 'e) -> Array<'a> throws 'e;

    /// filter_map returns a new array containing the outputs of f
    /// that were not null
    val filter_map: fn(Array<'a>, fn('a) -> Option<'b> throws 'e) -> Array<'b> throws 'e;

    /// return a new array where each element is the output of f applied to the
    /// corresponding element in a
    val map: fn(Array<'a>, fn('a) -> 'b throws 'e) -> Array<'b> throws 'e;

    /// return a new array where each element is the output of f applied to the
    /// corresponding element in a, except that if f returns an array then it's
    /// elements will be concatenated to the end of the output instead of nesting.
    val flat_map: fn(Array<'a>, fn('a) -> ['b, Array<'b>] throws 'e) -> Array<'b> throws 'e;

    /// return the result of f applied to the init and every element of a in
    /// sequence. f(f(f(init, a[0]), a[1]), ...)
    val fold: fn(Array<'a>, 'b, fn('b, 'a) -> 'b throws 'e) -> 'b throws 'e;

    /// each time v updates group places the value of v in an internal buffer
    /// and calls f with the length of the internal buffer and the value of v.
    /// If f returns true then group returns the internal buffer as an array
    /// otherwise group returns nothing.
    val group: fn('a, fn(i64, 'a) -> bool throws 'e) -> Array<'a> throws 'e;

    /// iter produces an update for every value in the array a. updates are produced
    /// in the order they appear in a.
    val iter: fn(Array<'a>) -> 'a;

    /// iterq produces an update for each value in a, but only when clock updates. If
    /// clock does not update but a does, then iterq will store each a in an internal
    /// fifo queue. If clock updates but a does not, iterq will record the number of
    /// times it was triggered, and will update immediately that many times when a
    /// updates.
    val iterq: fn(#clock:Any, Array<'a>) -> 'a;

    /// returns the length of a
    val len: fn(Array<'a>) -> i64;

    /// returns the concatenation of two or more arrays. O(N) where
    /// N is the size of the final array.
    val concat: fn(Array<'a>, @args: Array<'a>) -> Array<'a>;

    /// return an array with the args added to the end. O(N)
    /// where N is the size of the final array
    val push: fn(Array<'a>, @args: 'a) -> Array<'a>;

    /// return an array with the args added to the front. O(N)
    /// where N is the size of the final array
    val push_front: fn(Array<'a>, @args: 'a) -> Array<'a>;

    /// return an array no larger than #n with the args
    /// added to the back. If pushing the args would cause the
    /// array to become bigger than #n, remove values from the
    /// front. O(N) where N is the window size.
    val window: fn(#n:i64, Array<'a>, @args: 'a) -> Array<'a>;

    /// flatten takes an array with two levels of nesting and produces a flat array
    /// with all the nested elements concatenated together.
    val flatten: fn(Array<Array<'a>>) -> Array<'a>;

    /// applies f to every element in a and returns the first element for which f
    /// returns true, or null if no element returns true
    val find: fn(Array<'a>, fn('a) -> bool throws 'e) -> Option<'a> throws 'e;

    /// applies f to every element in a and returns the first non null output of f
    val find_map: fn(Array<'a>, fn('a) -> Option<'b> throws 'e) -> Option<'b> throws 'e;

    /// return a new copy of a sorted ascending (by default). If numeric is true then
    /// values will be cast to numbers before comparison, resulting in a numeric sort
    /// even if the values are strings.
    val sort: fn(?#dir:Direction, ?#numeric:bool, Array<'a>) -> Array<'a>;

    /// return an array of pairs where the first element is the index in
    /// the array and the second element is the value.
    val enumerate: fn(Array<'a>) -> Array<(i64, 'a)>;

    /// given two arrays, return a single array of pairs where the first
    /// element in the pair is from the first array and the second element in
    /// the pair is from the second array. The final array's length will be the
    /// minimum of the length of the input arrays
    val zip: fn(Array<'a>, Array<'b>) -> Array<('a, 'b)>;

    /// given an array of pairs, return two arrays with the first array
    /// containing all the elements from the first pair element and second
    /// array containing all the elements of the second pair element.
    val unzip: fn(Array<('a, 'b)>) -> (Array<'a>, Array<'b>);
}

Map

mod map: sig {
    /// return a new map where each element is the output of f applied to
    /// the corresponding key value pair in the current map
    val map: fn(Map<'a, 'b>, fn(('a, 'b)) -> ('c, 'd) throws 'e) -> Map<'c, 'd> throws 'e;

    /// return a new map containing only the key-value pairs where f applied to
    /// (key, value) returns true
    val filter: fn(Map<'a, 'b>, fn(('a, 'b)) -> bool throws 'e) -> Map<'a, 'b> throws 'e;

    /// filter_map returns a new map containing the outputs of f
    /// that were not null
    val filter_map: fn(Map<'a, 'b>, fn(('a, 'b)) -> Option<('c, 'd)> throws 'e) -> Map<'c, 'd> throws 'e;

    /// return the result of f applied to the init and every k, v pair of m in
    /// sequence. f(f(f(init, (k0, v0)), (k1, v1)), ...)
    val fold: fn(Map<'a, 'b>, 'c, fn('c, ('a, 'b)) -> 'c throws 'e) -> 'c throws 'e;

    /// return the length of the map
    val len: fn(Map<'a, 'b>) -> i64;

    /// get the value associated with the key k in the map m, or null if not present
    val get: fn(Map<'a, 'b>, 'a) -> Option<'b>;

    /// insert a new value into the map
    val insert: fn(Map<'a, 'b>, 'a, 'b) -> Map<'a, 'b>;

    /// remove the value associated with the specified key from the map
    val remove: fn(Map<'a, 'b>, 'a) -> Map<'a, 'b>;

    /// iter produces an update for every key-value pair in the map m.
    /// updates are produced in the order they appear in m.
    val iter: fn(Map<'a, 'b>) -> ('a, 'b);

    /// iterq produces an update for each value in m, but only when clock updates. If
    /// clock does not update but m does, then iterq will store each m in an internal
    /// fifo queue. If clock updates but m does not, iterq will record the number of
    /// times it was triggered, and will update immediately that many times when m
    /// updates.
    val iterq: fn(#clock:Any, Map<'a, 'b>) -> ('a, 'b);
}

Str

mod str: sig {
    type Escape = {
        escape: string,
        escape_char: string,
        tr: Array<(string, string)>
    };

    /// the default escaping config escapes \, /, \n, \r, \t, \0, and non printable
    /// characters to Unicode \u{HHHH} format
    val default_escape: Escape = {
        escape: "\\/\n\r\t\0",
        escape_char: "\\",
        tr: [("\n", "n"), ("\r", "r"), ("\t", "t"), ("\0", "0")]
    };

    /// return true if s starts with #pfx, otherwise return false
    val starts_with: fn(#pfx:string, string) -> bool;

    /// return true if s ends with #sfx otherwise return false
    val ends_with: fn(#sfx:string, string) -> bool;

    /// return true if s contains #part, otherwise return false
    val contains: fn(#part:string, string) -> bool;

    /// if s starts with #pfx then return s with #pfx stripped otherwise return null
    val strip_prefix: fn(#pfx:string, string) -> Option<string>;

    /// if s ends with #sfx then return s with #sfx stripped otherwise return null
    val strip_suffix: fn(#sfx:string, string) -> Option<string>;

    /// return s with leading and trailing whitespace removed
    val trim: fn(string) -> string;

    /// return s with leading whitespace removed
    val trim_start: fn(string) -> string;

    /// return s with trailing whitespace removed
    val trim_end: fn(string) -> string;

    /// replace all instances of #pat in s with #rep and return s
    val replace: fn(#pat:string, #rep:string, string) -> string;

    /// return the parent path of s, or null if s does not have a parent path
    val dirname: fn(string) -> Option<string>;

    /// return the leaf path of s, or null if s is not a path. e.g. /foo/bar -> bar
    val basename: fn(string) -> Option<string>;

    /// return a single string with the arguments concatenated and separated by #sep
    val join: fn(#sep:string, @args: [string, Array<string>]) -> string;

    /// concatenate the specified strings into a single string
    val concat: fn(@args: [string, Array<string>]) -> string;

    /// escape all the characters in #to_escape in s with the escape character #escape.
    /// The escape character must appear in #to_escape
    val escape: fn(?#esc:Escape, string) -> Result<string, `StringError(string)>;

    /// unescape all the characters in s escaped by the specified #escape character
    val unescape: fn(?#esc:Escape, string) -> Result<string, `StringError(string)>;

    /// split the string by the specified #pat and return an array of each part
    val split: fn(#pat:string, string) -> Array<string>;

    /// reverse split the string by the specified #pat and return an array of each part
    val rsplit: fn(#pat:string, string) -> Array<string>;

    /// split the string at most #n times by the specified #pat and return an array of
    /// each part
    val splitn: fn(#pat:string, #n:i64, string) -> Result<Array<string>, `StringSplitError(string)>;

    /// reverse split the string at most #n times by the specified #pat and return an array of
    /// each part
    val rsplitn: fn(#pat:string, #n:i64, string) -> Result<Array<string>, `StringSplitError(string)>;

    /// give an escape character #esc, and a #sep character, split the string s into an array
    /// of parts delimited by it's non escaped separator characters.
    val split_escaped: fn(#esc:string, #sep:string, string) -> Result<Array<string>, `SplitEscError(string)>;

    /// give an escape character #esc, and a #sep character, split the string s into an array
    /// of at most #n parts delimited by it's non escaped separator characters.
    val splitn_escaped: fn(#n:i64, #esc:string, #sep:string, string) -> Result<Array<string>, `SplitNEscError(string)>;

    /// split the string once from the beginning by #pat and return a
    /// tuple of strings, or return null if #pat was not found in the string
    val split_once: fn(#pat:string, string) -> Option<(string, string)>;

    /// split the string once from the end by #pat and return a tuple of strings
    /// or return null if #pat was not found in the string
    val rsplit_once: fn(#pat:string, string) -> Option<(string, string)>;

    /// change the string to lowercase
    val to_lower: fn(string) -> string;

    /// change the string to uppercase
    val to_upper: fn(string) -> string;

    /// C style sprintf, implements most C standard format args
    val sprintf: fn(string, @args: Any) -> string;

    /// return the length of the string in bytes
    val len: fn(string) -> i64;

    /// extract a substring of s starting at #start with length #len.
    /// both #start and #len are Unicode character indexes,
    /// not byte indexes. e.g. str::sub(#start:0, #len:2, "💖💖💖")
    /// will return "💖💖"
    val sub: fn(#start:i64, #len:i64, string) -> Result<string, `SubError(string)>;

    /// parse the specified string as a value. return the value on success or an
    /// error on failure. Note, if you feed the parser a well formed error then
    /// parse will also return an error
    val parse: fn(string) -> Result<PrimNoErr, `ParseError(string)>;
}

Re

mod re: sig {
    /// return true if the string is matched by #pat, otherwise return false.
    /// return an error if #pat is invalid.
    val is_match: fn(#pat:string, string) -> Result<bool, `ReError(string)>;

    /// return an array of instances of #pat in s. return an error if #pat is
    /// invalid.
    val find: fn(#pat:string, string) -> Result<Array<string>, `ReError(string)>;

    /// return an array of captures matched by #pat. The array will have an element for each
    /// capture, regardless of whether it matched or not. If it did not match the corresponding
    /// element will be null. Return an error if #pat is invalid.
    val captures: fn(#pat:string, string) -> Result<Array<Array<Option<string>>>, `ReError(string)>;

    /// return an array of strings split by #pat. return an error if #pat is invalid.
    val split: fn(#pat:string, string) -> Result<Array<string>, `ReError(string)>;

    /// split the string by #pat at most #limit times and return an array of the parts.
    /// return an error if #pat is invalid
    val splitn: fn(#pat:string, #limit:i64, string) -> Result<Array<string>, `ReError(string)>;
}

Time

mod time: sig {
    /// When v updates wait timeout and then return it. If v updates again
    /// before timeout expires, reset the timeout and continue waiting.
    val after_idle: fn([duration, Number], 'a) -> 'a;

    /// timer will wait timeout and then update with the current time.
    /// If repeat is true, it will do this forever. If repeat is a number n,
    /// it will do this n times and then stop. If repeat is false, it will do
    /// this once.
    val timer: fn([duration, Number], [bool, Number]) -> Result<datetime, `TimerError(string)>;

    /// return the current time each time trigger updates
    val now: fn(Any) -> datetime;
}

Rand

mod rand: sig {
    /// generate a random number between #start and #end (exclusive)
    /// every time #clock updates. If start and end are not specified,
    /// they default to 0.0 and 1.0
    val rand: fn<'a: [Int, Float]>(?#start:'a, ?#end:'a, #clock:Any) -> 'a;

    /// pick a random element from the array and return it. Update
    /// each time the array updates. If the array is empty return
    /// nothing.
    val pick: fn(Array<'a>) -> 'a;

    /// return a shuffled copy of a
    val shuffle: fn(Array<'a>) -> Array<'a>;
}

Building UIs With Graphix

Graphix excels at building user interfaces thanks to its reactive dataflow nature. Changes in data automatically propagate through the UI graph, updating only the components that need to change. This makes building complex, interactive applications surprisingly straightforward.

Why Graphix for UIs?

Traditional UI frameworks require you to manually manage state changes, update DOM elements, and coordinate between different parts of your application. Graphix eliminates this complexity by treating your entire application as a reactive graph where:

  • Data flows automatically: When underlying data changes, dependent UI components update automatically
  • State is declarative: You describe what the UI should look like, not how to update it
  • Composition is natural: Complex UIs are built by composing simple, reusable components
  • Performance is built-in: Only components that depend on changed data will re-render

Currently Targeting TUIs

The first UI target for Graphix is TUIs. Surprisingly complex and useful UIs can be built in the standard terminal, and it is the absolute lowest common denominator that will always be present even on a bandwidth constrained remote system. Graphix uses the excellent ratatui library as a basis to build upon.

Future UI Targets

While Graphix currently implements support for building TUIs, the reactive architecture makes it well-suited for other UI paradigms:

  • Desktop Applications: Native desktop applications. Support for this is planned next.
  • Web UIs: The dataflow model maps naturally to modern web frameworks
  • Mobile UIs: Touch-based interfaces with gesture handling

The core concepts of reactive data flow, component composition, and declarative styling will apply across all UI targets.

Getting Started

The Graphix shell will automatically build a UI if the last value in your module has type tui::Widget (or in the future gui::Widget). You can try out the examples in this book by pasting them in a file, or even typing (the short ones) into the interactive REPL. Each TUI component has detailed documentation in the following sections, including complete API references and practical examples.

You can also study and run the examples in graphix-shell/examples/. Start with simple components like text.gx and block.gx, then work your way up to more complex examples like browser.gx and table-advanced.gx.

Terminal User Interfaces (TUIs)

Graphix includes a comprehensive TUI library built on top of the popular Rust ratatui crate. This allows you to build rich, interactive terminal applications with:

Core Components

The TUI library provides all the essential building blocks:

  • Layout: Flexible container system with horizontal/vertical arrangement and various sizing constraints
  • Block: Wrapper component that adds borders, titles, and styling to other components
  • Text: Rich text rendering with styling, colors, and formatting
  • Paragraph: Multi-line text with scrolling and word wrapping

Interactive Widgets

  • Table: Sortable, selectable data tables with custom styling
  • List: Scrollable lists with selection and highlighting
  • Tabs: Tabbed interface for organizing content
  • Browser: Netidx browser component
  • Calendar: Date picker and event display

Data Visualization

  • Chart: Line charts with multiple datasets, custom axes, and styling
  • Bar Chart: Grouped and individual bar charts with labels
  • Sparkline: Compact inline charts perfect for dashboards
  • Gauge: Progress indicators and meters
  • Canvas: Low-level drawing surface for custom graphics

Input Handling

Interactive components use the input widget to handle UI events that flow through the widget graph from parent to child. Parents can choose to pass on events they receive or not. See the input widget for more details.

Building Your First TUI

Here's a simple example that demonstrates the core concepts:

use tui;
use tui::block;
use tui::text;
use tui::layout;

let counter = 0;
let clock = time::timer(duration:1.s, true);
counter <- clock ~ (counter + 1);

let content = text(&"Counter: [counter]");

block(
  #border: &`All,
  #title: &line("My First TUI"),
  #style: &style(#fg: `Green),
  &content
)

This creates a bordered block with a counter that increments every second. The key insight is that when counter changes, the text automatically updates because of Graphix's reactive nature.

First Overview

Styling and Theming

Graphix TUIs support rich styling with:

  • Colors: Named colors (Red, Green, Blue), indexed colors (Indexed(202)), and RGB (Rgb({r: 255, g: 100, b: 50}))
  • Text Effects: Bold, italic, underline, strikethrough
  • Background Colors: Set background colors for any component
  • Conditional Styling: Use select expressions to change styles based on state

Many widgets take optional style arguments, allowing styling to be applied at many levels.

use tui;

let is_selected = true;

style(
  #fg: select is_selected { true => `Yellow, false => `White },
  #bg: `DarkGray,
  #add_modifier: [`Bold]
)

Layout System

The layout system provides flexible component arrangement:

  • Direction: Horizontal or Vertical
  • Constraints: Percentage(50), Length(20), Min(10), Max(100)
  • Alignment: Left, Center, Right for horizontal; Top, Center, Bottom for vertical
  • Focus Management: Built-in focus handling for interactive components
use tui;
use tui::layout;
use tui::text;

let selected_pane = 0;
let sidebar = text(&"Sidebar");
let main_content = text(&"Main");

layout(
  #direction: &`Horizontal,
  #focused: &selected_pane,
  &[
    child(#constraint: `Percentage(30), sidebar),
    child(#constraint: `Percentage(70), main_content)
  ]
)

State Management

In Graphix, UI state is just regular program state. Use variables to track:

  • Selection states in lists and tables
  • Input field contents
  • Window/pane focus
  • Application modes (normal, edit, command)

State changes automatically trigger UI updates:

use tui;
use tui::list;
use tui::text;

let selected_item = 0;
let items = [line("Item 1"), line("Item 2"), line("Item 3")];
let arrow_pressed = never();

// When user presses down arrow assume the event is handled as
// shown above and arrow_pressed is set using connect
selected_item <- arrow_pressed ~ ((selected_item + 1) % array::len(items));

// UI automatically reflects the change
list(#selected: &selected_item, &items)

Real-time Data Integration

Graphix TUIs excel at displaying real-time data. Connect to data sources via netidx and the UI updates automatically:

use tui;
use tui::gauge;
use tui::text;

// Subscribe to live data
let temperature = cast<f64>(net::subscribe("/sensors/temperature")?)?;

// Will display updates automatically when data changes
gauge(
  #gauge_style: &style(#fg: `Red),
  #label: &span("Temperature"),
  &(temperature / 100.0)
)

style

The Bar Chart Widget

The barchart widget displays categorical data as vertical bars, supporting grouped bars, custom styling, and dynamic updates. It's ideal for comparing values across categories, showing rankings, or displaying resource usage.

APIs

mod barchart: sig {
    /// Creates a bar chart from bar groups
    val bar_chart: fn(
        ?#max: &i64,
        ?#bar_width: &i64,
        ?#bar_gap: &i64,
        ?#group_gap: &i64,
        ?#style: &Style,
        &Array<BarGroup>
    ) -> Widget;

    /// Creates a group of bars
    val bar_group: fn(?#label: Line, Array<Bar>) -> BarGroup;

    /// Creates an individual bar
    val bar: fn(
        ?#style: &Style,
        ?#label: &Line,
        ?#text_value: &Line,
        &i64
    ) -> Bar;
}

Parameters

bar_chart

  • max - Maximum value for chart scale (auto-scales if not specified)
  • bar_width - Width of each bar in characters
  • bar_gap - Space between bars within a group
  • group_gap - Space between bar groups
  • style - Base style for the chart

bar_group

  • label - Line labeling the group (displayed below bars)

bar

  • style - Style for the bar
  • label - Line labeling the bar
  • text_value - Line displayed above bar (defaults to numeric value)

Examples

Basic Usage

use tui;
use tui::barchart;

let bar1 = bar(
    #style: &style(#fg: `Cyan),
    #label: &line("Sales"),
    &42
);

bar_chart(&[bar_group(#label: line("Q1"), [bar1])])

Basic Bar Chart

Grouped Bars with Dynamic Data

use tui;
use tui::barchart;
use tui::block;
use rand;

let clock = time::timer(duration:0.7s, true);

let group0 = [
    bar(#style: &style(#fg: `Red), #label: &line("CPU"), &rand(#start:0, #end:100, #clock)),
    bar(#style: &style(#fg: `Yellow), #label: &line("Memory"), &rand(#start:25, #end:200, #clock))
];

let group1 = [
    bar(#style: &style(#fg: `Cyan), #label: &line("Network"), &rand(#start:0, #end:50, #clock)),
    bar(#style: &style(#fg: `Magenta), #label: &line("Disk"), &rand(#start:1, #end:25, #clock))
];

let chart = bar_chart(
    #bar_gap: &2,
    #bar_width: &8,
    #max: &200,
    &[
        bar_group(#label: line("Server 1"), group0),
        bar_group(#label: line("Server 2"), group1)
    ]
);

block(#border: &`All, #title: &line("Resource Usage"), &chart)

Grouped Bar Chart

Color-coded Values

use tui;
use tui::barchart;

let make_colored_bar = |label, value| {
    let color = select value {
        v if v > 80 => `Red,
        v if v > 50 => `Yellow,
        _ => `Green
    };
    bar(#style: &style(#fg: color), #label: &line(label), &value)
};

let bars = [
    make_colored_bar("Service A", 35),
    make_colored_bar("Service B", 65),
    make_colored_bar("Service C", 92)
];

bar_chart(&[bar_group(bars)])

Colored Bar Chart

See Also

  • chart - For continuous data visualization
  • sparkline - For compact trend display
  • gauge - For single value display

The Block Widget

The block widget is a container that wraps other widgets with optional borders, titles, and styling. It's one of the most commonly used widgets for creating structured layouts and visually separating different sections of your TUI.

API

mod block: sig {
    type Borders = [`All, `None, `Top, `Bottom, `Left, `Right];

    /// Creates a block widget that wraps content with borders and styling
    val block: fn(
        ?#border: &Borders,
        ?#border_style: &Style,
        ?#title: &Line,
        ?#title_bottom: &Line,
        ?#style: &Style,
        ?#size: &Size,
        &Widget
    ) -> Widget;
}

Parameters

  • border - Border style: All, None, Top, Bottom, Left, or Right
  • border_style - Style for the border
  • title - Line displayed at the top of the block
  • title_bottom - Line displayed at the bottom of the block
  • style - Style for the block's interior
  • size (output) - Rendered size of the block

Examples

Basic Usage

use tui;
use tui::block;
use tui::paragraph;

let content = paragraph(&"Hello, World!");

block(
    #border: &`All,
    #title: &line("My Block"),
    &content
)

Basic Block

Focus Indication

Use dynamic styling to show which block has focus:

use tui;
use tui::block;
use tui::paragraph;

let focused_block = 0;
let content = paragraph(&"Content here");

block(
    #border: &`All,
    #border_style: &style(
        #fg: select focused_block {
            0 => `Red,
            _ => `Yellow
        }
    ),
    #title: &line("Block 1"),
    &content
)

Styled Block

Dynamic Titles

Titles can contain reactive values that update automatically:

use tui;
use tui::block;
use tui::paragraph;

let count = 0;
let timer = time::timer(duration:1.s, true);
count <- timer ~ (count + 1);

let content = paragraph(&"Content here");

block(
    #border: &`All,
    #title: &line("Counter: [count]"),
    &content
)

Block With Dynamic Title

See Also

  • layout - For arranging multiple blocks
  • paragraph - Common content for blocks
  • text - For creating styled text content

The Browser Widget

The browser widget provides a specialized interface for browsing and interacting with netidx hierarchies. It displays netidx paths in a tree structure with keyboard navigation, selection, and cursor movement support.

APIs

mod browser: sig {
    type MoveCursor = [`Left(i64), `Right(i64), `Up(i64), `Down(i64)];

    /// Creates a browser widget for navigating netidx hierarchies
    val browser: fn(
        ?#cursor: MoveCursor,
        ?#selected_row: &string,
        #selected_path: &string,
        ?#size: &Size,
        string
    ) -> Widget;
}

Parameters

  • cursor - Programmatic cursor movement: Left(n), Right(n), Up(n), Down(n)
  • selected_row (output) - Display name of the selected row
  • selected_path (output, required) - Full path of the currently selected item
  • size (output) - Rendered size of the browser

Examples

Basic Usage

use tui;
use tui::browser;

let selected_path: string = never();

browser(
    #size: {width: 40, height: 10},
    #selected_path: &selected_path,
    "/"  // Start browsing from root
)

Basic Browser Widget

Basic Navigation

use tui;
use tui::browser;
use tui::input_handler;

let path = "/";
let selected_path: string = never();
let selected_row: string = never();
let cursor: MoveCursor = never();

let handle_event = |e: Event| -> [`Stop, `Continue] select e {
    `Key(k) => select k.kind {
        `Press => select k.code {
            e@ `Up => { cursor <- e ~ `Up(1); `Stop },
            e@ `Down => { cursor <- e ~ `Down(1); `Stop },
            e@ `Left => { cursor <- e ~ `Left(1); `Stop },
            e@ `Right => { cursor <- e ~ `Right(1); `Stop },
            e@ `Enter => { path <- e ~ selected_row; `Stop },
            _ => `Continue
        },
        _ => `Continue
    },
    _ => `Continue
};

input_handler(
    #handle: &handle_event,
    &browser(
        #size: {width: 80, height: 24},
        #cursor,
        #selected_path: &selected_path,
        #selected_row: &selected_row,
        path
    )
)

Browser With Navigation

See Also

  • list - For simpler selection interfaces
  • table - For tabular data display
  • block - For containing browsers with borders

The Calendar Widget

The calendar widget displays a monthly calendar view with support for highlighting specific dates and displaying events. It's perfect for date pickers, event schedulers, and time-based visualizations.

APIs

mod calendar: sig {
    /// Creates a calendar widget displaying a month
    val calendar: fn(
        ?#show_month: &Style,
        ?#show_weekday: &Style,
        ?#show_surrounding: &Style,
        ?#events: &Array<CalendarEvent>,
        &Date
    ) -> Widget;

    /// Creates an event marker for a specific date
    val calendar_event: fn(Style, Date) -> CalendarEvent;

    /// Creates a date object
    val date: fn(i64, i64, i64) -> Date;  // (year, month, day)
}

Parameters

calendar

  • show_month - Style for the month header
  • show_weekday - Style for weekday headers (Mon, Tue, etc.)
  • show_surrounding - Style for dates from surrounding months
  • events - Array of CalendarEvent objects to highlight dates

calendar_event

Takes a style and a date to create an event marker.

date

Creates a date with year, month (1-12), and day (1-31).

Examples

Basic Usage

use tui;
use tui::calendar;

let current_date = date(2024, 5, 15);

calendar(&current_date)

Basic Calendar

Event Calendar

use tui;
use tui::calendar;
use tui::block;
use tui::text;

let today = date(2024, 5, 15);

let events = [
    calendar_event(style(#fg: `Red), date(2024, 5, 5)),
    calendar_event(style(#fg: `Green), date(2024, 5, 15)),
    calendar_event(style(#fg: `Yellow), date(2024, 5, 20)),
    calendar_event(style(#fg: `Cyan), date(2024, 5, 28))
];

block(
    #border: &`All,
    #title: &line("May 2024"),
    &calendar(
        #show_month: &style(#fg: `Yellow, #add_modifier: [`Bold]),
        #show_weekday: &style(#fg: `Cyan),
        #show_surrounding: &style(#fg: `DarkGray),
        #events: &events,
        &today
    )
)

Calendar With Events

Color-coded Events by Type

use tui;
use tui::calendar;

type EventType = [`Meeting, `Deadline, `Holiday, `Birthday];
type CalendarEntry = {date: Date, event_type: EventType};

let entries = [
    {date: date(2024, 5, 5), event_type: `Meeting},
    {date: date(2024, 5, 10), event_type: `Deadline},
    {date: date(2024, 5, 15), event_type: `Holiday},
    {date: date(2024, 5, 25), event_type: `Birthday}
];

let event1 = calendar_event(style(#fg: `Blue), date(2024, 5, 5));
let event2 = calendar_event(style(#fg: `Red), date(2024, 5, 10));
let event3 = calendar_event(style(#fg: `Green), date(2024, 5, 15));
let event4 = calendar_event(style(#fg: `Magenta), date(2024, 5, 25));
let events = [event1, event2, event3, event4];

calendar(#events: &events, &date(2024, 5, 1))

Calendar Typed

See Also

  • table - For tabular date-based data
  • list - For event lists
  • block - For containing calendars with borders

The Canvas Widget

The canvas widget provides a low-level drawing surface for custom graphics. You can draw lines, circles, rectangles, points, and text labels at specific coordinates, making it perfect for diagrams, plots, and custom visualizations.

APIs

mod canvas: sig {
    type Bounds = {min: f64, max: f64};
    type Shape = [
        `Line({color: Color, x1: f64, y1: f64, x2: f64, y2: f64}),
        `Circle({color: Color, x: f64, y: f64, radius: f64}),
        `Rectangle({color: Color, x: f64, y: f64, width: f64, height: f64}),
        `Points({color: Color, coords: Array<(f64, f64)>}),
        `Label({line: Line, x: f64, y: f64})
    ];

    /// Creates a canvas widget for custom graphics
    val canvas: fn(
        ?#background_color: &Color,
        ?#marker: &Marker,
        #x_bounds: &Bounds,
        #y_bounds: &Bounds,
        &Array<&Shape>
    ) -> Widget;
}

Parameters

  • background_color - Background color for the canvas
  • marker - Marker type: Dot, Braille (default), or Block
  • x_bounds - X-axis range with min and max fields (required)
  • y_bounds - Y-axis range with min and max fields (required)

Shape Types

Line

`Line({color: `Red, x1: 0.0, y1: 0.0, x2: 10.0, y2: 5.0})

Circle

`Circle({color: `Blue, x: 5.0, y: 5.0, radius: 2.0})

Rectangle

`Rectangle({color: `Green, x: 2.0, y: 2.0, width: 3.0, height: 4.0})

Points

`Points({color: `Yellow, coords: [(1.0, 1.0), (2.0, 3.0), (3.0, 1.5)]})

Label

`Label({line: line("Hello"), x: 5.0, y: 0.5})

Examples

Basic Usage

use tui;
use tui::canvas;

let line = `Line({color: `Red, x1: 0.0, y1: 0.0, x2: 10.0, y2: 5.0});
let circle = `Circle({color: `Blue, x: 5.0, y: 5.0, radius: 2.0});

canvas(
    #x_bounds: &{min: 0.0, max: 10.0},
    #y_bounds: &{min: 0.0, max: 10.0},
    &[&line, &circle]
)

Basic Canvas

Function Plotting

use tui;
use tui::canvas;

let coords = [
    (0.0, 0.0), (0.5, 0.48), (1.0, 0.84), (1.5, 1.0),
    (2.0, 0.91), (2.5, 0.60), (3.0, 0.14), (3.5, -0.35),
    (4.0, -0.76), (4.5, -0.98), (5.0, -0.96)
];

let plot = `Points({color: `Cyan, coords});

canvas(
    #x_bounds: &{min: 0.0, max: 10.0},
    #y_bounds: &{min: -1.0, max: 1.0},
    &[&plot]
)

Scatter Plot

Network Diagram

use tui;
use tui::canvas;
use tui::text;

let circle1 = `Circle({color: `Blue, x: 2.0, y: 5.0, radius: 0.5});
let circle2 = `Circle({color: `Blue, x: 8.0, y: 5.0, radius: 0.5});
let circle3 = `Circle({color: `Blue, x: 5.0, y: 8.0, radius: 0.5});

let line1 = `Line({color: `White, x1: 2.0, y1: 5.0, x2: 8.0, y2: 5.0});
let line2 = `Line({color: `White, x1: 2.0, y1: 5.0, x2: 5.0, y2: 8.0});
let line3 = `Line({color: `White, x1: 8.0, y1: 5.0, x2: 5.0, y2: 8.0});

let all_shapes = [&line1, &line2, &line3, &circle1, &circle2, &circle3];

canvas(
    #x_bounds: &{min: 0.0, max: 10.0},
    #y_bounds: &{min: 0.0, max: 10.0},
    &all_shapes
)

Network Diagram

Animated Graphics

use tui;
use tui::canvas;

let clock = time::timer(duration:0.1s, true);
let x = 0.0;
x <- {
    let new_x = (clock ~ x) + 0.1;
    select new_x > 10.0 { true => 0.0, false => new_x }
};

let moving_circle = `Circle({color: `Red, x, y: 5.0, radius: 1.0});

canvas(
    #x_bounds: &{min: 0.0, max: 10.0},
    #y_bounds: &{min: 0.0, max: 10.0},
    &[&moving_circle]
)

Animated Canvas

Marker Comparison

  • Braille: Highest resolution, smoothest curves, best for detailed graphics
  • Dot: Fast rendering, lower resolution, good for simple shapes
  • Block: High contrast, blocky appearance, good for filled areas

Coordinate System

  • Origin (0, 0) is at the bottom-left
  • X increases to the right
  • Y increases upward
  • Shapes outside bounds are clipped

See Also

  • chart - For pre-built line charts
  • barchart - For bar-based visualizations

The Chart Widget

The chart widget renders line charts with multiple datasets, custom axes, labels, and styling. It's ideal for visualizing time series data, trends, sensor readings, and any numeric data relationships.

APIs

mod chart: sig {
    type GraphType = [`Line, `Scatter];
    type Marker = [`Dot, `Braille, `Block];

    /// Creates a chart widget with datasets and axes
    val chart: fn(
        ?#style: &Style,
        #x_axis: &Axis,
        #y_axis: &Axis,
        &Array<Dataset>
    ) -> Widget;

    /// Creates an axis configuration
    val axis: fn(
        ?#title: Line,
        ?#labels: Array<Line>,
        ?#style: Style,
        {min: f64, max: f64}
    ) -> Axis;

    /// Creates a dataset to display on the chart
    val dataset: fn(
        ?#style: &Style,
        ?#graph_type: &GraphType,
        ?#marker: &Marker,
        ?#name: &Line,
        &Array<(f64, f64)>
    ) -> Dataset;
}

Parameters

chart

  • style - Background style for the chart area
  • x_axis - X-axis configuration (required)
  • y_axis - Y-axis configuration (required)

axis

  • title - Line for axis title
  • labels - Array of lines displayed along axis
  • style - Style for axis lines and ticks

dataset

  • style - Style for the dataset (line and markers)
  • graph_type - Line or Scatter
  • marker - Dot, Braille, or Block
  • name - Line naming the dataset (for legends)

Examples

Basic Usage

use tui;
use tui::chart;

let data: Array<(f64, f64)> = [(0.0, 0.0), (1.0, 1.0), (2.0, 4.0), (3.0, 9.0)];

let ds = dataset(
    #style: &style(#fg: `Cyan),
    #graph_type: &`Line,
    #marker: &`Dot,
    &data
);

chart(
    #x_axis: &axis({min: 0.0, max: 3.0}),
    #y_axis: &axis({min: 0.0, max: 9.0}),
    &[ds]
)

Basic Chart

Real-time Data Visualization

use tui;
use tui::chart;
use tui::text;

let data: Array<(f64, f64)> = {
    let clock = time::timer(duration:0.5s, true);
    let x = 0.0;
    x <- (clock ~ x) + 1.0;
    let y = rand::rand(#clock, #start: f64:0., #end: f64:100.);
    let a = [];
    a <- array::window(#n: 32, clock ~ a, (x, y));
    a
};

let ds = dataset(
    #style: &style(#fg: `Cyan),
    #graph_type: &`Line,
    #marker: &`Dot,
    &data
);

let label_style = style(#fg: `Yellow);

chart(
    #style: &style(#bg: `Rgb({r: 20, g: 20, b: 20})),
    #x_axis: &axis(
        #title: line(#style: label_style, "Time (s)"),
        #labels: [
            line(#style: label_style, "[(data[0]$).0]"),
            line(#style: label_style, "[(data[-1]$).0]")
        ],
        {min: (data[0]$).0, max: (data[-1]$).0}
    ),
    #y_axis: &axis(
        #title: line(#style: label_style, "Value"),
        #labels: [
            line("0"), line("50"), line("100")
        ],
        {min: 0.0, max: 100.0}
    ),
    &[ds]
)

Real Time Chart

Multiple Datasets

use tui;
use tui::chart;
use tui::text;

let temp_data = [(0.0, 20.0), (1.0, 22.0), (2.0, 21.5)];
let humidity_data = [(0.0, 50.0), (1.0, 55.0), (2.0, 52.0)];

let temp_ds = dataset(
    #style: &style(#fg: `Red),
    #name: &line("Temperature"),
    &temp_data
);

let humidity_ds = dataset(
    #style: &style(#fg: `Blue),
    #name: &line("Humidity"),
    &humidity_data
);

let x_axis =
    axis(#labels: [line("0"), line("1"), line("2")], {min: 0.0, max: 2.0});
let y_axis =
    axis(#labels: [line("0"), line("50"), line("100")], {min: 0.0, max: 100.0});

chart(
    #x_axis: &x_axis,
    #y_axis: &y_axis,
    &[temp_ds, humidity_ds]
)

Multi Chart

Marker Comparison

  • Dot: Fastest, lowest resolution, good for dense data
  • Braille: Smoothest curves, medium performance, best visual quality
  • Block: High contrast, medium performance

See Also

The Text Widget

The text widget renders styled text in the terminal. It's a fundamental building block for displaying formatted content with colors, modifiers, and multiple lines. Text is built from Line objects, which are in turn composed of Span objects.

APIs

mod text: sig {
    type Alignment = [`Left, `Center, `Right];
    type Modifier = [
        `Bold,
        `Italic,
        `Underlined,
        `SlowBlink,
        `RapidBlink,
        `Reversed,
        `Hidden,
        `CrossedOut
    ];
    type Color = [
        `Red, `Green, `Yellow, `Blue, `Magenta, `Cyan, `Gray, `DarkGray,
        `LightRed, `LightGreen, `LightYellow, `LightBlue, `LightMagenta, `LightCyan,
        `White, `Black,
        `Indexed(i64),
        `Rgb({r: i64, g: i64, b: i64})
    ];

    /// Creates styled text from a string or array of lines
    val text: fn(&[string, Array<Line>]) -> Widget;

    /// Creates a line of text from a string or array of spans
    val line: fn(?#style: Style, ?#alignment: Alignment, [string, Array<Span>]) -> Line;

    /// Creates a styled text span
    val span: fn(?#style: Style, string) -> Span;

    /// Creates a text style
    val style: fn(?#fg: Color, ?#bg: Color, ?#add_modifier: Modifier) -> Style;
}

Text Hierarchy

  • Span: A single segment of text with a single style
  • Line: A collection of spans forming one line
  • Text: A collection of lines forming multi-line content

Examples

Basic Usage

use tui;
use tui::text;

text(&"Hello, World!")

Basic Text

Status Messages

use tui;
use tui::text;

let make_status = |level, msg| select level {
    `Error => line([
        span(#style: style(#fg: `Red, #add_modifier: [`Bold]), "ERROR: "),
        span(msg)
    ]),
    `Warning => line([
        span(#style: style(#fg: `Yellow, #add_modifier: [`Bold]), "WARNING: "),
        span(msg)
    ]),
    `Info => line([
        span(#style: style(#fg: `Cyan), "INFO: "),
        span(msg)
    ])
};

text(&[
    make_status(`Info, "Application started"),
    make_status(`Warning, "Cache miss"),
    make_status(`Error, "Connection failed")
])

Multi Line Status

Dynamic Colors

use tui;
use tui::text;

let count = 0;
let timer = time::timer(duration:1.s, true);
count <- timer ~ (count + 1);

let colors = [`Red, `Green, `Yellow, `Blue, `Magenta, `Cyan];
let color = colors[count % array::len(colors)]$;

let l = line([
    span(#style: style(#fg: `White), "Count: "),
    span(#style: style(#fg: color, #add_modifier: [`Bold]), "[count]")
]);

text(&[l])

Dynamic Text

Alignment

use tui;
use tui::text;

text(&[
    line(#alignment: `Left, "Left aligned"),
    line(#alignment: `Center, "Centered"),
    line(#alignment: `Right, "Right aligned")
])

Text Alignment

Color Support

  • Named colors: Red, Green, Blue, Yellow, Magenta, Cyan, White, Black, Gray, DarkGray, and Light* variants
  • Indexed colors: Indexed(202) for 256-color palette
  • RGB colors: Rgb({r: 255, g: 100, b: 50}) for true color

Text Modifiers

  • Bold, Italic, Underlined, CrossedOut
  • SlowBlink, RapidBlink (terminal support varies)
  • Reversed, Hidden

See Also

  • paragraph - For wrapped and scrollable text
  • block - For containing text with borders
  • list - For selectable text items

The Paragraph Widget

The paragraph widget displays multi-line text with automatic word wrapping and scrolling support. It's ideal for displaying long text content, logs, or any content that needs to flow across multiple lines.

API

mod paragraph: sig {
    type ScrollPosition = {x: i64, y: i64};

    /// Creates a paragraph widget with text content
    val paragraph: fn(
        ?#scroll: &ScrollPosition,
        ?#alignment: &Alignment,
        ?#wrap: &bool,
        &[string, Text]
    ) -> Widget;
}

Parameters

  • scroll - Record with x and y fields for scroll position
  • alignment - Left, Center, or Right
  • wrap - Enable/disable word wrapping (default: true)

Examples

Basic Usage

use tui;
use tui::paragraph;

paragraph(&"This is a simple paragraph. It will automatically wrap to fit the available width.")

Basic Paragraph

Scrollable Content

use tui;
use tui::paragraph;
use tui::block;
use tui::text;
use tui::input_handler;

let long_text = "I have got a lovely bunch of coconuts. Very long text continues here. More text. Even more text. This is a very long paragraph that will need scrolling to see all of it.";
let scroll_y = 0;

let handle_event = |e: Event| -> [`Stop, `Continue] select e {
    `Key(k) => select k.kind {
        `Press => select k.code {
            k@`Up if scroll_y > 0 => {
                scroll_y <- (k ~ scroll_y) - 1;
                `Stop
            },
            k@`Down if scroll_y < 100 => {
                scroll_y <- (k ~ scroll_y) + 1;
                `Stop
            },
            _ => `Continue
        },
        _ => `Continue
    },
    _ => `Continue
};

input_handler(
    #handle: &handle_event,
    &block(
        #border: &`All,
        #title: &line("Scrollable Text"),
        &paragraph(
            #scroll: &{x: 0, y: scroll_y},
            &long_text
        )
    )
)

Scrollable Paragraph

Live Log Viewer

Display real-time updating content:

use tui;
use tui::paragraph;
use tui::text;

let log_entries = [];
let new_entry = net::subscribe("/local/logs/application")?;

log_entries <- array::window(
    #n: 100,
    new_entry ~ log_entries,
    line(cast<string>(new_entry)?)
);

paragraph(&log_entries)

Log Viewer

Centered Message

use tui;
use tui::paragraph;
use tui::text;

paragraph(
    #alignment: &`Center,
    &[
        line(""),
        line(#style: style(#fg: `Yellow, #add_modifier: [`Bold]), "Welcome"),
        line(""),
        line("Press any key to continue")
    ]
)

Paragraph Centered

Word Wrapping

The paragraph widget automatically wraps long lines to fit the available width. Word boundaries are respected, so words won't be split in the middle unless they're longer than the available width.

See Also

  • text - For creating styled text content
  • scrollbar - For adding scrollbars
  • block - For containing paragraphs with borders
  • list - For line-by-line selectable content

The Gauge Widget

The gauge widget displays a single value as a filled progress indicator, perfect for showing percentages, completion status, or resource usage. It provides a clear visual representation of how full or complete something is.

API

mod gauge: sig {
    /// Creates a gauge widget showing progress from 0.0 to 1.0
    val gauge: fn(
        ?#gauge_style: &Style,
        ?#label: &Line,
        ?#use_unicode: &bool,
        ?#style: &Style,
        &f64
    ) -> Widget;
}

Parameters

  • gauge_style - Style for the filled portion
  • label - Line or span displayed in the center
  • use_unicode - Use Unicode block characters for smoother rendering
  • style - Style for the unfilled portion

Examples

Basic Usage

use tui;
use tui::gauge;

let progress = 0.75;  // 75%

gauge(
    #gauge_style: &style(#fg: `Green),
    &progress
)

Basic Gauge

Progress with Color Thresholds

use tui;
use tui::gauge;
use tui::block;
use tui::text;

let clock = time::timer(duration:0.5s, true);
let power = 0.0;
power <- min(1.0, (clock ~ power) + 0.01);

let color = select power {
    x if x < 0.10 => `Red,
    x if x < 0.25 => `Yellow,
    x => `Green
};

let percentage = cast<i64>(power * 100.0)?;

block(
    #border: &`All,
    #title: &line("Power Level"),
    &gauge(
        #gauge_style: &style(#fg: color),
        #label: &span("[percentage]%"),
        &power
    )
)

Gauge With Color

Resource Usage

use tui;
use tui::gauge;
use tui::text;

let used_memory = 6.5;  // GB
let total_memory = 16.0;  // GB
let usage_ratio = used_memory / total_memory;

let color = select usage_ratio {
    x if x > 0.9 => `Red,
    x if x > 0.7 => `Yellow,
    _ => `Green
};

gauge(
    #gauge_style: &style(#fg: color),
    #label: &span("[used_memory] GB / [total_memory] GB"),
    &usage_ratio
)

Resource Usage Gauge

See Also

The Line Gauge Widget

The line_gauge widget displays a horizontal progress indicator using line-drawing characters. It's more compact than gauge and ideal for dashboards where vertical space is limited.

API

mod linegauge: sig {
    type LineSet = [`Thin, `Thick, `Double];

    /// Creates a line gauge widget showing progress from 0.0 to 1.0
    val line_gauge: fn(
        ?#filled_style: &Style,
        ?#unfilled_style: &Style,
        ?#line_set: &LineSet,
        ?#label: &Line,
        ?#style: &Style,
        &f64
    ) -> Widget;
}

Parameters

  • filled_style - Style for the filled portion
  • unfilled_style - Style for the unfilled portion
  • line_set - Character set: Thin, Thick (default), or Double
  • label - Line or span displayed within the gauge
  • style - Base style for the widget

Examples

Basic Usage

use tui;
use tui::line_gauge;

let progress = 0.75;  // 75%

line_gauge(
    #filled_style: &style(#fg: `Green),
    &progress
)

Basic Line Gauge

Color-coded Status

use tui;
use tui::line_gauge;
use tui::block;
use tui::text;

{
    let clock = time::timer(duration:0.5s, true);
    let power = 0.0;
    power <- min(1.0, (clock ~ power) + 0.01);

    let color = select power {
        x if x < 0.10 => `Red,
        x if x < 0.25 => `Yellow,
        x => `Green
    };

    let percentage = cast<i64>(power * 100.0)?;

    block(
        #border: &`All,
        #title: &line("Power"),
        &line_gauge(
            #filled_style: &style(#fg: color),
            #line_set: &`Thick,
            #label: &line("[percentage]%"),
            &power
        )
    )
}

Line Gauge Color Coded

Compact Multi-metric Display

use tui;
use tui::line_gauge;
use tui::text;
use tui::layout;

layout(
    #direction: &`Vertical,
    &[
        child(#constraint: `Percentage(5), line_gauge(
            #line_set:&`Thick,
            #filled_style: &style(#fg: `Red),
            #label: &line("CPU 45%"),
            &0.45
        )),
        child(#constraint: `Percentage(5), line_gauge(
            #line_set:&`Thick,
            #filled_style: &style(#fg: `Yellow),
            #label: &line("MEM 67%"),
            &0.67
        )),
        child(#constraint: `Percentage(5), line_gauge(
            #line_set:&`Thick,
            #filled_style: &style(#fg: `Green),
            #label: &line("DSK 23%"),
            &0.23
        ))
    ]
)

Line Gauge Multi Colored

Use Cases

  • System resource monitors (CPU, RAM, disk, network)
  • Download/upload progress indicators
  • Compact status dashboards
  • Progress tracking in limited space

Comparison with gauge

Use line_gauge when:

  • You need compact, single-line displays
  • Vertical space is limited
  • You want a more technical/modern look

Use gauge when:

  • You have more vertical space available
  • You want larger, more prominent indicators

See Also

  • gauge - For block-style progress indicators
  • sparkline - For historical trend display
  • barchart - For categorical value comparison

input

The Layout Widget

The layout widget arranges child widgets in horizontal or vertical layouts with flexible sizing constraints. It's the primary tool for organizing complex TUI interfaces and supports focus management for interactive applications.

APIs

mod layout: sig {
    type Direction = [`Horizontal, `Vertical];
    type Flex = [`Start, `Center, `End, `SpaceAround, `SpaceBetween];
    type Constraint = [
        `Percentage(i64),
        `Length(i64),
        `Min(i64),
        `Max(i64),
        `Ratio(i64, i64),
        `Fill(i64)
    ];

    /// Creates a layout that arranges child widgets
    val layout: fn(
        ?#direction: &Direction,
        ?#focused: &i64,
        ?#flex: &Flex,
        &Array<Child>
    ) -> Widget;

    /// Creates a child widget with sizing constraints
    val child: fn(?#constraint: Constraint, Widget) -> Child;
}

Parameters

  • direction - Horizontal or Vertical (default: Vertical)
  • focused - Index of the currently focused child (0-indexed)
  • flex - Alignment when children don't fill space: Start, Center, End, SpaceAround, SpaceBetween

Constraint Types

  • Percentage(n) - Allocates n% of available space
  • Length(n) - Fixed width/height in cells
  • Min(n) - At least n cells
  • Max(n) - At most n cells
  • Ratio(num, den) - Fractional allocation (num/den)
  • Fill(n) - Takes remaining space after other constraints

Examples

Basic Layout

use tui;
use tui::layout;
use tui::block;
use tui::text;

let content1 = text(&"Sidebar content");
let content2 = text(&"Main content");

let sidebar = block(#border: &`All, #title: &line("Sidebar"), &content1);
let main = block(#border: &`All, #title: &line("Main"), &content2);

layout(
    #direction: &`Horizontal,
    &[
        child(#constraint: `Percentage(30), sidebar),
        child(#constraint: `Percentage(70), main)
    ]
)

Basic Layout

Three-Pane Layout with Focus

use tui;
use tui::layout;
use tui::text;
use tui::input_handler;
use tui::block;

let focused = 0;

let handle_event = |e: Event| -> [`Stop, `Continue] select e {
    `Key(k) => select k.kind {
        `Press => select k.code {
            k@`Tab => {
                focused <- ((k ~ focused) + 1) % 3;
                `Stop
            },
            _ => `Continue
        },
        _ => `Continue
    },
    _ => `Continue
};

let focused_border = |i| select focused { n if n == i => `All, _ => [`Top] };
let left_pane = block(#border:&focused_border(0), &text(&"Left"));
let center_pane = block(#border:&focused_border(1), &text(&"Center"));
let right_pane = block(#border:&focused_border(2), &text(&"Right"));

input_handler(
    #handle: &handle_event,
    &layout(
        #direction: &`Horizontal,
        #focused: &focused,
        &[
            child(#constraint: `Percentage(25), left_pane),
            child(#constraint: `Percentage(50), center_pane),
            child(#constraint: `Percentage(25), right_pane)
        ]
    )
)

Layout With Focus

Nested Layouts

use tui;
use tui::layout;
use tui::text;
use tui::block;

let widget1 = block(#border:&`All, &text(&"Widget 1"));
let widget2 = block(#border:&`All, &text(&"Widget 2"));
let bottom_widget = block(#border:&`All, &text(&"Bottom"));

let top_row = layout(
    #direction: &`Horizontal,
    &[
        child(#constraint: `Percentage(50), widget1),
        child(#constraint: `Percentage(50), widget2)
    ]
);

layout(
    #direction: &`Vertical,
    &[
        child(#constraint: `Percentage(50), top_row),
        child(#constraint: `Percentage(50), bottom_widget)
    ]
)

Nested Layout

Header/Content/Footer

use tui;
use tui::layout;
use tui::text;
use tui::block;

let header = block(#border:&`All, &text(&"Header"));
let content = block(#border:&`All, &text(&"Main Content"));
let footer = block(#border:&`All, &text(&"Footer"));

layout(
    #direction: &`Vertical,
    &[
        child(#constraint: `Percentage(20), header),
        child(#constraint: `Fill(1), content),
        child(#constraint: `Percentage(20), footer)
    ]
)

Layout With Header and Footer

See Also

The List Widget

The list widget displays a scrollable, selectable list of items with keyboard navigation support. It's perfect for menus, file browsers, option selectors, and any interface that requires choosing from a list of items.

API

mod list: sig {
    /// Creates a list widget from an array of lines
    val list: fn(
        ?#selected: &i64,
        ?#scroll: &i64,
        ?#highlight_style: &Style,
        ?#highlight_symbol: &string,
        ?#repeat_highlight_symbol: &bool,
        ?#style: &Style,
        &Array<Line>
    ) -> Widget;
}

Parameters

  • selected - Index of the currently selected item (0-indexed)
  • scroll - Scroll position (offset from the top)
  • highlight_style - Style for the selected item
  • highlight_symbol - String displayed before selected item (e.g., "▶ ")
  • repeat_highlight_symbol - Whether to repeat symbol on wrapped lines
  • style - Base style for all list items

Examples

Basic Usage

use tui;
use tui::list;

let items = [
    line("Apple"),
    line("Banana"),
    line("Cherry")
];

list(
    #selected: &0,
    &items
)

Basic List

Interactive List with Navigation

use tui;
use tui::list;
use tui::block;
use tui::text;
use tui::input_handler;

let items = [
    line("Apple"), line("Banana"), line("Cherry"),
    line("Date"), line("Elderberry"), line("Fig"), line("Grape")
];

let last = array::len(items) - 1;
let selected = 0;
let scroll_pos = 0;
let visible = 5;

// Auto-scroll to keep selection visible
scroll_pos <- select selected {
    s if s < scroll_pos => s,
    s if s > (scroll_pos + visible - 1) => s - visible + 1,
    _ => never()
};

let handle_event = |e: Event| -> [`Stop, `Continue] select e {
    `Key(k) => select k.kind {
        `Press => select k.code {
            k@`Up if selected > 0 => {
                selected <- (k ~ selected) - 1;
                `Stop
            },
            k@`Down if selected < last => {
                selected <- (k ~ selected) + 1;
                `Stop
            },
            k@`Home => { selected <- k ~ 0; `Stop },
            k@`End => { selected <- k ~ last; `Stop },
            _ => `Continue
        },
        _ => `Continue
    },
    _ => `Continue
};

input_handler(
    #handle: &handle_event,
    &block(
        #border: &`All,
        #title: &line("Fruit Selection"),
        &list(
            #highlight_style: &style(#fg: `Black, #bg: `Yellow),
            #highlight_symbol: &"▶ ",
            #selected: &selected,
            #scroll: &scroll_pos,
            &items
        )
    )
)

Interactive List

Styled Items

use tui;
use tui::list;
use tui::text;

let make_item = |text, priority| select priority {
    `High => line(#style: style(#fg: `Red, #add_modifier: [`Bold]), text),
    `Medium => line(#style: style(#fg: `Yellow), text),
    `Low => line(#style: style(#fg: `White), text)
};

let items = [
    make_item("Critical bug", `High),
    make_item("Feature request", `Medium),
    make_item("Documentation", `Low)
];

list(#selected: &0, &items)

Styled List

See Also

  • table - For multi-column structured data
  • scrollbar - For adding scrollbars
  • block - For containing lists with borders
  • tabs - For switching between different lists

The Scrollbar Widget

The scrollbar widget adds a visual scrollbar indicator to scrollable content, making it clear when content extends beyond the visible area and showing the current scroll position.

API

mod scrollbar: sig {
    /// Wraps a widget with a scrollbar indicator
    val scrollbar: fn(
        #position: &i64,
        ?#content_length: &i64,
        ?#size: &Size,
        &Widget
    ) -> Widget;
}

Parameters

  • position (required) - Current scroll position (typically the Y offset)
  • content_length - Total length of the content (auto-detected if not specified)
  • size (output) - Rendered size of the scrollbar area

Examples

Basic Usage

use tui;
use tui::scrollbar;
use tui::paragraph;

let long_text = "This is a very long text that needs scrolling. More content here. Even more content. And even more. Keep going with lots of text to demonstrate scrolling.";
let position = 0;
let content = paragraph(
    #scroll: &{x: 0, y: position},
    &long_text
);

scrollbar(
    #position: &position,
    &content
)

Basic Scrollbar

Scrollable Paragraph

use tui;
use tui::scrollbar;
use tui::paragraph;
use tui::block;
use tui::text;
use tui::input_handler;

let long_text = "Very long text content...";
let position = 0;
let max_position = 100;

let handle_event = |e: Event| -> [`Stop, `Continue] select e {
    `Key(k) => select k.kind {
        `Press => select k.code {
            k@`Up if position > 0 => {
                position <- (k ~ position) - 1;
                `Stop
            },
            k@`Down if position < max_position => {
                position <- (k ~ position) + 1;
                `Stop
            },
            k@`PageUp if position > 10 => {
                position <- (k ~ position) - 10;
                `Stop
            },
            k@`PageDown if position < (max_position - 10) => {
                position <- (k ~ position) + 10;
                `Stop
            },
            k@`Home => { position <- k ~ 0; `Stop },
            k@`End => { position <- k ~ max_position; `Stop },
            _ => `Continue
        },
        _ => `Continue
    },
    _ => `Continue
};

input_handler(
    #handle: &handle_event,
    &block(
        #border: &`All,
        #title: &line("Scrollable Content"),
        &scrollbar(
            #position: &position,
            #content_length: &max_position,
            &paragraph(#scroll: &{x: 0, y: position}, &long_text)
        )
    )
)

Scrollable Paragraph

Scrollable List

use tui;
use tui::scrollbar;
use tui::list;
use tui::text;

let items = [
    line("Item 1"),
    line("Item 2"),
    line("Item 3"),
    line("Item 4"),
    line("Item 5")
];
let selected = 0;
let scroll_pos = 0;
let visible = 10;

// Auto-scroll to keep selection visible
scroll_pos <- select selected {
    s if s < scroll_pos => s,
    s if s >= (scroll_pos + visible) => s - visible + 1,
    _ => never()
};

scrollbar(
    #position: &scroll_pos,
    &list(
        #scroll: &scroll_pos,
        #selected: &selected,
        &items
    )
)

Scrollable List

See Also

  • paragraph - For scrollable text content
  • list - For scrollable lists
  • table - For scrollable tables
  • block - For containing scrollable content

The Sparkline Widget

The sparkline widget renders compact inline charts perfect for dashboards and status displays. It shows data trends in minimal space, with support for color-coded bars based on thresholds.

APIs

mod sparkline: sig {
    type Direction = [`LeftToRight, `RightToLeft];

    /// Creates a sparkline widget from data values
    val sparkline: fn(
        ?#max: &i64,
        ?#style: &Style,
        ?#direction: &Direction,
        &Array<[SparklineBar, f64]>
    ) -> Widget;

    /// Creates a sparkline bar with custom styling
    val sparkline_bar: fn(?#style: Style, f64) -> SparklineBar;
}

Parameters

sparkline

  • max - Maximum value for scaling (auto-scales if not specified)
  • style - Default style for bars
  • direction - LeftToRight (default) or RightToLeft

sparkline_bar

  • style - Style for this specific bar

Examples

Basic Usage

use tui;
use tui::sparkline;

let data = [10.0, 25.0, 40.0, 55.0, 70.0, 85.0, 100.0];

sparkline(#max: &100, &data)

Basic Sparkline

Threshold-based Coloring

use tui;
use tui::sparkline;
use tui::block;
use tui::text;

let data = {
    let clock = time::timer(duration:0.3s, true);
    let v = rand::rand(#clock, #start:0., #end:100.);
    let d = [];
    let color = select v {
        v if v <= 25. => `Green,
        v if v <= 50. => `Yellow,
        _ => `Red
    };
    let v = sparkline_bar(#style: style(#fg: color), v);
    d <- array::window(#n:80, clock ~ d, v);
    d
};

block(
    #border: &`All,
    #title: &line("Network Traffic Rate"),
    &sparkline(
        #style: &style(#fg: `Green),
        #max: &100,
        &data
    )
)

Threshold Colors

Multi-metric Dashboard

use tui;
use tui::sparkline;
use tui::block;
use tui::text;
use tui::layout;

let cpu_data = [50., 60., 55., 70., 65.];
let mem_data = [30., 35., 40., 38., 42.];
let net_data = [10., 20., 15., 25., 30.];

layout(
    #direction: &`Vertical,
    &[
        child(#constraint: `Percentage(33), block(
            #title: &line("CPU"),
            &sparkline(#style: &style(#fg: `Red), #max: &100, &cpu_data)
        )),
        child(#constraint: `Percentage(33), block(
            #title: &line("Memory"),
            &sparkline(#style: &style(#fg: `Yellow), #max: &100, &mem_data)
        )),
        child(#constraint: `Percentage(33), block(
            #title: &line("Network"),
            &sparkline(#style: &style(#fg: `Cyan), &net_data)
        ))
    ]
)

Multi Sparkline Dashboard

Sparkline from Netidx

use tui;
use tui::sparkline;

let data: Array<f64> = [];
let new_value = net::subscribe("/local/metrics/cpu")?;

data <- array::window(
    #n: 60,
    new_value ~ data,
    cast<f64>(new_value)?
);

sparkline(#max: &100, &data)

Rolling Sparkline

showing the output of the below shell pipeline during a netidx build,

top | \
grep --line-buffered Cpu | \
awk '{ printf("/local/metrics/cpu|f64|%s\n", $6); fflush() }' | \
netidx publisher

Use Cases

Sparklines are ideal for:

  • System resource monitoring (CPU, memory, network)
  • Real-time metrics dashboards
  • Compact data visualization in lists or tables
  • Rate of change visualization

See Also

  • chart - For detailed charts with axes
  • gauge - For single current value display
  • linegauge - For horizontal progress bars

The Table Widget

The table widget displays structured data in rows and columns with support for selection, scrolling, and custom styling. It's ideal for data grids, process monitors, file listings, and any tabular data display.

APIs

mod table: sig {
    type HighlightSpacing = [`Always, `WhenSelected, `Never];

    /// Creates a table widget from an array of row references
    val table: fn(
        ?#header: &Row,
        ?#selected: &i64,
        ?#row_highlight_style: &Style,
        ?#highlight_symbol: &string,
        ?#highlight_spacing: &HighlightSpacing,
        ?#widths: &Array<Constraint>,
        ?#column_spacing: &i64,
        ?#style: &Style,
        &Array<&Row>
    ) -> Widget;

    /// Creates a table row from cells
    val row: fn(?#style: Style, Array<Cell>) -> Row;

    /// Creates a table cell from a line
    val cell: fn(?#style: Style, Line) -> Cell;
}

Parameters

  • header - Row object for the table header
  • selected - Index of the currently selected row
  • row_highlight_style - Style for the selected row
  • highlight_symbol - String before selected row
  • highlight_spacing - When to show highlight symbol: Always, WhenSelected, Never
  • widths - Array of column width constraints
  • column_spacing - Number of spaces between columns
  • style - Base style for the table

Examples

Basic Usage

use tui;
use tui::table;

let header = row([
    cell(line("Name")),
    cell(line("Age")),
    cell(line("City"))
]);

let row1 = row([
    cell(line("Alice")),
    cell(line("28")),
    cell(line("New York"))
]);

let row2 = row([
    cell(line("Bob")),
    cell(line("32")),
    cell(line("San Francisco"))
]);

table(
    #header: &header,
    #selected: &0,
    &[&row1, &row2]
)

Basic Table

Interactive Table

use tui;
use tui::table;
use tui::block;
use tui::text;
use tui::input_handler;

type User = {name: string, age: i64, city: string};

let users: Array<User> = [
    {name: "Alice", age: 28, city: "New York"},
    {name: "Bob", age: 32, city: "San Francisco"},
    {name: "Charlie", age: 25, city: "Chicago"}
];

let header = row(
    #style: style(#fg: `Yellow, #add_modifier: [`Bold]),
    [cell(line("Name")), cell(line("Age")), cell(line("City"))]
);

let row1 = row([
    cell(line("Alice")),
    cell(line("28")),
    cell(line("New York"))
]);
let row2 = row([
    cell(line("Bob")),
    cell(line("32")),
    cell(line("San Francisco"))
]);
let row3 = row([
    cell(line("Charlie")),
    cell(line("25")),
    cell(line("Chicago"))
]);
let rows = [&row1, &row2, &row3];

let selected = 0;

let handle_event = |e: Event| -> [`Stop, `Continue] select e {
    `Key(k) => select k.kind {
        `Press => select k.code {
            k@`Up if selected > 0 => {
                selected <- (k ~ selected) - 1;
                `Stop
            },
            k@`Down if selected < 2 => {
                selected <- (k ~ selected) + 1;
                `Stop
            },
            _ => `Continue
        },
        _ => `Continue
    },
    _ => `Continue
};

input_handler(
    #handle: &handle_event,
    &block(
        #border: &`All,
        #title: &line("User Directory"),
        &table(
            #header: &header,
            #row_highlight_style: &style(#bg: `Yellow, #fg: `Black),
            #selected: &selected,
            #column_spacing: &2,
            #widths: &[`Percentage(30), `Percentage(20), `Percentage(50)],
            &rows
        )
    )
)

Interactive Table

Conditional Cell Styling

use tui;
use tui::table;
use tui::text;

let make_cpu_cell = |cpu| {
    let s = select cpu {
        c if c > 80 => style(#fg: `Red),
        c if c > 50 => style(#fg: `Yellow),
        _ => style(#fg: `Green)
    };
    cell(#style: s, line("[cpu]%"))
};

let row1 = row([
    cell(line("process-1")),
    make_cpu_cell(85)  // Red
]);

let row2 = row([
    cell(line("process-2")),
    make_cpu_cell(60)  // Yellow
]);

let row3 = row([
    cell(line("process-3")),
    make_cpu_cell(30)  // Green
]);

let header = row([
    cell(#style: style(#add_modifier: [`Bold]), line("Process")),
    cell(#style: style(#add_modifier: [`Bold]), line("CPU"))
]);

table(
    #header: &header,
    #widths: &[`Percentage(60), `Percentage(40)],
    &[&row1, &row2, &row3]
)

Conditional Styling

Real-time Updates

use tui;
use tui::table;
use tui::text;

let clock = time::timer(duration:1.s, true);

let cpu_val = 5;
cpu_val <- {
    let v = clock ~ cpu_val;
    v + rand::rand(#clock, #start: -5, #end: 5)
};

let row1 = row([
    cell(line("1")),
    cell(line("init")),
    cell(line("[cpu_val]%"))
]);
let row2 = row([
    cell(line("2")),
    cell(line("kthreadd")),
    cell(line("0%"))
]);
let rows = [&row1, &row2];

let header = row([cell(line("PID")), cell(line("Name")), cell(line("CPU"))]);
table(#header: &header, &rows)

Realtime Table

See Also

  • list - For simpler single-column selection
  • scrollbar - For adding scrollbars
  • block - For containing tables with borders

The Tabs Widget

The tabs widget creates a tabbed interface for organizing content into multiple switchable panels. Each tab has a title displayed in the tab bar and associated content that's shown when the tab is selected.

API

mod tabs: sig {
    /// Creates a tabbed interface from an array of (title, content) tuples
    val tabs: fn(
        ?#selected: &i64,
        ?#highlight_style: &Style,
        ?#style: &Style,
        ?#divider: &[string, Span],
        &Array<(Line, Widget)>
    ) -> Widget;
}

Parameters

  • selected - Index of the currently selected tab (0-indexed)
  • highlight_style - Style for the selected tab title
  • style - Base style for unselected tab titles
  • divider - String or span separating tab titles

Examples

Basic Usage

use tui;
use tui::tabs;
use tui::paragraph;

let tab1 = paragraph(&"This is tab 1");
let tab2 = paragraph(&"This is tab 2");
let tab3 = paragraph(&"This is tab 3");

tabs(
    #selected: &0,
    &[
        (line("One"), tab1),
        (line("Two"), tab2),
        (line("Three"), tab3)
    ]
)

Basic Tabs

use tui;
use tui::tabs;
use tui::block;
use tui::text;
use tui::input_handler;

let selected_tab = 0;

let handle_event = |e: Event| -> [`Stop, `Continue] select e {
    `Key(k) => select k.kind {
        `Press => select k.code {
            k@`Left if selected_tab > 0 => {
                selected_tab <- (k ~ selected_tab) - 1;
                `Stop
            },
            k@`Right if selected_tab < 2 => {
                selected_tab <- (k ~ selected_tab) + 1;
                `Stop
            },
            k@`Tab => {
                selected_tab <- ((k ~ selected_tab) + 1) % 3;
                `Stop
            },
            _ => `Continue
        },
        _ => `Continue
    },
    _ => `Continue
};

let overview = text(&"Overview");
let items = text(&"Items");
let settings = text(&"Settings");

input_handler(
    #handle: &handle_event,
    &block(
        #border: &`All,
        #title: &line("Application (←/→ to navigate)"),
        &tabs(
            #highlight_style: &style(#fg: `Yellow, #add_modifier: [`Bold]),
            #style: &style(#fg: `Gray),
            #selected: &selected_tab,
            &[
                (line("Overview"), overview),
                (line("Items"), items),
                (line("Settings"), settings)
             ]
        )
    )
)

Tabs With Navigation

Styled Tab Titles

use tui;
use tui::tabs;
use tui::text;

let completed_content = text(&"Completed");
let progress_content = text(&"In Progress");
let failed_content = text(&"Failed");

let tab_list = [
    (line([
        span(#style: style(#fg: `Green), "✓ "),
        span("Completed")
    ]), completed_content),
    (line([
        span(#style: style(#fg: `Yellow), "⚠ "),
        span("In Progress")
    ]), progress_content),
    (line([
        span(#style: style(#fg: `Red), "✗ "),
        span("Failed")
    ]), failed_content)
];

tabs(&tab_list)

Styled Tabs

Tab with Badge

use tui;
use tui::tabs;
use tui::text;

let unread_count = 5;

let messages_tab = line([
    span("Messages"),
    span(#style: style(#fg: `Red, #add_modifier: [`Bold]), " ([unread_count])")
]);

let home_content = text(&"Home");
let messages_content = text(&"Messages");
let settings_content = text(&"Settings");

tabs(&[
    (line("Home"), home_content),
    (messages_tab, messages_content),
    (line("Settings"), settings_content)
])

Tabs With Badges

Keyboard Navigation

Common patterns:

  • Left/Right - Switch to previous/next tab
  • Tab - Cycle forward through tabs
  • Number keys - Jump directly to tab

See Also

  • block - For containing tabs with borders
  • list - Common content for tabs
  • table - For tabular content in tabs

Embedding and Extending Graphix

There are multiple ways you can embed Graphix in your application and extend it with rust code.

Writing built-in functions in rust

You can implement Graphix functions in rust. Most of the standard library is actually written in rust (to improve startup time), and you can easily add more built-ins using rust code for computationally heavy tasks, or IO.

There are two different ways to write built-ins, for a simple pure function you can use a the CachedArgs interface which takes care of most of the details for you. You only need to implement one method to evaluate changes to your arguments. For example min finds the minimum value of all it's arguments,

#![allow(unused)]
fn main() {
use graphix_stdlib::{deftype, CachedArgs, EvalCached};
use anyhow::{bail, Result};
use arcstr::{literal, ArcStr};
use netidx::subscriber::Value;

#[derive(Debug, Default)]
struct MinEv;

impl EvalCached for MinEv {
    const NAME: &str = "min";
    deftype!("core", "fn('a, @args:'a) -> 'a");

    fn eval(&mut self, from: &CachedVals) -> Option<Value> {
        let mut res = None;
        for v in from.flat_iter() {
            match (res, v) {
                (None, None) | (Some(_), None) => return None,
                (None, Some(v)) => {
                    res = Some(v);
                }
                (Some(v0), Some(v)) => {
                    res = if v < v0 { Some(v) } else { Some(v0) };
                }
            }
        }
        res
    }
}

type Min = CachedArgs<MinEv>;
}

Then you must register this built-in with the exec context,

#![allow(unused)]
fn main() {
ctx.register_builtin::<Min>()?
}

And then you can call it from Graphix,

let min = |@args| 'min

The special form function body 'min references a built-in rust function.

See Writing Built in Functions for details.

Building Stand Alone Graphix Applications

You can build single binary stand alone Graphix applications using the graphix-shell crate. All your Graphix source code, and built-ins are compiled together with the compiler and runtime into a single binary that you can then deploy and run. When combined with writing built-ins in rust this becomes a powerful mixed language toolset.

For example here is the netidx browser from netidx-tools:

#![allow(unused)]
fn main() {
use crate::publisher;
use anyhow::{Context, Result};
use arcstr::literal;
use graphix_rt::NoExt;
use graphix_shell::{Mode, ShellBuilder};
use netidx::{
    config::Config,
    publisher::{DesiredAuth, PublisherBuilder},
    subscriber::Subscriber,
};

pub async fn run(
    cfg: Config,
    auth: DesiredAuth,
    params: publisher::Params,
) -> Result<()> {
    let publisher = PublisherBuilder::new(cfg.clone())
        .desired_auth(auth.clone())
        .bind_cfg(params.bind)
        .build()
        .await
        .context("creating publisher")?;
    let subscriber = Subscriber::new(cfg, auth).context("create subscriber")?;
    ShellBuilder::<NoExt>::default()
        .mode(Mode::Static(literal!(include_str!("browser.gx"))))
        .publisher(publisher)
        .subscriber(subscriber)
        .no_init(true)
        .build()?
        .run()
        .await
}
}

See Stand Alone Graphix Applications for details

Embedding Graphix in Your Application

Using the graphix-rt crate you can embed the Graphix compiler and runtime in your application. Then you can,

  • compile and run Graphix code
  • receive events from Graphix expressions
  • inject events into Graphix pipelines
  • call Graphix functions

The runtime uses tokio and runs in a background task so it integrates well into a normal async workflow.

See Using Graphix as Embedded Scripting for details

Writing Built in Functions

As mentioned in the introduction you can extend Graphix by writing built in functions in rust. This chapter will deep dive into the full API, if you just want to write a pure function see the Overview.

In order to implement a built-in graphix function you must implement two traits, graphix_compiler::BuiltIn and graphix_compiler::Apply. See the rustdoc for details. These two traits give you more control than the graphix_stdlib::CachedArgs method we covered in the overview. Lets look at the simplest possible example,

Understanding The Once Function

The once function evaluates it's arugment every cycle and passes through one and only one update. The update method is the most important method of Apply, it is called every cycle and returns something only when the node being updated has "updated". The meaning of that is specific to what the node does, but in the case of once it means that the argument to once updated, and once has not already seen an update. Consider the example program,

let clock = time::timer(1, true);
println(once(clock))

We expect this example to print the datetime exactly one time. Lets dig in to how that actually works. The clock created by time::timer will tick once per second forever. The time::timer built-in will call set_timer in the Rt, which is part of the ExecCtx. This will schedule a cycle to happen 1 second from now, and will also register that this toplevel node (let clock = ...) depends on the timer event. When the timer event happens the approximate sequence of events is,

  • let clock = time::timer(1, true), update called on toplevel node (Bind)
    • time::timer(1, true), bind calls update on it's rhs
    • time::timer checks events to see if it should update, returns Some(DateTime(..))
    • bind sets the id of clock in events to Value::DateTime(..)
    • Rt checks for nodes that depend on clock schedules println(..)
  • println(once(clock)), update called on toplevel node (CallSite)
    • once(clock), println calls update on it's argument, once(clock)
    • once::update calls update on clock
    • ref clock checks events to see if it updated, returns Some(Value::DateTime(..))
    • once::update checks if it's the first time it's argument has updated, it is
    • once::update returns Some(Value::DateTime(..))
    • println prints the datetime

Implementing Once

#![allow(unused)]
fn main() {
#[derive(Debug)]
struct Once {
    val: bool,
}

impl<R: Rt, E: UserEvent> BuiltIn<R, E> for Once {
    const NAME: &str = "once";
    deftype!("core", "fn('a) -> 'a");

    fn init(_: &mut ExecCtx<R, E>) -> BuiltInInitFn<R, E> {
        Arc::new(|_, _, _, _, _| Ok(Box::new(Once { val: false })))
    }
}

impl<R: Rt, E: UserEvent> Apply<R, E> for Once {
    fn update(
        &mut self,
        ctx: &mut ExecCtx<R, E>,
        from: &mut [Node<R, E>],
        event: &mut Event<E>,
    ) -> Option<Value> {
        match from {
            [s] => s.update(ctx, event).and_then(|v| {
                if self.val {
                    None
                } else {
                    self.val = true;
                    Some(v)
                }
            }),
            _ => None,
        }
    }

    fn sleep(&mut self, _ctx: &mut ExecCtx<R, E>) {
        self.val = false
    }
}
}

The BuiltIn trait is for construction, it declares the built in's name, and it's type (which we get to write out in syntax due to the deftype macro). It's init method is called every time the built in needs to be registered with a new context, and returns a closure that allocates the initial state of the function given a bunch of information about the context it's called in. In this case we don't care about any of that information, but it will be useful later.

The most important method of Apply is update. sleep is expected to reset all the internal state and unregister anything registered with the context.

Higher Order Functions

Right, now that the easy stuff is out of the way, lets see how we can implement a built-in that takes another Graphix function as an argument. This gets compiler guts all over the place, sorry about that. Again lets look at the simplest example from the standard library, which is array::group.

#![allow(unused)]
fn main() {
#[derive(Debug)]
pub(super) struct Group<R: Rt, E: UserEvent> {
    queue: VecDeque<Value>,
    buf: SmallVec<[Value; 16]>,
    pred: Node<R, E>,
    ready: bool,
    pid: BindId,
    nid: BindId,
    xid: BindId,
}

impl<R: Rt, E: UserEvent> BuiltIn<R, E> for Group<R, E> {
    const NAME: &str = "array_group";
    deftype!(
        "core::array",
        "fn('a, fn(i64, 'a) -> bool throws 'e) -> Array<'a> throws 'e"
    );

    fn init(_: &mut ExecCtx<R, E>) -> BuiltInInitFn<R, E> {
        Arc::new(|ctx, typ, scope, from, top_id| match from {
            [_, _] => {
                let scope =
                    scope.append(&format_compact!("fn{}", LambdaId::new().inner()));
                let n_typ = Type::Primitive(Typ::I64.into());
                let etyp = typ.args[0].typ.clone();
                let mftyp = match &typ.args[1].typ {
                    Type::Fn(ft) => ft.clone(),
                    t => bail!("expected function not {t}"),
                };
                let (nid, n) =
                    genn::bind(ctx, &scope.lexical, "n", n_typ.clone(), top_id);
                let (xid, x) = genn::bind(ctx, &scope.lexical, "x", etyp.clone(), top_id);
                let pid = BindId::new();
                let fnode = genn::reference(ctx, pid, Type::Fn(mftyp.clone()), top_id);
                let pred = genn::apply(fnode, scope, vec![n, x], &mftyp, top_id);
                Ok(Box::new(Self {
                    queue: VecDeque::new(),
                    buf: smallvec![],
                    pred,
                    ready: true,
                    pid,
                    nid,
                    xid,
                }))
            }
            _ => bail!("expected two arguments"),
        })
    }
}

impl<R: Rt, E: UserEvent> Apply<R, E> for Group<R, E> {
    fn update(
        &mut self,
        ctx: &mut ExecCtx<R, E>,
        from: &mut [Node<R, E>],
        event: &mut Event<E>,
    ) -> Option<Value> {
        macro_rules! set {
            ($v:expr) => {{
                self.ready = false;
                self.buf.push($v.clone());
                let len = Value::I64(self.buf.len() as i64);
                ctx.cached.insert(self.nid, len.clone());
                event.variables.insert(self.nid, len);
                ctx.cached.insert(self.xid, $v.clone());
                event.variables.insert(self.xid, $v);
            }};
        }
        if let Some(v) = from[0].update(ctx, event) {
            self.queue.push_back(v);
        }
        if let Some(v) = from[1].update(ctx, event) {
            ctx.cached.insert(self.pid, v.clone());
            event.variables.insert(self.pid, v);
        }
        if self.ready && self.queue.len() > 0 {
            let v = self.queue.pop_front().unwrap();
            set!(v);
        }
        loop {
            match self.pred.update(ctx, event) {
                None => break None,
                Some(v) => {
                    self.ready = true;
                    match v {
                        Value::Bool(true) => {
                            break Some(Value::Array(ValArray::from_iter_exact(
                                self.buf.drain(..),
                            )))
                        }
                        _ => match self.queue.pop_front() {
                            None => break None,
                            Some(v) => set!(v),
                        },
                    }
                }
            }
        }
    }

    fn typecheck(
        &mut self,
        ctx: &mut ExecCtx<R, E>,
        _from: &mut [Node<R, E>],
    ) -> anyhow::Result<()> {
        self.pred.typecheck(ctx)
    }

    fn refs(&self, refs: &mut Refs) {
        self.pred.refs(refs)
    }

    fn delete(&mut self, ctx: &mut ExecCtx<R, E>) {
        ctx.cached.remove(&self.nid);
        ctx.cached.remove(&self.pid);
        ctx.cached.remove(&self.xid);
        self.pred.delete(ctx);
    }

    fn sleep(&mut self, ctx: &mut ExecCtx<R, E>) {
        self.pred.sleep(ctx);
    }
}
}

This implements array::group, which given an argument, stores that argument's updates internally, and creates an array out of them when the predicate returns true. It's type is

fn('a, fn(i64, 'a) -> bool) -> Array<'a>

For example,

let n = seq(0, 100);
array::group(n, |_, n| (n == 50) || (n == 99))

seq(0, 100) updates 100 times from 0 to 99. The array::group will create two arrays, one containing [0, .. 50] and the other containing [51, .. 99]

The implementation needs to build a Node representing the predicate. Node is the fundamental type of everything in the graph, ultimately the entire program compiles to a node. The kind of node we need to create here is a function call site, that will handle all the details of late binding optional arguments, default args, etc, etc. The genn module is specifically for generating nodes.

Typecheck

Because we generated code, we have to hook into the typecheck compiler phase and make sure the type checker runs on it. This requires that we implement the typecheck method. In our case all we have to do is typecheck our generated call site. You can do powerful things with this hook however.

BindIds and Refs

BindId is a very fundamental type in compiler guts. The Event struct contains two tables indexed by it. The most important is variables. Every bound variable has a BindId. If a variable has updated this cycle, then it's updated value will be in the variables table indexed by it's BindId. In order to call this predicate function we actually create three different variables and store their BindIds as xid, nid, and pid. gen::ref returns a reference Node and the BindId of the variable it is referencing. Since those ref nodes become the arguments to the predicate call site we create, xid and nid allow us to control the arguments passed into the function. We just have to set xid and nid in Event::variables before we update the predicate in order to call the function. This may cause it to update immediatly, or, it may depend on something else that needs to update before it will update. Either way, once we've set xid and nid once and called update on the predicate we've done our duty (it may never update, and that's ok). That just leaves pid, what is it for? Well, earlier it was mentioned that functions are always late bound. This is how that works. The lambda argument we were passed from[1], whatever kind of node it is, will ultimately update and return a LambdaId, which is an index into a table in the context where all compiled functions actually reside. So every cycle we need to call update on this node just like any other node, because the LambdaId of the function we are supposed to be calling might change, and if that happens the call site we created with genn::apply needs to know about it. Luckily we don't have to handle any of the wonderful details of late binding beyond this simple passing through of updates, the call site will take care of that.

ExecCtx::cached, refs, delete

What is all this ctx.cached stuff. Well, when call sites get initialized for the first time, or when an branch of select wakes from sleep, it turns out we need to know what the current value of every variable they depend on is. Which means we need to cache globally the current value of every variable. So if you're setting variables, 90% chance you need to update cached.

And this also explains another function that you have to implement when you're generating nodes, which is refs. Turns out we need to know all the variables a node depends on, so we can, you know, set them when it's being woken up from sleep, or stood up at a call site for the first time.

That just leaves delete. Turns out the structure of the graph changes at run time, and we need to keep everything straight. It would be nice if we could do this with Drop, but that would require holding a reference to the ExecCtx we are in at every Node. I'd really rather not pay to wrap every access to the context in a mutex, so we're doing it the hard way (for now).

Dynamic Linking and Non Rust Builtins

At the moment there is no dynamic linking of rust built-ins, nor is there explicit support for writing built-ins in a language other than rust. However both of those goals are on the roadmap.

Essentially the plan is to export a minimal C abi interface for adding built-ins to Graphix, and then any language that can target that abi can be used directly to write Graphix built-ins, and we can dynamically load built-ins from shared libraries.

Stand Alone Graphix Applications

As we saw in the overview, using the graphix-shell crate you can build a stand alone Graphix application using rust. With those basics out of the way, in this section we'll see,

  • linking multiple Graphix files into a module tree
  • integrating rust built-ins
  • building a custom repl with rust built-ins added

Module Trees, VFS resolver

In order to split a stand alone Graphix application up into mutiple files and modules and link it all into a single rust binary you can use a kind of module resolver called the VFS. This is pretty much exactly what it sounds like. Here is an example from the TUI library that is built into the shell by default.

#![allow(unused)]
fn main() {
fn tui_mods() -> ModuleResolver {
    ModuleResolver::VFS(HashMap::from_iter([
        (Path::from("/tui"), literal!(include_str!("tui/mod.gx"))),
        (Path::from("/tui/input_handler"), literal!(include_str!("tui/input_handler.gx"))),
        (Path::from("/tui/text"), literal!(include_str!("tui/text.gx"))),
        (Path::from("/tui/paragraph"), literal!(include_str!("tui/paragraph.gx"))),
        (Path::from("/tui/block"), literal!(include_str!("tui/block.gx"))),
        (Path::from("/tui/scrollbar"), literal!(include_str!("tui/scrollbar.gx"))),
        (Path::from("/tui/layout"), literal!(include_str!("tui/layout.gx"))),
        (Path::from("/tui/tabs"), literal!(include_str!("tui/tabs.gx"))),
        (Path::from("/tui/barchart"), literal!(include_str!("tui/barchart.gx"))),
        (Path::from("/tui/chart"), literal!(include_str!("tui/chart.gx"))),
        (Path::from("/tui/sparkline"), literal!(include_str!("tui/sparkline.gx"))),
        (Path::from("/tui/line_gauge"), literal!(include_str!("tui/line_gauge.gx"))),
        (Path::from("/tui/gauge"), literal!(include_str!("tui/gauge.gx"))),
        (Path::from("/tui/list"), literal!(include_str!("tui/list.gx"))),
        (Path::from("/tui/table"), literal!(include_str!("tui/table.gx"))),
        (Path::from("/tui/calendar"), literal!(include_str!("tui/calendar.gx"))),
        (Path::from("/tui/canvas"), literal!(include_str!("tui/canvas.gx"))),
        (Path::from("/tui/browser"), literal!(include_str!("tui/browser.gx"))),
    ]))
}
}

And then when you build the shell, you can specify this ModuleResolver, and your toplevel program can simply use these modules as if they were files on disk.

#![allow(unused)]
fn main() {
ShellBuilder::<NoExt>::default()
    .module_resolvers(vec![tui_mods()])
    .mode(Mode::Static(literal!(include_str!("main.gx"))))
    .publisher(publisher)
    .subscriber(subscriber)
    .no_init(true)
    .build()?
    .run()
    .await
}

You can have as many module resolvers as you like, when loading modules they are checked in order, so earlier ones shadow later ones.

Custom Builtins

To use custom builtins with the shell you must provide a register function

#![allow(unused)]
fn main() {
fn register(ctx: &mut ExecCtx<NoExt>) -> Result<ArcStr> {
    BuiltIn0::register(ctx)?;
    BuiltIn1::register(ctx)?;
    Ok(literal!("builtins.gx"))
}

ShellBuilder::<NoExt>::default()
    .register(Arc::new(|ctx| register(ctx)))
    .module_resolvers(vec![tui_mods()])
    .mode(Mode::Static(literal!(include_str!("main.gx"))))
    .publisher(publisher)
    .subscriber(subscriber)
    .no_init(true)
    .build()?
    .run()
    .await
}

See Implementing Builtins for details on actually implementing builtins.

Custom REPL

You can build a REPL with custom additional Graphix code, or custom builtins, when you build your shell, just change the mode to Mode::Repl. Then you'll get a custom REPL with your desired builtins and pre loaded modules already present.

Embedded Scripting Systems

You can use Graphix as an embedded scripting engine in your application. For this application we leave the shell behind and move to the graphix-rt crate. This is a lower level crate that gives you a lot more control over the compiler and run time.

Tokio

graphix-rt uses tokio internally, it will run the compiler and run loop in it's own tokio task. You Interface with this task via a GXHandle. As a result, all operations on Graphix objects are async, and the compiler/run loop will run in parallel with your application.

Setting it up

The shell itself is actually the best example of using the graphix-rt crate. As we get further into internals, the details can change more often, but in general getting a Graphix runtime going involves the following,

#![allow(unused)]
fn main() {
// set up an execution context using the generic runtime with no customization
let mut ctx = ExecCtx::new(GXRt::<NoExt>::new(publisher, subscriber));
// ... use the context to register all your built-ins, etc

// set up a channel to receive events from the RT
let (tx, rx) = mpsc::channel(100);
// build the config and start the runtime
let handle = GXConfig::builder(ctx, tx)
    // set up a root module. Note if you want the standard library you must load
    // it as part of the root module. Otherwise you will get the bare compiler.
    .root(literal!("root.gx"))
    .build()?
    .start().await?
}

Once that all succeeds you have a running compiler/run loop, and a handle that can interact with it. You are expected to read the rx portion of the mpsc channel. If you do not, the run loop will block waiting for you to read once the mpsc channel fills up.

Compiling Code, Getting Results

Once setup is complete, lets compile some code and get some results! To compile code we call compile on the handle. This results in one or more toplevel expressions and a copy of the environment (or a compile error).

#![allow(unused)]
fn main() {
let cres = handle.compile(literal!("2 + 2")).await?;

// in this case we know there is just one top level expression
let e = cres.exprs[0];

// the actual result will come to us on the channel. If the expression kept
// producing results, we'd keep seeing updates for it's id on the channel.
// This is a batch of GXEvents
let mut batch = rx.recv().await.ok_or_else(|| anyhow!("the runtime is dead"))?;
let mut env = handle.get_env().await?;
for ev in batch.drain(..) {
    match ev {
        GXEvent::Updated(id, v) if id == e.id => println!("2 + 2 = {v}"),
        GXEvent::Env(e) => env = e
    }
}
}

Refs and TRefs, Depending on Graphix Variables

If you want to be notified when a variable in Graphix updates you can register a Ref, or a TRef if you have a corresponding rust type that implements FromValue. There are two ways to get a ref, by id and by name. By id is probably the most common, because BindId will appear in any data structure that has a value passed by ref (e.g. &v), which should be common for large structures that don't change often.

#![allow(unused)]
fn main() {
// assume we got id from a data structure and it's type is &i64
let mut r = handle.compile_ref(id)

// if the variable r is bound to has a value right now it will be in last
if let Some(v) = &r.last {
    println!("current value: {v}")
}

// now we will get an update whenever the variable updates
let mut batch = rx.recv().await.ok_or_else(|| anyhow!("the runtime is dead"))?;
for ev in batch.drain(..) {
    match ev {
        GXEvent::Updated(id, v) => {
            if let Some(v) = r.update(id, &v) {
                println!("current value {v}")
            }
        },
        GXEvent::Env(_) => ()
    }
}
}

You can also set refs, which is exactly the same thing as the connect operator <-, and does what you expect it should do.

Ref By Name

We can also reference a variable by name,

#![allow(unused)]
fn main() {
let mut r = handle.compile_ref_by_name(&env, &Scope::root(), &ModPath::from(["foo"])).await?;

// the rest of the code is exactly the same
}

Calling Graphix Functions

Now lets register a call site, call a Graphix function, and get it's result. We do this by calling compile_callable_by_name on the handle.

#![allow(unused)]
fn main() {
let mut f = handle.compile_callable_by_name(&env, &Scope::root(), &ModPath::from(["sum"])).await?;
f.call(ValArray::from_iter_exact([Value::from(1), Value::from(2), Value::from(3)])).await?;

// now we must update f to drive both late binding, and get our return value
// we need a loop this time because there will be multiple updates
loop {
    let mut batch = rx.recv().ok_or_else(|| anyhow!("the runtime is dead"))?;
    for ev in batch.drain(..) {
        match ev {
            GXEvent::Updated(id, v) => {
                if let Some(v) = f.update(&v).await {
                    println!("sum returned {v}")
                }
            }
            GXEvent::Env(e) => env = e,
        }
    }
}
}

Calling Functions by LambdaId

The above case applies when we only know the name of the function we want to call, which is less common than you might imagine. If the function was passed in to us, for example we evaluated an expression that returned a function, then it's actually easier to deal with because we don't have to handle late binding. In this case we can call compile_callable on the handle.

#![allow(unused)]
fn main() {
// id is the LambdaId of the function as a Value. Lets assume it's sum
let f = handle.compile_callable(id).await?;
f.call(ValArray::from_iter_exact([Value::from(1), Value::from(41)])).await?;

// now wait for the value
let mut batch = rx.recv().ok_or_else(|| anyhow!("the runtime is dead"))?;
for ev in batch.drain(..) {
    match ev {
        GXEvent::Updated(id, v) => {
            if let Some(v) = f.update(&v) {
                println!("sum returned {v}")
            }
        }
        GxEvent::Env(e) => env = e,
    }
}
}