Channels ▼
RSS

Open Source

Nimrod: A New Systems Programming Language


Nimrod is a statically typed, imperative programming language that tries to give the programmer ultimate power without compromising runtime efficiency. This means it focuses on compile-time mechanisms in all their various forms.

It uses a syntax that is a reminiscent of both Python and Pascal, has an AST-based clean macro system that is ideal for metaprogramming. It supports soft realtime GC on thread-local heaps and uses asynchronous message passing between threads, so no "stop the world" mechanism is necessary. An unsafe shared memory heap is also provided for the increased efficiency that results from that model.

It compiles to commented C code, which attains consistently outstanding results on benchmarks. It can also be made to output JavaScript. The entire Nimrod toolchain (compiler, library, build tool, and so on) is written in Nimrod.

This article gives a quick overview of Nimrod's many features. After an introduction to Nimrod's syntax, I show how the language allows for common procedural, functional, OO, and metaprogramming techniques while remaining simple and efficient.

Introduction to Nimrod's Syntax

Nimrod uses a conventional infix-based syntax. Like Python or Haskell, it uses indentation rather than braces to group statements. The usual control flow statements such as if, case, while, and for are provided.

Slightly unusual are the many ways in which function applications can be written: There is the traditional prefix notation f(x, y). If the call is a statement, the parentheses may be omitted: f x, y. This is called the command notation and this version of "hello world" makes use of it:

echo "hello world!"

echo "abc" is an alias for write stdout, "abc" with the notable difference that it abstracts away the output stream, which makes it easier to emulate for the JavaScript target or to make the compiler evaluate it at compile time.

Another notation for invoking functions is the so-called method invocation syntax: x.f(y, z). If there is only one argument, the () can be left out: x.f. So you can write x.len instead of x.len() or len(x). This way, there is no need for special getters or read-only properties.

The language clearly distinguishes between f and f() because functions are first-class citizens and can be passed around like in functional programming languages.

Finally, there are "generalized string literals" that introduce yet another piece of syntactical sugar: Instead of f("abc"), you can write f"abc" and then backslashed escape sequences like \n are not interpreted. This is designed for easy embedding of mini-languages like regular expressions: re"\w+" is much easier to write and read than re("\\w+").

Functions are called procedures in Nimrod and are declared with the proc keyword. Unlike in C and C++, parameters are read-only unless they are declared as var, in which case "pass by reference" is used (pass by reference is implemented with a hidden pointer).

Similar to Haskell, operators in Nimrod are simply sugar for functions. The following example declares a procedure named ++ that can take 1, 2, or 3 arguments. The value of y defaults to 1, and z defaults to 0. ++ modifies x and adds y and z to it.

proc `++`(x: var int; y: int = 1; z: int = 0) =
  x = x + y + z

var g = 70
# ++ can then be used like this:
++g
g ++ 7
g.`++`(10, 20)

Nimrod features the concept of a routine abstraction. A routine in Nimrod can be a procedure, method, template, macro, iterator, or a converter. All routines are invoked with the same syntax; thus, you cannot tell from the invocation which kind of routine it is. Similar to Lisp, Nimrod consciously decouples the syntax from the semantics to allow for powerful metaprogramming:

template `!=`(x, y: expr): expr = not (x == y)

# invocation is as if '!=' were a proc:
echo 34 != 33

A template is a simple form of a macro; the example shows how the unequals operator is defined in Nimrod. For metaprogramming, the type system is weakened and very general types like expr (expression), stmt (statement), or typedesc (type descriptor) are available. Note how the template is invoked like an operator.

Functional Programming with Nimrod

As I mentioned earlier, parameters that are not var are read-only, so Nimrod has a notion of immutability. Immutability is not deep, however: As soon as any kind of pointer is involved, the location that the pointer points to can be modified:

proc modify(n: ref Node) =
  n.value = 45

There are two kinds of pointers in Nimrod: ref and ptr. A ref is a pointer that is considered by the garbage collector  (traced), while a ptr is not (untraced). In general, ptr is used for interfacing with C/C++ or to implement weak references or simply for manual memory management. Nimrod is, after all, a systems programming language.

Similar to parameters are let variables. A let can be assigned only once. Of course, Nimrod also has variables, which use the var keyword:

let lv = stdin.readline
var vv = stdin.readline
vv = "abc" # valid, reassignment allowed
lv = "abc" # fails at compile time

let has been designed to emulate parameter passing semantics so that proc square(x: int): int = x*x can be emulated with:

template square(x: int): int =
  # ensure 'x' is only evaluated once:
  let y = x
  y * y

Finally, there is also const, which declares true constants. Constants can't be assigned at all, not even once. Instead, their value has to be known at compile time. Nimrod has a sophisticated compile-time evaluation engine, so the following works:

proc mostSignificantBit(n: int): int =
  # naive algorithm:
  var m = n 
  while m != 0:
    m = m shr 1  # 'shr' means "shift right"
    result += 1
  result -= 1

const msb3999 = mostSignificantBit(3999)

Procs that return a value have an implicitly declared result variable that represents the return value, so there is no need to write return result. result is Nimrod's way to guarantee what is called return value optimization in C++.

Most variables are initialized implicitly in Nimrod and the initial value is a binary 0. Hence, result starts with 0, which is the natural start for counting. shr is Nimrod's shift-right operator, a keyword has been chosen to avoid confusions as >> has the same precedence as >. The reason for this is that Nimrod supports user-defined operators and thus needs a simple rule of how operator precedence should be handled. The (simplified) rule is that the first character of the operator determines the precedence.

Now let's get back to functional programming. Since procs are first-class citizens, defining map and filter is straightforward:

proc filter[T](a: openarray[T]; predicate: proc (x: T): bool): seq[T] =
  result = @[] # @[] constructs the empty seq
  for x in a:
    if predicate(x): result.add(x)

proc map[T, S](a: openarray[T]; fn: proc (x: T): S): seq[S] =
  newSeq(result, a.len)
  for i in 0 .. >a.len: result[i] = fn(a[i])

openarray is a special type that is only valid for parameters, it is compatible with arrays and sequences. A sequence (seq) is a growable array. openarray is implemented as a pointer to the first element and a length. Lists instead of arrays are, of course, possible, too, but relatively uncommon. (In this, Nimrod shows its imperative roots.)

Pattern Matching and Metaprogramming

Nimrod supports product and sum types with some twists: A sum type (also called an algebraic data type) is supported by Nimrod with a classical enum plus a so-called object variant. Let's say we want to create a library for working with mathematical expressions such as x^2 + 5*x. It's natural to define our data types like this:

type
  FormulaKind = enum
    fkVar,        ## element is a variable like 'X'
    fkLit,        ## element is a literal like 0.1
    fkAdd,        ## element is an addition operation
    fkMul,        ## element is a multiplication operation
    fkExp         ## element is an exponentiation operation

type
  Formula = ref object
    case kind: FormulaKind
    of fkVar: name: string
    of fkLit: value: float
    of fkAdd, fkMul, fkExp: left, right: Formula

Nimrod's enum is an old-school typesafe enum, as in Ada, without any fields. To avoid name clashes, it's common to prefix the enum values with a two-letter abbreviation. The case part in the object declaration introduces a checked union. So the access of f.name will raise an exception if f.kind != fkVar. Everything in Nimrod, including object, is a value type, but I prefer reference semantics here for easier manipulation of formulas.


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.
 

Comments:

Video