A quick tour of Sing

the Sing source file layout

A single sing source file makes a sing compilation unit.
Sing sources must be UFT-8 encoded and have this general structure:

  • optionally a namespace directive

  • 0 or more inclusions

  • 0 or more private or public declarations


for example:

namespace the.name.space
requires "module1/stuff", msx;
requires "module2/even_more";
// ... other requires directives if needed ...
// ... declarations, as many as you want


In the example the namespace directive tells the sing compiler to place the symbols declared in this file in the c++ namespace the::name::space.
If the directive is absent the generated code goes in the global namespace.

The requires directives (similarly to #include in C) imports symbols from another file.
The full syntax is:

requires <full_path>, <alias>;

symbols from the other file are accessible as <alias>.<symbol>
In the above example 


could be the DynaStorage type from the module1/stuff file.

If <alias> is omitted, the last part of the filename is used instead:


could be another symbol (a constant ?) from module2/even_more.

Note that the fact that a file is or is not inside a namespace doesn't change a bit the way it is accessed in Sing !
The namespace is there only to handle name conflicts at the c++ level.


All Sing declarations have the same structure

they all start with a keyword identifying the declaration type (type, const, var, class, func, ..), followed
by the declared stuff name:

type  <name> ...
let   <name> ...
var   <name> ...
fn    <name> ...
enum  <name> ...
class <name> ...
interface <name> ...

they can be preceded by the keyword 'public' to make it possible to import them with a 'require' statement.

For example:

public let a_five i32 = 5;

Is a constant which is visible from other files, has type i32 and value 5.


type, var, let declarations

[public] type <name> <type spec>; 
[public] let <name> [<type_spec>] = <initializer>; 
         var <name> [<type_spec>] [= <initializer>]; 

type is used to define an alias name for a type.
for example:

type Weight i32

means that Weight is an alias for i32, not a different type. 
It is typically used to define a shortcut for a complicated type definition like for example:

type MyCallback (a i32, b i32)*[*]f32;


let is used to define a new constant variable, i.e. a variable that can't be written and will always hold the initialization value.
Constant variables make your code more readable and safe, because in every part of your code you can know their value without the need to follow the flow of the code.
If you don't specify a type, the variable type is the same of the initialization expression.


let max_size = 1000;    // an integer variable of value 1000

var is used to define a writable variable.
If you omit the type specification, the variable type is the same of the initialization expression.
If you don't specify the initializer, the variable is initialized to a default value which is 0 for all numerics,  the empty string for strings, false for bools.

Public variables are forbidden.
var filename string;    // an empty string



Sing base types

i8, i16, i32, i64    // 2's complement, respectively 8,16,32,64 bit signed integers
u8, u16, u32, u64    // respectively 8,16,32,64 bit unsigned integers
f32, f64        // IEEE754 32 and 64 bit float point
c64, c128       // complex types, respectively made of 2xf32 and 2xf64 
string          // an UTF-8 encoded sequence of unicode characters.
bool            // can only be false or true
void            // a fake type you use to tell that a function has no return value.


numeric precision guideline:

It is too easy to make a mess using too much heterogeneous types in the code. mixing signed with unsigned or types of different precision you risk errors and fill your code with conversions.
My personal guidelines are:
Use whenever possible i32, f32, c64 types. (to invite to do so, sing literals have by default one of those types)
Use higher precision types only if there is an actual need for higher precision.
Use smaller types to save space in memory when it is worth the pain (huge arrays or classes which are destined to be placed in arrays).

Compound unnamed types

type declarations in sing are always read left to right. 
use the following constructs to build your declaration:

[n]...            // static array of n elements of type...
[]...             // as above, but the size is determined by the initializer
[*]...            // variable size vector of ytpe...
map(key_type)...  // map indexed with a key of type key_type, storing elements of type...
*...              // pointer to...
(argument_list)...// reference to function having arguments argument_list and returning...

for example:

var monster [5][*]map(string)(a i32, b i32) string;

monster is an array of 5 variable size vectors of maps which receive strings and return a reference to a
function which receives two integers and returns a string. 

you can give a name to a compoud decalaration through the type declaration:

type Insane [5][*]map(string)(a i32, b i32) string;
var monster Insane;

Named declarations


enum, interface, class are other types that can be declared with an ad-hoc declaration (more on them below).

Aggregate initializers

The initializer for a base type is a single expression, for an aggregate variable (map, vector) is a list of comma separated initializers enclosed in curly braces.


var vv [*]i32 = {1, 2, 3, 4};
var vvv[][3]i32 = {{1, 2, 3}, {4, 5, 6}};
var mm map(string)i32 = {"a", 97, "A", 65};  // note the touples of keys and values

Arrays and vectors

[n]<element_type>    // is the type of an array of n elements of type element_type
[] <element_type>    // as above, size from the initializer length 
[*]<element_type>    // is the type of a dynamic array (variable sized)

The above types are not interchangeable. You can't for example pass a dynamic array to a function which requires a fixed size one. The only exception to this rule is that you can always assign a dynamic vector with a static one if the elements are of  the same type.

You can access the values of the element of a vector through the [] operator:

candidates[top] = entered[0];

To manipulate dynamic vectors you additionally have methods you can call with the following syntax:

var numbers [*]i32;

numbers.push_back(30);    // appends an element at the end and make the vector longer of one unit
numbers.resize(100);      // makes the vector 100 elements long. Extra elements are discarded,                              // new elements are default initialized.


Here is the full list of dynamic vectors built in functions.




is a map which is indexed through elements of type key_value and contains elements of type element_type.

maps have built-in methods to add and retrieve elements:

var costs map(string)f32;

costs.insert("car", 20000.0);    // adds an element to the map
costs.get("car");                // returns 20000.0


Here is the full list of maps built in functions.


[weak] [const]*pointed_type

A pointer can be used to reference a variable, like in:

var mystuff MyType;

var stuffp = &mystuff;

In the above code, the & operator gets the address of mystuff which is then saved inside stuffp. After that mystuff can be accessed as *stuffp, like in:

*stuffp = ...

You can copy around stuffp and have multiple pointers pointing to the same variable.
If you want prevent the code from writing 'mystuff' through some of those pointers, declare them as 'const'. 

Objects pointed by a pointer, like "mystuff" are allocated on the heap. They live as long as at least a pointer points at them.

Note that this rises the problem of the loops of pointers: if a group of objects reference each-other in circle they will never be freed, because all of them will be referenced by a pointer as long as they all live.

To break pointer loops you can use weak pointers. Weak pointers don't force the variable they point to to stay alive. 

For example, if in a tree you need a pointer from a node to its ancestor, knowing the ancestor has a pointer to the child you must use a weak pointer.


The only legal operation you can do on a weak pointer is assignment. If you want to dereference a weak pointer you first need to copy it into a regular pointer. 

Function variables


They keep a reference to a function and allow to call the function through the variable:

var funref = fn1;
funref(..);    // calling fn1
funref = fn2;
funref(..);    // calling fn2


function declarations


here an exemple of function declaration:

fn theFunctionName (io a i32, out b i32, in c string = "good !!") i8 

Of each argument must be declared if it is an input or an output or both (if the programmer omits the qualifier it is assumed to be 'in'), the parameter name and type.

Arguments can have default values that take effect if such arguments are not specified in a call. If an argument has a default value, all the followings ones must have one.

The argument list is followed by the returned type (you MUST specify 'void' if there is not a return value).

IMPORTANT: all variables are passed by reference. You must think of the arguments' names as temporary alias for the actually passed variables. 

the part included between the curly braces, including the braces themselves, is a block.
Blocks may contain statements, other blocks, variables and constants declarations.

Variables and constants declared inside a block can be used from the point in which are declared to the end of the block.
Note: unlike in c++, definitions from inner blocks can't shadow variables from outer blocks or declared outside the function.

var first = 0;
    var first = 0;  // ops !! name conflict !!





a = b;

The value of a is overwritten by the value of b;
Note: the assignment is not an operator, you can't do: c = 5 + (a = b);


a += b;

this is the same as a = a + b;
and can be done with many other operators:

a += x;  // a = a + x
a -= x;  // a = a - x
a *= x;  // a = a * x
a /= x;  // a = a / x
a ^= x;  // a = a ^ x 
a %= x;  // a = a % x
a >>= x; // a = a >> x
a <<= x; // a = a << x
a &= x;  // a = a & x
a |= x;  // a = a | x

function call statement

A single function call is a valid statement.

afunctionToCall(100 : iteration, "smell", 1.0 : timeout);

Note that:


  • You can omit the rightmost parameters if they have a default.

  • You can put the argument name (the same you find in the function declaration) after the argument value, separated by a colon. This is mostly used, when arguments are literals, to document the meaning of the arguments and to be sure that you didn't invert/skip values.

  • Even if you specify the argument names, you still need to list the arguments in the same order of the declaration.





Note that those above are statements, not expressions, and ++, -- are not operators.
In other terms, you can't place ++/-- inside an expression.

Increment and decrement operations are only allowed on integer types.


swap(a, b);  


Copies the value of a in b and viceversa

Iterations: while and for

while (condition) {    

keeps executing a block as long as the boolean expression 'condition' is true.

for ([<count>,] <ref> in <aggregate>) {


Iterates through the items of a vector.

<ref> is a reference that loops through all the values in the aggregate. You can write/read to <ref> to access the iterated item.

for ([count,] <index> in <start>:<stop> [step <step>]) {

Iterates through a range of integer values from the <start> value to the <stop> value excluded with increments of <step> units.

if <step> is omitted it is assumed to be 1 if <stop> is more than <start>, else -1.


for (count, item in buffer) {

for (idx in 0:100 step 4) {

<ref>, <index> must NOT be declared before the loop. They must be fresh names !

<count> must be a fresh name or the name of the <count> portion of a previous for.

<count> is an optional counter. It always starts from 0 and increments at each loop, its scope is 
from the start of the for to the end of the block which contains the for. This is done to let the user check
count past the loop to see if it exited prematurely (due to a break statement).

<ref> and <index> have their scope inside the loop.

<count> is always of type i64.
<index> is of type i32 if <start>,<stop>,<step> fit an i32, else is an i64.


Note that inside the loop you can't access <aggregate> if not through the <ref> iterator. 
you can't modify <count> or <index>. If you change <stop> you get no effect because its value is 
sampled at the beginning of the loop.

break and continue

break and continue can be used only inside a for or while loop. When executed they modify the flow of the loop.


exits the innermost loop.


skips what remains of the current iteration of the innermost loop;

Conditionals: if, switch, typeswitch

This is the general syntax of an if statement:

if (<condition1>) {
} else if (<condition2>) {
} else if ...

} else {

  • If <contition1> is true execute the first block.

  • If <condition1> is false and <conition2> is true executes the second, etc..

  • If no one of the listed conditions is true executes the last block

  • The else block is optional and so are all the else if blocks.

  • Curly braces are NOT optional.

switch (<expression>) {
    case <value1>: 
    case <value2>: <statement1>
    case <value3>: {}
    case <value4>: {
    default: <statement2>

Is a switch construct. It executes a different statement based on the value of expression.
Note that:

  • if a case has no statement it will fall back to the next case who has one.

  • if you want a case to take no action you can use an empty block as the statement.

  • since a block is a statement, you can put a full block after the case colon.

  • the type of <expression> must be integer or an enumeration.

The typeswitch construct is used to downcast an interface to the class which is implementing it. More on this below.


    return( < expression > );

terminates the function and returns the value of the expression.
Legal types for the return expression are:

  • all the base types

  • pointers

  • function variables or addresses

  • enumerations



Sing operators with their priority and function:

group        associativity   priority    operator    function


postfix     left-to-right   0           a[]         subscript
                                        a()         function call            
                                        .           extern symbol resolution, member access,

                                                    enum case selection
prefix      right-to-left   1           +           unary plus
                                        -           unary minus
                                        *           dereference
                                        &           get address of
                                        !           logic not (acts on bool)
                                        ~           bitwise not (acts on integers)
power       left-to-right   2           **          power rise

mult.       left-to-right   3           *           multiply
                                        /           divide
                                        %           modulus
                                        &           bitwise and (logic multiply)
                                        >>          shift right (unsigned divide by power of two)
                                        <<          shift left (multiply by power of two)
sum         left-to-right   4           +           binary plus
                                        -           binary minus
                                        |           bitwise or (logic sum)
                                        ^           bitwise xor (a variant of the above)

relational  left-to-right   5           <           less than
                                        <=          less equal
                                        >           more
                                        >=          more equal
                                        ==          equal
                                        !=          different
boolean mpy left-to-right   6           &&          and

boolean add left-to-right   7           ||          or

Sing has many less priorities than c++. This help the programmer to remember the priorities and avoid cluttering the code with brackets, which can easily hide bugs.

Priorities are not arbitrary but follow reasonable criteria:

  • First math operators compute numeric values, then by comparing them we get bool values, finally bools get combined.

  • additionally unaries takes precedence over power which takes precedence over multiply which takes precedence over sum (both logic and numerics), like in school.

  • finally, postfix operators come before prefix, like in C.

Other notable facts:

  • you can use relational operators to compare any two scalars, including signed with unsigned, and the returned value is always value-preserving (even if it can be imprecise comparing floats and integers).

  • equality and inequality comparisons can be done also on two identical types except maps, weak pointers, vectors (if vector elements are not comparable), classes and interfaces.

Min and max


min and max are operators which return the minimum or maximum of two operators. They can be applied to any scalar value and they look like function calls:

min(op1, op2)
max(op1, op2)

Priority doesn't apply to them because of the brackets.

Explicit conversions


The conversion operator syntax is:

< newtype >(< expression >)

It is allowed in the following cases:

  • any number or bool to a string

  • any number or string to a number

Note that bools can not be converted to numeric values and vice versa: for that you can use relational operators and the if statement.

Priority doesn't apply to the conversion operator because it always comes with brackets.

Implicit conversions 


they are carried on only if they are super-safe: 

  • non-narrowing assignments - destination is guaranteed to represent the source value .

  • integer promotion - 16 and 8 bit integers are converted to i32 before operation to reduce the likeliness of overflows.

  • operations between a complex and a float of same precision - the float gets converted to complex.



numeric literals are by default 32 bits values:

  • integer values are i32

  • float values (have a '.' or 'e') are f32

  • imaginary values (have an 'i' or 'I' suffix) are c64


for example:


1_000                   // integer literal, you can insert _ characters to separate thousands
0xfff                   // hexadecimal integer
10.1e3                  // float literals must have an exponent or a decimal point.
100i, 0xffI, 1_015e2i   // imaginary

to get an higher precision numeric literal, enclose it in a fake conversion:

c128(100 + 3i)

Other literals are bool, strings, and null pointer (a pointer who doesn't points a valid location):

"a string literal" 
"neighbouring" "strings" // automatically concatenated, even if on different rows

true                     // bool literal
false                    // bool literal

null                     // the null pointer



Enum decalarations


An enum is a type that can assume only a discrete set of values that are identifyed by symbols:

enum StrengthClass { Weak, Medium, Tough, Ultra }

The values of the enum can be referred separating with a dot the name of the enum and that of the case:



for example I can init a variable:

var the_class = StrengthClass.Medium;

Enum types only allow assignment and comparison for equality. (this includes being an expression in a switch or being returned by a function). Additionally, enum values can be used as indices or sizes in arrays.




Class declarations


Basic declaration:

class TheClassName {
    var public_var = 100;       // same as any other variable declaration  
    fn is_ready() bool;         // same as any other function declaration 
    var private_var string;                 // same as any other variable declaration 
    fn private_fun(errmess string) void;    // same as any other function declaration

The declaration defines a new type of name "TheClassName" which is composed by the set of all the variables declared in the private and public sections of the class.
Such variables are called "member variables" and the functions declared inside the class are called "member functions".

You declare a variable of type "TheClassName" in the usual way:

var class_instance TheClassName;

and you can access the public variables of the class with the dot operator:


you can't directly access private variables. Private variables are accessible only through the member functions. The member's function bodies must be defined separately:

fn TheClassName.is_ready() bool
    // statements here

fn TheClassName.private_fun(errmess string) void
    // statements here

Inside the member function, member variables can be accessed through the "this" pointer:

this.private_var = "bang";
this.public_var = 0;

In c++ the this pointer is optional, as a result often programmers use special naming conventions to disinguish what is a member variable and what is not.

muting functions

An important aspect is that a member function which affects members variables MUST be declared muting with the "mut" keyword. The keyword must be inserted between "fn" and the function name like in:

fn mut init() void;

It is important to distinguish functions which alter / not alter the member variables because you can't access the former with a const pointer to the class instance.


var pp *TheClassName = &class_instance;
var cp const*TheClassName = &class_instance;

pp.init();  // ok
cp.init();  // ko: can't access a muting function through a const pointer


The class can delegate the execution of a public function to a local member variable if that member variable is a class too and if it has a function with the same name and signature.
The syntax is:

fn <name> by <implementing variable>;

for example:

class TheClassName {
    fn is_ready by status;
    var status StatusManager;   // an instance of a class that has an is_ready function.



When a class instance is destroyed (because no pointer is pointing it or because it exits its scope), a special public function is called whose signature must be:

fn [mut] finalize() void;

This is done to allow the class to free resources it may have acquired.

If the Sing compiler finds a function named 'finalize' but with wrong casing (es: Finalize) or in the private section it assumes the user made a mistake and emits an error. 

Other notable differences with C++

  • You can access a class variable (instance) through a pointer or through its name with the same '.' operator.

  • There is nothing like a constant member in sing. Use a private constant for that.

  • There are no static members (use private plain member variables or functions)

  • There are no constructors because their complexity overweighs the advantages. There is instead default initialization and you can also use initializers on all the base type variables.

  • There is no concrete class inheritance.



Interfaces, upcast, downcast and typeswitch

Basic facts

Interfaces are used to implement class polymorphism. They allow a set of classes that implement a same set of functions to be interchangeably used:

  • passed to a function that has an argument of type interface.

  • pointed by an interface pointer and manipulated through it.

You start declaring the functions which are present in the Interface:

interface ExampleInterface {
    fn mut eraseAll() void;
    fn getItemsCount() i32;

The syntax for the function declarations is the same as for any other class member function.

Once you have defined an interface you can declare several classes that implement the same interface:


class Implementer1 : ExampleInterface {
    // other stuff here that could be needed.


class Implementer2 : ExampleInterface {
    // other different stuff here.


Note: unlike in c++ you MUST NOT declare the functions of the interface in the implementing class.

A first way you can use the classes in a polymorphic way is having function which accept a parameter of
the same type of the interface. Such functions will accept any class type implementing the interface.


var item1 Implementer1;
var item2 Implementer2;


fn do_stuff(the_argument ExampleInterface) void


do_stuff(item1);        // ok, Implementer1 automatically converted to ExampleInterface
do_stuff(item2);        // ok, Implementer2 automatically converted to ExampleInterface


Another way is to have an interface pointer (see pp, the polymorphic pointer, below) pointing to an implementing class. 


var ptr1 = &item1;      // type: *Implementer1
var ptr2 = &item2;      // type: *Implementer2


var pp *ExampleInterface;


pp = ptr1;    // Implementer1 automatically converted to *ExampleInterface


pp = ptr2;    // Implementer2 automatically converted to *ExampleInterface

Delegation of an interface


You can delegate the implementation of an interface to a member class like in:


class DelegatingClass : ExampleInterface by implementer {
    var implementer ImplementingClass;


The member class must explicitly declare it implements the interface (it's not enough if it just implements all the interface's functions).

Multiple interfaces and interfaces hierarchy

A single class can implement many interfaces

class IDoAll : AnInterface, AnotherInterface, ... {

Also, an interface can inherit its functions from other interfaces

interface Derived : Base1, Base2 ... {

This means that objects implementing Derived, must implement all the functions from Base1 and Base2.

Upcasting and Downcasting

Upcasting is the operation of converting from an implementing class to an interface or from an inheriting interface to the base interfaces. Upcasting is automatic and implicit.

Downcasting is the opposite (from base/interface to derived/inheriting). You may need to downcast if you must do further work (in a class type dependent way) on objects which underwent a polymorphic operation.
For example you could have a bunch of classes which implement the "Message" interface (they are handled as messages). You place the messages in a queue of Message interfaces and the queue doesn't need to know which class each message belongs to. But when you extract a message from the queue you may need to know its original type.

In Sing, Typeswitch serves this purpose. The typeswitch syntax is:

    typeswitch (<reference> = <expression>) {
        case <type1>:{} 
        case <type2>:<statement>
        case <type3>: {
            // here I can use <reference> to access <expression> with the right type.
        default: <default_statement>

Typeswitch is similar to a switch statement except that:

  • the statement is selected on the base of the true type of <expression>.

  • The cases must be class types (not interfaces or values). 

  • <reference> is a new name of a variable which references the value of <expression>. the type of <reference> is different for every case and matches the case type, so you can access it in the statement with the proper type.

  • you can't fall through. if there is no statement after the case, the parser emits an error.

like for the ordinary switch:

  • if you want a case to take no action you can use an empty block as the statement

  • since a block is a statement, you can put a full block after the case colon.


if <expression> is of type interface pointer, the case labels must be class pointers, 
if <expression> is of type interface, the case labels must be classes:

    typeswitch (ref = the_argument) {
        case Implementer1:{
            // ref is an alias of a variable of type Implementer1
        case Implementer2:<statement>

    typeswitch (ref = pp) {
        case *Implementer1:{
            // ref is of type Implementer1 pointer
        case *Implementer2:<statement>






There are two styles (the same used in c++):

The double slash format, limited to a single line:

// this comment runs to the end of the line


the /* */ delimited comment. This one can run multiple lines and (unlike c++) supports nesting.


/* this one is multiline
   /* nested text */ note: comment doesn't end here !!



While sing tries to match double slash comments with a block of code, and puts that same comment in the generated c++, it just ignores everything that is in a multiline comment.

Additionally double slash comments can't be placed everywhere. To allow sing to attach them to the right piece of code, they are allowed soon before a declaration/statement or at the right of the first line of a declaration/statement.


// comment of the var declaration
var count = 0;

// a longher comment for the function

fn ....  


size /= 100;    // at the right of the statement

This comment doesn't appear in c++...

the following code is ignored



Symbol Conflicts


Theoretically, the sing compiler should always generate c++ files which compile and link nicely.
Actually this doesn't always happen because of the effect of two important choices:

  • To keep C++ readable, c++ symbols match verbatim with sing symbols (except class member variables).

  • Sing doesn't automatically create a different c++ namespace for each file, because this is unusual among c++ programmers. 


What happen is that Sing, unlike c++, has a different namespace for each compilation unit and conflicts that don't exist in sing could arise in c++.

Should errors happen during the linking phase, you can fix it easily setting a namespace for the compilation unit which is conflicting.

Additionally, other conflicts can arise which are due to the c++ pre-processor. try declaring a var of name NULL, for example !!!
These conflicts can't be solved with the namespace directive but are fortunately really rare (not more frequent than in c++) and force you to chang your symbol. 


Source formatting and naming conventions


Not a very important part and not really part of the language, but since uniformity is a value I make here
a proposal for all the Sing programmers:


Indentation style

K&R style.

You can see it in action on the previous chapters. In short:
The opening { is at the end of the preceding row.
The opening { of the body of a function and the } have a row on their own and are not indented.

Names style:


lowercase with _ separator applies to:

  •    filenames

  •    namespace_names

  •    variable_names (var defined symbols, class member variables, function arguments, for iterators)

  •    constant_names (let defined symbols, enum cases)


camelcase starting uppercase:        
   UserDefinedTypes (type defined symbols, classes, interfaces, enums) 

camelcase starting lowercase:
   functionNames (including class member functions)