Pitruck

Pitruck 1.6.1 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.0 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 (read/write/net allowed by default)
pitruck script.pr --speed           // Run and show execution timing (lex, parse, run)
pitruck script.pr --deny-write      // Run with file writes disabled
pitruck script.pr --deny-all        // Run with read/write/net all disabled
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|path>      // Download or copy a library into lib/
pitruck lib list                    // List installed libraries
pitruck lib delete <name>           // Remove an installed library

Script-Mode Flags (running pitruck file.pr)

When running a script directly (not in --serve mode), file I/O and outbound networking are allowed by default. The user is explicitly running a local script, so the serve-mode default-deny posture does not apply. Opt out with the --deny-* flags:

FlagDescription
--speedShow lex/parse/run timings.
--debugNo effect in script mode (verbose serve-mode errors).
--allow-read / --allow-write / --allow-net / --allow-allNo-op in script mode. Accepted for muscle-memory compatibility with serve mode.
--deny-readForbid sys_readfile in script mode.
--deny-writeForbid sys_writefile in script mode.
--deny-netForbid http_request in script mode.
--deny-allForbid read, write, and net in script mode (equivalent to all three above).
Why this matters Before Pitruck 1.6.1, calling write_file(...) / read_file(...) from a script run via pitruck main.pr would fail with "file write access is not allowed in this context", even when passing --allow-write. The flags were only parsed in --serve mode. Scripts now run with full permissions by default; pass --deny-all if you want to lock down an untrusted script.

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. Opt in explicitly with --allow-read, --allow-write, --allow-net, or --allow-all.

Quickstart: How To Use Libraries

Pitruck ships with seven bundled libraries in the lib/ directory: color, math, struct, system, time, std (a meta-module that brings all of the previous five plus uuid_v4), and trucky (a web framework). Load any of them with the bring keyword:

bring std              // brings color + math + struct + system + time + uuid_v4
bring system           // brings just the system helpers
bring trucky           // brings the web framework classes (Trucky, UI, StateRef)

print os()             // "linux" / "windows" / "macos"
var p = struct({ "name": "", "age": 0 })
p.name = "Alice"       // works! dicts support dot-assignment since 1.6.1
print p.name           // Alice

After bring <module>, every top-level func and class defined in that module becomes a global name in your script. There is no module.function() namespace — call the function directly. The full reference for each library is in Bundled Libraries below.

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:

  • Number: 64-bit float.
  • String: UTF-8 string. Supports indexing via str[idx] to retrieve individual characters.
  • Bool: true or false.
  • Null: represents the absence of a value (null).

Complex types include:

  • List: ordered, mutable array of values. [1, 2, 3]
  • Dict: key-value map. Keys must be strings, numbers, or booleans. {"key": "value"}
  • Function: first-class callable object (closures and lambdas).
  • Class / Instance: object-oriented classes and their instantiations.
  • BoundMethod: a method bound to its instance context.

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)

The + operator also acts as a string concatenator. It coerces numbers into strings when one operand is a string, and it 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, notand 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 — 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 following locations, in order, and loads the first match:

  1. The directory of the current script (or the resolved directory of the parent module).
  2. <script_dir>/lib/<name>.pr
  3. The current working directory.
  4. ./lib/<name>.pr
  5. The directory of the Pitruck executable, then <exe_dir>/lib/<name>.pr
  6. Up to three parent directories of the executable, looking for lib/<name>.pr
bring math
bring color
bring trucky

bring color
print red("error text")
print bold(underline("hi"))

Modules execute in the global context and modify the global scope. Module inclusion is cached: a module is only loaded once per execution, so duplicate bring statements — including from inside other modules — are no-ops.

Common pitfall A bare bring foo does not create a variable called foo. It just runs foo.pr in the global scope. If foo.pr defines func bar(), call it as bar() — never as foo.bar(). (The only exception is when a library defines a class with the same name as the file, e.g. trucky.pr defines class Trucky, in which case you instantiate it with var app = Trucky().)

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.0 adds native TLS support for the inbound server, powered by rustls and the ring crypto backend:

  • Auto dev cert: pitruck --serve script.pr --https generates a self-signed certificate cached in .pitruck-tls/. Zero config for local development.
  • Custom cert: pitruck --serve script.pr --https --tls-cert cert.pem --tls-key key.pem uses your own PEM files for production.
