#lang something: An alternative syntax for Racket

Recent discussions (e.g. 1, 2) about potentially revising Racket syntax for Racket2 have reminded me I never properly announced #lang something, an experiment from back in 2016.

The main idea is S-expressions, but with usually-implicit parentheses and support for prefix/infix/postfix operators. Indentation for grouping is explicitly represented in the S-expression returned from the reader.

  • (+) keeps a semi-structured input format: reader yields ordinary syntax
  • (+) macros Just Work
  • (+) you can do “if … then … else …”: an example
  • (+) you can do “… where …”: an example
  • (-) uses indentation (though it doesn’t have to; see for example this module)
  • (-) the function syntax isn’t function(arg, ...)

(More links at the bottom of this post.)

In addition to the reader, #lang something provides a small selection of special forms that take advantage of the new syntax, and #lang something/shell adds Unix-shell-like behaviour and a few associated utilities.

This program:

#lang something
for { x: 1 .. 10 }
  def y: x + 1
  printf "x ~a y ~a\n" x y

… reads as this S-expression:

(module something-module something/base
  (#%rewrite-body
   (for (block (x (block (1 .. 10))))
        (block (def y (block (x + 1)))
               (printf "x ~a y ~a\n" x y)))))

The #%rewrite-body macro, together with its companion #%rewrite-infix, consults an operator table, extendable via the def-operator macro, to rewrite infix syntax into standard prefix S-expressions using a Pratt parser.

The block syntax has many different interpretations. It has a macro binding that turns it into a Racket match-lambda*, and it is used as literal syntax as input to other macro definitions.

For example, here’s one possible implementation of that for syntax:

#lang something

provide
  for

require
  for-syntax something/base
  prefix-in base_ racket/base

def-syntax for stx
  syntax-case stx (block)
    _ (block (v (block exp)) ...) (block body ...)
      (syntax (base_for ((v exp) ...) body ...))

def-operator .. 10 nonassoc in-range

Notice how the block S-expressions are rewritten into a normal S-expression compatible with the underlying for from racket/base.

Generally, all of these forms are equivalent

x y z          x y z:          x y z { a; b }
  a              a
  b              b

and they are read as

(x y z (block a b))

and are then made available to the normal macro-expansion process (which involves a new infix-rewriting semi-phase).

Colons are optional to indicate a following suite at the end of an indentation-sensitive line. Indentation-sensitivity is disabled inside parentheses. If inside a parenthesised expression, indentation-sensitivity can be reenabled with a colon at the end of a line:

a b (c d:
      e
      f)

= (a b (c d (block e f)))

a b (c d
      e
      f)

= (a b (c d e f))

Conversely, long lines may be split up and logically continued over subsequent physical lines with a trailing \:

a b c \
  d \
  e

= (a b c d e)

Semicolons may also appear in vertically-laid-out suites; these two are equivalent:

x y z
  a
  b; c
  d

x y z { a; b; c; d }

Suites may begin on the same line as their colon. Any indented subsequent lines become children of the portion after the colon, rather than the portion before.

This example:

x y z: a b
  c d
  e

reads as

(x y z (block (a b (block (c d) e))))

Square brackets are syntactic sugar for a #%seq macro:

[a; b; c; d e f]    →        (#%seq a b c (d e f))

[                   →        (#%seq a (b (block c)) (d e f))
  a
  b
    c
  d e f
]

Forms starting with block in expression context expand into match-lambda* like this:

{
  pat1a pat1b
    exp1a
    exp1b
  pat2a
    exp2
}

 (match-lambda*
    [(list pat1a pat1b) exp1a exp1b]
    [(list pat2a) exp2])

The map* function exported from something/base differs from map in racket/base in that it takes its arguments in the opposite order, permitting maps to be written

map* [1; 2; 3; 4]
  item:
    item + 1

map* [1; 2; 3; 4]
  item: item + 1

map* [1; 2; 3; 4]: item: item + 1

map* [1; 2; 3; 4] { item: item + 1 }

A nice consequence of all of the above is that curried functions have an interesting appearance:

def curried x:: y:: z:
  [x; y; z]

require rackunit
check-equal? (((curried 1) 2) 3) [1; 2; 3]

A few more links:

Another small example “shell script”, which I used while working on the module today:

#lang something/shell

def in-hashlang? re :: source-filename
  zero? (cat source-filename | grep -Eq (format "^#lang.*(~a).*" re))

(find "." -iname "*.rkt"
  | port->lines
  |> filter (in-hashlang? "something")
  |> ag -F "parameterize")