Pitruck 1.6 Technical Reference

Pitruck is a dynamically-typed, interpreted scripting language implemented in Rust. It features a lightweight, C-style syntax with modern conveniences like optional chaining, null-coalescing, pattern matching, template strings, and first-class functions.

A major feature of Pitruck is its built-in HTTP server capabilities, allowing scripts to be instantly served as web endpoints with zero external dependencies. Version 1.6 introduces native HTTPS support for both outbound requests and inbound server mode, a persistent server store for cross-request state, permission controls for serve mode, and a program cache for faster repeated request handling.

Getting Started

Pitruck source files use the .pr extension.

Hello World

print "Hello, World!"

CLI Usage

The Pitruck executable provides a REPL, script runner, package manager, and a web server:

pitruck                             # Start the interactive REPL
pitruck script.pr                   # Run a source file
pitruck script.pr --speed           # Run and show execution timing (lex, parse, run)
pitruck --serve script.pr           # Serve script.pr as an HTTP handler (port 8000)
pitruck --serve public/ --port 8080 # Serve a directory using file-based routing
pitruck --serve script.pr --https   # Serve with auto-generated self-signed HTTPS
pitruck --serve script.pr --https --tls-cert cert.pem --tls-key key.pem  # Serve with custom HTTPS cert
pitruck lib install <url>           # Download and install a library
pitruck lib list                    # List installed libraries
pitruck lib delete <name>           # Remove an installed library

Serve-Mode Flags

When running in --serve mode, the following flags are available:

FlagDescription
--port NHTTP/HTTPS port (default 8000).
--httpsEnable HTTPS with auto-generated dev cert (cached in .pitruck-tls/).
--https --tls-cert FILE --tls-key FILEHTTPS with your own PEM cert/key files.
--debugVerbose server error output (prints runtime errors to stderr).
--allow-readAllow file read access (sys_readfile) in serve mode.
--allow-writeAllow file write access (sys_writefile) in serve mode.
--allow-netAllow outbound HTTP/HTTPS (http_request) in serve mode.
--allow-allAllow read, write, and net (equivalent to all three above).
Default Permissions: In serve mode, all potentially dangerous operations (file read, file write, outbound HTTP) are disabled by default for security. You must explicitly opt in with --allow-read, --allow-write, --allow-net, or --allow-all.

Lexical Structure

Comments

Pitruck supports three comment styles:

# Python-style single-line comment
// C-style single-line comment
/* Multi-line 
   block comment 
*/

Identifiers

Identifiers must begin with a letter or underscore _ and can contain letters, numbers, and underscores. The standalone underscore _ is a reserved keyword used as the default wildcard in match statements.

Keywords

The following words are reserved and cannot be used as identifiers:

var, bring, func, return, if, elif, else, while, for, in, print, class, self, match, break, continue, try, catch, and, or, not, true, false, null, _ (underscore wildcard).

Strings

Strings can be defined using double quotes, triple quotes (for multi-line strings), or backticks (for template strings).

var normal = "Hello\nWorld"
var raw_multiline = """Line 1
Line 2"""
var name = "Alice"
var template = `Hello ${name}!`

Supported string escapes: \n, \t, \r, \e (ESC), \", \`, \\, \$, \xHH (Hex byte), \u{HHHH} (Unicode codepoint).

Numbers

All numbers are 64-bit floating-point (IEEE 754 f64). Underscores can be used for readability.

var num = 1_000_000.5

Data Types

Pitruck is dynamically typed. The primitive types are:

Complex types include:

Truthiness

In conditionals, only false and null are considered falsey. Everything else (including 0, 0.0, and "") evaluates to true.

String Indexing

Strings support numeric indexing to retrieve individual characters, similar to list indexing. Indexing out of bounds throws a runtime error.

var word = "hello"
print word[0]   # "h"
print word[4]   # "o"

Variables

Variables are declared using the var keyword. They are block-scoped.

var a = 10
{
    var a = 20 # Shadows outer 'a'
    print a    # 20
}
print a        # 10

Attempting to access or assign to an undeclared variable throws a RuntimeError. Redeclaring a variable with the same name in the same scope also throws a RuntimeError:

var x = 5
var x = 10 # RuntimeError: 'x' is already declared in this scope

Operators

Arithmetic

+, -, *, /, % (Modulo)

Note: The + operator also acts as a string concatenator. It coerces numbers into strings when one operand is a string. It also concatenates Lists.

