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 |
|---|---|---|
|
integer |
|
|
integer |
hex digits are lowercase |
|
float |
|
|
float |
exponent form |
|
float |
trailing |
|
fraction |
|
|
decimal |
trailing |
|
complex |
trailing |
Strings are double-quoted, with optional single-character prefixes attached directly to the opening quote:
Written |
Meaning |
|---|---|
|
plain string |
|
format string – interpolates |
|
raw string – no backslash escapes |
|
bytes literal – ASCII text plus escapes for any byte ( |
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 |
|---|---|
|
Python booleans |
|
Python |
|
|
|
the empty list, |
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 |
|---|---|---|
|
|
a tuple |
|
|
a list |
|
|
a dict |
A #dict spine is a sequence of entries. Four entry forms are accepted:
Entry |
Example |
Reads as |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
|---|---|
|
|
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 |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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.
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 |
|---|---|
|
|
|
|
|
|
|
|
|
a method call on a literal |
|
|
|
|
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.
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.