Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Packages and project commands

A package is a directory containing plenty.toml. Project commands search the current directory and its parents for the nearest manifest, then for an enclosing workspace. The compiler receives a fixed package graph; imports never fetch or resolve packages.

Commands

CommandBehavior
plenty new PATH [--lib] [--name NAME]Create PATH with a manifest, src/main.plenty (or src/lib.plenty), and .gitignore
plenty init [--lib] [--name NAME]Create a package in the current directory; refuses to replace a manifest or source
plenty check [--locked] [--offline] [--check-all-methods]Check the library (without requiring main) and every executable, and report unchecked sources; --check-all-methods also checks unused methods of the package’s own generic instances, not those of std or dependency types; prints each package’s warnings once
plenty build [--fast-compile] [--no-strip] [--locked] [--offline]Check the library and compile every executable to target/PROFILE/NAME
plenty run [--fast-compile] [--no-strip] [--locked] [--offline] [--bin NAME] [-- ARGS...]Build one executable and run it with ARGS
plenty test [--fast-compile] [--locked] [--offline] [--no-capture] [--seed N] [--list] [--jobs N] [--timeout SECONDS] [FILTER...]Compile and run the package’s tests
plenty add [ALIAS] SOURCE [--version REQ] [--offline]Add a dependency to the manifest and lock it
plenty fetch [--locked] [--offline]Resolve, download, and lock every dependency without compiling
plenty update [PACKAGE...] [--offline]Move Git dependencies to the commits their tags and revisions name now
plenty graph [--locked] [--offline]Print the package graph file for plentyc --package-graph
plenty cleanRemove target/
plenty lspServe the language server on stdin and stdout

--locked fails instead of creating or changing plenty.lock. --offline never runs git fetch; it uses only the package cache in .pkg/. Executables are stripped of their symbol table and debug sections in both profiles; --no-strip keeps them.

build --test-hooks and run --test-hooks enable failure injection in project executables. Switching this flag rebuilds the executable. These instrumented builds retain symbols, like plenty test, and are intended for integration tests.

The package name defaults to the directory name and must be a lowercase identifier other than std. Status lines go to standard error.

plenty run inherits standard streams, environment, and working directory, and exits with the program’s status; Unix signal termination becomes 128 plus the signal number. With several executables, --bin NAME is required.

Any other first argument is a compiler command: plenty FILE still compiles and runs one file. A file named like a project command needs a path, such as plenty ./run. The plentyc binary provides only the compiler commands.

Check coverage

A target’s check covers the files its import graph reaches: its entry, the package modules it imports, and the dependency libraries those import. plenty check checks every target, then reports each .plenty file below the package’s src/ that no target reached. Such a file was not parsed or type-checked, so it may contain errors:

  Checking tools v1.0.0
warning: src/util/orphan.plenty is not imported by any target of `tools`; it was not checked
   Checked tools v1.0.0: library and 2 executables; 4 of 5 files in src/

The warning does not change the exit status. The summary names the targets checked and counts the files below src/ that they reached. Test files are not targets: plenty test checks tests/ when it compiles them, and plenty check ignores that directory. Every .plenty file below src/ is package source, which build reuse also fingerprints; keep fixtures, generated samples and other intentionally unbuilt .plenty files in another directory, such as tests/fixtures/ or fixtures/. Files without the .plenty extension are never reported.

Tests

Each file directly in tests/ is a test file; files in subdirectories are helper modules. A test file defines either main or test_ functions, not both, and defining neither is an error:

  • A file with main is one test, named by the file stem, that passes when the program exits with status zero.
  • Each module-level function whose name starts with test_ is a test named FILE::FUNCTION. It takes no parameters and returns () or Result[(), E] written out, not through an alias; any other signature is an error. It passes when it returns normally or returns Ok. A returned Err is written to standard error as error: NAME returned ERROR, except Failure.Unspecified and std.test.TestError.Assertion: the first carries nothing to show, and the second follows an explanation std.test already wrote.

Plenty has no attributes or reflection, so plenty test finds test functions by parsing each file, then compiles the file with a generated main that calls the function named by its one argument. Every test runs in its own process, so a trap, abort, or leaked state fails or affects only that test. The generated main exits 0 when the test passes and 3 when it returns Err; a trap exits 1.

A test file imports the package’s library under the package name, the package’s dependencies under their aliases, and its own helpers under tests/. Because those names are dependency aliases, a test file or helper cannot be named after the package or one of its dependencies.

plenty test compiles every selected test file to target/PROFILE/tests/PACKAGE/STEM before running any. These executables also link the hooks behind std.test failure injection, which wrap the C library’s allocation and file calls; plenty build and plenty run never link them. Each test runs with the package directory as its working directory, empty standard input, and TMPDIR set to a new empty directory that the runner removes, with everything in it, when the test ends. That removal also covers scratch directories of a test that traps or times out, which skip destructors. The per-test directories live in one plenty-test-run- directory under the runner’s own temporary directory.

