belte

1 Overview

The Belte language is syntactically very similar to C#; Belte is a “C-Style” language that focuses on the object-oriented paradigm.

Currently, the Belte compiler, Buckle, supports interpretation and building to a .NET executable.

Using the compiler CLI

1.1 Conventions

// ... and /* ... */ in code samples refer to an arbitrary statement or expression or continuation of a pattern.

For example, if a code sample showed:

int a = /* ... */;

The usage of /* ... */ is meant to imply the actual expression used does not matter for the sake of demonstrating the feature being discussed.

Similarly, if a code sample showed:

void Func() {
  // ...
}

The usage of // ... is meant to imply the body of the function Func does not matter for the sake of the feature being discussed.

Note that these shortcuts do not necessarily ensure correctness and are used for brevity. For example:

int Func() {
  // ...
}

In this example, Func in this state is invalid because it fails to return even though the function signature has a return type of int. In this case the // ... implies an arbitrary valid return.

1.2 Endpoint Specific Features

Some features are not supported across all endpoints for various reasons.

The following list describes all of the features where full parity is not currently implemented or was not always implemented.

Feature Evaluator Executor IL Emitter Explanation
--type=graphics projects Standalone graphics DLL under development
Non-type templates Not supported by the .NET runtime
Non-integral enums Not supported by the .NET runtime
Pointers Partially supported the Evaluator but not stable due to internal memory structure
Function pointers Disallowed in the Evaluator due to internal memory structure
Externs/DllImport Incompatible with the Evaluator
Inline IL Incompatible with the Evaluator
.NET DLL references Incompatible with the Evaluator

1.3 Keywords

The following lists all keywords used in the language. No type names (e.g. int) are reserved.

Some keywords have multiple meanings depending on context. Those keywords will be disambiguated in the lists below.

1.3.1 Non-Contextual Keywords

These keywords are reserved names and cannot be used as identifiers.

1.3.2 Contextual Keywords

These keywords only act as keywords inside specific contexts. As such they can be used as identifiers in most places.

1.4 Nullability and Types

Nullability is treated in a non-standard way in Belte. As such, a close read of the following section is recommended, which is a consolidation of important nullability semantics found in the rest of the documentation.

To summarize:

A type is “nullable” when it permits null.

1.4.1 Normal Types

Classes are reference types meaning they are heap allocated, garbage collected, and not copied when passed.

For example:

var a = new MyClass();
var b = a;
b.i = 5;

class MyClass {
  public int i = 0;
}

In this example, a.i is 5 because both a and b refer to the same object in memory.

Reference types are non-nullable by default. To make it nullable, a ? annotation can be used:

var a = new MyClass();
a = null; // Invalid

var? a = new MyClass();
a = null; // Okay

Notice how nullable annotations apply normally even when implicitly typing. The following are identical:

MyClass! a = new MyClass();
var! a = new MyClass();

Value types are also non-nullable by default (this includes primitives, structs, and pointers).

To make a non-reference type nullable, a ? annotation can be used just like reference types:

int a = 3;
a = null; // Invalid

int? a = 3;
a = null; // OK

Most value-types (non-reference-types) are able to made nullable. Pointers and function pointers are not.

In much of the documentation and standard library, redundant annotations are used for clarity. Note that the following are identical because int defaults to being non-nullable:

int a = 3;
int! a = 3;

1.4.2 Pointers and Function Pointers

Nullability in pointers is not covered with the type system, but rather with the nullptr keyword. A nullptr under the hood is just a 0-valued pointer. The following are equivalent:

int* ptr = nullptr;
int* ptr = (int*)0;
int* ptr;

Even though pointers and function pointers themselves do not use the nullable type system, their elements can:

int?* ptr; // OK
int?*? ptr; // Invalid

void(int?)* ptr; // OK
void(int?)*? ptr; // Invalid

1.4.3 Initializers

Local initializers are required for non-nullable types. For nullable types, initializers are optional and a local without one will be set to null. The following are identical:

int? a = null;
int? a;

The exception to this is pointers and function pointers. They are set to nullptr by default if no initializer is present. The following are identical:

int* ptr = nullptr;
int* ptr;

When implicitly typing, the inferred type is the direct type of the initializer. The following are identical:

int a = 3;
var a = 3;

