Urlang is JavaScript with a sane syntax

@jbclements

Hi,

The text became longer than I anticipated, but there were a lot of ground to cover.

The core Urlang language more or less translates constructs from Urlang forms directly to JavaScript forms. In particular Urlang does not introduce new data types like symbols.

As a trivial example

(var [x 42] [y 43])

is translated to

var x = 42,
    y = 43;

There are a few differences though:

  • #f is the only falsy value
  • functions allow default arguments
  • functions don't require return - the value returned is the value of the last expression in the body
  • let expressions (JavaScript let is a statement)
  • identifiers follow Racket syntax (i.e. - and ? allowed)

There are a few important twists though:

  • unbound variable references are detected at compile time
  • users can define macros to extend Urlang

The first twist: If Urlang detects a reference to an unbound identifier, DrRacket or racket-mode will highlight the offending identifier as usual. This has two use cases: One, if Urlang is used directly to write JavaScript, then fixing the errors become faster. Two, consider the case where Urlang is used as the output language for a compiler. If the unbound identifier were inserted by a compiler pass, then the identifier highlighted will be the one in the compiler source that caused the problem. This is a big deal: compiling directly to JavaScript would have postponed detecting of the error to runtime in the browser. And going from the identifier in the output to the original is not trivial.

The second twist: Users can define macros. This is feature is intended to be used, when Urlang is used directly (i.e. not as a compiler back-end language). Anyone used to Racket attempting to write a JavaScript program quickly discovers that JavaScript is limited in its selection of forms. Especially statements are favored over expressions.

In particular, in Racket if, cond and case are all expressions.
JavaScript does have a ternary conditional (e ? e1 : e2) but that's it.
With macros available it is easy to fix this.

In urlang/extra I have defined forms that work like their Racket counter parts (i.e. they are expressions): ​begin0, when, unless, cond, case, letrec, let*. If needed, there are statement versions as well: swhen, sunless and scond.

In urlang/for I have defined a for macro that feels like Racket for.

When these expressions are available Urlang begins to feel more like Racket than JavaScript.

As an example. This program will produce an array of factorials.

(urlang
 (urmodule main
   (define (fact n)
     (if (= n 0)
         1
         (* n (fact (- n 1)))))

   (for/array ([x in-range 1 5])
     (fact x))))

Running this Racket program will write the JavaScript output to "main.js".
If the parameter current-urlang-run? is set to #t, then the JavaScript runtime node will be invoked. And the output will be displayed directly in the DrRacket/racket-mode repl.

In this particular I see "[ 1, 2, 6, 24 ]\n" in the repl.

The original plan was to write a Racket to JavaScript compiler.
I decided to split the project in two parts. The first part Urlang is simply JavaScript with S-expressions syntax and macros as described above. The second part was a compiler from Racket to Urlang.
It turned out that I was content using Urlang directly.

When Urlang development started browsers only supported ES5, so the compilation target was ES5. However ES6 has now become standard, so recently I have added support for some ES6 features in particular ES6 modules, but also operators like spread (the ... operator).

The second part of the project (the Racket to Urlang) compiler is not done.
I have implemented a compiler for top-level programs. I.e. no support for Racket modules. The next step would be to look at linklets.
The runtime for the Racket compiler is implemented in Urlang. It represents Racket values as arrays, whose first element contains a "tag" that describes the type. This differs from most compiler targeting JavaScript, that represent foreign values as objects. The hope was that arrays would be fast to allocate and that array references with known indices would be optimized by even simple JavaScript engines.

The compiler compiles all Racket tail calls to JavaScript tail calls.
If only ES6 had supported TCO as promised, then ...

Personally I am using Urlang directly to write a web site where high school students can solve various math problems. For the front-end I chose React (Hooks).
React introduces a virtual DOM representing html elements. When the program changes the virtual DOM, React will update the real DOM based on the changes made to the virtual dom. The trick is that React compares the old virtual dom with the new one, so it can restrict updates of the real DOM to the parts that changed. Modern React (React Hooks) represent user components as functions. It feels Rackety.

One complication of using React is that all React tutorials use so-called jsx expressions to generate html-elements.

This

const element = <h1>Hello</h1>;

doesn't work in vanilla JavaScript, so JavaScripters run their programs with jsx expression through a pre-processor that translates <h1>Hello</h1> into JavaScript expressions that produce a value that represents a h1-header in the virtual DOM.

In order to escape back to JavaScript, one can use braces {} inside a jsx expression:

const element = <h1>The sum 1+2 is {1+2}.</h1>;

In Urlang I could similar expressions as a macro without changing the core and without added an extra build step.

Instead of jsx expressions, I have urx expressions.
Instead of {} to escape back, I use `ur.

It looks like:

(var [element @urx[@h1{Hello}]])
(var [element @urx[@h1{The sum 1+2 is @ur(+ 1 2).}]])

Note: If the html tag is written with lower case, it must be one of the tags in the html spec. Spelling errors are detected at compile time.
User defined tags are written with upper case.

If there is interest, I can produce a few examples of using React with Urlang.

In short: I see Urlang as JavaScript with macros and a nicer syntax.
If JavaScript had macros, I would not have written Urlang.

Guide to the Urlang sources:

  • urlang/main.rkt Urlang->JS compiler using NanoPass
  • urlang/extra.rkt implements cond, if, when, ... as Urlang macros
  • urlang/for.rkt implements for loops as an Urlang macro
  • urlang/react/urx.rkt urx expressions
  • compiler-rjs/compiler.rkt the Racket->Urlang compiler
  • compiler-rjs/runtime.rkt

Github:

8 Likes