Pitruck 1.5 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.

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 lib install <url>           # Download and install a library
pitruck lib list                    # List installed libraries
pitruck lib delete <name>           # Remove an installed library

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.

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.

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, \", \`, \\, \$, \xHH (Hex), \u{HHHH} (Unicode).

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.

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.

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.

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.

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, and the executable's path.

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
}

HTTP Web Server

A flagship feature of Pitruck is its ability to natively serve HTTP 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.

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.
Sandbox Restrictions: When running in --serve mode, outbound HTTP requests (http_request) and file I/O (sys_readfile, sys_writefile) are disabled for security.

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

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).

Limitations & Known Quirks