Welcome to Pith
pith /pɪθ/ noun
- The essential substance or central core of a matter; the heart.
- 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 usingpith repl. - Standalone Binaries: Turn any script into a standalone native executable with
pith build.
Getting Started
- Installation: Download and install Pith in one command.
- Your First Script: Follow our step-by-step beginner tutorial.
- Language Reference: Learn how variables, loops, and functions work.
- Built-in Tools: Work with files (
fs), system info (os), and networking (net).
Installation
From GitHub releases (recommended)
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):
| Path | What it is |
|---|---|
<prefix>/bin/pith | The toolchain binary (libtcc embedded statically) |
<prefix>/lib/pith/tcc/libtcc1.a | The vendored tcc runtime |
<prefix>/lib/pith/runtime/libruntime.a | The 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:
| Tool | Purpose | Install |
|---|---|---|
qbe | Lowers QBE IL to machine assembly | QBE |
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
| File | Description |
|---|---|
pith | The toolchain binary, libtcc is embedded statically |
libtcc1.a | tcc runtime, needed by the JIT at relocate time |
libruntime.a | The ARC runtime, linked into pith build output |
include/pith.h | The FFI header for imported C modules |
include/api.h | Runtime ABI bindings |
include/compiler.h | Internal API (tokens, AST, engine) |
include/pith_embed.h | Embeddable 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:
- Variables & Mutability: Learn how variables and
mutwork in depth - Functions: Break your code into reusable actions
- Working with Files & Network: Read files, save data, and connect to servers
- Setting Up a Project: Organize larger projects with multiple files
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 do | Example | Description |
|---|---|---|
| Create a variable | name = "Alex" | Stores a value that won’t change |
| Create a changeable variable | mut score = 0 | Stores a value you can change later |
| Print something | print "Hello!" | Shows text or numbers in the terminal |
| If / else decision | if score > 10 ... end | Runs code only when something is true |
| Repeat code | while score < 10 ... end | Repeats code in a loop |
| Break out of a loop | break | Stops a loop immediately |
| Skip to next loop step | continue | Jumps to the next turn of the loop |
| Check two things | if logged_in and is_admin | True only if both conditions are true |
| Check either thing | if is_weekend or on_vacation | True if at least one condition is true |
| Invert a condition | if not finished | True if finished is false |
| Create a function | fn add(a, b) ... end | Reusable block of code |
| Return from function | return a + b | Sends 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:
\ncreates a new line\tcreates a tab space\"includes a quote inside the text
Detailed Chapters
- Variables & Mutability: Storing values with
=and changing them withmut. - Types of Data: Working with numbers, text, and true/false values.
- Math & Logic: Adding numbers, comparing values, and using
and,or,not. - If Statements & Loops: Making decisions with
ifand repeating code withwhile. - Functions: Grouping code into reusable actions with
fn. - Error Guide: How to read and fix common mistakes.
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 mut | With 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:
- Types of Data: Numbers, text, and true/false conditions
- Math & Logic: Adding numbers, comparing values, and logic
- If Statements & Loops: Making choices and repeating code
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:
1for True (yes, active, enabled)0for 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
Popular Sizes at a Glance
| Size | Values Allowed | Ideal For |
|---|---|---|
u8 | 0 to 255 | Bytes, colors (Red/Green/Blue), small counters |
i8 | -128 to 127 | Small signed values |
u16 | 0 to 65,535 | Network ports, game inventory slots |
i32 | Over 2 billion | High scores, large item counts |
f64 | Decimals | Scientific 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:
- Math & Logic: Learn how to add, compare, and check conditions
- If Statements & Loops: Guide how your program makes choices
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).
| Symbol | What It Checks | Example | Meaning |
|---|---|---|---|
== | Exactly equal to | score == 100 | Is the score exactly 100? |
!= | Not equal to | items != 0 | Are items different from 0? |
< | Less than | age < 18 | Is age under 18? |
<= | Less than or equal | level <= 5 | Is level 5 or lower? |
> | Greater than | score > 50 | Is score higher than 50? |
>= | Greater than or equal | coins >= 10 | Do 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:
- If Statements & Loops: Put your comparisons to work in real programs
- Functions: Package calculations into reusable actions
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 aroundscore >= 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, andnot
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!
- Working with Built-in Tools: Learn how to read and write files (
fs), inspect the system (os), and connect to networks (net) - Setting Up a Project: Organize code into multiple files with
pith.toml
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 = 10and later tried to change it toscore = 20. - How to fix it: Add
mutwhen 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:
breakandcontinueonly make sense inside a loop. - How to fix it: Place
breakorcontinueinside awhile ... endloop.
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
: u8to 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 filesos: Check system details and platform kernelsproc: Control processes, read CLI arguments, environment variables, and process IDnet: Send and receive data over TCP networksstr: 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
| Function | What it does | Returns |
|---|---|---|
fs.readFile(path) | Reads an entire file | File contents as text (or "" on error) |
fs.writeFile(path, content) | Writes text into a file | 1 on success, 0 on error |
fs.exists(path) | Checks if a file exists | 1 if exists, 0 otherwise |
fs.remove(path) | Deletes a file | 1 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
| Member | What it does | Returns |
|---|---|---|
os.identifyKernel | Operating system kernel | "linux", "darwin", "nt", "freebsd" |
os.identifyKernelVersion | Kernel release version string | e.g. "6.5.0-generic" |
os.isLinux | Checks if running on Linux | 1 if true, 0 otherwise |
os.isMacOS | Checks if running on Apple macOS | 1 if true, 0 otherwise |
os.isNT | Checks if running on Windows | 1 if true, 0 otherwise |
os.isDarwin | Checks if kernel is Darwin | 1 if true, 0 otherwise |
os.isFreeBSD | Checks if running on FreeBSD | 1 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
| Member | What it does | Returns |
|---|---|---|
proc.argCount | Total CLI arguments passed | Integer count |
proc.getArg(index) | Gets argument at 0-based index | String argument |
proc.getEnv(name) | Gets an environment variable | String value (or "" if unset) |
proc.pid | Process ID of current process | Integer PID |
proc.sleep(ms) | Pauses execution for milliseconds | void |
proc.exit(code) | Terminates program immediately | Exits 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
| Function | What it does | Returns |
|---|---|---|
net.connect(host, port) | Connects to a TCP host and port | Socket ID (or -1 on error) |
net.send(fd, message) | Sends text over the socket | Bytes sent (or -1 on error) |
net.recv(fd, maxBytes) | Reads text from the socket | Received text (or "" on EOF/error) |
net.close(fd) | Closes the connection | Nothing |
net.socket(domain, type, proto) | Creates a raw socket | Socket 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
| Function | What it does | Returns |
|---|---|---|
str.length(s) | Payload byte length | Integer length |
str.contains(s, sub) | Substring search | 1 if found, 0 otherwise |
str.startsWith(s, prefix) | Prefix match | 1 if true, 0 otherwise |
str.endsWith(s, suffix) | Suffix match | 1 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:
root.fs.*/root.os.*/root.proc.*/root.net.*/root.str.*: The built-in runtime functions directly. They can never be overridden.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.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.
- POSIX:
- 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.
- Allocates a contiguous buffer sized to file length plus one byte. The returned
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:
1if all bytes were successfully written to disk.0on system error or partial write.
- Underlying Syscalls:
- POSIX:
open(path, O_WRONLY | O_CREAT | O_TRUNC, 0644), followed by complete loop write andclose(2). - Windows:
CreateFileA(..., GENERIC_WRITE, ..., CREATE_ALWAYS, ...).
- POSIX:
fn exists(path: string) int
Checks whether a filesystem entry exists at path.
- Parameters:
path: Target file or directory path.
- Return value:
1if the file or directory exists.0if it does not exist or access is denied.
- Underlying Syscalls:
- POSIX:
access(path, F_OK). - Windows:
GetFileAttributesA(path).
- POSIX:
fn remove(path: string) int
Deletes the file entry at path.
- Parameters:
path: Target file path.
- Return value:
1if the file was deleted successfully.0on system error (ENOENT,EACCES, etc.).
- Underlying Syscalls:
- POSIX:
unlink(path). - Windows:
DeleteFileA(path).
- POSIX:
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().
- POSIX:
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
nameas a string. - Empty string
""if the key is not found in the environment block.
- Value associated with
- Underlying Syscalls:
- POSIX:
getenv(3). - Windows:
GetEnvironmentVariableA.
- POSIX:
fn getArg(index: int) string
Returns the argument at 0-based index.
- Return value:
- String argument value.
- Returns an empty string
""ifindex < 0orindex >= 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).
- POSIX:
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.
- Invokes C
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 (2forAF_INET,10forAF_INET6).type: Socket semantics (1forSOCK_STREAM,2forSOCK_DGRAM).protocol: Protocol number (0for IP default).
- Return value:
- Socket integer descriptor (
>= 0) on success. -1on socket creation error.
- Socket integer descriptor (
- Underlying Syscalls:
- POSIX:
socket(2). - Windows:
WSASocket/socket.
- POSIX:
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 (1to65535).
- Return value:
- Connected socket descriptor (
>= 0). -1if resolution or connection establishment fails.
- Connected socket descriptor (
- Underlying Syscalls:
- POSIX:
gethostbyname(3)/getaddrinfo(3), socket creation, andconnect(2).
- POSIX:
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). -1on write error or closed connection (EPIPE,ECONNRESET).
- Number of bytes sent (
- Underlying Syscalls:
- POSIX:
send(fd, buf, len, 0)/write(2).
- POSIX:
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).
- POSIX:
fn close(fd: int) int
Closes the active socket descriptor and releases system resources.
- Return value:
0on success.-1on error (EBADF).
- Underlying Syscalls:
- POSIX:
close(2). - Windows:
closesocket.
- POSIX:
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.lengthheader field).
- Integer byte count (from the
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:
1ifsubis found,0otherwise.
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:
1ifsbegins withprefix,0otherwise.
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:
1ifsends withsuffix,0otherwise.
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
| Constant | Value | Description |
|---|---|---|
PITH_TAG_INT | 1 | 64-bit signed integer |
PITH_TAG_FLOAT | 2 | 64-bit IEEE-754 floating-point |
PITH_TAG_STRING | 3 | Refcounted, length-prefixed, NUL-terminated byte array |
PITH_TAG_BOOL | 4 | 32-bit boolean value (0 or 1) |
PITH_TAG_OBJECT | 5 | Refcounted record / dictionary pointer |
Bit Flags
PITH_FLAG_STATIC(0x01): Marks immortal static data (e.g. string literals emitted directly into the.dataELF section). Calls topithRetainandpithReleaseare no-ops on static-flagged values.PITH_FLAG_SHARED(0x02): Marks values managed across shared boundaries.
Calling Convention & Lowering
- 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 used(double, 64-bit).
- Machine ABI:
- System V AMD64 ABI (Linux, FreeBSD, macOS).
- Parameters passed in registers:
%rdi,%rsi,%rdx,%rcx,%r8,%r9. - Floating-point parameters passed in
%xmm0through%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:
- When you create or share data, Pith notes that it’s in use.
- When your code finishes with that data (like exiting an
ifblock, 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
mallocorfree - 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
| Flag | Description |
|---|---|
-o <path> | Output path (default: input basename sans .pi, or .o for --object) |
--embed-source | Attach the workspace as a tar overlay with a PITHDEBG footer |
--plugin | Build an installable plugin (.ppkg) instead of an executable |
--object | Write 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:
| Entry | What it is |
|---|---|
plugin.o | The compiled object with exported c_<author>_<module>_<fn> symbols |
manifest | The 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:
- An uncompressed ustar tar archive containing the project’s
pith.toml, input scripts, and all.pifiles from the project root andsrc/ - A 16-byte footer:
{ uint64_t payload_size; char magic[8] }wheremagicis"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
| Scope | Flag | Install root | Binaries 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):
PITH_REGISTRY, a local directory of tarballs (<name>-<ver>.taror compiled plugin bundles<name>-<ver>.ppkg)- Already-installed scopes (user, then machine root)
- A path directly in
pith.toml(e.g.,mylib = "./libs/mylib"or a plugin bundlemyplugin = "./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:
- The nearest
pith.tomlis found (cwd, then traversing upwards) - The pinned version is looked up in
~/.pith/toolchains/<ver>/bin/pith - If found:
argcandargvare forwarded directly viaexecv, bypassing the rest of the host execution - 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:
- Check if
<verb>is a built-in (run,build,decompile,pkg,engine,help,version) - If not built-in, find the nearest
pith.toml(cwd upwards) - If
<sub>is provided, look uptasks.<verb>.<sub>first - Fall back to
tasks.<verb> - Append remaining arguments to the command string
- Execute via
execvp(POSIX) orCreateProcess(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]
| Key | Type | Description |
|---|---|---|
name | string | Project name |
version | string | Project version |
[build]
| Key | Type | Default | Description |
|---|---|---|---|
target | string | "native" | Lowering target |
linker | string | "auto" | Linker selection: "auto" uses mold (Darwin) or tcc (elsewhere) |
engine | string | "auto" | Execution backend: "auto" uses libtcc (Linux/NT/FreeBSD) or temp-exec (Darwin) |
embedSource | bool | false | Attach 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]
| Key | Type | Default | Description |
|---|---|---|---|
pithVersion | string | (none) | Pin the compiler version; forwards via execv when it differs from the running binary |
pithPlugin | yes/no (or true/false) | no | Build 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
| Key | Type | Description |
|---|---|---|
author | string | The 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:
| Variable | Default | Description |
|---|---|---|
PITH_QBE | qbe | Path to the QBE compiler binary |
PITH_CC | cc | Compiler driver / system linker |
PITH_MOLD | mold | mold linker override |
PITH_TCC | tcc | tcc binary override |
PITH_TCCDIR | auto-detected | Directory containing libtcc1.a |
PITH_RUNTIME | auto-detected | Path to runtime/libruntime.a |
PITH_CACHE | ~/.cache/pith | Dependency 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):
$PITH_TCCDIR(if it containslibtcc1.a)<prefix>/lib/pith/tcc/(installed layout, relative to the binary)<root>/vendor/tcc/(build-tree layout)<root>/../vendor/tcc/(relocated build tree)/usr/lib/tcc/(system tcc install)
libruntime.a (PITH_RUNTIME):
$PITH_RUNTIME(if the file exists)./runtime/libruntime.a(cwd, the normal case)<prefix>/lib/pith/runtime/libruntime.a(installed layout)<root>/runtime/libruntime.a(build-tree layout)<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 type | QBE type | Pith type |
|---|---|---|
int, enum, bool, int32_t | w | integer (widened via extsw) |
long, int64_t, size_t, T* | l | integer or string |
float | s | float (narrowed via truncd) |
double | d | float |
void | , | (statement only) |
PithValue* | l | string (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 aPithValue*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:
| Function | Description |
|---|---|
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
| Function | Description |
|---|---|
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:
| Class | C type | Pith type |
|---|---|---|
'v' | void | statement calls only (return only) |
'w' | int, int32_t, enum, bool | integer (widened via extsw) |
'l' | long, int64_t, size_t, T* | integer or string |
's' | float | float (narrowed via truncd) |
'd' | double | float |
'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.