Back to UNDERWARE
Link to the Compiler

N26

Yo

N26 is a redesign of N19, and the seventh attempt to design and implement the Nectar programming language.

As a "better C", Nectar is designed to do low-level ergonomically, something that most self-proclaimed better Cs fail at. Ignorance is not excused; unless the programmer keeps to very high-level concepts only, he/she must still understand the intricacies of the target architecture.

A Nectar compilation unit is composed of separate modules, each in its own namespace. A module may define global functions or variables. By default, no definitions are exposed to the outside. To do so a definition must be "exported", which locks it to a given ABI. What is not exported however, has no guarantee of making it to the finished binary or looking the way it is written. Non-exported items should be seen as being in a wibbly-wobbly, high-energy state (later on these wobbles will be explained in detail).

The only standardized library Nectar has is fully compile-time, in the module N. Nectar does not expect any runtime environment such as POSIX or even libc, however compatibility should be trivial: to write a C-compatible main function, simply do:

export main: (i32 argc, u8[?]*[?]* argv) -> () @c {
	return;
}

The main function must be exported to be visible to the C runtime. We specify the @c convention to use the system's C convention. In the Unix world, this will be the System V ABI. In Windows it is System V for IA-32, and x64 for AMD64.

When we get to low-level features such as the ABI, you will see a lot of @ symbols, which were chosen to prevent most words from being keywords.

In general, a Nectar function may have any number of inputs and outputs and need not be assigned an ABI, for example:

div: (i32 a, i32 b) -> (i32 q, u32 r) {
	q = a / b;
	r = a % b;
	return;
}

An equivalent function body would be return a / b, a % b;.

Although Nectar is statically-typed, variable declarations implicitly use the expression's type. This usually works but sometimes we must be explicit using a cast:

foo = u32 5

We cast the 5 to u32, because otherwise Nectar doesn't know what to do with the 5.

If you wish to declare a variable without assigning a value to it, use the ? expression:

foo = u32 ?

Unlike most languages, strings are nothing more than compile-time byte arrays, so if we, for example, have a 4-char string, we can assign it to a u32:

foo = u32 '\x7FELF'

Nectar comments also differ, in that they are note statements:

note This is a comment;

note {
	This is a block comment.
}

note foo {
	This is a named block comment.
}

Nectar has only if and loop statements for control flow. While loops and for loops can be simulated using these two:

i = u32 0;
loop {
	if i >= 10 {
		break;
	}
	
	note DO SOMETHING;
	
	i = i + 1;
}

Modules

Compilation begins with a main module specified by the programmer. A module may use other another with use:

use C;

Symbols defined in other modules may be used with ., for example C.errno.

All symbols are visible to user modules, and the export specifier concerns only outside the Nectar compilation unit.

Type system

N26 supports the following types:

Scalar types

These are of the form u8 or i16. Any bit size is supported.

Only 8-bit, 16-bit and 32-bit works as of now.

Pointer types

An object elsewhere in memory can be referred to by these types.

Example: i32*, u8**, u8[?]*.

Array types

Any type may form an array: u8[5]. The length can be a constant, or ? for unknown. The latter having an unknown size, can only be used through a pointer.

Structure types

Structure types in N26 are a combination of product types and C-style unions.

Most simply, they are defined by parentheses, with fields separated by commas: (u8, u16*). Bare parentheses () define an empty structure.

The above syntax only defines the semantics of the type but cannot control the memory layout. For that, use the record syntax.

record SockAddrIPv4 {
	u16 family;
	u16 port;
	u8[4] address;
	u32 zero0;
	u32 zero1;
}

Exact field offsets may be specified:

record SockAddrIPv4 {
0:	u16 family;
2:	u16 port;
4:	u8[4] address;
8:	u32 zero0;
12:	u32 zero1;
}

Unions are achieved by overlapping fields offsets.

Unions don't work :)

Function types

A function type takes one structure as a parameter, and one structure as the output. Unlike most C-likes, Nectar can give the programmer more ergonomic multiple-value returns.

The div function from above has the type (i32 a, i32 b) -> (i32 q, u32 r).

The calling convention is part of the function type, so a function with a C calling convention would be suffixed: (i32 a, i32 b) -> (i32 q, u32 r) @c

Type casting

Nectar does not have an explicit type for some constructs, and must be told what those values are. For example:

  1. String literals have no concrete type and must be immediately casted to either a constant-size array or scalar type.
  2. Likewise, integer literals are untyped and must be immediately casted to a scalar type.
  3. The unknown literal ? must be immediately casted into any type.

Additionally, structures are beholden to implicit casts:

  1. Single-field structures can be implicitly casted to their field (e.g. (u8) to u8)
  2. Any value can be implicitly casted to a single-field structure (e.g. u8 to (u8))
  3. The integer literal 0 can be casted into any structure type as a shortcut for zero-initialization.

Generics

All symbols and records may be generic by using square brackets after the symbol name:

foo[T]: (T a, T b) -> (T c) @c {
	c = a + b;
	return;
}

Said square brackets insert "generic types" into the scope. A symbol without generic types is called specified and/or concrete.

A generic symbol or type is made concrete again with square brackets: foo[u32](1, 2). Alternatively, types may be specified in a key-value manner: foo[T = u32](1, 2).

As exported symbols must be concrete, they may not be generic.

Wobbles

Guaranteeed

Editor's note: from a programmer's PoV I dislike when optimizations aren't part of the language specification because this basically leaves me to the mercy of the compiler, and we've all heard the horror stories of GCC and clang both wreaking havoc with how they distort the C standard SCOTUS-style. This can influence programmers in one of two horrible ways: either it encourages ignorance because people are told to "trust the compiler" without even knowing what it is doing, or they don't trust the compiler to the point they manually optimize everything too hard and leave behind unreadable code. By enforcing the most common optimizations I hope these would be minimized.

Within the bounds of a compilation unit, the following wobbles are guaranteed:

  1. Variables that are unused are removed from the code.
  2. Functions that are unused are removed from the code.
  3. Structure fields that are proven unused within the bounds of a compilation unit are removed from the structure definition.
  4. Structure fields may be reordered to minimize padding.
  5. Constant integer expressions are propagated and evaluated.
  6. Values of variables are propagated to subsequent expressions if known, if such a move is profitable.
  7. Individual operations might not use instructions intended for them, depending on the discretion of the compiler. For example, a x * 15 may actually be computed as x << 4 - x.

Any of the above are disabled if they are to interfere with the specified ABI.

Optional

Unimplemented :)

The following are considered too risque for a compiler to perform without consent of the programmer.

  1. @bitpack boosts structure packing to the bit-level (for example two u3s and a u1 will all fit into one u8). Fields may still be reordered to minimize crossings of byte boundaries.
  2. @inline enforces function call inlining. When used in a function call, applies only to said call. When used in a function definition, all correponsing function calls are inlined.

ABI specification

none of the below is implemented :)

To achieve high portability, Nectar does not require a specific ABI. Each function can be locked to a given calling convention as long as it is formally specified.

There are many ways calling conventions can differ:

  1. Callee cleanup or caller cleanup
  2. Variable-length arguments
  3. How parameters are passed around:
  4. ...

The above is too much complexity to handle in a oneliner.