Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Welcome to Pith

pith /pɪθ/ noun

  1. The essential substance or central core of a matter; the heart.
  2. Vigorous, concise, and pointed expression.

A minimal, zero-dependency systems-scripting language built for deterministic execution and structural clarity.


Pith is designed to be dead simple to read and write, giving you the ease of a beginner-friendly scripting language with the raw speed and power of native machine code.

If you are new to programming, Pith is built for you: no curly braces {}, no semicolons ;, and no confusing boilerplate. If you already know other languages, Pith gives you speed and simplicity without getting in your way.

# A simple Pith program
name = "World"
print "Hello, " + name

mut count = 1
while count <= 3
    print "Count: " + count
    count = count + 1
end

Why Pith?

  • Easy to Read: Blocks close with a simple end. No braces, no semicolons, no clutter.
  • Fast & Lightweight: Pith compiles directly to fast machine code. There is no heavy virtual machine or garbage collector slowing things down.
  • Helpful Errors: When something goes wrong, Pith shows you exactly where the error is and how to fix it in plain English.
  • Instant Testing: Run scripts instantly with pith run, or experiment live in the terminal using pith repl.
  • Standalone Binaries: Turn any script into a standalone native executable with pith build.

Getting Started

  1. Installation: Download and install Pith in one command.
  2. Your First Script: Follow our step-by-step beginner tutorial.
  3. Language Reference: Learn how variables, loops, and functions work.
  4. Built-in Tools: Work with files (fs), system info (os), and networking (net).

Installation

POSIX shell (Linux, macOS, FreeBSD):

curl -fsSL https://raw.githubusercontent.com/abit-foggy/pith/main/install.sh | sh

Windows (PowerShell):

iwr https://raw.githubusercontent.com/abit-foggy/pith/main/install.ps1 -UseBasicParsing | iex

Windows (Command Prompt):

curl -fsSL https://raw.githubusercontent.com/abit-foggy/pith/main/install.cmd -o install.cmd && install.cmd

This downloads the latest release and installs under a prefix (~/.local on POSIX, %LOCALAPPDATA%\pith on Windows NT):

PathWhat it is
<prefix>/bin/pithThe toolchain binary (libtcc embedded statically)
<prefix>/lib/pith/tcc/libtcc1.aThe vendored tcc runtime
<prefix>/lib/pith/runtime/libruntime.aThe ARC runtime library
<prefix>/include/pith.h (+ other headers)The FFI and ABI headers

Add <prefix>/bin to your PATH if it isn’t already (the installer prints the exact command).

macOS note

The installer checks for the Xcode command line utilities (they provide clang and the SDK). If missing, it offers to install them (xcode-select --install, password prompt).

Installer overrides

PREFIX=/usr/local sh install.sh          # install system-wide (POSIX)
PITH_REPO=owner/pith sh install.sh       # use a fork
PITH_TAG=v0.1.0 sh install.sh            # install a specific version

From source

git clone --recurse-submodules https://github.com/abit-foggy/pith
cd pith
make

The vendored tcc is a git submodule, the build compiles it automatically. If you cloned without --recurse-submodules:

git submodule update --init

Then run the test suite to verify:

make check

External tools

Pith needs one external tool at runtime:

ToolPurposeInstall
qbeLowers QBE IL to machine assemblyQBE

The assembly is assembled in-process by the embedded tcc’s built-in assembler (Linux, Windows NT, FreeBSD); on Darwin, clang’s assembler is used (from the Xcode command line utilities). mold is optional (Darwin AOT builds only).

Verify

pith version
# pith 0.1.0

pith engine
# pith engine report
#   host os           : linux
#   execution backend : libtcc (in-memory)
#   aot linker        : tcc (embedded, in-process)
#   qbe               : /usr/bin/qbe
#   ...

What’s inside a release

FileDescription
pithThe toolchain binary, libtcc is embedded statically
libtcc1.atcc runtime, needed by the JIT at relocate time
libruntime.aThe ARC runtime, linked into pith build output
include/pith.hThe FFI header for imported C modules
include/api.hRuntime ABI bindings
include/compiler.hInternal API (tokens, AST, engine)
include/pith_embed.hEmbeddable C host interface

Tutorial: Your First Script

Welcome to Pith! In this tutorial, you’ll write your very first program, learn the basic building blocks, and turn your script into a real standalone application that runs at native speed.

It takes less than 5 minutes, so let’s jump right in!


Step 1: Hello, World!

First, create a new file named hello.pi in your favorite text editor.

Add this single line of code to it:

# hello.pi
print "Hello from Pith!"

Running Your Script

Open your terminal in the same folder and type:

pith run hello.pi

You will see:

Hello from Pith!

Congratulations! You just ran your first Pith program. Notice how instantaneous it was? pith run compiles and executes your code directly into memory in milliseconds.


Step 2: Adding a Variable

Let’s make our program a little more personal. Instead of hardcoding the message, let’s store a name in a variable:

# hello.pi
name = "Alex"
print "Hello, " + name + "!"

Run it again:

pith run hello.pi

Output:

Hello, Alex!

Clean Syntax

Look closely at what we wrote:

  • No semicolons ; at the end of lines
  • No parentheses required around print
  • Just clean, natural code

Step 3: Making Decisions with if

Now let’s teach our script how to make choices. We’ll check if the player’s score is high enough to win:

# hello.pi
player = "Alex"
score = 100

print "Welcome, " + player + "!"

if score >= 100
    print "You win the game!"
else
    print "Keep playing!"
end

Run it:

pith run hello.pi

Output:

Welcome, Alex!
You win the game!

In Pith, code blocks don’t need curly braces {} or indentation rules. You open an if block, write your code, and close it with a single end.


Step 4: Making Numbers Change with while and mut

Now let’s add a loop that counts down from 3 before starting:

# hello.pi
mut count = 3

while count > 0
    print "Starting in: " + count
    count = count - 1
end

print "Go!"

Notice the word mut in front of count? By default, Pith prevents variables from changing so you don’t accidentally introduce bugs. Adding mut (short for mutable) tells Pith: “I want this variable to change as the program runs!”

Run the script:

pith run hello.pi

Output:

Starting in: 3
Starting in: 2
Starting in: 1
Go!

Step 5: Build a Standalone Executable

Here is where Pith really shines. With other beginner-friendly languages, you need a heavy runtime, virtual machine, or interpreter installed on every computer that runs your script.

With Pith, you can compile your script into a single, standalone binary file with one command:

pith build hello.pi

Pith builds a standalone executable named hello. Now run it directly from your terminal:

./hello

You can take this hello file and run it on another computer without needing to install Pith at all! It runs directly on the processor with maximum speed and minimum memory usage.


What’s Next?

You now know how to write scripts, store variables, make decisions, repeat actions, and build standalone apps.

Here are great places to explore next:

Tutorial: Organizing a Project

When your program grows beyond a single file, Pith makes it easy to organize your code into a clean, professional project with dependencies and automated tasks.

In this tutorial, you’ll learn how a Pith project is structured, how to split code into multiple files, and how to use pith.toml.


Step 1: Project Structure

Here is what a complete Pith project typically looks like:

my_game/
├── pith.toml        # Project settings and tasks
├── main.pi          # Your main starting file
├── helpers.pi       # Extra helper functions
└── dist/            # Where built apps go (optional)

You don’t need complicated build systems or endless configuration files: just a folder, your .pi scripts, and an optional pith.toml.


Step 2: Creating pith.toml

The pith.toml file stores basic information about your project and custom commands you want to run.

Create a file named pith.toml in your project folder:

[project]
name = "my_game"
version = "0.1.0"

[build]
target = "native"

[tasks.start]
run = "pith run main.pi"

[tasks.test]
all = "pith run tests/test_game.pi"

Step 3: Splitting Code Across Multiple Files

As your app grows, you can divide your code into separate files to keep everything tidy.

For example, create helpers.pi with a helper function:

# helpers.pi
fn show_banner(title)
    print "===================="
    print "   " + title
    print "===================="
end

Then create your main program in main.pi:

# main.pi
show_banner("MY AWESOME GAME")
print "Ready to play!"

To run both files together, simply pass them to pith run:

pith run main.pi helpers.pi

Output:

====================
   MY AWESOME GAME
====================
Ready to play!

To build a standalone executable from all files together:

pith build main.pi helpers.pi -o my_game
./my_game

Pith automatically combines them into a single, lightning-fast native executable.


Step 4: Running Custom Shortcut Tasks

Remember the [tasks] we added to pith.toml? You can run them anytime as shortcuts:

# Runs "pith run main.pi" automatically
pith start

# Runs your test suite
pith test all

This saves you from typing long terminal commands over and over.


Step 5: Adding Packages with pith pkg

If you want to use a library written by another developer, install it with one command:

pith pkg add os-utils 1.0.0

Pith downloads the package and saves it locally in .pith/pkgs/. You can immediately use it in your code!


Step 6: Built-in Time Machine (--embed-source)

Have you ever lost the source code to an executable you built months ago? Pith has a built-in superpower:

pith build main.pi helpers.pi --embed-source -o my_game

When you add --embed-source, Pith safely packs your project files directly inside the executable. Anyone with the binary can recover the original source code:

