A dynamically-typed, whitespace-delimited, garbage-collected language focused on simplicity, expressiveness, and extensibility via a powerful C API.
-> - lambda expression operator:: - metatable assignment operator<=, >=, <, >, ==, != - comparison operators+, -, *, / - arithmetic operators (- is also a unary negation)&, | - logical boolean operators! - logical not operator$ - string concatenation operatoras, break, catch, continue, else, export, for, foreign,
func, if, import, in, let, library, link, loop, macro,
pass, return, save, until, while, with,"" | "(.*?[^\\])"(0 | [1-9][0-9]*) \. [0-9]+0 | [1-9][0-9]*true, falsetable[a-zA-Z_][a-zA-Z0-9_]*
#[], {}, and () pairs, indentation is ignored.Rain supports 8 data types:
int - 64-bit signed integersfloat - 64-bit precision floatsstr - character pointers with an associated length. String literals are
null-terminated for compatibility with Cbool - true or falsefunc - a functionnull - the single value null (it is possible for C extensions to create
other null-typed values, but it’s not recommended!)table - the only complex data type - a hash tablecdata - indicates that this value is only manipulated by C extensions.
Usually used to store pointersAll values are copied “by value”. A called function’s parameter is a copy of
the caller’s argument. However, because the “value” of a table object is a
pointer to its hash table, mutating a table will mutate the referenced hash table.
You can introduce new variables using the let keyword:
let x = "Hello, world!"
All values are strongly typed (eg, adding a table and an integer will raise an exception), but the variables are not typed:
let x = "Hello, world!"
x = 5
Note that let is only used to introduce a new variable - once the variable
has been introduced, let is no longer required.
Functions are defined using the func keyword and a list of parameters:
let add = func(x, y)
return x + y
The return keyword exits the function and returns its expression. An empty
or missing return statement will terminate the function and return null.
A secondary shorthand definition exists for single expressions:
let add = func(x, y) -> x + y
save statementThe save statement acts as a “lazy” return - it saves a value to return,
but does not immediately terminate the function. After saving a value, empty
or missing return statements will return the saved value. Explicit return
statements will override any saved values.
let add = func(x, y)
save x + y
print("This will still be printed.")
let add2 = func(x, y)
save "Incorrect sum"
print("This will still be printed.")
return x + y # this will override the previously saved value
Functions can be called by wrapping a list of arguments in parentheses.
let sum = add(3, 4)
Creating a function in a local scope (ie, inside another function) enables it to close over all non-global variables in the outer scope(s). Variables being closed over are copied by value into a closure environment for the function.
let i = 0
let counter = func()
save i
i = i + 1
print(counter()) # 0
print(counter()) # 1 - internal variable is changed
print(i) # 0 - original variable is untouched
The closure environment of a function can be inspected and mutated from outside of the function via indexing.
TODO update example
print(counter.i) # 2 - inspect closure environment from outside
counter.i = 0 # mutate closure environment from outside
print(counter()) # 0 - external mutations are visible internally
main functionEvery Rain program must define a no-argument main function at the top level:
let main = func()
print("Hello!")
This function serves as the entry point of the program.
The return value of main is used as the program’s exit status:
false exits as 10pass statementpass is a no-op instruction:
let main = func()
pass # does nothing
if statementlet test = func(n)
if n == 0
print("n is zero")
else if n > 0
print("n is positive")
else
print("n is negative")
All values except null, false, 0, and 0.0 are “truthy” - that is, they
evaluate to true in boolean contexts like if statements. Null cdata values
will also evaluate to false.
Rain supports four types of loops: loop, while, until, and for, as well
as break and continue statements to control iteration. In addition, Rain
supports conditional variants - break if and continue if - which evaluate the
following expression and only terminate the loop or continue to the next iteration
if their expression is truthy.
loopAn infinite loop that can only be terminated via the break statement:
let n = 0
loop
n = n + 1
print(n)
break if n == 20
whileA loop that repeats while a condition is met:
let n = 0
while n < 20
n = n + 1
print(n)
untilA loop that repeats until a condition is met:
let n = 0
until n == 20
n = n + 1
print(n)
forA loop that repeatedly calls an iterator and binds its return value to a
variable. The loop is terminated when the iterator returns null.
for n in range(20)
print(n)
An iterator is a no-argument function that returns the next value in a sequence.
let range = func(n)
let i = 0
let iter = func() # this is the "iterator" - a no-arg function
if i == n
return null # terminate iteration
save i # the next value in the sequence
i = i + 1
return iter
The operators (+, -, *, /) can only operate on floats and integers.
Mixed operand types will produce a float, while two integer operands produces
an integer, including integer divison (ie, 5/2 == 2)
The boolean operators (&, |) evaluate their operands based on their
“truthiness” value. They will short-circuit if the second operand is not
necessary (ie, & short-circuits if the first argument is falsy, while |
short-circuits if the first argument is truthy). If an operator short-circuits,
its first operand is returned. If an operator does not short-circuit, its
second operand is evaluated and returned.
let a = true | print("Condition was falsy.") # shorts - does not print
let b = false & print("Condition was truthy.") # shorts - does not print
let x = null | "default" # x = "default"
The comparison operators (<=, >=, <, >) can operate on floats and
integers, coercing integers to floats when necessary. Strings are compared in
lexicographic order.
The equality operators (==, !=) strictly compare for physical equality (ie,
if two values will index into the same location in a table). This includes type
checking as well as value checking. A consequence of this is that the expressions
x <= y and x >= y are not equivalent to the expressions x < y | x == y and
x > y | x == y, respectively. Function, table, and cdata values are compared by
their value, not their contents - if two tables contain the same content but are
not the same table reference, they are not equal.
Note: this behavior may be changed in the future.
Tables are the only complex data structure available in Rain. They are implemented as unordered hash tables.
let x = table
Tables can be indexed using square brackets:
x["name"] = "Rain"
x[0] = "Zero"
Any value can be used as a key into the table. Because strings are very
frequently used as indices, you can use standard . notation:
x.name = "Rain" # equivalent to x["name"] = "Rain"
When using a value from a table, missing keys will return null.
Note: when using . notation, the name normalization rules apply! x._name is equivalent to x["name"], not x["_name"]!
Function values can be added to tables just like any other value. However, because functions stored in a table frequently want to operate on the table that contains them, there is an additional notation that can be used to implicitly pass a reference to the contained function:
let x = table
x.name = "Rain"
x.hello = func(self)
print(self.name)
x.print(x) # . requires an explicit pass of x to itself
x:print() # : adds an implicit pass of x to itself
By convention, the first parameter is named self, but this is not required.
Rain supports a prototypal model via metatables. When looking up a key in a
table, a missing key will return null. However, if the table contains a
metatable, lookup proceed to that table instead. The “metatable chain” will be
followed until the key is found or a metatable is missing. A metatable can be
added using the :: operator.
let mt = table
mt.name = "meta"
let x = table :: mt
print(x.name) # "meta" - x.name isn't found, but x.mt.name is
print(x.age) # null - x.age isn't found, neither is x.mt.age
Assignment to an index does not traverse the metatable chain:
x.name = "x"
print(x.name) # "x"
print(mt.name) # "meta"
An object’s metatable can be retrieved using the meta(val) builtin:
print(meta(x) == mt) # true
Classes and inheritance can be emulated via metatables and methods:
let rectangle = table
rectangle.init = func(self, w, h)
self.w = w
self.h = h
rectangle.area = func(self)
return self.w * self.h
let square = table :: rectangle
square.init = func(self, size)
self.w = size
self.h = size
let r = table :: rectangle
let s = table :: square
r:init(3, 4) # "init" is defined on rectangle
print(r:area()) # 12 - "area" is defined on rectangle
s:init(5) # "init" is defined on square
print(s:area()) # 25 - "area" is first defined on rectangle
You can modify the metatable chain with multiple uses of the :: operator.
The :: operator is right-associative.
let a = table # a
let b = table :: a # b -> a
let c = table :: a # c -> a
let d = table :: c :: b :: a # d -> c -> b -> a
Rain has no formal “arrays” - it mimics them by using tables with keys
from 0 to n:
let arr = table
arr[0] = "Zero"
arr[1] = "One"
arr[2] = "Two"
A shorthand syntax is available for defining arrays:
let arr = ["Zero", "One", "Two"]
A shorthand syntax is also available for defining dictionaries:
let rain = {name = "Rain", type = "Language"}
By default, the key of a dictionary item is interpreted the same as
. notation would be - that includes name normalization. To use non-names as
the keys, use []:
let rain = {[0] = "Zero", ["__not_normalized__"] = true}
Throw exceptions with the builtin panic(val). Standard exceptions are found
in the builtin except module.
panic(except.arg_mismatch)
By default, a panicking program will unwind the stack until it reaches main, which will catch the panic and print a message.
You can throw any value.
panic(3)
panic("Hello")
Using a ? in a function call will allow it to stop a panic and return the
recovered value instead.
let div = func(x, y)
if y == 0
panic("Division by zero")
return x/y
let a = div(10, 2) # returns 5
let b = div?(10, 0) # div will panic, but its value will be recovered
# and returned: "Division by zero"
let c = div(10, 0) # panics and unwinds the entire stack
Using a catch block, you can catch any exception that occurs inside the block
and store it in a variable. The return value of a panicking function as well
as any code after the panic is undefined.
catch err
let a = div(10, 2)
let b = div?(10, 0)
let c = div(10, 0)
let d = div(15, 3)
print(a) # 3
print(b) # "Division by zero"
print(c) # undefined - could be anything
print(d) # undefined - could be anything
print(err) # "Division by zero"
The with statement allows you to pass anonymous function blocks to
a context function.
let ctx = func(block) # the context function - receives an anonymous func
print("Initialization")
block() # use the anonymous func
print("Clean up")
with ctx
print("Anonymous block")
The with..as statement allows the anonymous block to accept arguments from
the context function.
let ctx2 = func(block)
print("Initialization")
block("Port", "Starboard")
print("Clean up")
with ctx2 as left, right
print("The left side is: " $ left)
print("The right side is: " $ right)
Code can be put in separate files, called “modules”, and then reused inside other modules.
export statementIn order for a variable to be visible to other modules, it must be declared
with the export statement instead of the let statement:
export name = "math"
export pi = 3.1415926535
export statements can only be used in the global scope.
import statementOther modules can be used with the import statement:
import math
let main = func()
print(math.name) # "math"
print(math.pi) # 3.1415926535
Imported modules are just tables that are populated with the exported values of a module.
The module can be renamed using the import..as statement:
import math as m
Relative paths can also be used:
import "../math"
When importing module XYZ, several paths are searched for a file matching
XYZ.rn, XYZ, and XYZ/_pkg.rn. The first file found is used for the
module. The default paths searched are the current module’s base directory, the
current working directory and any paths found in $RAINPATH. $RAINBASE and
$RAINLIB are also searched.
Imported modules are never exported unless explicitly exported:
import math as math_private
export math = math_private
import statements can only be used in the global scope.
init FunctionBefore any main() function is called, if any imported modules have an
init() function, it will be called. init functions are called in the order
of import - that is, when compiling a program, the order modules are listed as
being built by the compiler.
Several modules can be combined together into a package by placing them in
the same directory and adding a file called _pkg.rn to the directory. The
_pkg.rn can be imported with the directory name, while other modules will
need to be imported as a path.
Typically, a module’s name is the normalized version with the .rn suffix removed:
my_file.rn -> myfile
To avoid name conflicts with other modules, package modules are prefixed with the package name.
base/_pkg.rn -> base
base/rand.rn -> base.rand
base/test/_pkg.rn -> base.test
base/test/extra.rn -> base.test.extra
Because modules are tables, they can be used as metatables / classes.
import rand
let rng = table :: rand # `rng` acts as a new "instance" of `rand`
rng:seed(0)
rand:seed(1)
print(rng:next())
print(rand:next())
Rain supports AST-rewriting macros evaluated at compile-time. At this time, while fully functional, macros are still fairly experimental and under heavy development.
Things that come with Rain. Usually.
Found in the core/_pkg.rn file.
print(val) - print a value to stdoutexit(val) - abort the programpanic(val) - raise an exception and unwind the stacktype(val) - return an integer corresponding to a value’s typeto_str(val) - return a string representation of a valueenv - the core.env moduleexcept - the core.except modulecore packageThings Rain needs to function correctly.
core.envThe execution environment.
args - command line arguments passed to the programget(name) - return an environment variablecore.exceptStandard exceptions.
error - the base metatable for all exceptionsarg_mismatch - raised when a function is called with incorrect
argumentsuncallable - raised when a value that is not a function is calledcore.astUsed in macros.
core.typesContains type helpers and base type metatables.
core.opsContains the functions used by Rain’s operators.
base packageThings that Rain likes to have.
base.macrosSome helper macros.
One of the primary goals of Rain is to be easily extensible with C. By exposing the runtime as a public API and allowing Rain modules to explicitly “import” foreign libraries, writing C extensions becomes a trivial task.
Note: the exposed API / extension system is subject to change.
link statementAdds a file that the Rain compiler should link into the final executable. The
search paths are the same as the import statement.
link "myfile.c"
library statementAdd a library that the compiler should link into the final executable. The
search paths are defined by your system configuration; libraries are passed to
clang via the -l flag.
library "m" # link with libm
library "pcre" # link with libpcre
foreign functionsReturn a function reference to an external function. The parameter names are currently ignored, but the number of them is significant. The function name can be a name or a string literal.
# get a reference to `void rain_add(box *ret, box *lhs, box *rhs)`
let add = foreign "rain_add"(lhs, rhs)
foreign statementExposes a global value that can be shared between Rain and extensions.
// core/except/except.h
box *rain_exc_arg_mismatch;
And:
# core/except/_pkg.rn
export arg_mismatch = table :: error
foreign "rain_exc_arg_mismatch" = arg_mismatch
Rain’s primary data structure is a “box” that contains the runtime type, value, length (if applicable), and a pointer to their metatable. Formally:
box {
unsigned char type;
int size;
cast data;
box* meta;
}
Where cast is an untagged union that provides access to the various
value types without conversion:
union {
unsigned long ui;
signed long si;
double f;
char *s;
struct table_s *lpt;
void *vp;
}
Rain provides several helper functions to set the type, metatable, and value of a box:
void rain_set_box(box *dest, box *src);
void rain_set_null(box *);
void rain_set_int(box *, signed long);
void rain_set_float(box *, double);
void rain_set_bool(box *, unsigned char);
void rain_set_str(box *, char *);
void rain_set_strcpy(box *, const char *, int);
void rain_set_table(box *);
void rain_set_func(box *, void *, int);
void rain_set_cdata(box *, void *);
void rain_set_env(box *, box *);
The exported name can be a name or a string literal.
Define your own function:
// mod.c
#include "rain.h"
void rain_ext_mod(box *ret, box *lhs, box *rhs) {
if(BOX_IS(lhs, INT) && BOX_IS(rhs, INT)) {
rain_set_int(ret, lhs->data.si % rhs->data.si);
}
rain_panic(rain_exc_arg_mismatch);
}
Link with the extension file in your Rain code and declare a foreign:
# mod.rn
link "mod.c"
export mod = foreign "rain_ext_mod"(lhs, rhs)
let main = func()
print(mod(5, 3))
print(mod(13, 3))