Share this page

Learn X in Y minutes

Where X=Sing

The purpose of sing is to provide a simple, safe, fast language that can be a good replacement for c++ for high performance applications.

Sing is an easy choice because it compiles to human-quality readable c++.

Because of that, if you work for a while with Sing and, at any time, you discover you don't like Sing anymore, you lose nothing of your work because you are left with nice and clean c++ code.

In some way you can also think Sing as a tool to write c++ in a way that enforces some best practices.

/* Multi- line comment. 
    /* It can be nested */ 
    Use it to remark-out part of the code.
    It leaves no trace in the intermediate c++ code. 
    (sing translates into nice human readable c++)
*/

// Single line comment, can be placed only before a statement or declaration...
// ...or at the right of the first line of a statement or declaration.
// single line comments are kept into c++.
//
// here we declare if we need to use public declarations from other files. 
// (in this case from files 'sio', 'sys')
requires "sio";
requires "sys";

//
// A sing function declaration.
// All the declarations can be made public with the 'public' keyword.
// All the declarations start with a keyword specifying the type of declaration
// (in this case fn for function) then follows the name, the arguments and the
// return type.
//
// Each argument starts with a direction qualifyer (in, out, io) which tells if
// the argument is an input, an output or both...
// ...then follows the argument name and the type.
public fn singmain(in argv [*]string) i32
{
    // print is from the sio file and sends a string to the console
    sio.print("Hello World\n");

    // type conversions are allowed in the form of <newtype>(expression).
    sio.print(string(sum(5, 10)) + "\n");

    // For clarity you can specify after an argument its name separated by ':'.
    var result i32;
    recursive_power(10:base, 3:exponent, result);

    // referred here to avoid a 'not used' error.
    learnTypes();

    // functions can only return a single value of some basic type.
    return(0);
}

// You can have as many arguments as you want, comma separated. 
// You can also omit the 'in' direction qualifyer (it is the default).
fn sum(arg1 i32, arg2 i32) i32
{
    // as 'fn' declares a function, 'let' declares a constant.
    // With constants, if you place an initializer, you can omit the type.
    let the_sum = arg1 + arg2;

    return(the_sum);
}

// Arguments are passed by reference, which means that in the function body you
// use the argument names to refer to the passed variables.
// Example: all the functions in the recursion stack access the same 'result'
// variable, supplied by the singmain function. 
fn recursive_power(base i32, exponent i32, out result i32) void
{
    if (exponent == 0) {
        result = 1;
    } else {
        recursive_power(base, exponent - 1, result);
        result *= base;
    }
}

//**********************************************************
//
// TYPES
//
//**********************************************************
fn learnTypes() void
{
    // the var keyword declares mutable variables 
    // in this case an UTF-8 encoded string
    var my_name string;

    // ints of 8..64 bits size
    var int0 i8; 
    var int1 i16; 
    var int2 i32; 
    var int3 i64; 

    // uints
    var uint0 u8;
    var uint1 u16;
    var uint2 u32;
    var uint3 u64;

    // floats
    var float0 f32;
    var float1 f64;

    // complex
    var cmplx0 c64;
    var cmplx1 c128;

    cmplx0 = 0;
    cmplx1 = 0;

    // and of course...
    var bool0 bool;

    // type inference: by default constants are i32, f32, c64
    let an_int32 = 15;
    let a_float32 = 15.0;
    let a_complex = 15.0 + 3i;
    let a_string = "Hello !";
    let a_bool = false;

    // To create constant of different types use a conversion-like syntax:
    // NOTE: this is NOT a conversion. Just a type specification
    let a_float64 = f64(5.6);

    // in a type definition [] reads as "array of"
    // in the example []i32 => array of i32.
    var intarray []i32 = {1, 2, 3};

    // You can specify a length, else the length is given by the initializer
    // the last initializer is replicated on the extra items
    var sizedarray [10]i32 = {1, 2, 3};

    // Specify * as the size to get a dynamic array (can change its length)
    var dyna_array [*]i32;

    // you can append items to a vector invoking a method-like function on it.
    dyna_array.push_back(an_int32);

    // getting the size of the array. sys.validate() is like assert in c
    sys.validate(dyna_array.size() == 1); 

    // a map that associates a number to a string. 
    // "map(x)..." reads "map with key of type x and value of type..." 
    var a_map map(string)i32;

    a_map.insert("one", 1);
    a_map.insert("two", 2);
    a_map.insert("three", 3);
    let key = "two";

    // note: the second argument of get_safe is the value to be returned 
    // when the key is not found.
    sio.print("\nAnd the value is...: " + string(a_map.get_safe(key, -1)));

    // string concatenation
    my_name = "a" + "b";
}

// an enum type can only have a value from a discrete set. 
// can't be converted to/from int !
enum Stages {first, second, last}

// you can refer to enum values (to assign/compare them)
// specifying both the typename and tagname separated with the '.' operator
var current_stage = Stages.first;


//**********************************************************
//
// POINTERS
//
//**********************************************************

// This is a factory for a dynamic vector.
// In a type declaration '*' reads 'pointer to..'
// so the return type is 'pointer to a vector of i32'
fn vectorFactory(first i32, last i32) *[*]i32
{
    var buffer [*]i32;

    // fill
    for (value in first : last) {
        buffer.push_back(value);
    }

    // The & operator returns the address of the buffer.
    // You can only use & on local variables
    // As you use & on a variable, that variable is allocated on the HEAP.
    return(&buffer);
}

