Notes On Night

Night is a programming language I am developing that is inspired by C and functional languages. The goal is for me to learn how to develop a compiler and implement useful language features. This page exists as a reference for some of the syntax and features (both extant and planned) of the language. It only assumes familiarity with C, everything else is explained.



Variable declarations in Night are based on the following form:

name: type;
name: type = expression;

The second is called a definition, and it initializes the variable to a certain value. In fact, essentially every declaration in Night takes the same form as this. You’ll see how that’s possible.


Arrays are similar. The array is part of the type, and its length and element type are inherently a part of it.

name: type[length];

If the length can be inferred from the initializer expression, it can be omitted.

name: type[] = { elem1, elem2 };


Functions in Night have types and are expressed in the same way as variables. The function’s type is of the form:

(args) return_type

where return_type is optional. A function’s “expression” takes the form of a block of statements (a scope)


I’ll use the following notation to refer to a generic scope:


So a completed function declaration would look like this:

name : (args) return_type = {...};

The args are a list of variable declarations separated by commas, i.e.

name1: type1, name2: type2, ... , nameN: typeN

So say you have a function foo that takes two integral parameters x and y and returns a floating point number.
In C it looks like:

float foo(int x, int y) {...}

And in Night it looks like:

foo: (x: int, y: int) float = {...};

Now what if you wanted to make a function bar that returns an integer and could take this function foo as an argument? In C you need to use function pointers with this syntax:

int bar(float(*foo)(int, int)) {...}

Which is rather unclean looking and additionally requires you to change the formatting of the code. One of the goals of Night is a consistent syntax throughout, for ease of refactoring. So you don’t need to change the type of foo at all, you can just copy its declaration and use it as the parameter of bar.

bar: (foo: (x: int, y: int) float) int = {...};

Also worth noting are the parameter names on bar’s parameter foo. They don’t need to be removed, which provides seamless code movement. They also don’t actually do anything, so they can be removed if you want, for readability:

bar: (foo: (int, int) float) int = {...};

The args can also have optional default values that allow the function to be called without explicitly specifying those parameters.

name1: type1 = value1, name2: type2 = value2, ... , nameN: typeN = valueN

This allows code like the following:

set_size: (width: int = 480, height: int = 320) = {...}


In Night, a type is a variable that exists at compile time. The keyword type is available to declare a type.

also_int: type = typeof(int);

You can then declare other variables using this new type.

my_also_int: also_int;

Custom Types

Declaring a custom type in Night is similar to C. Take this simple box data structure in C:

struct box {
  int x;
  int y;
  int w;
  int h;

Night maintains its declaration syntax of name: type = expression;. Because this declaration creates a new type, the expression part of the declaration is something that evaluates to a type. The function struct is one such thing, and it can be used as follows:

box: type = struct {
  x: int;
  y: int;
  w: int;
  h: int;

You may noticed that I called struct a function - this may seem strange, but it is indeed a function, one that is executed at compile time. It takes a block of declarations and transforms them into a new type. More on how this is possible later.

Declaring a variable of a custom type requires no special caveats.

x: int;
xbox: box = {1, 360, 1, x};

Advanced Usage: Naming

Anonymous Variables

In Night you can declare variables with no names.

type = expression;

The main use case for these is explicitly padding structures without needing to name the padding. They cannot be accessed except via pointer arithmetic.

vec3_padded: struct = {
  x: float;
  y: float;
  z: float;
  float = nan;

As Literals

An anonymous variable can also be used in places that expect literals.

foo: (x: int);
// ...
foo(int = 5);

This is also how to explicitly specify the type of a literal.

vec2: struct = {
  x: float; y: float;
do_thing: (i: vec2);
// ...
do_thing(vec2 = {1.0f, 0.0f});

You can also give a name to an expression or literal. All these calls do the same thing.

foo: (x: int, y: int) int;
// ...
foo(1, 2);
foo(int = 1, 2);
foo(x: int = 1, y := 2);
foo(one: int = 1, two := 2);

Named expressions can be used alongside default parameters to call functions in very expressive ways. The requirements are as follows:

  • If all arguments are named expressions with names that match parameter names, they can be in any order and correspond to the parameter by name.
  • If any arguments are named expressions with arbitrary names that don’t match parameter names, it is like a normal function call and the order corresponds to the parameter list.

Anonymous Functions

A function can also be declared with no name.

(args) return_type = {...};

This is useless, unless it is passed to another function. A common usage for this is comparators, such as in C’s qsort.

int values[] = { 88, 56, 100, 2, 25 };

int int_comparator(const void * a, const void * b) {
  return ( *(int*)a - *(int*)b );

int main () {
  qsort(values, 5, sizeof(int), cmpfunc);

In Night, the same code would be written like this:

values: int[] = { 88, 56, 100, 2, 25 };

main: () = {
  qsort(values, (int a, int b) int = { return a - b; });