# Syntax and Style
Deft has a small surface grammar with a few complementary syntactic
forms. This page covers the four call shapes, the rules that pick which
one to use, and the idiomatic style that runs through the rest of the
documentation.
## The Three Call Forms
Deft has three syntactic forms for function calls, plus parentheses for
grouping infix expressions.
| Form | Example | Use |
|------|---------|-----|
| **Prefix `[...]`** | `[trim $str]`, `[split $str ":"]` | Canonical. Works for any arity. Required when grouping a call inside a larger expression. |
| **Postfix `$var(...)`** | `$str(trim)`, `$items(first)`, `$xs(2..5)` | Sugar for a single-arg call on a variable — same as `[trim $str]`. Reads naturally for receiver-style access. A range argument `(a..b)` slices instead of calling (see below). |
| **Bare (no brackets)** | `echo $name`, `println "hi"` | TCL-style, parses identically to `[echo $name]`. Allowed at statement position so function calls sit naturally beside control-flow keywords (`if`, `try`, `for`, `def`) which are also bare. |
### Critical rule
Bare parentheses `(...)` are **never** function calls. They group infix
expressions only: `($a * $b)`, `($n == 0)`. The form `(trim $str)` is a
syntax error.
# ✅ Preferred [trim $str] # prefix $str(trim) # postfix (same thing) echo "hello" $name # bare top-level statement ($price * $quantity) # infix expression # ❌ Invalid (trim $some-str) # parens are NOT function calls
## When To Use Which
### Bare form — top-level statements
Use bare form at statement position so function calls sit naturally beside
control-flow keywords. Anything that's a one-shot statement reads better
without bracket noise.
echo "Count: $xs(len)" println "result:" $result fs/write "out.txt" $content http/serve $router 8080
### Postfix form — single-arg receiver call on a variable
Use `$var(call)` for the common receiver-style access patterns: queries
and transforms on a single value.
set length $name(len) # length of a string or collection set first $items(first) # first element set upper $name(upper) # upper-case a string set parts $str(split ",") # split a string set rounded $n(round) # round a number
### Index and slice access — `$x(a..b)`
A range argument in postfix position slices a string or collection
instead of calling a function — `"hello"(1..3)` is `"el"`. The same
syntax works on string and list literals, variables, and `~` field
chains. The `..` here is the range operator, not a spread.
set s "hello world" $s(0..5) # => "hello" end is exclusive: indexes 0..4 $s(2..) # => "llo world" open end → through the last element $s(..5) # => "hello" open start → from the first $s(-3..) # => "rld" negative bounds count from the end def xs @{ 10 20 30 40 50 } $xs(1..3) # => @{ 20 30 } $xs(..2) # => @{ 10 20 } $xs(3..) # => @{ 40 50 } $xs(0..-1) # => @{ 10 20 30 40 } (all but the last) $xs(2) # => 30 plain index — equivalent to nth "string"(0..1) # => "s"
Ends are exclusive: `(0..n)` includes indexes `0` through `n-1`.
Negative bounds count back from the end of the string or collection.
Open-ended ranges (`a..` and `..b`) need no placeholder number.
Postfix also chains for field access via `~`:
set role $user~profile~role # nested field access set name-len $user~name(len) # postfix on a field
### Prefix form `[...]` — anything else
Use `[...]` whenever:
- The call is part of a larger expression (assignment, argument, condition).
- The call has more than one argument that doesn't fit the receiver pattern.
- The function is slash-namespaced (`fs/read`, `http/get`, `sqlite/query`) —
postfix works on variables, but slash-name receiver calls are awkward.
set contents [fs/read "config.txt"] set rows [sqlite/query $db "SELECT * FROM users"] set result [math/sqrt (($x * $x) + ($y * $y))] set found [find $xs { ($it > 10) }]
### Infix inside `(...)` — arithmetic and comparison
Arithmetic (`+ - * / mod`), comparison (`== != < > <= >=`), boolean
(`and or`), and bit ops live inside parentheses:
set total ($price * $quantity) set next ($i + 1) if ($n == 0) { return 1 } if ($age >= 18 and !$minor?) { ... } set masked ($flags & 0xff)
Operators are **infix-only inside `(...)`** — they are not first-class
functions and cannot be called with `[...]`. `[+ $a $b]`, `[== $n 0]`,
`[and $x $y]`, `[or $x $y]` are all invalid: they happen to compile for
exactly two operands (a parser quirk), but anything else — `[+ 1 2 3]`,
`[or a b c]` — fails at runtime, which is the opposite of what a
Lispler would expect. Always use the infix form.
### Negation — unary `!`
`!` is a unary operator usable in every expression position, mirroring
unary `-`. It binds tight (like `-`), so `(!$a == $b)` is `(!$a) == $b`;
to negate a whole comparison, group it: `(!($a == $b))` — or just use
`!=`. Parens are optional around a lone negation where the grammar
already takes an expression:
if !$flag { set msg "off" } if ![nil? $m] { return } if ($a and !$b) { ... } set y (!!$x) # double bang set y [not $x] # the word form is also a call (one arg)
The old paren-prefix spelling `(not x)` is a parse error — the
diagnostic points at `!`. The `not` keyword survives in two places:
the call form `[not x]` and negative match patterns (`not :admin` in
`match` arms).
> `[...]` is prefix-only; `(...)` is infix-only. They cannot mix:
> `(* $a $b)`, `[+ $a $b]`, `[== $n 0]`, and `(trim $str)` are all
> syntax errors.
## The Spread Operator
`..$xs` splices a list's elements in place — either into a function
call's arguments or into a vector literal. The `..` prefix is the
spread marker; the expression after it must evaluate to a list.
def f {|a b c| ($a + $b + $c)} def xs @{1 2 3} [f ..$xs] # => 6 — passes 1, 2, 3 as three args [f 0 ..@{1 2}] # => 3 — fixed args mix freely with spreads [f ..@{1 2} 3] # => 6 — the spread expression needn't be a variable set ys @{0 ..$xs 4} # vector literal: @{0 1 2 3 4}
Notes:
- Spread is a **runtime** splice — the list is flattened when the call
executes, not when the code is parsed. Don't confuse it with the
compile-time `[splice $list]` used inside macro quotes (see
[Macros](macros)).
- A spread argument must be a list; anything else raises a runtime
error (`spread argument must be a list`).
- Spread is accepted in prefix calls and `@{...}` vector literals only
— postfix calls (`$f(...)`) and `%{...}` map literals don't support
it.
- In list **patterns**, `..$rest` (or `| $rest`) collects the tail of
the matched list — see [Grammar](grammar).
## Pipelines
The `|>` operator threads a value through a chain of single-arg
transformations. Each step receives the previous step's result as its
implicit final argument.
# Multi-step collection transform @{ 1 2 3 4 5 } |> filter { ($it % 2) == 0 } |> map { $it * 2 } |> collect # => @{ 4 8 } # String processing "$input" |> trim |> lower |> (split ",") |> map trim |> join "|"
**Prefer pipelines over nested calls or intermediate `def` bindings** for
multi-step transforms. Reach for `[...]` only when the call is part of a
larger expression.
For transducer-style reuse (same pipeline applied to multiple
collections or sinks), see [Transducers](transducers).
## State and Assignment
`set` rebinds a local; `def` introduces one at module scope.
def base-url "https://api.example.com" # module-scoped def counter 0 def bump {|amount^number| set counter ($counter + $amount) # rebind within the def $counter }
`<<` mutates a collection local in place and returns the mutated
collection — useful for accumulation:
def out @{} for x in $xs { if ($x > 0) { $out << $x } # mutates $out, returns it }
For numeric increment, prefer `incr`:
incr i # preferred set i ($i + 1) # works, but unidiomatic
## String Interpolation
Double-quoted and backtick strings support five interpolation forms:
| Form | Use | Example |
|------|-----|---------|
| `$name` | Simple variable | `"hello $name"` |
| `$name~field` | Variable with field chain | `"$user~name"` |
| `$name(args)` | Postfix call | `"count: $xs(len)"` |
| `$(expr)` | Infix expression | `"total: $($qty * $price)"` |
| `$[func args]` | Prefix function call | `"count: $[len $xs]"` |
The infix form mirrors the infix-grouping syntax outside strings — `(` and `)`
group arithmetic and comparison everywhere. A bare identifier inside `$(...)`
also works as a variable lookup (e.g. `$(name)`).
Escape with `\$` for a literal `$`. Literal `[`, `]`, `{`, `}`, `(`, `)` are
safe inside double-quoted strings without escaping.
def name "Alice" set age 30 set role :admin set msg "Hello $name, age $($age + 1), role: $[upper $role]" # => "Hello Alice, age 31, role: ADMIN" set hint "Press \[Enter] to continue (\$5 fee)"
## Naming Conventions
### Predicates — `X?` suffix
Predicates use the `X?` suffix (Clojure-style), never the `is-X` prefix.
This applies to stdlib functions (`nil?`, `empty?`, `even?`, `starts-with?`,
`ends-with?`, `chars/word?`, `chars/digit?`, `chars/alpha?`) and to
user-defined predicates (`attached?`, `image-ext?`, `fresh?`).
def admin? {|user| :admin in? $user~roles} # ✓ predicate def is-admin {|user| ...} # ✗ avoid
### Math — split by frequency, not by domain
Numeric stdlib functions are split two ways; the boundary is **frequency of
use**, not "domain":
- **Bare** (no namespace prefix): the handful of general-purpose ops that
show up in everyday code — `abs`, `floor`, `ceil`, `round`, `min`,
`max`, `clamp`, `random`, `mod`, `int`.
- **`math/` namespace**: specialised / rarely-used ops — `math/sqrt`,
`math/pow`, `math/sum`, `math/avg`, `math/sin`, `math/cos`,
`math/lerp`, `math/random-range`, `math/pi`, etc.
A new numeric stdlib function goes bare if it'll be reached for often
(`median` would probably warrant bare `median`), and under `math/`
otherwise. The split mirrors Python's builtins-vs-`math` module split
and keeps the top-level namespace uncluttered.
### kebab-case for identifiers
Use `kebab-case` for variable and function names (`my-cool-function`,
`user-name`). Type names use `PascalCase` (`%Point`, `%User`).
Keywords use `kebab-case` (`:on-init`, `:row-count`).
## Comments
`#` starts a line comment that runs to the end of the line. `#|` opens
a **block comment** that runs until the matching `|#`; it can span any
number of lines, and its content is raw text — no `#` prefix needed on
each line. Block comments **nest**: `#|` inside `#| ... |#` bumps the
depth, and only the matching number of `|#` closers ends the comment.
# Compute the Euclidean distance from origin. def distance {|p^Point| [math/sqrt (($p~x * $p~x) + ($p~y * $p~y))] } #| Block comments span lines with no per-line #: handy for long explanations, license headers, or temporarily disabling a chunk of code. They nest too: #| inner #| deepest |# back out |# |#
An unterminated block comment (`#|` with no closing `|#`) is a lexer
error, not silently accepted.
For documenting a function, prefer `@doc` inside the body (see
[Functions](functions) and [Metaprogramming](metaprogramming)):
def distance {|p^Point| @doc "Euclidean distance from origin." [math/sqrt (($p~x * $p~x) + ($p~y * $p~y))] }
## Appending To Collections
`<<` mutates the receiver in place when the receiver is a function-local
variable. Use the bare form `$xs << x` for accumulation — it returns the
mutated collection, so it works both as a statement and in expression
context.
# Preferred — mutates $xs in place, returns the (mutated) collection $xs << 4 # Equivalent, slightly more verbose — still works, still mutates set xs ($xs << 4) # Pure-functional — does NOT mutate, returns a new collection. Use when you # need to preserve the original. [append $xs 4]
For multi-value appends in one statement, use separate `$xs << a`,
`$xs << b`, ... statements. Chained infix `$xs << a << b` does not
currently propagate the local through the chain.
## Mutating Enclosing Locals From Closures
Mutating a local from inside a closure (`{ … }`, `each`/`map`/`filter`
body, `defer`, etc.) propagates back to the enclosing scope via
heap-boxed capture-by-reference. `<<`, `set`, and `incr` all work
transparently across the boundary.
# Accumulates into $out — bare `<<` mutates the captured local in place @{ 1 2 3 } |> each { $out << $it } # Also works inside plain `for` loops (the body is inlined into the same frame) for x in $xs { $acc << $x }
**Performance note:** locals captured by any inner closure are boxed
(one `ObjBox` per captured slot, allocated once at function entry). Hot
numeric loops that don't involve closures keep using the fast
`load_local`/`store_local` opcodes with zero overhead. If you need
maximum throughput in a tight loop, factor the loop body into its own
`def` so the loop variables aren't captured by anything.
## Quick Style Cheat-Sheet
# ✅ Preferred set total ($price * $quantity) # infix expression set length $name(len) # postfix on a variable $items |> filter even? |> map { $it * 2 } |> collect # pipeline echo "hello" $name # bare top-level incr i # numeric increment # ❌ Avoid set length [len $name] # use postfix $name(len) set i ($i + 1) # use incr i for unit increments # ❌ Invalid (trim $some-str) # parens are NOT function calls [+ $a $b] # operators are infix-only, not prefix calls [== $n 0] # use ($n == 0) [or $a $b] # use ($a or $b)
## Single-Line `if`
For simple conditions and expressions, the single-line `if` form reads
best:
set msg [if logged-in? "Welcome!" "Please log in."] if ($n == 0) { return 1 } if ($count > 10) { set msg "too many" }
For complex logic, use the multi-line form with `else`:
if logged-in? { "Welcome!" } else { if admin? { "Welcome, admin!" } else { "Welcome, user!" } }
See [Control Flow](02-control-flow.md) for the full set of conditionals,
loops, and cleanup forms.