Division by zero: Since version 1.6, dividing by zero throws a RuntimeError ("division by zero") instead of returning positive or negative infinity.

Comparison

==, !=, <, >, <=, >=

Values of different types are generally not equal, except via formatting comparison fallback in lists.

Logical

and, or, not

and and or are short-circuiting.

Null-Coalescing & Optional Chaining

OperatorExampleDescription
??a ?? bReturns a if it is not null, otherwise returns b.
?.obj?.propReturns obj.prop if obj is not null, else null.
?[]arr?[idx]Returns arr[idx] if arr is not null, else null.

Assignment

=, +=, -=, *=, /=

Ternary

condition ? true_expr : false_expr

Control Flow

If / Elif / Else

if x < 10 {
    print "Small"
} elif x < 20 {
    print "Medium"
} else {
    print "Large"
}

Loops

While loop:

var i = 0
while i < 10 {
    i += 1
    if i == 5 { continue }
    if i == 8 { break }
}

For-In loop: Iterates over a List or a String.

for item in [1, 2, 3] {
    print item
}

for char in "abc" {
    print char
}

Match Statement

Pattern matching on values. The underscore _ serves as the default (wildcard) case.

match status {
    200 => { print "OK" }
    404 => { print "Not Found" }
    _   => { print "Unknown" }
}

Functions

Declaration

func add(a, b) {
    return a + b
}

If no return is reached, the function implicitly returns null.

Lambdas (Closures)

Anonymous functions use the fat arrow => syntax. They capture their surrounding lexical environment.

var multiply = (a, b) => a * b
var block_lambda = (x) => {
    return x * 2
}

Classes and Objects

Classes encapsulate methods and fields. Instantiation does not use a new keyword; you simply call the class as a function. The init method acts as the constructor.

class Person {
    func init(name, age) {
        self.name = name
        self.age = age
    }

    func greet() {
        print "Hi, I am " + self.name
    }
}

var p = Person("Alice", 30)
p.greet()

Modules & Imports

Use the bring keyword to load external .pr files. The interpreter searches the current directory, the lib/ subdirectory, the executable's path, and its parent directories.

bring math_utils  # Loads math_utils.pr or lib/math_utils.pr
# Functions/Classes defined in the module become available in the global scope.

Note: Modules are executed in the global context and modify the global scope. Module inclusion is cached (a module is only loaded once per execution).

Error Handling

Runtime errors can be caught using try/catch. The caught exception is represented as a dictionary containing type, message, and line.

try {
    var x = 10 / 0
} catch (err) {
    print "Error occurred on line " + to_string(err.line)
    print err.message
}

The catch variable is scoped to the catch block only; it does not leak into the surrounding scope.

HTTP Web Server

A flagship feature of Pitruck is its ability to natively serve HTTP (and now HTTPS) requests via the --serve CLI flag.

Modes of Operation

  1. Single Handler: pitruck --serve index.pr funnels all requests through index.pr.
  2. File-Based Routing: pitruck --serve public/ routes GET /about to public/about.pr, falling back to public/index.pr if the specific file does not exist.

HTTPS Support

Version 1.6 adds native TLS support for the inbound server, powered by rustls and the ring crypto backend:

Browser trust: Self-signed dev certificates are not trusted by browsers by default. Open the HTTPS URL in a browser, accept the security warning, then reload. Alternatively, use curl -k to skip certificate verification.

The Request / Response Contract

When running in --serve mode, the interpreter automatically injects two global instances into your script: request and response.

request object

response object

Modify this object to shape the HTTP response.

Content-Type Inference: If you don't explicitly set a Content-Type, Pitruck checks if your response.body starts with <!DOCTYPE or <. If so, it sets it to text/html; charset=utf-8. Otherwise, it defaults to text/plain; charset=utf-8.

Server Store (Persistent Key-Value Storage)

A thread-safe, in-memory key-value store is available in serve mode for sharing state across requests. All store functions operate on string keys and string values.

FunctionDescription
server_set(key, value)Set a string value for a string key. Only available in serve mode.
server_get(key)Get the value for a key, or null if not found. Only available in serve mode.
server_has(key)Returns true if the key exists. Only available in serve mode.
server_delete(key)Delete a key; returns true if the key existed. Only available in serve mode.
server_keys()Returns a list of all store keys. Only available in serve mode.
# Simple visit counter
var count = server_get("visits")
if count == null { count = "0" }
var n = to_number(count) + 1
server_set("visits", to_string(n))
response.body = "Visits: " + to_string(n)

