Language Documentation

The purpose of this documenation is to briefly describe every aspect of the Juniper programming language, and provide usage examples. If you're already proficient with another functional programming language this documentation should be sufficient to get you up to speed on Juniper. Juniper is a ML family language, so knowing another ML family language (ex: Haskell, OCaml, SML, F#) will be of great benefit.

All language features can be seen in the language grammar file.


Juniper modules are used to organize code into logical groups and prevent naming collisions. Each module is defined in its own .jun file. A Juniper module should always start with the module keyword, followed by the module name. The name of the module should be the same as the name of the .jun file. The open() declaration is then used to import all exported declarations from another module into a current module. Otherwise, the module name followed by a colon and the declaration name ModuleName:declarationName can be used to refer to a declaration in another module.


Single line comments in Juniper begin with two slashes //. A multiline comment begins with (* and ends with *).


Juniper functions are declared using the fun keyword, followed by the name of the function, an optional template, the parameters list, and a required return type. Let's take a look at a simple function which will double a number.

fun doubleMe(x : int16) : int16 = x + x

As you can see in the example above, the parameters of the functions are placed in parentheses and the colons indicate type constraints.

The general structure of a function declaration is as follows:

fun funname<template>(a1 : t1, a2: t2, ..., an : tn) : rettype where constraint1, ..., constraintM = expr

If Statements

If statements are used to control whether or not the program enters a section of code. In Juniper, if statements return values, much like the ternary operators in imperative languages like Javascript and C++. For this reason, all if statements must have an else branch.

Let's take a look at a simple if statement which is a slight modification of the previous function.

fun doubleSmallNumber(x : int16) : int16 = if x > 100 then x else 2*x end

The general structure of an if statement is as follows:

if condition then truebranch else falsebranch end

if cond1 then expr1 elif cond2 then expr2 elif ... else exprN end

Binary and Unary Operations

Juniper has support for many common operations:

Keyword Unary/Binary Meaning



Logical and



Logical or



Logical not


















Greater than or equal



Less than or equal



Greater than



Less than






Not equal



Bitwise and



Bitwise or



Bitshift left



Bitshift right



Bitwise not




The general structure of a binary operation is:

expr binary-op expr

The general structure of a unary operation is:

unary-op expr

Parentheses can be used to group operators in different ways.


Primitive Types

Juniper has a number of different primitive types built into the language: int8, uint8, int16, uint16, int32, uint32, int64, uint64, float, double, bool, unit and pointer

The intN types represent a signed integer of size N bits. The uintN types represent an unsigned integer of size N bits. Integer types can be constructed simply by typing the number (ex: 6, 480)

float and double are two floating point types with different precisions. The float type takes up 32 bits and the double type takes up 64 bits. The advantage of the double type is that it is more precise than the float type. Floating point types can be constructed by typing the number with a decimal point (ex: 42.0, 3.14)

The bool type represents a boolean value which can either have the values true or false.

The unit type is a type with only one value, and it can be thought of as a zero element tuple. The value for the unit type can be constructed using empty parentheses ().

The pointer type represents a C++ smart pointer. The pointer type is really only useful when interacting with other C++ code. If you're writing a wrapper around a C++ library, you may come across situations in which it is necessary to use this type. This type is rarely encountered in typical Juniper programs.


Tuple types are an ordered collection of values of different types. Tuples are constructed using parentheses with expressions separated by commas.

(expr1, expr2, ..., exprN)

The type of a tuple can be written as a product of types, separated by asterisks.

t1 * t2 * ... * tN

For example, the two element tuple

(true, ())

Has type bool * unit

Record Types

Record types represent aggregates of named values. Record types are structural, and are determined by the field names and field types. A record type is defined by a list of field names separated by semicolons and enclosed in curly braces. Each field name has a type associated with it, which is indicated by the colon followed by the type.

The general structure of a record type expression is:

{ fieldName1 : t1; fieldName2 : t2; ... ; fieldNameN : tN }

