Dialects in NaaN

Dialects in NaaN

"To have another language is to possess a second soul."

    — Charlemagne

NaaN is not a language, but a software platform. You access it through a language layer that can host multiple languages simultaneously. This agility allows NaaN to adapt to people instead of vice versa. It also allows programmers to indulge in that favorite pastime of customizing their environment.

The default NaaN language is Lingo, which is designed to be familiar to programmers who are comfortable with curly-braces syntax, the style used by JavaScript, Java, C#, C++, Swift, and many others.

Lingo is an unopinionated language. It is a reasonably complete, but does not attempt to define every nuance of programmer expression. Common programming idioms have the expected syntax, so one can dereference objects with . and arrays with [] as you would expect. Most of the standard operators are available with ordinary precedence, so arithmetic expressions look and work correctly. If there isn't syntax for a certain functionality, there are usually procedure calls to fall back on. Lingo is designed to be small and extendable.

For example, the NaaN platform supports objects, classes, and inheritance but the Lingo language does not have specific syntax for them. NaaN has bitwise arithmetic functions but Lingo does not define bitwise operators. Lingo also does not define operators for either exponentiation or factorial, but the ^ and ! symbols are available for assignment.

In NaaN, a new language can be loaded as a separate module, defined on the fly, or derived from an existing language. Every language and variant is termed a dialect, and all available dialects in an instance of NaaN are listed in its language registry.

One benefit of a small language is that it's easy to customize. If Lingo were to define | as a bitwise-or operator, then one could not derive a dialect from Lingo and repurpose this symbol for algebraic absolute value delimiters like |⍺|. Existing code might already rely on the bitwise version. Because Lingo defers this decision it allows us to derive separate dialects for each purpose.

Factorial example

Let's define fact to calculate the factorial function on the positive integers.

function fact(x) {
    if x > 1
        x * fact(x-1)
    else
        1
}
$: fact

Play-lingo> fact(120)
$:66895029134491270575881180540903725867527463331380298
1029567135230163355724496298936687416527198498130815763
7893214090552534408589408121859898481114389650005964960
521256960000000000000000000000000000

That seems to work well enough, but if we're going to be coding mathematical formulae it would be much more attractive to use the standard ! symbol. Although Lingo defines this symbol as the logical-not prefix operator, it simply returns the original expression in the postfix position because no other functionality is defined.

Play-lingo> !true     // prefix logical-not operator
$: false

Play-lingo> 120!      // postfix undefined
$: 120!

To define postfix functionality we can use the special inbuilt Dialect object:

Play-lingo> Dialect.redefine(`!, fact)
$: !

Play-lingo> 120!      // postfix factorial
$:66895029134491270575881180540903725867527463331380298
1029567135230163355724496298936687416527198498130815763
7893214090552534408589408121859898481114389650005964960
521256960000000000000000000000000000

It's not actually a good practice to redefine operators of an existing dialect, even if convenient when hacking around. The right approach is to derive a new dialect and make our changes there. This helps people know what to expect.

Let's implement another mathematical function. The ^ operator could be either exponentiation or a Boolean exclusive-or. Why not both?

We will define two new dialects, call them Mathy and Bitty. Mathy will use ! for factorial and ^ for exponentiation, while Bitty will use ^ for bitwise exclusive-or.

First let's define power to calculate exponentials. It uses the inbuilt JavaScript Math.pow function, but if both arguments are integers then it calculates a fast and exact result by squaring.

function power(x,y, local result) {
    if !integer(x,y)
        return (Math.pow(x,y))
    result = 1
    loop {
        if bitand(y,1)
            result *= x
        if ((y = shiftright(y,1)) == 0) {
            result, break }
        x *= x
    }
}

The following restores ! within Lingo and then derives a new Bitty dialect with the ^ exclusive-or operator.

Play-lingo> Dialect.redefine(`!, false)
$: !                  // ! is once again undefined in Lingo

Play-lingo> Dialect.derive("Bitty")
$: Object{22}

Play-lingo> /dialect Bitty
language is now Bitty

Play-bitty> x ^ y
$: x ^ y              // undefined and unchanged

Play-bitty> Dialect.redefine(`^, bitxor)
$: ^

Play-bitty> (0xf0f ^ 0x0f0).toString(16)
$: "fff"

Play-bitty> /dialect Lingo
language is now Lingo

The following lines derive a new Mathy dialect with the ! factorial and ^ exponentiation operators.

Play-lingo> Dialect.derive("Mathy")
$: Object{22}         // the new dialect object is returned

Play-lingo> /dialect Mathy
language is now Mathy

Play-mathy> Dialect.redefine(`!, fact)
$: !

