Skip to content

Design principles

parsh does not have a formal manifesto. These are the principles the implementation keeps returning to, stated as plainly as possible:

Replace the string conventions of a shell with expressions, values, and explicit boundaries, and accept a little verbosity in exchange for predictability.

The prompt is an expression with the opening parenthesis implied. $ () waits for echo hi, which evaluates (echo hi). Interactive input and scripts share the same reader, evaluator, and diagnostics, so there is no separate command-line grammar to learn or to keep in sync.

Comments follow the same rule: (; ...) is a form that nests and obeys bracket balancing, instead of a line comment that ends at a newline. See syntax and values for the details.

Bare words are text. 123 is the text 123, and true, false, and nil have no literal meaning in value positions. Explicit literals such as #123, #false, and #nil create typed values. Arithmetic and numeric comparisons accept only numbers; int, float, and bool convert runtime values explicitly:

(eq 001 1)
(= #001 #1)
(let mode fast)
(if (eq $mode fast) (echo fast))

A condition must produce a boolean or a command status. Text is never implicitly truthy.

Text stays one argument. Lists expand one level into external command arguments. Globbing is a call, not a syntax rule:

(let files (list "a file.rs" "b.rs"))
(printf "<%s>\n" $files)
(for file (glob "*.rs") (echo $file))

The point is that what a script says is what it runs: no word splitting, no hidden pathname expansion, and no guessing what spaces mean at the call site.

Commands are expressions, programs are opaque

Section titled “Commands are expressions, programs are opaque”

An expression can call a built-in, a macro, a function, a struct constructor, or a program. External programs start directly: the expression is not handed to another shell to parse again, so there is no second round of quoting and no injection layer.

Command results are values. A command used as a value captures stdout as UTF-8 text with trailing newlines removed, binary output goes through redirection or pipelines, and the exit status is a status value. A nonzero status stops the current batch, but conditions and explicit capture may inspect it. Complete results retain exit and signal information, and exceptions provide recovery. See commands and composition.

Values copy when read, assigned, or passed, including nested lists and maps. Collection updates return new values instead of mutating old ones:

(let config {mode fast})
(set config (map/set $config mode slow))

Copies of a function retain its identity and captured environment; ordinary data updates still return new values.

Every parenthesized expression creates a fresh lexical scope. let binds in that expression’s enclosing scope, rejects duplicate names there, and set updates the nearest existing binding while returning nil. Functions capture their defining scope, and loops create a fresh scope per iteration. This removes a class of questions about where a value was changed, and it lets structs and macros compose without aliasing surprises.

Lists, maps, structs, pipelines, and redirections are all expressions. type reports how a name resolves without running it, and completion covers commands, paths, variables, struct members, and macros:

(defstruct Point x y)
(type Point.x)
(| (find . -name "*.rs") (sort))

The goal is for people and tools to read structure rather than guess at strings. The same property makes generated scripts easier to review: one grammar and no quoting layers, so the script reads the way it runs. See structs and macros.

The language says what it does not do yet. Pipeline stages accept external commands only; macros have no hygiene; nested quasiquotes are copied literally; library imports remain future work. Native background tasks use isolated interpreter threads. Exit codes and diagnostic positions are stable enough to depend on: language errors use 2, a missing program 127, permission denied 126. See errors and exit codes.

Limits are documented rather than discovered.

  • Structure carried by spaces and quotes.
  • Text treated as truth.
  • Implicit word splitting and pathname expansion.
  • Re-parsing expressions inside another shell.
  • Undocumented behavior presented as compatibility.

These choices are not free:

  • One-off commands are wordier: (if (eq $x y) ...) instead of [ "$x" = y ].
  • Typed scalar literals require a # prefix; plain (echo 1) still passes text.
  • Words starting with # need quotes when they are intended as text.
  • Copy semantics make large collections expensive to pass around.
  • POSIX habits do not transfer: for f in *.rs becomes (for f (glob "*.rs")).

The trade is deliberate: a little more typing for a script whose behavior can be read off the page.