Tests run in parallel, at most one per available CPU unless --jobs N says otherwise. Results are printed in discovery order whatever order the tests finish in, so repeated runs print the same lines. Tests that share files in the package directory must use --jobs 1 or their own names.

Each test’s standard output and error are captured together and printed under failures: only if it fails; --no-capture passes them through instead, and parallel tests’ output then interleaves. The command prints one line per test and a summary, and exits nonzero if any test fails:

running 2 tests
test area::test_square ... ok
test area::test_negative ... FAILED (returned Err)

failures:

---- area::test_negative output ----
assertion failed: left == right
  left: -9
 right: 9

test result: FAILED. 1 passed; 1 failed
OptionEffect
FILTER...Run only the tests whose names contain one of the filters; the summary counts the rest as filtered out
--listPrint the selected test names, one per line, without compiling
--no-captureLet test output through as it is written
--seed NSet PLENTY_TEST_SEED=N for every test; std.test seeds its generator from it
--jobs NRun at most N tests at once; N is at least 1
--timeout SECONDSKill a test still running after SECONDS (at least 1) and fail it as timed out after SECONDSs; processes it started are not killed
--fast-compile, --locked, --offlineAs for plenty build

A failing test’s line gives the reason: returned Err, exit status N (a trap is status 1), terminated by signal N, or timed out after Ns. Without --timeout a test can run forever.

Profiles

ProfileSelected byOutputCode generation
optimizeddefaulttarget/optimized/Cranelift opt_level=speed and optimized call-site passes
fast-compile--fast-compiletarget/fast-compile/Cranelift opt_level=none; skips optimized call-site passes

Profiles change code generation only. Overflow, bounds, ownership, and every other language check behave identically; neither profile adds debug information. Use --fast-compile for shorter edit-run cycles in large projects. Measure runtime performance with the optimized default. Compiled functions, program objects and executables remain cached separately by profile.

plentyc FILE and plentyc --compile FILE -o OUT also optimize by default; --fast-compile selects the same unoptimized code generation for single files. --release is accepted as a compatibility alias for the optimized default. These are Plenty profiles, independent of Cargo’s profiles for building the compiler.

Build reuse

build, run, and test skip a target whose output exists and whose inputs are unchanged since its last successful build; the package’s status line then reads Fresh instead of Compiling. build reuses a library check the same way; check always checks again. The inputs are the content of every file below each graph package’s src/ (not only the imported ones), the package graph, the entry file, the target kind, the profile and linker options, and the compiler executable (path, size, and modification time; the compiler embeds std and the runtime). Fingerprints live in target/PROFILE/.fingerprint/. A build forgets the old fingerprint before it starts, so a failed or interrupted build is never reused. Reuse is per target: a stale target is recompiled whole.

Manifest

Language editions are not supported; edition is an unknown manifest key. The editions proposal explores per-package source compatibility independently of plenty-version and manifest-version.

manifest-version = 1

[package]
name = "app"
version = "0.1.0"
description = "optional"
authors = ["optional"]
license = "optional"
plenty-version = "0.1.0"

[lib]
path = "src/lib.plenty"

[[bin]]
name = "app"
path = "src/main.plenty"

[dependencies]
util = { path = "../util", version = "^0.1.0" }

[dependencies.audio]
git = "https://example.com/audio.git"
tag = "v1.2.0"
subdir = "packages/audio"

manifest-version and [package] with name and version are required. version and plenty-version are MAJOR.MINOR.PATCH. plenty-version is the minimum toolchain version; an older toolchain rejects the package. The numbers above are sample package versions and compatibility requirements, not the current compiler version. plenty new and plenty init start the new package at 0.1.0 and set plenty-version from the compiler that creates it. Unknown keys and tables are errors, so a misspelled setting is never ignored. The manifest is a subset of TOML 1.0: basic and literal strings, decimal integers, booleans, arrays, tables, inline tables, and arrays of tables. Dotted keys, dates, floats, and multi-line strings are errors.

Targets default to src/lib.plenty (the library) and src/main.plenty (an executable named after the package) when those files exist. [lib] and [[bin]] replace the defaults; their paths are .plenty files below src/. A package needs at least one target. A library entry may define main; it is an ordinary function there.

Dependencies

Each [dependencies] key is an alias: the only name under which that package’s code imports the dependency’s library. import util binds the library module, and import util.shapes reads src/shapes.plenty in the dependency. Aliases are lowercase identifiers. std cannot be an alias, and an alias that matches a local top-level module or directory is an error.

path is relative to the declaring manifest and names a live directory containing plenty.toml. A git dependency is described below.

version is an optional requirement on the dependency’s version, checked after resolution; it never selects a version. X.Y.Z and ^X.Y.Z accept versions at least X.Y.Z with the same leftmost nonzero component (^0.2.1 accepts 0.2.5 but not 0.3.0), =X.Y.Z accepts only that version, and >=X.Y.Z accepts that version or later. A mismatch names both packages:

error: package `app` requires `util` ^2.0.0, but /work/util is version 1.4.0

