Grammar

Koskari source combines layout syntax (indentation-based blocks and lines in .ki files) with s-expressions (parenthesized forms). The reader turns both into list structure; the compiler then expands macros and dispatches on special forms.

S-expression forms

Inside any (, [, or {, and at the top level of a form, the base reader applies. It reads atoms and lists and a handful of prefix and collection syntaxes. The everyday shapes are in Model of evaluation.

Whitespace is the only separator. Tokens are split on spaces, tabs, and newlines (and on the delimiter characters). Punctuation that other languages use to separate items – most notably the comma – is an ordinary atom character here, so #[1, 2, 3] reads the symbols 1, and 2, followed by the integer 3, not three integers. Separate list items with spaces.

Atoms

An atom is one whitespace- or delimiter-bounded token. The reader matches each token against the literal patterns below; a token that matches none of them is a symbol.

Numbers. Each numeric shape is a distinct literal:

Written

Reads as

Notes

42, -7

integer

0xff, 0o17, 0b1010

integer

hex digits are lowercase

3.14, .5, 6.

float

1e9, 2.5e-3

float

exponent form

3f, 1.0f

float

trailing f forces float

1/2

fraction

fractions.Fraction

9.99d, 5d

decimal

trailing d, decimal.Decimal

3+4j, 2.0+1i

complex

trailing i or j

Strings are double-quoted, with optional single-character prefixes attached directly to the opening quote:

Written

Meaning

"text"

plain string

f"x is {x}"

format string – interpolates {...} expressions

r"a\d+"

raw string – no backslash escapes

b"bytes"

bytes literal – ASCII text plus escapes for any byte (b"\xff"); a literal non-ASCII character is an error

A prefix is recognised only when it sits immediately before the quote (f"...", not f "..."). A trailing hyphen continues a string into the next quoted segment; see Suffix syntax.

Symbols are names: any token that is not a number, string, keyword, or singleton. Symbols may contain letters, digits, -, _, /, and most punctuation (+, <, list->vector, None? are all symbols). A token that matches a numeric pattern is a number; any other token is a symbol, so 2x and 1st are symbols.

Keywords are self-evaluating name markers written with a colon on either end. :foo and foo: both read as the keyword foo; the trailing-colon spelling is what carries keyword arguments in calls and headers.

Singletons and constants:

Written

Reads as

True, False

Python booleans

None

Python None

...

Ellipsis

(), []

the empty list, nil

nil is both the empty list and the terminator of every proper list.

Lists

Parentheses group forms into a pair chain (a cons list):

(+ 1 2 3)         ;; a three-argument call
(a b (c d) e)     ;; nested lists
()                ;; the empty list, nil

A dotted pair places a value directly in the tail with .:

(k . v)           ;; one cons cell: car k, cdr v
(1 2 . 3)         ;; improper list ending in 3

Square brackets [ ] read with the same grammar as ( ) and produce a value-equal chain; the reader records the bracket style so a call-position lint can flag it, but the compiler treats the two the same. See Brackets and the call convention.

Collection literals

A # prefix on a delimiter reads a container literal whose items are whitespace-separated:

Written

Reads as

Builds

#(1 2 3)

(#tuple 1 2 3)

a tuple

#[1 2 3]

(#list 1 2 3)

a list

#{"a": 1 "b": 2}

(#dict (#tuple "a" 1) (#tuple "b" 2))

a dict

A #dict spine is a sequence of entries. Four entry forms are accepted:

Entry

Example

Reads as

key: value

#{name: n}

(#dict (#tuple name n))

KEY : VALUE

#{"name": n}, #{(str 1) : "one"}

(#dict (#tuple "name" n))

**: EXPR

#{**: other}

(#dict **: other)

(key . value)

#{("a" . 1)}

(#dict ("a" . 1))

Keys are ordinary expressions, so a key: key lowers to the name key, not the string "key" – quote or write the string explicitly (#{"name": n}) when you want a literal string key. The KEY : VALUE spelling takes any key expression, with the : standing alone as its own token. A dotted entry evaluates both halves the same way, and its value must be an atom: ("a" . (+ 1 2)) is the proper list ("a" + 1 2), which compiles as a call to "a".

Alternating key value items are not an entry form. #{"a" 1} reads as (#dict "a" 1), and the call raises ValueError: build_dict items must be (key, value) pairs.

Classification is all-or-nothing: one entry the reader cannot classify leaves the whole spine raw, so mixing a dotted entry with key: value entries fails even though each form works on its own.

Thunk braces

Braces read their contents as the body of a thunk:

Written

Reads as

{(+ %1 %2)}

(gensym-thunk (+ %1 %2))

The body is a form sequence, so the two-argument thunk is spelled {(+ %1 %2)} – the inner parentheses are the call, and the braces only delimit the body. The %N placeholders become the formals. See gensym-thunk.

>>> (var add {(+ %1 %2)})
>>> (add 3 4)
7
>>> (list (map {(* %1 %1)} #[1 2 3]))
#[1 4 9]

Prefix reader sugar

Seven prefixes wrap the form that follows them:

Written

Reads as

'form

(quote form)

`form

(quasiquote form)

#'form

(quote-syntax form)

#`form

(quasiquote-syntax form)

,form

(unquote form)

,@form

(unquote-splicing form)

-'form

(datumq form)

In layout mode the four quoting prefixes also take a block head: 'head:, `head:, #'head:, and #`head: quote the indented block as its #block: cons; see Reader and layout syntax.

, and ,@ are meaningful only inside a quasiquote. -'form writes form raw: in a macro template it is emitted unmarked, and -'-'form nests one level per template; see Hygiene. &form reads as (ref form) for by-reference passing; a bare & with nothing attached is the ordinary & symbol.

The unary operators -, +, and ~ are prefixes too. Attached to a name, a digit, or an opening delimiter, each reads as the one-argument call of its operator: -x is (- x), +foo.bar is (+ (attr foo bar)), ~(f y) is (~ (f y)), and ~5 is (~ 5). The operand is read as a whole form, so -foo["a"] is (- (item foo "a")). The one-argument builtins call the Python unary methods, so +x reaches __pos__, which is how a Decimal rounds to its context and a Counter drops its non-positive counts. A - before a digit is a signed number, so -1 and -.5 are literals rather than calls. An operator standing alone is its bare symbol, and one followed by any other character, ->, -=, --x, and += included, is an ordinary symbol; a - before a quote is the -' prefix above, whatever the pragma. The unary prefixes are governed by the reader/unary pragma.

Fenced literals

A triple-backtick fence captures its body as raw text and reads as a compile/apply-fence form naming the format given on the opener line:

(apply-fence NAME RAW META)

The compiler resolves fence/<name> in the module namespace and compiles the source the handler returns in place of the fence. The reader/fences pragma governs the syntax; with it off, a backtick keeps only its quasiquote reading.

var shout ```bash
echo -n "$1" | tr a-z A-Z
```

(print (shout "koskari").text)

See Fence handler reference for the handlers that ship and for defining new ones.

Comments

A ; begins a line comment that runs to the end of the line; the doubled ;; is a common style and reads the same. Comments may appear between any tokens and are skipped by the reader.

Suffix syntax

Three pieces of syntax attach to the value just read, wrapping it rather than starting something new. They read left to right and chain freely with each other:

Written

Reads as

foo[0]

(item foo 0)

grid[y][x]

(item (item grid y) x)

obj.value

(attr obj value)

(get-fn x).baz

(attr (get-fn x) baz)

("hello".upper)

a method call on a literal

foo[0].bar

(attr (item foo 0) bar)

"a"- " b"

(#str "a" " b")

Attachment is strict. The suffix character must follow the value with no space between them. A space ends the value instead, which is what keeps foo [a] two separate forms and leaves the leading-dot method call (.baz obj) alone.

Index brackets (foo[k]) read an item access, and a group may carry several keys (foo[1 2]). An empty group is an error.

Dots read an attribute. This is the same expansion whether the dot sits inside a single name (obj.value) or attaches to a form or literal ((get-fn x).baz, "".join) – the dot is the attribute read, and enclosing parentheses are what make it a call. A dot with no name after it is an error.

A trailing hyphen on a string continues that literal into the next quoted segment, so a long string can be broken across lines without restructuring the call around an explicit join:

(print "This is one"-
       " long string broken"-
       " up across lines")

Segments may be separated by any whitespace, including newlines and blank lines, and comments in between are skipped. Each segment carries its own prefix, so an f segment interpolates and an r segment stays raw. All-literal chains fold to a single constant, so the join costs nothing at run time.

Each of the three is governed by its own setting – reader/index-brackets, reader/dot-attrs, and reader/string-segments. See Pragmas and feature settings for turning them off.

Decorator lines

A layout line beginning with @ is a decorator line. One or more @ lines, followed by the form they decorate, read as (compile/decorate (DECORATOR ...) FORM):

def loud [f]:
  lambda [*: args]:
    (print "calling")
    (f *: args)

@loud
def greet [who]:
  (print "hello" who)

(greet "world")

The call prints calling, then hello world.

Every @ line must begin at the same column as the decorated form; a mismatched column is the error decorator lines must align with the decorated form. An @ with nothing after it on the line is empty decorator line.

The reader/decorators pragma governs this layout surface only. With it off, the compile/decorate macro is still available in its s-expression spelling.

Layout handlers

Per-head read-time lowering (layout/if, layout/block, …) is documented in the Layout handler reference.

See also: Koskari and s-expressions for the layout rules beside their s-expressions, and Reader and layout syntax for the normative syntax rules.