Browser trust Self-signed dev certificates are not trusted by browsers by default. Open the HTTPS URL, 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

  • request.method (String): "GET", "POST", etc.
  • request.path (String): the URL path (e.g. "/about").
  • request.query_str (String): the raw query string.
  • request.query (Dict): parsed URL query parameters.
  • request.form (Dict): parsed form data (if Content-Type is application/x-www-form-urlencoded).
  • request.body (String): raw request body.
  • request.headers (Dict): HTTP headers (keys are lowercase).

response object

Modify this object to shape the HTTP response.

  • response.status (Number): defaults to 200.
  • response.body (String): the content to send back.
  • response.headers (Dict): custom headers to send back, e.g. response.headers["Content-Type"] = "application/json".
Content-type inference If you don't explicitly set a Content-Type, Pitruck checks whether response.body starts with <!DOCTYPE or <. If so, it sets 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. Serve mode only.
server_get(key)Get the value for a key, or null if not found. Serve mode only.
server_has(key)Returns true if the key exists. Serve mode only.
server_delete(key)Delete a key; returns true if the key existed. Serve mode only.
server_keys()Returns a list of all store keys. Serve mode only.
// 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)
Note The script runs on every request, so without verification the counter above increments twice per visit — the browser also requests GET /favicon.ico.

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

  • rand(min, max): returns a random integer between min and max (inclusive).
  • range(stop) / range(start, stop) / range(start, stop, step): returns a list of numbers.
  • to_number(val): converts string or bool to a float.
  • to_string(val): converts a value to its string representation.
  • is_number(val): returns true if the value can be parsed as a number.
  • typeof(val): returns "number", "string", "bool", "null", "list", "dict", "function", "class", or "instance". Bound methods return "function".
  • clone(val): performs a deep clone of a List or Dict.
  • math_abs(n), math_sqrt(n), math_pow(a, b), floor(n), ceil(n), round(n): standard math operations.

String Manipulation

  • len(str_or_list_or_dict): returns the length.
  • split(str, sep): splits a string into a list.
  • trim(str): removes leading/trailing whitespace.
  • upper(str), lower(str): changes casing.
  • replace(str, from, to): replaces occurrences of a substring.
  • starts_with(str, prefix), ends_with(str, suffix): boolean checks.
  • substr(str, start, length?): extracts a substring.
  • char_at(str, index): gets the character at an index.
  • pad_left(str, width, fill), pad_right(...): pads a string.
  • repeat_str(str, count): repeats a string count times.
  • index_of(str_or_list, needle): returns the index of the first occurrence, or -1.
  • html_escape(str): escapes &, <, >, ", ' for HTML safety.

Lists & Dictionaries

  • push(list, val): appends a value to a list.
  • pop(list): removes and returns the last element of a list.
  • contains(str|list|dict, needle): checks for existence — substring, list element, or dict key.
  • keys(dict): returns a list of dict keys.
  • values(dict): returns a list of dict values.
  • remove(dict|list, key_or_index): removes an item by key or index.
  • join(list, sep): joins list elements into a string.
  • list_slice(list_or_str, start, end): extracts a slice.
  • list_reverse(list_or_str): reverses a list or string.
  • list_sort(list): sorts a list in place using string representation.
  • list_sort_by(list, cmp_fn): sorts using a comparator function that returns a number (>0 to swap).
  • list_map(list, fn): returns a new mapped list.
  • list_filter(list, fn): returns a new filtered list.
  • list_reduce(list, fn, initial_acc): reduces a list to a single value.

JSON & Encoding

  • json_encode(val): serializes to a JSON string. Instances are serialized as their field dictionary; functions and classes cannot be serialized.
  • json_decode(str): parses a JSON string into Pitruck values (Lists, Dicts, Strings, Numbers, Bools, Null).
  • url_encode(str), url_decode(str): application/x-www-form-urlencoded escaping.

System & Network

  • time(): returns current time formatted as "HH:MM:SS".
  • timestamp(): returns Unix timestamp in seconds.
  • sys_os(): returns the host OS name.
  • sys_exit(code): terminates the process.
  • sys_sleep(ms): blocks execution for ms milliseconds.
  • sys_env(key): reads an environment variable.
  • sys_writefile(path, contents): writes a string to a file (disabled in serve mode unless --allow-write).
  • sys_readfile(path): reads a file to a string (disabled in serve mode unless --allow-read).
  • sys_fileexists(path): returns a boolean.
  • http_request(method, url, body, headers_dict): makes an outbound HTTP or HTTPS request, returning a dict with status, ok, body, and headers. HTTPS is natively supported via rustls + webpki-roots; the client also supports chunked transfer-encoding responses (disabled in serve mode unless --allow-net).
  • input(prompt): reads a line from STDIN.
  • clear(): clears the terminal screen via ANSI escape codes.
  • server_set(key, value), server_get(key), server_has(key), server_delete(key), server_keys(): the persistent server store, serve mode only.