A record can be constructed using a record expression. A record is constructed by a pair of curly braces enclosing field names and field initializing expressions.

The general structure of a record expression is as follows:

{ fieldName1=expr1; fieldName2=expr2; ... ; fieldNameN=exprN }

Note that the fields do not have to appear in any particular order.

Beginning in Juniper 3.0, the dot notation may be used once more.

To access a particular field of a record, use a dot followed by the field name.


The type of a record is structurally based on the field names and types of fields of the record being created.

Records may be packed, in which case the order of the fields are important. Internally, a record which is packed is represented as a C/C++ packed struct. This makes a packed record useful for networking/communication situations, in which the field/byte ordering is important. To use a packed record, use the packed keyword before the record type and record expression.

The alias keyword is very useful when working with record types. See the section on type aliases below.

Algebraic Data Types

Algebraic data types in Juniper are variant types representing a tagged union. Each named variant has its own value constructor, which is just a function whose return type is the name of the algebraic data type.

Algebraic data types can be declared with the type keyword, followed by the type name and an optional template. This is followed by an equal sign and a pipe separated list of value constructors. Each value constructor has a name, followed by parenthesis and the argument types.

The general structure of an algebraic data type declaration is:

type typeName<template> = valCon1(t11, t12, ...) | valCon2(t21, t22, ...) | ... | valConN(tN1, tN2, ...)

The type of each value constructor is defined by the type of the arguments. Each value constructor may have 0 or more types as arguments. The closure type of a value constructor is empty.

Here is an example of an algebraic data type representing shapes. The circle value constructor takes in a tuple representing the position of the center of the circle and its radius. The rectangle value constructor takes in a tuple representing the position of the top left corner of the rectangle and its width and height.

type shape = circle(float, float, float) | rectangle(float, float, float, float)

circle is a function with the following type:

(||)(float, float, float) -> shape

rectangle is a function with the following type:

(||)(float, float, float, float) -> shape

Type aliases

Type aliases can be written using the alias keyword, followed by the alias name, an optional template, equal sign and a type expression. Type aliases in particular are very useful for record types, where we wish to assign a name to a particular record type (recall that record types are structural types).

alias name<template> = typeExpression

Closure Types and Function Types

Beginning in Juniper 3.0, the closure of a function is stored in the function's type. This allows closures in Juniper to be completely stack allocated, and represents Juniper's solution to the funarg problem. Duly note that closure types can be used anywhere a type expression is accepted. A closure type is very similar to a record type, except that pipes "|" are used to enclose the type instead of curly braces. The field names in the closure type are the variable names of variables being captured, and the types are the types of these variables. Like other types in Juniper, closures can be automatically inferred by the type checking engine.

|varName1 : varTy1; ...; varNameN : varTyN|

The closure of a function appears in function types. The closure should be enclosed by parentheses before the argument type list. The syntax for a function type is:

(closureTy)(argTy0, ... argTyN) -> retTy

Note that top level functions and value constructors have empty closures "||".


A sequence is a list of expressions enclosed in parentheses and separated by semicolons. The return value of a sequence is the value returned by the last expression in the sequence. The return type of the sequence is the value returned by the last expression. For example, the following sequence is of type int32 and returns a value of 2.

(true; 2)

The general structure of a sequence is:

(expr1; expr2; ...; exprN)

Sequences are often used when binding a variable using a let expression, which are discussed in the next section.

Let Expressions

A let expression is used to bind a variable for later use. A let expression begins with the keyword let, followed by the variable name, and optionally a colon and the type of the variable. This is followed by an equals sign and an expression whose value will be assigned to the variable. Consider the following example:

(let x = 5; let y = 3; x + y)

In this code, the variable x is bound, making it available in later expressions in the sequence. Then the variable y is bound, making it available as well. The return value of this expression is 8, and the return type is int32.

