lark icon indicating copy to clipboard operation
lark copied to clipboard

The Lark programming language.

Lark
----

Lark is an experiment in designing a homoiconic language with a syntax inspired by SmallTalk. It's very early, so there isn't much to see yet, but you can play with it.

Lark Syntax

Core syntax

Internally, Lark has a very simple syntax. It's more complex than Scheme, but just barely. It has three core expression types:

  • Atoms are things like names, numbers and other literals.
  • Lists are an ordered set of other expressions.
  • Calls are a pair of expressions, the function and the argument.

That's it. Scheme has the first two, and Lark just adds one more. Every expression in Lark can be desugared to these core elements. The rest of this section explains the syntactic sugar added on top of that, but that's handled only by the parser. To the interpreter, the above is the only syntax.

This should mean that it'll be as easy to make powerful macros in Lark as it is in Scheme.

Names

The simplest syntactical element in Lark are names, identifiers. There are three kinds of identifiers. Regular ones start with a letter and don't contain any colons. Like:

a, abs, do-something, what?!, abc123

They are used for regular prefix function calls and don't trigger any special parsing, so you can just stick them anywhere.

Operators start with a punctuation character, like:

+, -, !can-have-letters-too!, !@#%#$$&$

An operator name will cause the parser to parse an infix expression. If you don't want that, and just want to use the name of an operator for something (for example, to pass it to a function), you can do so by surrounding it in (). This:

(+) (1, 2)

is the same as:

1 + 2

Keywords start with a letter and contain at least one colon. A colon by itself is also a keyword. Ex:

if:, :, multiple:colons:in:one:

Like operators, keywords will trigger special parsing, so must be put in parentheses if you just want to treat it like a name.

Prefix functions

The basic Lark syntax looks much like C. A regular name followed by an expression will call that named function and pass it the given argument. Ex:

square 123

Dotted functions

Lark also allows functions to be called using the dot operator. It's functionally equivalent to a regular function call, but has two minor differences: the function and argument are swapped, and the precedence is higher. For example:

a.b c.d

is desugared to:

(b(a))(d(c))

Swapping the argument and function may sound a bit arbitrary, but it allows you to write in an "object.method" style familiar to many OOP programmers. For example, instead of:

length list

you can use:

list.length

Operators

Operators are parsed like SmallTalk. All operators have the same precedence (no Dear Aunt Sally), and are parsed from left to right. The expression:

a + b * c @#%! d

is equivalent to:

@#%!(*(+(a, b), c), d)

Keywords

Lark has keywords that work similar to SmallTalk. The parser will translate a series of keywords separated by arguments into a single keyword name followed by the list of arguments. This:

if: a then: b else: c

is parsed as:

if:then:else:(a, b, c)

Lists

A list can be created by separating expressions with commas, like so:

1, 2, 3

Parentheses are not necessary, but are often useful since commas have low precedence. For example:

this:              means:

difference 1, 2     ===>  (difference(1), 2)
difference (1, 2)   ===>  difference(1, 2)

Braces

A series of expressions separated by semicolons and surrounded by curly braces is syntactical sugar for a "do" function call. This gives us a simple syntax for expressing a sequence of things that executed in order (which is what the "do" special form is for). So this:

{ print 1, 2; print 3, 4 }

is parsed as:

do (print (1, 2), print (3, 4))

Note that the semicolons are separators, not terminators. The last expression does not have one after it.

Precedence

The precedence rules follow SmallTalk. From highest to lowest, it is prefix functions, operators, keywords, then ",". So, the following:

if: a < b then: c, d

is parsed as:

(if:then:(<(a, b), c), d)

That's the basic syntax. The Lark parser reads that and immediately translates it to the much simpler core syntax.

Running Lark

Lark is a Java application stored in a single jar, you can run it by doing either:

$ java -jar lark.jar

Or, if you have bash installed, just:

$ ./lark

This starts up the Lark REPL, like this:

lark v0.0.0

Type 'q' and press Enter to quit.

From there you can enter expressions, which the interpreter will evaluate and print the result.

You can also tell lark to load and interpret a script stored in a separate file by passing in the path to the file to lark:

$./lark path/to/my/file.lark

Built-in functions

So what can you actually do with it? Not much, yet. Only a few built-in functions are implemented:

'

The quote function simply returns its argument in unevaluated
form. Ex:

> ` print 123
: print 123
(note: does not call print function)

do

If the expression is anything but a list, it simply evaluates it
and returns. If it's a list, it evaluates each item in the list,
in order, and then returns the evaluated value of the last item.

In other words, it's Lark's version of a { } block in C. Ex:

> do (print 1, print 2, print 3)
1
2
3
: ()

print

Simply evaluates the argument and prints it to the console.

=>

Creates an anonymous function (a lambda). <params> should either
be a single name or a list of parameter names. <body> is an
expression that forms the body of the function. Ex:

> (a => do (print a, print a)) 123
123
123
: ()

def: is:

Binds a value to a variable in the current scope. <name> should
be a single name. <expr> will be evaluated and the result bound
to the name. Ex:

> def: a is: 123
: ()
> a
: 123

if: then: if: then: else:

Evaluates the condition. If it evaluates to true, then it
evaluates and returns <expr>. If it evaluates to false, it will
either return () or, if an else: clause is given, evaluate and
return that. Ex:

> if: true then: print 1 else: print 2
2
: ()

bool? int? list? name? unit?

Evaluates <expr> then returns true if it is the given type. Note
that ().list? returns *true* because unit is the empty list. Ex:

> 123.int?
: true
> ('foo).name?
: true
> 123.list?
: false

Evaluates <expr>, expecting it to be a list. Returns the item
at the <int> position in the list (zero-based). Basically, this
means an int is a function you can pass a list to to get an item
from it. Ex:

> 0 (1, 2, 3)
: 1
> (4, 5, 6, 7).3
: 7

count

Evaluates <expr>, expecting it to be a list. Returns the number of
items in the list. Ex:

> (1, 2, 3).count
: 3

That's it so far. Like I said, very early...