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
{
}
string: result = 0 % 2 == 0 ? "0 is even" : "0 is odd";
Sugar offers the for
, while
and do while
.
while (condition)
{ }
for(int x = 0; x < 10; x++)
{ }
do { } while(condition)
Sugar uses the print
, println
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.
let: y = format("{0} ate {1}", /* 0th argument */ tostring(7), /* 1st argument */ 9);
print(y); // "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. - Exception:
exception
serves to represent compile time exceptions that can be thrown. - Math:
math
serves as a built in static class to define mathematical constants and functions. - Object:
object
serves as the base object reference for any type (as in C#).
Sugar supports the collections mentioned above.
array
:
let: collection = create array<int>(3);
collection[2] = 3;
print(collection[2]);
list
:
let: collection = create list<int>();
collection.Add(5);
print(collection[0]);
- You may create arrays and lists using the { } expression too
let: array = create array<int> { 1, 2, 3 };
let: list = create list<int> { 1, 2, 3, 4, 5 };
dictionary
:
let: collection = create dictionary<int, string>();;
collection.Add(10, "ten");
print(collection[10]);
tuple
:
let: collection = create tuple<int, string>(1, "one");
print(collection.Element1);
print(collection.Element2);
let: nullableInt = create nullable<int>(); // int with value "null"
if (nullableInt.IsNull)
print("int is null");
print(nullableInt.Value); // will throw an error as the value is "null"
Nullable wraps around value types only and is itself a value type.
let: e = create Exception("something went wrong!");
throw e;
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>(/* load function from this */ this, HelloWorld);
The funcref
function is used to get the reference to a function. It contains 2 necessary arguments: the object of interest and the name of the function to be called.
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.
Sugar supports custom data structures: class
, struct
and enum
.
Sugar is garbage collected since it compiles to CIL. The only difference between a class and a struct in sugar is how they're handled in memory.
Criteria | Class | Struct |
---|---|---|
Creation | Allocated on the heap | Allocated on the stack |
Arguments | Passed by reference | Passed by value (unless specified using ref ) |
Returning | Returned a reference | Returns a copy |
class Type
{
[public] int: x;
[public] void Modify() { x = 10; }
}
let: a = create Type();
let: b = a; // references the same instance
b.Modify();
print(a.x) // prints 10
print(b.x) // prints 10
struct Type
{
[public] int: x;
[public] void Modify() { x = 10; }
}
let: a = create Type();
let: b = a; // creates a copy (even without explicitly using `create`)
a.Modify(); // the original is modified, the copy is unchanged.
print(a.x) // prints 10
print(b.x) // prints 0
Enums in sugar are a collection of immutable constant integers. Members are initialised to compile time constant values.
enum EncodingBase
{
Binary = 2,
Octal = Binary << 2,
Hex = Binary << 3,
Base64 = Binary << 5
}
Enums implicitly define bitwise operations and an explicit conversion to their integer value.
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:
-
static
: Declares a member static. -
public
: An access specifier for public items. -
private
: An access specifier for private items. -
ref
: Allows reference like behaviour with structs.
ref
is special in sugar because it lets you avoid struct copying, this is useful with have large structs.
The ref
function is used to obtain references of values. It expects one argument which must be a variable.
let: x = 20;
[ref] int: y = ref(x);
y = 10;
print(x); // prints 10
A referenced struct is treated as a different data type. copy
is used to create a value copy of the reference.
A few more rules with references:
- References must be initialized and cannot be reassigned. This ensures validity of lifetimes.
- Member fields and properties cannot be of a referenced struct type and functions cannot return a reference type.
int FunctionDescribers([ref] int: x)
{
return copy(x = 10);
}
let: a = 20;
let: b = PassByReference(a);
print(++b); //prints 11
print(a); //prints 10
Sugar lets you customise member fields using properties. A rather basic implementation:
[public] int: x { get; [private] set; }
- A public get, private set property. It can be accessed anywhere but changed only in the implementation of the structure it's defined in
[private] int: x { [public] get; [public] set; } // a private get set property
- In case of conflicts like above, the describer on the field definition is given preference.
[public] int: x { get; }
- Creates a runtime constant that can only be initialised in a constructor or during creation.
[public] int: x { set; }
- While it compiles, the above has virtually no practical use.
list<int> values;
[public] int: Count { get { return values.Count; } }
- Accessors can define bodies. the
set
accessor implicitly defines thevalue
parameter to represent the value assigned.
Sugar features functions for cast overloading, operator overloading, indexers and constructors.
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, internally called by tostring() and format()
{
return format("{0} + {1}j", real, imaginary);
}
[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 public and static. indexers and constructors cannot be static. All structures have a default string conversion and constructor unless specified.
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. You may also import a specific public structure.
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:
interface IArea<TDimension : INumeric>
{
[public] TDimension Area();
}
class Shape<TDimension : INumeric> : IArea<TDiemsion>
{
[public] string: name { [public] get; }
[protected]
constructor(string: name)
{
this.name = name;
}
[public, abstract]
TDimension Area();
}
class Square : Shape<int>
{
[private] int side;
[public]
constructor(string: name, int: side) : super(name)
{
this.side = side;
}
[public, override] int Area() { return side * side; }
}