pith decompile ./my_game

Your files are cleanly restored to ./restored_workspace/. It’s like having source code version recovery baked right into your binary!


Next Steps

Now that your project is organized:

  • Built-in Tools: Read and write files (fs), inspect the computer (os), and connect to sockets (net)
  • Interactive REPL: Try out Pith commands live in your terminal

Language Reference

Pith is designed to be simple and natural to read.

There are no curly braces {} and no semicolons ;. Each line is its own statement, and code blocks simply finish with the word end.


Quick Cheat Sheet

What you want to doExampleDescription
Create a variablename = "Alex"Stores a value that won’t change
Create a changeable variablemut score = 0Stores a value you can change later
Print somethingprint "Hello!"Shows text or numbers in the terminal
If / else decisionif score > 10 ... endRuns code only when something is true
Repeat codewhile score < 10 ... endRepeats code in a loop
Break out of a loopbreakStops a loop immediately
Skip to next loop stepcontinueJumps to the next turn of the loop
Check two thingsif logged_in and is_adminTrue only if both conditions are true
Check either thingif is_weekend or on_vacationTrue if at least one condition is true
Invert a conditionif not finishedTrue if finished is false
Create a functionfn add(a, b) ... endReusable block of code
Return from functionreturn a + bSends a value back to whoever called it

The Basic Rules

1. One Statement Per Line

You don’t need semicolons. Just write one command per line:

x = 10
y = 20
print x + y

2. Comments Start with #

Use # to write notes in your code that the computer ignores:

# This is a helpful comment
score = 100  # You can also add comments at the end of a line

3. Strings Use Double Quotes

Text is written inside double quotes:

message = "Hello, World!"

To include special characters:

  • \n creates a new line
  • \t creates a tab space
  • \" includes a quote inside the text

Detailed Chapters

Variables & Mutability

Think of a variable as a labeled storage box in your computer’s memory. You give it a name, put some data inside, and use it whenever you need it.


Step 1: Creating Your First Variable

To create a variable, write its name, followed by an equals sign =, and the value you want to store:

name = "Alex"
score = 10

print "Player: " + name
print "Score: " + score

When you run this, Pith prints:

Player: Alex
Score: 10

Notice how easy that was? You didn’t have to declare complex types, write semicolons, or wrap anything in brackets. Pith takes care of the details so you can focus on building your idea.


Step 2: Understanding Why Variables Stay Safe by Default

Imagine you’re writing a game. You set a player’s score at the start:

score = 10

Later on in your code, you accidentally try to overwrite it without realizing it:

score = 10
score = 20      # Oops! Did you mean to change it, or was it an accident?

In many other languages, this bug slips through quietly and breaks your program later. But Pith protects you right away! If you run this code, Pith gives you a friendly, clear message:

error: cannot assign twice to immutable variable `score` (declare with `mut` to reassign)

By default, every variable in Pith is immutable (meaning unchangeable). This simple rule prevents common bugs before they ever happen.


Step 3: Making Variables Changeable with mut

What if you do want a variable to change (like when a player scores points, or when you are counting items in a loop)?

Just put the word mut (short for mutable, or changeable) in front of the variable name when you first create it.

Look at how the exact same variable score works now:

# Notice the only change: we added `mut` at the beginning!
mut score = 10
score = 20          # Works perfectly! The score is now 20.
score = score + 5   # We can keep updating it as much as we want!

print "Final score: " + score

Output:

Final score: 25

Side-by-Side Comparison

Look at both examples together:

Without mutWith mut
pith<br>score = 10<br>score = 20 # Error! Protected from changes<br>pith<br>mut score = 10<br>score = 20 # Allowed! Can change freely<br>

Both examples use the exact same variable score. Adding mut is simply your way of telling Pith: “I plan on updating this value later.”


Step 4: Where Variables Live (Scope)

Variables belong to the block of code where you created them. For example, if you create a variable inside an if block, it only lives inside that block:

if 1
    secret = "Top secret message"
    print secret     # Works!
end

# Once the block ends, `secret` disappears to keep memory clean.

However, variables created outside a block can easily be read inside it:

welcome = "Welcome back, Alex!"

if 1
    print welcome    # Works! Reads the variable from outside.
end

Fast & Automatic Cleanup

You never have to manage memory, free pointers, or wait on a sluggish garbage collector. When your variables finish their job, Pith cleans them up instantly behind the scenes. You get the simplicity of a beginner-friendly scripting language with the blazing speed of native code.


Next Steps

Now that you know how to save and change information, let’s explore what kinds of data you can store:

Tutorial: Working with Data Types

Every computer program works with data: whether it’s keeping track of a player’s score, greeting a user by name, or checking if a setting is switched on or off.

In Pith, you don’t have to wrestle with complicated type declarations. You just write your value, and Pith figures out the type automatically!


1. Numbers: Counting and Measuring

Pith handles both whole numbers and decimals naturally.

Whole Numbers (Integers)

Use whole numbers for counting items, tracking scores, or measuring age:

score = 100
level = 1
items_collected = 5
temperature = -4    # Negative numbers work just as easily!

Decimals (Floating-Point Numbers)

When you need to measure something with precision (like money or percentages), add a decimal point:

price = 19.99
multiplier = 1.5
pi = 3.14159

You can do math with numbers freely using +, -, *, and /:

total = price * 2
print "Total cost: " + total

2. Text (Strings)

Text in Pith is called a string. You write text by enclosing words between double quotes "...":

player_name = "Alex"
message = "Welcome to the game!"

Joining Text Together

You can glue pieces of text together using the + symbol:

greeting = "Hello, " + player_name + "!"
print greeting    # prints "Hello, Alex!"

You can even add numbers directly into text messages:

score = 250
print "Your score is: " + score   # prints "Your score is: 250"

3. True & False (Booleans)

Sometimes your program needs to know if a statement is true or false. In Pith, decisions evaluate to:

  • 1 for True (yes, active, enabled)
  • 0 for False (no, inactive, disabled)
is_logged_in = 1
has_key = 0

if is_logged_in
    print "Welcome back!"
end

if not has_key
    print "The door is locked."
end

Conditions like score > 50 or name == "Alex" automatically produce 1 or 0.


4. Power Feature: Exact Sizes (When You Need Extra Control)

One of Pith’s greatest strengths is that it’s as easy as Python, but gives you the low-level power of C or Rust whenever you want it.

If you are building games, networking tools, or high-performance apps and want to limit a number to an exact size, you can add an optional type with a colon ::

# u8 means "unsigned 8-bit integer": holds whole numbers from 0 to 255
byte_value: u8 = 200

# i32 means standard 32-bit integer: holds numbers up to 2 billion
large_count: i32 = 1000000
SizeValues AllowedIdeal For
u80 to 255Bytes, colors (Red/Green/Blue), small counters
i8-128 to 127Small signed values
u160 to 65,535Network ports, game inventory slots
i32Over 2 billionHigh scores, large item counts
f64DecimalsScientific precision and 3D game coordinates

Automatic Safety Checks

If you use an exact size, Pith protects you from putting the wrong value into it before your program even runs:

score: u8 = 500     # Error: 500 is too large for u8 (max is 255)

Next Steps

Now that you know what kinds of values you can work with:

Tutorial: Math, Comparisons & Logic

Computers are world-class calculators. In this tutorial, you’ll learn how to do calculations, compare values, and combine conditions using simple, everyday English words like and, or, and not.


Step 1: Doing Math

Pith handles regular arithmetic just like you’d write it on paper:

# 1. Addition and Subtraction
items = 10 + 5       # 15
remaining = 20 - 8   # 12

# 2. Multiplication and Division
area = 5 * 10        # 50
half = 100 / 2       # 50

# 3. Negative numbers
change = -15

Controlling Order with Parentheses

Just like in school math, multiplication and division happen before addition and subtraction. If you want addition to happen first, wrap it in parentheses ( ):

# Without parentheses: 3 * 4 = 12, then 2 + 12 = 14
result1 = 2 + 3 * 4

# With parentheses: (2 + 3) = 5, then 5 * 4 = 20
result2 = (2 + 3) * 4

print "Result 1: " + result1
print "Result 2: " + result2

Step 2: Combining Text with +

The + symbol is extra handy in Pith: it also glues text together!

first_name = "Alex"
message = "Welcome, " + first_name + "! Ready to code?"
print message

Output:

Welcome, Alex! Ready to code?

Step 3: Comparing Values

Whenever your program needs to check if two things match or if one number is bigger than another, use comparison symbols. Comparisons always evaluate to 1 (true) or 0 (false).

SymbolWhat It ChecksExampleMeaning
==Exactly equal toscore == 100Is the score exactly 100?
!=Not equal toitems != 0Are items different from 0?
<Less thanage < 18Is age under 18?
<=Less than or equallevel <= 5Is level 5 or lower?
>Greater thanscore > 50Is score higher than 50?
>=Greater than or equalcoins >= 10Do you have 10 or more coins?

Comparing Text

You can also compare text directly with == and !=:

user_role = "admin"

if user_role == "admin"
    print "Welcome to the dashboard!"
end

Step 4: Logic with and, or, and not

