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) = {...}

Custom Types (Data Structures)

Declaring a data structure in Night is extremely 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; with a small variation. Because this declaration creates a new type, the type part of the declaration is replaced with the keyword struct.

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

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; });