Resolution reads every manifest before compiling. A graph contains one package per name: two dependency paths reaching the same directory share one package and its types, whatever their aliases; two different directories with the same package name are an error that names both paths. Package cycles are errors. Dependencies are not transitive: a package imports only what it declares.

Each package’s sources live in its own src/ directory and cannot import files of another package except through a declared alias. The same module path in two packages names two modules, so Point in two packages is two distinct types. Diagnostics name a dependency’s declarations as PACKAGE::MODULE.Name, such as util::lib.Point; the root package’s names are unqualified.

Git dependencies

[dependencies]
tz = { git = "https://github.com/plentylang/tz.git", tag = "v0.3.0" }
codec = { git = "https://example.com/media.git", rev = "4f2a9c1", subdir = "codec" }

A git dependency needs exactly one of tag (a tag name) or rev (7 to 40 lowercase hexadecimal digits of a commit). subdir is the package’s directory inside the repository, relative and without ... Plenty runs the system git program; any URL it accepts works, including file:// and SSH. Credentials come from Git’s own configuration; Plenty never prompts.

The first resolution fetches every branch and tag of the repository into a bare repository under .pkg/git/db/, records the commit that the tag or revision names in plenty.lock, and extracts that commit’s tree into .pkg/git/checkouts/REPO/COMMIT/. Later commands build the locked commit, even if the tag has moved upstream; they fetch only when the cache lacks the commit, as after deleting .pkg/. Each repository is fetched at most once per command. Snapshots are extracted beside their final path and renamed into place, so an interrupted fetch never leaves a partial snapshot. .pkg/ is generated: the .gitignore that plenty new writes excludes it.

plenty update resolves every tag and revision again and rewrites the lockfile; plenty update NAME... resolves only the named packages. It prints each changed commit.

A path dependency inside a Git package names another package of the same commit, and cannot leave the repository. A graph contains one commit per package: if two dependents reach the same repository directory at different commits, resolution fails and names both chains:

error: conflicting requirements for package `geo` from https://example.com/mono.git:
  app requires tag=v2 (commit c25fe24762e9)
  app -> shapes requires commit 421b7a9fd85a through a path in the same repository
a graph may contain one commit per package
SituationBehavior
--offline and the repository or commit is not cachedError naming the URL; nothing is fetched
--locked and the lockfile has no commit for a requirementError before any fetch
The locked commit is gone upstream and not cachedError suggesting plenty update
A locked commit is not 40 lowercase hexadecimal digitsError: the lockfile is malformed; delete it to resolve again
The tag or revision does not existError naming the URL and tag or revision
git is not installedError saying Git dependencies need git on PATH

Adding dependencies

plenty add appends one entry to the [dependencies] table of the package containing the current directory, keeping the rest of the file unchanged, then resolves and locks the graph. If resolution fails, the manifest is restored.

plenty add --path ../util
plenty add tz --git https://github.com/plentylang/tz.git --tag v0.3.0 --version 0.3.0
plenty add codec --git https://example.com/media.git --rev 4f2a9c1 --subdir codec

--path is relative to the current directory and is written relative to the manifest. The alias defaults to the dependency’s package name; adding an alias that already exists is an error.

Workspaces

A workspace is a directory whose plenty.toml lists member packages:

manifest-version = 1

[workspace]
members = ["app", "util"]

Members are directories below the workspace, without ... The same manifest may also contain [package], which makes the workspace directory a member too. Members share one plenty.lock, one .pkg/, and one target/ in the workspace directory, and depend on each other through ordinary path dependencies.

A command run in the workspace directory acts on every member: check and build process each in manifest order, run chooses among all their executables (whose names must be unique), and test prefixes each test name with PACKAGE/. A command run inside a member acts on that member only, with the shared lockfile and outputs. plenty add and plenty graph need a member.

Lockfile

Every project command writes plenty.lock beside the root manifest (or in the workspace directory) when it is missing or out of date. It lists each package’s name, version, source, and dependencies as alias/package pairs: the root package first, then the others sorted by name. A source is root, path+RELATIVE from the lockfile’s directory, or git+URL with the subdir, the full commit, and the selectors (such as tag=v1.2.0) that resolved to it. It contains no absolute paths, so a checkout can move. Writes replace the file atomically. --locked fails instead of creating or changing it. A lockfile that cannot be read is an error; deleting it resolves every dependency again.

Package graph files

plenty graph prints the resolved graph of the current package as TOML, which plentyc --package-graph FILE reads in place of --module-root:

graph-version = 1

[[package]]
identity = "app"
root = "/work/app/src"
dependencies = { util = "util" }

[[package]]
identity = "util"
root = "/work/util/src"
library = "/work/util/src/lib.plenty"
dependencies = {}

The first package contains the entry file. identity is unique and qualifies the package’s declarations in diagnostics; dependencies maps each alias to an identity whose package has a library. Relative paths are relative to the file. --package-graph works with FILE, --check, --check-module, and --compile. It lets another build system compile a package without plenty.toml, and makes the compiler’s input visible when debugging resolution.