In some other languages, you have to remember cryptic symbols like &&, ||, and !. In Pith, you write the exact English words you’re thinking!

1. and: Both Must Be True

Use and when both requirements have to be met:

has_ticket = 1
is_open = 1

if has_ticket and is_open
    print "You may enter the concert!"
end

2. or: At Least One Must Be True

Use or when having either option is good enough:

is_weekend = 1
on_holiday = 0

if is_weekend or on_holiday
    print "Time to relax!"
end

3. not: Flip True to False (or False to True)

Use not when you want to check if something is not the case:

is_raining = 0

if not is_raining
    print "Let's go for a walk outside!"
end

Combining Everything Naturally

You can combine them easily using parentheses:

has_id = 1
is_vip = 0
is_banned = 0

if (has_id or is_vip) and not is_banned
    print "Entry approved!"
end

Next Steps

Now that you can calculate and compare values:

Tutorial: Decisions & Loops

Programs become truly powerful when they can make choices on their own and repeat actions automatically.

In this tutorial, you’ll learn how to guide your code using if statements and repeat work using while loops.


Step 1: Making Decisions with if

An if statement tells your computer: “Only run this code if a specific condition is true.”

Let’s test if a player has reached a winning score:

score = 100

if score >= 100
    print "Congratulations, you won!"
end

Notice how clean the syntax is:

  • No parentheses () required around score >= 100
  • No colon : at the end of the line
  • Just write your code, and close it with end

Step 2: Handling Alternatives with else and elseif

What if the condition isn’t met? You can provide a fallback response with else:

score = 45

if score >= 50
    print "You passed the test!"
else
    print "Keep practicing, you can do it!"
end

If you have multiple options to check (like assigning grades), chain them together using elseif:

score = 85

if score >= 90
    print "Grade: A"
elseif score >= 80
    print "Grade: B"
elseif score >= 70
    print "Grade: C"
else
    print "Grade: Needs improvement"
end

Pith checks each condition in order from top to bottom. As soon as one matches, it runs that code and moves on!


Step 3: Repeating Actions with while

What if you want to print a countdown or repeat an action 10 times? Instead of copying and pasting your code, use a while loop!

A while loop keeps running as long as its condition stays true:

# Remember: we use `mut` because `count` is going to change!
mut count = 1

while count <= 5
    print "Turn: " + count
    count = count + 1
end

print "All turns completed!"

Output:

Turn: 1
Turn: 2
Turn: 3
Turn: 4
Turn: 5
All turns completed!

Step 4: Special Loop Controls (break and continue)

Sometimes you want to break out of a loop early, or skip one specific round. Pith gives you two simple commands:

1. break: Stop the Loop Immediately

Use break when you’ve found what you were looking for and don’t need to keep searching:

mut number = 1

while number <= 10
    if number == 4
        print "Found number 4! Stopping early."
        break
    end
    print "Checking: " + number
    number = number + 1
end

Output:

Checking: 1
Checking: 2
Checking: 3
Found number 4! Stopping early.

2. continue: Skip to the Next Round

Use continue when you want to skip the rest of the current turn and jump straight to the next one:

mut number = 0

while number < 5
    number = number + 1
    if number == 3
        # Skip number 3!
        continue
    end
    print "Processing item: " + number
end

Output:

Processing item: 1
Processing item: 2
Processing item: 4
Processing item: 5

Notice that Processing item: 3 was skipped completely!


Next Steps

Now that you can guide your code’s decisions and repeat actions:

  • Functions: Learn how to bundle your code into reusable actions
  • Math & Logic: Combine conditions with and, or, and not

Tutorial: Reusable Functions

As your programs get bigger, you’ll often want to perform the same action in several different places. Instead of copying and pasting code, you can group it into a function!

Think of a function like a recipe or an action button: you define what it does once, and then you can trigger it whenever you need it.


Step 1: Creating Your First Function

In Pith, you create a function with the fn keyword, followed by the name you choose, parentheses ( ) for any inputs, and finish with end:

fn greet(name)
    print "Hello, " + name + "! Welcome to Pith."
end

# Now call your function whenever you want:
greet("Alex")
greet("Jordan")

Output:

Hello, Alex! Welcome to Pith.
Hello, Jordan! Welcome to Pith.

Step 2: Sending Back an Answer with return

Sometimes you want a function to calculate a value and hand it back to you. You do this using return:

fn add(first, second)
    return first + second
end

# Store the result in a variable:
total = add(15, 25)
print "The sum is: " + total

Output:

The sum is: 40

Step 3: Making Decisions Inside Functions

You can use if statements inside your function to return different results depending on the input:

fn check_pass(score)
    if score >= 70
        return "Passed"
    end
    return "Needs Retest"
end

print check_pass(85)   # prints "Passed"
print check_pass(55)   # prints "Needs Retest"

Notice that as soon as Pith hits a return, it immediately exits the function with the answer and skips any lines below it.


Step 4: Functions That Call Themselves (Recursion)

A function can even call itself to solve a countdown or repetitive task!

fn blastoff(seconds)
    if seconds <= 0
        print "Blast off! 🚀"
        return 0
    end

    print seconds
    return blastoff(seconds - 1)
end

blastoff(3)

Output:

3
2
1
Blast off! 🚀

High Performance & Zero Waste

In Pith, functions run at native machine speed. Even better: if you write helper functions in your project that you don’t end up using, Pith’s compiler automatically removes them when building your final application. Your executable stays as small, lean, and fast as possible with zero bloat!


Next Steps

Now you have mastered the core foundations of Pith!

Common Errors & How to Fix Them

When something in your code needs attention, Pith points directly to the line and explains the problem in plain English.

error: cannot assign twice to immutable variable `score` (declare with `mut` to reassign)
  --> game.pi:3:1
   |
 3 | score = 20
   | ^

Here are the most common errors and how to solve them:


1. Changing a Variable Without mut

error: cannot assign twice to immutable variable `score` (declare with `mut` to reassign)
  • What it means: You created score = 10 and later tried to change it to score = 20.
  • How to fix it: Add mut when you first create the variable:
    mut score = 10
    score = 20    # Works!
    

2. Using break or continue Outside a Loop

error: `break` outside of a loop
error: `continue` outside of a loop
  • What it means: break and continue only make sense inside a loop.
  • How to fix it: Place break or continue inside a while ... end loop.

3. Using a Variable Before Creating It

error: use of undeclared identifier `name`
  • What it means: Pith doesn’t recognize name.
  • How to fix it: Make sure you created the variable with name = "..." earlier in the code, or check for typos.

4. Multiple Commands on One Line

error: expected end of line between statements
  • What it means: You wrote two commands on the same line without pressing Enter.
  • How to fix it: Put each command on its own line:
    # Instead of: x = 10 y = 20
    x = 10
    y = 20
    

5. Number Too Big for Sized Type

error: literal is out of range for type u8 (maximum value is 255)
  • What it means: You gave a number larger than the maximum allowed by that type (like val: u8 = 300).
  • How to fix it: Use a smaller number, or remove the : u8 to let Pith handle the size automatically.

Helpful Notes

You may also see green or blue notes in the terminal:

  • note: private function is never referenced; eliminated: Pith noticed a function you wrote was never called, so it left it out to keep your program lean and fast.
  • note: unreachable exit path: A function returns early before reaching subsequent lines.

Built-in Tools (Namespaces)

Pith comes packed with powerful built-in tools right out of the box. You don’t have to install heavy third-party packages or write dozens of lines of boilerplate just to read a file, check the operating system, or connect over a network.

These built-in tools are organized into namespaces:

  • fs: Read, write, and manage files
  • os: Check system details and platform kernels
  • proc: Control processes, read CLI arguments, environment variables, and process ID
  • net: Send and receive data over TCP networks
  • str: Search, inspect, and transform ASCII strings

1. Filesystem Tools: fs

Reading and writing files in Pith takes just one line of code:

Writing to a File

Use fs.writeFile(path, content). It returns 1 if the write succeeded, or 0 if there was an error:

success = fs.writeFile("notes.txt", "Pith makes coding fun!")

if success
    print "File saved successfully!"
else
    print "Could not write to file."
end

Reading from a File

Use fs.readFile(path). It returns the text inside the file as a string (or an empty string "" if the file could not be read):

content = fs.readFile("notes.txt")
print "File contents:"
print content

Checking if a File Exists

Use fs.exists(path). It returns 1 if the file or path exists, or 0 if it does not:

if fs.exists("notes.txt")
    print "notes.txt is present!"
end

Deleting a File

Use fs.remove(path) to delete a file. It returns 1 on success, or 0 on error:

if fs.remove("notes.txt")
    print "File removed."
end

fs Reference

FunctionWhat it doesReturns
fs.readFile(path)Reads an entire fileFile contents as text (or "" on error)
fs.writeFile(path, content)Writes text into a file1 on success, 0 on error
fs.exists(path)Checks if a file exists1 if exists, 0 otherwise
fs.remove(path)Deletes a file1 on success, 0 on error

2. Operating System Tools: os

The os namespace lets your program ask questions about the computer it’s running on:

# Check the platform
if os.isLinux
    print "Running on Linux!"
