# 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.