1.4.4 Fields

Unlike locals, non-nullable struct fields do not allow initializers and instead are set to a default value. The default value of numeric types is 0, for booleans false, and an empty string for strings.

An mentioned earlier, pointers and function pointers default to nullptr.

A non-nullable struct’s default value is a created struct where each field is set to it’s default value. For example:

var a = new MyClass();
var b = a.str.num;
// b equals 0

class MyClass {
  MyStruct str = default;
}

struct MyStruct {
  int num;
}

Class fields always require an initializer or definite assignment, which can be read about here.

Since structs set their fields to their default value, the default value of a struct is a struct where every field is set to it’s default value. If a struct contains a field that has no default value, the struct also has no default value:

class A { }

struct MyStruct {
  A a;

  constructor(A a) {
    this.a = a;
  }
}

MyStruct myStruct = default; // Invalid because type `A` has no default value

1.4.5 Implicit Typing

var, const, final, and constexpr can be used when defining a local indicating that their type should be inferred. The inferred type is usually the exact type of the initializer. The following are identical:

int a = 3;
var a = 3;

Nullability persists when inferring a type. The following are identical:

int? a = Func();
var a = Func();

int? Func() { /* ... */ }

var means a normal local declaration. const, final, and constexpr infer the type of a local with the respective modifiers. The following are identical:

const int a = 3;
const a = 3;

constexpr int a = 3;
constexpr a = 3;

final int a = 3;
final a = 3;

const means the local cannot be assigned to or otherwise modified. constexpr means the value of the local is a compile-time constant that will be substituted at compile time. For classes, a const modifier means fields can only be read but not written to, and only methods marked const can be called. Class-types cannot use the constexpr modifier because they are not compile-time constants. final means the local cannot be assigned to but can be modified. This means any class method can be called or array elements can be modified.

1.4.6 Null-Flow Analysis

Belte does not currently perform automatic null-flow analysis. Instead, explicit null checks and control flow should be used.

When a value is needed to be non-null at a certain time, the ! operator can be used that asserts that the value is not null, otherwise a runtime error occurs. For example:

int? a = Func();
int! b = a!; // If a is null, runtime exception

To conditionally execute code if a value is not null, a null-binding contract can be used:

int? a = Func();

if (a -> b!) {
  // ...
}

In this example, b is declared as a non-nullable local set to the value of a, so in this case the type of b is int!. b only lives inside of the block.

To use a type’s default value in the case of null, the ? operator can be used. For example:

bool? a = Func();

if (a?) {
  // ...
} else {
  // ...
}

In this example, the else block is executed if a is false or null. Note that conditions inside if, for, and while constructs can be nullable. If the condition is null at runtime, an exception is thrown. For example:

bool? a = Func();

if (a) {
  // ...
}

In this example, if a is null, a runtime exception is thrown at the if condition.

1.4.7 Arrays

Arrays are a collection of elements. Elements cannot be read before they are written:

var arr = new int[10];
arr[0]; // Exception
var arr = new int[10];
arr[0] = 45;
arr[0]; // Okay

A Buffer<T> can be used to avoid these checks, but should be avoided if possible because it potentially allows reading invalid values for a type:

class A { }

var buffer = new Buffer<A>(10);
A a = buffer[0]; // null

In the above example, the non-nullable local a is given a null value from the buffer. Buffers should only be used when performance is critical or working with unconstrained templates.

1.5 Differences from C#

Belte is similar enough to C# so that the differences are more notable than the similarities. The following is a list of most of the differences to make it more clear where the language is unique with links to relevant doc sections:

1.6 Identifiers

Identifiers are used to name symbols. For example, in the statement var a = 3;, the name of the symbol is a, which is the identifier.

Identifiers are continuous strings of letters, digits, and the underscore (_) character, where the first character has to be a non-digit. Legal identifier could be myLocal, My_Local, MyTemp3, or _, whereas 3myLocal would be illegal because it starts with a digit. Identifiers cannot be the same as any non-contextual keywords. For example, var class = 3; would be illegal because class is a non-contextual keyword.

The verbatim specifier @ can be used to treat what would be a keyword as an identifier. For example, var @class = 3; would be legal where class is the identifier (note that the @ is not included, so myLocal and @myLocal are the same identifier).