elseif os.isMacOS
    print "Running on macOS!"
elseif os.isNT
    print "Running on Windows!"
end

# Get the exact kernel name
print "Kernel: " + os.identifyKernel

os Reference

MemberWhat it doesReturns
os.identifyKernelOperating system kernel"linux", "darwin", "nt", "freebsd"
os.identifyKernelVersionKernel release version stringe.g. "6.5.0-generic"
os.isLinuxChecks if running on Linux1 if true, 0 otherwise
os.isMacOSChecks if running on Apple macOS1 if true, 0 otherwise
os.isNTChecks if running on Windows1 if true, 0 otherwise
os.isDarwinChecks if kernel is Darwin1 if true, 0 otherwise
os.isFreeBSDChecks if running on FreeBSD1 if true, 0 otherwise

3. Process Tools: proc

The proc namespace controls the running process: checking command-line arguments, reading environment variables, querying the process ID, and exiting.

Reading Command-Line Arguments & Environment

You can read arguments passed into your script from the terminal:

# Check how many arguments were passed:
count = proc.argCount

# Read the first argument (0 is the first argument after your script name):
if count > 0
    first_arg = proc.getArg(0)
    print "First argument: " + first_arg
end

# Read environment variables (e.g. USER, PATH, HOME):
user = proc.getEnv("USER")
print "Current user: " + user

# Get the process ID:
pid = proc.pid
print "Running PID: " + pid

Exiting a Program

Use proc.exit(code) to terminate the program immediately with an exit status:

if count == 0
    print "Error: missing required argument"
    proc.exit(1)
end

Pausing Execution (Sleeping)

Use proc.sleep(ms) to pause execution for a given number of milliseconds:

print "Waiting 250 milliseconds..."
proc.sleep(250)
print "Done!"

proc Reference

MemberWhat it doesReturns
proc.argCountTotal CLI arguments passedInteger count
proc.getArg(index)Gets argument at 0-based indexString argument
proc.getEnv(name)Gets an environment variableString value (or "" if unset)
proc.pidProcess ID of current processInteger PID
proc.sleep(ms)Pauses execution for millisecondsvoid
proc.exit(code)Terminates program immediatelyExits with given status code

4. Network Tools: net

Need to talk to a web server or create a TCP connection? Pith includes dead-simple networking:

# Connect to a web server on port 80:
fd = net.connect("example.com", 80)

if fd >= 0
    # Send an HTTP request
    net.send(fd, "GET / HTTP/1.0\r\nHost: example.com\r\n\r\n")

    # Read the response (up to 4096 bytes)
    response = net.recv(fd, 4096)
    print response

    # Close the connection when done
    net.close(fd)
end

net Reference

FunctionWhat it doesReturns
net.connect(host, port)Connects to a TCP host and portSocket ID (or -1 on error)
net.send(fd, message)Sends text over the socketBytes sent (or -1 on error)
net.recv(fd, maxBytes)Reads text from the socketReceived text (or "" on EOF/error)
net.close(fd)Closes the connectionNothing
net.socket(domain, type, proto)Creates a raw socketSocket ID (or -1 on error)

5. String Tools: str

The str namespace provides byte-oriented string queries, substring tests, and ASCII case folding:

greeting = "Hello, World!"

# Query byte length
len = str.length(greeting)
print len  # 13

# Substring and prefix/suffix queries
if str.contains(greeting, "World")
    print "Found World!"
end

if str.startsWith(greeting, "Hello")
    print "Starts with Hello"
end

if str.endsWith(greeting, "!")
    print "Ends with exclamation"
end

# Case transformation (ASCII)
shout = str.upper(greeting)
whisper = str.lower(greeting)
print shout    # "HELLO, WORLD!"
print whisper  # "hello, world!"

str Reference

FunctionWhat it doesReturns
str.length(s)Payload byte lengthInteger length
str.contains(s, sub)Substring search1 if found, 0 otherwise
str.startsWith(s, prefix)Prefix match1 if true, 0 otherwise
str.endsWith(s, suffix)Suffix match1 if true, 0 otherwise
str.upper(s)Uppercase copy (ASCII)New string
str.lower(s)Lowercase copy (ASCII)New string

6. How Namespaces Work Under the Hood

Pith uses a simple, predictable hierarchy:

  1. root.fs.* / root.os.* / root.proc.* / root.net.* / root.str.*: The built-in runtime functions directly. They can never be overridden.
  2. fs.* / os.* / proc.* / net.* / str.*: The active tools you use every day. If you import a module that enhances one of these, the enhancement applies here.
  3. alice.fs.*: If you import a custom package from another developer (like Alice), you can call their specific version directly by author name!

All strings, buffers, and data returned by built-in namespaces are automatically managed and cleaned up for you with zero performance overhead.

Standard Library Specification

This document provides a low-level, authoritative reference for experienced systems developers. It details the runtime signatures, memory contracts, error semantics, and underlying system call implementations of Pith’s standard library namespaces.


Namespace: fs

The fs namespace exposes file descriptor operations and direct filesystem access.

Functions

fn readFile(path: string) string

Reads the complete contents of the file specified by path into a newly allocated, null-terminated string.

  • Parameters:
    • path: Relative or absolute filesystem path.
  • Return value:
    • A reference-counted string containing the file data.
    • Returns an empty string "" on failure (e.g. ENOENT, EACCES, or if allocation fails).
  • Underlying Syscalls:
    • POSIX: stat(2) / open(2) / read(2) / close(2).
    • Windows: CreateFileA / GetFileSizeEx / ReadFile / CloseHandle.
  • Memory Safety:
    • Allocates a contiguous buffer sized to file length plus one byte. The returned PithValue* has an initial strong reference count of 1 and is reclaimed deterministically at scope exit.
fn writeFile(path: string, content: string) int

Creates or truncates the file at path and writes the provided content string.

  • Parameters:
    • path: Target file path.
    • content: Data buffer to write.
  • Return value:
    • 1 if all bytes were successfully written to disk.
    • 0 on system error or partial write.
  • Underlying Syscalls:
    • POSIX: open(path, O_WRONLY | O_CREAT | O_TRUNC, 0644), followed by complete loop write and close(2).
    • Windows: CreateFileA(..., GENERIC_WRITE, ..., CREATE_ALWAYS, ...).
fn exists(path: string) int

Checks whether a filesystem entry exists at path.

  • Parameters:
    • path: Target file or directory path.
  • Return value:
    • 1 if the file or directory exists.
    • 0 if it does not exist or access is denied.
  • Underlying Syscalls:
    • POSIX: access(path, F_OK).
    • Windows: GetFileAttributesA(path).
fn remove(path: string) int

Deletes the file entry at path.

  • Parameters:
    • path: Target file path.
  • Return value:
    • 1 if the file was deleted successfully.
    • 0 on system error (ENOENT, EACCES, etc.).
  • Underlying Syscalls:
    • POSIX: unlink(path).
    • Windows: DeleteFileA(path).

Namespace: os

The os namespace provides process control, command-line arguments, environment variable access, and host kernel identification.

Host Detection Properties

All platform inspection properties are pure query values evaluated at runtime:

val identifyKernel: string

Returns a static string literal identifying the host kernel: "linux", "darwin", "nt", "freebsd", or "unknown".

val identifyKernelVersion: string

Returns the operating system kernel release string. On POSIX systems, this reads from utsname.release via uname(2).

val isLinux: int     # 1 on Linux kernels, 0 otherwise
val isDarwin: int    # 1 on any Darwin kernel (uname = Darwin), 0 otherwise
val isMacOS: int     # 1 specifically on Apple macOS (__APPLE__ && __MACH__), 0 otherwise
val isNT: int        # 1 on Windows NT, 0 otherwise
val isFreeBSD: int   # 1 on FreeBSD kernels, 0 otherwise

Namespace: proc

The proc namespace provides process control, command-line arguments, environment variable inspection, and process lifecycle primitives.

Properties

val pid: int

Returns the unique process identifier of the current calling process.

  • Underlying Syscalls:
    • POSIX: getpid(2).
    • Windows: GetCurrentProcessId().
val argCount: int

Returns the number of command-line arguments passed to the script (equivalent to argc - 1 from the interpreter, or full argc for standalone compiled binaries). Can also be invoked as a function proc.argCount().

Functions

fn getEnv(name: string) string

Queries the environment list for the key name.

  • Return value:
    • Value associated with name as a string.
    • Empty string "" if the key is not found in the environment block.
  • Underlying Syscalls:
    • POSIX: getenv(3).
    • Windows: GetEnvironmentVariableA.
fn getArg(index: int) string

Returns the argument at 0-based index.

  • Return value:
    • String argument value.
    • Returns an empty string "" if index < 0 or index >= argCount().
fn sleep(ms: int) void

Suspends execution of the calling process for at least ms milliseconds.

  • Parameters:
    • ms: Duration in milliseconds (non-negative).
  • Underlying Syscalls:
    • POSIX: nanosleep(2).
    • Windows: Sleep(DWORD).
fn exit(code: int) void

Immediately terminates the calling process with status code.

  • Underlying Syscalls:
    • Invokes C exit(code) / _exit(2). Buffered file streams are flushed.