Bundled Libraries

Pitruck ships with seven libraries in the lib/ directory. Every library is a plain .pr file loaded with the bring keyword. After bring, the functions and classes defined in the file become global names — there is no module namespace, call the function directly.

Read this first The following call styles are wrong and will fail with "cannot find variable" or "static method not found":
bring system
print system.os()         // WRONG - `system` is not a variable
bring color
print color.red("hi")     // WRONG - `color` is not a variable
The correct style is to call the bare function name:
bring system
print os()                // OK
bring color
print red("hi")           // OK
The only exception is when a library defines a class with the same name as the file — e.g. trucky.pr defines class Trucky — in which case you instantiate it with var app = Trucky() and call methods like app.run().
ModuleFileProvides
bring colorlib/color.pr13 ANSI color/style functions
bring mathlib/math.prabs, sqrt, pow, max, min, inc
bring structlib/struct.prstruct, struct_copy
bring systemlib/system.pros, get_env, write_file, read_file, file_exists, clear_screen, exit
bring timelib/time.prnow, timestamp, sleep
bring stdlib/std.prMeta-module: brings color+math+struct+system+time, plus uuid_v4()
bring truckylib/trucky.prWeb framework: Trucky, UI, StateRef classes

bring color — Terminal Colors

ANSI escape-code wrappers for coloring and styling terminal output. Each function wraps a string in the appropriate escape sequence, with a reset (\x1b[0m) suffix so styles do not bleed into neighboring output.

bring color
print red("Error!")              // red foreground
print green("Success!")          // green foreground
print bold(underline("Important")) // bold + underlined
print bg_blue("Highlighted")     // blue background

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 the built-in math_* functions, plus max/min and inc.

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

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

bring struct — Lightweight Structs

Simple dict-based struct helpers. A "struct" in Pitruck is just a Dict with named fields. Because dict field access — both dot-notation obj.name and bracket-notation obj["name"] — is symmetric for reads and writes, these helpers spawn record-shaped values you can mutate with dot-assignment syntax.

bring struct
var person = struct({ "name": "", "age": 0 })
person.name = "Alice"        // works - dicts support dot-assignment
person.age  = 30
var copy = struct_copy(person)
copy.name = "Bob"
print person.name            // Alice  (deep clone preserves the original)
print copy.name              // Bob
FunctionDescription
struct(template)Deep-clones a template dict so the caller's literal is not aliased. Returns an empty dict if template is null.
struct_copy(s)Deep-clones an existing struct (or any value). List and Dict children are recursively cloned; scalars are returned as-is.
Under the hood Both helpers delegate to the clone() builtin. Since 1.6.1, obj.name = value works on dicts the same way obj["name"] = value does — this is what makes the struct pattern usable. Previously, dot-assignment only worked on Instance values, so struct({...}).name = "x" crashed with "cannot set property 'name' on a non-instance value".

bring system — System Helpers

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

bring system
print os()                              // "linux" / "windows" / "macos"
print get_env("HOME")                   // /home/alice
write_file("out.txt", "Hello, world")   // writes a file
var contents = read_file("out.txt")     // "Hello, world"
print file_exists("out.txt")            // true
clear_screen()                          // ANSI clear
exit(0)                                 // terminate the process
FunctionDescription
os()Returns the host OS name (linux, windows, macos).
get_env(key)Reads an environment variable. Returns "" if not set.
write_file(path, contents)Writes a string to a file (overwrites).
read_file(path)Reads a file's contents as a string.
file_exists(path)Returns true if the file exists.
clear_screen()Clears the terminal via ANSI escape codes.
exit(code)Terminates the process with the given exit code.
Permission rules Script mode (pitruck main.pr): read/write are allowed by default, opt out with --deny-read, --deny-write, --deny-net, or --deny-all. Serve mode (pitruck --serve ...): read/write are denied by default, opt in with --allow-read, --allow-write, --allow-net, or --allow-all. Before 1.6.1, script mode inherited the serve-mode default-deny posture, so write_file(...) always crashed — this is now fixed.

bring time — Time Utilities

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

bring time
print now()              // "19:17:59" (current wall-clock time, HH:MM:SS)
print timestamp()        // 1750000000 (Unix epoch seconds)
sleep(1000)              // blocks for 1 second
FunctionDescription
now()Current wall-clock time as "HH:MM:SS" (UTC).
timestamp()Unix epoch seconds (integer-valued float).
sleep(ms)Blocks execution for ms milliseconds.

bring std — Meta-Module

lib/std.pr is a convenience meta-module. bring std is equivalent to bringing color, math, struct, system, and time in a single line, plus it defines uuid_v4() for generating RFC-4122 v4 UUIDs.

bring std

// Now all of these are globally callable:
print red("error")               // from color
print sqrt(81)                   // from math
var p = struct({ "x": 0 })       // from struct
print os()                       // from system
print now()                      // from time
print uuid_v4()                  // 5f1a9c2d-3e44-4b7a-9c1d-2a5e7f3b8a04
FunctionDescription
uuid_v4()Generates a random RFC-4122 v4 UUID string of the form xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx where y is one of 8, 9, a, b.

bring trucky — Web Framework

Trucky is a bundled web framework for building HTML UIs and handling form submissions in Pitruck serve mode — it also works in script mode, where it just prints the rendered HTML to stdout. It provides:

  • Per-session reactive state management via the StateRef class.
  • Declarative HTML element construction via the UI class (h1–h4, p, card, button, form, input, table, tabs, modal, tooltip, toast, ...).
  • The Trucky orchestrator class that wires everything together, manages themes, dispatches actions, persists state in the server store, and renders full HTML documents.
  • Form validation via a schema-driven validate(schema, values) method.

Core Classes

ClassPurposeConstructor
TruckyThe orchestrator/app instance. Holds theme, state cache, action registry, errors, notices.var app = Trucky() — no args.
UIElement factory. All element constructors live here as methods.var ui = UI() — no args.
StateRefA reactive reference to a value stored in the per-session server store.Created via app.state(default) — do not instantiate directly.

Minimal Trucky Example

bring trucky

var app = Trucky()
app.set_theme({ "primary": "#7c3aed", "title": "Trucky Demo" })
var ui = UI()

// app.state(default_val) returns a StateRef bound to the per-session
// store. On first GET it initialises to 0; on subsequent POSTs the
// action fires and bumps the persisted value.
var counter = app.state(0)

func build_page() {
    return ui.container([
        ui.h1("Hello from Trucky"),
        ui.card([
            ui.card_header([ ui.h3("Counter") ]),
            ui.card_body([
                ui.p("Current value:"),
                ui.h2(counter),
                ui.button(app, "Increment", () => {
                    counter.set(counter.get() + 1)
                    app.notify("Incremented!", "success")
                }, "primary"),
                ui.notice_slot(app)
            ])
        ])
    ])
}

app.mount(build_page())
app.run()

Save this as demo.pr and run:

pitruck --serve demo.pr --port 8000 --allow-all

Open http://localhost:8000/ in a browser. Clicking the button fires an HTTP POST with the X-Trucky-Action header, the lambda runs server-side, the counter is incremented in the per-session server store, and the page is re-rendered with the new value.

StateRef — Reactive State

A StateRef is created by app.state(default_val). It is bound to a per-session key in the server store, so the value persists across HTTP requests as long as the user keeps the trucky_sid cookie.

MethodDescription
ref.get()Reads the current value (lazily loaded from the server store on first access).
ref.set(v)Writes a new value. Persists immediately.
ref.update(fn)ref.set(fn(ref.get())) — a read-modify-write shortcut.
var count = app.state(0)
count.set(10)
count.update((v) => v + 1)   // count is now 11
print count.get()            // 11

UI — Element Constructors

The UI class has around 40 methods that build element-tree dicts. Each method returns a dict with kind, tag, cls, style, attrs, and children fields. The renderer walks this tree and produces an HTML string.

Text-content elements (accept a value that may be a string, number, or StateRef):

ui.h1(value)        ui.h2(value)        ui.h3(value)        ui.h4(value)
ui.p(value)         ui.p_lead(value)    ui.p_muted(value)   ui.span(value)
ui.code(value)      ui.blockquote(value)  ui.pre_code(value)
ui.link(value, href)

Layout / container elements (accept a list of children):

ui.container(children)         ui.container_sm(children)
ui.card(children)              ui.card_header(children)     ui.card_body(children)
ui.vstack(children, gap)       ui.hstack(children, gap)
ui.grid(children, cols, gap)   ui.grid_auto(children, min_w, gap)
ui.flex_between(children)      ui.flex_center(children)
ui.divider()                   ui.spacer(height)

Form elements:

ui.label(value, for_id)
ui.input_text(name, placeholder, value)
ui.input_email(name, placeholder, value)
ui.input_password(name, placeholder, value)
ui.input_number(name, placeholder, value)
ui.textarea(name, placeholder, value)
ui.checkbox(name, value_label)
ui.select(name, options)              // options is a list of strings
ui.form(app, children, on_submit)     // wires a data-trucky-submit action
ui.submit(label)                      // <button type="submit">
ui.button(app, label, onclick, variant)  // variant: "primary"|"secondary"|"outline"|"ghost"|"danger"
ui.error_for(app, field)              // error message slot bound to a field

Display elements:

ui.badge(value, variant)           // variant: "primary"|"success"|"warning"|...
ui.alert(title, message, variant)
ui.image(src, alt)
ui.list(items)                      // bulleted list
ui.table(headers, rows)             // headers: list of str; rows: list of list of str
ui.notice_slot(app)                 // renders the current flash notice, if any

Interactive widgets (rendered via inline JS in the runtime script):

ui.tabs(headers, contents, id_prefix)   // headers: list of str, contents: list of nodes
ui.modal(id, btn_text, title, content)
ui.tooltip(content, tip_text)
ui.toast(id, text)

Low-level helpers:

ui.text(value)    // creates a text node, OR a state node if value is a StateRef
ui.raw(html_str)  // injects raw HTML without escaping (use with care!)
Pitfall — do not double-wrap state nodes Text-accepting element methods (ui.h1, ui.p, ui.span, etc.) internally call self.text(value). Passing them a StateRef directly creates a state node. Passing them an already-built node — e.g. ui.h2(ui.text(counter)) — causes the outer method to re-wrap that state dict as a plain text node, stringifying it into JSON output. Pass StateRefs directly: ui.h2(counter), not ui.h2(ui.text(counter)).

Trucky (App) — Methods

MethodDescription
app.set_theme(overrides)Apply theme overrides. Recognized keys: primary, secondary, accent, background, background_alt, text, text_muted, border, success, error, warning, info, radius, title.
app.state(default_val)Creates or restores a StateRef for the next available state slot. On first contact per session, initialises to default_val; thereafter restores the persisted value.
app.action(fn)Registers a callback (zero-arg function/lambda) and returns its action id (e.g. "act0"). Fires when a POST arrives with X-Trucky-Action: <id>.
app.form_value(name)Reads a field from the incoming JSON form body of an action POST.
app.set_errors(errors_dict)Replaces the current field-error map.
app.error_for(name)Gets the error message for a field (empty string if none).
app.notify(text, kind)Sets a flash notice. kind is one of "success", "error", "info", "warning".
app.validate(schema, values)Validates values against schema, returning an errors dict. Schema fields: required (bool), type ("number"), min, max, minlen, maxlen.
app.mount(tree)Installs the root element tree.
app.run()Renders the tree. In serve mode, sets response.body and response.headers["Content-Type"]. In script mode, prints the rendered HTML to stdout.

Form Validation Example

bring trucky

var app = Trucky()
var ui = UI()

var schema = {
    "name":  { "required": true, "minlen": 2, "maxlen": 50 },
    "email": { "required": true, "minlen": 5 },
    "age":   { "type": "number", "min": 18, "max": 120 }
}

func handle_submit() {
    var form = {
        "name":  app.form_value("name"),
        "email": app.form_value("email"),
        "age":   app.form_value("age")
    }
    var errors = app.validate(schema, form)
    app.set_errors(errors)
    if len(keys(errors)) == 0 {
        app.notify("Saved!", "success")
    } else {
        app.notify("Please fix the errors below.", "error")
    }
}

func build_form() {
    return ui.form(app, [
        ui.label("Name", "name"),
        ui.input_text("name", "Your name", ""),
        ui.error_for(app, "name"),
        ui.label("Email", "email"),
        ui.input_email("email", "you@example.com", ""),
        ui.error_for(app, "email"),
        ui.label("Age", "age"),
        ui.input_number("age", "18", ""),
        ui.error_for(app, "age"),
        ui.submit("Save")
    ], () => {
        handle_submit()
    })
}

app.mount(ui.container([ ui.card([ ui.card_body([ build_form() ]) ]) ]))
app.run()

Rendering Pipeline

Internally, every UI element is just a dict like { "kind": "el", "tag": "div", "cls": "tk-card", "style": "", "attrs": {}, "children": [...] }. The renderer walks the tree recursively and produces an HTML string:

  • kind: "text" — html-escaped to_string(value)
  • kind: "state" — html-escaped to_string(ref.get()), dereferencing the StateRef
  • kind: "raw" — the raw string, not escaped (use with care)
  • kind: "el"<tag class="..." style="..." attrs>children</tag>; void tags (input, br, hr, img, meta) are self-closing
  • kind: "error_slot" / kind: "notice_slot" — renders the field error / flash notice if present, empty string otherwise

You can build element dicts by hand — UI is just sugar. The full HTML document — doctype, theme CSS, runtime JS for action dispatch, modals, tabs, tooltips, toasts — is assembled internally and assigned to response.body when you call app.run() in serve mode.

Session & State Persistence

Every visitor gets a trucky_sid cookie, auto-generated on first contact. All state slots are namespaced under <sid>:<key> in the serve-mode server store. State persists across page reloads and survives the action POST → re-render cycle, but is isolated per browser session.

In script mode, with no request / response globals available, Trucky's init catches the lookup error and falls back to non-serve mode — app.run() just prints the rendered tree to stdout, useful for testing layouts from the command line.

Theming

Default theme tokens are exposed as CSS custom properties (--tk-primary, --tk-secondary, etc.) on :root. Override them with app.set_theme({ ... }) before calling app.run(). The base stylesheet uses these variables, so changing one token recolors the entire UI consistently.

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.
  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. A lightweight compiler pass 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. 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 program cache — for compiled AST reuse — and the store — for persistent key-value storage — via Arc. The store uses a mutex-protected hash map 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 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

  • Concurrency: Pitruck scripts run single-threaded within each request. The web server spawns a new OS thread per connection, and the store provides thread-safe shared state — but more complex concurrency patterns (async, futures) are not natively supported in user space.
  • Integer precision: because all numbers are f64, integers lose exact precision beyond 2^53 - 1.
  • Error messages: parse errors report the exact line and column, but runtime errors only report the line number.
  • Division by zero: since 1.6, division by zero throws a RuntimeError rather than returning infinity or NaN — a deliberate design choice for safer scripting.

Changelog

1.6.0 → 1.6.1 — library & permissions fix-up release

Fixed File I/O in script mode

pitruck main.pr used to crash on write_file(...) / read_file(...) with "file write access is not allowed in this context", because the interpreter defaulted to all permissions denied and the script-run code path never applied the CLI flags. The --allow-read / --allow-write flags were only parsed in the --serve branch, so passing them to a plain script run did nothing. Script mode now defaults to fully-allowed; the --allow-* flags are accepted as no-ops for muscle-memory compatibility, and new --deny-read / --deny-write / --deny-net / --deny-all flags let you opt out.

Fixed Dot-assignment on dicts (obj.name = value)

Previously only Instance values supported obj.name = value; assigning to a Dict via dot-notation crashed with "cannot set property 'name' on a non-instance value". This broke the entire struct/struct_copy API, since structs are dicts. Dot-assignment now works on dicts the same way bracket-assignment does. Error messages also report the actual type instead of the opaque "non-instance value".

New Static method lookup on classes

MyClass.method_name now returns the underlying function value, so class methods can be called statically. Previously this returned "cannot access property on a non-instance value" because property access only handled Instance and Dict.

Fixed Bundled library restructure (matches the docs)

The docs claimed bring color, bring math, bring struct, bring system, bring time, but only lib/std.pr and lib/trucky.pr existed, and std.pr wrapped everything inside a class so none of the documented bare-function calls actually worked. Now lib/ contains the five per-category modules the docs always described, plus std.pr as a meta-module that brings them all — plus uuid_v4() — plus trucky.pr.

Enhanced Library documentation rewritten

The Bundled Libraries section now matches the actual API surface of every library. The Trucky section in particular was completely wrong before — it described classes and functions that don't exist. The new docs describe the actual Trucky, UI, and StateRef classes, every UI constructor, the rendering pipeline, session persistence, theming, and a complete form-validation example.

Pzcax — Maintainer for Pitruck Liuxium — Website Dev Claude — Used for the docs, and some help in the site