The Semaprax Handbook
Ernesto, the Semaprax mascot, guides you from one .spx file to a checked,
tested project. This handbook matches Semaprax 0.9.0.
Semaprax is beta software. Syntax, protocols, and binary interfaces can change. Use it to experiment and prototype, not for production or safety-critical work.
Start here
- Install Semaprax. It has a one-command installer and a Homebrew formula.
- Write and run your first program.
- Create a project with modules and tests.
Prefer to watch first? See the recorded walkthrough.
What Semaprax is
Semaprax is a systems language where people and AI agents work on the same
program. You write .spx source. The compiler checks it and exposes a
semantic graph of declarations, types, contracts, effects, and calls.
| Idea | What it means |
|---|---|
| Readable source in Git | .spx files are the source of truth. One formatter, one layout. |
| Stable identities | Each declaration has an @id("math.add") that survives renames. |
| Contracts and effects | requires, ensures, and uses are checked by the compiler. |
| Ownership | The compiler rejects use-after-move and data races. |
| One meaning, three engines | Checked code behaves the same in the interpreter, native C11, and WebAssembly. |
The whole loop in one example
module examples.meaning;
@id("math.add")
fn add(left: i64, right: i64) -> i64
requires left >= 0
requires right >= 0
ensures result == left + right
{
left + right
}
@id("app.main")
fn main() -> i64
ensures result == 42
{
add(19, 23)
}
semaprax fmt meaning.spx # canonical layout
semaprax check meaning.spx # types, contracts, effects, ownership
semaprax run meaning.spx # prints 42
What do you want to do?
| Goal | Read |
|---|---|
| Install | Install |
| Run one file | First program |
| Build a multi-file project | First project → Modules |
| Learn the language | Essentials → Types → Ownership |
| Use functions, loops, collections | Functions · Loops · Collections |
| Model data and behavior | Classes · Matching · Contracts and effects · I/O · Resources |
| Prove properties | Laws and proofs |
| Configure a project and pick a target | Manifests → Profiles → Targets |
| Call Semaprax from Rust or JavaScript | Integrations |
| Use VS Code | Editor setup |
| Let a coding agent edit your code | Agent workflow → Semantic explorer |
| Build an agent as a Semaprax program | Agent programs → Budgets and recovery |
| Measure context size | Token reports and caches |
| Test, debug, release | Testing · Debugging · Shipping |
| Run the agent harness, check trust limits, find a specialist command | Harness · What Semaprax verifies · Specialist commands |
| Find a command | Command reference |
| Look something up | Cheatsheet · Standard library · Built-ins · Cookbook · Glossary |
For implementation details, use the source map and
the specifications in docs/.
Get help from the compiler
semaprax help # commands you need first
semaprax help all # every command
semaprax help language topics # language topics, one at a time
semaprax help diagnostic SPX-T208 # the fix for one error code
Install
Install a published Semaprax release, then run a first project. You do not need to clone the repository, install Rust, or compile anything for this route, and you do not need a model account, API key, or editor extension.
Pick your download
The current published release is listed at github.com/wavect/semaprax/releases/latest. That address always moves to the newest release, so use it only to find out which version is current. The commands below name one exact tag, v0.8.0, so every download in a session comes from the same release.
| Your computer | Archive to download | Runtime requirement |
|---|---|---|
| macOS on Apple Silicon (M1 or newer) | semaprax-v0.8.0-aarch64-apple-darwin.tar.gz | macOS 11.0 or newer (the binary’s recorded minimum). |
| Linux on x86-64 | semaprax-v0.8.0-x86_64-unknown-linux-gnu.tar.gz | GNU/Linux with glibc 2.39 or newer. The v0.8.0 binary needs GLIBC_2.39, so it does not start on older distributions or on musl systems such as Alpine. |
| Windows on x86-64 | semaprax-v0.8.0-x86_64-pc-windows-msvc.zip | 64-bit Windows. |
Other hosts (Intel macOS, Linux on ARM, older Linux) have no v0.8.0 archive. Use Install from source there. Later releases are planned to target glibc 2.35, but v0.8.0 does not, so check the release notes of the version you pick.
GitHub also shows Source code (zip) and Source code (tar.gz) under every release. Those are automatically generated snapshots of the repository, not Semaprax programs you can run. Download the archive named in the table.
Each archive unpacks to a directory named after itself. It contains semaprax
(the full build of the command-line tool), semapraxd (the daemon), LICENSE,
README.md, a per-archive release-manifest.json, and a smoke/ program. On
Windows the two programs are semaprax.exe and semapraxd.exe.
Homebrew (Apple Silicon macOS)
Apple Silicon only for now; Homebrew installs the same prebuilt archive, it does not compile Semaprax.
brew install wavect/tap/semaprax
semaprax --version
Upgrade and remove with Homebrew:
brew update
brew upgrade wavect/tap/semaprax
brew uninstall semaprax
Homebrew owns semaprax and semapraxd in its bin directory
(/opt/homebrew/bin). If you also installed by hand or with the installer, the
first semaprax on your PATH wins; check which with command -v semaprax and
type -a semaprax. Homebrew never overwrites another manager’s files, so remove
or reorder the other entry rather than expecting it to be replaced.
To install without Homebrew, use the archive steps below.
macOS and Linux
The steps below use the Apple Silicon archive. For Linux, replace the target
with x86_64-unknown-linux-gnu and shasum -a 256 -c - with
sha256sum -c -.
-
Download the one archive for your computer and the checksum list. Do not download the other platforms’ archives.
TAG=v0.8.0 TARGET=aarch64-apple-darwin BASE=https://github.com/wavect/semaprax/releases/download/$TAG curl -fLO "$BASE/semaprax-$TAG-$TARGET.tar.gz" curl -fLO "$BASE/SHA256SUMS" -
Verify the archive you downloaded.
SHA256SUMSlists every platform, so select only your archive’s line; checking the whole file would report the archives you did not download as missing.grep " semaprax-$TAG-$TARGET.tar.gz$" SHA256SUMS | shasum -a 256 -c -It must print
semaprax-v0.8.0-aarch64-apple-darwin.tar.gz: OK. If it printsFAILEDor nothing at all, delete the download and start again; do not unpack it. -
Optional: verify who built it. A checksum only shows that the file matches the list published beside it. If you have the GitHub CLI, this also checks the publisher’s attestation:
curl -fLO "$BASE/release-attestation-$TARGET.json" gh attestation verify "semaprax-$TAG-$TARGET.tar.gz" \ --bundle "release-attestation-$TARGET.json" --repo wavect/semaprax -
Unpack it somewhere permanent. This example uses
~/.local/opt.mkdir -p "$HOME/.local/opt" tar -xzf "semaprax-$TAG-$TARGET.tar.gz" -C "$HOME/.local/opt" ls "$HOME/.local/opt/semaprax-$TAG-$TARGET"You should see
LICENSE,README.md,release-manifest.json,semaprax,semapraxd, andsmoke. If you downloaded the archive in a web browser instead of withcurl, macOS may refuse to open the unsigned program; the archives are not notarized, so verify the checksum first and then clear the download flag withxattr -dr com.apple.quarantine "$HOME/.local/opt/semaprax-$TAG-$TARGET". -
Put that directory on your
PATH. For the current terminal only:export PATH="$HOME/.local/opt/semaprax-$TAG-$TARGET:$PATH"To keep it for every new terminal, add the same line, with the tag and target written out, to your shell’s startup file, then open a new terminal:
Shell Add this line To this file zsh (the macOS default) export PATH="$HOME/.local/opt/semaprax-v0.8.0-aarch64-apple-darwin:$PATH"~/.zshrcbash the same exportline~/.bashrc(and~/.bash_profileon macOS)fish fish_add_path $HOME/.local/opt/semaprax-v0.8.0-aarch64-apple-darwinrun once; fish remembers it A new version unpacks to a new directory, so update this line when you upgrade.
Windows (PowerShell)
-
Download the archive and the checksum list, then verify only that archive.
SHA256SUMSlists every platform, so select your archive’s line.$Tag = "v0.8.0" $Target = "x86_64-pc-windows-msvc" $Name = "semaprax-$Tag-$Target.zip" $Base = "https://github.com/wavect/semaprax/releases/download/$Tag" Invoke-WebRequest "$Base/$Name" -OutFile $Name Invoke-WebRequest "$Base/SHA256SUMS" -OutFile SHA256SUMS $Line = Get-Content SHA256SUMS | Where-Object { $_ -match " $([regex]::Escape($Name))$" } $Expected = ($Line -split "\s+")[0] $Actual = (Get-FileHash $Name -Algorithm SHA256).Hash.ToLower() if (-not $Expected -or $Actual -ne $Expected) { throw "Checksum mismatch for $Name - do not unpack it" } "$Name : OK" -
Optional publisher verification, if the GitHub CLI is installed:
Invoke-WebRequest "$Base/release-attestation-$Target.json" -OutFile "release-attestation-$Target.json" gh attestation verify $Name --bundle "release-attestation-$Target.json" --repo wavect/semaprax -
Unpack it somewhere permanent and add it to
PATH:$Dest = "$env:LOCALAPPDATA\Programs\semaprax-manual" Expand-Archive $Name -DestinationPath $Dest $Bin = "$Dest\semaprax-$Tag-$Target" Get-ChildItem $Bin # semaprax.exe and semapraxd.exe are here # This terminal: $env:Path = "$Bin;$env:Path" # Every new terminal (your user account only): $UserPath = [Environment]::GetEnvironmentVariable("Path", "User") if (($UserPath -split ";") -notcontains $Bin) { [Environment]::SetEnvironmentVariable("Path", "$UserPath;$Bin".TrimStart(";"), "User") }Windows only reads the saved
Pathwhen a terminal starts, so open a new PowerShell window to see it there.
Run your first project
From any directory, with nothing checked out:
semaprax --version
semaprax new first-semaprax
semaprax check first-semaprax/semaprax.toml
semaprax test first-semaprax/semaprax.toml
semaprax run first-semaprax/semaprax.toml
With the v0.8.0 macOS archive these print the version and commit,
created calculator project first-semaprax, verified project first-semaprax (sha256:...), project tests passed, and finally 42. new needs a
destination that does not exist yet. Continue with
your first program or the
first project.
semapraxd is the same release’s daemon. You do not start it for the steps in
this handbook; keep it beside semaprax.
Fix “command not found”
- Open a new terminal, or run the current-terminal
exportor$env:Pathline from the step above. - Check which executable your shell finds:
command -v semapraxon macOS and Linux,Get-Command semapraxin PowerShell. If it is not the one you unpacked, an earlier directory onPATHis shadowing it. version 'GLIBC_2.39' not foundon Linux means the host’s C library is older than the v0.8.0 build requires; use the source route below.
Install from source
Choose this to follow the latest main, to contribute, or when no archive
fits your computer. It needs Git and Rust/Cargo 1.88 or newer. Clang is
needed only when you build native executables, and Node.js 22 or newer only for
the web-package tooling. The checker and interpreter path needs neither Clang
nor Node.js, and nothing here needs an API key.
git --version
rustc --version
cargo --version
Clone the repository and run the starter example without installing anything:
git clone --branch main https://github.com/wavect/semaprax.git
cd semaprax
cargo run --locked -p semaprax -- check examples/meaning.spx
cargo run --locked -p semaprax -- run examples/meaning.spx
check reports a verified file and run prints 42. The -- separates
Cargo’s options from Semaprax’s. Cargo downloads dependencies and compiles on
the first run.
To get the short semaprax command, install from the checkout. Each command
installs different programs:
| Command, run in the repository root | Installs |
|---|---|
cargo install --locked --path . | semaprax (the standalone build) and semapraxd. |
cargo install --locked --path . --bin semaprax | Only the standalone semaprax. |
cargo install --locked --path crates/semaprax-toolchain | Only semaprax-full, the full build, under that name. |
cargo install --locked --git https://github.com/wavect/semaprax --tag v0.8.0 semaprax | The v0.8.0 standalone semaprax and semapraxd, without a checkout. |
A release archive’s semaprax is the full build, the same code as
semaprax-full, renamed when packaged. A source-installed semaprax is the
standalone build, which has fewer commands; the
technical install reference lists the difference. Use
semaprax-full from source when a chapter needs a command the standalone build
lacks.
Cargo installs into ~/.cargo/bin. If semaprax is not found afterwards, add
it for the current terminal:
export PATH="$HOME/.cargo/bin:$PATH"
$env:Path = "$env:USERPROFILE\.cargo\bin;$env:Path"
To reproduce the exact source this edition of the handbook was reviewed against, run the following in a clean clone before installing, and create a branch before you develop:
git switch --detach 508b851a5fda25002ec27453bb559755a6a0d930
Latest main can contain features that no published release has yet. A command
absent from semaprax help all may need a newer build or the full toolchain.
Find help for your build
semaprax help run
semaprax help build
semaprax help diagnostic SPX-T208
Use the help of the executable you installed rather than guessing a flag.
Next: Write your first program, or configure VS Code.
See Semaprax in action
You will see a file checked and run, queried by meaning, and a project tested and built for the web. Run the commands from a repository checkout, or copy the examples from the examples folder. Install first.
The animation and still image replay recorded output from an earlier release (v0.6.0). The commands below are unchanged.
Check and run a file
semaprax check examples/meaning.spx
semaprax run examples/meaning.spx
check prints a source revision. run prints 42.
Ask what the compiler knows
semaprax query examples/meaning.spx
function math.add fn add(left: i64, right: i64) -> i64
function app.main fn main() -> i64
query lists declarations by stable ID. Other views:
| Command | Answers |
|---|---|
semaprax doc examples/meaning.spx | What are the contracts? |
semaprax context examples/meaning.spx math.add --depth 1 | What surrounds one declaration? |
Agent workflow explains when to use each.
Test a project and build for the web
semaprax test examples/calculator-project/semaprax.toml
semaprax build examples/calculator-project/semaprax.toml \
--target web -o target/handbook-demo-web
Results: project tests passed, then built project web package target/handbook-demo-web. The package holds app.wasm, JavaScript bindings,
TypeScript declarations, and an export descriptor. The output directory must
not exist yet; pick a new one when you rebuild.
With Node.js 22 or newer, call an export by stable ID:
node --input-type=module <<'JS'
import { readFile } from 'node:fs/promises';
import { instantiateBytes } from './target/handbook-demo-web/semaprax.bindings.js';
const runtime = await instantiateBytes(
await readFile('target/handbook-demo-web/app.wasm')
);
console.log(runtime.call('calculator.add', 19n, 23n));
JS
{ ok: true, value: 42n }
The n marks a JavaScript BigInt, used for i64. The result reports
failure as data instead of throwing.
Next
Write your first program, or scaffold a project.
First program
You will write one file, format it, check it, and run it. Install first, then work in an empty directory.
1. Save hello.spx
module app.hello;
@id("app.main")
fn main() -> i64
{
42
}
| Line | Meaning |
|---|---|
module app.hello; | Names the module. Every file starts with one. |
@id("app.main") | Gives the function a stable identity that tools can address. |
fn main() -> i64 | Declares a function with no arguments that returns a 64-bit integer. |
42 | The result. The last expression has no semicolon. |
The name main is for people. The ID app.main is for tools. For a
single-file run, Semaprax uses fn main.
2. Format, check, run
semaprax fmt hello.spx
semaprax check hello.spx
semaprax run hello.spx
42
| Command | Does |
|---|---|
fmt | Rewrites the file in the one canonical layout. |
check | Parses, type-checks, and verifies contracts, effects, and ownership. |
run | Runs main and prints its result. |
semaprax fmt hello.spx --check reports formatting drift and leaves the file
alone. The printed 42 is the return value, not the process exit status.
3. Change it
Replace 42 with 6 * 7 and run the three commands again. The result is the
same. A tail expression is the last expression in a block; its value is the
block’s result. There is no return here.
4. Write text
Printing is an effect, so the module must permit it and the function must
declare uses. Save this as hello-print.spx:
module app.print_greeting;
permit { process.stdout.write }
@id("app.main")
fn main() -> i64
uses { process.stdout.write }
{
let greeting = "Hello, Semaprax!\n";
let view = string_as_str(greeting);
let written = stdout_write(str_as_bytes(view));
if written == 17usize { 0 } else { 1 }
}
semaprax run hello-print.spx
Hello, Semaprax!
0
stdout_write prints the text and returns the byte count (17, including the
newline). The runner then prints the return value, 0. string_as_str and
str_as_bytes borrow the text; Ownership explains
them.
When a command fails
Read the first diagnostic and its help: line, fix that, and run check
again. Each SPX- code has a stable fix page:
semaprax help diagnostic SPX-T208
semaprax help language topics
See Debugging for more.
Next: Create a project.
First project
You will create a three-module calculator project, run its checks and tests,
build it for the web, and break one test on purpose. The project’s
semaprax.toml is its manifest: it lists the modules, tests, and exports.
1. Create it
semaprax new first-semaprax
cd first-semaprax
The destination must not exist. Add --name <project-name> to override the
name, or --template library or --template service for another starter.
first-semaprax/
├── semaprax.toml
├── README.md
├── AGENTS.md
└── src/
├── app.spx
├── core.spx
└── tests.spx
| File | Holds |
|---|---|
src/app.spx | The entry point, main. |
src/core.spx | The logic: add. |
src/tests.spx | The tests. |
AGENTS.md | Commands and language rules for coding agents. Keep it. |
2. Run it
Run these inside first-semaprax/:
semaprax fmt . --check
semaprax check .
semaprax test .
semaprax run .
verified project first-semaprax (sha256:...)
project tests passed
42
fmt . --check prints nothing when every file is canonical. Every command also
accepts semaprax.toml instead of ..
3. Read the files
schema = "semaprax.manifest.v1"
[package]
name = "first-semaprax"
version = "0.1.0"
[modules]
entry = "first_semaprax.app"
sources = ["src/app.spx", "src/core.spx", "src/tests.spx"]
tests = ["first_semaprax.tests"]
[exports]
web = ["first-semaprax.add"]
module first_semaprax.core;
@id("first-semaprax.add")
fn add(left: i64, right: i64) -> i64
{
left + right
}
src/app.spx imports add by stable ID and calls it:
module first_semaprax.app;
use function @id("first-semaprax.add") from first_semaprax.core as add;
@id("first-semaprax.app.main")
fn main() -> i64
{
add(19, 23)
}
module first_semaprax.tests;
@id("first-semaprax.tests.main")
fn main() -> i64
{
if 19 + 23 == 42 { 0 } else { 1 }
}
Three names do three jobs:
| Name | Example | Job |
|---|---|---|
| File path | src/core.spx | Where the source lives. |
| Module | first_semaprax.core | Which module declares the function. |
| Stable ID | first-semaprax.add | What other modules import. |
use function @id("...") from <module> as <name>; imports by stable ID. See
Modules and imports.
4. Inspect and build
semaprax query . --kind function
src/app.spx function first-semaprax.app.main fn main() -> i64
src/core.spx function first-semaprax.add fn add(left: i64, right: i64) -> i64
src/tests.spx function first-semaprax.tests.main fn main() -> i64
semaprax build . --target web -o dist/web
The [exports] table in the manifest picks which functions the web package
exposes (first-semaprax.add). Building does not start a server. The output
directory must not exist yet. See Targets.
5. Break a test
Open src/tests.spx and change 19 + 23 == 42 to 19 + 23 == 41. Run
semaprax test . and read the failure. Restore the line and run it again.
To add named cases, write fn test_<name>() -> i64 functions with an @id
that return 0 on success. See Testing.
Add a module
- Create
src/<name>.spxwithmodule first_semaprax.<name>;. - Add its path to
sourcesinsemaprax.toml. List a test module undertests. - Run
semaprax check .. Check the whole project, not a single file: a lone file that imports another module reportsSPX-G172orSPX-T105.
When the manifest is rejected
Semaprax accepts one canonical manifest layout. Keep the generated table order,
one-line arrays, and blank lines, and follow the first SPX-J100 hint. The
manifest guide lists every field.
semaprax project-scaffold --name <name> prints a starter as one JSON capsule
without writing files. It is for tools; use new to create a project.
Next: Language essentials, or write your own modules.
Set up VS Code
You will connect VS Code to your compiler, see diagnostics when you save a
.spx file, and find the commands for navigation and review. The extension is
wavect.semaprax. Its README
has the full command list.
1. Install a compiler
Install Semaprax first. The extension never downloads or runs a compiler by itself.
2. Select the compiler
- Open the command palette and run SEMAPRAX: Configure Compiler. Clicking the SEMAPRAX status bar item does the same.
- Choose Select installed compiler… and pick the
semapraxexecutable.
The list also shows compilers it found in per-user install folders, Homebrew’s
folders, and on your PATH. Listing one does not run it. Other choices:
Re-check current compiler and Open installation guide.
After you pick a file, the extension runs version --json and help all
once to confirm it is Semaprax, then saves the absolute path in your user
settings. A failed check or a cancel changes nothing.
The status bar item shows the state:
| Status | What to do |
|---|---|
| select compiler | Run Configure Compiler. Diagnostics stay off until you do. |
| compiler x.y.z | Ready. Hover for missing prerequisites. |
| compiler unavailable | The file moved or was removed. Select it again. |
| incompatible compiler | The file is not a compatible Semaprax. Select another. |
| untrusted workspace | Trust the workspace. Nothing runs until you do. |
To set the path by hand, use user settings (not workspace settings, which cannot choose the compiler):
{
"semaprax.compilerPath": "/absolute/path/to/semaprax"
}
3. Check a file
Save an .spx file and read the Problems panel. Or run SEMAPRAX: Check
Project. This needs only the compiler: no manifest, policy, or session.
If a feature is missing, hover the status item. Features follow your compiler’s
help all list, so an older compiler offers fewer commands.
4. Navigate by meaning
| Command | Use it to |
|---|---|
| Go to Declaration by Stable ID | Jump to a declaration even after a rename. |
| Show Callers of a Declaration | See who calls it. |
| Show Ownership, Contracts, and Effects | Inspect what the compiler knows. |
| Safe Rename by Stable ID | Rename across the project. |
| Open Semantic Explorer | Browse the project visually. See Explorer. |
| Show Token Report | Open a report snapshot you pick. See Token reports. |
| Inspect Agent Definition | Read an agent’s AgentGraph. |
Save files before you use results that depend on a revision. If the source changed, refresh the session instead of trusting an old location.
5. Review changes in a saved-source session
A session ties the editor to a manifest and a host policy (the file that says what your machine permits). Set both in user settings:
{
"semaprax.compilerPath": "/absolute/path/to/semaprax",
"semaprax.manifestPath": "/absolute/project/semaprax.toml",
"semaprax.hostPolicyPath": "/absolute/path/to/host-policy.json"
}
An empty {} is not a valid policy. The format is in the
technical guide.
Sessions need a compiler that advertises serve-workspace-mcp.
Then:
- Start Saved-Source Session, then Open Candidate. A candidate is a proposed revision, held in memory.
- Select Stable Target ID, then Show Target Change Catalog.
- Apply Active Typed Intent. A typed intent is a structured edit, not free text.
- Preview Candidate Source Diff, then Run Candidate Interpreter Tests.
For unfinished code, use the typed-hole commands (Open Typed Hole, New Hole Fill Scratch, Fill Selected Hole from Active Scratch). They show the hole’s context and checked fills for its type.
For a rejected change, Show Compiler-Admitted Repair Catalog lists exact fixes.
Hot reload (opt-in)
Start Hot Reload runs semaprax dev for the interpreter in a trusted
local workspace. It checks each change, activates valid code between
invocations, and keeps the last good version when a change is invalid. Native
and Wasm swapping are not supported.
What the extension does not do
Build, commit, approval, publication, package installation, and native execution stay outside the extension. Finish those with your normal workflow after you review the diff.
Next: Inspect a project visually, or guide a coding agent.
Essentials
After this page you can write a complete Semaprax program: functions, values,
if, while, and match. Save each example as a .spx file and run it with
semaprax run file.spx.
Write a program
module app.basics;
@id("basics.double")
fn double(value: i64) -> i64
{
value * 2
}
@id("app.main")
fn main() -> i64
{
double(21)
}
run prints 42, the value main returns. The rules:
- A file starts with one
module dotted.name;line. - Give every declaration an
@id("dotted.name"). It is the declaration’s permanent identity. Without it,checkwarnsSPX-S103, and renaming the function changes its identity. - The entry point is exactly
fn main() -> i64. - A function body is statements followed by one final expression. That
expression is the result. There is no
return. - To use a function from another file, a Project imports it by
@idwithuse function @id("…") from module as name;. See Modules and imports. - Run
semaprax fmt file.spxto apply the one canonical layout. It keeps//comments.
Pick a type
| Type | Example | Use it for |
|---|---|---|
i64 | 42, -1 | Integers. A plain integer literal is i64. |
i32 | 42i32 | 32-bit integers. |
u8 | 255u8 | One byte. |
usize | 3usize | Lengths and indexes. |
f64, f32 | 1.5, 1.5f32 | Floating point. |
bool | true, false | Conditions. |
char | 'a', '\n', '\u{2603}' | One Unicode scalar. |
string | "hello" | Owned UTF-8 text. == compares contents. |
These eight scalar types (everything except string) are Copy: using a value
does not consume it. Text and bytes follow ownership rules, see
Ownership.
Operators never mix types. If n is a usize, write n < 5usize, not
n < 5 (SPX-T208). Write 5i32 when an i32 is expected, because 5 is an
i64 (SPX-T232). Integer arithmetic is checked: overflow stops the program
with a status such as addition overflow, never wraps.
module app.scalars;
@id("app.main")
fn main() -> i64
{
let small = 1i32 + 2i32;
let ratio = 1.5 * 2.0;
let letter = 'a';
if small == 3i32 && ratio > 2.5 && letter == 'a' && !(1 > 2) { 3 } else { 0 }
}
Use &&, ||, and ! for booleans. && and || run their right side only
when needed, always left to right.
Bind a value
let count = 3; makes an immutable binding. let mut count = 3; lets you
assign again with count = count + 1;. Parameters are immutable. There is no
+=, and a second let with the same name is an error (SPX-T209).
To ignore a result, bind it to _: let _ = work(1);. A bare work(1); is a
syntax error.
Choose a value with if
module app.branch;
@id("app.main")
fn main() -> i64
{
let score = 42;
let accepted = if score >= 40 { score } else { 0 };
if accepted > 100 { 1 } else { if accepted > 10 { accepted } else { 2 } }
}
if is an expression and always has an else. Both branches have the same
type. For a third case, nest an if inside the else block. There is no
else if.
Repeat with while
module app.counting;
@id("app.main")
fn main() -> i64
{
let mut next = 1;
let mut total = 0;
while next <= 3 {
total = total + next;
next = next + 1;
next <= 3
}
total
}
The condition after while is checked before every pass. The body must end
with an expression, and while throws its value away. Ending a body with an
assignment is SPX-P203. There is no break or continue: put the exit test
in the condition. See Loops for for and the loop limits.
Pick a case with match
module app.classification;
@id("app.main")
fn main() -> i64
{
let value = -2;
match value { 0 => 0, -1 | -2 => -9, n if n < 0 => -1, _ => 1, }
}
The first matching arm wins. | joins alternatives, if adds a guard, and _
matches anything. A match on numbers or chars needs a last arm without a guard
(SPX-T257). Every arm ends with a comma, including the last. Matching
variants is in Matching.
Mistakes to skip
| You write | Error | Write this |
|---|---|---|
return x; | SPX-P106 | Put x last in the block. |
else if c { … } | SPX-P106 | else { if c { … } else { … } } |
i += 1; | SPX-P201 | i = i + 1; |
f(x); alone | SPX-P106 | let _ = f(x); |
for i in 0..n | SPX-P106 | while with a counter, or for item in vector |
break, continue | SPX-P106 | Test in the while condition. |
x as i64 | SPX-P106 | No casts. Keep one type and suffix literals. |
c ? a : b | SPX-P106 | if c { a } else { b } |
"a" + "b" | SPX-T250 | string_concat("a", "b") |
struct, enum, pub, const | SPX-P104 | record, variant; no visibility keyword. |
fn main() -> bool | SPX-T104 | main returns i64. Use 0 for success. |
tuples, (), fn f() | SPX-P106 | Declare a record; every function returns a value. |
When check fails, read the first error: its help: line is usually the fix.
For a code, run semaprax help diagnostic SPX-T208.
Next: Types. Exact rules: RFC 0001.
Types: records, variants, Option, Result
After this page you can model your data with records (fields), variants (one of
several cases), Option (maybe absent), and Result (value or error). Classes
have their own page.
| Your data | Use |
|---|---|
A point with x and y | record |
| A shape that is a dot or a box | variant |
| A value that may be missing | Option<T> |
| A call that can fail | Result<T, E> |
| A value with methods | class |
The examples on this page build records and variants. The default interpreter
behind semaprax run does not admit those (SPX-F102), so run them with
semaprax run file.spx --native, which compiles generated C11. check and
fmt need no flag.
Group fields with a record
module app.data;
@id("data.point")
record Point {
@id("data.point.x")
x: i64,
@id("data.point.y")
y: i64,
}
@id("app.main")
fn main() -> i64
{
let mut origin = Point { x: 1, y: 2 };
origin.x = origin.x + 1;
let moved = origin with { y: 10 };
moved.x + moved.y
}
- A record literal names every field, in any order. A missing field is
SPX-T213. origin.x = …changes a field and needslet mut.origin with { y: 10 }builds a new record and leavesoriginunchanged.- Give each field its own
@id. - Records have no methods:
point.get()isSPX-T203. Callget(point).
Pick one case with a variant
module app.shapes;
@id("data.shape")
variant Shape {
@id("data.shape.dot")
Dot,
@id("data.shape.box")
Box {
@id("data.shape.box.width")
width: i64,
@id("data.shape.box.height")
height: i64,
},
}
@id("data.area")
fn area(shape: Shape) -> i64
{
match shape { Shape::Dot {} => 0, Shape::Box { width: w, height: h } => w * h, }
}
@id("app.main")
fn main() -> i64
{
area(Shape::Box { width: 6, height: 7 })
}
A case without data is declared Dot, and written Shape::Dot {} everywhere
else. A match must cover every case, so adding a case makes the compiler
point at each match you must update. See Matching.
Handle missing values and errors
Option<T> has Some { value } and None {}. Result<T, E> has
Ok { value } and Err { error }. Both are ordinary generic variants, so one
spelling rule covers them:
| Where | Spelling | Example |
|---|---|---|
| Build a generic variant | with type arguments | Option<i64>::Some { value: 1 } |
| Match a generic variant | without them | Option::Some { value: v } => … |
| Call a generic function | with type arguments | identity<i64>(4) |
Some(1) and bare None do not exist (SPX-T203, SPX-T202). Leaving off
the type arguments when you build one is SPX-T221.
module app.checked;
@id("data.checked_div")
fn checked_div(left: i64, right: i64) -> Result<i64, i64>
{
if right == 0 { Result<i64, i64>::Err { error: 1 } } else { Result<i64, i64>::Ok { value: left / right } }
}
@id("data.half_of_quotient")
fn half_of_quotient(left: i64, right: i64) -> Result<i64, i64>
{
let quotient = checked_div(left, right)?;
checked_div(quotient, 2)
}
@id("app.main")
fn main() -> i64
{
match half_of_quotient(80, 2) { Result::Ok { value: v } => v, Result::Err { error: code } => code, }
}
expr? returns early with the error when expr is an Err. It works only in
a function that itself returns a Result (SPX-T218 elsewhere), so main
uses match.
An arm cannot build a record or variant (SPX-T258). Pull scalars out of the
match, then build the value with if.
Make a type generic
module app.pair;
@id("data.pair")
record Pair<T> {
@id("data.pair.first")
first: T,
@id("data.pair.second")
second: T,
}
@id("data.swap")
fn swap(pair: Pair<i64>) -> Pair<i64>
{
Pair<i64> { first: pair.second, second: pair.first }
}
@id("app.main")
fn main() -> i64
{
let swapped = swap(Pair<i64> { first: 1, second: 2 });
swapped.first + swapped.second
}
Write the type arguments when you construct a generic value. Generic functions are in Functions.
Choose the boundary before you move a helper
Standalone files accept any of these types in any function. A Project export
is stricter: the default profile allows only Copy scalars in public
signatures (SPX-G174). Records, variants, Option, and Result can still
appear inside a function. Pick a profile before you
move a helper to a project.
Return Option or Result instead of a sentinel value such as -1, so
callers must handle each case.
Exact rules: RFC 0002.
Ownership, strings, bytes
After this page you can pass text and bytes between functions without
ownership errors. The whole idea fits in one rule: passing an owned value
moves it, and a borrow only reads it. Using a moved value is a compile-time
error (SPX-O101), never a crash.
Why did SPX-O101 fail?
A string or Bytes value has one owner. Passing it to a function, or to
string_concat, moves it. The second use below fails:
let s = string_concat("ab", "cd");
size(s) + size(s) // error[SPX-O101]: use of resource `s` after ownership was moved
Fix it in one of three ways:
| Fix | When to use it |
|---|---|
Take a view: let v = string_as_str(s); and pass v to a borrow str parameter. | The callee only reads. This is the usual fix. |
| Build a second value for the second call. | Each call really needs its own copy. |
Copy bytes with bytes_copy(bytes_as_slice(data)). | You need a second owned Bytes. |
Copy scalars (i64, bool, char, and the rest) never move. They copy freely.
Borrow in helpers, own in sinks
module app.bytes;
permit { process.stdout.write }
@id("bytes.count_a")
fn count_a(text: borrow str) -> usize
{
let view = str_as_bytes(text);
let length = byte_len(view);
let mut index = 0usize;
let mut hits = 0usize;
while index < length {
hits = match byte_get(view, index) { Option::Some { value: byte } => if byte == 97u8 { hits + 1usize } else { hits }, Option::None {} => hits, };
index = index + 1usize;
index < length
}
hits
}
@id("app.main")
fn main() -> i64
uses { process.stdout.write }
{
let greeting = string_concat("banana", "!");
let borrowed = string_as_str(greeting);
let hits = count_a(borrowed);
let written = stdout_write(str_as_bytes(borrowed));
if hits == 3usize && written == 7usize && string_len(greeting) == 7 { 0 } else { 1 }
}
run prints banana! from stdout_write, then 0 from main.
borrow Treads a value without taking it. Make it the default for helpers.own Ttakes the value. Use it for functions that finish with the value: builders, transfers, destructors.ownis valid forBytes,Vec,Box, iterators, and resources. A plainstringparameter moves too. Writingown stringisSPX-O002.
Convert text to bytes in steps
Text goes down a one-way ladder. Each step is a named call:
string owned text: "hello", string_concat(a, b), string_from_i64(n)
| string_as_str(binding)
v
str (borrowed) read-only view: pass it to `borrow str` parameters
| str_as_bytes(view)
v
Slice<u8> borrowed bytes: byte_len, byte_get, byte_range, stdout_write
Every conversion takes a named binding, not a literal or a call result.
| You write | Error | Write this |
|---|---|---|
string_as_str("hi") | SPX-T266 | let s = "hi"; string_as_str(s) |
str_as_bytes(string_as_str(s)) | SPX-T266 | let v = string_as_str(s); str_as_bytes(v) |
str_as_bytes(text) with a string | SPX-T263 | Take the str view first. |
f("abc") for a borrow str parameter | SPX-T205 | let s = "abc"; f(string_as_str(s)) |
"a" + "b" | SPX-T250 | string_concat("a", "b") |
string_concat("n=", 5) | SPX-T205 | string_concat("n=", string_from_i64(5)) |
Going up: bytes_copy(view) makes an owned Bytes. array_as_slice(array) and
bytes_as_slice(bytes) give a Slice<u8>. The type table is in
Collections.
Build a byte buffer
Allocate with a literal capacity and chain bytes_set. Binding the result
freezes it:
module app.buffer;
@id("app.main")
fn main() -> i64
{
let buffer = bytes_set(bytes_set(bytes_zeroed(2usize), 0usize, 65u8), 1usize, 66u8);
let view = bytes_as_slice(buffer);
if byte_len(view) == 2usize { 0 } else { 1 }
}
To fill a buffer in a loop, allocate it outside, then assign the result back to
the same let mut binding. This is the only way to re-open a buffer:
module app.buffer_loop;
@id("app.main")
fn main() -> i64
{
let mut buffer = bytes_zeroed(3usize);
let mut index = 0usize;
let mut value = 65u8;
while index < 3usize {
buffer = bytes_set(buffer, index, value);
index = index + 1usize;
value = value + 1u8;
0
}
let view = bytes_as_slice(buffer);
if byte_len(view) == 3usize { 0 } else { 1 }
}
Limits: bytes_zeroed takes a usize literal capacity and cannot sit inside a loop
(SPX-T267). A literal index past the capacity is SPX-T272. A computed index
past the capacity fails at run time before anything is written. Do not hold a
view across the reassignment (SPX-T265). You cannot re-open a named buffer any
other way (SPX-T271).
Exact rules: RFC 0003, Owned String Borrowed View v1, Owned Bounded Byte Buffer v1.
Contracts and effects
After this page you can state what a function promises (requires,
ensures) and what it touches (permit, uses). Both sit in the signature,
the compiler checks both, and tools read them without reading the body.
Add a contract
module examples.meaning;
@id("math.add")
fn add(left: i64, right: i64) -> i64
requires left >= 0
requires right >= 0
ensures result == left + right
{
left + right
}
@id("app.main")
fn main() -> i64
ensures result == 42
{
add(19, 23)
}
requiresis what the caller must satisfy.ensuresis what the function guarantees.resultnames the return value.- Clauses go between the signature and the body. They are part of the function’s meaning, so changing one is a change to the interface.
- Contracts are checked at run time, on every backend. A violation stops the program and names the clause, the function, and the arguments:
SEMAPRAX contract failure
contract: requires left >= 0 in math.add
arguments: left = -1, right = 2
Start with bounds (value >= 0), exact results for small pure helpers
(result == left + right), and non-empty results for builders.
Contract or Result?
Use requires for something the caller must already have checked. Use a
Result for an outcome callers
should expect, such as bad user input. A failed contract is a bug report, not
control flow. To keep deliberate rejection checks out of the passing suite, see
Testing. For a rule with its own identity and
solver evidence, see Laws and proofs.
Declare effects
A function that performs an effect, or calls one that does, must say so. The module permits effects first, then each function lists the ones it uses:
module app.hello;
permit { process.stdout.write }
@id("app.main")
fn main() -> i64
uses { process.stdout.write }
{
let text = "hi";
let view = string_as_str(text);
let written = stdout_write(str_as_bytes(view));
if written == 2usize { 0 } else { 1 }
}
run prints hi from stdout_write, then 0 from main.
A missing permit is SPX-E101. A missing uses is SPX-E102. Each message
names both edits.
Effects pass up through callers. A function that calls an effectful function lists that effect too:
module app.ticking;
permit { audit.log }
@id("flow.tick")
fn tick(value: i64) -> i64
uses { audit.log }
{
value + 1
}
@id("app.main")
fn main() -> i64
uses { audit.log }
{
tick(41)
}
audit.log is a name you chose. Declaring an effect adds a visible
requirement and nothing more. The compiler-owned operations need these exact
names:
| Effect | Operations |
|---|---|
process.stdout.write | stdout_write |
process.stderr.write | stderr_write |
process.stdin.read | stdin_read |
process.args.read | args_len, arg_utf8 |
fs.read, fs.write | file_read, file_write_new, file_stat, file_list, … |
network.connect, network.read, network.write | net_connect, net_send, net_recv, … |
The operations are in Input and output. Installing the compiler grants no filesystem, process, network, or signing authority. A declared effect still needs a host that provides it. A test host can return fixed answers, and a configured runtime host does the real work. Check the profile before you move an effectful helper to a new target.
Single-file semaprax run evaluates declared effects only for
process.stdout.write. Other declared effects stop with SPX-F102; add
--native.
Mark an unsafe boundary
unsafe marks code that a reviewer must read. It adds no raw memory access. It
needs a module permit { unsafe } and a one-line audit note on each block
(SPX-N102, SPX-N103). The body is ordinary checked code:
module app.audited;
permit { unsafe }
@id("app.main")
fn main() -> i64
{
let mut x = 1;
@audit("bump the counter")
unsafe {
x = x + 3;
x
}
x
}
Each boundary appears as a node in the semantic graph. Status: partial, see Unsafe Boundaries v1.
Resumable effects (preview)
A function can declare yields Request -> Response and yield one request at
a time so a driver can answer it later. Only the interpreter engine and a Rust
driver run this. Ordinary native and Wasm builds refuse it (SPX-B116,
SPX-W126). Read
Resumable Effects v1
before you use it.
Ask for contracts and effects
semaprax context examples/meaning.spx math.add --depth 1 --filters contracts
semaprax doc examples/meaning.spx
context returns one declaration’s neighborhood as bounded JSON. doc renders
signatures, contracts, and effects as documentation. See
Driving Semaprax from an AI agent.
Exact rules: RFC 0001.
Functions, generics, function values
After this page you can write generic functions, pass a function to another function, and capture a value in a closure.
Write a generic function
module app.identity;
@id("generic.identity")
fn identity<T>(value: T) -> T
{
value
}
@id("app.main")
fn main() -> i64
{
identity<i64>(41) + 1
}
Type parameters go in angle brackets after the name. At a call, write the type
arguments: identity<i64>(41). The compiler can fill them in when the
arguments decide them (identity(41) also works), but this is a private
profile and not a promise for public signatures. If it cannot decide, you get
an error that asks for them (SPX-T225). Always write them for vec_*,
box_*, and iter_* calls (SPX-T281).
- A generic function cannot call itself, directly or through other generic
functions (
SPX-T226). - Generic records and variants spell their arguments when you build one:
Pair<i64> { first: 1, second: 2 }. See Types. - Generic functions stay inside a module. Public Project signatures take Copy scalars only unless the project profile says more (Profiles).
Pass a function
A parameter of type fn(A) -> B takes a function. Pass a named function, or an
unnamed fn literal, which is a function without its name and @id:
module app.callbacks;
@id("iterator.fold")
fn fold<T, A>(input: own Iter<T>, initial: A, combine: fn(A, T) -> A) -> A
{
let mut accumulator = initial;
for own item in input {
accumulator = combine(accumulator, item);
0
}
accumulator
}
@id("fn.twice")
fn twice(value: i64, step: fn(i64) -> i64) -> i64
{
step(step(value))
}
@id("fn.inc")
fn inc(value: i64) -> i64
{
value + 1
}
@id("app.main")
fn main() -> i64
{
let input = vec_push<i64>(vec_push<i64>(vec_with_capacity<i64>(2usize), 5), 5);
let total = fold<i64, i64>(vec_into_iter<i64>(input), 0, fn(acc: i64, value: i64) -> i64 { acc + value });
twice(total, inc)
}
A function value has up to eight parameters. Its target must be a local,
effect-free, non-generic function, or a literal. A literal takes no trailing
comma. Bind one to a name to reuse it:
let positive = fn(value: i64) -> bool { value > 0 };
Capture a value in a closure
A literal copies the Copy scalars it reads at the moment it is built. Later changes to the original do not reach the copy:
module app.snapshot;
@id("app.main")
fn main() -> i64
{
let mut limit = 10;
let within = fn(value: i64) -> bool { value < limit };
limit = 0;
if within(5) { 1 } else { 0 }
}
Closure bodies are ordinary scalar expressions and calls. A closure cannot
change what it captured, cannot capture an effect, and cannot sit inside a
generic collection position (SPX-T288). The Rust-style |x| x + 1 is
SPX-P201.
Move one owned value into a callback
once fn captures one owned Bytes value and carries it in the type
FnOnce() -> i64. Calling it consumes it, and so does passing it to an own
parameter:
module affine.example;
@id("affine.consume")
fn consume(payload: own Bytes) -> i64
{
42
}
@id("affine.make")
fn make() -> FnOnce() -> i64
{
let payload = bytes_zeroed(4usize);
once fn() -> i64 { consume(payload) }
}
@id("affine.run")
fn run(callback: own FnOnce() -> i64) -> i64
{
callback()
}
@id("app.main")
fn main() -> i64
{
let callback = make();
run(callback)
}
The body is exactly one call that passes the captured buffer to a function
fn target(payload: own Bytes) -> i64. Reusing the buffer or the callback after
the move is SPX-O101. There are no other captures, parameters, or generics
(SPX-T308).
The older own fn() -> i64 { target(payload) } form is in the same family. In
0.9.0, run executes it on all three backends, but check reports SPX-H006.
Use once fn. Closures that borrow a borrow str parameter are a narrower
profile, see
Synchronous Borrowed Text Closures v1.
Pick a name or a literal
Use a named function when the logic has a meaning, a contract, or a second
caller: it gets an @id, contracts, and tests. Keep a literal to one short
expression.
Exact rules: Function Values v1, v2, Scalar Snapshot Closures, Generic and Loop Closures, Retained Affine Callback v1, Owning-Capture Closures v1.
Loops and iterators
After this page you can repeat work three ways: while for a counter or other
scalar state, for to read a vector, and for own to drain an iterator.
| You want to | Use |
|---|---|
| Count or update scalar state | while |
| Visit each item of a vector and keep the vector | for item in values |
| Hand a vector to an iterator and consume it | for own item in iterator |
Count with while
module app.factorial;
@id("loops.factorial")
fn factorial(value: i64) -> i64
requires value >= 0
{
let mut remaining = value;
let mut total = 1;
while remaining > 1 {
total = total * remaining;
remaining = remaining - 1;
remaining > 1
}
total
}
@id("app.main")
fn main() -> i64
{
factorial(3)
}
The result is 3 * 2 * 1 = 6. The rules:
- The condition is checked before every pass and must be a
bool. - The body ends with an expression. Its value is discarded, and the condition
alone decides repetition. A body that ends after an assignment is
SPX-P203. - There is no
breakorcontinue. - A body may use Copy scalars, calls that return scalars, and the
byte_get/Option<u8>pattern. Building a record or variant, or calling a function that returns one, isSPX-T252. Loop over scalars, then build the value after the loop. net_recvreturns an owned value and is not allowed in a body (SPX-T270).bytes_zeroedstays outside. The one buffer write a body may do isbuffer = bytes_set(buffer, index, value), see Ownership.
Visit a vector with for
module app.traverse;
@id("app.main")
fn main() -> i64
{
let values = vec_push<i64>(vec_push<i64>(vec_with_capacity<i64>(2usize), 20), 22);
let mut total = 0;
for item in values {
total = total + item;
0
}
total
}
for item in values visits the elements of a Vec<T> binding in index order.
T is one of the eight Copy scalars. The length is read once, the vector is
frozen inside the body, and the body’s value is discarded. The vector must be an
immutable binding (SPX-T284 for let mut) and a plain name, not a call. Do
not move or change it, or the item, inside the body. Build the vector first with
the Vec calls.
Drain an iterator with for own
vec_into_iter<T> moves a vector into an Iter<T>. for own consumes it:
module app.consume;
@id("app.main")
fn main() -> i64
{
let input = vec_push<i64>(vec_push<i64>(vec_with_capacity<i64>(2usize), 20), 22);
let iterator = vec_into_iter<i64>(input);
let mut total = 0;
for own item in iterator {
total = total + item;
0
}
total
}
After the loop, input and iterator are both moved. Using either is
SPX-O101.
Step by hand
iter_next<T> consumes an iterator and returns an IterStep<T>: Done {} or
Yield { item, rest }. Take it apart with match own. item is a Copy value
and rest owns the remaining iterator. Pass rest to the next step, or let it
go out of scope:
module app.step;
@id("lazy.first_if_step")
fn first_if_step<T>(step: own IterStep<T>, keep: fn(T) -> bool) -> bool
{
match own step { IterStep::Done {} => false, IterStep::Yield { item, rest } => keep(item), }
}
@id("app.main")
fn main() -> i64
{
let input = vec_push<i64>(vec_with_capacity<i64>(1usize), 7);
let found = first_if_step<i64>(iter_next<i64>(vec_into_iter<i64>(input)), fn(value: i64) -> bool { value > 0 });
if found { 0 } else { 1 }
}
Map, filter, and fold
There is no built-in map. You write these helpers once, as generic functions
over for own, and reuse them. Each call spells its type arguments:
module app.pipeline;
@id("iterator.map")
fn map<T, U>(input: own Iter<T>, capacity: usize, transform: fn(T) -> U) -> Vec<U>
{
let mut output = vec_with_capacity<U>(capacity);
for own item in input {
output = vec_push<U>(output, transform(item));
0
}
output
}
@id("iterator.filter")
fn filter<T>(input: own Iter<T>, capacity: usize, keep: fn(T) -> bool) -> Vec<T>
{
let mut output = vec_with_capacity<T>(capacity);
for own item in input {
if keep(item) { output = vec_push<T>(output, item); 0 } else { 0 }
}
output
}
@id("iterator.fold")
fn fold<T, A>(input: own Iter<T>, initial: A, combine: fn(A, T) -> A) -> A
{
let mut accumulator = initial;
for own item in input {
accumulator = combine(accumulator, item);
0
}
accumulator
}
@id("app.main")
fn main() -> i64
{
let input = vec_push<i64>(vec_push<i64>(vec_push<i64>(vec_with_capacity<i64>(3usize), -1), 2), 3);
let mapped = map<i64, bool>(vec_into_iter<i64>(input), 3usize, fn(value: i64) -> bool { value > 0 });
let filtered = filter<bool>(vec_into_iter<bool>(mapped), 3usize, fn(value: bool) -> bool { value });
let count = fold<bool, usize>(vec_into_iter<bool>(filtered), 0usize, fn(count: usize, value: bool) -> usize { if value { count + 1usize } else { count } });
if count == 2usize { 1 } else { 0 }
}
Elements are the eight Copy scalars and Bytes. Owned items, public iterator
signatures, and adapters that build another vector are outside the profile.
The lazy adapters, which do their work as the iterator is consumed, are in
examples/lazy-iterator-adapters.spx. Run it from a repository checkout:
semaprax run examples/lazy-iterator-adapters.spx
Exact rules: While Loops v1, Vec For Traversal v1, Owning Iterators v1, Owning Iterator Loops v1, Generic Iterator Operations v1, Lazy Iterator Adapters v1.
Collections: Vec, Box, arrays, bytes
After this page you can pick the right container and use it: Vec for lists
of scalars, Box for one boxed scalar, [u8; N] for a small fixed byte
block, and Bytes for a byte buffer you build.
| You have | Use |
|---|---|
| A list of numbers, bools, or chars | Vec<T> |
| One scalar that must live behind an owner | Box<T> |
| A few known bytes | [u8; N] |
| Bytes you fill in | Bytes |
| Text | string, see Ownership |
Every call spells its element type, such as vec_push<i64>. T is one of the
eight Copy scalars: i64, i32, u8, usize, char, f32, f64, bool.
All capacities are bounded.
Build and read a vector
module app.stats;
@id("stats.sum")
fn sum(readings: borrow Vec<i64>) -> i64
{
let length = vec_len<i64>(readings);
let mut position = 0usize;
let mut total = 0;
while position < length {
total = total + vec_get<i64>(readings, position);
position = position + 1usize;
position < length
}
total
}
@id("app.main")
fn main() -> i64
{
let mut building = vec_with_capacity<i64>(3usize);
building = vec_push<i64>(building, 2);
building = vec_push<i64>(building, 3);
building = vec_push<i64>(building, 0);
let readings = building;
sum(readings)
}
Mutators take the vector and return the next one, so you always assign the
result back: building = vec_push<i64>(building, 2);. Build with let mut,
then move the finished vector into an immutable binding to traverse it.
| Call | Result |
|---|---|
vec_with_capacity<T>(n) | An empty vector with room for n items. |
vec_push<T>(v, x) | The vector with x added. |
vec_set<T>(v, i, x) | The vector with item i replaced. |
vec_clear<T>(v) | The emptied vector. |
vec_reserve_exact<T>(v, extra) | The vector with room for extra more items. |
vec_len<T>(v) | The length as a usize. Reads only. |
vec_get<T>(v, i) | A copy of item i. Reads only. |
vec_into_iter<T>(v) | An Iter<T> that consumes the vector, see Loops. |
Pushing past the capacity stops the program (semaprax.vec.v1 status 1, “vec_push
beyond capacity”), and so does an index past the length (status 2, “vector
index out of bounds”). Reserve enough room first. A for loop avoids the
index bookkeeping: see Loops.
In a Project, Vec needs the owned-data-api.v1 profile and
std.collections = "^0.1.0". See Profiles.
Keep a scalar in a Box
module app.boxed;
@id("app.main")
fn main() -> i64
{
let boxed = box_new<i64>(7);
let peek = box_get<i64>(boxed);
box_into_inner<i64>(boxed)
}
box_new<T> makes a uniquely owned Box<T>. box_get<T> copies the value out
through a borrow. box_into_inner<T> consumes the box. These three names
select the compiler’s box. A record you declare yourself as record Box<T> is
an ordinary inline record. The box holds one Copy scalar only, with no shared
ownership. See
Owned Bounded Box v1.
Use a fixed byte array
[97u8, 98u8] has type [u8; 2]. An array literal holds bytes only. A list of
i64 is a Vec<i64>, not [1, 2, 3] (SPX-T262). There is no a[0]
(SPX-P106). Borrow a view and read with byte_get, which returns an
Option<u8> so you handle the missing case:
module app.lookup;
@id("app.main")
fn main() -> i64
{
let sample = [97u8, 98u8];
let view = array_as_slice(sample);
match byte_get(view, 0usize) { Option::Some { value: b } => if b == 97u8 { 0 } else { 1 }, Option::None {} => 2, }
}
Bytes, slices, and strings
| Type | Owns its data? | Get one from | Turn it into |
|---|---|---|---|
string | Yes | A literal, string_concat, string_from_* | string_as_str(binding) gives str |
str | No | string_as_str, arg_utf8 | str_as_bytes(view) gives Slice<u8> |
Bytes | Yes | bytes_zeroed + bytes_set, bytes_copy, stdin_read | bytes_as_slice(binding) gives Slice<u8> |
Slice<u8> | No | str_as_bytes, array_as_slice, bytes_as_slice | byte_len, byte_get, byte_range, stdout_write |
A slice is a view. It borrows and never owns. Each conversion takes a named
let binding, not a literal or a call result.
Work with text
| Call | Does |
|---|---|
string_concat(a, b) | A new string. Consumes both. |
string_len(s), string_len_chars(s) | Length in bytes, in Unicode scalars. |
string_is_empty(s) | true when the length is 0. |
string_starts_with(s, p), string_contains(s, p) | Search. |
string_from_char(c), string_from_i64(n), string_from_usize(n) | Render a value as text. |
str_len_bytes(v), str_is_empty(v), str_starts_with(v, p), str_contains(v, p) | The same reads on a borrowed str. |
== compares string contents. These names are reserved: declaring your own
string_len is SPX-S113. The full signatures are in
Built-in functions.
Exact rules: Owned Bounded Vec v1, v2, String Operations v1, Portable Indexed Byte Data v1.
Classes and inheritance
After this page you can attach methods to data with a class, extend a class
with a subclass, and bind a protocol to a record. Classes are the only values
that answer value.method() calls.
Use a record and free functions by default. Use a class when the methods belong to the value, such as a counter or a builder.
The examples on this page build records and classes, so run them with
semaprax run file.spx --native. check needs no flag.
Add methods
module example.counter;
@id("example.counter")
class Counter {
@id("example.counter.value")
value: i64,
@id("example.counter.get")
fn get(self: Counter) -> i64
{
self.value
}
@id("example.counter.bumped")
fn bumped(self: Counter, amount: i64) -> Counter
{
Counter { value: self.value + amount }
}
}
@id("app.main")
fn main() -> i64
{
let base = Counter { value: 40 };
let next = base.bumped(2);
if next.value == 42 && base.get() == 40 { next.value } else { 0 }
}
- The first parameter is
self: Counter, written out. - A method usually returns a changed copy.
basestays at 40. - A class literal names every field. A field changes with
counter.value = …on alet mutbinding. - Records have no methods:
point.get()isSPX-T203. Calling a method on a number or string is the same error: usestring_len(s).
Extend a class
module example.inheritance;
@id("example.animal")
class Animal {
@id("example.animal.legs")
legs: i64,
@id("example.animal.speak")
fn speak(self: Animal) -> i64
{
self.legs
}
}
@id("example.dog")
class Dog : Animal {
@id("example.dog.bark_count")
bark_count: i64,
@id("example.dog.speak")
fn speak(self: Dog) -> i64
{
super.speak() + self.bark_count
}
}
@id("example.main")
fn main() -> i64
{
let d = Dog { legs: 4, bark_count: 2 };
let a: Animal = d;
if d.speak() == 6 && a.speak() == 4 { d.speak() } else { 0 }
}
class Dog : Animalinherits the fields and methods ofAnimal. ADogliteral names all fields, inherited ones too.super.speak()calls the parent’s method.- A
Dogis anAnimal:let a: Animal = d;converts it, and calls throughause the parent’s methods. - A method with the same name overrides the parent’s for the subclass.
Keep the tree shallow. Every extra level adds fields to every literal. To reuse behavior without substituting types, hold the other value in a field.
Bind a protocol to a record
A protocol lists the functions a type must have. An impl binds each one to
a function you already wrote, by @id. The compiler checks that every required
function is bound exactly once. The binding is checked and then erased: it
adds no dispatch, no protocol value, and no runtime cost.
module geometry.app;
@id("geometry.read-x")
protocol ReadX {
@id("geometry.read-x.get")
fn get(self: Self) -> i64;
}
@id("geometry.point")
record Point {
@id("geometry.point.x")
x: i64,
}
@id("geometry.point.read-x")
impl "geometry.read-x" for "geometry.point" {
"geometry.read-x.get" = "geometry.point.get";
}
@id("geometry.point.get")
fn get(point: Point) -> i64
{
point.x
}
@id("app.main")
fn main() -> i64
{
get(Point { x: 7 })
}
The receiver must be a local record with an @id. Bound functions are
top-level, non-generic, and not main. Projects import protocols with
use protocol @id("…") from module as name;.
Exact rules: Class Inheritance v1, Static Protocol Conformance v1.
Matching
After this page you can pick a result with match on numbers, bytes, chars,
and variants. Arms are tried in order and the first match wins.
Match numbers, bytes, and chars
module app.sign;
@id("refutable.sign_class")
fn sign_class(value: i64) -> i64
{
match value { 0 => 0, -1 | -2 => -9, n if n < 0 => -1, n => 1, }
}
@id("app.main")
fn main() -> i64
{
sign_class(-2)
}
| Pattern | Example | Meaning |
|---|---|---|
| Literal | 0 => … | Equal to the literal. |
| Alternatives | -1 | -2 => … | Any of them. |
| Binding | n => … | Anything, named n. |
| Guard | n if n < 0 => … | The binding, plus a condition. |
| Wildcard | _ => … | Anything, unnamed. |
- The last arm must be
_or a binding without a guard. Otherwise you getSPX-T257. - Order matters. Put specific arms first. In the example,
-2is caught by the alternatives before the guard sees it. - Chars and bytes work the same way, with their own literals:
module app.route;
@id("refutable.digit_name")
fn digit_name(digit: u8) -> i64
{
match digit { 0u8 => 10, 9u8 => 90, k if k > 4u8 => 2, _ => 1, }
}
@id("refutable.route")
fn route(code: char) -> i64
{
match code { 'a' => 1, 'b' | 'c' => 2, _ => 3, }
}
@id("app.main")
fn main() -> i64
{
digit_name(4u8) + route('c') + digit_name(0u8)
}
A range such as 0..=5 is not a pattern. Use a guard.
Match a variant
Name the case and bind each payload field as field: name. Building a generic
variant spells its type arguments. Matching one does not:
module app.pick;
@id("data.first_positive")
fn first_positive(left: i64, right: i64) -> Option<i64>
{
if left > 0 { Option<i64>::Some { value: left } } else { if right > 0 { Option<i64>::Some { value: right } } else { Option<i64>::None {} } }
}
@id("app.main")
fn main() -> i64
{
match first_positive(0, 4) { Option::Some { value: v } => v, Option::None {} => 0, }
}
- A case without data is matched as
Shape::Dot {}, never bareDot. - A
matchover a variant must cover every case. Add a case and the compiler lists eachmatchto update. Some(v)is not a pattern. WriteOption::Some { value: v }.- Arms produce numbers, bools, and calls, not new records or variants
(
SPX-T258). Match out the scalars first, then build the value withif. - A missing comma between arms is a syntax error. The last arm needs a comma.
if letdoes not exist. Usematch.
Matching a call result in main needs semaprax run file.spx --native. The
default interpreter reports SPX-F102.
Match an owned value
match own moves the value into the arms. Use it for IterStep from
iter_next:
match own step { IterStep::Done {} => false, IterStep::Yield { item, rest } => keep(item), }
Yield gives a Copy item and the owning rest. The full example is in
Loops.
Keep arms short
Match where a value arrives, then work with plain scalars. If an arm needs its
own match, move it into a named function with its own @id.
Exact rules: Refutable Match v1, RFC 0002.
Input and output
After this page you can print, read arguments and input, touch files, and open a network connection from Semaprax, and you will know what each one needs. Every operation needs three things: the module permits the effect, the function declares it, and the host running the program provides it.
Print a number
module app.print_count;
permit { process.stdout.write }
@id("app.main")
fn main() -> i64
uses { process.stdout.write }
{
let count = 42usize;
let text = string_from_usize(count);
let view = string_as_str(text);
let written = stdout_write(str_as_bytes(view));
if written == 2usize { 0 } else { 1 }
}
run prints 42 from stdout_write, then 0, the value main returns.
stdout_write returns the number of bytes written, so compare it and fail
loudly on a short write. Use string_from_i64 for signed values.
Read arguments and standard input
| Call | Returns | Effect |
|---|---|---|
args_len() | usize, the argument count | process.args.read |
arg_utf8(i) | borrow str, argument i | process.args.read |
stdin_read() | own Bytes, all of standard input | process.stdin.read |
stdout_write(v) | usize, bytes written | process.stdout.write |
stderr_write(v) | usize, bytes written | process.stderr.write |
module spxgrep_lines.app;
permit { process.args.read, process.stdin.read }
@id("spxgrep-lines.run")
fn run() -> bool
uses { process.args.read, process.stdin.read }
{
if args_len() == 1usize {
let needle = arg_utf8(0usize);
let data = stdin_read();
let input = bytes_as_slice(data);
byte_len(input) > 0usize
} else {
false
}
}
@id("main")
fn main() -> i64
{
0
}
stdout_append and stderr_append take the same borrow Slice<u8> and
return the bytes accepted. They add to the output instead of writing once, so a
line command can print many times. All appends share one 65,536-byte budget
and appear only when the command ends with a settled result. They belong to
the line-command-io.v1 profile.
Bind stdin_read() to a name before you borrow it: bytes_as_slice(stdin_read())
is SPX-T266. A single-file run supports only process.stdout.write. The
others need a Project with the useful-data-command.v1 profile built for the
native target. See Profiles.
Read and write files
| Call | Returns | Effect |
|---|---|---|
file_read(path, len, max) | own Bytes | fs.read |
file_write_new(path, len, data, data_len) | usize status | fs.write |
file_stat(path, len), file_create_dir, file_remove | usize status | fs.read or fs.write |
file_list(path, len, max) | own Bytes, sorted names | fs.read |
file_write_atomic(path, len, data, data_len) | usize status | fs.write |
module app.load;
permit { fs.read }
@id("load.size")
fn size() -> usize
uses { fs.read }
{
let path = [100u8, 97u8, 116u8, 97u8];
let bytes = file_read(array_as_slice(path), 4usize, 64usize);
let view = bytes_as_slice(bytes);
byte_len(view)
}
@id("app.main")
fn main() -> i64
{
0
}
Paths are bounded, relative byte strings (here data), resolved under a root
the host injects. There is no absolute path and no ambient filesystem.
file_write_new creates a new file and never overwrites. file_write_atomic
replaces one atomically. Only stat and list accept an empty path for the root.
The std.fs package builds typed Path and reader/writer values on top. See
Standard library.
Know whether a write landed
file_write_atomic aborts the command when something fails.
file_write_atomic_checked returns a classified code instead: 0 published,
1 not published (the target is untouched), 2 uncertain (the replace started
and the host cannot say whether it finished). Check for uncertain before you
retry. It needs fs.write and the private filesystem-io.v3 profile, so use it
through std.fs.write_atomic_checked, which returns a WriteOutcome:
module save.app;
use type @id("std.fs.write-outcome") from std.fs as WriteOutcome;
use type @id("std.io.writer") from std.io as Writer;
use type @id("std.path.value.path") from std.path.value as Path;
use function @id("std.fs.write-atomic-checked") from std.fs as fs_write_atomic_checked;
use function @id("std.io.writer.from-bytes") from std.io as writer_from_bytes;
use function @id("std.io.writer.write-u8") from std.io as writer_write_u8;
use function @id("std.path.value.from-bytes") from std.path.value as path_from_bytes;
permit { fs.write }
@id("save.app.run")
fn run() -> bool
uses { fs.write }
{
let name = [110u8, 111u8, 116u8, 101u8];
let path = path_from_bytes(bytes_copy(array_as_slice(name)));
let w0 = writer_from_bytes(bytes_zeroed(2usize));
let w1 = writer_write_u8(w0, 111u8);
let w2 = writer_write_u8(w1, 107u8);
match fs_write_atomic_checked(path, w2) { WriteOutcome::Published {} => true, WriteOutcome::NotPublished {} => false, WriteOutcome::Uncertain {} => false, }
}
@id("save.app.main")
fn main() -> i64
{
0
}
The project around it sets profile = "filesystem-io.v3",
[capabilities] required = ["fs.read", "fs.write"], and depends on std.fs,
std.io and std.path.value. Its test module needs its own main. This
project passes semaprax check and semaprax test. Spec:
Host operation outcome v1.
Connect over TCP
module net_http_get.app;
permit { network.connect, network.read, network.write }
@id("net-http-get.fetch")
fn fetch() -> bool
uses { network.connect, network.read, network.write }
{
let host = [101u8, 120u8, 97u8, 109u8, 112u8, 108u8, 101u8, 46u8, 111u8, 114u8, 103u8];
let handle = net_connect(array_as_slice(host), 80usize);
let sent = net_send(handle, array_as_slice(host));
let closed = net_close(handle);
sent == 11usize && closed == 0usize
}
@id("app.main")
fn main() -> i64
{
0
}
| Call | Effect |
|---|---|
net_connect(host, port) | network.connect |
net_send(handle, bytes) | network.write |
net_stream_stdout(handle, max) | network.write, process.stdout.write |
net_wait(handle, ms), net_recv(handle, …) | network.read |
net_close(handle) | none; settles the handle |
Calls run only through an injected provider, so tests can use a fixed one.
net_recv returns an owned value and is not allowed inside a while body
(SPX-T270): receive first, then inspect the bytes in the loop.
TLS and listeners
Five more calls open encrypted connections and accept inbound ones:
| Call | Effect | Returns |
|---|---|---|
net_tls_connect(host, port) | network.tls | handle to an authenticated TLS client connection |
net_listen(host, port) | network.listen | listener handle |
net_accept(listener) | network.accept | handle to an accepted connection |
net_tls_accept(listener) | network.accept, network.tls | handle to an accepted TLS connection |
net_close_listener(listener) | network.listen | 0 |
The call checks the server name and the host owns the certificates; there is no
cleartext fallback and no implicit bind address. Handles share the same 8-slot
space as net_connect. These calls run only on the interpreter through a hosted provider.
Native and Wasm builds reject them before emission, and network-run fixtures
(v2) replay them with tls: true and a listeners queue. Spec:
Bounded Network Services v1.
HTTPS in one call
https_get(url, max) and https_post(url, body, max) return the whole
response as owned bytes, shaped like an HTTP/1.1 message, so the std.http
parsers read it. Each needs network.http and the https-command-io.v1
profile. This function checks and builds:
module https_status.app;
permit { network.http }
@id("https-status.fetch")
fn fetch() -> usize
uses { network.http }
{
let url = [104u8, 116u8, 116u8, 112u8, 115u8, 58u8, 47u8, 47u8, 101u8, 120u8, 97u8, 109u8, 112u8, 108u8, 101u8, 46u8, 111u8, 114u8, 103u8];
let reply = https_get(array_as_slice(url), 4096usize);
byte_len(bytes_as_slice(reply))
}
@id("app.main")
fn main() -> i64
{
0
}
The URL is HTTPS only, at most 2,048 bytes, with no credentials or fragment.
max is 1 to 65,536 and counts the headers. https_post sends a body of up to
65,536 bytes as application/octet-stream, follows no redirects, and only
reaches origins the host allow-lists (at most eight). A failure aborts the
command and leaves no partial response, and a POST error does not prove the
server did nothing, so do not retry blindly. Test against recorded replies with
semaprax network-run. Start from examples/https-project.
Wire a standard-library package
The packages std.fs and std.http supply typed values and parsers over these
calls. Three steps: depend on the package, set its profile, import by stable id,
as in the std.fs example above. Use semaprax help library to list the
exact signatures; for example semaprax help library std.fs.read.
Narrow the effects
Declare only the effects a function needs. A tool that prints should not
permit the network, and reviewers and the semantic graph both see the
difference. Convert I/O bytes to views at the edge, scan them with
byte_get and byte_range, and build owned results at the end.
Exact rules: Bounded Language Command I/O v1, Filesystem I/O v1 and v2, Bounded Language Network I/O v1, HTTPS Client I/O v2.
Resources and cleanup
After this page you can declare a value with a defined end of life, a
resource, and describe a state machine with a session protocol. The
compiler proves that every owned resource is settled exactly once on every
exit path. There is no leak, no double free, and no finalizer that fails
halfway.
Declare a resource
module buffer.app;
@id("buffer.type")
resource Buffer {
@id("buffer.type.drop")
drop trivial;
}
@id("buffer.inspect")
fn inspect(buffer: borrow Buffer) -> i64
{
1
}
@id("buffer.consume")
fn consume(buffer: own Buffer) -> i64
{
inspect(buffer)
}
@id("buffer.pipeline")
fn pipeline(buffer: own Buffer) -> i64
ensures result == 2
{
inspect(buffer) + consume(buffer)
}
@id("app.main")
fn main() -> i64
{
0
}
pipeline borrows first (inspect), then transfers (consume). A borrow never
changes ownership. The final own transfer settles the value. Using buffer
after consume(buffer) is SPX-O101, the same error as for a moved string,
see Ownership.
A single-file semaprax run rejects modules that declare resources
(SPX-B104). Verify them with check, and run them through a Project’s native
or Wasm build or with run file.spx --native.
Choose how a resource ends
| Drop | Meaning |
|---|---|
drop trivial; | Nothing to finalize. The value ends at scope exit. |
drop import "host.symbol"; | A host function finalizes the value when it goes out of scope. |
An imported finalizer is declared in an interface that states its effect,
its failure mode, and the value it consumes:
module platform.app;
@id("platform.token")
resource Token {
@id("platform.token.drop")
drop import "platform.token.finalize";
}
@id("platform.token.host")
interface TokenHost
permits { platform.token.release }
{
@id("platform.token.finalize")
import fn finalize(token: own Token) -> unit
effects { platform.token.release }
failure infallible
consumes token always;
}
@id("app.main")
fn main() -> i64
{
0
}
A finalizer that runs automatically must be infallible and must consume the
token. A teardown that can fail is an explicit close function that returns a
status for the caller to handle. A destructor never reports an error.
Cleanup runs in a fixed order set by the cleanup plan, once for each owned resource that was not transferred, on every exit. Failure selection is sticky: cleanup cannot replace the status that was already selected.
semaprax context examples/ownership.spx buffer.pipeline --depth 1 --filters ownership
context --filters ownership lists each parameter as own or borrow and
where each loan starts and ends.
Describe a state machine
A session protocol names states and the moves between them. It is checked and
then erased: native and Wasm output do not change, and it grants no authority.
module app.checkout;
@id("checkout.session")
session protocol "checkout-v1" {
states { Idle, Open, Committed, Failed }
initial Idle;
terminal Committed cleanup {}
terminal Failed cleanup {}
on Idle begin: send BeginRequest via "checkout.begin" -> Open;
on Idle abort: fail Unit -> Failed;
on Open commit: send CommitRequest via "checkout.commit" -> choice { committed: Committed, refused: Failed };
on Open lost: fail Unit -> Failed;
}
@id("checkout.begin")
fn begin() -> i64
{
1
}
@id("checkout.commit")
fn commit() -> i64
{
2
}
@id("app.main")
fn main() -> i64
{
0
}
stateslists the states andinitialpicks the start.- Each
terminalstate names its cleanup, and a terminal has no moves out. on <state> <label>: <kind> <Payload> … -> <state>is one move. The kind issend,receive,call,return,cancel,timeout, orfail. Usechoice { label: state, … }for several outcomes.via "<function-id>"ties a move to a function in the same module by@id(SPX-K104).- Every non-terminal state needs a
cancel,timeout, orfailexit. Violations areSPX-K101toSPX-K106.
A function can opt in with follows session protocol to have its call order
checked (SPX-K107 to SPX-K109). Status: local evidence, see
Session Protocol Types v1.
Borrow, then own
Helpers borrow. Only sinks, builders, and close own. Structure a consuming
function as a borrow phase followed by one transfer.
Exact rules: RFC 0003, Shared Loan Plan v1.
Laws and proofs
After this page you can write a law, which is a rule with its own stable
@id, select it in a Project, and list it. A contract describes one function
call. A law names a rule so a project can keep it while the code changes, for
example during a refactor or an agent-proposed edit.
Write a law
A law lives in a law module, not in a standalone program. The committed
examples/native-law-project selects this one:
module native_law.laws;
@id("native-law.add.right-nonnegative")
law contract "native-law.add" requires (right: i64)
right >= 0
evidence theorem_proved;
@id("native-law.order.total")
law relational (left: i64, right: i64)
left <= right || right < left
evidence smt_proved;
| Part | Meaning |
|---|---|
law contract "id" requires (…) | A rule that selects one requires clause of the function with that @id. |
law contract "id" ensures (…) | The same for an ensures clause. It may bind result. |
law relational (…) | A rule between typed values, with no function. |
(right: i64) | The variables. Each has a scalar type. They are the whole scope. |
evidence … | The kind of evidence the rule needs. |
The rule is a pure scalar expression: literals, the variables, and operators.
Calls, fields, records, branches, and quantifiers are refused. The evidence
kinds are runtime_guarded, compiler_proved, model_checked, smt_proved,
and theorem_proved.
Declaring a law records the rule and the evidence it needs. It does not prove anything. Checking a project admits the law and leaves its row open until evidence arrives.
Select the law module
The Project manifest is semaprax.manifest.v2. List the file in both
sources and law_sources:
[modules]
entry = "native_law.app"
sources = ["src/LAWS.spx", "src/app.spx", "src/core.spx", "src/tests.spx"]
law_sources = ["src/LAWS.spx"]
tests = ["native_law.tests"]
LAWS.spx is only a naming habit. The explicit law_sources entry is what
selects it. See the example manifest.
List the laws
From a repository checkout:
semaprax check examples/native-law-project
semaprax query examples/native-law-project --kind law --json
semaprax project-assurance-manifest examples/native-law-project/semaprax.toml
The query returns both law IDs above. The assurance manifest lists the project’s obligations and the evidence for each. Keep it with the source revision it describes.
More law shapes live in examples/law-packs/ (collection, finite retry,
architecture, foreign boundary, money state).
Words you will see
| Term | Meaning |
|---|---|
| Proposition | The rule, such as left <= right || right < left. |
| Proof obligation | A rule that needs evidence under the selected policy. |
| SMT solver | A tool such as Z3 that checks formulas in supported theories. |
| Theorem prover | A tool such as Lean used by the theorem-checking route. |
| Assumption | A condition the proof relies on. It stays visible in the result. |
| Counterexample | An input that shows a claimed rule fails. |
| LawSet | The selected inventory of laws the project must account for. |
Check a law with an installed solver
project-proof-check runs one selected tool against one law or one function
postcondition. It takes an absolute manifest path, the tool, its executable
and exact version line, and a host profile. Start with its help:
semaprax help project-proof-check
Replace the angle-bracket values in this template. Do not paste it as is:
semaprax project-proof-check <absolute-manifest> --tool z3 --executable <absolute-z3-path> --version-line <exact-version-line> --host-profile trusted-local --law native-law.order.total
trusted-local means you trust local tool execution. A solver somewhere on
PATH does not replace naming the executable and its version. Add
--workflow summary or --workflow detail for the strict-law review route.
Witness values are hidden unless you pass --show-witness-values.
Keep the rule while you repair the code
When a proof fails, read the subject, the assumptions, and the reported reason. A run-time guard, a test result, and a solver proof answer different questions, so keep the evidence class with the verdict.
The protected-law workflow keeps an independent baseline. A repair changes the implementation and is then checked again. Removing a law, loosening a precondition, or changing the selected set is a specification change and needs its own approval. Cached proof work is reused only when it matches the current source and tool, so an old report never stands in for a new check.
Next: Source map. References: Native Law Declarations v1, Law Set v1, Protected Law Intent v1.
Modules and imports
Split a program into modules: reusable logic in one, main in another, tests
in a third. After this page you can build, test and run a three-module project
by hand.
Want a ready-made layout instead? Run semaprax new <dir>
(see Manifests). To follow along, create a
tutorial/ directory with a src/ subdirectory and save each block at its
stated path.
1. Describe the project
Save this as tutorial/semaprax.toml:
schema = "semaprax.manifest.v1"
[package]
name = "handbook-demo"
version = "0.1.0"
[modules]
entry = "tutorial.app"
sources = ["src/app.spx", "src/core.spx", "src/tests.spx"]
tests = ["tutorial.tests"]
[exports]
web = ["tutorial.add"]
entry is the module with main. sources lists the files. [exports] web
lists the functions a web build exposes, by stable ID.
2. Write the reusable function
Save this as tutorial/src/core.spx:
module tutorial.core;
@id("tutorial.add")
fn add(left: i64, right: i64) -> i64
requires left >= 0
requires right >= 0
ensures result == left + right
{
left + right
}
This module has no main: it only provides add. Check it through the
project, not alone (a single file without main fails with SPX-T105).
3. Import it into the entry module
Save this as tutorial/src/app.spx:
module tutorial.app;
use function @id("tutorial.add") from tutorial.core as add;
@id("tutorial.main")
fn main() -> i64
{
add(19, 23)
}
Read the import as a sentence: use the function with ID tutorial.add from
module tutorial.core, and call it add here. An import names a stable ID, not
a file path. Imports come right after the module line, before any permit
block.
4. Add tests
Save this as tutorial/src/tests.spx:
module tutorial.tests;
use function @id("tutorial.add") from tutorial.core as add;
@id("tutorial.tests.add")
fn test_add() -> i64
{
if add(19, 23) == 42 { 0 } else { 1 }
}
@id("tutorial.tests.zero")
fn test_zero() -> i64
{
if add(0, 0) == 0 { 0 } else { 1 }
}
@id("tutorial.tests.main")
fn main() -> i64
{
0
}
Each test_* function returns 0 to pass. The manifest’s tests entry tells
the runner which module to read. See Testing.
5. Run the project
From inside tutorial/:
semaprax fmt .
semaprax check .
semaprax test .
semaprax run .
semaprax query . --id tutorial.add
test prints project tests passed, run prints 42, and query prints
the tutorial.add declaration. A directory operand (.) selects its
semaprax.toml.
Keep names and paths separate
src/core.spx file containing source
tutorial.core module declared by that file
tutorial.add persistent identity of one function
add local name used at a call site
A display rename keeps tutorial.add. Moving a file means updating the
manifest path. Renaming a module means updating its imports. Three separate
edits, which is why the ID is the stable handle.
Fix common mistakes
| Symptom | Check first |
|---|---|
| The new file seems invisible | Its path is present in sources. |
| An import cannot be resolved | The provider’s module name and declaration ID both match. |
| Tests are not running | The module is in tests, and test functions use the test_ prefix. |
| A helper works alone but fails when linked | The project’s profile admits its signature. |
| A previous semantic preview is stale | Re-query the project after changing source or the manifest. |
SPX-G172 or SPX-T105 on one file | Check the project (semaprax check .), not the single file. |
Use a standard-library module
Declare the package, then import from it:
semaprax add . std.num "^0.1.0" # adds one [dependencies] row
semaprax help library std.num # signatures and stable IDs
add edits only the manifest. Bundled std.* packages are version 0.1.0
and need no download. See the standard library.
Next: Choose a profile for richer data, or learn how the test runner reports failures.
Project manifests
semaprax.toml says which files make up a project, where it starts, which
modules hold tests, and what it exports. After this page you can read, edit
and add to a manifest without breaking it.
Start from a template
semaprax new my-app # calculator (default)
semaprax new my-lib --template library
semaprax new my-svc --template service
new creates the directory, a semaprax.toml, sources, tests, a README.md
and an AGENTS.md. It never writes into an existing path. Names are lowercase
([a-z][a-z0-9-]*, SPX-J115 otherwise).
| Template | You get |
|---|---|
calculator | Entry, core and test modules; one web export. |
library | A reusable module plus examples and tests. |
service | A task-tracking scenario on ten bundled std.* packages (useful-data.v1). Every step is a deterministic fixture: no socket, file or clock. |
semaprax project-scaffold --name <n> [--template ...] [--layout frozen|tables]
prints the same project as one JSON capsule for tools instead of writing files.
Read a manifest
schema = "semaprax.manifest.v1"
[package]
name = "calculator"
version = "0.1.0"
[modules]
entry = "calculator.app"
sources = ["src/app.spx", "src/core.spx", "src/tests.spx"]
tests = ["calculator.tests"]
[exports]
web = ["calculator.add"]
[dependencies]
std.num = "^0.1.0"
| Table | What you put there |
|---|---|
schema | semaprax.manifest.v1 (tables) or semaprax.manifest.v2 (adds law_sources). |
[package] | name, version, optional profile (see profiles). |
[modules] | entry (module with main), sources (2 to 16 .spx paths, sorted), tests (one test module). |
[exports] web | Stable IDs a web or Rust build exposes. 1 to 32, sorted. |
[dependencies] | name = "range". Ranges: =1.2.3, ~1.2.3, ^1.2.3. |
[dependency-sources] | Local Subject-v3 files for non-std packages (up to four). |
[rust-dependencies] | Exact crates for a generated Rust SDK. |
[targets] matrix | Allowed targets: native64, wasm32. Absent means both. |
[command], [capabilities] | Only for command profiles. |
A filename is not a module name, and a module name is not a declaration ID. See Modules.
Edit a manifest safely
The loader accepts one canonical byte layout. Keep the table order, one blank line between tables, one-line arrays, and no comments.
| You see | Meaning and fix |
|---|---|
SPX-J100 | Not canonical, or a missing or mistyped key. help names the first differing line. |
SPX-J120 | Unknown table or key. |
SPX-J121 | Unknown bundled package, or a range the bundled 0.1.0 does not satisfy. |
SPX-J122 | You built a target outside [targets] matrix. |
SPX-J123 | A local dependency subject failed replay or resolution, or semaprax.lock is stale. |
SPX-J127 | add found the row already present, or the manifest is the frozen layout. |
New source file: add its path to sources. New test module: add it to
sources and tests. A file in src/ is not part of the project until listed.
semaprax fmt . --check # also reports manifest drift
semaprax add . std.num "^0.1.0" # appends one [dependencies] row
add edits only the manifest. It fetches nothing. Next steps are in
Shipping.
Three kinds of version
| Version | Example | Meaning |
|---|---|---|
| Compiler | 0.9.0 | semaprax version. |
| Manifest schema | semaprax.manifest.v1 | Grammar of this file. |
| Your package | version = "0.1.0" | Your project’s own version. |
A spec named PROJECT-MANIFEST-V18.md describes one project profile. It does
not mean you change your manifest schema.
Add laws
Native law files need schema = "semaprax.manifest.v2" and
[modules] law_sources. A law file also appears in sources. See
Laws and proofs and the
native-law example.
Recognize the older frozen layout
Committed examples may use schema = "semaprax.project.v1" (or v2 to v13)
with flat keys such as entry, sources, web_exports, tests. Each frozen
schema equals one profile of the table layout. Keep its key order. For new
projects use semaprax new. add and [dependencies] need the table layout
(SPX-J127 otherwise).
Next: Choose a profile, then build a target. References: Package Manifest v1, v2, Project Dependencies v1.
Choose a project profile
A profile fixes which values may cross a function boundary, who owns them, and
which targets can build the project. After this page you can pick the profile
for your interface and fix SPX-G174.
Pick by interface
A function’s boundary is its parameters and result. Locals inside a function
can use records and variants even when the boundary is a single i64.
| You are building | [package] profile | Example project |
|---|---|---|
| Calculator or numeric library | omit it (scalar) | examples/calculator-project |
| Function taking borrowed text | useful-text-consumer.v1 | examples/config-validator-project |
| Byte data, fixed arrays, borrowed slices | useful-data.v1 | examples/binary-frame-project, examples/task-service-project |
| Owned bytes in and out | owned-data-api.v1 | examples/frame-payload-project |
| Owned UTF-8 text | owned-utf8-api.v1 | see the spec below |
| One owned record result | flat-owned-record-api.v1 | see the spec below |
| Nested owned records, agents, routing | nested-owned-record-api.v1 | examples/support-routing-project, examples/job-service-project |
| Command: stdin bytes plus one UTF-8 argument | useful-data-command.v1 / .v2 | examples/spxgrep-project, examples/spxgrep-native-command-project |
| Command: argv and stdin | language-command-io.v1, line-command-io.v1 | examples/spxgrep-language-command-project, examples/spxgrep-lines-project |
| Command with HTTP or HTTPS | network-command-io.v1, https-command-io.v1 | examples/network-http-project, examples/https-project |
| Local futures | source-local-future.v1 (and -indexed-rust.v1) | examples/ri13-m3-local-http |
These profiles are private. They exist for the bundled std packages, have no
public ABI and no web exports, and may change. Do not rely on them:
| Profile | Gives a command | Package | Spec |
|---|---|---|---|
filesystem-io.v3 | fs.read and fs.write | std.fs (examples/everyday-agent-project) | Filesystem I/O v2 |
environment-io.v1 | a read-only snapshot of the environment the host passes in (process.environment.read); never the real process environment | std.env | Environment I/O v1 |
process-io.v1 | process.execute: run one registry tool by number, with argv and stdin, and get its output back; no shell, no PATH lookup | std.process | Process I/O v1, Project v18 |
useful-data.v2 | owned Reader and Writer values inside the project; exports stay on the useful-data.v1 boundary | std.data.json.write, std.email, std.format, std.export.policy | Project v16 |
https-command-io.v1 is not in that list. It adds network.http for
https_get and https_post, and examples/https-project builds on it
(Input and output).
Every row is one bounded contract. A profile name is not a switch you flip on
an existing project: change one type, then run check, test and your
consumer.
Why did SPX-G174 fire?
A function crosses the boundary with a type the profile does not admit. Read the named signature, then pick one:
- Keep it private: remove it from
[exports]. - Return a scalar: add a small wrapper.
- Move the project to a profile that admits the type.
SPX-G174 has more than one cause, so read the whole message.
Scalar boundary (the default)
Parameters and results are Copy scalars (i64, bool, u8, f64, …). The
scalar profile builds to web, native and oci (and rust with the full toolchain).
Owned data
An owned boundary says who keeps the bytes and who frees them. A borrowed
input stays owned by the caller. An owned input or output has a defined
transfer and cleanup. Pick it when JavaScript or Rust calls your code with
buffers. The npm target needs a profile that admits it (the scalar profile
fails with SPX-W120). Start from the
frame-payload project
and its web
or Rust
consumer.
Command I/O
A command profile has a [command] entry point and a fixed
[capabilities] required list, so the project declares exactly the authority it
uses. The input value is fixed per profile:
stdin-bytes+one-utf8-arg.v1 for useful-data-command.v2,
argv-utf8+stdin-bytes.v1 for the four -io.v1 profiles.
semaprax run <project>runs the ordinary project entry, not the command.semaprax network-run <project> --fixture f.json [--arg UTF8]... [--stdin path]runs anetwork-command-io.v1command against a recorded fixture (semaprax.network-fixture.v1, at most 1 MiB and 8 connections). No real socket opens.- Build the command with
build --target native; see Targets.
See Input and output for the source operations.
Check before you change
- Which input and output types cross the boundary?
- Who owns each non-Copy value before and after the call?
- Which target and host supply external operations?
- Which committed example covers that combination?
Build that example first, then change one thing at a time.
Next: Integrate with Rust, C or a browser. References: Package Manifest v1 (profile table), Public Owned Data API v1, Public Flat Owned Record API v1, Public Owned UTF-8 API v1, Bounded Language Network I/O v1.
Targets: interpreter, native, web
You can run a program in the interpreter, build it to a native executable, or build it to a WebAssembly package. After this page you can pick one and fix the usual failures.
One checked meaning feeds every target. A safe program behaves the same on each backend that admits the features it uses.
Run it
semaprax check examples/meaning.spx
semaprax run examples/meaning.spx # interpreter, prints 42
semaprax run examples/meaning.spx --native # generated C11, needs Clang
semaprax run . # a project: runs its entry
The interpreter is bounded: --max-steps N limits work, --max-bytes N limits
the output envelope, and call depth stops at 256 frames. --json gives a
machine-readable result. A single file needs a fn main() -> i64
(SPX-T105 otherwise). If the interpreter refuses a program (SPX-F102), try
--native.
Build it
semaprax build examples/meaning.spx --target native -o meaning-native
semaprax build . --target web -o dist/web
| Input | --target | You get |
|---|---|---|
| File | native (default) | Executable file. Needs Clang. |
| File | native-callable | Bundle for a function with a direct own resource parameter (SPX-B105 otherwise). Add --function <id>. |
| File | web, wasm | Package directory with app.wasm. --export <id> picks exported functions. |
| Project | web (default), wasm | Package directory: app.wasm, index.html, package.json, semaprax.js, bindings and a boundary description. wasm is an alias for web. |
| Project | native | Native executable of the project. Needs Clang. |
| Project | npm | Owned-data npm package. Needs a profile that admits it (SPX-W120 for the scalar profile). |
| Project | oci | Offline OCI Image Layout (oci-layout, index.json, blobs/). Scalar and Useful Data profiles only. Not signed, not pushed anywhere. |
| Project | rust | Generated Rust SDK. Only in the full toolchain built from source. Read semaprax help build first. |
Rules that save time:
-oand--outputare the same. The path must be new: an existing one fails withSPX-I307, a bad parent withSPX-I301.--jsonreportsstatus,target,productandoutput.[targets] matrixin the manifest can forbid a target (SPX-J122).web,wasmandnpmneedwasm32; the rest neednative64.- Run
semaprax help buildfor the exact list on your binary.
Strings in a standalone web module
A source file that passes string values between its own functions needs the
internal String profile. Name each exported function:
semaprax build app.spx --target web --profile internal-strings-v1 \
--export app.length -o app-web
It needs a source file, --target web or wasm, and 1 to 32 --export ids.
A project, another target or a missing export is a usage error (exit 2).
Strings never cross the exported boundary. Spec:
Standalone internal String Web package v1.
Check a web package
semaprax test examples/calculator-project
semaprax build examples/calculator-project --target web -o dist/calculator-web
node scripts/verify-wasm-scalar-exports.mjs dist/calculator-web # source checkout
semaprax.scalar-exports.json lists the exported functions. A build only
writes files. Serving them and loading the module is your app’s job; follow the
calculator browser consumer.
Edit and re-run (hot reload)
semaprax dev semaprax.toml --human
dev keeps one checked interpreter session open. It starts only after you send
a start frame on stdin, then reads one JSON control frame per line
(semaprax.hot-reload-control.v1). Operations are start, status, plan,
activate, invoke and stop. Saving a file never runs code; invoke does.
A broken revision is rejected and the previous one stays usable.
What must stay compatible before a swap
activate replaces the code only if everything reachable from the entry and
test roots still agrees with the running revision:
- entry points and the permit set;
- type and interface records;
- the set of reachable functions, with the same parameter and return types and ownership;
- declared effects;
- pre- and postconditions, and the cleanup and loan plans.
Otherwise the swap is refused (incompatible_closure, policy_changed,
identical_revision or unsupported_target) and the old revision keeps
running. Functions no entry or test reaches are not compared.
plan only reports what activate would do. Its output says
authority: none, and activate rebuilds the plan itself, so a plan you saved
cannot be replayed. If a worker panics or an acknowledgement is lost, the
session enters terminal_uncertainty: the status shows the flag (a JSONL
boolean, or a line in --human), every later operation is refused, and nothing
is retried or rolled back. Start a new session.
Frame limits: 64 frames per session, 4 KiB per input frame, 8 KiB per response.
printf '%s\n' \
'{"schema":"semaprax.hot-reload-control.v1","id":1,"op":"start"}' \
'{"schema":"semaprax.hot-reload-control.v1","id":2,"op":"invoke"}' \
'{"schema":"semaprax.hot-reload-control.v1","id":3,"op":"stop"}' |
semaprax dev semaprax.toml --human
Use --jsonl for tools. The VS Code extension drives this for you
(editor setup). Native and Wasm swapping are
not supported; --source-agent is refused by the public binary.
Specs: Hot Reload Watcher v1,
Hot Reload Session v1.
Check the environment
semaprax doctor # versions and OS
semaprax doctor --profile <id> # probe tools through an admitted offline profile
semaprax doctor --target native|web|all --json
doctor never discovers tools on PATH. Without --profile it reports
failed profile: an explicit offline profile is required and lists tools
(clang, node, rust) as not probed. That is expected. If a native build
fails with SPX-B101 failed to start clang, install Clang and put it on PATH.
Richer data, commands, resources
These use the execution route of their profile. A web scalar
package and an owned-data npm package are different interfaces, even though
both are WebAssembly.
Next: Integrate with another language, or prepare the package for review. References: Interpreter v1, Wasm Scalar Exports v1, OCI Deployable Artifact v1, Native Callable ABI v3.
Use Semaprax from another language
You can call Semaprax from a browser, Rust, C or C++, and you can let Semaprax call Rust. After this page you can choose a route and run its first command. Start with one function (a price calculation, a parser, a validator), not a whole application.
Choose a route
| Your goal | Route | Start here |
|---|---|---|
| Call scalar functions from a browser | build --target web | calculator-web |
| Call Semaprax from Rust | Generated safe-Rust SDK | calculator-rust |
| Exchange owned bytes with Rust or JavaScript | Owned-data SDK | owned-data-rust, frame-payload-web |
| Call a Rust host operation from Semaprax | Checked native Rust import | Native Rust Interop v1 |
| Embed checking in a Rust tool | Embedding API | embedding-api |
| Call from C or C++ | c-header, cxx-shim, cxx-package | below |
| Describe functions as HTTP | openapi | below |
Inspect a C boundary
Pick functions by name or stable ID. Each command is read-only and prints deterministic output.
semaprax c-header examples/meaning.spx --function math.add --emit-header
semaprax abi-report examples/meaning.spx --function math.add
semaprax cxx-shim examples/meaning.spx --function math.add --emit-fragment
semaprax cxx-package examples/meaning.spx --function math.add --max-bytes 1000000
semaprax freestanding-object examples/meaning.spx
| Command | Prints |
|---|---|
c-header | A C signature report; --emit-header prints the header text. |
abi-report | Argument, result, failure and ownership facts per function. |
cxx-shim | A C++17 header fragment of extern "C" declarations for scalar functions (--emit-fragment prints it). No wrappers. |
cxx-package | The header and shim as one package. SPX-X103 means raise --max-bytes. |
freestanding-object | One freestanding C11 translation unit for a whole effect-free scalar module, with profile assertions. |
A header describes an interface; the link step supplies the implementation. Read the ownership facts before you write a consumer.
Describe functions as OpenAPI
semaprax openapi examples/meaning.spx --function math.add
semaprax openapi-compat base.json candidate.json # breaking change check
openapi prints an OpenAPI document for the named functions, including the
shared failure-status schema. openapi-compat compares two documents and
reports whether the candidate breaks the base. Spec: OpenAPI v1.
Call Semaprax from Rust
The calculator example has two packages. The setup package reads Semaprax source and builds the SDK. The consumer package depends on the generated crate as an ordinary Cargo dependency and does not compile the compiler.
Follow the calculator README
for tool paths. It names Clang, the archiver (on macOS /usr/bin/libtool),
the manifest and output paths. Build the SDK first, then the matching
consumer. Keep generated output out of Git.
semaprax build <project> --target rust is a different route. It exists only
in the full toolchain built from source. Run semaprax help build and read the
matching example first.
In CI, prepare the SDK in its own step so failures separate cleanly: the
build-script consumer
uses a prepared SDK and never starts the compiler from build.rs.
Call Rust from Semaprax
A Rust import declares one operation Semaprax may call. The generated adapter connects it to your Rust code. Keep input types, result type, effects and failure behavior explicit.
- Indexed route. A prepared Rust API index lists items and signatures.
Binding checks package, version, source digest, target, features, path and
signature.
semaprax context <file.spx> <rust-path> --rust-index <index.json>answers questions about one imported item; add--candidateswith a path prefix to list matches. - Selected bindings. Owned Regex and Url bindings and generated callback adapters exist as starting points.
- Receiver-tied views. A returned view borrows from its owner. Keep the owner alive while you use the view.
Sources: checked Rust bindings, native Rust builder.
Test both sides
Call the generated interface with an ordinary value, a boundary value and an input that hits the documented error path. For owned data, also test an empty value and transfer and cleanup. Keep the consumer in the test: a source-only check cannot catch a consumer wired to the wrong package.
Next: Review and ship the selected package. References: C Header v1, C++ Shim v1, Public Owned Data API v1, Native Callable ABI v3.
Shipping: lock, resolve, review
You can pin a project’s interface, declare and resolve dependencies, preview a change before it touches source, and verify what you ship. After this page you know which command to run at each step. Every command here reads files you name and makes no network call.
Lock the interface
semaprax lock . --write # create semaprax.lock
semaprax lock . --verify # re-check it
semaprax lock . --compare base.lock # breaking or not? (CI gate)
semaprax lock . --emit-interface > iface.json
semaprax lock . --compare-interface iface.json
The lock records identity, source digests, interface digest, targets and
capabilities. --compare prints a verdict and exits 1 when the change is
breaking:
{"changes":[{"classification":"breaking","detail":"a retained export changed its types, ownership, or contracts","kind":"interface-digest"}],"schema":"semaprax.project-lock-compatibility.v1","verdict":"breaking"}
A stale lock fails --verify with SPX-J123; run --write again or restore
the sources. --compare-interface takes an interface file made by
--emit-interface (SPX-J124 for anything else).
Resolve dependencies
semaprax add . std.num "^0.1.0" # declare
mkdir cache
semaprax fetch cache vendor/pkg.subject.json # verify and file by digest
semaprax resolve . --target wasm32 --cache cache --write
semaprax resolve . --target wasm32 --cache cache --verify
| Step | What it does |
|---|---|
add | Adds one [dependencies] row. Touches nothing else. |
fetch | Replays each Subject-v3 file and stores it as <digest>.json. --lock <lock.json> also checks the files against that lock. Up to 64 subjects. |
resolve | Selects versions only from that cache and pins the result per target (native64 or wasm32). The cache directory must exist (SPX-J126). |
Bundled std.* packages need no fetch. Other packages need a
[dependency-sources] row. Nothing discovers a registry or reads the network.
For work outside a project, the package commands take subject files directly:
semaprax package report <file> [--max-bytes N]
semaprax package lock <subject.json>...
semaprax package resolve <subject.json>... --require <pkg>:<range> --target native64|wasm32 [--allow-capability <cap>]...
package report describes one checked file. lock and resolve pin a
dependency closure; capabilities are granted only with --allow-capability.
Use a package registry file
A registry is one JSON document. The registry commands read
semaprax.package-registry-document.v1 and never write, sign or publish.
Schemas v2 and v3 also exist (snapshots that carry artifact manifests and
build facts). They are library and host contracts, not read by these commands.
What the commands do and do not verify:
What Semaprax verifies.
semaprax registry search registry.json num
semaprax registry add registry.json std.num "^0.1.0" # highest matching version
semaprax registry lock registry.json template.json --raw > registry.lock.json
semaprax registry fetch registry.json my.pkg 1.2.0 --raw > my.pkg.subject.json
semaprax registry verify registry.json snapshot-evidence.json
semaprax registry verify registry.json template.json lock-evidence.json
semaprax registry publish registry.json entry.json # decide and print only
publish shows the registry document that would result. It publishes nothing.
There is no default registry and no search path. A missing coordinate is
SPX-Z927; an unreadable file is SPX-Z926; registry rules keep their
SPX-PKR6xx codes. Specs:
Registry Snapshot v1,
Registry-Bound Resolution v1.
Change with review
Look first, then preview, then review. Impact, preview and review are read-only and bound to the exact source bytes: drift fails closed.
semaprax query . impact declaration calculator.add --depth 1 --max-bytes 4096
semaprax query . available-operations calculator.add
semaprax change preview . rename-display-name calculator.add sum
semaprax change preview . add-contract calculator.add ensures predicate.json
semaprax change preview . replace-expression <fn-id> <expression-id> replacement.json
semaprax change preview . add-declaration <anchor-id> declaration.json
semaprax review . transaction.json [--evidence]
| Command | Use it to |
|---|---|
query ... available-operations <id> | See which typed changes the project allows for a declaration. |
change preview | Validate a change and print its candidate, impact and review. Writes nothing. --evidence or --structural-diff print those artifacts instead. Pass --revision <digest> to bind a revision. |
change rebase <base> rename-display-name <id> <name> --onto <project> | Replay a change on another project. |
change merge <project> rename-display-name <a> <x> --with rename-display-name <b> <y> --order left-then-right | Combine two changes in an explicit order. |
review <project> <transaction.json> | Review a canonical Universal Semantic Transaction. --evidence binds intent, impact and review. |
verify <subject> <change> <evidence.json> | Replay an evidence capsule. Its schema selects the verifier (SPX-V201, SPX-V202 if none). |
A preview is not a transaction. Do not pass change preview output to
review: review wants the closed transaction envelope
(SPX-G525 otherwise). Evidence carries no authority; a passing verify is
proof data, not permission to write.
Single-file patches
For one .spx file, a semantic patch (.spatch) works the same way:
semaprax impact app.spx change.spatch # read-only blast radius
semaprax review app.spx change.spatch
semaprax patch-evidence app.spx change.spatch > evidence.json
semaprax verify app.spx change.spatch evidence.json
semaprax patch-with-evidence app.spx change.spatch evidence.json # writes
semaprax patch app.spx change.spatch # writes
patch and patch-with-evidence are the only commands in this list that write
source. The -v2 forms (patch-evidence-v2, verify-patch-evidence-v2,
patch-with-evidence-v2) use the second evidence schema, and target-evidence
adds per-target facts.
Change a managed workspace
A managed workspace holds 2 to 32 canonical .spx files under
.semaprax-workspace. A successful change publishes one complete generation
through a single ACTIVE pivot. It does not rewrite your original files or
make the change atomic for Git or editors.
semaprax semantic-workspace-init ws paths.json
semaprax semantic-workspace-change-preview ws proposal.json # read-only
semaprax semantic-workspace-change-evidence ws proposal.json > ev.json
semaprax verify-semantic-workspace-change-evidence ws proposal.json ev.json
semaprax apply-semantic-workspace-change-evidence ws proposal.json ev.json # publishes
The order is always preview, evidence, verify, apply. Apply replays the
evidence under the workspace lock first; stale or failed input leaves the
workspace unchanged. The same four steps exist for structural changes
(semantic-workspace-structural-change-*) and for operations derived from a
declaration-level change (semantic-workspace-operations-derive,
-change-proposal, -evidence). For read-only questions use
workspace-snapshot, workspace-graph, workspace-context,
workspace-impact and workspace-review. The older .wspatch route
(workspace-init, workspace-preview, workspace-apply,
workspace-patch-evidence) also remains.
The two workspace kinds are not interchangeable. The workspace-* commands expect a root made by workspace-init. Run
workspace-snapshot on a semantic-workspace-init root and it fails with
SPX-G150 (wrong ACTIVE schema). Pick one route per root. Specs:
Semantic Workspace v1,
Workspace Change v1,
Operations v1.
Keep candidates and images
For tool builders. A candidate is a proposed project revision kept as data.
| Command | Use it to |
|---|---|
project-image <manifest> | Print the project’s semantic image. |
project-image-store / -load / -verify | Keep an image in a store and re-check it. |
project-symbol <manifest> <id> | Read one symbol from the image. |
project-candidate-preview / -export / -restore | Preview a change, export it as a capsule, restore it later. |
project-candidate-persist / -load, project-draft-persist / -load | Store candidate and draft archives by digest. |
project-candidate-git-publish <manifest> <capsule> <approved-digest> <host-policy.json> | Commit an approved candidate to Git under a host policy. This is the one publishing step. |
patch-receipt <project> render|verify|compare|... | Render and check receipts for a transaction and candidate digest. |
Run semaprax help <command> for each exact shape.
Serve a project to tools
semaprax service . [--mcp] # JSON-RPC (or MCP) on stdin/stdout
semaprax serve-workspace semaprax.toml host-policy.json
semaprax serve-workspace-mcp semaprax.toml host-policy.json
semaprax serve <file> [--max-request-bytes N]
service authenticates one project at startup and answers queries and
transaction validation over line-delimited JSON-RPC 2.0, single client, local
only. serve-workspace speaks the image-agent protocol; its closed
host-policy file (semaprax.workspace-host-policy.v1) decides whether
candidates, diagnostics, builds, tests and Git commits are allowed. The client
cannot widen it. serve-image, serve-candidates, serve-test-candidates,
serve-diagnostics and serve-diagnostics-tested are narrower variants of the
same protocol. Specs:
Service Transport v1,
Workspace session CLI.
Check assurance and proofs
semaprax assurance-policy app.spx --profile require-static
semaprax assurance-diff base.spx candidate.spx --profile require-static
semaprax assurance-manifest app.spx
semaprax project-assurance-manifest semaprax.toml
semaprax properties app.spx --max-cases 64 --seed 11
semaprax region-report app.spx
assurance-policy checks each obligation against a profile
(require-static, allow-runtime-guard, allow-test-evidence,
report-only). assurance-diff shows what a candidate changes.
properties generates bounded inputs from contracts and evaluates them
(scalar, effect-free functions only). project-proof-check runs an external
Lean or Z3 you name by absolute path against a project law; it needs
--tool, --executable, --version-line and --host-profile. See
Laws and proofs.
Verify a release you downloaded
semaprax release verify <release-dir>
semaprax doctor verify-release <release-dir> --trusted-root-sha256 <64-hex>
release verify reads release-manifest.json, release-provenance.json and,
if present, release-signature-claim.json. It recomputes the manifest digest
and re-hashes every archive the manifest names; nothing a document says about
itself is trusted. doctor verify-release requires complete signed material and
checks it against the root digest you pass (SPX-Z707 on mismatch). Get that
digest from a channel you trust, not from the release directory. Missing
manifest: SPX-Z705. Verifying does not install anything; see
Install.
Check an audit capsule or a workflow
semaprax audit inspect capsule.json
semaprax audit verify capsule.json objects/ --require-role reviewer
semaprax audit diff a.json b.json
semaprax workflow validate workflow.json
semaprax workflow inspect workflow.json
semaprax workflow checkpoint checkpoint.json
semaprax workflow dispatch policy.json request.json
audit inspects, verifies and diffs evidence capsules offline. Verification
can check Ed25519 signatures against a trust roster you supply
(--trust-roster); nothing here signs or submits to a log. workflow validate
and inspect check a typed workflow graph. workflow checkpoint decodes a
checkpoint and prints its state; it never resumes anything. workflow dispatch
decides whether a request target is in a declared policy and records the
decision. All are read-only. See
Audit Capsule v1.
Checklist
- Commit
semaprax.lockbesidesemaprax.toml. - Gate CI on
lock --compare <base.lock>. - Run
query impact, thenchange preview, thenreviewbefore you apply.
Exact rules: Project Lock v1, Project Dependency Resolution v1, Semantic Impact v1, Semantic Review v1, Unified CLI v1.
Build an agent as a program
You will learn how a Semaprax agent declaration splits one model-driven task
into checked stages, how a proposal differs from permission, and how to pick a
model route. This chapter is for people building an agent. To let a coding
assistant edit .spx files, read Agent workflow.
Status. Source agents, the runtime, and model routing are partial and beta. The CLI replays recorded transcripts. A live model needs a Rust host that you write and that owns the provider credentials. Routing evidence is local and fixture-based: no live provider, hosted, or billing claim.
What is a turn?
Each turn runs these stages. Your code owns every stage except the model call.
initialize once
|
observe -> propose (model) -> decode -> authorize -> execute (effect) -> reduce
^ |
+------------------------------- Continue -----------------------------+
or Complete / Suspend / Fail
| Stage | Job |
|---|---|
observe | Build what the model sees from the current state. |
propose | The model returns a typed proposal. This is data, not permission. |
authorize | Deterministic code decides whether the proposal is allowed now. |
execute | The host runs the one allowed effect. |
reduce | Turn state and outcome into Continue, Complete, Suspend, or Fail. |
Authorization runs again on every turn. A grant for one turn never carries over.
Declare an agent
An agent declaration names six types and the stage functions, each with an
@id:
@id("example.agent")
agent Example {
types { type task; type state; type observation;
type proposal; type outcome; type result; } // each with an @id
operations {
fn initialize; fn observe; model fn propose;
fn authorize; effect fn execute; fn reduce; // each with an @id
}
runtime_v1 { canonical_json "..."; }
}
Only propose is model fn. Only execute is effect fn. Missing or
duplicate IDs report SPX-P124. The full rules are in
Language-native Agent syntax.
Check and inspect the committed examples:
semaprax check examples/everyday-agent-v2-project
semaprax check examples/routed-agent-project
semaprax agent skill # the agent contract this compiler carries
Run a recorded transcript
The CLI never calls a model. It replays recorded input:
| Command | Does |
|---|---|
semaprax agent inspect <definition.json> [--profile] | Prints the AgentGraph. |
semaprax agent run <definition.json> <task.json> <transcript.json> [--evidence|--trace] | Runs the scripted turns. |
semaprax agent replay <definition.json> <task.json> <transcript.json> <evidence.json> | Re-checks recorded evidence. |
Do not put an API key in a transcript. A live provider connects through the host integration, not these commands.
Give the model only what it needs
The runtime enforces your declared limits on every run:
- It calls only tools the profile allows. Each tool has a closed argument schema and a read effect.
- One run is single-threaded and bounded: at most 16 turns, 32 provider attempts, 32 tool calls, and five minutes. A profile may lower these, never raise them.
- The runtime never retries a tool or an uncertain provider call.
- Cancellation is cooperative, not forced.
See Budgets, checkpoints, and recovery for limits and what happens when one is reached.
Route to a model
Routing picks one approved model profile per task, and can change it between turns. A router names a profile ID only. It cannot add a tool, capability, or limit beyond what the deployment grants.
| You want | Use |
|---|---|
| One model per task | route_new_invocation over an approved profile set. |
| A different model each turn | RoutedSession, which re-routes only at a durable turn boundary. |
| Pick one granted tool or specialist agent | choice-select/v1, then an authorize-stage recheck (SPX-HPJ024, SPX-HPJ026). |
| See why a route was chosen | route.explain in harness reports; semaprax harness status --routing (Harness). |
Rules mode makes zero router calls. An operator pin that fails screening is refused, not replaced. A revoked deployment is refused on resume.
Runnable examples, offline with fixture models:
examples/routed-agent-project: two approved profiles, a pin, and a re-route.examples/support-routing-project: route a support request to one of two agents.examples/tool-choice-project: select one of two granted read-only tools.
Run them with cargo test --locked -p semaprax --test agent_runtime_v1 routed_agent_example
(or choice_examples). The harness command is in the release archive’s
semaprax and in a source-built semaprax-full. A standalone semaprax
refuses it. See Harness.
Test it before a model is involved
- Supply a fixed task and a scripted sequence of proposals.
- Check the terminal case and its data.
- Add one denied proposal and confirm
authorizerefuses it. - Set a low turn limit and confirm a broken reducer cannot loop.
The interpreter is the default route. Native C11 and Core Wasm stage routes are explicit host choices with their own requirements.
Next: Budgets, checkpoints, and recovery.
References: Agent Runtime v1, Runtime model routing v1, Iterative lifecycle v2, Typed effects v3.
Budgets, checkpoints, and recovery
You will set limits for an agent, and learn what happens when it stops between asking for an operation and seeing the result. Decide this before an agent does important external work. The question is always: what was authorized, what may have happened, and what can safely happen next?
Status. Local and fixture-based evidence. The recovery routes (
semaprax-full source-live) are private tooling, not the public CLI.
What limits does a run have?
| Limit | Behavior |
|---|---|
| Turns, provider attempts, tool calls | Runtime v1 caps: 16, 32, 32. A profile may lower them. |
| Deadline | Five minutes at most. Elapsed time equal to the limit counts as expired. |
| Cost, tokens, calls, context | Checked before each model call by the budget policy. |
| Cancellation | Cooperative. It does not interrupt a call already in flight. |
Since v0.9.0 the runtime checks the live clock and the remaining allowance before each model call, including routing calls. Each call also gets its remaining deadline as a positive budget. A new turn that has no time or allowance left stops before any provider call.
When no model is both eligible and affordable, the run ends policy_rejected
with SPX-G206. It still returns its Trace, Evidence, and accounting receipt
for work already done. That terminal is replayable: replaying returns the same
result and calls no provider.
How is cost recorded?
A reservation sets money aside before a call. An observation records what the provider reported afterward.
| Case | Recorded as |
|---|---|
| Success with an explicit usage report | observed |
| Explicit all-zero report | observed zero |
| Missing report, failed attempt, or response rejected after dispatch | unknown |
A lost response does not prove the provider did no work, so unknown stays unknown. The accounting receipt reports reserved, observed, and unknown totals. It cannot bill or settle.
What does the journal record?
The journal writes intent before dispatch and keeps the result after.
| State on restart | What recovery does |
|---|---|
| Not started | Needs fresh authorization and a fresh reservation. |
effect_intent recorded, no result | Marks the effect uncertain. It makes no model call and no callback call. |
| Result recorded | Replays the result without repeating the operation. |
| Terminal recorded | Returns it without new work. |
Since v0.9.0 a routed session journals effect_intent before the host callback
that may run tool effects. Only model bytes recovered before any durable intent
allow exactly one callback. An uncertain dispatch or effect halts the session
for reconciliation. It is never retried with another model.
Do not repeat an uncertain write, message, or payment. Reconcile it first.
What is a checkpoint?
A checkpoint belongs to one execution and one source or deployment. Recovery checks its bindings, limits, and retained results before doing more work. A JSON file with plausible state does not replace the checkpoint producer and the trusted store. A hash identifies bytes but does not stop someone who controls the storage from rolling the whole history back. Protect the store at the host.
Resume after an interruption
RoutedSession::resume restores the journal. Completed turns replay with no
route, model, or effect calls. An in-flight turn reuses its recorded route with
zero router calls. A revoked deployment is refused and never rebound.
For source agents, the private semaprax-full tool has source-live run,
resume, migrate, and repair:
semaprax-full help source-live
It takes a project config, a checkpoint directory, a provider executable, and an empty scratch directory. Start with an injected handler or recorded input, not live credentials. See Source live CLI, Source journal, and Source migration.
Change the agent’s state type
When the State type changes, an old checkpoint may not fit. A migration binds the old and new revisions and checks the transformation. Spending carries across it: a new revision does not reset the budget already used. A mismatched predecessor is refused.
Delegate to a child agent
A child session runs under a grant: an allowance reserved from the parent, a depth limit, one profile, and the parent’s deadline. A child never widens the deadline or overspends. Settling a child never refunds. Delegation is bounded and runs on the same host.
Money-moving operations
A model’s payment proposal is input to a workflow of intent, policy, approval, signing, broadcast, and reconciliation. The host supplies the wallet and the authority. Keep simulations apart from real payments. See Economic agent.
Test recovery
Interrupt a run at each point and check that settled work is never repeated:
- Before dispatch.
- After intent is recorded.
- After the handler returns.
- After the terminal result.
Then try a changed source revision, a lowered budget, a cancelled task, and a mismatched checkpoint.
Next: Source map.
References: Agent Runtime v1, Runtime model routing v1, Per-operation checkpoints, Model budget policy.
Style guide
fmt and check enforce most style. This page covers what tools cannot decide:
identities, naming and layout. After this page you can name things so that
agents, queries and patches keep working across renames.
Give every declaration an ID
An @id is a declaration’s permanent address. Renames do not change it, and
every query, patch and graph edge uses it.
module calculator.core;
@id("calculator.add")
fn add(left: i64, right: i64) -> i64
{
left + right
}
Without an @id, check warns SPX-S103: the automatic identity changes when
the function is renamed. Assign one by hand, or use
semaprax fix --plan (see Debugging).
- Namespace by module:
calculator.add,calculator.tests.test_add. Avoid flat global names and trees likea.b.c.d.e.f. - Name the meaning, not today’s name:
math.addsurvives a rename toplus. - ID fields, cases and methods too. Queries and patches address them by ID.
- Never reuse an ID for a different declaration. A stale ID with new meaning is worse than none, and nothing can warn about it.
Names
| Thing | Style |
|---|---|
| Modules | dotted.lowercase, matching the file’s role |
| Functions, bindings | snake_case |
| Types | PascalCase |
| Tests | test_<what>() -> i64, no parameters, 0 is pass |
main | Returns i64; 0 is success |
| Project names | [a-z][a-z0-9-]* |
Layout: let fmt do it
semaprax fmt . # rewrite every file in canonical form
semaprax fmt . --check # report drift, write nothing
Canonical form: the body’s { on its own line; one statement per line; compact
if, match and record literals; a trailing , after every field, case and
arm including the last. fmt parses all files before rewriting any, and keeps
// comments (placement rules).
Workspace transactions do not promise to keep comments, so put durable intent in
@id names, contracts and tests. Run fmt before check. A formatting diff is
never the interesting part of a review.
Organize files
- One module per file, one concern per module.
- The entry module holds
mainand wiring. Logic lives in sibling modules imported by ID, right after themoduleline:use function @id("calculator.add") from calculator.core as add; - Tests live in their own module, listed under
testsin the manifest. - Keep bodies shallow. Nesting deeper than 128 levels fails with
SPX-P207, but a few levels is already a sign to extract a named helper with its own@idand contracts. Then it is queryable and testable.
Put intent in contracts
requires and ensures state what callers may pass and what they get back, and
run on every call, including under test. Prefer them to comments. Write the
effect list (permit and uses) as narrowly as the code allows. See
Contracts and effects.
Keep the manifest canonical
Keep semaprax.toml byte-canonical: table order, one blank line between tables,
one-line arrays, no comments. SPX-J100 names the first differing line. See
Manifests. Pin what you ship:
semaprax lock . --write, then --verify, and --compare <base.lock> in CI
(Shipping).
Generate docs from the source
semaprax doc <file> renders declarations, signatures, contracts, effects and
comments from the checked graph, so your docs cannot drift from the code
(--json for tools). A file given to doc needs a fn main() -> i64.
Testing
A test is a function that returns i64: 0 passes, anything else fails. After
this page you can write tests, read a failure, and run the full check ladder
before you push.
Run a project’s tests
Save these four files in a new testing-demo/ directory (the src/ files go in
testing-demo/src/).
schema = "semaprax.manifest.v1"
[package]
name = "testing-demo"
version = "0.1.0"
[modules]
entry = "demo.app"
sources = ["src/app.spx", "src/core.spx", "src/tests.spx"]
tests = ["demo.tests"]
[exports]
web = ["demo.divide"]
module demo.core;
@id("demo.divide")
fn divide(left: i64, right: i64) -> i64
requires right != 0
ensures result * right <= left
{
left / right
}
module demo.app;
use function @id("demo.divide") from demo.core as divide;
@id("demo.main")
fn main() -> i64
{
divide(84, 2)
}
module demo.tests;
use function @id("demo.divide") from demo.core as divide;
@id("demo.tests.test_divide")
fn test_divide() -> i64
{
if divide(84, 2) == 42 { 0 } else { 1 }
}
@id("demo.tests.test_divide_by_one")
fn test_divide_by_one() -> i64
{
if divide(7, 1) == 7 { 0 } else { 2 }
}
@id("demo.tests.main")
fn main() -> i64
{
0
}
semaprax test .
project tests passed (2 named cases)
test runs the test module’s main, then every zero-parameter test_*
function in that module. Each needs its own @id. The module must be in the
manifest’s tests entry. A test_* function with another shape is not a case.
| Output | Meaning |
|---|---|
project tests passed | main returned 0 and there are no named cases. |
project tests passed (N named cases) | main and all N cases returned 0. |
failed <id>: returned 2 | That case returned 2. Use distinct codes per assertion. |
project tests failed: 1 of 2 named cases in demo.tests | The summary line. Exit code is 1. |
Options: --json for the full case list (semaprax.project-execution.v1),
--max-steps N, --max-bytes N. Tests run in the bounded interpreter only.
They do not exercise native or Wasm output, so build and test those targets
separately.
Read a contract failure
A failed requires or ensures fails the case that triggered it and names the
clause and arguments. Change divide(7, 1) to divide(7, 0) and run again:
failed demo.tests.test_divide_by_one: language status {"schema":"semaprax.status.v1","domain_id":"semaprax.contract.v1","code":1,"class":"contract","retryable":false}
contract: requires right != 0 in demo.divide
arguments: left = 7, right = 0
Write the contract first, then test valid inputs at the edges: zero, one item,
the largest accepted value. Do not keep a deliberately invalid call in the normal
suite: it fails the suite. Put it in a separate fixture and assert the failure
status from your harness. For expected user-facing errors, return Result and
test the error arm. See Contracts and effects.
Prove the test can fail
Change == 42 to == 41, run semaprax test ., confirm it fails, then change
it back. A test that cannot fail checks nothing. Keep one purpose per test:
test_divide_by_one is easier to diagnose than test_everything.
More test tools
| Tool | Use it to |
|---|---|
semaprax properties <file> | Generate bounded inputs from signatures and contracts and evaluate them. Scalar, effect-free functions only. Options: --max-cases, --max-functions, --max-bytes, --seed. |
semaprax interpret <file> --function <id> --arg 1 --arg 2 | Call one function with scalar literals. |
semaprax assurance-policy <file> --profile <p> | Check that each obligation is met statically, by guard or by test evidence. |
semaprax network-run <project> --fixture f.json | Run a network command against a recorded fixture. |
| Law files | Prove or check declared laws. See Laws and proofs. |
A single file given to properties or interpret needs a fn main() -> i64
(SPX-T105 otherwise).
The check ladder
Run these in order and stop at the first failure:
semaprax fmt . --check # canonical layout, manifest order
semaprax check . # types, contracts, effects, ownership
semaprax test . # executable checks
semaprax build . --target web -o dist/web # target acceptance
semaprax lock . --compare base.lock # CI: fail on breaking interfaces
fmt . parses every file before it rewrites any. check, test, build and
fmt accept ., a directory, semaprax.toml or --manifest-path.
Check handbook examples
If you edit these docs, run from the repository root:
/usr/bin/python3 scripts/check-handbook.py --compiler /absolute/path/to/semaprax
It formats temporary copies, checks and runs every marked example, compares
output, and verifies links and chapter navigation. --structure-only skips the
compiler. Unmarked snippets are not run.
References: Project Test Cases v1, Property-Test Generation v1.
Debugging diagnostics
A diagnostic has a stable SPX-... code, a message, a file:line:column
location and usually a help: line with the fix. After this page you can find
the cause of an error and the smallest fix for it. Match on the code, not the
wording: wording changes, codes do not.
error[SPX-P106]: `return` is not admitted; a block's value is its final expression at bad.spx:6:5
help: delete `return` and the trailing `;` so the value is the block's last expression
The loop
semaprax fmt <file>. Many “errors” are layout the formatter fixes.semaprax check <file>. Add--jsonfor one JSON object per diagnostic (code,severity,message,path,location,help).- Fix the first diagnostic at its location. Later ones are often knock-on.
- Still unclear? Look up the code (case-sensitive):
semaprax help diagnostic SPX-T208 # what you wrote, and the fix
semaprax help diagnostic codes # every indexed code
semaprax explain SPX-T208 [--json] # the installed explanation
Exit codes: 1 for compile or run failures, 2 for a bad command line. A
warning such as SPX-S103 (a function with no @id) still exits 0.
Check the project, not one file
A module without main is a library module. Checking it alone fails with
SPX-T105, SPX-G172 or similar. Run semaprax check . from the project.
Single-file commands (run, doc, properties, package report, …) need a
file with fn main() -> i64.
Apply a one-step repair
Some fixes have a plan. fix --plan lists the repair operations your binary
knows. For a function missing its @id (SPX-S103):
semaprax fix --plan
semaprax fix app.spx assign-function-id <automatic-function-id> --plan
semaprax repairs app.spx assign-function-id <automatic-function-id>
semaprax repair app.spx <repair-id> --persistent-id my.namespace.main
--plan changes nothing. repair is the step that writes source.
The top fixes
| You wrote | Code | Fix |
|---|---|---|
return 42; | SPX-P106 | Tail expression: 42 |
else if | SPX-P106 | else { if ... } |
while body ending in an assignment | SPX-P203 | End with the continuation condition |
for i in 0..n | SPX-P106 | while with a let mut counter |
f(x); as a statement | SPX-P106 | let _ = f(x); |
let t = (1, 2); | SPX-P106 | No tuples; declare a record |
i = i + 1 on an immutable i | SPX-U101 | let mut i = ... |
let x = 1; let x = ... | SPX-T209 | No shadowing; new name |
index + 1 with index: usize | SPX-T208 | index + 1usize (no mixed types) |
let a: i32 = 5 | SPX-T232 | 5i32 (literals default to i64) |
"a" + "b" | SPX-T250 | string_concat("a", "b") |
Some(1) / None | SPX-T203 | Option<i64>::Some { value: 1 } / Option<i64>::None {} |
Some(b) => in a pattern | SPX-P106 | Option::Some { value: b } => |
f("abc") for borrow str | SPX-T205 | Bind, then f(string_as_str(s)) |
string_as_str("lit") | SPX-T266 | Bind the literal first |
point.get(), s.len() | SPX-T203 | Only classes have methods: get(point), string_len(s) |
Second use after an own move | SPX-O101 | Callee takes borrow, or pass a fresh value |
fn main() -> bool | SPX-T104 | main returns i64; 0 is success |
Missing permit or uses | SPX-E101, SPX-E102 | Declare the effect at module and function level |
Last field or arm without , | SPX-P106 | Trailing comma everywhere |
| Non-canonical manifest | SPX-J100 | help names the first differing line |
SPX-F102 (interpreter does not admit the program) is not a source error: use
run --native.
Environment and command errors
| You see | Do this |
|---|---|
SPX-B101 failed to start clang | Install Clang and put it on PATH. |
SPX-I001 cannot read ... | Wrong path or working directory. |
SPX-I307 | The build output path exists. Choose a new one. |
SPX-J102 cannot inspect ... semaprax.toml | No manifest in that directory. |
SPX-J102 on fmt | A path alias. Use the real path. |
unknown command ...; did you mean ...? | Use the suggested command. |
doctor: failed profile: an explicit offline profile is required | Expected. Pass --profile <id>. See Targets. |
Every rejected command prints hint: run semaprax <command> --help. Use it.
Ask a smaller question
semaprax help language topics # then: help language ownership
semaprax help shapes record # smallest declaration example
semaprax help library std.core.compare # one stdlib entry (~200 bytes)
semaprax skills get language # packaged language guidance
The full card is the
Agent quick reference,
printed offline by semaprax help language. The diagnostics reference
lists codes by family.
Cookbook
Copy-paste recipes. After this page you can do the common jobs: print text,
loop, match, handle errors, and run the everyday project commands. Language
recipes are complete modules: save one as app.spx and semaprax run app.spx.
A program that exits 0 prints 0 after its own output.
Print a greeting
module app.greet;
permit { process.stdout.write }
@id("app.main")
fn main() -> i64
uses { process.stdout.write }
{
let greeting = string_concat("hello, ", "world");
let view = string_as_str(greeting);
let written = stdout_write(str_as_bytes(view));
if written == 12usize { 0 } else { 1 }
}
stdout_write returns the byte count. Assert it so a truncated write fails
loudly. The module needs permit { process.stdout.write } and uses on main
exactly as above (SPX-E101, SPX-E102 otherwise).
Sum digits with a loop
module app.sum;
@id("flow.digit_sum")
fn digit_sum(value: i64) -> i64
requires value >= 0
ensures result >= 0
{
let mut remaining = value;
let mut total = 0;
while remaining > 0 {
total = total + remaining % 10;
remaining = remaining / 10;
remaining > 0
}
total
}
@id("app.main")
fn main() -> i64
{
if digit_sum(98765) == 35 { 0 } else { 1 }
}
The loop pattern: let mut state, while with scalar updates, continuation
condition as the body’s last line, pure scalar result.
Classify with match
module app.sign;
@id("flow.classify")
fn classify(value: i64) -> i64
{
match value { 0 => 0, -1 | -2 => -9, n if n < 0 => -1, _ => 1, }
}
@id("app.main")
fn main() -> i64
{
if classify(-2) == -9 { 0 } else { 1 }
}
Order arms from specific to general; the final _ catch-all is mandatory.
Safe division with Result
module app.divide;
@id("data.checked_div")
fn checked_div(left: i64, right: i64) -> Result<i64, i64>
{
if right == 0 { Result<i64, i64>::Err { error: 1 } } else { Result<i64, i64>::Ok { value: left / right } }
}
@id("app.main")
fn main() -> i64
{
let ok = match checked_div(8, 2) { Result::Ok { value: v } => v, Result::Err { error: code } => code, };
let err = match checked_div(8, 0) { Result::Ok { value: v } => v, Result::Err { error: code } => code, };
if ok == 4 && err == 1 { 0 } else { 1 }
}
Construct with type arguments (Result<i64, i64>::Ok), match without
(Result::Ok). Callers handle both arms; the compiler enforces it. The
interpreter does not admit this program (SPX-F102), so run it with
semaprax run app.spx --native.
Update a record immutably
module app.move_point;
@id("data.point")
record Point {
@id("data.point.x")
x: i64,
@id("data.point.y")
y: i64,
}
@id("app.main")
fn main() -> i64
{
let origin = Point { x: 1, y: 2 };
let moved = origin with { y: 10 };
if moved.x == 1 && moved.y == 10 { 0 } else { 1 }
}
with builds a new value; the original is untouched. Construction must name
every field. Run it with --native (SPX-F102 in the interpreter).
Count bytes in a string
module app.count;
@id("bytes.count_a")
fn count_a(text: borrow str) -> usize
{
let view = str_as_bytes(text);
let length = byte_len(view);
let mut index = 0usize;
let mut hits = 0usize;
while index < length {
hits = match byte_get(view, index) { Option::Some { value: byte } => if byte == 97u8 { hits + 1usize } else { hits }, Option::None {} => hits, };
index = index + 1usize;
index < length
}
hits
}
@id("app.main")
fn main() -> i64
{
let word = "banana";
if count_a(string_as_str(word)) == 3usize { 0 } else { 1 }
}
The byte pattern: borrow the string, view it as bytes, walk with usize
indices, destructure byte_get’s Option<u8>. Byte 97u8 is 'a'.
Start a project and run its checks
semaprax new my-app && cd my-app
semaprax fmt . && semaprax check . && semaprax test . && semaprax run .
fmt first, check second. Stop at the first failure.
Add a test
Add a function to your test module and list that module under tests:
@id("my_app.tests.test_add")
fn test_add() -> i64
{
if add(2, 2) == 4 { 0 } else { 1 }
}
semaprax test . runs every zero-argument test_* function and names failures.
Add a dependency
semaprax add . std.num "^0.1.0"
semaprax help library std.num # exact signatures and stable IDs
Find who calls a function
semaprax query . --calls my-app.add # callers
semaprax context . my-app.add --direction both --depth 1 --max-bytes 4096
Rename safely
semaprax change preview . rename-display-name my-app.add sum
Read the preview. It writes nothing. See Shipping.
Ship a web package
semaprax build . --target web -o dist/web
semaprax lock . --write
Gate CI on interface breaks
semaprax fmt . --check && semaprax check . && semaprax test .
semaprax lock . --compare base.lock # exits 1 when breaking
Run a network command offline
semaprax network-run . --fixture http.fixture.json --arg https://example.test/
This uses a recorded fixture; no real connection opens. See Profiles.
Driving Semaprax from an AI agent
The compiler answers questions about program meaning as small, bounded data. An agent spends tokens on source and decisions, not on dumping a repository. After this page you can set up a coding agent to write, check and change Semaprax safely.
New to the CLI? The visual tour shows the first commands. This page is for an assistant editing your project. An agent program written in Semaprax is a different thing.
Give the agent its instructions
semaprax new writes an AGENTS.md into every project. It lists the commands
and the rules that differ from other languages, and points at one language topic
instead of the 33 KB card. For other tools:
semaprax skills get agent # also: language, graph, stdlib, packages, effects
semaprax agent skill # the installed agent skill: authority classes, verbs
semaprax query --capabilities # what this binary's query and change commands accept
Each verb in the skill has an authority class: read_only, candidate_only,
test_execute, source_write or publication. Grant an agent the lowest class
that does its job.
The edit loop
- Write the file. Run
semaprax fmt <file> && semaprax run <file>(one call). - On failure, fix the first diagnostic at its line and column. Match the
SPX-...code, not the message. Plain output is smaller than--json. - Read small
.spxfiles directly. Never fetchgraphto look around: on the calculator it is about 40 times the source.
In a project use semaprax check ., semaprax test .. See
Debugging.
Ask bounded questions
semaprax query <project> --id <stable-id> # find a declaration
semaprax query <project> --calls <stable-id> # who calls it
semaprax query <project> --kind function --effect clock.read
semaprax context <project> <stable-id> --direction both --depth 1 --max-bytes 4096 --max-nodes 16
semaprax context <file> <stable-id> --depth 1 --filters contracts,ownership --max-bytes 4096
Check truncation before treating a result as complete. --max-bytes for
context is at least 2048. Project context does not accept --filters. Use
graph only when a tool needs the whole expression tree or cleanup plan, and
--json only when you need exact revision fields. semaprax doc <file> renders
declarations, contracts and effects as text or --json.
Ask for narrow help
semaprax help <command> # one command's grammar
semaprax help language topics # then: help language <topic>
semaprax help diagnostic <SPX-code> # one fix
semaprax help shapes <kind|stable-id> # minimal declaration example
semaprax help library <module|name|stable-id> # one stdlib entry
help all and the full card are for broad questions. One help library lookup
is about 200 bytes; the full catalog is about 22 KB.
Write source an agent can change later
- Put an explicit
@idon every declaration, field and case. Laterquery,contextand patches address them by ID, and IDs survive renames. - Put intent in
requires/ensures, tests and ID names.fmtand single-filepatchkeep//comments, but workspace transactions do not promise to. - Tell the agent the project’s profile before it changes parameter or result types.
Make checked changes
semaprax query <project> impact declaration <id> --depth 1 --max-bytes 4096
semaprax change preview <project> rename-display-name <id> <new-name>
semaprax review <project> <transaction.json>
semaprax verify <subject> <change> <evidence.json>
Impact, preview and review write nothing and are bound to exact source bytes:
drift fails closed. A preview, a review report and an applicable transaction are
different objects; do not feed one to a command that wants another. Evidence
carries no authority: replay it with verify before you apply anything. The
full flow, including managed workspaces, is in
Shipping.
Give an agent a live connection
| Command | What the agent gets |
|---|---|
semaprax service <project> [--mcp] | One authenticated project over line-delimited JSON-RPC 2.0, or MCP, on stdin/stdout. Queries and transaction validation. |
semaprax serve-workspace <manifest> <host-policy.json> (and serve-workspace-mcp) | Image-agent protocol with candidates, diagnostics, tests, builds and Git commits only as the host policy allows. |
semaprax serve <file> | A single-file request loop. |
semaprax dev <manifest> --jsonl | Hot reload control frames (Targets). |
The host decides authority. The client cannot widen the policy. An agent inside VS Code uses saved-source candidate sessions; see Editor setup.
Smaller context
semaprax compact ... and the token report script shrink what a model reads.
See Context and performance. For a visual map use the
semantic explorer.
A coding agent is not an agent program
Agent programs written in Semaprax have task, state, proposal and authorization
roles. Inspect and replay them with semaprax agent inspect|run|replay
(Agent programs). The development harness
(semaprax harness ...) lets a coding agent propose changes under compiler
checks (Harness). The release archive ships the full
build as semaprax, so the command works there. A standalone or crates.io
semaprax refuses it (exit 2); build semaprax-full from source instead. It is
development tooling, so do not build a product on it.
The complete agent contract is the
Agent quick reference,
printed verbatim by semaprax help language.
Explore a project’s meaning
The semantic explorer turns a checked project into a view you can read, share or compare. After this page you can export one declaration’s neighborhood as HTML, Markdown, JSON or SVG, and answer smaller questions from the terminal.
Export a focused view
Run this in a project directory (the output file must not exist):
semaprax explore semaprax.toml --target calculator.add --depth 1 --format html --output calculator-explorer.html
Open the file in a browser. The view is centered on calculator.add.
--depth limits how far the neighborhood extends. Omit --target and
--depth for the whole project.
--format | Use it for |
|---|---|
html | Browsing. |
markdown | A written review. The page is source-free and may still contain names and paths. |
json | A tool. |
svg | A diagram. |
An existing output path fails with SPX-G328. Use one new filename per format
and subject. Keep generated reports out of commits unless you version them on
purpose, and keep the revision lines when you share one.
Ask smaller questions first
semaprax query . --id calculator.add # where is it?
semaprax query . --calls calculator.add # who calls it?
semaprax query . --called-by calculator.app.main # what does it call?
semaprax query . available-operations calculator.add # which typed changes are allowed?
semaprax context . calculator.add --direction both --depth 1 --max-bytes 4096
semaprax doc <file.spx> # declarations, contracts, effects
A caller uses the function. A callee is a function it calls. Keep the two directions straight when you estimate the effect of a change.
Preview a rename
semaprax change preview . rename-display-name calculator.add sum
The preview renames the display name and keeps the stable ID. It writes nothing. Next steps are in Shipping.
Review a candidate
Pass a candidate capsule and the exact digest you intend to review:
semaprax explore semaprax.toml --candidate-capsule capsule.json --expect-candidate sha256:<digest> --format markdown --output review.md
If the digest differs, the export fails: you never review a candidate you did
not mean to. A capsule is revision-bound candidate data. Make one with
project-candidate-export (Shipping).
semaprax compact candidate-diff <project> <capsule> gives a compact diff.
Fill a typed hole or take a compiler repair
Use these when a change is half done or was rejected. Both work on a candidate, which is in memory, so source stays unchanged until you commit.
A typed hole marks one expression in a function body that you will fill later. The compiler gives you the hole’s expected type and ownership, the effects allowed there, the names in scope and the calls you may use. A fill becomes a normal typed change and goes through the full checks. There are no placeholder source text, no invalid code and no run. You can open up to 16 holes, and they must not overlap.
semaprax serve-diagnostics semaprax.toml # JSON-RPC over stdin and stdout
A client sends hole/open-expression, then hole/query (context),
hole/fill-suggestions, hole/fill, and finally hole/complete.
hole/discard drops the draft. complete refuses while any hole is open
(SPX-G232 for a stale or unresolved selection). In VS Code use
Open Typed Hole and Fill Selected Hole from Active Scratch
(Editor setup).
A repair route is a fix the compiler derived and already admitted. When
candidate/attempt rejects a change, the same server keeps the attempt:
attempt/repair-catalog lists only proposals that pass every check (or says
there are none), and attempt/repair-apply turns one into a new candidate.
The compiler never picks or ranks for you. For files rather than candidates,
the one repair today is the missing @id fix (SPX-S103): semaprax fix --plan, then repairs and repair
(Debugging).
Specs: Expression Holes v1, Image Candidate Diagnostic Protocol v4.
What to ask before you accept a change
Which declaration changed? Which callers are involved? Did contracts or effects change? Which tests cover it? Refresh the report after every edit: an old diagram stays readable long after it stops describing the project.
Next: Use the same workflow from an AI coding agent. References: Semantic Explorer v1, installed command catalog.
Measure context size and reuse checked work
Two things make a tool-driven workflow cheaper: a smaller context to read, and a cache that reuses checked compiler work. After this page you can produce a compact view, measure it, and warm a semantic cache. Measure the two separately so you know which change helped.
Pick the smallest view
- Source, when the file is small.
queryto find a declaration, thencontextfor its neighborhood.compactfor a model-facing encoding.graphonly when a tool needs the whole thing.
compact has five forms. Each takes --encoding text|binary|model-text and
--replay <encoded>, which rebuilds from current source and compares with an
encoding you already hold:
| Form | Selects |
|---|---|
compact context <file> <stable-id> [--max-bytes N] | One declaration’s context. |
compact task-context <file> <stable-id> [--goal text] [--seed id]... [--max-tokens N] [--tokenizer byte-v1|lexical-v1] | Context ranked for a stated goal and budget. |
compact graph <file> | The whole graph. |
compact api-surface <project> | Public API of an owned-data-api.v1 project only (SPX-J105 otherwise). |
compact candidate-diff <project> <capsule> | What a candidate changes. |
compact agent-definition <file> | An agent definition (canonical JSON input). |
model-text is the text form meant for models. binary belongs with a
binary-aware consumer. From the repository root:
semaprax compact context examples/meaning.spx math.add --max-bytes 4096 --encoding model-text
semaprax compact api-surface examples/frame-payload-project/semaprax.toml --encoding model-text
Measure a real input
scripts/token_report.py compares exact payloads with a local tokenizer. It
needs locally installed tiktoken and cached cl100k_base or o200k_base
assets, and never downloads anything.
python3 scripts/token_report.py projection \
--semaprax "$(command -v semaprax)" \
--input examples/meaning.spx \
--profile context \
--selection math.add \
--encoding model-text \
--measurement-tokenizer cl100k_base \
--allow-bytes-only \
--output target/meaning-token-report.json
python3 scripts/token_report.py show target/meaning-token-report.json --format text
--output must be a new file unless you pass --overwrite. With
--allow-bytes-only, missing token counts stay empty while byte counts are
reported. Drop the flag when the check must fail without token counts. On
Windows, give the absolute path to semaprax.
Other subcommands: compare (two reports) and session (aggregate a recorded
event stream; show renders it). The
MCP session recorder
is a recording example. VS Code shows a snapshot with SEMAPRAX: Show Token
Report.
Read a report
A projection report compares the compact payload with the same selected JSON content. That is not the same as comparing a narrow context with a whole repository dump.
| Field | Meaning |
|---|---|
| Baseline | The explicit reference payload. |
| Actual payload | The output produced for this measurement. |
| Positive token delta | Fewer tokens than the baseline. |
| Negative token delta | More tokens than the baseline. |
| Tokenizer fingerprint | The exact vocabulary used. |
| Source revision | The source snapshot measured. |
Bytes, local token counts and provider-reported usage are different
measurements. Planner choices (byte-v1, lexical-v1) are not model-tokenizer
counts. Keep the measurement kind with any number you share.
Reuse compiler work with a semantic cache
A semantic cache stores checked HIR (the compiler’s resolved program) so a new process can skip some frontend work on the same project. It carries no source authority. On a supported Unix host, from the repository root:
mkdir -p target
mkdir -m 700 target/handbook-cache
CACHE="$(cd target/handbook-cache && pwd -P)"
semaprax semantic-cache-init "$CACHE"
semaprax semantic-cache-persist examples/calculator-project/semaprax.toml "$CACHE"
The store directory must be new, empty and owner-only. Init creates a private
key: keep it protected and out of Git. The persist receipt holds the
entry_digest you need next.
semaprax semantic-cache-warm-open <manifest> <absolute-store-root> <entry-digest>
semaprax semantic-cache-refresh <manifest> <absolute-store-root> <entry-digest>
semaprax semantic-cache-cold-open <manifest>
semaprax semantic-cache-load <store-root> <entry-digest>
semaprax semantic-cache-evict <store-root> <entry-digest>
semaprax semantic-cache-lifecycle <manifest> <empty-store-root>
| Command | Does |
|---|---|
warm-open | Authenticates the entry and admits the current project. |
refresh | Writes a successor entry after a current-source check. |
cold-open | Opens without a cache: the recovery route after a rejected warm open. It reports which sources it invalidated. |
load | Reads a historical entry. Not the same as warm-open. |
evict | Removes an entry. |
lifecycle | One receipt over the cold, restored, refreshed, evicted and cold-rebuilt stages, with retained byte counts. Needs an empty private store. |
A compiler upgrade can invalidate an entry even when the version string is
unchanged: compatibility includes the executable identity. For a fair cold and
warm comparison, fix the compiler build, source revision, cache state and task.
retention-metadata-* commands manage retention checkpoints for stores; see
Semantic Retention Metadata CLI v1.
Benchmark context size
semaprax context-benchmark <benchmark-manifest> measures agent-context sizes
from a tab-separated manifest that must start with
schema<TAB>semaprax.agent-context-benchmark.v1 (SPX-G005 otherwise). See
Agent Context v1 for fixtures.
Next: Configure the editor report view. References: token reporting, Compact Projection v2, Persistent Semantic Cache v1.
Harness: let an agent propose changes under checks
After this page you can set up the development harness, run one agent-proposed repair through compiler checks, and connect it to an outside coding agent such as Claude Code.
The harness is optional tooling around the compiler. Ordinary check, build
and run never start it, read its files or need its adapters. The compiler is a
service the harness calls; the harness cannot overrule a compiler verdict.
Where it lives. semaprax harness <verb> exists in the release archive’s
semaprax and in a source-built semaprax-full. The standalone semaprax build
answers harness is unavailable in the standalone crates.io package and exits 2.
semaprax-harness <verb> is the same code as its own binary in a checkout.
Status. Development harness. The specs record local macOS arm64 evidence; Linux is untested. Treat model routing as rules-based unless you select a provider yourself (see Routing).
Set up
semaprax harness setup --project . --preset native
semaprax harness setup --project . --preset native --yes
Without --yes, setup prints a plan and changes nothing. With it, setup unpacks
the bundled adapters into the harness home ($SEMAPRAX_HARNESS_HOME, default
~/.config/semaprax/harness), adopts and trusts the ones you chose, sets your
preference, and writes semaprax.harness.toml into the project if none exists
(provider ids only: no paths, no trust). It keeps an existing project file and
never overwrites it. Running it twice reports noop. It never scans PATH or
$HOME and downloads nothing; pass tools with --path-dirs or
--tool graft=/abs/path. Presets: native (built-in context and command view,
official skills) and local-efficient (adds RTK and one repository provider,
Graft by default). A missing optional tool falls back to the built-in and
exits 0.
semaprax harness status [--json] shows what resolved, and
explain <kind> says why.
Run one repair
semaprax harness run . --task task.json --proposal proposal.json
run follows one pipeline on one revision: authenticate the source, run
check and test, gather context within a byte budget, get a proposal (from your
--proposal file or a model you selected), preview it as a candidate, check the
candidate in a scratch copy, and stop. It stops at approved-candidate-ready
unless you pass --apply-policy policy.json, which publishes the candidate to a
local Git repository under that policy.
The harness refuses a proposal that deletes a requires/ensures line, adds a
uses effect, or weakens the requirements (SPX-HPD042 to HPD044). A candidate
that fails checks is SPX-HPD050, never success. A publication that may have
happened is uncertain and is never retried. Exit 0 means
approved-candidate-ready, published or no-repair-needed. --disable runs
with built-ins only and no external calls. Spec:
Harness workflow.
All verbs
| Verb | Use it to |
|---|---|
setup | Plan or apply the one-time setup above. |
status, explain, resolve [--frozen], inspect <provider-id> | See and freeze which providers handle each capability, per semaprax.harness.toml and its lock. |
adopt <descriptor> [--upstream <abs>], trust <id>, revoke <id> | Record, approve and withdraw a provider. adopt is the only verb that runs an upstream tool, with a cleared environment. Revocation applies on the next call. |
run, apply | Run the repair pipeline. apply <project> --session <result-dir> --expected-revision <digest> re-verifies a finished session, requires your project to still match the session’s baseline, and writes the changed files. It never publishes. |
context <project> <query> [--max-bytes N] | Get compiler-verified context plus optional repository-provider results, within a byte budget. |
exec <project> -- <argv...>, recover <handle> | Run one command once, return a short model-facing view, and keep the full output retrievable by handle. |
decide <project> <task.json> | Ask the routing layer which model route a task gets. |
endpoints <project> [adopt|bind|reprobe|litellm-config] | Register loopback model endpoints (Ollama, LiteLLM, OpenAI-compatible) and bind logical models to them. |
skills <project> [list|load <digest>] | List and load instruction skills from approved roots. |
updates ... | Keep skills and adapter packages current from pinned sources, by exact commit. |
bridge <project> --stdio|--mcp|--setup claude-code|--host claude-code | Connect an outside coding agent (Bridge). |
report <observations.jsonl> [--json] | Attribute token use across stages of a run. |
conformance <descriptor> | Test an adapter against the provider contract and a hostile-input suite. |
bench ... | Run journey and routing benchmarks. Results are evidence, not a support claim. |
evolve run|promote|status | Experimental: propose a skill revision from traces. Gated and off by default. |
Exit codes: 0 success, 1 refused or failed with diagnostics, 2 usage error.
Most verbs accept --json.
Project configuration
semaprax.harness.toml sits in the project and holds no paths, secrets or trust
grants:
schema = "semaprax.harness-config.v1"
[profile]
enabled = true
[capability."context.repository"]
mode = "auto" # disabled | auto | required
[budget]
context_max_bytes = 16384
Capability kinds are context.repository, command.view, decision.evaluate,
model.generate and skill.catalog. For each, an explicit pin beats a user
preference, which beats the single compatible trusted installation, which beats
the built-in. required never falls back to a built-in. resolve writes
semaprax.harness.lock; --frozen fails on any difference. Permissions in an
adapter descriptor are requests. Grants live only in the harness home and are
bound to the descriptor, adapter and upstream digests, so a changed or widened
adapter loses its grant. Spec:
Harness provider host.
Adapters
Bundled under packages/semaprax-harness-adapters/ and embedded in the binary.
Each is a separate process on JSON-RPC over stdio, and none enters the compiler
build.
| Adapter | Capability | Needs |
|---|---|---|
graft, graphify | context.repository (orient, search, skeleton, references) | Your own installed Graft or Graphify. Graphify is opt-in. |
rtk, caveman | command.view (shorter command output) | RTK; Caveman is opt-in and needs a user-started local runtime. |
laya, jev, minijev-local, clef-local | decision.evaluate (model routing) | Their own runtimes. Experimental; no learned adapter is live-tested on the reference host. |
wikiskill | Skill evolution | A pinned community tool. Experimental. |
To write your own, start from the SDK in packages/semaprax-harness-adapters/sdk/
and check it with semaprax harness conformance. Spec:
Adapter SDK.
Routing models
Routing is policy-first. By default simple rules pick the route with zero router
calls. Modes are rules (default), pin, experimental and auto; auto is used only for a profile that passed
its qualification gate, and none has yet. Every report carries route.explain.
semaprax harness status --routing lists profiles; --check validates them
without cost, and --probe makes one announced, metered call. The current
support table is in
Harness decision.
Connect Claude Code
semaprax harness bridge . --setup claude-code # shows what it would write
semaprax harness bridge . --setup claude-code --write # writes the host settings
semaprax harness bridge . --stdio # protocol over stdin and stdout
The bridge is a host-side surface: LF-delimited JSON-RPC (semaprax.harness-bridge.v1)
that begins with a bridge/handshake. The outside agent keeps its own model.
Semaprax does not claim to route it, and the handshake says who owns the model.
Phase routing applies to Semaprax-owned worker requests only. The bridge adds no
authority to semantic methods. Spec:
Harness bridge.
Related
- Installed guidance for agents without the harness: Driving Semaprax from an AI agent.
- Serve a project to an agent over MCP: Shipping.
- Measure token use: Context performance.
- Trust limits of grants and evidence: What Semaprax verifies.
Specialist commands
After this page you know the commands that no other chapter teaches, what each one prints, and which ones are not products yet. Most people never need these; they exist for tool builders, CI authors and compiler contributors.
Every analysis below reads one source file, changes nothing, and prints one JSON
envelope with a schema, a digest and usually nonclaims that list what the
output does not say. The output is data and never grants permission. Add
--max-bytes N to cap it; an overflow fails instead of truncating.
Ask about one module
| Question | Command | What you get |
|---|---|---|
| What authority must a build grant this module? | capability-manifest <file> | The capabilities it needs. Only filesystem, home, network, process and secrets are admitted; a module that uses clock.read fails with SPX-K202. |
Which protocol declarations exist and what conforms? | protocol-check <file> | Declared protocols, signature rules and conformance. A file with none prints "protocols_total":0. |
| Which functions could be vectorized? | simd-report <file> | Eligibility facts for effect-free scalar functions. It emits no vector code. |
| What would typed generation add? | hygienic-gen <file> [--templates default-constructor,field-accessors] | Generated constructors and accessors, as output only. It never edits your file. |
| What does this function return for these arguments? | interpret <file> --function <name|id> --arg 1 --arg 2 | A JSON report of one interpreted call. interpret-strings does the same for functions that use strings. |
| How are values created and released? | region-report <file> | Lifetime structure. It adds no regions or arenas to the language. |
semaprax simd-report examples/meaning.spx
{"schema":"semaprax.simd-report.v1","digest":"sha256:0c7487af...","bytes":2549,"payload":{...}}
For proofs, property tests and assurance policy see Shipping and Laws and proofs.
Describe a module for other systems
| Command | Prints | Status |
|---|---|---|
plugin-manifest <file> | A read-only description of the module’s compiled exports. | It loads and runs nothing. There is no plugin system yet. |
ui-schema <file> | The schema and facts of the UI dialect for one module. | The UI runtime targets (iOS, Android, desktop, Linux) are not shipped. |
WIT and components. The compiler can derive a scalar WIT interface
(semaprax:project-scalar@1.0.0) from a project’s web exports, but only as a library
call. There is no command for it, and a Component binary exists only behind a private
feature that is off by default. For WebAssembly use the Core Wasm web package
(Targets). Specs:
Public scalar WIT,
private WIT boundary.
Java/Kotlin (JNI) and Swift/Apple bridges, and public generic signatures, stay private or unsupported in 0.9.0. The completion matrix gives each row’s status.
Serve one file
semaprax serve examples/meaning.spx
-> {"jsonrpc":"2.0","id":1,"method":"ping"}
<- {"jsonrpc":"2.0","id":1,"result":{"pong":true}}
-> {"jsonrpc":"2.0","id":2,"method":"context","params":{"symbol":"math.add","depth":1}}
<- {"jsonrpc":"2.0","id":2,"result":{"context":{"schema":"semaprax.agent-context.v1",...}}}
-> {"jsonrpc":"2.0","id":3,"method":"shutdown"}
<- {"jsonrpc":"2.0","id":3,"result":{"ok":true}}
serve checks one file once, then answers many newline-delimited JSON-RPC
requests on stdin and stdout. Methods: protocol, graph, context,
context_v2, ping, shutdown. Each result is byte-identical to the matching
CLI command. A request without an id is a notification and gets no reply. For a
whole project use semaprax service (Shipping).
Release archives also ship semapraxd. semapraxd --stdio [--manifest-path semaprax.toml] is the older project session: it authenticates one project once and
answers graph, context and test requests over the same framing. Prefer service
for new work. Specs:
Agent Transport,
Project Agent Transport v2.
@semaprax/agent-workflow (packages/semaprax-agent-workflow in a checkout) is a
Node package that drives one bounded workflow, review then publish a function
signature change, over a generated codec and an MCP transport your host supplies.
It opens no files, starts no processes and holds no secrets.
Keep derived data
These commands store analysis on disk. Inputs are explicit, and what they restore carries no authority.
| Commands | Purpose |
|---|---|
semantic-cache-* (eight commands) | Reuse checked project analysis across processes. Covered in Context performance. |
retention-metadata-inventory, -plan, -persist, -load | Decide which retained analysis subjects to keep, store the plan and its checkpoint, and restore them by exact digest. |
Spec: Retention metadata CLI.
Version and contributor gates
semaprax version --json
semaprax quality-plan quick
{"schema":"semaprax.version.v1","version":"0.9.0","commit":null,"maturity":"beta","rust_min":"1.88"}
commit is null for a build that recorded none. quality-plan quick|changed|full [changed-path ...] prints the gate list that the repository’s scripts/quality.sh
runs for that profile. It is for people changing the compiler.
Next: What Semaprax verifies and what it does not.
What Semaprax verifies and what it does not
After this page you can read the output of the registry, audit, release and evidence commands without claiming more than they prove.
The commands themselves are in Shipping. This page states their trust limits in one place. The common rule: evidence is data, not permission. A digest binds exact bytes. A passing check says those bytes match what was checked. It never authorizes a write, a publication or a signature.
Table of limits
| Surface | Checked | Not checked or not done |
|---|---|---|
registry commands | Digests bind subject bytes; names are not std.*; versions are immutable; duplicates refused (SPX-PKR602 to PKR605). | No network. No default registry or search path. No signature check: an entry’s signature.identity is an unverified claim, and a forged signature passes this layer. publish only prints what would result. |
fetch, resolve | Each subject is replayed before it is cached; selection uses only the local cache. | Nothing downloads. A build does not yet link resolved external dependencies. |
audit verify | Required object types per profile, associations, subject bindings, every object’s digest, role and revocation policy. | With no --trust-roster, signatures are checked against policy only, and the output says so. Semaprax signs nothing and contacts no transparency log. |
release verify | Manifest digest, provenance binding, every named archive re-hashed. With signed material, the Sigstore check offline against the trusted-root bytes you supplied. | A directory with no signed material prints VERIFIED UNSIGNED RELEASE. A pass does not say the roots are current or that you downloaded from an official place. |
doctor verify-release | The same, but it requires signed material and your --trusted-root-sha256. | The digest must come from a channel you trust. One copied from the release directory proves nothing (SPX-Z707 on a wrong one). |
verify, *-evidence | Evidence is replayed against exact source and patch bytes. | The result is proof data. Applying still needs the apply command, and a stale source (SPX-G409, SPX-G530) is refused. |
workflow dispatch | Whether a declared request is inside a declared allow-list. | It performs nothing. It records a decision. |
| Hot-reload plans | A candidate passes the full project check before it can swap in. | A plan has "authority": "none"; only activate swaps, between invocations. |
| Harness providers | Permission requests in a descriptor are checked against grants in your harness home, bound to the descriptor, adapter and upstream digests. | A changed or widened adapter loses its grant. Adapters do not get ambient PATH or $HOME access. |
Read these four words
authority:falseornone. The document is information. Having it changes nothing you may do.nonclaims. A list of things the producer states it does not establish. Read it before you quote the result.- Stale. The source or revision moved since the evidence was made. Re-run the producing command. Do not edit the evidence.
- Fail closed. The command stops with a code and writes nothing rather than guess.
What to say in a review
Say what the command proved, with its scope: “release verify re-hashed all five
archives against release-manifest.json”, not “the release is signed”. When a
capsule or release is unsigned, say unsigned. Local test evidence is local; hosted
or physical-device evidence needs its own record. The same honesty applies to
this handbook: where a feature is private, experimental or main-only, the page
says so.
Specs: Audit Capsule v1, Release signing policy, Registry trust.
Cheatsheet
The language and the command line of Semaprax 0.9.0 on one page. Each row links to the page that explains it.
The daily loop
Run these in your project directory.
semaprax fmt . # rewrite source to the one canonical form (--check only reports)
semaprax check . # parse, resolve, type-check, verify
semaprax test . # run the project's test functions
semaprax run . # run main and print its i64 result
semaprax build . --target web -o dist/web
fmt writes files; fmt --check does not. check never runs your code; run
and test do. For one file, semaprax fmt f.spx && semaprax run f.spx is the
whole loop. Add --json to check, run or test for one JSON object per
diagnostic.
Commands by task
Run semaprax help all for the full list, or see the
Command catalog.
| I want to | Command | Page |
|---|---|---|
| Start a project | semaprax new demo (--template library|service) | First project |
| Read a declaration’s meaning | doc <file>, query <project> --id <id>, context <input> <id> --depth 1 --max-bytes 4096, graph <file> | Explore |
| Find who calls what | query <project> --calls <id>, --called-by <id> | Explore |
| Look at a project visually | explore <manifest> --format html --output out.html | Explore |
| Change code by meaning | change preview, patch, impact, review | Shipping |
| Pin and compare an interface | lock . --write|--verify|--compare base.lock | Shipping |
| Build for a target | build . --target native|web|wasm|npm|oci | Targets |
| Check my toolchain or a download | doctor, version, release verify <dir> | Targets, Shipping |
| Reload code while editing | dev semaprax.toml --jsonl | Targets |
| Serve a project to an agent | service <project> [--mcp] | Shipping |
| Run an agent in a coding harness | harness setup, harness run, harness bridge | Harness |
| Run an agent or check its definition | agent inspect|run|replay | Agent programs |
| Get an error’s fix | help diagnostic SPX-T208 | Diagnostics |
| Look up a library function | help library compare | Standard library |
| See a language topic | help language topics, help language ownership | Essentials |
| Copy a declaration shape | help shapes record | Types |
The language at a glance
| Need | Spelling | Page |
|---|---|---|
| A file | module app.name; first, then declarations | Essentials |
| Stable identity | @id("app.name.fn") before every declaration | Essentials |
| Entry point | exactly fn main() -> i64 | Essentials |
| Result of a block | A tail expression, with no return and no trailing ; | Essentials |
| Bindings | let x = 1; immutable; let mut n = 0; then n = n + 1; | Essentials |
| Number types | i64 (default), i32, u8, usize, f64, f32; suffix 5i32, 3usize; operators never mix types | Types |
| Other scalars | bool, char ('a'), string (owned UTF-8), str (borrowed view) | Ownership |
| Conditional | if c { a } else { b }; always an expression, no else if | Essentials |
| Loop on a condition | while cond { ...; cond }; the last line is the continuation test | Loops |
| Loop over a vector | for item in values { ...; 0 } over an immutable Vec binding | Loops |
| Consume an iterator | for own item in it { ... }; match own on IterStep | Loops |
| Record | record P { @id("p.x") x: i64, }; build P { x: 1 }; update p with { x: 2 } | Types |
| Variant | cases Name, or Name { f: i64, }; build Shape::Dot {} | Types |
| Match | match v { Shape::Box { width: w } => w, _ => 0, }; guards n if n < 0; or-patterns -1 | -2 | Matching |
| Option and Result | Option<i64>::Some { value: 1 }; match Option::Some { value: v }; ? in a Result function | Matching |
| Class | class Dog : Animal { fn m(self: Dog) -> i64 { ... } }; call d.m(); super.m() | Classes |
| Generics | fn id<T>(v: T) -> T; call id<i64>(4) | Functions |
| Function values | fn(x: i64) -> i64 { x + 1 }; parameter f: fn(i64) -> i64 | Functions |
| Contracts | requires x >= 0 and ensures result >= 0 between signature and body | Contracts |
| Effects | permit { process.stdout.write } on the module, uses { ... } on each function | Contracts |
| Ownership | own T consumes; borrow T reads; a moved value cannot be reused | Ownership |
| Resources | resource R { drop trivial; } or drop import "host.symbol"; | Resources |
| Strings | string_concat(a, b); view string_as_str(binding); bytes str_as_bytes(view) | Built-ins |
| Vectors | vec_with_capacity<i64>(4usize); v = vec_push<i64>(v, 1); | Collections |
| Laws | @id("l.order") law relational (a: i64, b: i64) a <= b || b < a evidence smt_proved; | Laws |
| Session protocol | session protocol "name" { states {...} initial S; on S label: send T via "id" -> S2; } | semaprax help language |
| Import across files | use function @id("pkg.fn") from other.module as name; right after module | Modules |
| A test | fn test_add() -> i64 with an @id in a test module; return 0 to pass | Testing |
| Standard library | [dependencies] std.core = "^0.1.0" then use function @id("std.core.min") ... | Standard library |
Habits that fail: return, else if, x += 1, tuples, a[0], "a" + "b",
Some(1), i++, break, as casts, macros. The fix for each is in
Diagnostics.
A complete syntax reminder
module app.reminder;
@id("reminder.double")
fn double(value: i64) -> i64
requires value >= 0
ensures result == value * 2
{
value * 2
}
@id("app.main")
fn main() -> i64
{
let answer = double(21);
if answer == 42 { answer } else { 0 }
}
Reading a command synopsis
Angle brackets name values to replace, square brackets are optional parts, and |
means choose one. Do not paste the brackets into a shell. If a command says
<input>, give it a .spx file, a project directory or semaprax.toml.
Need a word explained? Open the glossary.
Command catalog
Every semaprax command in 0.9.0, grouped by what you want to do, with the page
that teaches it. semaprax help all prints the exact grammar of each command on
your installed build; semaprax help <command> prints one.
<input> means a .spx file, a project directory or semaprax.toml.
semaprax harness exists only in release archives and semaprax-full
(Harness); everything else is in the standalone build.
Write, check and run
| Command | Does | Page |
|---|---|---|
new <dest> [--name n] [--template calculator|library|service] | Creates a project in an unused directory. | First project |
project-scaffold --name n [--template t] [--layout frozen|tables] | Prints the same project as one JSON capsule; writes nothing. | Manifests |
check [<input>] [--json] | Parses, resolves, type-checks, verifies. | First program |
fmt <input> [--check] | Rewrites source to canonical form. | Style |
run <input> [--native] [--json] [--max-steps N] [--max-bytes N] | Runs main in the interpreter; --native runs a single file as C11. | First program |
test [<dir>|semaprax.toml] [--json] | Runs every test_ function in the project’s test modules. | Testing |
build <input> --target native|native-callable|web|wasm|npm|oci | Emits an artifact. | Targets |
network-run <project> --fixture f.json [--arg s] [--stdin p] | Runs a project against recorded network replies. | Input and output |
dev <semaprax.toml> --jsonl|--human | Hot-reload session for the interpreter. | Targets |
interpret, interpret-strings <file> --function f --arg v | Runs one function and prints a JSON report. | Specialist commands |
Inspect meaning
| Command | Does | Page |
|---|---|---|
graph <file> | The whole semantic graph as JSON. | Explore |
context <input> <id> [--depth N] [--max-bytes N] [--filters ...] | Bounded facts about one declaration. With --rust-index it answers about a Rust import. | Agents, Integrations |
doc <file> [--json] | Documentation generated from the graph. | Explore |
query <input> ... | Finds declarations by kind, name, id, effect or call edge. Subforms: declarations, symbol, context, impact, available-operations, --capabilities. | Agents, Shipping |
explore <manifest> --format html|json|markdown|svg --output p | A visual project map. | Explore |
compact graph|agent-definition|context|task-context|api-surface|candidate-diff | Smaller replayable encodings of the same answers. | Context performance |
context-benchmark <manifest> | Measures the context size of maintenance questions. | Context performance |
explain <SPX-code> [--json] | Confirms a code exists in this compiler. | Diagnostics |
help [<command>|all|language [topic]|library [name]|shapes [kind]|diagnostic [code]] | Offline help. | Debugging |
skills get <agent|language|graph|stdlib|packages|effects> | Version-matched agent guides as JSON. | Agents |
fix --plan, repairs, repair | Plan, then apply, the one repair offered (add a missing @id). | Debugging |
Change by meaning
| Command | Does | Page |
|---|---|---|
impact, review, patch <file> <patch.spatch> | Preview, review and apply a one-file patch. | Shipping |
patch-evidence[-v2], verify-patch-evidence[-v2], patch-with-evidence[-v2], target-evidence | Produce, verify and apply with replayed evidence. | Shipping |
change preview|rebase|merge <project> ... | Semantic changes to a project. | Shipping |
review <project> <transaction.json> [--evidence] | Reviews a transaction file. | Shipping |
verify <subject> <change> <capsule.json> | Replays an evidence capsule; the schema picks the verifier. | Shipping |
patch-receipt <project> render|verify|refusal|verify-refusal|compare|evidence-summary|evidence-page | Receipts for transactions. | Shipping |
semantic-workspace-init, workspace-snapshot|graph|context|impact|review | Read a managed workspace of 2 to 32 files. | Shipping |
semantic-workspace-change-preview, semantic-workspace-change-evidence, verify-semantic-workspace-change-evidence, apply-semantic-workspace-change-evidence | Preview, evidence, verify, apply a workspace change. | Shipping |
semantic-workspace-structural-change-preview, semantic-workspace-structural-change-evidence, verify-semantic-workspace-structural-change-evidence, apply-semantic-workspace-structural-change-evidence | The same four steps for structural changes. | Shipping |
semantic-workspace-operations-derive, semantic-workspace-operations-change-proposal, semantic-workspace-operations-evidence, verify-semantic-workspace-operations-evidence, apply-semantic-workspace-operations-evidence | Derive operations, then evidence, verify, apply. | Shipping |
workspace-init|preview|apply|patch-evidence, verify-workspace-patch-evidence, workspace-apply-with-evidence | The older .wspatch route. | Shipping |
project-image, project-image-store, project-image-load, project-image-verify, project-symbol | Disposable semantic images. | Shipping |
project-candidate-preview, project-candidate-export, project-candidate-restore, project-candidate-persist, project-candidate-load, project-draft-persist, project-draft-load, project-candidate-git-publish | Candidates, drafts and local Git publication. | Shipping |
hygienic-gen <file> | Prints generated constructors and accessors. | Specialist commands |
Serve
| Command | Does | Page |
|---|---|---|
serve <file> | One file over JSON-RPC. | Specialist commands |
service <project> [--mcp] | One project over JSON-RPC or MCP. | Shipping |
serve-image, serve-candidates, serve-test-candidates, serve-diagnostics, serve-diagnostics-tested <manifest> | Image protocol v1 to v4. | Shipping |
serve-workspace, serve-workspace-mcp <manifest> <host-policy.json> | Image protocol v5 under a host policy. | Shipping |
Agents
| Command | Does | Page |
|---|---|---|
agent inspect <definition.json> [--profile] | Prints an agent definition’s graph. | Agent programs |
agent run <definition> <task> <transcript> [--evidence|--trace] | Runs an agent against a recorded transcript. | Agent programs |
agent replay <definition> <task> <transcript> <evidence> | Replays and checks evidence. | Recovery |
agent skill [--require-schema s] | Prints the installed agent skill. | Agents |
verify <definition> <profile> <graph>, verify <manifest> <image.json> | Replays agent and image evidence. | Agent programs |
workflow inspect|validate|checkpoint|dispatch | Reads typed workflow files; runs nothing. | Shipping |
harness <verb> | The development harness (archive and full build). | Harness |
Package, lock and ship
| Command | Does | Page |
|---|---|---|
add <dir> <package> <range> | Adds a dependency row. | Shipping |
lock [<input>] --write|--verify|--compare f|--emit-interface|--compare-interface f | Pins and compares a project. | Shipping |
resolve <input> --target native64|wasm32 --cache dir --write|--verify | Pins per-target dependency choices. | Shipping |
fetch [--lock l] <cache> <subject.json>... | Fills a local cache from subjects you hold. | Shipping |
package report|lock|resolve (aliases package-report, package-lock, package-resolve) | Package descriptor, lock and resolution for explicit inputs. | Shipping |
registry search|add|lock|fetch|verify|publish | Offline registry document operations. | Shipping |
audit inspect|verify|diff | Audit capsules. | Shipping |
release verify <dir>, doctor verify-release <dir> --trusted-root-sha256 h | Verifies a downloaded release offline. | Shipping |
How far each of these is trusted: What Semaprax verifies.
Interfaces and analyses
| Command | Does | Page |
|---|---|---|
openapi, openapi-compat, c-header, abi-report, cxx-shim, cxx-package, freestanding-object | Descriptions and headers for other systems. | Integrations |
plugin-manifest, ui-schema | Read-only module descriptions. | Specialist commands |
capability-manifest, protocol-check, simd-report, region-report | Read-only analyses. | Specialist commands |
assurance-policy, assurance-diff, assurance-manifest, project-assurance-manifest, properties, project-proof-check | Proof and evidence accounting. | Shipping, Laws |
semantic-cache-* | Reuse checked analysis across processes. | Context performance |
retention-metadata-inventory, retention-metadata-plan, retention-metadata-persist, retention-metadata-load | Plan and store retained-analysis metadata. | Specialist commands |
Toolchain
| Command | Does | Page |
|---|---|---|
doctor [--profile id] [--target native|web|all] [--json] | Reports the toolchain, offline. | Targets |
version [--json], --version | Version and maturity. | Specialist commands |
quality-plan quick|changed|full | Prints the contributor gate plan. | Specialist commands |
Glossary
Use this page when a word stops you. Each entry says how the handbook uses the term and, where useful, where to learn more.
| Term | Meaning |
|---|---|
| ABI | The agreement about values, ownership, failure and calling conventions across a compiled interface. |
| Adapter | A separate program that gives the harness one capability, such as repository context or command-output views. |
| Agent | A program with explicit task, state, proposal, authorization, operation and result roles. See Agent programs. |
| Artifact | A produced file or package, such as a binary, web package or OCI layout. |
| Audit capsule | One JSON manifest of digests tying together the evidence for a decision. See Shipping. |
| Authority | Permission to do something. A document with "authority": false or none grants none. |
| Backend | What executes or lowers checked code: the interpreter, the C11 native route or the Core Wasm route. |
| Binding | A name attached to a value, as in let count = 3;. |
| Borrow | Temporary read access to a value without taking ownership of it. |
| Bridge | The harness surface that lets an outside coding agent, such as Claude Code, use the harness. |
| Candidate | A proposed project revision, kept as data, that can be inspected, tested and stored before anyone publishes it. |
| Canonical | The one representation a format’s rules select. fmt writes canonical source. |
| Capability | Explicit authority supplied for one operation. In the harness, a kind of service such as context.repository. |
| Capsule | A package of revision-bound data for inspection or replay. |
| Checkpoint | Saved execution state used for recovery. |
| Class | A type with fields and methods that can inherit from another class. Records have no methods. |
| Cleanup | Releasing owned values in the checked order when their lifetimes end. |
| Contract | A function’s requires and ensures clauses. |
| Copy scalar | A basic value such as an integer or boolean, copied without consuming its owner. |
| Declaration | A definition that introduces a named thing: function, type, field, law. |
| Diagnostic | A compiler message with a stable SPX-... code. See Diagnostics. |
| Digest | A hash identifying exact bytes. A digest is never permission by itself. |
| Doctor | semaprax doctor, the offline toolchain report. It never searches PATH. |
| Draft | A candidate that is not finished, stored so you can resume it. |
| Effect | An operation category a function declares with uses, such as process.stdout.write. A module allows effects with permit. |
| Entry point | The function where execution starts: fn main() -> i64. |
| Evidence | Data produced or checked for one claim about one subject and revision. It carries no authority. |
| Export | A declaration made available through a package interface. |
| Fail closed | Stop with a code and change nothing, instead of guessing. |
| Fixture | Fixed test input, or a controlled stand-in, used to make a run repeatable. |
| Generation | One complete immutable published state of a managed workspace. |
| Harness | Optional tooling that runs an agent-proposed repair through compiler checks. See Harness. |
| Hot reload | Swapping a checked revision into a running interpreter session between calls (semaprax dev). |
| HIR | The compiler’s high-level representation after names and types are resolved. |
| Host | The environment that supplies runtime services, tools, storage or operation handlers. |
| Image | A disposable semantic summary derived from a project. It is never source. |
| Immutable | Not reassigned through the binding in question. |
| Import | A declaration selected from another module with use function @id("...") from ... as ...;. |
| Interface | A declaration of host operations (import fn) with their effects and failure mode. |
| Journal | An ordered record of progress used for recovery. |
| JSON-RPC | The request and response framing the servers use, one JSON object per line. |
| Law | A named rule tracked independently of any implementation. See Laws and proofs. |
| LawSet | The selected laws and evidence requirements a project must account for. |
| Lock | semaprax.lock: the pinned identity, digests and interface of a project. |
| Manifest | semaprax.toml: a project’s modules, tests, exports and dependencies. |
| MCP | Model Context Protocol, a standard way for an assistant to call tools. service --mcp offers one. |
| Module | A named group of declarations. A .spx file begins with its module line. |
| Move | Transfer ownership so the old binding cannot be used. |
| Nonclaims | A list in a report of what it does not establish. |
| Owned value | A value with one tracked owner responsible for its transfer and cleanup. |
| Patch | A .spatch file naming a graph revision and edits by stable id. |
| Postcondition | A promise about a result, written with ensures. |
| Precondition | A requirement on inputs, written with requires. |
| Profile | The rules a project selects for types, ownership, execution or packaging, such as scalar or useful-data.v1. See Profiles. |
| Proposal | Typed input describing a requested action, before authorization. |
| Provider | In the harness, an adapter that supplies a capability. In network-run, the host side that answers network calls. |
| Reducer | Checked logic that combines state and an outcome to choose the next agent step. |
| Registry | A file listing packages and versions. Semaprax reads it offline. |
| Replay | Rechecking retained data against the subject and rules that give it meaning. |
| Resource | A value with a declared end of life, such as a handle. |
| Revision | The identity of one source or project snapshot, as a sha256: digest. |
| Scalar | One basic value: a number, boolean or character. |
| Semantic graph | Structured facts about declarations, types, effects, contracts and relationships. |
| Session protocol | A declared state machine for an interaction, checked and then erased. |
| Skill | Passive instruction text for an agent. It is data, not code. |
| Stable ID | The persistent identity written with @id, separate from the display name. |
| Stale | Based on an older revision than the current source. Stale input is refused. |
| Subject | The exact thing an evidence document is about, such as a package or a patch. |
| Tail expression | The last expression of a block, which gives the block its value. |
| Target | The selected output form: native, web, wasm, npm or oci. |
| Transaction | A canonical, revision-bound set of semantic edits that is validated before it is applied. |
| Typed hole | A marked incomplete part of a candidate with a known type, filled later under checks. |
| UTF-8 | The byte encoding of text. One character can take more than one byte. |
| Variant | A type whose value is one of several named cases. |
| Workspace | Several .spx files read or changed together as one managed set. |
Return to: Essentials, project profiles or Agent programs.
Standard library
The standard library ships inside the compiler: no checkout, cache or network. You add a dependency, import by stable id, and set the project profile the package requires.
Find a function
Do not memorize names. Ask the installed compiler:
semaprax help library # the whole catalog, offline
semaprax help library compare # one entry, about 200 bytes
semaprax help library std.core.compare # by stable id
An entry shows the stable id, dependency row, required profile, signature,
effects and contracts. The generated catalog is
Standard library catalog;
the machine-readable form is std/catalog.json.
Use a package in three steps
# 1. semaprax.toml: depend on the package
[dependencies]
std.core = "^0.1.0"
# 2. set the profile the entry requires (omit it for `scalar`)
[package]
profile = "owned-data-api.v1"
// 3. import by stable id, right after the module line
module app.using_std;
use function @id("std.core.min") from std.core as min;
Every bundled package is at version 0.1.0. An unknown package or an
unsatisfied range fails with SPX-J121. If you call a standard-library name
without importing it, the error prints the exact dependency and use lines to
add (for example for min). Spell the type argument of generic calls
(vec_push<i64>(...)). Pick the profile with
Choose a project profile.
Every package
47 packages, all marked partial in the catalog: each works on the interpreter,
native C11 and Core Wasm for its listed profile, and each has a conformance
module. “Functions” counts declarations in the catalog entry.
| Package | Profile | Functions | What it gives you |
|---|---|---|---|
std.agent | owned-data-api.v1 | 17 | Task, Context, Observation and Outcome records, plus stage-transition and retry helpers for agent loops. |
std.async | useful-data.v1 | 6 | Wait and retry arithmetic: clamp a wait, next timeout, remaining time, stream end. |
std.auth | owned-data-api.v1 | 48 | Secret, Identity and Authorization records, constant-time byte compare, session-state rules (expiry, rotation). |
std.bytes | useful-data.v1 | 20 | Byte-slice helpers: get_or, index_of, count, prefix and suffix tests, u16/u32 reads, trimming, fields. |
std.collections | owned-data-api.v1 | 8 | Bounded Vec operations over Copy scalars: with_capacity, push, len, capacity, get, set, clear, reserve_exact. |
std.core | none (scalar) | 12 | compare, min, max, clamp, in_range, bool and i64 conversion, xor, implies. |
std.data.csv | useful-data.v1 | 8 | CSV field scanning, quote balance and record well-formedness. |
std.data.json | useful-data.v1 | 12 | JSON scanning primitives: whitespace, hex, escapes, string ends, failure offsets. |
std.data.json.dec | owned-data-api.v1 | 27 | Decode JSON strings (escapes, UTF-8) into an owned buffer and compare decoded tokens. |
std.data.json.digits | none (scalar) | 5 | Decimal digit helpers for JSON numbers. |
std.data.json.doc | useful-data.v1 | 19 | Whole-document JSON structure: document end, key iteration, key uniqueness. |
std.data.json.token | useful-data.v1 | 13 | JSON number and literal tokens: integer, fraction and exponent ends, true/false/null. |
std.data.json.utf8 | useful-data.v1 | 11 | UTF-8 validation for JSON text. |
std.data.json.write | useful-data.v2 | 16 | Write JSON: quoted strings, escapes and decimal numbers into a buffer. |
std.data.toml | useful-data.v1 | 16 | TOML scanning: bare and quoted keys, values, comments, failures. |
std.db | useful-data.v1 | 18 | Database-access rules: descriptor matching, safe identifiers, transaction state machine. |
std.email | useful-data.v2 | 32 | Email address, header and envelope validation. |
std.encoding | none (scalar) | 10 | Hex and Base64 digit encode and decode helpers. |
std.encoding.base64 | owned-data-api.v1 | 3 | Base64 length and byte access for an owned buffer. |
std.env | environment-io.v1 | 7 | Read process environment entries: count, name, value. |
std.env.policy | owned-data-api.v1 | 12 | Validity rules for environment variable names and assignments. |
std.export.policy | useful-data.v2 | 9 | Admission rules for export batches: sizes, target ids, queue depth, backoff. |
std.format | useful-data.v2 | 14 | Build text in a buffer: append str, usize, i64 and bool, with padding. |
std.fs | filesystem-io.v3 | 22 | Typed Path, FileInfo and WriteOutcome over the fs.* effects: read, write, metadata, list, create, remove, atomic write. |
std.http | useful-data.v1 | 58 | HTTP/1.1 message parsing: status, headers, Content-Length, method and token validity. |
std.io | owned-data-api.v1 | 13 | Reader and Writer cursors over byte buffers. |
std.io.lines | owned-data-api.v1 | 7 | Line-oriented reading over a Reader. |
std.jobs | useful-data.v1 | 29 | Durable-job state machine: states, leases, claim, heartbeat, retry and dead-letter rules. |
std.log | useful-data.v2 | 27 | Structured JSON log events with levels and guarded append. |
std.log.redact | useful-data.v2 | 13 | Log redaction policy: protected field names and safe events. |
std.mem | owned-data-api.v1 | 3 | Owned Box: new, get, into_inner. |
std.metrics | useful-data.v2 | 44 | Counters, gauges, histogram observation, label and cardinality rules. |
std.net | useful-data.v1 | 24 | Network value checks: ports, hosts, IPv4 classes (loopback, private, link-local), wait results. |
std.num | none (scalar) | 15 | abs, sign, gcd, pow, isqrt, div_euclid, rem_euclid, digit_count, log2_floor, log10_floor. |
std.num.overflow | none (scalar) | 13 | Overflow detection plus wrapping and saturating add, sub, neg, mul. |
std.path | useful-data.v1 | 6 | Path text queries: absolute, segments, file name, parent, extension. |
std.path.normalize | owned-data-api.v1 | 17 | Normalize a path (resolve . and ..) into an owned buffer. |
std.path.value | owned-data-api.v1 | 16 | Owned Path value: validation, join, parent. |
std.process | process-io.v1 | 29 | Argv and Output records and run for subprocesses. |
std.random | none (scalar) | 4 | Deterministic seeded generator: next_seed, sample_below. |
std.test | none (scalar) | 10 | Assertion helpers: equal_i64, equal_bool, failure bit sets. |
std.test.bytes | useful-data.v2 | 8 | Byte assertions and snapshot comparison. |
std.text | useful-text-consumer.v1 | 5 | Byte length, contains, equals, is_empty, starts_with over borrowed text. |
std.time | none (scalar) | 8 | Millisecond and second arithmetic: deadlines, remaining and elapsed time. |
std.tracing | useful-data.v2 | 42 | W3C traceparent and tracestate validation. |
std.url | none (scalar) | 5 | URL scheme and percent-encoding byte predicates. |
std.webhook | useful-data.v2 | 23 | Webhook admission: replay window, signature shape, delivery, backoff and idempotency rules. |
scalar packages work in any project. Other profiles gate what a project
may contain, for example std.fs needs filesystem-io.v3 and std.process
needs process-io.v1, and those also need the matching effect and a provider
that grants it (Input and output).
Traps
- Mutators thread the owner. Write
let next = vec_push<i64>(values, x);; there is no in-place mutation. - Most
std.*packages are rule and parsing helpers over byte slices and scalars, not high-level clients.std.httpparses messages; it does not open connections. For network access use the compiler-ownednet_*andhttps_*functions (Built-in functions). - Compiler-owned functions (
string_len,byte_get,stdout_write,box_new) are not in the standard library. They are reserved names in every file. - A stable id is
<package>.<name>, for examplestd.core.min.
Contract: Standard Library v1.
Built-in functions
Compiler-owned functions are reserved names available in every file: no import,
no dependency. Declaring your own string_len fails with SPX-S113. Spell
every type argument. Every borrowed view takes a plain let binding.
Print the same table from your compiler with semaprax help language builtins.
For library functions you import, see Standard library.
Strings
| Function | Signature |
|---|---|
string_len, string_len_chars | (s: string) -> i64: bytes, or Unicode scalars |
string_is_empty | (s: string) -> bool |
string_concat | (a: string, b: string) -> string; consumes both |
string_starts_with, string_contains | (s: string, other: string) -> bool |
string_from_char | (c: char) -> string |
string_from_i64 | (value: i64) -> string; canonical decimal text |
string_from_usize | (value: usize) -> string; canonical decimal text |
string_as_str | (binding: string) -> borrow str; a let binding, never a literal |
String views
| Function | Signature |
|---|---|
str_len_bytes | (s: borrow str) -> i64 |
str_is_empty | (s: borrow str) -> bool |
str_starts_with, str_contains | (s: borrow str, other: borrow str) -> bool |
str_as_bytes | (s: borrow str) -> Slice<u8> |
Bytes and buffers
| Function | Signature |
|---|---|
byte_len | (v: borrow Slice<u8>) -> usize |
byte_get | (v: borrow Slice<u8>, i: usize) -> Option<u8> |
byte_range | (v: borrow Slice<u8>, start: usize, end: usize) -> Slice<u8> |
bytes_copy | (v: borrow Slice<u8>) -> Bytes |
bytes_zeroed | (count: usize) -> Bytes; literal capacity |
bytes_set | (b: own Bytes, i: usize, v: u8) -> Bytes; write-once chain |
bytes_as_slice | (b: borrow Bytes) -> Slice<u8> |
array_as_slice | (a: borrow [u8; N]) -> Slice<u8> |
Vectors, iterators and boxes
T is one of the eight Copy scalars (i64, i32, u8, usize, f64, f32,
bool, char).
| Function | Signature |
|---|---|
vec_with_capacity<T> | (usize) -> Vec<T> |
vec_push<T> | (own Vec<T>, T) -> Vec<T>; thread the owner. Pushing past capacity fails at run time. |
vec_set<T> | (own Vec<T>, index: usize, value: T) -> Vec<T> |
vec_reserve_exact<T> | (own Vec<T>, additional: usize) -> Vec<T> |
vec_clear<T> | (own Vec<T>) -> Vec<T> |
vec_len<T>, vec_capacity<T> | (borrow Vec<T>) -> usize |
vec_get<T> | (borrow Vec<T>, usize) -> T |
vec_into_iter<T> | (own Vec<T>) -> Iter<T> |
iter_next<T> | (own Iter<T>) -> IterStep<T>; match with match own on IterStep::Done {} and IterStep::Yield { item, rest } |
box_new<T> | (value: T) -> Box<T> |
box_get<T> | (value: borrow Box<T>) -> T |
box_into_inner<T> | (value: own Box<T>) -> T; consuming |
Walk a vector with for item in values { ... } or consume an iterator with
for own item in iterator { ... } (Loops).
Command I/O
Each needs its effect in permit and uses.
| Function | Signature | Effect |
|---|---|---|
stdout_write, stderr_write | (v: borrow Slice<u8>) -> usize | process.stdout.write, process.stderr.write |
stdout_append, stderr_append | (v: borrow Slice<u8>) -> usize; cumulative, 65,536 bytes shared | same |
args_len | () -> usize | process.args.read |
arg_utf8 | (i: usize) -> borrow str | process.args.read |
stdin_read | () -> own Bytes | process.stdin.read |
Single-file run admits stdout_write only. args_len, arg_utf8,
stdin_read and stderr_write need a project with the useful-data-command.v1
profile on the native target. stdout_append and stderr_append belong to
the line-command profile (line-command-io.v1).
Filesystem
Paths are bounded relative byte prefixes against a provider root the host injects. Writes create new files unless you use the atomic forms.
| Function | Signature | Effect |
|---|---|---|
file_read | (path: borrow Slice<u8>, length: usize, max: usize) -> own Bytes | fs.read |
file_write_new | (path, length, data: borrow Slice<u8>, data_length: usize) -> usize | fs.write |
file_stat, file_create_dir, file_remove | (path: borrow Slice<u8>, length: usize) -> usize | fs.read or fs.write |
file_list | (path, length, max: usize) -> own Bytes | fs.read |
file_write_atomic | (path, length, data, data_length) -> usize | fs.write |
file_write_atomic_checked | like file_write_atomic, but returns a classified outcome (not published, published or uncertain) instead of aborting. Use std.fs.write_atomic_checked. | fs.write |
Network
Effect-gated operations that run only through a provider the host injects. A
handle is a usize token valid for one invocation, at most 8 open at once.
| Function | Effect | Notes |
|---|---|---|
net_connect(host, port) | network.connect | TCP client; returns a handle |
net_send(handle, bytes) | network.write | blocking full write |
net_recv(handle, max) | network.read | owned result; not allowed in while bodies (SPX-T270) |
net_stream_stdout(handle, max) | network.read and process.stdout.write | appends to the stdout transcript |
net_wait(handle, ms) | network.read | 0 timeout, 1 readable, 2 peer closed |
net_close(handle) | network.connect | settles the handle |
net_tls_connect(host, port) | network.tls | authenticated TLS client |
net_listen(host, port), net_accept(listener), net_close_listener(listener) | network.listen, network.accept | explicit listener lifecycle |
net_tls_accept(listener) | network.accept and network.tls | TLS server side |
https_get(url, max) | network.http | (borrow Slice<u8>, usize) -> own Bytes; whole response |
https_post(url, body, max) | network.http | HTTPS only; no redirects; the host sets an origin allow-list |
The TLS and listener operations run only through the hosted provider on the
interpreter; native and Wasm builds reject them before emission. https_get and
https_post belong to the https-command-io.v1 profile. Try the network
operations against recorded replies with
semaprax network-run <project> --fixture fixture.json
(Input and output).
Usage patterns for each family are in Ownership, Collections and Input and output. Specs: Network I/O, Network services, HTTPS client I/O.
Diagnostics reference
Every diagnostic carries a stable code, SPX- plus a family letter and digits.
Match on the code, not the message wording. The letter says which part of the
toolchain refused your input.
error[SPX-U103]: `mut` is only allowed on local `let` bindings; parameters are immutable at u103.spx:4:6
help: drop `mut` and copy the parameter into a new mutable local: `let mut current = <parameter>;`
The help: line is usually the fix. Fix the first diagnostic first; later ones
often follow from it.
Look a code up
| You want | Command |
|---|---|
| The fix for a common mistake | semaprax help diagnostic SPX-T208 prints what you wrote and the fix |
| The list of codes with an indexed fix | semaprax help diagnostic codes (also bare semaprax help diagnostic) |
| Whether this compiler emits a code | semaprax explain SPX-T208 [--json] prints the family and how many places emit it |
| The exact grammar of a command | semaprax help <command> |
| One diagnostic per line for tools | add --json to check, build, run or test |
help diagnostic indexes 24 codes, the mistakes people make most. For every other code,
read the message and its help: line, then the spec for the feature. Codes are
case-sensitive.
Common mistakes and fixes
Habits from other languages cause most first errors. The full habit table with
runnable examples is in semaprax help language mistakes-code.
| Code | You wrote or hit | Fix |
|---|---|---|
SPX-P003 | 9223372036854775808 or -(9223372036854775808) | Write the minimum as one literal: -9223372036854775808 |
SPX-P104 | struct, enum, pub, const | record, variant; there is no visibility keyword |
SPX-P105, SPX-P106 | return, else if, for i in 0..n, tuples, f(x);, a[0], fn f(), c ? a : b, break, as, a last field or arm without , | Use the tail expression, nested else { if ... }, while, a record, let _ = f(x);, byte_get(...), if; end every field and arm with , |
SPX-P130 | own fn with parameters | An owning closure takes none |
SPX-P201 | x += 1, a Rust or JavaScript closure | x = x + 1;; fn(x: i64) -> i64 { x + 1 } |
SPX-P203 | A block with no final expression | End a while body with its continuation condition; end an if branch with a value |
SPX-P207 | Nesting deeper than 128 | Extract a named helper |
SPX-S103 | A declaration without @id (warning) | Add @id("your.name"); semaprax fix --plan can plan it |
SPX-S113 | Your own string_len | Built-in names are reserved; rename yours |
SPX-T001, SPX-T281 | String, int, an unsupported Vec element type | string, i64, i32, u8, usize |
SPX-T104 | fn main() -> bool | Exactly fn main() -> i64 |
SPX-T202, SPX-T203 | Some(1), None, s.len(), a method on a record | Option<i64>::Some { value: 1 }; string_len(s); only classes have methods |
SPX-T205 | An owned value or literal where a borrow str goes | Bind it, then pass string_as_str(binding) |
SPX-T207, SPX-T208 | index + 1 with a usize, or two different integer types | The message names both types. Suffix the literal: index + 1usize |
SPX-T209 | let x = x + 1; reusing a name | No shadowing; pick a new name |
SPX-T213 | A record literal missing a field | Name every field |
SPX-T218 | f()? outside a Result function | match the result in main |
SPX-T221, SPX-T225 | Option::Some { ... } to construct, or identity(4) | Option<i64>::Some { ... }; identity<i64>(4). Matching omits the arguments. |
SPX-T232 | let a: i32 = 5 | 5i32 |
SPX-T250 | "a" + "b", string_concat("n=", 5) | string_concat(a, b); use string_from_i64(5) for numbers |
SPX-T252, SPX-T258 | A record built in a while body or yielded from a match arm | Compute scalars in the loop and build after; or build with if |
SPX-T257 | A scalar match with no catch-all | Add a final _ arm without a guard |
SPX-T262 | [1, 2, 3] | Array literals hold bytes ([1u8, 2u8]); use a Vec<i64> for numbers |
SPX-T263, SPX-T266 | str_as_bytes(text) on an owned string, string_as_str("lit") | let view = string_as_str(text); str_as_bytes(view); bind a literal first |
SPX-T265, SPX-T267, SPX-T271, SPX-T272 | Buffer misuse | No live view across a replacement; bytes_zeroed outside loops; do not re-open a named buffer; keep the index in range |
SPX-T270 | net_recv in a while body | Receive outside the loop |
SPX-T284 | A for over a let mut vector | Move the finished vector into an immutable binding first |
SPX-T288, SPX-T291 | Nested generic-collection closures; owning closures in generic functions | Name a helper; keep the closure out of the generic |
SPX-O101 | Using a moved string or Bytes | The callee takes borrow, or pass a fresh value or a copy |
SPX-O116 | A function returning str | Return an owned string |
SPX-U101 | Assigning an immutable binding | let mut first |
SPX-U103 | mut on a parameter | Copy it into a new let mut local |
SPX-E101, SPX-E102 | Missing permit or uses | permit { ... } at module level, uses { ... } on the function and its callers. The message names both edits. |
SPX-F102 | run refused to admit the program | Try run --native, or build the project |
SPX-G170 | use std::io; | Built-ins need no import; import one declaration with use function @id("...") from module as name; |
SPX-G174 | A rich type in a project function signature | Keep records module-local; cross boundaries with Copy scalars |
SPX-B104 | run on a module with resource | check it, or run through a native or Wasm project build, or run <file> --native |
SPX-J100 | Non-canonical manifest | The help names the first differing line |
SPX-J120, SPX-J121, SPX-J122 | Unknown manifest key; bad dependency; target outside the matrix | Remove or fix the key; bundled std.* at 0.1.0 with a satisfied range; build a listed target |
SPX-J102 | A path alias given to a writing fmt | Use the real path |
An unknown function name that a project module or the standard library provides
gets the exact use function @id("...") from ... as ...; line and, for the
library, the dependency to add. A string_concat("n=", 5) or a wrong integer
width names the fix.
Which part refused it?
| Letter | Area | Where to read next |
|---|---|---|
P | Parsing, plus size limits of reports | Essentials |
S, J | Identities and declarations; manifests | Manifests |
T, O, U, E, M, N | Types, ownership, mutation, effects, matches, unsafe boundaries | Types, Ownership, Contracts and effects |
K | Session protocols (K1xx) and capability manifests (K2xx) | semaprax help language (the full card) |
F, B, H | Interpreter admission, backends, replay of retained HIR | Targets |
G | Projects, graphs, patches and workspaces: G409 stale patch, G530 stale revision, G150 wrong workspace kind | Shipping |
I | Workspace, candidate and agent-runtime I/O | Agent programs |
W | Wasm and web export profiles (W115 signature outside the profile) | Targets |
A, D, X, Y, Q, V | ABI report, C header, C++ shim, hygienic generation, plugin manifest and verify front, SIMD report | Integrations, Specialist commands |
L, PKR, Z926, Z927 | Package locks, registry rules, registry reads | Shipping |
Z70x | Release verification (Z701 shape, Z702 binding, Z703 identity, Z704 artifact, Z705 missing, Z707 cryptographic) | Shipping |
HP + letter | Harness: HPB config and lock, HPD run pipeline, HPE context, HPJ routing, HPM skills, HPN bridge | Harness |
Other families exist for specialist features (generic ownership, WIT, laws).
semaprax explain <code> tells you if your compiler emits a code and which family
it belongs to.
The debugging workflow around these codes (fmt first, fix the first
diagnostic, read the help: line) is in Debugging.
Implementation map and handbook maintenance
Use this page to connect a handbook explanation to the code behind it, and to keep the handbook complete when a release adds commands or features.
Source baseline
This edition describes Semaprax 0.9.0, tag
v0.9.0. Implementation links
below point at that tag. Topic guides link to the living specifications on
main. When you reproduce a result, record the version and the commit.
Follow a source file through the compiler
.spx source → parse → resolve and check → checked HIR → cleanup plan
↓
queries / interpreter / target build
Parsing reads structure. Resolution names declarations and types. HIR records the checked program. Cleanup planning decides how owned values and resources settle. Queries and execution routes read those results for their own jobs. A source program, a semantic report, an approved operation and an executable package are different objects: keep the producing command and revision with each.
Find the code for a topic
| Topic | Code and spec | Handbook page |
|---|---|---|
| Commands and flags | CLI catalog, dispatch | Command catalog |
| Single-file run and stdout | Source execution | First program |
| Project creation | Project creator | First project |
| Linked functions and profiles | HIR linker | Profiles |
| Law declarations and proof binding | Law parser, proof binding, example manifest | Laws |
| Agent runtime, recovery, migration | Runtime v2, lifecycle | Agent programs, Recovery |
| Rust API index and bindings | Index, binding, builder | Integrations |
| Token reports | Report helper, measurement | Context performance |
| Semantic cache | Cache contract | Context performance |
| Registry front | Registry CLI, registry rules | Shipping, Trust |
| Audit capsules and workflows | Capsule, workflow engine | Shipping, Trust |
| Harness | Harness crate, adapters | Harness |
| Editor | Extension guide | VS Code |
| Documentation tests | Harness, examples | Testing |
The architecture map has the full module map. The completion matrix and quality gates say what is implemented and how it is tested. Read a gate for its named subject and target; a module name does not prove a test result.
Write pages that stay true
- Open each page with what the reader can do after it. Lead with a runnable example, then explain it.
- Define a term at first use and link the glossary.
- Give a runnable example a file name, a working directory, a command and its output. Keep command templates in their own block and say which parts to replace.
- Label private, experimental, preview and main-only features as such.
- When source changes, follow the change through the parser, the checker, the runner and the target. Never change compiler behavior to make an example fit.
Run the handbook checks
Python 3.10 or newer, from the repository root:
python3 scripts/test-check-handbook.py
python3 scripts/check-handbook.py --structure-only
python3 scripts/check-handbook.py --compiler /absolute/path/to/semaprax
The structure check validates local links, that every page has one SUMMARY
entry, fence closure and example markers. The compiler-backed run also formats
temporary copies of marked modules, checks and runs them, and compares the output.
Only blocks marked handbook-smoke or handbook-project-file run. Shell fences
never do. Marked examples must pass in the interpreter, so a program that needs
--native stays unmarked.
Keep the handbook complete for each release
Run this audit before each release. A release is not documented until every public command and every user-visible feature has a page that is true for that version.
1. Commands. List what the binary accepts and find names no page mentions:
semaprax help all | grep '^semaprax ' | cut -d' ' -f2 | sort -u > commands.txt
for c in $(cat commands.txt); do
grep -rqE "(^|[^a-z-])$c([^a-z-]|\$)" handbook --include='*.md' || echo "missing: $c"
done
The 0.9.0 audit lists 132 commands and no missing name. Command catalog
spells every command in full, so a new command must be added there with its
page. A hit in grep shows a mention, not an explanation; open the page.
Also read crates/semaprax-harness/src/cli.rs for harness verbs, which help all
does not list.
2. Features. Read the new rows and status changes in the completion matrix and the release section of the changelog. Give each user-visible item a row in the tables below.
3. Language and library. Compare semaprax help language and semaprax help library with Cheatsheet, Built-in functions and
Standard library. std/catalog.json lists every package. Regenerate
the package table in the standard-library page when it changes.
4. Diagnostics. Run semaprax help diagnostic codes and add new indexed codes
to Diagnostics reference.
5. Examples. Run the compiler-backed check above.
Coverage by area (0.9.0)
“Page” is where a reader learns it. “Overview” means one section or a short entry with a link to the spec. “Not in the handbook” means private, internal or not shipped.
| Area | Page | Depth |
|---|---|---|
| Install by archive, Homebrew, release verify | Install, Shipping | Full for 0.8.0 archives. The 0.9.0 installers, five targets and WinGet are pending the post-release install rewrite. |
| First program, project, editor, Configure Compiler | Getting started, VS Code | Full |
| Language: scalars, control flow, records, variants, classes, generics, closures, matching, loops, iterators, collections, Box | Language chapters | Full |
Ownership, borrow, strings, bytes, resources, cleanup, unsafe, session protocols, laws | Ownership, Resources, Contracts and effects, Laws | Full |
| Effects and I/O: stdout, args, stdin, files, TCP | Input and output | Full |
Effects and I/O: TLS, listeners, HTTPS, stdout_append, checked atomic write | Built-in functions | Overview |
| Projects, manifests, profiles, modules, targets (native, web, wasm, npm, oci, native-callable) | Projects chapters | Full |
Hot reload (dev) | Targets | Full |
| Lock, resolve, add, fetch, packages, registry, audit, workflow, release verify | Shipping, Trust | Full |
| Semantic changes: preview, rebase, merge, patch, evidence, receipts, workspaces, candidates, images | Shipping, Explore | Full |
Servers: serve, service, MCP, image protocols, host policy, semapraxd | Shipping, Specialist commands | Full |
| Query, context, graph, doc, compact, cache | Agents, Context performance | Full |
| Agent programs: declare, run, route, budgets, journals, checkpoints, migration | Agent programs, Recovery | Full |
| Agent harness, adapters, bridge, routing | Harness | Full, labeled development tooling |
| C, C++, OpenAPI, Rust, freestanding | Integrations | Full |
| Capability manifest, protocol check, SIMD, region report, hygienic generation, plugin manifest, UI schema | Specialist commands | Overview |
| WIT and components | Specialist commands | Overview, labeled not a product |
| Assurance policy, proofs, property tests | Shipping, Laws, Testing | Full |
| Standard library | Standard library | All 47 packages listed |
Diagnostics and fixes, fix, repair | Diagnostics, Debugging | Full |
| Doctor, version, quality plan | Targets, Specialist commands | Full |
| Retention metadata stores | Specialist commands | Overview |
| Structured concurrency (Rust scoped-thread runtime) | std.async in Standard library | Not in the handbook as a language feature |
| Java/Kotlin, Swift/Apple bridges, UI runtimes for iOS, Android, desktop, public generic signatures, ARC zones | None | Not shipped; see the completion matrix |
| LAW16 campaigns, CI repairs, kernel and bootstrap documents | None | Internal evidence |
What 0.9.0 added
| Changelog item | Page |
|---|---|
| One-command installers, version pinning, receipts, uninstall | Install, pending rewrite |
aarch64-unknown-linux-gnu and x86_64-apple-darwin archives, glibc 2.35 baseline | Install, pending rewrite |
| Homebrew tap (covered), WinGet manifests (pending) | Install |
| VS Code Configure Compiler and status item | VS Code |
| Hot-reload stdin and framing fixes | Targets |
Harness fixes and model routing (MR-00 to MR-15), choice-select/v1, harness status --routing | Harness, Agent programs |
New help: hints and diagnostics (SPX-U103, SPX-O116, SPX-T207, SPX-F102 and others) | Diagnostics, Debugging |
Native operand read order fix for let mut (#561) | None; a bug fix |