2021-11-02 15:26:19 +03:00
|
|
|
---
|
|
|
|
name: Sing
|
|
|
|
category: language
|
|
|
|
language: Sing
|
|
|
|
filename: learnsing.sing
|
|
|
|
contributors:
|
|
|
|
- ["Maurizio De Girolami", "https://github.com/mdegirolami"]
|
|
|
|
---
|
|
|
|
|
|
|
|
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.
|
|
|
|
|
|
|
|
```go
|
|
|
|
/* 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.
|
2022-12-10 18:05:34 +03:00
|
|
|
// "map(x)..." reads "map with key of type x and value of type..."
|
2021-11-02 15:26:19 +03:00
|
|
|
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
|
|
|
|
{
|
2024-08-28 16:57:24 +03:00
|
|
|
// inside a member function, members can be accessed through the
|
|
|
|
// 'this' keyword and the field selector '.'
|
2021-11-02 15:26:19 +03:00
|
|
|
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](https://mdegirolami.wixsite.com/singlang).
|
|
|
|
|
|
|
|
If you want to play with sing you are recommended to download the vscode plugin. Please
|
|
|
|
follow the instructions at [Getting Started](https://mdegirolami.wixsite.com/singlang/copy-of-interfacing-sing-and-c-2)
|