Namespace: net

The net namespace provides minimalist, non-blocking-capable TCP socket networking primitives.

Functions

fn socket(domain: int, type: int, protocol: int) int

Allocates a raw communication socket descriptor.

  • Parameters:
    • domain: Address family (2 for AF_INET, 10 for AF_INET6).
    • type: Socket semantics (1 for SOCK_STREAM, 2 for SOCK_DGRAM).
    • protocol: Protocol number (0 for IP default).
  • Return value:
    • Socket integer descriptor (>= 0) on success.
    • -1 on socket creation error.
  • Underlying Syscalls:
    • POSIX: socket(2).
    • Windows: WSASocket / socket.
fn connect(host: string, port: int) int

Resolves the destination host and establishes a TCP stream connection on port.

  • Parameters:
    • host: IPv4/IPv6 address or domain hostname (e.g. "127.0.0.1", "example.com").
    • port: Destination port number (1 to 65535).
  • Return value:
    • Connected socket descriptor (>= 0).
    • -1 if resolution or connection establishment fails.
  • Underlying Syscalls:
    • POSIX: gethostbyname(3) / getaddrinfo(3), socket creation, and connect(2).
fn send(fd: int, message: string) int

Transmits bytes over an open socket descriptor.

  • Parameters:
    • fd: Connected socket descriptor.
    • message: Payload string to transmit.
  • Return value:
    • Number of bytes sent (>= 0).
    • -1 on write error or closed connection (EPIPE, ECONNRESET).
  • Underlying Syscalls:
    • POSIX: send(fd, buf, len, 0) / write(2).
fn recv(fd: int, maxBytes: int) string

Receives incoming stream bytes from fd.

  • Parameters:
    • fd: Open socket descriptor.
    • maxBytes: Maximum buffer capacity to read in this call.
  • Return value:
    • A newly allocated string containing received bytes.
    • Returns "" on EOF (graceful remote shutdown) or socket read error.
  • Underlying Syscalls:
    • POSIX: recv(fd, buf, maxBytes, 0) / read(2).
fn close(fd: int) int

Closes the active socket descriptor and releases system resources.

  • Return value:
    • 0 on success.
    • -1 on error (EBADF).
  • Underlying Syscalls:
    • POSIX: close(2).
    • Windows: closesocket.

Namespace: str

The str namespace provides byte-oriented string queries, substring matching, and ASCII case-folding routines. All returned strings are newly allocated ARC-managed values (+1 reference).

Functions

fn length(s: string) int

Returns the active byte length of s.

  • Parameters:
    • s: Input string.
  • Return value:
    • Integer byte count (from the PithValue.length header field).
fn contains(s: string, sub: string) int

Tests whether sub is contained as a contiguous substring within s.

  • Parameters:
    • s: Haystack string.
    • sub: Needle substring.
  • Return value:
    • 1 if sub is found, 0 otherwise.
fn startsWith(s: string, prefix: string) int

Tests whether s begins with the prefix bytes prefix.

  • Parameters:
    • s: Target string.
    • prefix: Prefix substring.
  • Return value:
    • 1 if s begins with prefix, 0 otherwise.
fn endsWith(s: string, suffix: string) int

Tests whether s terminates with the suffix bytes suffix.

  • Parameters:
    • s: Target string.
    • suffix: Suffix substring.
  • Return value:
    • 1 if s ends with suffix, 0 otherwise.
fn upper(s: string) string

Allocates and returns a new string where ASCII lowercase letters a-z are mapped to uppercase A-Z. Non-ASCII bytes and other characters remain unchanged.

  • Parameters:
    • s: Input string.
  • Return value:
    • Newly allocated ARC string (+1 reference).
fn lower(s: string) string

Allocates and returns a new string where ASCII uppercase letters A-Z are mapped to lowercase a-z. Non-ASCII bytes and other characters remain unchanged.

  • Parameters:
    • s: Input string.
  • Return value:
    • Newly allocated ARC string (+1 reference).

Memory & ABI Specification

PithValue Representation

All dynamic data (strings, objects) share an identical binary layout defined in include/pith.h:

typedef struct PithValue PithValue;
struct PithValue {
    volatile uint32_t strongRefs;  /* Atomic reference counter */
    uint16_t          typeTag;     /* Type identifier          */
    uint16_t          flags;       /* Bit flags                */
    uint32_t          capacity;    /* Allocated byte capacity  */
    uint32_t          length;      /* Active byte length       */
    char              data[];      /* Contiguous byte payload  */
};

Type Tags

ConstantValueDescription
PITH_TAG_INT164-bit signed integer
PITH_TAG_FLOAT264-bit IEEE-754 floating-point
PITH_TAG_STRING3Refcounted, length-prefixed, NUL-terminated byte array
PITH_TAG_BOOL432-bit boolean value (0 or 1)
PITH_TAG_OBJECT5Refcounted record / dictionary pointer

Bit Flags

  • PITH_FLAG_STATIC (0x01): Marks immortal static data (e.g. string literals emitted directly into the .data ELF section). Calls to pithRetain and pithRelease are no-ops on static-flagged values.
  • PITH_FLAG_SHARED (0x02): Marks values managed across shared boundaries.

Calling Convention & Lowering

  1. Intermediate Representation:
    • Pith statements lower into QBE SSA intermediate representation.
    • Expressions evaluate in 64-bit registers: integer values use l (long, 64-bit), float values use d (double, 64-bit).
  2. Machine ABI:
    • System V AMD64 ABI (Linux, FreeBSD, macOS).
    • Parameters passed in registers: %rdi, %rsi, %rdx, %rcx, %r8, %r9.
    • Floating-point parameters passed in %xmm0 through %xmm7.
    • Callee-preserved registers: %rbx, %rsp, %rbp, %r12, %r13, %r14, %r15.

Memory & Performance

One of Pith’s best features is that you never have to manage memory yourself.

In some languages (like C), you have to manually allocate and free memory, which is easy to mess up. In other languages (like Python or Java), a heavy “garbage collector” runs in the background, which can cause sudden stuttering and slowdowns.

Pith gives you the best of both worlds: automatic cleanup with zero stuttering and maximum speed.


How It Works (Without the Jargon)

When you create a variable or join two strings together, Pith saves it in memory. It keeps a small counter on that data called a reference count:

  1. When you create or share data, Pith notes that it’s in use.
  2. When your code finishes with that data (like exiting an if block, a loop, or a function), Pith frees that memory immediately and automatically.

There is no background collector sweeping through memory, no “stop-the-world” freeze, and no sluggish memory leaks. Everything happens right when your code finishes using it.


Why This Matters for You

1. Perfect for Games & Audio

In game development and audio processing, even a 10-millisecond pause can ruin the experience. Because Pith cleans up data continuously in real time, your animations and sounds stay buttery smooth.

2. Tiny Memory Footprint

Pith programs only use the exact amount of memory they need at any given moment. They don’t require hundreds of megabytes of RAM just to start up.

3. Native Machine Code

When you run pith build, Pith produces a real binary tailored to your processor. It doesn’t run inside an emulated virtual machine; it runs directly on the metal for top-tier speed.


Sized Types & Saving Space

If you are working with large sets of numbers (like image pixels, sound waves, or 3D coordinates), you can tell Pith the exact size of your variables:

# Use u8 (0 to 255) for RGB color channels to use only 1 byte per value:
mut red: u8 = 255
mut green: u8 = 120
mut blue: u8 = 0

By choosing the right size, you can make your programs use a fraction of the memory of other scripting languages while running even faster.


Summary

You don’t need to be a systems engineer to build high-performance software. With Pith:

  • You never write malloc or free
  • You never experience garbage collector pauses
  • Your programs start instantly and run at native speed

pith run

pith run <file.pi> [more.pi ...]

Compile and execute via the instant pipeline, no temp executable, no heavyweight compiler driver.

Pipeline

source.pi → lex → parse (bump arena) → QBE IR (WPSSAC)
          → qbe → assembly → as → .o
          → libtcc (TCC_OUTPUT_MEMORY, symbols registered, relocated)
          → main() called natively in host memory

On Darwin, an ad-hoc signed temp executable is written by the system clang at -O0 and executed immediately.

Multi-file (WPSSAC)

Pass multiple translation units to compile them into a single .ssa module:

pith run main.pi utils.pi helpers.pi

All units’ top-level statements share the one exported $main. The single-pass QBE lowering performs whole-program constant folding and register allocation. Unreferenced private functions are eliminated.

Native C imports

When a script contains import "path/to/file.c", the engine compiles each imported unit into its own libtcc state (in memory), registers the runtime symbols into it, relocates it, and links its exported functions into the main execution state. The import states stay alive until execution finishes.

See C Imports (FFI).

Diagnostics

Compile errors print rustc-style diagnostics and exit with status 1:

error: use of undeclared identifier `undefinedVar`
  --> script.pi:1:7
   |
 1 | print undefinedVar
   |       ^~~~~~~~~~~~
error: aborting due to 1 previous error

Runtime exit codes propagate: the script’s return value (or 0) becomes the pith run exit code.

pith repl

pith repl
# or simply:
pith