Type constraints can also be added to let expressions. Although not necessary in let expressions, a constraint may aid in catching type errors. A type constraint can be added by inserting a colon followed by the type after the variable name. Here is the same code above, but with type constraints added.

(let x : int32 = 5; let y : int32 = 3; x + y)

The general form of a simple let expression in a sequence is:

(...; let varName = expr; ...)

(...; let varName : typeConstraint = expr; ...)

In reality, let expressions are much more powerful than the simple examples presented above. See the section on pattern matching for more details.

Mutation and References

Variables can be marked as mutable by inserting the mutable keyword in front of the variable name. By default variables are immutable, which means that they cannot be changed after they are bound.

The mutable keyword is usually used in conjunction with a set expression. A set expression begins with the set keyword, followed by a left assign expression, an equal sign and then the new value. Here is a simple example of using mutation to sum an array of integers:

fun sum<;n>(arr : int32[n]) : int32 = ( let mutable sum = 0; for i : uint32 in 0 to n - 1 do set sum = sum + arr[i] end; sum )

Set expressions can mutate certain fields of a record or a certain value in an array using the record dot notation or array square bracket notation.

Here is an example of mutating the field of a point record.

(let mutable p = { x=2i32; y=3i32 }; set p.x = 5; p)

Here is another example that uses the array square bracket notation. This function will add make a copy of the input array, add one to every value in the array, and then return that array.

fun addOne<;n>(arr : int32[n]) : int32[n] = ( let mutable ret = arr; for i : uint32 in 0 to n - 1 do set ret[i] = ret[i] + 1 end; ret )

A reference is a pointer to a location in memory (on the heap). A value of type bool ref is a pointer to a location in memory, where the location in memory contains a boolean. It's similar to bool* in C/C++. A ref is like a box that can store a single value. The value inside of a reference can be accessed by placing a exclamation point before the reference expression. The contents of a reference can be updated by using the set ref keywords in a similar manner to the ordinary mutable variables above.

Here is an example of a function that will double the contents of a float ref. Notice that the return type is unit in this function.

fun double(x : float ref) : unit = ( set ref x = 2.0 * !x; () )

References can be created using the ref keyword. In this example we pass a reference to the function we defined above. The return value of this expression is 8.0.

(let x : float ref = ref 4.0 double(x); !x)

In general a reference can be created using:

ref expr

The value of a reference can be retrieved using:


The contents of the reference can be changed using:

set ref varName = expr

Internally, the memory for references are managed by a reference counting system (also called a smart pointer in the C++ world). This means that if a cycle is created among references, the memory will fail to be freed.

Var Keyword

The var keyword was added in Juniper 3.0 and allows the declaration of a variable without having to initialize it. This is very useful in certain situations in which the variable will be initialized by C++ code. The syntax of var is as follows:

var name : tyExpr

Here is a real world use of var from a Bluetooth low energy library wrapper, where we read a generic type from a Bluetooth low energy characteristic (which is a sort of shared "whiteboard" if you are not familiar with BLE):

fun readGeneric<'t>(c) = ( let characterstic(p) = c; var ret : 't; #((BLECharacteristic *) p)->read((void *) &ret, sizeof(t));#; ret )


Functions are first class entities in Juniper. Lambdas (also called anonymous functions) are used extensively in Juniper. When combined with higher order functions, they allow very powerful methods of abstraction to be used. Functions in Juniper also support closures. However, variables marked as mutable will not be mutable inside of the lambda closure.

Lambdas are declared using the fn keyword, followed by a pair of parentheses containing the arguments of the lambda, then the return type, a right arrow -> then the lambda body, then the end keyword.

Here is an example of using a lambda in conjunction with the List:foldl higher order function in order to sum a list of int32 numbers.

fun sum(lst) = List:foldl(fn (x, total) -> x + total end, 0, lst)

Here is another example of the addOne example used in the previous section.

fun addOne(lst) = List:map(fn (x) -> x + 1 end, lst)

Here is an example of using a closure in Juniper. The return value of this expressions is 42. In this example the type of myFun is (|theAnswer : uint32|)() -> int32.

