Syntax Reference

This chapter is intended to enumerate and explain the primitive syntactic constructs in the Kronos language, as well as to cover the functions supplied with the compiler in source form. It is adapted and updated from Vesa's doctoral thesis.

Identifiers and Reserved Words

A token is the primitive entity of the language. It roughly corresponds to an unit of meaning, such as a word in a natural language. However, tokens can be groups of other tokens, and this is very often the case.

An identifier is a name for either a function or a symbol. Kronos identifiers may contain alphabetical characters, numbers and certain punctuation. The first character of an identifier must not be a digit. In most cases it should be an alphabetical character. Identifiers beginning with punctuation are treated as infix functions.

Identifiers are delimited by whitespace, commas or parentheses of any kind. Please note that punctuation does not delimit symbols; as such, a+b is a single symbol, rather than three.

Identifiers may be either defined in the source code or be reserved for specific purpose by the language.

argtuple of arguments to current function
Breakremove type tag from data
cbufring buffer, returns buffer content
Letbind a symbol dynamically
Makeattach type tag to data
PackageDeclaring a namespace
rbufring buffer, returns overwritten slot
rcbufring buffer, returns overwritten and buffer
rcsbufring buffer, returns overwritten, index and buffer
TypeDeclaring a type tag
UseSearch for symbols in package
Whenexplicit overload resolution rule
z-1Unit delay
Reserved Words

Constants and Literals

Number types

Constants are numeric values in the program source. The standard decimal number is interpreted as a 32-bit floating point number. Different number types can be specified with suffixes.

Typed Number Literals
ExampleSuffixDescription
55 as a 32-bit floating point number
3ii3 as a 32-bit integer
3.1415dd3.1415 as a 64-bit floating point number
9qq9 as a 64-bit integer

Invariants

In addition, numeric constants can be given as invariants. This is a special number that is lifted to the type system. That is, every invariant number has a distinct type. Invariant numbers carry no runtime data. Due to type determinisim, this is the only kind of number that can be used to direct program flow. Invariants are prefixed with the hash tag, such as #2.71828.

Invariant Strings

Kronos strings are also lifted to the type system. Each unique string thus has a distinct type. This allows strings to be used to direct program flow. They do not contain runtime data. Strings are written in double quotes, such as "This is a string"

Some special characters can be encoded by escape notation.

Reserved Words
Escape sequenceMeaning
\nnewline
\ttabulator
\rcarriage return
\\single backslash

Symbols

A symbol is an identifier that refers to some other entity. Symbols are defined by equalities:

My-Number = 3 My-Function = x => x * 10