Interactive Read-Eval-Print Loop (REPL) for experimenting with Pith syntax, exploring APIs, and testing code.

Interactive Sessions

When launched with no arguments or with pith repl, the Pith shell displays the interactive prompt:

pith 0.1.0 interactive repl
type exit or press ctrl+d to quit

pith> x = 10
pith> x + 32
42

Bare expressions are automatically evaluated and formatted to stdout.

Multi-line Blocks

Multi-line blocks (if, while, fn) continue across lines using the ... continuation prompt until closed with matching end statements:

pith> fn greet(name)
...       return "hello, " + name
...   end
pith> greet("world")
hello, world

Session Persistence

Variable declarations (name = val, mut name = val) and function declarations persist across evaluations in the REPL session:

pith> mut count = 0
pith> while count < 3
...       print count
...       count = count + 1
...   end
0
1
2

Exiting

To exit the REPL, type exit and press Enter, or send EOF via Ctrl+D.

pith build

pith build <file.pi> [more.pi ...] [--embed-source] [-o output]

Build a standalone native binary.

Pipeline

source.pi → lex → parse → QBE IR (WPSSAC) → qbe → assembly
          → as → .o
          → link: embedded tcc linker (Linux/NT/FreeBSD)
                  or mold via compiler driver (Darwin)
                  or system linker (fallback)
          → standalone executable

On Linux, Windows NT, and FreeBSD, the link happens in-process via the embedded tcc’s built-in ELF linker, no external linker or subprocess. On Darwin, mold is driven through the compiler driver (clang -fuse-ld=mold). Fallbacks: a tcc binary, then the system linker.

Options

FlagDescription
-o <path>Output path (default: input basename sans .pi, or .o for --object)
--embed-sourceAttach the workspace as a tar overlay with a PITHDEBG footer
--pluginBuild an installable plugin (.ppkg) instead of an executable
--objectWrite the raw plugin-mode object file instead of bundling into .ppkg

--embed-source can also be set permanently via build.embedSource = true in pith.toml; --plugin via toolchain.pithPlugin = "yes".

Plugin builds (.ppkg)

With --plugin (or [toolchain].pithPlugin = "yes"), the build produces <name>.ppkg, a tar bundle containing:

EntryWhat it is
plugin.oThe compiled object with exported c_<author>_<module>_<fn> symbols
manifestThe plugin’s symbol table: author, module, and each fn (name, return class, parameter count)

The author comes from [project].author; the module from [project].name. Only fn declarations are exported (zero-parameter, returning 64-bit integers in v0.1); top-level statements are ignored.

pith build myos.pi --plugin -o myplugin
# built plugin myplugin.ppkg (2 exported fns)

The plugin is installable with pith pkg (from the .ppkg or from source) and callable from consuming projects as <author>.<module>.<fn>:

if alice.myos.identifyKernel == 42
    print "plugin works"
end

See pith pkg for installing and Namespaces for the resolution rules.

Raw object emission (–object)

With --object, pith build lowers and compiles the source in plugin mode (exporting functions as c_<author>_<module>_<fn>) but writes the raw relocatable object directly to the output path without packaging a .ppkg tar bundle or manifest.

pith build plugin.pi --object -o plugin.o
# built object plugin.o (1 exported fn)

This raw object can be linked directly by host C programs (for example, as a fallback link object when embedding Pith via pith_register_link_object).

Multi-file (WPSSAC)

Multiple translation units concatenate into one .ssa module, the same as pith run. All imported C units are compiled to object files and linked in alongside the QBE-generated object and the runtime archive.

Output

The resulting binary is a stripped, dynamically linked executable that needs only libc at runtime. It contains no VM, no interpreter, and no pith-specific runtime beyond libruntime.a (the ARC engine and platform primitives).

–embed-source

When enabled, the build appends:

  1. An uncompressed ustar tar archive containing the project’s pith.toml, input scripts, and all .pi files from the project root and src/
  2. A 16-byte footer: { uint64_t payload_size; char magic[8] } where magic is "PITHDEBG"

Recover with pith decompile <binary>.

Default builds are stripped

Without --embed-source, the binary contains no source metadata, the last 16 bytes are normal ELF data, not the PITHDEBG magic.

pith decompile

Two modes, selected by the file extension.

IR mode (.pi input)

pith decompile <file.pi> [more.pi ...]

Prints the generated QBE SSA IL to stdout. Useful for inspecting what the compiler produces:

pith decompile hello.pi
data $str.1 = { w 1, h 3, h 1, w 6, w 5, b "pith", b 0 }
data $str.2 = { w 1, h 3, h 1, w 8, w 7, b "hello, ", b 0 }
data $str.3 = { w 1, h 3, h 1, w 2, w 1, b "!", b 0 }

export function w $main() {
@main.start
    %.v1_name =l alloc8 8
    storel $str.1, %.v1_name
    %.t1 =l loadl %.v1_name
    %.t2 =w call $pith_str_equals(l %.t1, l $str.1)
    jnz %.t2, @L2, @L3
@L2
    %.t3 =l call $pith_str_concat(l $str.2, l %.t1)
    %.t4 =l call $pith_str_concat(l %.t3, l $str.3)
    call $pith_rt_print(l %.t4)
    call $pith_release(l %.t4)
    call $pith_release(l %.t3)
    jmp @L1
@L3
@L1
@main.exit
    ret 0
}

Binary mode (workspace restore)

pith decompile <binary>

Unpacks an embedded debug workspace from a binary built with --embed-source:

1. Open the executable in binary read mode.
2. Seek to SEEK_END - 16.
3. Read the 16-byte PithDebugFooter trailer.
4. Check the magic ("PITHDEBG").
5. Read payload_size, seek backward by 16 + payload_size.
6. Extract the archived files into ./restored_workspace/.
pith decompile ./myapp
# restored workspace successfully extracted to ./restored_workspace/

If the binary has no embedded payload (was built without --embed-source), prints:

error: binary contains no embedded debug workspace payload.

and exits with status 1.

Security

