← All documentation

Read and write PTC-Lisp

Read what a model wrote in a trace, or try the language by hand before building anything.

PTC-Lisp is the small, bounded, Clojure-like language the model writes its mission programs in. You can learn to read it in ten minutes, and the fastest way is to type it. This page is a tour; the language specification is the contract, and the function reference lists everything callable.

Every example below is validated against the interpreter, so what you see is what the runtime returns.

Open a REPL

ptc repl

Type an expression, get a value back:

PTC-Lisp REPL (:quit to exit; :help for commands)
ptc> (+ 1 2)
3
ptc> (str "tick" "-" "tock")
"tick-tock"

A REPL session keeps state: what one input defines, the next input can use. The model works the same way across turns, narrowing a problem step by step.

Values are JSON values

PTC-Lisp data is JSON-shaped: numbers, strings, booleans, nil, vectors, and maps. Literals evaluate to themselves:

42            ; => 42
"hello"       ; => "hello"
true          ; => true
[1 2 3]       ; => [1 2 3]
{"name" "Ada"} ; => {"name" "Ada"}

Map keys are strings. Keywords like :name are a shorthand you will see everywhere in generated code. Writing a keyword key stores the string key:

(assoc {"a" 1} :b 2) ; => {"a" 1 "b" 2}

Call functions

A call is a list: the function first, then its arguments. There is no other syntax to learn.

(+ 1 2)          ; => 3
(count [1 2 3])  ; => 3
(str "a" "b")    ; => "ab"

The REPL can describe any function for you:

ptc> (doc "str")
(str ...)
  Convert and concatenate to string

Name things

def names a value for the whole session; let names values for one expression:

ptc> (def answer 42)
#'answer
ptc> answer
42
(let [a 2
      b 3]
  (* a b)) ; => 6

Nothing is ever reassigned. Functions return new values and leave their arguments untouched. The assoc above returned a new map.

Reach into maps

(get {"name" "Ada"} "name")                        ; => "Ada"
(:name {"name" "Ada"})                             ; => "Ada"
(get-in {"user" {"name" "Ada"}} ["user" "name"])   ; => "Ada"

A keyword in the function position looks itself up, and string and keyword access are interchangeable. Generated code uses both.

Transform collections

map, filter, and reduce do most of the work in mission programs:

(map inc [1 2 3])           ; => [2 3 4]
(filter odd? [1 2 3 4 5])   ; => [1 3 5]
(reduce + [1 2 3 4])        ; => 10

The threading macro ->> chains steps in reading order. This is the shape most generated programs take:

(->> [1 2 3 4 5]
     (filter odd?)
     (map (fn [n] (* n n)))
     (reduce +)) ; => 35

Go parallel

map has a parallel twin. pmap runs the function on each element in its own process and returns the results in order:

(pmap (fn [n] (* n n)) [1 2 3 4]) ; => [1 4 9 16]

Processes are cheap here, so fanning a slow step out over a whole collection is one word, not an architecture. It stays bounded like everything else: a worker budget, a per-worker heap limit, and the run's shared deadline.

Decide

nil and false are falsy; everything else, including 0 and "", is truthy:

(if (> 3 2) "yes" "no") ; => "yes"
(if 0 "truthy" "falsy") ; => "truthy"
(nil? nil)              ; => true

Mistakes are values

Where many languages raise, the sandbox returns a recoverable error value and the session continues:

ptc> (/ 1 0)
Error (arithmetic_error): arithmetic_error: division by zero
ptc> (+ 1 2)
3

This is deliberate: a mistake costs the model one expression, not the run. The Clojure conformance gaps reference records every place this value model diverges from Clojure, with rationale.

Define functions

defn defines a named function. In a REPL session it persists across inputs; in one expression, group the definition and the call with do:

(do
  (defn double [n] (* 2 n))
  (double 21)) ; => 42

Anonymous functions are (fn [n] ...), as in the threading example above.

Declare types

A function can declare a signature: what it takes and what it returns.

(ns invoice "Invoice helpers" {:visibility :prompt})

(defn total
  "Sum the amounts on an invoice."
  {:signature "(lines [{amount :float}]) -> :float"}
  [lines]
  (reduce + (map :amount lines)))

The runtime checks the arguments before the body runs and the result before it leaves. A wrong shape names the field that broke, so the model can fix it on the next turn:

prelude_contract_error: invoice/total input lines.0.amount: expected float, got string

Signatures are PtcRunner's, not Clojure's, and they belong to components and preludes, not to a defn typed into the REPL. The signature reference has the full type vocabulary.

Where to go next