Play-mathy> Dialect.redefine(`^, power)
$: ^

Play-mathy> 5^10
$: 30517578125

Play-mathy> 10!
$: 3628800

Play-mathy> /dialect
dialects: Lingo Bitty Mathy

The NaaN dialect mechanism simplifies language customization by making the changes explicitly defined and explicitly applied. Only one dialect can be active at a time, but it's not hard to create a merged dialect when that is necessary.

Live Demo

Please try this out for yourself. The terminal is preloaded with the fact and power functions but no operators have been redefined.

You might notice that the recursive fact() gets a stack depth exception with an argument of 1200. NaaN implements tail-call optimization, but fact is not written to take advantage of that. The necessary improvement is left as an exercise for the reader. Or if you're lazy, the terminal above is preloaded with factr, an optimized version that handles arguments of 10000 or more. Use factr.proc to see the source code.

Working inside a small, transient, embedded terminal doesn't really convey the full power of NaaN. For a better experience please install the NaaN interpreter and NaanIDE that are available under the MIT license.

Further Customization

The examples here cheated a bit, because the operators were already defined in the Lingo grammar even if the functionality was not. More ambitious changes will often require adjusting entries in the grammar table or even writing a little code. This is quite powerful because it can define new keywords, change operator precedence, and generate code on the fly like a LISP macro.

NaaN uses a modified Pratt parser along with an analogous unparser, both driven by the same grammar table. The expression Dialect.info() displays the grammar table of the current parser, while methods on the Dialect object provide various ways of manipulating the dialect. Please stay tuned for additional documentation on creating languages and customizing dialects.

Surface Languages and LISP

The idea of surface languages is as old as LISP, but they never became terribly popular. Sandewall[1] writes an interesting anecdote:

In the early stages of LISP's history, everyone seems to have assumed that surface languages would appear. After all, John McCarthy, who originated LISP, was also a member of the committee that defined ALGOL60. The name LISP 1.5 seems to indicate that it was an intermediate solution to be used until LISP 2 (which would use such a surface language) appeared. The LISP 2 project started out ambitiously, but reportedly caught the PL/I disease (proliferation of features) and did not enjoy the PL/I antidote (money), and therefore was never completed.

ALGOL60 was the original curly braces language, so it's interesting to consider that NaaN has adopted ambitions arising from before I was born.

Even if surface languages never became popular, language customization certainly did. LISP macros are probably the most-loved feature of the language. They make LISP a "programmable programming language"[2] that can be adapted to the requirements of the current environment.

NaaN takes a different approach, refactoring language customization into the following separate elements:

  • Dialects provides parsing and unparsing services. Parsing can include code generation with all the functionality of a LISP macro, but provisions must be made for unparsing to the original source. Dialects can be maintained in the same codebase where they are used, or formalized as plugins that can be shared with other teams.

  • The macro procedure form provides unevaluated "call by name" capability when meta operations are required on code. Because these do not change the language, they need not reside in a dialect. In the same way that Lingo can fall back on procedure calls when it doesn't have specific syntax, NaaN macros can be used to parameterize meta expressions that might otherwise be processed during parsing.

  • Derived dialects allow small changes to be made without the effort required to develop an entirely new language. Lingo is designed for easy extension using this capability.

  • NaaN's language registry maintains dialects as separate named contexts that are made active for code as needed. Explicitly named dialects helps keep the software maintainable and well organized.

If LISP is a programmable programming language, then NaaN is a programmable software platform. Language agility is important, but NaaN includes numerous mechanisms for customizing the structure of the software environment. Inbuilt math operations can invoke custom hooks on objects and environments, namespaces enable a wide variety of structural compositions, and the module system provides offers both customization and protection. Please stay tuned for more details.


  1. Erik Sandewall. 1978. Programming in an Interactive Environment: the "Lisp" Experience. ACM Comput. Surv. 10, 1 (March 1978), 35–71. https://doi.org/10.1145/356715.356719 ↩︎

  2. https://www.paulgraham.com/chameleon.html ↩︎

Richard Zulch

Richard Zulch

Richard C. Zulch is a technologist and inventor, and been a founder, developer, and CTO. He has deeply analyzed the software of over 100 startups for M&A investors, strongly informing NaaN's design.
San Francisco Bay Area, California