The tar extraction is hardened against:

  • Path traversal (../../ components), rejected before any filesystem access
  • Absolute paths (/etc/passwd), rejected
  • Empty path components (a//b), rejected
  • Corrupt headers (missing ustar magic), rejected
  • Implausibly huge declared sizes, rejected against the actual buffer
  • Truncated blocks, handled gracefully, nothing written

pith pkg

Local-first package management across three isolated scopes.

Commands

pith pkg install [--global|--global-root]
pith pkg sync    [--global|--global-root]
pith pkg add <name> <version>
pith pkg                          # status report

Scopes

ScopeFlagInstall rootBinaries dir
Local (default),<cwd>/.pith/pkgs/<name>@<ver>/(none)
User global--global~/.pith/pkgs/<name>@<ver>/~/.pith/bin/
Machine root--global-root/usr/local/pith/pkgs//usr/local/bin/

Local (default)

Hermetic to the current project. Installs into <cwd>/.pith/pkgs/ and updates <cwd>/pith.lock. Nothing touches the global system.

User global (--global)

No elevated privileges required. Installs into ~/.pith/pkgs/ and symlinks any package-provided executables into ~/.pith/bin/.

Machine root (--global-root)

Requires root. Checks geteuid(), if not 0, re-executes the command via sudo (or fails with a clear error if sudo is unavailable). Installs into /usr/local/pith/pkgs/ and copies tools into /usr/local/bin/.

On Windows NT: checks token elevation via OpenProcessToken and GetTokenInformation, relaunching via ShellExecuteExW with the runas verb to trigger the UAC prompt.

Sources

Packages resolve from (in order):

  1. PITH_REGISTRY, a local directory of tarballs (<name>-<ver>.tar or compiled plugin bundles <name>-<ver>.ppkg)
  2. Already-installed scopes (user, then machine root)
  3. A path directly in pith.toml (e.g., mylib = "./libs/mylib" or a plugin bundle myplugin = "./dist/myplugin.ppkg")

Remote fetching is not implemented in v0.1, the local-first design means the cache is consulted before anything external.

Installing pith plugins (.ppkg)

A .ppkg (a plugin built with pith build --plugin) installs like a tar bundle: it is unpacked into the scope directory, giving plugin.o (the compiled, author-namespaced object) and manifest (the symbol table).

pith pkg: installed myplugin@/path/myplugin.ppkg -> .pith/pkgs/myplugin@...
pith pkg: 1 package installed

Consuming projects list the plugin under [dependencies]; their builds read the manifest, register the plugin’s namespace (<author>.<module>.*), and link the plugin’s object. The plugin’s functions are then callable as <author>.<module>.<fn>:

if alice.myos.identifyKernel == 42
    print "plugin works"
end

See pith build for building plugins and Namespaces for the resolution rules.

pith pkg install

Installs all [dependencies] from pith.toml into the selected scope. If a package provides an executable matching its name, that tool is symlinked (or copied on NT) into the scope’s binaries dir.

pith pkg sync

Reads [dependencies], checks which are already installed in the target scope, verifies their integrity against pith.lock (FNV-1a hash), installs missing ones, and writes pith.lock.

pith pkg: os-utils@1.0.0 ok (verified)
pith pkg: synced mylib@0.1.0 -> /path/.pith/pkgs/mylib@0.1.0
pith pkg: sync complete (2 packages)

pith pkg add

pith pkg add <name> <version>

Appends the dependency to pith.toml under [dependencies] and invokes sync (local scope).

pith.lock

Generated by sync/install. Records resolved versions with FNV-1a integrity hashes over the package contents (file paths + file bytes):

# pith.lock, concrete resolved dependencies (generated by pith pkg)
os-utils@1.0.0 9c35886d5a608143

On subsequent syncs, installed packages are re-hashed and compared against the lock, mismatches are reported as unverified.

pith engine

Transparent toolchain version proxying, multiple pith compiler versions coexist, switching automatically per project.

Commands

pith engine                        # report the engine configuration
pith engine list                    # installed toolchains + active default
pith engine use <version>           # set the global default
pith engine install <version>        # install the running toolchain locally

Toolchain pinning

A project’s pith.toml may pin a compiler version:

[toolchain]
pithVersion = "0.1.0"

When the pinned version differs from the running binary’s version:

  1. The nearest pith.toml is found (cwd, then traversing upwards)
  2. The pinned version is looked up in ~/.pith/toolchains/<ver>/bin/pith
  3. If found: argc and argv are forwarded directly via execv, bypassing the rest of the host execution
  4. If not found: an info message is printed and the running binary continues

A forwarding guard (PITH_TOOLCHAIN_ACTIVE) prevents the forwarded binary from re-forwarding (infinite loops when both binaries read the same project config).

pith engine list

pith toolchains (/home/user/.pith/toolchains):
  0.1.0   <- active default
  0.2.0

The active default is read from ~/.pith/default_version.

pith engine use

pith engine use 0.2.0
# pith: default toolchain set to v0.2.0

Writes the version pointer to ~/.pith/default_version.

pith engine install

pith engine install 0.1.0
# pith: toolchain v0.1.0 installed to ~/.pith/toolchains/0.1.0/bin/pith

Copies the currently running binary into the local toolchain cache. In v0.1, only the running version can be installed (remote fetching is not implemented).

Engine report

pith engine
pith engine report
  host os           : linux
  execution backend : libtcc (in-memory)
  aot linker        : tcc (embedded, in-process)
  qbe               : /usr/bin/qbe
  as                : /usr/bin/as
  mold              : not found in PATH
  runtime library   : /path/to/runtime/libruntime.a

Custom Tasks

Projects define command recipes in pith.toml under [tasks]. Any unknown verb dispatches through the task table.

Defining tasks

[tasks.build]
run = "pith run main.pi"

[tasks.test]
all = "pith run tests/all.pi"
unit = "pith run tests/unit.pi"

[tasks.deploy]
prod = "pith build main.pi -o dist/app --embed-source"
dev = "pith build main.pi -o dist/app-dev"

Running tasks

pith build              # -> tasks.build ("pith run main.pi")
pith test all           # -> tasks.test.all ("pith run tests/all.pi")
pith deploy prod        # -> tasks.deploy.prod
pith test               # -> tasks.test (falls back, no bare key)

Resolution order

When pith <verb> [sub] [args...] is received:

  1. Check if <verb> is a built-in (run, build, decompile, pkg, engine, help, version)
  2. If not built-in, find the nearest pith.toml (cwd upwards)
  3. If <sub> is provided, look up tasks.<verb>.<sub> first
  4. Fall back to tasks.<verb>
  5. Append remaining arguments to the command string
  6. Execute via execvp (POSIX) or CreateProcess (Windows NT)

Built-in commands take precedence

pith run, pith build, etc. are always handled as built-ins, a task named run in pith.toml is unreachable. Choose task names that don’t collide:

[tasks.quick-run]     # works, "run" is built-in, "quick-run" isn't
script = "pith run main.pi"

Arguments

Extra CLI arguments are appended to the task command:

pith deploy prod --verbose
# executes: pith build main.pi -o dist/app --embed-source --verbose

Configuration

pith.toml

The project manifest, read from the current directory (or the nearest ancestor that has one).

[project]

KeyTypeDescription
namestringProject name
versionstringProject version

[build]

KeyTypeDefaultDescription
targetstring"native"Lowering target
linkerstring"auto"Linker selection: "auto" uses mold (Darwin) or tcc (elsewhere)
enginestring"auto"Execution backend: "auto" uses libtcc (Linux/NT/FreeBSD) or temp-exec (Darwin)
embedSourceboolfalseAttach the workspace as a tar overlay when building

When embedSource = true, every pith build in this project embeds the workspace, the same as passing --embed-source on the command line.

[toolchain]

KeyTypeDefaultDescription
pithVersionstring(none)Pin the compiler version; forwards via execv when it differs from the running binary
pithPluginyes/no (or true/false)noBuild this project as an installable plugin (.ppkg) rather than an executable

With pithPlugin = yes, pith build produces <name>.ppkg: a bundle containing the compiled plugin object (with exported c_<author>_<module>_<fn> symbols) and a manifest (author, module, fn list). Other projects install it with pith pkg and call its functions as <author>.<module>.<fn> (see pith build and pith pkg).

[project] author

KeyTypeDescription
authorstringThe project’s author scope: plugin symbols are exported under c_<author>_<module>_<fn> and consumed as <author>.<module>.<fn>
[project]
name = "myos"
author = "alice"

[toolchain]
pithPlugin = "yes"

A consumer of this plugin calls alice.myos.identifyKernel.

[dependencies]

Keys are package names; values are versions or local paths:

[dependencies]
os-utils = "1.0.0"          # resolved from the registry or cache
mylib = "./libs/mylib"       # resolved from a local directory path
tools = "./packages/tools"   # another local path

[tasks.<name>]

Custom command recipes, dispatchable via pith <name>:

[tasks.build]
run = "pith run main.pi"

[tasks.deploy]
prod = "pith build main.pi -o dist/app --embed-source"

Nested keys become subcommands: pith deploy prod looks up tasks.deploy.prod, falling back to tasks.deploy if absent.

pith.lock

Generated by pith pkg sync and pith pkg install. Records concrete resolved versions with FNV-1a integrity hashes:

# pith.lock, concrete resolved dependencies (generated by pith pkg)
os-utils@1.0.0 9c35886d5a608143

The hash covers the installed package’s contents (file paths + file bytes). On subsequent syncs, installed packages are re-hashed and verified against the lock.

Environment variables

All runtime behavior is overridable without editing config files:

VariableDefaultDescription
PITH_QBEqbePath to the QBE compiler binary
PITH_CCccCompiler driver / system linker
PITH_MOLDmoldmold linker override
PITH_TCCtcctcc binary override
PITH_TCCDIRauto-detectedDirectory containing libtcc1.a
PITH_RUNTIMEauto-detectedPath to runtime/libruntime.a
PITH_CACHE~/.cache/pithDependency cache directory
PITH_REGISTRY(none)Local package registry directory (for pith pkg sync tarball sources)
PITH_TOOLCHAIN_ACTIVE(unset)Internal guard, prevents toolchain forward loops

Auto-detection order

The engine discovers its resources in this order (first hit wins):

libtcc1.a (PITH_TCCDIR):

  1. $PITH_TCCDIR (if it contains libtcc1.a)
  2. <prefix>/lib/pith/tcc/ (installed layout, relative to the binary)
  3. <root>/vendor/tcc/ (build-tree layout)
  4. <root>/../vendor/tcc/ (relocated build tree)
  5. /usr/lib/tcc/ (system tcc install)

libruntime.a (PITH_RUNTIME):

  1. $PITH_RUNTIME (if the file exists)
  2. ./runtime/libruntime.a (cwd, the normal case)
  3. <prefix>/lib/pith/runtime/libruntime.a (installed layout)
  4. <root>/runtime/libruntime.a (build-tree layout)
  5. <root>/../runtime/libruntime.a (relocated build tree)

Native C Imports (FFI)

Pith scripts import C source directly. The exported (non-static) functions become callable through the unit’s namespace, with the compiler handling type conversions and ARC at every boundary.

Syntax

import "ffi/math.c"

sum = math.addInts(3, 4)
msg = math.greet("world")     # owned string return, ARC-released
math.logNote(42)              # void call as a statement

The namespace is the file’s basename sans .c, ffi/math.c becomes math.

How it works

JIT (in-memory)

Each imported unit compiles into its own libtcc state (per-state isolation prevents symbol collisions between imports). The pre-baked <pith.h> virtual header is staged into a temp include dir, and the runtime symbols (pithRetain, pithNewString, etc.) are registered into the import’s state before compilation. After relocation, the exported function addresses are linked into the main execution state under their real symbol names.

AOT (standalone binary)

Each unit compiles to an object file via libtcc TCC_OUTPUT_OBJ (or the platform C compiler when libtcc is absent). The object joins the QBE-generated .o and runtime/libruntime.a at link time.

Codegen

The compiler emits a private namespaced shim per foreign function with the typed System V AMD64 / AAPCS64 signature:

function s $c_math_halfFloat(s %a0) {
@c_math_halfFloat.start
    %r =s call $halfFloat(s %a0)
    ret %r
}

Call sites emit typed calls through the shim, with widening/narrowing conversions at the boundary.

Type mapping

C typeQBE typePith type
int, enum, bool, int32_twinteger (widened via extsw)
long, int64_t, size_t, T*linteger or string
floatsfloat (narrowed via truncd)
doubledfloat
void,(statement only)
PithValue*lstring (owned, +1 reference)

The prototype scanner (src/cffi.c) discovers non-static function definitions at compile time, parsing return types and parameter lists from the C source. Multi-line signatures, pointer params, and (void) parameter lists are handled.

ABI contract

Imported C code includes <pith.h> and follows these rules:

  • Parameters are borrowed, the callee must call pithRetain() before storing a PithValue* beyond the call, and owns that extra reference afterwards.
  • Returns must be +1, a function returning PithValue* must return a new reference; the Pith compiler injects the matching release at scope boundaries.
  • Handoffs are leak-free, the compiler’s ASan-verified test suite confirms zero leaks across the FFI boundary.

Writing an import module

#include <pith.h>
#include <stdio.h>

/* 32-bit ints: w */
int addInts(int a, int b)
{
    return a + b;
}

/* borrowed string param */
long stringLength(PithValue *s)
{
    return (long)pithStringLength(s);
}

/* owned string return: +1 reference */
PithValue *greet(PithValue *name)
{
    char buf[256];
    snprintf(buf, sizeof(buf), "hello, %s!", pithStringData(name));
    return pithNewString(buf);
}

/* void: call as a statement */
void logNote(int code)
{
    fprintf(stderr, "note: %d\n", code);
}

Runtime helpers available to imports

From pith.h:

FunctionDescription
pithRetain(PithValue*)Atomically bump the refcount
pithRelease(PithValue*)Drop a ref; frees at zero
pithNewString(const char*)Allocate a string (+1 reference)
pithNewStringN(const char*, uint32_t)Allocate with explicit length
pithStringData(PithValue*)Borrowed payload pointer
pithStringLength(PithValue*)Payload byte length
pithStringEquals(PithValue*, PithValue*)Content equality
pithStringConcat(PithValue*, PithValue*)New concatenated string (+1)

Embeddable C ABI

Host C/C++ applications embed pith via include/pith_embed.h, a pure C ABI with direct function pointer registration (no virtual stack marshaling). Host functions registered with a signature are callable from pith code as a namespace, the way a game engine exposes its API to an embedded scripting language.

API

#include "pith_embed.h"

/* create a fresh embedding context */
PithContext *ctx = pith_context_new();

/* register a callable host function (see below) */
pith_register_ns_fn(ctx, "host", "addTarget",
                   addTarget, 'w', "pw");

/* register a plain runtime symbol (no signature, not callable
   from pith source; useful for resolving names the engine emits) */
pith_register_fn(ctx, "logInfo", my_log_fn);

/* register an object for the temp-executable fallback */
pith_register_link_object(ctx, "host_api.o");

/* evaluate a pith source string */
int rc = pith_eval_string(ctx, "print \"hello from pith\"");

/* free the context */
pith_context_free(ctx);

pith_eval_string runs the full pipeline, lex, parse (bump arena), QBE lowering, qbe, execution, with the context’s registered functions available alongside the runtime ABI. Returns the program exit code, or 1 on a compile/engine failure.

Functions

FunctionDescription
pith_context_new()Create a fresh embedding context
pith_context_free(ctx)Free a context
pith_register_fn(ctx, name, fn_ptr)Register a raw runtime symbol under name
pith_register_ns_fn(ctx, ns, name, fn_ptr, ret_class, param_classes)Register a host function callable from pith as ns.name(...)
pith_register_link_object(ctx, obj_path)Supply the object the fallback links so host functions resolve there
pith_eval_string(ctx, source)Compile and execute a pith source string

Typed host functions

pith_register_ns_fn teaches the frontend about a host function: pith code calls it as ns.name(...) with the same typed, direct C ABI calls used for native C imports. Every registered function joins a namespace (the ns argument); the namespace is the call prefix, and zero-parameter functions read as properties.

The signature is written with one class letter per slot:

ClassC typePith type
'v'voidstatement calls only (return only)
'w'int, int32_t, enum, boolinteger (widened via extsw)
'l'long, int64_t, size_t, T*integer or string
's'floatfloat (narrowed via truncd)
'd'doublefloat
'p'PithValue*string (parameters are borrowed; a 'p' return transfers a new +1 reference the compiler releases at scope exit)

param_classes holds one letter per parameter, in order ("" or NULL for zero parameters, at most 8). Registration fails with a diagnostic on unknown letters, void parameters, duplicate names, or a full namespace (64 functions per namespace).

Calling conventions

Calls lower to direct typed calls through the System V AMD64 / AAPCS64 ABI, with widening and narrowing inserted at the boundary, exactly as for native C imports:

# host.addTarget is registered as ('w', "pw")
host.addTarget("demo", host.exe)      # statement call, int return

if host.add(2, 3) == 5
    print "int parameters cross as 32-bit words"
end

print host.tag("owned")               # 'p' return: an owned string
host.note("borrowed parameter")       # 'v': statement only

Execution backends

Evaluation runs in-memory through the embedded tcc when the host kernel allows it. On hardened kernels (SELinux mprotect denials) and on Darwin, pith falls back to a temporary executable: the script object is linked with runtime/libruntime.a and executed immediately. The fallback child process cannot bind in-memory addresses, so hosts register their compiled API object with pith_register_link_object; the engine links it into the child, and evaluation behaves identically on both backends.

The registered object must export the c_<ns>_<fn> symbols the generated calls reference. Hosts compile their API module with the same author-aware renames pith applies to imported C units:

cc -DaddTarget=c_host_addTarget -Dtag=c_host_tag -c host_api.c -o host_api.o

Registering a link object is optional when in-memory execution is available, and recommended for portability.

Complete example

#include <stdio.h>
#include "pith.h"
#include "pith_embed.h"

static int count;
static int cycles(void) { return count; }
static int addTarget(PithValue *name, int type)
{
    printf("[host] target %s (type %d)\n", pithStringData(name), type);
    count++;
    return 1;
}
static PithValue *tag(PithValue *s)
{
    char buf[512];
    snprintf(buf, sizeof(buf), "[%s]", pithStringData(s));
    return pithNewString(buf);   /* +1 reference */
}

int main(void)
{
    PithContext *ctx = pith_context_new();
    if (!ctx)
        return 1;

    pith_register_ns_fn(ctx, "host", "cycles", cycles, 'w', "");
    pith_register_ns_fn(ctx, "host", "addTarget", addTarget, 'w', "pw");
    pith_register_ns_fn(ctx, "host", "tag", tag, 'p', "p");
    pith_register_link_object(ctx, "host_api.o");

    int rc = pith_eval_string(ctx,
        "host.addTarget(\"demo\", 1)\n"
        "print host.tag(\"owned\")\n"
        "print host.cycles\n");

    printf("[host] eval exit code: %d\n", rc);
    pith_context_free(ctx);
    return rc;
}

Building the embedder

Link the embedder against the pith frontend objects, the runtime, the embedded qbe backend, and libtcc:

cc host_app.c -Iinclude -Ivendor/tcc \
   src/lexer.o src/parser.o src/gen_qbe.o \
   src/engine_proxy.o src/pith_embed.o src/tar.o src/config.o src/cffi.o \
   runtime/memory.o runtime/os_fs.o runtime/network.o \
   vendor/qbe/libqbe.a vendor/tcc/libtcc.a -ldl -o host_app

The PithContext is opaque; internally it holds the registered runtime symbols, the typed namespace units the code generator consumes, and the fallback link objects, and the engine registers everything into every execution alongside the runtime ABI.

Host namespaces are root-level: they resolve like a bare import "thorn_engine.c" and use the same c_<ns>_<fn> mangling. Because the frontend resolves registered namespaces before the builtin ones, a namespace named fs, os, proc, or net deliberately shadows the builtin; prefer a distinct name.

Registered functions see exactly the ABI described in C Imports (FFI): parameters are borrowed, PithValue* returns are +1, and the test suite verifies the boundary leak-free.

Special Thanks & Acknowledgements

Pith is built on the philosophy of simplicity, minimalism, and true zero-dependency software. We could not have built Pith without the brilliant work of the pioneers who showed that compilers and developer tools can be small, fast, and understandable.

Our heartfelt thanks go to:


Quentin Carbonneaux & The QBE Project

For creating QBE, the beautiful, minimalist compiler backend that powers Pith. QBE proved that a modern compiler optimizer and native code generator does not need millions of lines of code or gigabytes of memory to be blazingly fast and reliable.


Fabrice Bellard & The Tiny C Compiler (TCC)

For creating TCC and libtcc. Fabrice Bellard’s work made it possible for Pith to compile, assemble, and link native executables directly in memory in milliseconds with zero external toolchains.


The Minimalist & Systems Tooling Community

  • The creators and maintainers of mdBook for making clean, readable documentation tools.
  • The POSIX and open systems communities who champion simplicity and portability.

To You, The Developer

Thank you for trying Pith! Whether you are writing your very first program, automating a daily task, or building something fun, we are excited to see what you create.