This manual is the primary reference for the Riptide programming language and defines its core syntax and semantics. The intended audience is for developers working on the language itself and for curious users who wish to dig deeper into the language. This document is not a complete formal specification at this time.
If you are just getting started with Riptide, we recommend checking out the Guide first.
The Riptide syntax describes how to read the source code of a valid Riptide program into valid structures.
Riptide programs are always written as a sequence of UTF-8 characters.
Horizontal whitespace has no meaning, except when used as a separator. When whitespace is used to separate syntactic elements, any one or more combination of horizontal whitespace counts as one separator.
Line separators are treated just like horizontal whitespace, except inside blocks. For greater cross-platform support, a newline can be represented in any of three ways: line feed (\n
), carriage return (\r
), or carriage return followed by a line feed (\r\n
).
Single line comments begin with a hash character (#
) and continue until the end of the line. Multiline comments begin with ###
and end with ###
. Nesting multiline comments is allowed, but the comment markers must be balanced.
Comments are ignored by the parser and are otherwise treated as whitespace.
A string literal is a sequence of any Unicode characters enclosed within two single-quote characters.
println 'I am a string literal.'
A table literal is an expression used to construct a table with entries defined in code.
[
foo: 'bar'
say-hello: {
println 'hello'
}
]
A block is a special section of code whose execution is deferred until at a later point in a program. A block is also a means of executing multiple statements in sequential order.
A block is defined using curly braces ({
and }
) and includes all code between the opening and closing braces.
{
println "I am in a block."
}
Inside a block is a list of statements, which are each pipelines to be executed. Statements may be separated by newlines or optionally by the statement terminator, a single semicolon (;
). Both separators are equivalent.
{
println "Statement one."
println "Statement two."; println "Statement three."
}
Below is the full specification of the Riptide grammar. This is the actual specification used to generate the language parser from.
link:../syntax/src/grammar.pest[role=include]
The grammar is written in the Pest syntax, an excellent modern parser generator. Reading through the Pest book to get a thorough understanding of how the Riptide grammar works.
Riptide has a simple data model and only offers a few basic data types.
The string is the most fundamental data type in Riptide. A string is a fixed-length array of bytes. Usually it contains text encoded using the UTF-8 encoding, but non-text strings are also valid.
String are immutable values; functions that combine or modify text return new strings rather than modify existing strings.
Since strings are immutable, it is implementation-defined as to whether strings are passed by value or by reference, as it makes no difference in program behavior.
Strings can be created without quotes, single quotes ('
), or double quotes ("
), each with a slightly different meaning.
Only one number type os offered. All numbers are double-precision floating-point numbers.
Numbers are immutable values.
Lists are the first compound data type, or a container type that can hold multiple other data items. A list is a fixed-length array containing zero or more values in sequence. The contained values can be of any data type.
Lists are immutable values, and cannot be modified once they are created.
It is implementation-defined as to whether lists are passed by value or by reference, as it makes no difference in program behavior.
A table (or associative array) is a collection of key-value pairs, where each key appears at most once in the collection.
Unlike other data types, tables are mutable and can be modified in place.
Tables are passed by reference instead of by value.
The storage representation of a table is implementation-defined.
Riptide is an expression based language, where nearly every construct is an expression, and is the most important building block of Riptide.
Every expression has a resulting value when it is executed.
A literal expression consists of a single literal value. The resulting value for a literal expression is always the the literal value written. See Literals for details.
Local variables are lexically scoped bindings of names to values, and only exist inside the function they are defined in.
Local variables are mutable in the sense that they can be redefined at any time.
Local variables can be referenced by name using the $
sigil.
Variables can be defined or reassigned using the set
builtin function:
# Bind the string "Hello world!" to the variable $foo.
set foo "Hello world!"
In contrast with local variables, which are lexically scoped, context variables are a form of global variables that offers dynamic scoping.
Context variables can be referenced by name using the @
sigil.
let @cvar = foo {
println @cvar # foo
let @cvar = bar {
println @cvar # bar
}
println @cvar # foo
}
As is common in many languages, exceptions offer a means of breaking out of regular control flow when runtime errors are encountered or other exceptional situations arise.
When the Riptide runtime encounters a recoverable error, it raises an exception that describes the error that occurred.
Note
|
Not all errors in the runtime get turned into exceptions. If an error occurs that the runtime cannot safely recover from, such as running out of memory or data corruption, the program will be aborted instead. |
Riptide programs are also free to raise their own exceptions at any time during program execution using the throw
builtin function.
Regardless of the origin of the exception, when an exception is raised, the current function call is aborted recursively in a process called stack unwinding, until the exception is caught. A raised exception may be caught by the first try
block encountered that wraps the offending code.
If a raised exception is not caught during stack unwinding before the top of the stack is reached, then the runtime will attempt to print a stack trace of the exception if possible, then abort the program.
External commands can be executed in the same way as functions are, and use the same function call mechanism.
Native data types passed to a command as arguments are coalesced into strings and then passed in as program arguments. The function call waits for the command to finish, then returns the exit code of the command as a number.
Process environment variables are exposed to a Riptide program via a environment
context variable. This variable is populated with a map of all of the current process environment variables when the runtime is initialized.
The environment
map is not linked to the process environment map after initialization; modifying the contents of the map at runtime does not update the current process’s environment. Whenever a subprocess is spawned, the subprocess’s environment is created by exporting the current value of environment
. This mimics normal environment variable support without the normal overhead required, and offers the benefits of being a regular context variable.
Example:
let @environment->FOO "bar" {
printenv
}
The current "working directory" of the current process is exposed as a special cwd
context variable. This variable is populated when the process starts from the working directory reported by the OS.
Changes to cwd
are not required to be reflected in the process working directory, but cwd
must be respected for all relative path resolution, and newly spawned processes must inherit the current value of cwd
.
As process parallelism and external commands are essential features of Riptide, defining how Riptide manages external and child processes is paramount.
The runtime acts as a form of process supervisor, and keeps track of all child processes owned by the current process. This removes much of the burden of managing processes from the programmer.
New child processes can be created in one of two ways:
-
The
spawn
builtin, which creates a new child process and executes a user-supplied block inside it in parallel with the current process. -
Calling external commands, which executes the command in a child process.
In both of these cases, newly created processes have their process IDs recorded in the global process table, which maintains a list of all child processes the runtime is aware of.
On Unix-like systems, when the process
This section of the reference describes all of the built-in functions that must be provided by the Riptide runtime for any program.
Define a new variable. Throws an exception if the variable is already defined.
def myvar "Hello, World!"
Introduces a scoped local variable binding.
def foo "bar"
let foo "baz" {
println $foo # prints "baz"
}
println $foo # prints "bar"
Assigns a new value to an existing variable. Throws an exception if the variable is not defined.
Terminate the current process, with an optional status code.
Note
|
By default, all child processes will also be terminated in as safe a manner as possible before the current process exits. Child processes that do not respond will be terminated forcefully. To bypass this behavior, pass the --orphan flag.
|
Spawn a new process and execute a given block within it. Returns the PID of the new process.
Calling spawn
will never interrupt the current fiber; the spawned fiber will not be started until at least the current fiber yields.
Execute a command, replacing the current process with the executed process.
Note
|
Like exit , exec will do its best to clean up the current process as safely as possible before replacing the current process.
|
Warning
|
This replaces the current process, which includes all fibers in the current process. |
-
The language should be simple to parse and evaluate so the interpreter can be simple, fast, and maintainable.
-
Only a few orthogonal language semantics so that the core language is easy to learn.
-
Support traditional command line syntax as the core of the language syntax (
command args
), so users can get started right away using Riptide as a shell, and then learn the language gradually afterward. -
Provide a built-in module system. Let users create their own package managers that work together automatically.
-
Low-level functionality can be scripted through C extension modules.
-
Provide built-in support for concurrency through forking processes.
-
Scripts should fail fast using exceptions with clear messages, rather than continue lumbering along, leaving the user unclear of the state of the world.
-
Provide data structures needed to create complex programs.
-
Extend the UNIX philosophy of many small programs that work together. Instead of creating functions that run inside your shell, encourage users to create their scripts as standalone files that can be run from within any shell.