(let myFun = (let theAnswer = 42u32; fn () -> theAnswer end); myFun())


Juniper includes a number of different imperative loops. The loops supported are for loops, while loops and do while loops.

The general structure of a while loop and a do while loop are respectively:

while conditionExpr do bodyExpr end

do bodyExpr while conditionExpr end

The for loops can interate either up or down. In both cases the interval iterated over by the loop is inclusive.

The general structure of a for loop is:

for indexVar : type in lowExpr to highExpr do bodyExpr end

for indexVar : type in highExpr downto lowExpr do bodyExpr end

These for loops are roughly equivalent to these C++ loops:

for (type indexVar = lowExpr; indexVar <= highExpr; i++) { bodyExpr }

for (type indexVar = highExpr; indexVar >= lowExpr; i--) { bodyExpr }

The return type of all loops is unit.

Pattern Matching

Pattern matching is a powerful feature in many functional programming languages. Pattern matching ensures that some value conforms to some form while also providing a way to deconstruct it. Pattern matching in Juniper happens in the case expression and in the let expression.

A case statement in Juniper begins with the keyword case, followed by the value to pattern match on and then the keyword of. This is followed by a list of pipe separated case clauses and the end keyword. Each case clause consists of a pattern followed by a fat arrow => then an expression to execute if the pattern matches. The underscore character _ can be used in a pattern to indicate a wildcard pattern match. Pattern matching can be used on numbers, value constructors, tuples and records.

In general the syntax is:

case valueExpr of | pattern1 => expr1 | pattern2 => expr2 ... | patternN => exprN end

let pattern = valueExpr

Here is an example of deconstructing a tuple using pattern matching and the let expression. This function returns the third element of any tuple.

fun third<'a,'b,'c>(tup : ('a * 'b * 'c)) : 'c = ( let (_, _, x) = tup; x )

Here is an equivalent function that uses a case expression:

fun third<'a,'b,'c>(tup : ('a * 'b * 'c)) : 'c = case tup of | (_, _, x) => x end

Beginning in Juniper 2.1.0, type inference can be used to simplify the example above.

fun third(tup) = ( let (_, _, x) = tup; x )

Here is an example of using pattern matching on value constructors:

type color = red() | green() | blue() fun nextColor(c : color) : color = case c of | red() => green() | green() => blue() | blue() => red() end

In the following example, a distance function is declared which gives the distance between two 2D points.

alias point = { x : float; y : float } fun distance(p1 : point, p2 : point) : float = ( let { x=x1; y=y1 } = p1; let { x=x2; y=y2 } = p2; let dx = x1 - x2; let dy = y1 - y2; Math:sqrt_((dx * dx) + (dy * dy)) )

Templates and Capacities

In Juniper, templates enable parametric polymorphism. Parametric polymorphism enables functions and types to be written which are generic over other types. Type variables are used to express this genericity. A template is declared using angle brackets. Inside is a comma separated list of type variables, where each type variable has a single quote ' (tick) in front of it. These templates can be declared just after the name of a function, algebraic data type or record.

As an example of a templated algebraic data type, here is the declaration for the maybe type, as declared in the Prelude module:

type maybe<'a> = just('a) | nothing()

Here is an example of a function which operates on the maybe type, as declared in the Maybe module. The map function takes in another function, and uses it to map the value contained in the maybe if it is not nothing().

fun map<'a,'b>(f : ('a) -> 'b, maybeVal : maybe<'a>) : maybe<'b> = case maybeVal of | just<'a>(val) => just<'b>(f(val)) | _ => nothing<'b>() end

Here is the compose function, as defined in the Prelude module:

fun compose<'a,'b,'c,'closureF, 'closureG>(f : ('closureF)('b) -> 'c, g : ('closureG)('a) -> 'b) : (|f : ('closureF)('b) -> 'c; g : ('closureG)('a) -> 'b|)('a) -> 'c = fn (x : 'a) : 'c -> f(g(x)) end

