Syntax

Atomo has a rather simple-to-use syntax, enabled by the powerful Parsec library it uses for parsing.

General Rules

Atomo is whitespace-sensitive. Operators and keywords must be surrounded by whitespace, and indentation provides hints to the parser when you don't want to use semicolons. For example; 1/2 is a Rational, while 1 / 2 is integral division. foo-bar is an identifier, while foo - bar is subtraction.

Requiring whitespace around operators enables the use of more symbols in identifier names, allowing pleasant conventions like questions marks (e.g. empty?) and exclamation points (e.g. do-something-destructive!) for indicating the behaviour of a method.

Atomo's whitespace indentation rules are similar to Haskell's - they are not as strict as Python's, you just increase the spacing amount to indicate continuing the previous line, and continue that indentation amount to indicate continuing the current "block" of code.

Using significant whitespace, this block:

{ "hi" print
  goodbye
  2 + 2
} call

is equivalent to:

{ "hi" print; goodbye; 2 + 2 } call

With these simple line-continuing rules in place, you can spread a single chain of messages across multiple lines:

something
  foo
  (+ 2)
  sqrt

Which is parsed as:

something foo (+ 2) sqrt

The same goes for keyword messages:

if: x
  then: { y }
  else: { z }

It is recommended that continued keyword roles are indented relative to the initial keyword, though this is not necessary. As long as it is indented more than the first role, the message is "continued."

These are all OK:

0 up-to: 5
    do: { n | n print }

something up-to: 5
  do: { n | n print }

something up-to: 5
            do: { n | n print }

something
  up-to: 5
    do: { n | n print }

The same rules apply to operators, which are equivalent to keywords:

x foo =
  { a = 1
    a + 1
  } call

Which is parsed as:

x foo = { a = 1; a + 1 } call 

Indentation Guidelines

Two spaces for indentation is highly recommended, as it often aligns things neatly on one column. For example, a block's arguments, its contents, and the message sent to the block all end up on the same column:

{ foo |
  do-something: foo
  foo * 2
  [ foo
    foo + 1
    foo * 2
  ] length
} call: 2

Using this convention you also avoid stray closing braces, which would be common with other indentation styles.

Usually, don't do this:

foo := [1, 2, 3] map: { n |
  -- ...
}

as it indicates the final closing brace is somehow related to foo, when really it's just the ending delimiter for the block passed to map:.

There are some exceptions, though; for example, if a block's body is more important than the message being sent, or if it's used structurally as part of a DSL, it is highly recommended to just use the hanging-brace style, so you don't have to indent too far. match:, condition:, case-of: and many more are examples of these. Another "rule of thumb" is, if the block takes arguments, you should use the two-space-indentation style. Otherwise, it's fair game.

Comments

Atomo uses the same comment syntax as Haskell: -- for linewise comments, {- -} for block comments (which can be nested).

1 -- The number, "one."
({- Blah blah blah, {- yo dawg -}, fizz buzz! -} "foo") print

Literals

integers

1, -1, 0xdeadbeef, 0o644, -0x10, -0o10 ...

doubles

1.0, -1.5, 1.5e10, 1.4e-3, -1.4e4...

characters

$a, $b, $\n, $\1234 (unicode number)...

Escape codes supported (in addition to numeric escapes):

ascii-2:
  \b \t \n \v \f \r \SO \SI \EM \FS \GS \RS \US ␣ (space)
  \BS \HT \LF \VT \FF \CR \SO \SI \EM \BS \GS \RS \US \SP

ascii-3:
  \NUL \SOH \STX \ETX \EOT \ENQ \ACK \a \DLE \DC1 \DC2
  \DC3 \DC4 \NAK \SYN \ETB \CAN \SUB \ESC \DEL
rationals

1/2, 30/500, -1/2, 1/-2, ...

strings

"", "foo", "fizz \"buzz\"", containing the above-mentioned escape codes

macro-quotes

r{\d+}m, w{foo bar}, ...

See Macro Quotes.

General format:

name(d1)content*(d2)[flag*]
  where
    name = identifier
    content = arbitrary, raw text
    flag = case-sensitive alphabetic character
    (d1) = one of ({["$|`'~@
    (d2) = the matching delimiter for (d1)

Note that a large number of delimiters may be used; these are punctuation characters that are already reserved for other uses, so that the name will not "flow" into them, because they're not valid characters for use in an identifier. That is, allowing / as a delimiter, for example, would lead to parsing ambiguity with foo/ bar/.

The content between the delimiters is parsed with no escapes except for the delimiters. That is, r"foo\"bar" is the same as r{foo"bar}, and r"foo\bar" is sent with "foo\\bar" as the contents.

boolean

True and False

lists

[], [1], [1, 2], [1, $2, "three"], ...

tuples

(), (1, 2), (1, $2, "three"), ...

Note that there is no such thing as a tuple with only one value.

expressions
quoted

An apostrophe (') before an expression "quotes" it, turning it into an expression literal:

Example:

'1
'a
'(1 + 1)
''(1 + b)
'{ a = 1; a + 1 }
quasiquoted

Atomo supports quasiquotation as seen in most Lisps, most similarly Clojure. A backquote (`) begins a quasiquote, inside of which you can use tilde (~) to "unquote".

These can be nested infinitely; unquoting works inside of aggregate expressions such as lists, blocks, and definitions.

Example:

> `1
'(1)
> `(1 + ~(2 + 2))
'(1 + 4)
> ``(1 + ~~(2 + 2))
'(`(1 + ~4))
> `{ a = ~(2 + 2) }
'({ a = 4 })
> `[1, 2, ~(1 + 2)]
'([1, 2, 3])

Note that unquoting too far throws an @out-of-quote: error.

Example:

> `~~(2 + 2)
ERROR: <error @(out-of-quote: '(2 + 2))>

Identifiers beginning with ! inside of a quasiquote are decorated with a clock value to ensure name collision does not occur in a macro's expansion. This is similar to Common Lisp's gensym mechanism, but it is automated and more implicit.

Example:

> a = '{ x + 2 }
'({ x + 2 })
> `{ !x | ~a } expand
'({ x:1134 | { x + 2 } })
particles

Particles begin with an at-sign (@), followed by a "partial message."

Regular single messages look like this:

@foo
@fizz-buzz

The message's target can be specified, in which case the message isn't actually "partial":

@(1 foo)
@("whatever" bar)

Optional roles in single particles require parentheses surrounding the message portion:

@(foo &bar: 2)
@(1 sqrt &fizz: _)

Keyword particles are similar:

@(print: "hi!")
@(+ 2)
@(from: _ to: 3)
@(1 + 2 &foo: 2 &bar: _)
@(this foo: _ &bar: 100)

Additionally, the roles in a keyword particle may be omitted entirely, leaving only selector names:

@from:to:
@+

These are equivalent to @(from: _ to: _) and @(+ _), respectively. Note that @(from: _) means @(_ from: _), not @(this from: _).

blocks

Blocks are surrounded by curly braces ({ }) and contain 0 or more expressions separated by semicolons or whitespace.

A block may have arguments, which are patterns separated by spaces. The arguments are placed after the opening brace, and are terminated by a pipe (|), followed by the block's contents (if any).

Example:

{ 1 + 1 }
{ a | a * 2 }
{ }
{ a b | a + b }
{ a b | }
{ "hi" print; another; 6 * 7 }
{ a (b . c) | x; y; z }

Block parsing is whitespace-aware; see General Rules.

scope

this is a reserved word that always refers to the current toplevel object.

Example:

> { a = 1; this } call
<object (delegates to 1 object)>
  a := 1
> x = this
<object (delegates to 1 object)>
  x := <object>
> { a = 1; @(them: x me: this) } call
@(them: <object (delegates to 1 object)>
          x := <object> me: <object (delegates to 1 object)>
                              a := 1)

Dispatch

Atomo's dispatch syntax is similar to the Smalltalk family, naming conventions aside. There are two types of messages to dispatch: single messages and keyword messages.

To dispatch a single message to some target, the message name is placed after the target value, separated by whitespace.

Example:

1 foo
{ 1 sqrt } call

If there is no "target" for the message, the implied target is this (the toplevel object representing the current scope).

Keyword messages are slightly different in that they have more than one target, each separated by a "keyword" which is part of one whole message. Like single messages, if there is no initial role, this is assumed.

Example:

> 3 divides?: 12
True
> 1 + 2
3
> delegating-to: Integer
<object (delegates to 2 objects)>
> (0 .. 10) (as: List) from: 3 to: 5
[3, 4]

Keyword message dispatch normally requires parentheses to chain multiple message sends, starting from the start of the chain to the end of the current dispatch. This leads to a lot of jumping around whenever you want to add another keyword dispatch:

(((1 x: a) y: b) z: c) foo

Atomo remedies this by allowing parentheses to be used inside the chain of sends itself, effectively turning keyword message dispatch into something that's as easily chained as single messages:

1 (x: a) (y: b) (z: c) foo

Messages can also have optional roles, which have the same syntax as keyword roles but are prefixed by a & (ampersand).

Example:

> [1, 2, 3] zip: [2, 3, 4] &zipper: @*
[2, 6, 12]
> 1 sqrt &foo: 2
1.0

This also works in chained message dispatches which requires parentheses to disambiguate it from the other messages:

1 sqrt (+ 3 &fizz: @buzz) (sqrt &foo: 1 &bar: 2)

macro

The macro keyword is used to define macros; the format is as follows:

See Defining Macros.

macro (pattern) expression
  where
    pattern = a message pattern
    expression = the macro's body

for-macro

The for-macro keyword is used to specify that an expression should be evaluated before the macroexpansion phase. The format is as follows:

See Macro Environment.

for-macro expression
  where
    expression = the expression to evaluate

operator

You can control how Atomo parses binary operators like + and -> via the operator keyword. The syntax is as follows:

operator [associativity] [precedence] operators
  where
    associativity = "right" | "left"
    precedence = integer
    operators = operator +

An omitted associativity implies left, and an omitted precedence implies 5 (the default). One of the two must be provided.

Example:

operator right 0 ->
operator right 8 ^
operator 7 % * /
operator 6 + -

Operator expressions, when evaluated, just return @ok.