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.
arg | tuple of arguments to current function |
Break | remove type tag from data |
cbuf | ring buffer, returns buffer content |
Let | bind a symbol dynamically |
Make | attach type tag to data |
Package | Declaring a namespace |
rbuf | ring buffer, returns overwritten slot |
rcbuf | ring buffer, returns overwritten and buffer |
rcsbuf | ring buffer, returns overwritten, index and buffer |
Type | Declaring a type tag |
Use | Search for symbols in package |
When | explicit overload resolution rule |
z-1 | Unit delay |
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.
Example | Suffix | Description |
5 | 5 as a 32-bit floating point number | |
3i | i | 3 as a 32-bit integer |
3.1415d | d | 3.1415 as a 64-bit floating point number |
9q | q | 9 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.
Escape sequence | Meaning |
\n | newline |
\t | tabulator |
\r | carriage 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.
Operator | Function | Description |
/ | :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.
Operator | Arguments | Returns |
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}.
Operator | Arguments | Returns | Description |
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 |