Beginning in Juniper 2.1.0, the above types can now be inferred. The above compose function could be written as:

fun compose(f, g) = fn (x) -> f(g(x)) end

Capacity expressions put compile time constraints on the sizes of data structures. In particular, the built in array type uses a capacity variable to determine the amount of space occupied in memory at compile time. Consider the following example:

let myBoolArray : bool[5] = [true, false, false, true, true]

In the above example, the number 5 is hard coded. Obviously hard coding the number will not be useful in general. To get around the need for hard coding, capacity variables are used. Capacity variables can be declared after type variables, and consists of a comma separated list of names. The capacity variables are separated from the type variables by a semicolon. For example, the list record in the Prelude is defined as:

alias list<'a; n> = { data : 'a[n]; length : uint32 }

And here is the function last, as defined in the List module. In this case the capacity variable is n.

fun last<'t;n>(lst : list<'t;n>) : 't =[lst.length - 1]

Capacity variables can even act sort of like dependent types. This flattenSafe function takes in a list of lists and returns a list of capacity m*n.

fun flattenSafe<'t;m,n>(listOfLists : list;n>) : list<'t;m*n> = ( let mutable ret = array 't[m*n] end; let mutable index : uint32 = 0; for i : uint32 in 0 to listOfLists.length - 1 do for j : uint32 in 0 to[i].length - 1 do (set ret[index] =[i].data[j]; set index = index + 1) end end; {data=ret; length=index} )

The arithmetic operations that can be used on capacity variables includes multiplication, addition, subtraction and division. Parentheses can be used to group arithmetic operators.

Arrays and Lists

An array is a series of elements of the same type that can be individually referenced by using an index as a unique identifier. In Juniper, an array also has a capacity associated with its type, which determines how much memory the array consumes.

Arrays can be created using an array literal syntax. The array literal syntax consists of a list of comma separated expressions surrounded by square brackets. The type of the expressions in the array literal must be the same. The array literal syntax is as follows:

[expr1, expr2, ..., exprn]

Which has type t[n], where t is the type of expr1, expr2, ..., exprn.

Arrays can also be created using the replication syntax, which creates an array filled with a given value. The array replication syntax consists of the array keyword, followed by the type of the array, the of keyword, the expression which determines the end value and finally the end keyword.

array t[n] of fillExpr end

There is also an unsafe array creation syntax, which should only be used in very specific circumstances. This is primarily used by the Juniper standard library and is not usually necessary when writing standard Juniper code. The following is unsafe since none of the array elements are initialized:

array t[n] end

Values of an array can be accessed using the square bracket array access notation:


There is currently no support for a list literal syntax. However, additional support for lists are planned for future releases. Currently a list is just a record type as defined in the Prelude module:

alias list<'a; n> = { data : 'a[n]; length : uint32 }

Interacting with C++ Libraries

Using existing C++ libraries in Juniper is perhaps the most tricky and error prone part of the language. Typically if you wish to use a C++ library in your Juniper project, you should begin by writing a wrapper module around the library. The quickest way to get started is to take a look at already existing wrappers. Typically wrappers all follow the same pattern, so modifying an existing wrapper should be fairly straightforward. Here are some example wrappers bundled with the Juniper distribution:

Juniper allows C++ code to be written inline wherever an expression can be written. Inline C++ code is wrapped inside of an immediately invoked function, which means it is impossible to introduce variables into the current function scope. The return value of the immediately invoked function is unit, which means that the return value of any inline C++ code is unit. Inline C++ code is written between two hashtag # symbols. Therefore inline C++ code is executed for its side effects.

#Insert your C++ code here#

Juniper performs no name mangling of variable names, type names, or function names. This means that inline C++ can safely use these entities without restriction.

Beginning in Juniper 3.0 you can also write C++ in the top level module which is very useful for declaring global variables. Note that this C++ code is placed by the Juniper compiler before any top level Juniper global variables.