`

Subsequently, there is no difference between invoking the symbol or spelling out the expression assigned to it.

Functions

Functions can be bound to symbols via the lambda arrow:

Test = x => x + 5

Or the compound form:

Test2(x) { Test2 = x + 5 }

In the case of a monomorphic function, the return value can be shortened:

Test3(x) { x + 5 }

The compound form allows multiple definitions of the function body, and these will be treated as polymorphic. Unlike normal symbols, compound definitions of the same function from multiple source code units are merged. This allows code units to extend a function that was defined in a different unit.

The compound form can only appear inside packages and other function bodies, while the lambda arrow is an expression and thus more flexible -- it can be used to define nested functions.

My-Fold(func data) { My-Fold = data (x xs) = data My-Fold = func(x My-Fold(func xs)) } My-Fold(Add 1 2 3 4 5)15

Packages

A Package is an unit of organization that conceptually contains parts of a Kronos program. These parts can be functions or symbols. The packaging system provides a unique, globally defined way to refer to these functions or symbols.

Symbols defined in the global scope of a Kronos program reside in the root namespace of the code repository. They are visible to all scopes in all programs.

Global-Symbol = 42 Package Outer { Package Inner { Bar = Global-Symbol } Baz = Inner:Bar }

Scope

Scope is a context for symbol bindings within the program. By default, symbols are only visible to the expressions within the scope. Only a single binding to any given symbol is permitted within a scope. The compound form of a function is an exception: multiple compound forms are always merged into one, polymorphic definition.

Expressions

Expressions represent computations that have a value. Expressions consist of Symbols, Constants and Function Calls. The simplest expression is the tuple; an ordered grouping of Expressions.

Tuples

Tuples are used to bind multiple expressions to a single symbol. Tuples are denoted by parentheses, enclosing any number of Expressions. The tuple is encoded as a chain of ordered pairs.

(1 2 3 4)1 2 3 4 Pair(1 Pair(2 Pair(3 4)))1 2 3 4

Expressions within a tuple can also be tuples. Binding a symbol to multiple values via a tuple is called Structuring.

Destructuring a tuple

Destructuring is the opposite of structuring. Kronos allows a tuple of symbols on the left hand side of a binding expression. Such tuples may only contain Symbols or similar nested tuples.

my-tuple = (1 2 3 4 5) (b1 b2 b3 bs) = my-tuple First(my-tuple)1 b11 First(Rest(my-tuple))2 b22 First(Rest(Rest(my-tuple)))3 b33 Rest(Rest(Rest(my-tuple)))4 5 bs4 5

Destructuring proceeds by splitting the right hand side of the definition recursively, according to the structure of the left hand side. Each symbol on the left hand side is then bound to the corresponding part of the right hand side.

Lists

Kronos lists are tuples that end in the nil type. This results in semantics that are similar to many languages that use lists. The parser offers syntactic sugar for structuring lists.

(1 2 3 4 nil)1 2 3 4 nil [1 2 3 4]1 2 3 4 nil

Usage of nil terminators and square brackets is especially advisable when structuring extends to multiple levels. In the following listing, symbols a and b are identical, despite different use of parenthesis. Symbols c and d are not similarly ambiguous. As a rule of thumb, ambiguity is possible whenever several tuples end at the same time. This never arises in list notation, as the lists always end in nil.

((1 2) (10 20) (100 200))(1 2) (10 20) 100 200 ((1 2) (10 20) 100 200)(1 2) (10 20) 100 200 ; these are confusingly equivalent [(1 2) (10 20) (100 200)](1 2) (10 20) (100 200) nil [(1 2) (10 20) 100 200](1 2) (10 20) 100 200 nil ; these are not equivalent

Function Call

A symbol followed by a tuple, with no intervening whitespace, is considered a function call. The tuple becomes the argument of the function.

The symbols can be either local to the scope of the function call or globally defined in the code repository.

add-ten = x => x + 10 add-ten(5)15

Infix Functions

Infix functions represent an alternative function call syntax. They are used to enable standard notation for common binary functions like addition and multiplication. Kronos features only left associative binary infix functions, along with a special ternary operator for pattern matching.

Symbols that start with a punctuation character are considered infix functions by default.

The parser features a set of predefined infix functions that map to a set of binary functions. These infices also have a well defined standard operator precedence. In addition, it is possible to use arbitrary infix functions: these always have a precedence that is lower than any of the standard infices. Their internal precedence is based on the first character of the operator. They are divided into four groups based on the initial character; the rest of the characters are arbitrary. The initial characters of each group are listed in the table below.

OperatorFunctionDescription
/ :Div arithmetic division
* :Mul arithmetic multiplication
+ :Add arithmetic addition
- :Sub arithmetic substraction
== :Equal equality test
!= :Not-Equal non-equality test
> :Greater greater test
< :Less less test
>= :Greater-Equal greater or equal test
<= :Less-Equal less or equal test
& :And logical and
| :Or logical or
=> lambda arrow
*/+- custom infices group 1
?!=<> custom infices group 2
|&%$ custom infices group 3
.:~^ custom infices group 4

Unary Quote

Prepending an expression with the quote mark ' causes the expression to become an anonymous function. The undefined symbol _ within the expression is bound to the function call argument tuple. As an example, f(x) = 2^x can be written as an anonymous function; 'Math:Pow(2 _)

Anonymous Function

An expression enclosed in curly braces {} is an anonymous function. Anonymous functions can not be polymorphic, but they can refer to themselves via the Recur symbol. Any arguments are bound to arg, which the expression may destructure.

Section

Parentheses can be used to enforce partial infix expressions, which are called sections. These become partial applications of the infices. For example, (* 3) is an anonymous function that multiplies its argument by three. If one side is omitted, the anonymous function is unary. If both sides are omitted, the anonymous function is binary. This syntax is similar to Haskell.

Custom Infices

A symbol that begins with punctuation, but is not any of the predefined infices listed in Table \ref{tab:infices}, is considered a custom infix operator. It has the lowest precedence. During parsing, such an operator is converted into a function call by prepending Infix to the symbol. For example, a custom infix a +- b is converted to Infix+-(a b). Note that while the predefined infices refer to symbols in the root namespace, custom infices follow the namespace lookup rules of their enclosing scope. This allows constraining custom infix operators to situations where code is either located in or refers to a particular package, reducing the risk of accidental use and collisions.

Infixing notation

A normal function can be used as an infix function by enclosing its name in backticks. Such in situ infices always have the lowest precedence. For example, 3 + 4, Add(3 4) and 3 \lq Add\lq 4 are equivalent apart from precedence considerations.

Delays and Ring Buffers

Delays and ring buffers are operators that represent the state and memory of a signal processor. Their syntax resembles that of a function, but they are not functions -- they cannot be assigned to symbols directly. The reason for this is that these operators enable cyclic definitions. That is, the signal argument to a delay or ring buffer operator can refer to symbols that are only defined later.

There are multiple versions of delays for different common situations. They all share some characteristics, such as having two signal paths, one for initialization and the other one for feedback. The initializer path also decides the data type of the delay line.

An overview of the delay line operators is given in Table \ref{tab:delayline_ops}. The init argument is evaluated and used to initialize the delay line contents for new signal processor instances. For the higher order delays, the order argument is an invariant constant that determines the length of the delay line. The initialization value is replicated to fill out the delay line. The sig argument determines the reactivity -- clock rate -- of the delay operator. This clock rate is propagated to the output of the operator.

Most delay operators output the delayed version of the input signal. The delay amount is fixed at the order of the operator; one sample for the unit delay operator, and order for the higher order operators. Variable delays and multiplexing can be accomplished by the delay line operators that output the entire contents of the buffer, along with the index of the next write.

It is to be noted that all reads from a delay line always happen before the incoming signal overwrites delay line contents.

OperatorArgumentsReturns
z-1 (init sig) prev-sig
rbuf (init order sig) delayed-sig
cbuf (init order sig) buffer
rcbuf (init order sig) (buffer delayed-sig)
rcsbuf (init order sig) (buffer index delayed-sig)

Select and Select-Wrap

The selection operators provide variable index lookup into a homogenic tuple or list. If the source tuple is not homogenic, in that it contains elements of different types, both selection operators produce a type error. An exception is made for lists; a terminating nil type is allowed for a homogenic list, but cannot be selected by the selection operators.

Select performs bounds clamping for the index; indices less or equal to zero address the first element, while indices pointing past the end of the tuple will be constrained to the last element. Select-Wrap performs modulo arithmetic; the tuple is indexed as an infinite cyclic sequence, with the actual data specifying a single period of the cycle.

Reactive Primitives

Reactive primitives are operators that are transparent to data and values, and guide the signal clock propagation instead. All the reactive operators behave like regular functions in the Kronos language, but their functionality can't be replicated by user code.

Most primitives take one or more signals, manipulating their clocks in some way. They can be used to override the default clock resolution behavior, where higher priority signal clocks dominate lower priority signal clocks. An overview of all the primitives is given in Table \ref{tab:reactive_ops}.

OperatorArgumentsReturnsDescription
Tick (priority id) nil provides a reactive root clock with the supplied 'id' and 'priority'
Resample (sig clock) sig 'sig' now updates at the rate of 'clock' signal
Gate (sig gate) sig any updates to 'sig' will be inhibited while 'gate' is zero
Merge tuple atom outputs the most recently changed element in homogenic 'tuple'
Upsample (sig multiplier) sig output signal is updated 'multiplier' times for every update of 'sig'
Downsample (sig divider) sig output signal is updated once for every 'divider' updates of 'sig'
Rate sig rate returns the update rate of 'sig' as a floating point value