Program Cache

In serve mode, Pitruck caches the compiled AST of each script by its file modification time. If the file hasn't changed since the last request, the cached program is reused, skipping the lex and parse phases entirely. This significantly improves response latency for repeated requests.

Server Example

# file: server.pr
if request.path == "/api" {
    response.headers["Content-Type"] = "application/json"
    var data = { "status": "success", "method": request.method }
    response.body = json_encode(data)
} else {
    response.body = "<h1>Hello from Pitruck!</h1>"
}

Standard Library

Pitruck features a rich set of built-in functions.

Math & Utilities

String Manipulation

Lists & Dictionaries

JSON & Encoding

System & Network

Bundled Libraries

Pitruck ships with several standard library modules in the lib/ directory. Use bring to load them.

bring color — Terminal Colors

ANSI escape-code wrappers for coloring and styling terminal output. Each function takes a string and returns it wrapped in the appropriate escape sequence.

bring color
print color.red("Error!")
print color.green("Success!")
print color.bold("Important")
print color.bg_blue("Highlighted")

Available functions: red, green, blue, yellow, cyan, magenta, white, black, bg_red, bg_green, bg_blue, bold, underline.

bring math — Math Utilities

Convenience wrappers around built-in math functions, plus max/min and inc.

bring math
print math.abs(-5)       # 5
print math.sqrt(16)      # 4
print math.pow(2, 3)     # 8
print math.max(10, 20)   # 20
print math.min(10, 20)   # 10
print math.inc(7)        # 8

Available functions: inc, abs, sqrt, pow, max, min.

bring struct — Lightweight Structs

Simple dict-based struct utilities. struct_new creates a deep-cloned dict from a template, and struct_copy produces a deep clone of an existing struct.

bring struct
var person = struct_new({ "name": "", "age": 0 })
person.name = "Alice"
person.age = 30
var copy = struct_copy(person)

bring system — System Helpers

Wrapper functions that call the sys_* built-ins with shorter, more readable names.

bring system
print system.os()
system.clear_screen()
system.exit(0)
var home = system.get_env("HOME")
system.write_file("out.txt", "Hello")
var contents = system.read_file("out.txt")

Available functions: os, clear_screen, exit, get_env, write_file, read_file.

bring time — Time Utilities

Simple wrappers for the time() and sys_sleep() built-ins.

bring time
var now = time.now()
time.sleep(1000)  # Sleep for 1 second

bring trucky — Web Framework

Trucky is a bundled web framework library for building HTML UIs and handling form submissions in Pitruck serve mode. It provides reactive state management, declarative HTML element construction, form handling with validation, and alert/notice rendering.

State Management (Signal Pattern)

Trucky uses a Signal pattern for reactive state. A signal wraps a mutable value and notifies dependent UI elements when it changes.

var count = Signal(0)
count.set(count.get() + 1)

HTML Element Construction

Trucky provides helper functions for building HTML element trees declaratively. Elements are represented as dicts with kind, tag, cls, style, attrs, and children fields.

var page = el("div", "container", "", {}, [
    el("h1", "", "", {}, [text("Hello")]),
    el("p", "desc", "", {}, [state(count)])
])

Core element functions: el(tag, cls, style, attrs, children), text(value), state(signal), raw(html_string), error_slot(app, field), notice_slot(app).

Form Handling

Trucky provides an App class that manages form data, validation errors, and alert notices. Form fields are rendered with error message slots.

bring trucky

class MyApp {
    func init() {
        self._fields = { "name": "", "email": "" }
        self._errors = {}
        self._notice = null
    }

    func handle_submit() {
        var name = self.field("name")
        if name == "" {
            self.error_set("name", "Name is required")
        } else {
            self._notice = { "kind": "success", "text": "Submitted!" }
        }
    }
}

var app = MyApp()
app.load_form(request.form)

if request.method == "POST" {
    app.handle_submit()
}

response.body = render(app.build_ui())

App Methods

MethodDescription
app.field(name)Get the current value of a form field.
app.error_set(name, msg)Set an error message for a field.
app.error_for(name)Get the error message for a field (empty string if none).
app.has_errors()Returns true if any validation errors are set.
app.load_form(form_dict)Populate fields from a form dict (e.g., request.form).
app.notice_set(kind, text)Set a flash notice (kind: "success", "error", "info", "warning").
app.build_ui()Return the root element tree for rendering.

