Sugar is a compiled (transpiled) programming language I started working on a couple of years back. It initially started as a C# project. I transitioned to C++ in an attempt to grow comfortable with the language as this is my first "proper" C++ project.
I aimed to create something simple but useful. Syntactically, sugar inspired by C# but a sweeter version maybe? Personally, I think C# is a wonderful language and while sugar doesn't come close to it in regard of features, I've tried my best at implementing those aspects which I think are fundamental.
int: x;
The type of the variable is post fixed with a :
, followed by the name. Variables may be initialized and you may declare multiple variables at once.
int: x, y = 10;
Although sugar is statically typed, it does let one use the let
keyword to perform type inference.
let: x = 10; //type inference to integer
int Add(int x, int y)
{
return x + y;
}
int: result = Add(10, 20);
Function declarations and calling is similar to that in C# and C++.
Sugar offers if
conditions only as a control based structure. Sugar also offers C#/Java style ternary expression.
if (condition)
{
}
elif (condition2)
{
}
else
{
}
Sugar offers the for
, while
and do while
.
while (condition)
{ }
for(int x = 0; x < 10; x++)
{ }
do { } while(condition)
Sugar uses the print
and input
functions for output and input respectively.
print("hello world");
string: result = input();
All data types in sugar are converted to strings using the tostring
function. Sugar also provides built in string formatting using the format
method.
string: x = tostring(7);
let: y = format(x, " ate ", 9); // "7 ate 9"
These data types are built into sugar:
- Integers:
short [signed 2 bytes]
,int [signed 4 bytes]
,long [signed 8 bytes]
- Floating Point:
float [signed 7 digit precision]
,double [signed 15 digit precision]
- Characters, Booleans and Strings:
char
,bool
andstring
. Strings are immutable. - Arrays, List, Dictionaries and Tuple:
array
,list
,dictionary
andtuple
. - Funcs and Action:
func
andaction
. - Nullable:
nullable
serves a generic type to create nullable value type equivalents. - Object:
object
serves as the base object reference for any type (as in C#).
Sugar supports the collections mentioned above.
array
:
array<int>: collection = { 1, 2, 3, 4 };
collection[3] = 3;
print(collection[3]);
list
:
list<int>: collection = { 1, 2, 3, 4 };
collection.Add(5);
print(collection[4]);
dictionary
:
let: collection = create dictionary<int, string>();;
collection[10] = "ten";
print(collection[10]);
tuple
:
let: collection = create tuple<int, string>(1, "one");
print(collection[0]);
print(collection[1]);
Object creation in sugar is carried out through the
create
keyword.
Sugar offers partial delegate functionality using func
and action
.
void HelloWorld(string: message)
{
print(message);
}
action</* argument types are passed in order of declaration */ string>: helloWorld = funcref</* arguement types in order of declaration */ string>(HelloWorld);
The funcref
function is used to get the reference to a function. The argument signature is passed in using generic expression. Unfortunately, sugar does not feature the ability to create functions dynamically.
Delegates may be invoked using the invoke
function.
int Add(int x, int y)
{
return x + y;
}
func</* first arg is the return type */ int, int, int>: add = funcref<int, int>(Add);
int: result = invoke(add, 10, 20);
Sugar technically supports generics, but only for built-in types and functions as of now.
Sugar supports custom data structures: class
, struct
and enum
.
enum
: A type that can store multiple compile type constant integer values.class
: A reference type that is always created on the heap.struct
: A value type that is always created on the stack.
Sugar is garbage collected since it compiles to CIL.
Taking inspiration from C#'s attributes, which I adore, sugar has describers.
[public]
class Human
{
[public] int: age;
[public] string: name;
[public, static]
Human CreateHuman(string: name)
{
let: human = create Human();
human.name = name;
human.age = 0;
return human;
}
}
Describers can contain the following keywords:
const
: A compile time constantstatic
: Declares a member staticpublic
: An access specifier for public itemsprivate
: An access specifier for private itemsref
: Allows passing a value type by reference.
Sugar lets you customise member fields using properties. A rather basic implementation:
int: x { [public] get; [private] set; }
The above line creates a field that is accessible anywhere but only changeable inside the class.
[public] int: x; // a public get, public set field
[public] int: x { get; [private] set; } // a public get, private set field
The describer on the variable is given first precedence i.e. in absence of a describer, the accessor is given the same access level as the variable. Also in case of conflicts, the describer on the variable is given preference.
[public] int: x { get; }
This effectively creates a runtime constant that can only be initialised in the constructor or explicitly during creation.
[public] int: x { set; }
While it compiles, the above has virtually no practical use.
Sugar features functions for cast overloading, operator overloading, indexers and constructors (destructors only for a class. Even though theres not much use for it now).
struct Complex
{
float: real { [public] get; [private] set; }
float: imaginary { [public] get; [private] set; }
[public]
constructor()
{
real = imaginary = 0;
}
[public]
indexer float (int index) // allows instances of complex to be indexed using []
{
get { return index == 0 ? real : imaginary; }
[private] set { if (index == 0) real = value; else imaginary = value; }
}
[public, static]
explicit string(Complex: person) //allows the explicit conversion of complex to a string
{
return format(real,"i +", imaginary, "j");
}
[public, static]
implicit float(Complex: person) //allows the implicit conversion of complex to a float
{
return math.pow(real * real + imaginary * imaginary, 0.5);
}
[public, static]
operator Complex +(Complex: a, Complex b) //allows the usage of + operator between two complexs
{
Complex: complex = create Complex();
complex.real = a.real + b.real;
complex.imaginary = a.imaginary + b.imaginary;
return complex;
}
}
Cast and operator overloads must be static. indexers and constructors cannot be.
The explicit conversion to a string is internally called by the
tostring
andformat
function.
Sugar features the static
math
class for typical mathematical operations and constants.
Sugar defaults the directory structure as the project structure. Import statements are used to navigate this structure using relative file paths.
import "..directory.sub_directory.file.Class";
import "..directory.sub_directory.file.Class";
//code
let: x = create Class(param1, param2);
Importing a directory imports all files whereas importing a file imports all public structures within it.
Sugar has a built-in class for exceptions along with the throw keyword.
exception: e = create Exception("something went wrong!");
throw e;
So yes there a few things here. No try-catch-finally statements, no switch statements, no generics, no destructors and may have even noticed that there is no static constructor either.
But the big question: What about OOP? In the original C# version it was included. But even with CILs amazing features I realised I was in over my head and took it out of this version.
Will I add it? Maybe but sugar is functional. I'm not an OOP skeptic, I love it on the contrary. But it was too much for this project. That's not to say there's no chance of me revisiting it in the future. Here's the syntax I had planned for generics and OOP:
[public]
interface IArea<TDimension : INumeric>
{
[public] TDimension Area();
}
[public, asbtract]
class Shape<TDimension : INumeric> : IArea<TDiemsion>
{
string: name { [public] get; }
[protected]
constructor(string: name)
{
this.name = name;
}
}
[public]
class Square : Shape<int>
{
[private] int side;
[public]
constructor(string: name, int: side) : super(name)
{
this.name = name;
this.side = side;
}
[public] int Area() { return side * side; }
}