fn usePointers() void
{
    var bufferptr = vectorFactory(0, 100);

    // you don't need to use the factory pattern to use pointers.
    var another_buffer [*]i32;
    var another_bufferptr = &another_buffer;

    // you can dereference a pointer with the * operator
    // sys.validate is an assertion (causes a signal if the argument is false)
    sys.validate((*bufferptr)[0] == 0);

    /* 
    // as all the pointers to a variable exit their scope the variable is
    // no more accessible and is deleted (freed)
    */
}

//**********************************************************
//
// CLASSES
//
//**********************************************************

// This is a Class. The member variables can be directly initialized here
class AClass {
public:
    var public_var = 100;       // same as any other variable declaration  
    fn is_ready() bool;         // same as any other function declaration 
    fn mut finalize() void;     // destructor (called on object deletion)
private:
    var private_var string; 

    // Changes the member variables and must be marked as 'mut' (mutable)
    fn mut private_fun(errmsg string) void;    
}

// How to declare a member function
fn AClass.is_ready() bool
{
    // inside a member function, members can be accessed through the 
    // 'this' keyword and the field selector '.'
    return(this.public_var > 10);
}

fn AClass.private_fun(errmsg string) void
{
    this.private_var = errmsg;
}

// using a class
fn useAClass() void
{
    // in this way you create a variable of type AClass.
    var instance AClass;

    // then you can access its members through the '.' operator.
    if (instance.is_ready()) {
        instance.public_var = 0;
    }
}

//**********************************************************
//
// INTERFACES
//
//**********************************************************

// You can use polymorphism in sing defining an interface...
interface ExampleInterface {
    fn mut eraseAll() void;
    fn identify_myself() void;
} 

// and then creating classes which implement the interface
// NOTE: you don't need (and cannot) re-declare the interface functions
class Implementer1 : ExampleInterface {
private:
    var to_be_erased i32 = 3;
public:    
    var only_on_impl1 = 0;
}

class Implementer2 : ExampleInterface {
private:
    var to_be_erased f32 = 3;
}

fn Implementer1.eraseAll() void
{
    this.to_be_erased = 0;
}

fn Implementer1.identify_myself() void
{
    sio.print("\nI'm the terrible int eraser !!\n");
}

fn Implementer2.eraseAll() void
{
    this.to_be_erased = 0;
}

fn Implementer2.identify_myself() void
{
    sio.print("\nI'm the terrible float eraser !!\n");
}

fn interface_casting() i32
{
    // upcasting is automatic (es: *Implementer1 to *ExampleInterface)
    var concrete Implementer1;
    var if_ptr *ExampleInterface = &concrete; 

    // you can access interface members with (guess what ?) '.'
    if_ptr.identify_myself();

    // downcasting requires a special construct 
    // (see also below the conditional structures)
    typeswitch(ref = if_ptr) {  
        case *Implementer1: return(ref.only_on_impl1);
        case *Implementer2: {}
        default: return(0);
    }

    return(1);
}

// All the loop types
fn loops() void
{
    // while: the condition must be strictly of boolean type
    var idx = 0;
    while (idx < 10) {
        ++idx;
    }

    // for in an integer range. The last value is excluded
    // 'it' is local to the loop and must not be previously declared
    for (it in 0 : 10) {
    }

    // reverse direction
    for (it in 10 : 0) {
    }

    // configurable step. The loop stops when it's >= the final value
    for (it in 0 : 100 step 3) {
    }

    // with an auxiliary counter. 
    // The counter start always at 0 and increments by one at each iteration
    for (counter, it in 3450 : 100 step -22) {
    } 

    // value assumes in turn all the values from array
    var array [*]i32 = {0, 10, 100, 1000};
    for (value in array) {
    }

    // as before with auxiliary counter
    for (counter, value in array) {
    }
}

// All the conditional structures
interface intface {}
class c0_test : intface {public: fn c0stuff() void;}
class delegating : intface {}

fn conditionals(in object intface, in objptr *intface) void
{
    let condition1 = true;
    let condition2 = true;
    let condition3 = true;
    var value = 30;

    // condition1 must be a boolean.
    if (condition1) {
        ++value;    // conditioned statement
    } 

    // you can chain conditions with else if
    if (condition1) {
        ++value;
    } else if (condition2) {
        --value;
    } 

    // a final else runs if any other condition is false
    if (condition1) {
        ++value;
    } else if (condition2) {
        --value;
    } else {
        value = 0;
    }

    // based on the switch value selects a case statement
    switch (value) {
        case 0: sio.print("value is zero"); // a single statement !
        case 1: {}                          // do nothing
        case 2:                             // falls through
        case 3: sio.print("value is more than one");
        case 4: {                           // a block is a single statement !
            value = 0;
            sio.print("how big !!");
        }
        default: return;                    // if no one else matches
    }

    // similar to a switch but selects a case based on argument type.
    // - object must be a function argument of type interface.
    // - the case types must be classes implementing the object interface.
    // - in each case statement, ref assumes the class type of that case.
    typeswitch(ref = object) {
        case c0_test: ref.c0stuff();
        case delegating: {}
        default: return;
    }

    // - object must be an interface pointer.
    // - the case types must be pointers to classes implementing the objptr interface.
    // - in each case statement, ref assumes the class pointer type of that case.
    typeswitch(ref = objptr) {
        case *c0_test: {
            ref.c0stuff();
            return;
        }
        case *delegating: {}
        default: sio.print("unknown pointer type !!");
    } 
}

Further Reading

official Sing web site.

If you want to play with sing you are recommended to download the vscode plugin. Please follow the instructions at Getting Started


Got a suggestion? A correction, perhaps? Open an Issue on the GitHub Repo, or make a pull request yourself!

Originally contributed by Maurizio De Girolami, and updated by 4 contributors.