Skip to content

Syntax & values

Every call is a parenthesized expression. Inner calls are evaluated as arguments:

(str "hello, " (str "a " "friend"))
(list one "two words" #true)

In value positions, bare words are text: 123, true, false, and nil have no special literal meaning. Double-quoted strings interpolate $name and $(...), and support \", \\, \n, \r, \t, and \$. Single-quoted strings preserve their content without interpolation or escapes. ${name} remains literal text. Outside strings, variable references use $name. Binding and assignment targets use bare names, including function parameters, loop bindings, and environment settings.

The reader creates text, scalar literals, variable references, interpolated strings, and lists of expressions. Evaluation can return text, numbers, booleans, lists, maps, nil, or native command statuses. These are different value types even though words such as true and nil themselves are just text.

(eq true true)
(not #false)
(list a "b c" #true)

The interactive results are #true, #true, and ("a" "b c" #true). Text is displayed with quotes and escapes; nil is not displayed. A file or -c script runs expressions without automatically printing their results.

(map name parsh) constructs a map and displays as (map "name" "parsh"). Maps have text keys and preserve insertion order. See maps for construction, lookup, and update commands.

Use # to create typed scalars: #1 is an int, #1.0 and #1e3 are floats, #true and #false are booleans, and #nil is nil. Plain 1, false, and nil remain text. For example:

(let config {retries #3 timeout #1.5 enabled #false})
(list (typeof #1) (typeof 1) (eq #1 1))

The list displays as ("int" "text" #false). Numeric and boolean results display with #, including inside collections; nested nil displays as #nil. A top-level nil result stays silent. str, interpolation, external arguments, and environment values use ordinary text without the prefix: (str #1 #false) returns "1false".

All unquoted words beginning with # are reserved. Invalid forms such as #flase, #123abc, or a lone # are reader errors. Quote text such as "#fff"; abc#1 remains ordinary text. A shebang is allowed at the very start of source. Structured comments do not validate # literals, but still check strings and balanced delimiters. Literal spelling must be contiguous. Numbers use the existing signed decimal, fraction, and exponent syntax, with i64 integers and finite f64 floats; integer overflow never falls back to floating point. Boolean and nil names are lowercase, with no #t/#f aliases.

Literals can appear directly at the prompt or at script top level. Scripts do not print their results automatically. A literal is source syntax: quoted text, variable contents, and command output are never parsed again, and #$name is not a conversion. Use int, float, or bool for runtime conversion.

A command head must evaluate to text or a function. (#1) and a numeric variable used as a command name are errors; use str to explicitly construct a text command name. #false is an ordinary false value, while (false) retains its external command failure behavior. Numeric values and nil are not implicit conditions.

[...] is shorthand for (list ...), and {...} is shorthand for (map ...):

[]
[one "two words" #true]
{}
{name parsh items [one two] nested {enabled #true}}
(let items [a (; ignored) b])

Elements are separated by whitespace. Maps take alternating keys and values, with the same text-key and duplicate-key rules as map. Elements, keys, and values evaluate once from left to right. Collections can nest and appear anywhere a call can. Interactive results keep their existing format: ("one" "two words" #true) and (map ...).

All three delimiter pairs, (), [], and {}, must match, including inside structured comments. Only (; ...) introduces a comment; [; a] contains two text values. Outside strings, brackets and braces delimit expressions even without whitespace. Quote literal text and glob patterns containing them, for example (glob 'file[12].txt'). Brackets and braces inside strings are ordinary text, and ${name} remains literal text.

At the prompt, type or paste a collection directly. Enter can supply one final matching ] or } when that completes the entire input; unfinished nested structures continue on a new line. The gray closing delimiter is only a display hint until submission.

(let name parsh)
(str "hello, $name")
(str "$(get name)_suffix")
(str "sum=$(+ #1 #2)")
(str "nested=$(str "a" 'b')")
(str "cwd=$(pwd)")
(str '$name $(+ 1 2)\n')
(str "\$name \$(+ 1 2)")

$name reads the longest valid variable name. Use $(get name) when text such as _suffix follows the name. ${name} has no special meaning, and a $ that does not start a variable name or $( remains text.

The parentheses in $(...) are an ordinary call expression. $(name) invokes the zero-argument function or command name; $(get name) reads the variable. All existing expressions are supported, including nested calls, control flow, and structured comments. Inner strings use normal parsh syntax without extra escaping for the outer string. The pwd example needs a command execution host.

Parts evaluate once from left to right. Each parenthesized part has its own lexical scope, while let binds in the surrounding expression scope; side effects and errors otherwise match ordinary calls. Scalars convert as str does: numbers and booleans become text, nil becomes empty text, and lists and maps are errors. External commands capture UTF-8 stdout and remove trailing LF characters; stderr keeps its destination. Errors stop the current batch. The resulting text stays one argument, and inserted values are never parsed or interpolated again.

Ordinary single quotes preserve everything until the next single quote, including backslashes, dollar signs, and newlines. Use double quotes to include a single quote character. Adjacent quoted and bare expressions do not concatenate. To pass a literal $name or $(...) to another program such as sh -c, use single quotes or escape the dollar with \$. Text display also escapes dollars so copying a displayed string does not trigger interpolation.

Triple quotes let strings contain individual quotes or pairs of quotes of the same kind without escaping them. """...""" supports the same interpolation and escapes as ordinary double quotes; '''...''' preserves all content literally:

(str """hello, "$name"
sum=$(+ #1 #2)""")
(str '''literal '$name'
$(pwd)\n''')

All newlines, indentation, and leading or trailing whitespace are preserved, including a newline immediately after the opening delimiter. Only three matching quotes end the string. In triple double quotes, \" escapes a quote, so \""" inserts three literal double quotes. Six consecutive matching quotes form an empty string. Adjacent strings remain separate expressions.

123 is text, while #123 is an integer. Arithmetic operators +, -, *, / and numeric comparisons =, !=, <, <=, >, >= accept only runtime int or float values, including mixed numeric types. Text is never parsed by an operator: (+ 1 #2) is a type error; (+ (int 1) #2) returns #3. Convert environment variables, script arguments, and captured command output explicitly with int or float before using them as numbers.

Expression Interactive result
(+ #123 #2) #125
(+ (float "1.5") #2) #3.5
(/ #5 #2) #2.5
(/ #8 #2) #4.0
(str 00123) "00123"
(eq (+ #1 #2) 3) #false
(eq #3 #3.0) #false
(= (+ #1 #2) #3) #true
(= #001 #1.0) #true
(!= #1 #1.5) #true
(< #2 #10) #true
(<= #3 #3.0) #true
(> #10 #2) #true
(>= #2.5 #3) #false

Literal forms include #+001, #-12, #.5, #1., and #2.5e1. Integers use i64; a decimal point or exponent selects f64. Leading zeroes are decimal, not octal. Out-of-range integer literals are errors, not floats. For runtime text, int and float trim and parse completely; numeric operators do not. Booleans, nil, lists, and maps cannot be used as numbers.

Arguments are evaluated once from left to right; arithmetic operations fold left. + and * accept any number of arguments, with (+) returning integer 0 and (*) returning integer 1. - and / need at least one argument; (- #5) returns -5 and (/ #4) returns 0.25. Integer addition, subtraction, and multiplication check for overflow. Each step involving a float uses f64; division always returns a float. A later float does not prevent an earlier integer step from overflowing.

eq compares types and values, including elements inside lists and maps; map insertion order does not affect equality. Numeric = tests equality and != tests inequality; <, <=, >, and >= test order. Each numeric comparison takes exactly two arguments and returns a boolean. Comparisons require int or float values and compare them by value, preserving integer precision even for large mixed operands: (> #9007199254740993 #9007199254740992.0) is true. Float comparisons are exact, with no tolerance: (= (+ #0.1 #0.2) #0.3) is false because f64 has binary rounding error. Arithmetic involving floats can also lose large integer precision.

(let count (+ #1 #2))
(if (>= $count #3) (str "at least three") (str fewer))

Numbers display with #, and floats retain a decimal point or exponent, including #4.0 and #-0.0. str, external command arguments, and environment values use numeric text without #. Capturing a command or exporting a number produces text again. Numbers are not implicitly booleans; use a numeric comparison in a condition. Invalid operands, division by zero, integer overflow, and non-finite floats report a language error at the failing operand with exit status 2.

(; This is a whole comment expression.
(; Comments can nest. )
(no-such-command $undefined)
)
(echo (; This comment contributes no argument. ) hello)
(echo ; this;is;text)

A list whose first word is ; is discarded by the reader, wherever it occurs. Comment contents still follow the string and balanced-parenthesis rules, but are never evaluated or checked for defined names. Elsewhere, ; is ordinary text; there are no line comments. A quoted ";" is not a comment marker.

See the standard library for list, text, type, and numeric helpers.