Rendering

The render(node) function walks an element tree and produces an HTML string. It handles text escaping, state dereferencing, optional attribute rendering, and self-closing (void) tags like <input>, <br>, <img>.

Runtime Internals

The Pitruck interpreter operates in three main phases:

  1. Lexical Analysis: Source code is converted into a token stream.
  2. Parsing: A recursive descent parser builds an Abstract Syntax Tree (AST).
  3. Execution: A tree-walk interpreter evaluates the AST nodes.

Variable Resolution & Speed

To optimize lookups during the tree-walk phase, Pitruck hashes variable names (via a custom FNV-1a style hashing function in symbol.rs). A lightweight compiler pass (compiler::resolve_program) attaches these pre-computed 64-bit integer hashes to AST nodes. The interpreter scope is a flat vector of (hash, Value) tuples, allowing faster resolution than deep string map lookups.

Memory Model

Reference-counted, garbage-collected types (Lists, Dicts, Functions, Instances) are backed by Rust's Rc<RefCell<T>>. This permits mutable sharing across scopes and instances without exposing manual memory management to the user. (Note: Circular references will leak memory as there is no mark-and-sweep GC).

Server Architecture

When running in serve mode, the HTTP server spawns a new OS thread per incoming connection. Each thread creates a fresh Interpreter instance but shares the ProgramCache (for compiled AST reuse) and the Store (for persistent key-value storage) via Arc. The Store uses a Mutex<HashMap> internally for thread-safe access. TLS connections are handled via rustls with either auto-generated self-signed certificates (cached in .pitruck-tls/) or user-supplied PEM files.

Outbound HTTP Client

The http_request built-in now supports both HTTP and HTTPS. HTTPS connections use rustls with webpki-roots for certificate verification. The client sends a User-Agent: pitruck/1.6 header and supports chunked transfer-encoding decoding for responses.

Limitations & Known Quirks

Changelog: 1.5 -> 1.6

New Native HTTPS support (outbound & inbound) - http_request now supports HTTPS URLs via rustls + webpki-roots with automatic certificate verification. The inbound server supports --https with auto-generated self-signed dev certs (cached in .pitruck-tls/) or user-supplied PEM files via --tls-cert/--tls-key. Chunked transfer-encoding responses are now decoded correctly.
New Server Store (persistent key-value storage) - Five new built-in functions: server_set, server_get, server_has, server_delete, server_keys. Thread-safe, in-memory store shared across all request handlers in serve mode.
New Serve-mode permission controls - --allow-read, --allow-write, --allow-net, --allow-all flags. By default, all potentially dangerous operations are disabled in serve mode for security.
New Program cache for serve mode - Compiled ASTs are cached by file modification time. Repeated requests skip lex and parse phases entirely, significantly improving response latency.
New --debug server flag - Verbose runtime error output to stderr for easier debugging during development.
New Bundled library: trucky - A full web framework module with reactive state management (Signal pattern), declarative HTML element construction, form handling with validation, and alert/notice rendering.
Enhanced String indexing - str[idx] now returns the character at the given index, matching list-like access semantics. Out-of-bounds access throws a RuntimeError.
Enhanced JSON encoding of Instances - json_encode now serializes Instance values as their field dictionary, making it easy to return object data as JSON from API endpoints.
Fixed Division by zero - Previously returned Infinity/-Infinity/NaN. Now throws a RuntimeError with message "division by zero" for safer, predictable scripting.
Fixed Variable redeclaration guard - Redeclaring a variable with var in the same scope now throws a RuntimeError instead of silently shadowing or overwriting.
Fixed Catch-block scoping - The catch (err) variable is now properly scoped to the catch block and does not leak into the surrounding scope.
Removed Outbound HTTPS limitation - The old note "HTTPS is not natively supported by the outbound client" is no longer accurate; full HTTPS/TLS support is now built in.
Security Default-deny permissions in serve mode - File I/O and network access are now disabled by default when running --serve, preventing accidental exposure. Users must explicitly opt in with --allow-read, --allow-write, or --allow-net.
Security Auto-generated TLS certs use rcgen - Dev self-signed certificates are generated with proper SANs (localhost + 127.0.0.1), CA constraints, and key usage flags for safer local development HTTPS.