In Juniper there are two different types for passing around C++ pointers. These are the pointer and rawpointer types. rawpointer simply compiles down to void * and is the easiest to use. Beginning in Juniper 3.0, the null keyword has type rawpointer and is equivalent to writing C++ nullptr. Typically rawpointers variables are marked as mutable, and then set inside a C++ code block. This acts to initialize the rawpointer variable.

The pointer type is more complex and is used for automatically managing the lifetime of a C++ object. The pointer type is roughly equivalent to a C++ shared_ptr. The lifetime is managed by a reference counting system. This means that whenever a reference to the pointer is made a counter is incremented, and decremented when a reference is lost. If the count drops to 0, a destructor function is called. To create a pointer, use the following syntax:

smartpointer(rawpointerVal, destructor)

Where rawpointerVal has type rawpointer and destructor has type (||)(rawpointer) -> unit. As the name suggests, the destructor is a function that will clean up the object if the reference count drops to 0. Typically the destructor is implemented using a C++ block, in which the rawpointer passed to the destructor is casted to the proper type and is deleted with the C++ delete keyword.

To extract the underlying rawpointer from a pointer, use the Prelude:extractptr function.

Headers can be included into Juniper compiled source code by using the include() declaration. An include declaration takes in a list of double quoted C++ header files to #include in the Juniper source code. For example, in the Neopixel wrapper library, this include declaration is used:



Type Inference

In Juniper 2.0.0 a new type inference engine was added to the language. Most type constraints can now be ommited since Juniper can automatically infer the correct type. For example, types of parameters in functions and lambdas may be ommited. Templated functions do not need to have their type expressions explicitly given since they can now be inferred.

Strings and Character Lists

In Juniper 2.1.0 strings and character lists were added to the language. Strings can be created by placing extended ASCII characters inside double quotes. Character lists can be created by placing extended ASCII characters inside single quotes. Strings are pointers to a location in the program memory space, whereas character lists are stored on the stack. If you need to print debug messages, use strings. If your program needs to manipulate text, use character lists. Character lists are just lists of type uint8.

Pipe Operator

In Juniper 2.1.0 the pipe operator was added. It functions similarly to the pipe operator in F# and Elixir. The syntax for the pipe operator is:

expr |> myFunc(arg1, arg2, ..., argN)

which is equivalent to:

myFunc(arg1, arg2, ..., argN, expr)

The pipe operator takes an expression on its left side and a function invokation on its right side. The expression on the left is placed as the last argument of the function call on the right. This is useful for chaining a sequence of transformations of a signal or a list. The pipe operator is left associative.

Constraints over Types

Beginning in Juniper 3.0, constraints over types were introduced to allow for type safe numerical operators and type safe record access operators. These constraints over types operate very similarly to single parameter built in type classes. The easiest to understand constraints over types are num (satisfied by any numerical type), int (satisfied by any integer type), and real (satisfied by the float and double types). A constraint can be explicitly added by writing it after the where keyword in the function declaration. The constraint is made by writing the type expression to be constrained, followed by a colon, then the constraint. For example, the following is the definition of the add function in the Prelude module:

fun add<'a>(numA : 'a, numB : 'a) : 'a where 'a : num = numA + numB

Record constraints are slightly more advanced. Record constraints are written by writing the type expression to be constrained, followed optionally by packed, curly braces, then field names and type expression pairs within. The purpose of the record constraint is to constrain a type to have fields of certain names. Note that the types of the fields can even refer to type variables that are not explicitly declared inside of the function's <> brackets. This is because the concrete type of a record also uniquely determines the types of its fields. For example:

fun myExample<'a>(arg : 'a) : 'b where 'a : {myField0 : 'b; myField1 : int8} = arg.myField0

In the above example, the type 'a is constrained to be a record that must have fields named myField0 of any type and myField1 of type int8. Note that 'a can have more fields than this, but these are the fields it must have at a minimum.