Pima is dual-licensed under the MIT License or Apache License 2.0, at your option.
Pima is an experimental, expression-oriented language implemented with a register-based virtual machine in Rust. Programs are organized around commands, values, code blocks, and objects rather than punctuation-heavy call syntax.
The language is under active design. The normative description lives in docs/language-reference.md; implementation boundaries and invariants are described in docs/architecture.md.
Each physical line begins with a command. If its first value is callable, Pima invokes it with the remaining values packed into one argument list. Otherwise, the line returns that value.
import "/pima/library/standard"
Console.println "Hello from Pima"
+ (20 22)
42
Square brackets invoke an expression immediately, which is useful when a call must be nested inside another expression:
Console.println [+ 20 22]
Parentheses create list values; they do not call functions:
val numbers (1 2 3)
A bare word resolves its bound value. A leading colon requests the word itself as a symbol, generally when the surrounding expression would otherwise resolve it as a name:
val status :ready
Symbols are ordinary values and are commonly used for tags and type names.
val creates an immutable binding, var creates a mutable binding, and let
updates an existing mutable location:
val name "Pima"
var count 0
let count [+ count 1]
Lists and scalar data have value semantics. Objects, functions, blocks, futures, and native handles have reference identity when assigned to another binding.
Function arguments preserve an existing storage reference. The parameter points to the resolved location and inherits its mutability; literals and computed results instead produce immutable parameter values.
Use Value.copy when an explicit detached data snapshot is needed:
val saved_count [Value.copy counter.count]
It recursively copies unit, scalar, string, symbol, and list data. Errors and
identity-bearing values such as objects, functions, blocks, futures, remote
objects, and native handles fail with :copy_error :uncopyable_value.
Braces create an inert code block. do executes one in the current context,
while new combines one or more blocks into a new object:
val Counter {
pub var count 0
pub function increment () {
let this.count [+ this.count 1]
this.count
}
}
val counter [new Counter]
counter.increment
let counter.count 10
With several templates, new performs ordered namespace composition. It
selects complete definitions with leftmost precedence and then executes the
survivors to create one fresh namespace. Templates are not parent objects:
there is no hidden object chain, inheritance lookup, or super, and every
surviving method shares the one completed this.
Members are private unless declared with pub. A pub var is readable and
writable from outside the object. Inside a method, the reserved
value this refers to the object that owns the bound method.
Selected and wildcard imports are live references to those public bindings.
Importing a pub var preserves its writability, importing a pub val preserves
its immutability, and qualified and imported access always observe the same
storage location.
An extracted member reference keeps its complete owning object alive. Rebinding the original object name does not retarget the reference or discard that object; the object becomes collectible only after its final external reference is gone.
If construction fails, functions and blocks created inside it are invalidated,
including references published through earlier side effects. Using one later
produces :invalid_object and retains the original construction error for
diagnostics.
Accessing a function member produces a bound function reference:
val increment counter.increment
increment
Calling increment is equivalent to calling counter.increment; its this
value remains counter.
Errors are typed object values. throw raises one and attempt returns a
raised error as a value so it can be inspected. Like NaN, an error is never
equal to anything, including itself; use Types.is? to identify its type.
Reference.same? instead tests whether two references identify the same
storage location. It can recognize aliases after that location is moved or
invalidated without making their error values equal under =.
Remote objects execute in isolated workers. An annotated block declares the context it needs and how each value crosses the worker boundary:
val Worker @(
configuration
*workload
&service
) {
pub function run () {
workload
}
}
configurationcopies data accepted byValue.copy.*workloadmoves it and invalidates all caller-side references to its shared source location.&serviceshares a synchronized remote, future, or TCP-listener handle.
Bare requirements cannot implicitly share identity-bearing values. They fail
with :copy_error :uncopyable_value; use & to make sharing explicit.
A moved location becomes a typed :moved_value error that records the source
span and operation responsible for the move.
Local objects, functions, and blocks remain VM-bound; * does not implicitly
serialize their reachable graph. Construct worker-local objects inside the
worker from transported data. If an unsendable value is encountered, worker
creation fails transactionally and every caller-side alias remains usable.
The unified CLI runs programs and provides development tools:
cargo run -- run examples/fibonacci.pima
cargo run -- check examples
cargo run -- fmt --check examples
cargo run -- doc examples -o target/pima-doc
cargo run -- doc examples/showcase.pima --format markdown
cargo run -- doc examples/showcase.pima --format json
cargo run -- lspHTML is the default documentation format and produces an indexed static site.
Markdown and JSON are available with --format. For compatibility,
pima file.pima remains an alias for
pima run file.pima. check, fmt, and doc accept files or directories and
discover .pima files recursively. The formatter preserves
physical line boundaries because they are significant Pima syntax.
The interpreter is also available as a Rust library:
use pima::{Config, Interpreter};
let mut pima = Interpreter::new(Config::default());
let outcome = pima.run_source("<embedded>", "[+ 20 22]\n");
assert!(outcome.is_success());
assert_eq!(outcome.value, Some(pima::Value::Integer(42)));RunOutcome::diagnostics contains lexer, parser, and uncaught runtime errors.
An unsuccessful run has no value.
import "/pima/library/standard"
function factorial (number) {
if [<= number 1] {
1
} {
* number [factorial [- number 1]]
}
}
[factorial 6]
The ownership-aware context contract for remote objects keeps context requirements on the object template:
val Worker @(
configuration
*input
&database
) {
pub function run () {
// configuration is an immutable snapshot
// input is owned exclusively by this worker
// database is a shared remote handle
}
}
val configuration (:host "127.0.0.1" :port 8080)
val input [tcp.accept listener]
val database [remote Database]
val worker [remote Worker]
[worker.run]
configuration, input, and database must already be visible where
remote Worker is evaluated. A bare requirement copies a snapshot, *
transfers ownership and replaces the shared source location (including its
reference aliases) with a provenance-carrying moved-value error, and &
passes a synchronized remote, future, or TCP-listener handle.
More complete programs live in examples/, including a JSON parser and a
directory-backed static file server core.
Run the Criterion benchmark suite with:
cargo benchThe suite measures syntax processing, interpreter startup, repeated evaluation, recursion, closures, and larger list workloads. See docs/benchmarking.md for individual commands and comparison guidance.
The TCP module can run the Pima HTTP implementation as a real local server:
cargo run -- run demos/http_file_server.pimaThen open http://127.0.0.1:8080 or request it with curl.
The demo starts four isolated accept workers sharing one listener and four
remote file handlers, allowing independent requests to make progress in
parallel.
Use the same checks required by the repository:
cargo test --workspace
cargo clippy --workspace --all-targets --all-features -- -D warningsThe current implementation includes lexing, parsing, evaluation, closures,
immutable lists, objects, typed errors, source-aware stack diagnostics, the
embedded standard library, /pima/io, low-level /pima/tcp sockets, and
cached file imports with complete cycle reporting. The remaining
standard-library and host-embedding surfaces are still evolving.