Find file
Fetching contributors…
Cannot retrieve contributors at this time
633 lines (469 sloc) 17.6 KB

Sparkling Tutorial


Just like C - but unlike JavaScript -, Sparkling requires every simple statement (expression statements, return, break, continue and variable declarations) to end with a semicolon. Compound statements (blocks) and certain structured statements (if, while and for) need not end with a semi-colon. A semicolon in itself, without a preceding statement, denotes an empty statement that does nothing.


There are two types of comments in Sparkling: one-line comments:

// this is a one-line comment

# this is another type of one-line comment

and block comments:

/* block comments can
   span several lines


This is a list of all reserved words (keywords and names corresponding to other tokens):

  • and
  • break
  • continue
  • do
  • else
  • extern
  • false
  • fn
  • for
  • if
  • let
  • nil
  • not
  • or
  • return
  • true
  • while


All variables need to be declared with let before they can be used. Variables can be initialized when declared. For example:

let i = 10;
let j;

You can combine multiple declarations by separating them with commas:

let i, j;
let foo = "bar", bar = "quirk";

An identifier that is undeclared is assumed to refer to a global constant. It is not possible to assign to globals (for safety reasons), but it is possible to retrieve their value. Trying to access an undefined global or a global with the value nil results in a runtime error.

Variable names and other identifiers can begin with a lowercase or capital English letter (a...z, A...Z) or with an underscore (_), then any of a...z, A...Z, 0...9, or _ follows.

Global Constants

Constants can be declared and initialized at file scope only, using the extern keyword. It is obligatory to initialize a constant. It is also obligatory for the initializer expression to be non-nil (initializing a constant to nil will result in a runtime error).

extern E_SQUARED = exp(2);

extern my_number = 1 + 2, my_string = "foobar";

extern myLibrary = {
    "foo": fn {

Constants are globally available once declared.


You can write integers or floats in the same manner as in C:

  • 10 (decimal integer)
  • 0x3f (hexadecimal integer)
  • 0o755 (octal integer)
  • 0b01001011 (binary integer)
  • 2.0 (decimal floating-point)
  • 1.1e-2 (decimal floating-point in scientific notation)

The special values M_NAN and M_INF can be found in the standard maths library, and they represent the floating-point NaN and positive infinity.


String literals are enclosed in double quotes. You can access the individual characters of a string in the same way you would access array members, i. e. using the [] operator. Characters are represented by integers (by their character code). Indexing starts from zero.

> print("hello"[1]);

To get the number of bytes in a string, use the length property:

> let str = "hello"; print(str.length);

Character literals are enclosed between apostrophes: 'a'

This is a list of escape sequences (they are the same as in C) that can be used in string and character literals:

\\      ->      \
\/      ->      /
\'      ->      '
\"      ->      "
\a      ->      bell
\b      ->      backspace
\f      ->      form feed
\n      ->      LF
\r      ->      CR
\t      ->      TAB
\xHH    ->      the character with code HH, where HH denotes
two hexadecimal digits


To create an array you can write array literals:

let empty = [];
let primes = [ 2, 3, 5 ];

You can create an array containing different types of values. Array indexing starts with zero.

> print([ 1, 2, 3, "hello" ][3]);

It is also possible to modify an element in the array by assigning to it:

> let a = ["baz"];
> a[0] = "bar";
> print(a[0]);

To get the number of elements in an array, use the length property – similarly to strings.

let arr = [ "foo", "bar", "baz" ];

Prints 3.

To add an element to the array, use push():

> let a = [];
> a.push(1);
> a.push(2);
> a
= [

Alternatively, assign to the one-past-last element of the array:

> let a = [];
> a[0] = 1;
> a[a.length] = 2;
> a
= [

To remove the last element of a non-empty array, call pop().

You can remove an element from the middle of an array by calling its erase method with the appropriate index:

> let a = [ "foo", "bar", "baz" ];
> a.erase(1);
> print(a);


Hashmaps are associative containers (key-value pairs). Keys can be of any type except nil and the NaN floating-point value. Values can be of any type and value.

To create a hashmap, use hashmap literals. Keys and values are separated by a colon:

let words = { "cheese": "fromage", "apple": "pomme" };

It is even possible to intermix multiple types:

let mixed = { 0: "foo", "bar": "baz", 2: "quirk", "lol": 1337 };

Here, "foo" will have the key 0, "baz" corresponds to "bar", "quirk" to 2, and 1337 to "lol".

Hashmap keys and values can be arbitrarily complex expressions (as long as the keys obey the "not nil or NaN" rule). However, if a key is a single identifier ("word", "name"), then it's replaced by a string literal of which the value is the identifier itself. This aims to make the definition of objects using hashmap literals easier to read.

The following code:

let hm = {
    foo: "hello",
    bar: 42

is thus the same as

let hm = {
    "foo": "hello",
    "bar": 42

By the way, this is one idiomatic way of implementing and using modules or libraries in Sparkling: one assigns functions as members to a global hashmap and accesses them using the bracket notation.

If you really want to use the value of a variable as a key in a hashmap literal, you can parenthesize it (or, for numbers, prefix it with +):

let str = "the key";
let num = 1337;

let hm = {
    (str): "a value",
    +num: "yet another"

Generally though, this shouldn't be needed very frequently.

Use the keys and values methods of hashmaps to retrieve an array of all keys and all values, respectively. The following code snippet:

let a = { "foo": "bar", "baz": "quirk" };

may output this:


Objects, methods, properties

Sparkling contains syntactic (and somewhat semantic) sugar for treating certain values as objects. Some of the built-in types as well as user-defined values can be used in an object-oriented manner. More specifically:

  • Strings, arrays and functions have built-in properties and a "class descriptor" which contains functions. These functions can be used as methods and/or property accessors on the aforementioned values when called with the obj.method() or syntax.

  • Hashmaps also have this kind of class descriptor, but they are treated differently. When a method or a property accessor is called on a hashmap, it's first searched for in the hashmap itself (recursively, through the "super" chain). And only if it is not found there, will the lookup mechanism revert to searching in the default class descriptor of hashmaps.

  • User info values can be added to the global class descriptor using the C API, on a per-instance basis. There's no default class for them.

Classes and objects can inherit from one another. If a method or property cannot be found on a particular object, then its ancestors are searched recursively, by means of the "super" key:

let superObj = {
    "foo": fn (self, n) {
        print("n = ", n);

let other = {
    "super": superObj,
    "bar": fn (self, k) {
        print("k = ", k);

k = 42
n = 1337

Property accessors follow a special structure. A snippet of code is worth a thousand words:

let anObject = {
    "awesumProperty": {
        "get": fn (self /*, name */) {
            return self["backingMember"];
        "set": fn (self, newValue /*, name */) {
            self["backingMember"] = newValue;

With this setup, the expression


will call the getter (and yield its return value), whereas the assignment

anObject.awesumProperty = 1337;

will call the setter with the value 1337. NB: the property setter syntax ignores the return value of the setter and always yields the right-hand-side of the assignment. (Given the usual semantics of the assignment statement, you shouldn't expect anything else anyway.)

It is strongly recommended that your getter methods do not modify state or otherwise perform side effects. People expect getters to be pure functions.

If a getter or setter method – in the structure described above – is not found on an object (nor anywhere in its ancestor chain), and if the object is a hashmap itself, then raw hashmap indexing (with the property name as a string key) will take place instead. (Beware: this means that you have to implement both accessor functions for read-only properties as well, else the accessor structure will be overwritten upon an accidental assignment.)


This is a short list of the most important operators:

  • ++ - pre- and post increment
  • -- - pre- and post decrement
  • + - unary plus and addition
  • - - unary minus and subtraction
  • * - multiplication
  • / - division (truncates when applied to integers)
  • .. - string concatenation
  • = - assignment
  • +=, -=, *=, /=, ..=, &=, |=, ^=, <<=, >>= - compound assignments
  • ?: - conditional operator
  • .: property access (calls getter or setter if exists)
  • ::: raw member access by name: foo::bar desugars to foo["bar"] and it doesn't call getters or setters.
  • &&, ||: logical AND and OR
  • ==, !=, <=, >=, <, >: comparison operators
  • &, |, ^: bitwise AND, OR and XOR
  • <<, >>: bitwise left and right shift


Unlike in C (and similarly to Python), you don't need to wrap loop conditions inside parentheses. Loops work in the same manner as those in C.

for loop:

for let i = 0; i < 10; ++i { // the scope of i is the loop only

while loop:

while i < x {

do...while loop:

let i = 0;
do {
} while i < 10;

The if statement

You don't need to wrap the condition of the if statement in parentheses either. However, it is obligatory to use the curly braces around the body of the if and else statements:

if 0 == 0 {
} else {
    print("not equal");

In order to implement multi-way branching, use else if:

if i == 0 {
} else if i > 0 {
    print("greater then zero");
} else {
    print("less then zero");

In the condition of a loop or an if statement, you can only use boolean values. Trying to use an expression of any other type will cause a runtime error.


You can create named and anonymous functions with the fn keyword. If you initialize a local variable or a global constant with a function expression, then the function name will be deduced from the name of the variable or constant:

let square = fn (x) {
    return x * x;

extern f = fn (x) {
    return x + 1;

Similarly, if you use a function expression to initialize a value in a hash table where the key is a string literal, the function name will be deduced from the key:

let object = {
    "foo": fn (self) {
        print("hello world");

Parentheses around function parameters are optional; if they are omitted, then parameter names are not separated by commas; otherwise, they are:

let multiple_params_1 = fn (a, b) {
    return a + b;

let multiple_params_2 = fn x y {
    return x * y;

let no_params_1 = fn () {
    print("Look, I have no params");

let no_params_2 = fn {
    print("Me neither");

If you don't explicitly return anything from a function, it will implicitly return nil. The same applies to the entire translation unit itself (since it is represented by a function too).

If a function body consists of only one "return" statement, you can use the -> syntactic sugar to define the function body more concisely. The following two definitions:

fn x y -> x + y

fn (x, y) -> x + y

are both syntactically equivalent with

fn (x, y) { return x + y; }

Consequently, fn {} is the same as fn -> nil.

Naturally, such "one-expression" functions can also be declared without explicit arguments; in this case, you can refer to the arguments using the $ argument array. This can be useful when writing short helper functions:

let fortyFive = range(10).reduce(0, fn -> $[0] + $[1]);

Function statements enable you to quickly and cleanly define functions bound to local variables. The function statement

fn add(x, y) {
    return x + y;

desugars to:

let add = fn (x, y) {
    return x + y;

A function declared using a function statement must always have a name.

Function statements can occur in any scope, not just at file scope (top level). Please note that a statement beginning with the fn keyword will always be parsed as a function statement (rather than a function expression).

To invoke a function, use the () operator:

> print(square(10));

To get the number of arguments with which a function has been called, use $.length:

> extern bar = fn () { print($.length); }
> bar();
> bar("foo");
> bar("baz", "quirk", nil);

To access the variadic (unnamed) arguments of a function, use the $ array, which contains all the call arguments of the function. This array is also referred to as the "argument vector" or simply argv.

The standard library

A detailed description of the standard library functions and global constants can be found in doc/

Getting started with the C API

Typically, you access the Sparkling engine using the Context API. It's quite straightforward to use. First, create a new Sparkling context object:

SpnContext ctx;

Then you may take different approaches. If you only want to run a program once, then use spn_ctx_execstring() or spn_ctx_execsrcfile(). These are convenience wrappers around other functions that parse, compile and run the given string or source file in one go. They return 0 on success and nonzero on error.

If a program has run successfully, then its return value will be in retVal. You must relinquish ownership of this value if you no longer need it by calling spn_value_release() on it (since SpnValues are reference counted).

SpnValue retval;
if (spn_ctx_execstring(&ctx, "return \"Hello world!\";", &retval) == 0) {
    /* show return value */
    printf("Return value: ");

    /* then dispose of it */

If an error occurs, then an error message is available by calling the spn_ctx_geterrmsg() function (the returned pointer is only valid as long as you do not run another program in the context structure, so copy the string if you need it later!). You can also request a stack trace if the error was a runtime error by calling the spn_ctx_stacktrace() function.

else {
    fputs(spn_ctx_geterrmsg(&ctx), stderr);

    if (spn_ctx_geterrtype(&ctx) == SPN_ERROR_RUNTIME) {
        size_t i, n;

        SpnStackFrame *bt = spn_ctx_stacktrace(&ctx, &n);

        for (i = 0; i < n; i++) {
            printf("frame %zu: %s\n", i, bt[i].function->name);


The type of the last error is provided by spn_ctx_geterrtype().

It is also possible that you want to run a program multiple times. Then, for performance reasons, you may want to avoid parsing and compiling it repeatedly. In that case, you can use spn_ctx_compile_string() and spn_ctx_compile_srcfile() for parsing and compiling the source once. Once compiled, you can run the resulting code with the help of the spn_ctx_callfunc() function.

SpnFunction *main_func = spn_ctx_compile_string(&ctx, "print(42);")
if (main_func == NULL) {
    /* handle parser or syntax error */
} else {
    /* 'main_func' contains a function describing the main program */
    int i;
    for (i = 0; i < 1000000; i++) { /* run the program a lot of times */
        SpnValue retval;
        if (spn_ctx_callfunc(&ctx, main_func, &retval, 0, NULL) == 0) {
            /* optionally use return value, then release it */
        } else {
            /* handle runtime error */

If you don't want to run a full program, you can just compile a single expression using spn_ctx_compile_expr() and call the returned function with spn_ctx_callfunc(), as described above.

When you no longer need access to the Sparkling engine, you must free the context object in order to reclaim all resources:


Advanced C API concepts

You can do even better using the Context API. You can extend a context with libraries/modules/packages, call Sparkling functions from within a native extension function, and you can even run a Sparkling function as if it was the main program. For information on these features, please consult the C API reference in