#Command Line
Status: command-line specification draft.
This chapter is the CLI tier of the specification. It defines how the hd
command finds a package and what each command runs:
- package mode, workspace mode, and single files;
hd,hd FILE,hd FILE.wasm,hd run,hd build,hd check, andhd test;- the executables a manifest declares, and package tasks;
- program arguments, host capabilities, machine output, and exit status;
hd doc, which writes a package's documentation;hd add,hd update,hd remove, andhd fetch, which manage and fetch dependencies;hd installandhd uninstall, which manage installed executables;hd clean, which removes the build directory or the dependency cache;hd fmtandhd fix, which rewrite source files;- the compiled cache and
hd cache gc; - resource limits on a program, and threads;
hd new, and the REPL.
The language tier defines what a program means: its entry module and entry point, and single-file programs. This tier says which program a command starts. Its diagnostic codes are in the Diagnostics table with the language tier's.
#Commands
| Command | Effect |
|---|---|
hd |
opens the REPL, or runs standard input when it is not a terminal |
hd --repl [--engine NAME] FILE |
runs a REPL session whose inputs are the lines of FILE, with no terminal |
hd [--release] [--engine NAME] FILE |
runs FILE as a single file, in the build profile its flag selects, on the engine NAME |
hd [--engine NAME] FILE.wasm |
runs a prebuilt module that hd build wrote |
hd run [--release] [--engine NAME], hd run [--release] [--engine NAME] NAME |
runs an executable or a task of the package, in the build profile its flag selects |
hd build [--release] [--engine NAME] |
builds the package, or one FILE, in that profile |
hd check, hd test [--release] [--engine NAME] |
work on the package, or on one FILE; hd test is always checked |
hd lint [FILE] [--all] [--allow CODE] [--warn CODE] [--deny CODE] |
reports lint findings, without running or rewriting a program |
hd doc [--private] [--out DIR] [--open], hd doc NAME |
writes the package's documentation, or prints one item's |
hd new [--app | --lib] [--pages] [--vcs none] [PATH] |
creates a package |
hd add [--dev] [--name NAME] TARGET[@QUERY]..., hd update [NAME[@QUERY]], hd remove NAME, hd fetch |
change or fetch the package's dependencies |
hd install SOURCE, hd install --list, hd uninstall NAME |
install, list or remove executable commands |
hd clean, hd clean --cache |
removes the package's build directory, or the dependency cache |
hd fmt [--check] [FILE] |
formats the package's source files, or one FILE |
hd fix |
applies the package's safe fix-its |
hd cache gc |
evicts compiled entries down to the size cap |
hd help, hd --help, hd help COMMAND |
prints the command list, or one command's usage and flags |
- cli.command.help
hd helpandhd --helpprint the command list. It names every command of the table above. - cli.command.help.command
hd help COMMANDandhd COMMAND --helpprint the command's usage line and each flag it takes. So the help ofhd addnames--devand--name, and the help ofhd cleannames--cache. - cli.command.positional.operandsA positional word denotes the command's declared NAME, FILE, SOURCE, PATH or TARGET.
- cli.command.positional.package-directoryA directory operand to
hd runorhd testis an error whose message suggests-p.
hd help # the command list
hd help add # the usage and flags of add, --dev and --name included
hd add --help # the same text
#Package Mode
A command works on a package when it finds that package's hd.toml:
| Start directory | Nearest hd.toml |
Mode |
|---|---|---|
shop/src/cart |
shop/hd.toml |
package mode, package shop |
ws/libs/ui/src |
ws/libs/ui/hd.toml, a member of ws |
package mode, package ui, with the selection and hd.sum of ws |
ws, whose hd.toml lists members and declares no package |
ws/hd.toml |
workspace mode |
notes, with no hd.toml above it |
none | outside any package |
- cli.mode.package.nearestA command works in package mode exactly when the nearest
hd.tomlin its start directory or a directory above it declares a package. Thathd.tomlnames the package. - cli.mode.startThe start directory is the directory of the FILE that a command names, or the working directory when it names none.
- cli.mode.workspaceA command works in workspace mode when that nearest
hd.tomlis a workspace manifest, which lists members and declares no package. - cli.mode.outsideOtherwise the command works outside any package.
- cli.mode.workspace-fileA command that names a FILE under a workspace root but in no member works outside any package. So FILE is a single-file program that may use only
std. - cli.mode.memberIn package mode, the command also searches the directories above the package's directory for the nearest workspace manifest. When that manifest lists the package as a member, the package is still the one the command works on.
- cli.mode.member.sharedVersion selection and
hd.sumthen come from that workspace, by Workspaces, as Cargo's do. - cli.mode.member.unlistedWhen that workspace manifest neither lists the package in
membersnor inexclude, the command is an error that names the manifest, as Cargo's is. - cli.mode.member.unlisted.fixThe error has two fix-its: one adds the package's directory to the manifest's
members, and the other adds it to the manifest'sexclude. - cli.mode.member.excludedA package whose directory the workspace manifest's
excludelists is not a member. A command in it works on it as on a package outside any workspace.
shop/hd.toml # declares the package shop
shop/src/lib.hd
shop/src/cart/mod.hd
cd shop/src/cart
hd check # package mode: checks shop
Note. A
srcdirectory without anhd.tomldoes not make a package.
Why. Developers and agents mostly work inside a member. A command there uses the same selected versions as one at the workspace root.
#Single Files
hd FILE runs one file, as python x.py or deno run x.ts does:
hd notes.hd
- cli.file.run
hd FILEruns FILE as a single-file program, whether or not FILE lies in a package. - cli.file.in-packageWhen FILE lies in a package, the error for a use of
pkg,self, orsuperin it suggests a task andhd run NAME. - cli.file.entry-hintWhen FILE is the entry module of an executable or a task, that error instead names it and the exact
hd runcommand. Forsrc/main.hdof packageshop, it nameshd run shop. - cli.file.check-testOutside any package,
hd check FILEandhd test FILEcheck or test FILE as a single-file program. - cli.file.check-test.no-fileOutside any package,
hd checkorhd testwithout a FILE is an error whose message suggests passing a FILE or creating a package withhd new.
Why. One file or one run uses only
std. A program that needs several files or a dependency is a package.
Note. The
tests:block of a single file holds unit tests, sohd test FILEbinds themTestRunneralone (cli.test.env.unit). A script is tested against the real system by running it.
#Prebuilt Modules
hd FILE.wasm runs a Wasm module that hd build wrote, without its
source. FILE's extension picks the mode:
hd build # writes build/debug/greeter.wasm
hd build/debug/greeter.wasm -- a b # runs it with the arguments a and b
hd build/debug/greeter.wasm --cap Console=false # refused before it starts: 101
- cli.wasm.runWhen FILE's name ends in
.wasm,hd FILEinstantiates FILE as a module thathd buildwrote, and runs it. - cli.wasm.modeOnly the extension picks the mode:
hd FILEfor a FILE whose name ends in.hdruns a single-file program, bycli.file.run. - cli.wasm.hostThe module runs under the default profile, in the working directory of the
hdcommand. - cli.wasm.host.imports
hdbinds each trait of the default profile that the module's import list names. - cli.wasm.grant.package-contextA prebuilt module with package host capabilities uses the discovered app manifest and command flags for its grants.
- cli.wasm.grant.builtin-onlyA prebuilt module without package host capabilities takes grants from command flags alone.
- cli.wasm.total-denyThe total deny refusal applies, with the module's needs read from its import list by
cli.cap.total.needs. - cli.wasm.args
hd FILE.wasmpasses the words after--to the module as its program arguments, bycli.args.pass. - cli.wasm.statusOnce the module starts,
hd FILE.wasmexits with the program's own status, bycli.exit.program. - cli.wasm.invalidA FILE that is not a valid Wasm module is an error whose message names FILE. It exits with status 101, by
cli.exit.hd-failure. - cli.wasm.not-hdA valid module that
hd builddid not write is an error, with status 101. - cli.wasm.not-hd.detect
hdknows such a module by a missing entry export, or by an import of a host function thathddoes not provide. - cli.wasm.not-hd.messageThe error's message names the missing export or the unknown import, and suggests rebuilding the module with
hd build.
Why. Builtin-only modules need no package source or manifest. A package provider needs the app's manifest consent and its SDK's provider map.
Note. The runtime ABI is not specified yet (
module.host.abi), so a module records no build profile and nohdversion that this chapter defines.hdcannot tell which profile built a module, and the module keeps that profile's overflow behavior. A module from an incompatiblehdshows only as a missing entry export or an unknown import.
#Running A Package
hd run runs the programs a package declares: its executables, which it
ships, and its tasks, which it does not.
#Executables
Each [[executable]] table of hd.toml declares one program the package
ships:
[[executable]]
name = "invoice"
module = "main"
[[executable]]
name = "migrate"
module = "tools.migrate"
- cli.exe.tableEach
[[executable]]table declares one executable:namenames it, andmodulenames its entry module by its module path under the source root. - cli.exe.severalA package may declare several executables, with unique names.
- cli.exe.missing-moduleAn executable whose
modulenames no module of the package is an error. Error:missing-entry-point. - cli.exe.unselected-main.lintA public
mainormain!in a source-root module named by no executable produces a lint finding. Warning:unselected-main. - cli.exe.unselected-main.functionSuch a
mainormain!remains an ordinary function. - cli.exe.private-main.missingA run/build request requiring an entry fails when its only
mainis private, naming that function. Error:missing-entry-point. - cli.exe.private-main.analysisChecking, tests and library-module analysis accept an ordinary private
main. - cli.exe.default-mainWith no
[[executable]]table inhd.toml,src/main.hdis the package's default executable, as Cargo'ssrc/main.rsis. - cli.exe.main-unlistedWhen
hd.tomlhas an[[executable]]table, asrc/main.hdthat no table names is an error. Its fix-it adds a table for it, with the package's name andmodule = "main". Error:unlisted-entry. - cli.exe.other-moduleAn executable's entry module other than
src/main.hd, such assrc/tools/migrate.hd, resolves relative uses as an ordinary module does:selfis its own module. - cli.exe.entry-programEvery executable's entry module is its own program and never part of the package's library, as
src/main.hdis bymodule.path.main-file. - cli.exe.entry-no-useA use of an executable's entry module from another module is an error. Error:
unknown-module. - cli.exe.default-nameThe default executable is named after the package, the
nameof its[package]table, as Cargo namessrc/main.rs. - cli.exe.default-name.clashA task with the package's name therefore clashes with the default executable, by
cli.task.name-clash.
[package]
name = "shop"
[[executable]]
name = "migrate"
module = "tools.migrate"
# error: src/main.hd is no executable; the fix-it adds name = "shop", module = "main"
Note. The entry module's
main,main!, or top-level statements decide what the executable does, by Module Initialization and Entry Point.
Why. The manifest names the module, never the function. An agent that renames
mainthen sees an error in source, not a stale manifest. An entry module is a program, as in Go and Rust. Its top level may print, and no dependent sees it.
#Tasks
A task is a development program of the package, such as a data seeder or a release script:
hd run seed # runs tasks/seed.hd
Each task is its own program, and the subdirectories of tasks hold the
modules that tasks share:
| File | Role |
|---|---|
tasks/build.hd |
task build, which uses use self.shared.zip |
tasks/seed.hd |
task seed |
tasks/shared/zip.hd |
a shared task module |
- cli.task.fileEach file
tasks/NAME.hdin the package directory is a task namedNAME, and it is an entry module. - cli.task.programEach task is its own program, compiled separately from the package's other tasks.
- cli.task.no-pathTasks and shared task modules are not modules of the package: they have no module path, so a library module such as
src/tasks/zip.hdnever clashes withtasks/zip.hd. - cli.task.sharedA module in a subdirectory of
tasks, such astasks/shared/zip.hd, is a shared task module. Every task of the package may use it. - cli.task.beside-directoryA file directly under
tasksbeside a directory of the same name, such astasks/shared.hdbesidetasks/shared/, is an error. It has no fix-it, and its message names the file's target,tasks/shared/mod.hd. Error:duplicate-module-name. - cli.task.relativeIn a task or a shared task module, relative lookup works as it does under
src, withtasksin place of the package root. - cli.task.root-fileA task resolves relative uses as a root file does, so its lookup starts at
tasks. Fromtasks/build.hd,self.shared.zipnamestasks/shared/zip.hd. - cli.task.superA
superin a task is an error. Error:unknown-module. - cli.task.above-rootIn a shared task module, a
superthat moves abovetasksis an error. Error:unknown-module. - cli.task.program-useA use of a task from another module is an error. Error:
unknown-module. - cli.task.shared-copyEach task gets its own copy of the shared task modules it uses, so their top-level statements run once per task.
- cli.task.shared-unused.lintLint reports an unused declaration in a shared task module only when no task uses that declaration.
- cli.task.usesA task may use the package's modules through
pkg. - cli.task.dev-dependenciesA task may use the package's dependencies and its dev dependencies.
- cli.task.cyclic-dev-dependencyA task may use a dev dependency that itself depends on the package, as an integration test module may.
- cli.task.not-shippedA task is never part of the package's library or executables, and a dependent package never builds it.
- cli.task.name-clashA task and an executable with the same name are an error when
hdreads the manifest. Error:duplicate-executable-name.
Why. Tasks follow the test root's layout (Test Modules): a top-level file is a program, and a subdirectory holds what programs share. A top-level
x.hdbesidex/is a program under that layout, but the folder's parent undersrc, so it is rejected.
#Choosing What Runs
hd run # the package's one executable, such as src/main.hd
hd run shop # src/main.hd again, by the package's name
hd run migrate # the executable or task named migrate
- cli.run.nameIn package mode,
hd run NAMEruns the executable or task namedNAME, and is an error when there is none. - cli.run.default.one
hd runalone runs the package's executable when it has exactly one, declared or default. It is an error when the package has none, as a library-only package does, or several. - cli.run.file
hd run FILEis an error whose message suggestshd runorhd run NAME. - cli.run.package-onlyOutside any package,
hd runandhd buildare errors whose message suggests creating a package withhd new.
#Working Directory
- cli.run.cwd.taskA task runs with its package directory, the directory of its
hd.toml, as its working directory. - cli.run.cwd.executableAn executable runs in the working directory of the
hd runcommand.
cd shop/src/cart
hd run # the executable runs here, in shop/src/cart
hd run gen # the task gen runs in shop, beside its hd.toml
Why. A task acts on its package, as an npm script does. What it reads then does not depend on where the command ran. An executable is the user's tool, as with
cargo run.
#Program Arguments
hd run gen -- --out x # task gen gets the arguments --out and x
hd notes.hd -- a b # notes.hd gets the arguments a and b
- cli.args.separatorThe words after the first
--of a command line are arguments of the program, andhdreads none of them as its own. - cli.args.pass
hd FILEandhd runpass those arguments to the program they run, in order, as Cargo'scargo run --does. - cli.args.extra-wordA further positional word before
--, as inhd run gen x, is an error whose message suggests--.
Note. A program reads its arguments through the host capability trait
Args(module.entry.host-facilities).
#Host Capabilities
- cli.host.entry-bindingWhen
hdruns an executable, a task, or a single file that has amain, it binds each host capability trait that the row ofmainormain!names.
hd FILE, hd run, a task, and the REPL use the default
profile, which binds these traits:
| Trait | Module | What hd binds |
|---|---|---|
Console |
std.console |
write! writes to standard output, and write_error! to standard error |
ConsoleInput |
std.console |
reads standard input |
Args |
std.host |
the FILE or NAME the command ran, and the program arguments |
Env |
std.host |
the environment of the hd process |
Clock |
std.time |
the system's wall clock and monotonic clock; sleep! waits in real time |
Random |
std.random |
the operating system's random source |
FsRead, FsWrite |
std.fs |
the file system, with relative paths from the program's working directory |
Process |
std.process |
starts a host program, which the operating system finds by its name, in the program's working directory |
Http |
std.http |
sends each request from the host |
Net |
std.net |
the host's DNS resolver, and its TCP and UDP sockets |
Sys |
std.host |
the host's operating system, architecture, host name, and processor count |
- cli.host.default-profile
hd FILE,hd run, and a task run their program under the default profile, which binds the host capability traits in the table above. - cli.host.script-bindingWhen
hdruns a script, it binds every trait of the default profile, as the REPL does, so the script's top level may use each of them. The script's capability grant still limits the resources they touch. - cli.host.default-profile.main-rowThe row of
mainormain!still limits what the program gets:hdbinds only the traits of the default profile that the row names, bycli.host.entry-binding. - cli.host.default-profile.other.with-package-hostA row key outside the built-in profile and package-declared host traits is an error. Error:
nonhost-entry-requirement. - cli.host.default-profile.console-write-closedUnder the default profile,
write!andwrite_error!return.Err(ConsoleError.Closed)when the host cannot write the text. One such case is a pipe whose reader has closed it. - cli.host.default-profile.input-closed
read_line!returns.Err(ConsoleError.Closed)when standard input is not attached or a read fails. The end of input is.Ok(.None), and every later call returns.Ok(.None)again. - cli.host.default-profile.grantedEach provider of the default profile touches only the resources that its program's capability grant covers.
- cli.host.process.envA program that the default profile's
Processprovider starts inherits the whole environment of thehdprocess, whatever theEnvgrant covers. - cli.host.handles.exitA host handle is a value that a default profile provider opens and keeps in the host, such as a
Netsocket. When the program ofhd FILE,hd run, or a task ends,hdcloses every host handle it left open, and writes nothing about them. - cli.host.process.windows-launcherOn the Node engine on Windows,
hdsupplies its own executable as theprocessLauncherfor run and test hosts.
cat src/main.hd
# pub fn main() -> void $ Console:
# println("hello")
hd run # the default profile binds Console: prints hello
Why. A closed pipe makes
printlnpanic, bymodule.console.println-write-error.either, as Rust'sprintln!does;hd FILE | head -1stops the program instead of writing into nothing.
Why. A row can't name a path or a host, since those are run-time values. So control has two levels. The row says at compile time which traits a function may use. The capability grant says at run time which resources those traits may touch.
Why. The operating system closes a process's descriptors when it exits, and programs rely on that. A note on standard error for each handle left open would add noise to every such program, so
hd runcloses them silently, andhd testreports them.
Note. A program that
Processstarts runs with the operating system's authority ofhd, and no grant limits what it does. AProcessgrant names the programs that may start, as Deno warns of its own subprocesses.
#Capability Grants
A capability grant says, for each host capability trait, which resources a program may touch at run time:
# hd.toml
[capabilities]
FsRead = ["data/"]
Http = ["api.example.com"]
Process = false
hd run # reads under data/, calls api.example.com, starts no program
hd run --cap Http=localhost:8080 # this run calls localhost:8080 instead
| Key | Trait | Scope entries |
|---|---|---|
FsRead, FsWrite |
std.fs.FsRead, std.fs.FsWrite |
paths |
Http |
std.http.Http |
hosts, each with an optional :port |
Net |
std.net.Net |
host:port addresses, or hosts for every port |
Env |
std.host.Env |
variable names, or a prefix followed by * |
Process |
std.process.Process |
program names |
Sys |
std.host.Sys |
method names of Sys |
Console, ConsoleInput, Clock, Random, Args |
std.console.Console, std.console.ConsoleInput, std.time.Clock, std.random.Random, std.host.Args |
none: only true or false |
- cli.cap.tableThe
[capabilities]table ofhd.tomlsets the grant of each host capability trait that it names. - cli.cap.table.keys.with-package-hostIts keys are built-in capability names or quoted package capability paths beginning with
pkg.ordep.. - cli.cap.value.trueThe value
truegrants the trait with no limit on its resources. - cli.cap.value.falseThe value
falsedenies the trait totally, by Total Deny. - cli.cap.value.listA list of scope entries grants the trait only the resources that those entries cover, by Grant Scopes.
- cli.cap.value.emptyAn empty list covers no resource, so every call of the trait is refused by Partial Deny.
- cli.cap.value.unscopedA list is an error for a trait whose row in the table above has no scope entries.
- cli.cap.flag
--cap NAME=VALUEsets the grant of the traitNAMEfor one command. - cli.cap.flag.value
VALUEistrue,false, or a list of scope entries separated by commas. - cli.cap.flag.commands
hd FILE,hd run,hd test, andhdwith no FILE take--capany number of times, before the--of the program arguments. - cli.cap.flag.unknown.with-package-hostA
--capflag whoseNAMEis neither a built-in capability name nor a package capability path is an error. - cli.cap.flag.unscopedA
--capflag that gives a list to a trait without scope entries is an error. - cli.cap.no-prompt
hdnever asks whether to grant a resource, so a call outside its grant fails at once. - cli.cap.package.pathA package capability path uses its root source-use spelling, such as
pkg.ui.Uiordep.server_sdk.server.HttpServer. - cli.cap.package.booleanA package capability grant must be
trueorfalse. - cli.cap.package.nominalPackage capability paths resolve through the selected dependency graph to complete nominal trait identities.
- cli.cap.table.requiredA package's
[capabilities]table must list every host capability in each executablemainrequirement row. Error:missing-capability. - cli.cap.table.required-unusedThis check includes declared row capabilities that the program's linked code does not use.
- cli.cap.table.required-builtinsConsole, Args, Clock, Random and ConsoleInput have no exemption from this check.
- cli.cap.table.required-packageA package-owned host capability is listed by one of its source capability paths.
- cli.cap.table.required-flagsA command-line grant does not replace a required manifest entry.
- cli.cap.table.required-entry-messageThe diagnostic names the missing capability and points at its written entry row or test registration.
- cli.cap.table.required-entry-fixIts exact fix-it inserts unrestricted grants for missing capabilities into the applicable tables of
hd.toml. - cli.cap.table.unusedListing a capability that no
mainuses is not an error. - cli.cap.table.scriptA standalone script with no
hd.tomlneeds no capability table. - cli.cap.dev.table
[dev-capabilities]accepts the same keys and values as[capabilities]. - cli.cap.dev.entriesTests and tasks use the root package's production and development grants together.
- cli.cap.dev.mainAn executable
mainuses only production grants. - cli.cap.dev.requiredEvery test or task host capability must appear in either root grant table. Error:
missing-capability. - cli.cap.dev.denyA
falseentry in either table denies the capability for tests and tasks. - cli.cap.dev.allOtherwise,
truein either table grants the capability without resource limits. - cli.cap.dev.scopesOtherwise, tests and tasks receive the union of both tables' scope entries before command flags apply.
- cli.cap.dev.dependenciesA dependency's development grants never contribute to the root program's grants.
- cli.cap.dev.shippingInstallation and deployment adapters read production grants alone.
- cli.cap.dev.fixA missing test or task capability receives an exact fix-it inserting its grant into
[dev-capabilities]. - cli.cap.dev.test-internalTest-runner capabilities supplied by the testing harness require no manifest entry.
For example, a package's main row $ Process with no Process key
fails with missing-capability. Its fix-it adds Process = true to the
capability table. A present Process = false satisfies the listing rule;
Total Deny still applies to capabilities the module imports.
Why.
hd.tomlholds what a program always needs, reviewed in a diff. A flag changes one run, such as an agent's sandbox or a CI job. A prompt would stall an agent, and would make one command behave two ways.
#Grant Precedence
For each trait, the first row of this table that applies gives the trait's grant:
| Rule | When | The trait's grant |
|---|---|---|
| cli.cap.order.denyDeny wins | the table or any --cap flag gives the trait false |
a total deny |
| cli.cap.order.flagFlags | otherwise, when one or more --cap flags name the trait |
no limit when a flag gives true, else every entry that the flags list |
| cli.cap.order.tableTable | otherwise, when the table names the trait | the table's value |
| cli.cap.order.defaultDefault | otherwise | no limit, so a partial table or a flag restricts only the traits it names |
# hd.toml holds: [capabilities] FsRead = ["data/"] and Process = false
hd run --cap FsRead=out/ # FsRead covers out/ alone: the flag replaces the table's value
hd run --cap Process=true # Process stays denied: false always wins
hd run --cap Http=example.com # Http covers example.com; Console and the rest keep no limit
Where a program's grant comes from:
| Program | Its grant comes from |
|---|---|
an executable under hd run, or hd FILE for a FILE in a package |
the package's [capabilities] table, and the command's --cap flags |
a task under hd run |
both root grant tables and the command's --cap flags |
hd FILE outside any package, hd FILE.wasm, and hd with no FILE |
the command's --cap flags alone |
a program that hd_run! starts |
its executable or task grant tables, with no flags |
| an integration test case, or a doc test | the test grant |
| a unit test | none, since it gets no host provider |
- cli.cap.source.productionExecutables under
hd run, andhd FILEin a package, apply production grants and command flags. - cli.cap.source.flags-only
hd FILEoutside any package, andhdwith no FILE, apply only the command's flags, so the REPL takes its grant from them. - cli.cap.source.hd-run.entryA program that
hd_run!starts gets its executable or task grant tables, ashd run NAMEwould, without flags. - cli.cap.build
hd buildwrites no grant into its output. Another host that runs the module applies its own policy.
Why. A package's main requirements must appear in its manifest before this runtime policy applies. Standalone scripts still use the default for traits that neither a table nor a flag names.
#Grant Scopes
Each scope entry names resources of one trait:
[capabilities]
FsRead = ["data/", "config.toml"]
FsWrite = ["out/"]
Http = ["api.github.com", "*.example.com", "localhost:8080"]
Net = ["db.internal:5432"]
Env = ["HOME", "APP_*"]
Process = ["git"]
Sys = ["os", "arch"]
- cli.cap.scope.pathAn
FsReadorFsWriteentry covers the file it names, or every path under the directory it names. - cli.cap.scope.path.resolved
hdresolves..and symbolic links in the entry and in the accessed path before it compares them, so no path escapes a granted directory. - cli.cap.scope.path.accessOnce the grant check passes, the access itself resolves the path as the operating system does, on every engine. So reading
data/missing/../a.txtfails withNotFoundwhendata/missingdoes not exist. - cli.cap.scope.path.relativeA relative path in the table is relative to the package directory, and one in a flag to the command's working directory.
- cli.cap.scope.write-not-readAn
FsWriteentry grants no read. - cli.cap.scope.rename
rename!needs anFsWriteentry that covers each of its two paths. - cli.cap.scope.hostAn
Httpentry covers a request whose URL host matches the entry's host, and whose port equals the entry's port when the entry gives one. - cli.cap.scope.host.formsAn entry's host is a name, an IPv4 address, or a bracketed IPv6 address. The entry
*.example.commatches every name that ends in.example.com. - cli.cap.scope.redirect
hdchecks each redirect target of anHttprequest as a new request. - cli.cap.scope.netA
Netentry covers the address of a connect, listen, bind, or send by the host match of anHttpentry. An entry without a port covers every port. - cli.cap.scope.net.lookupA
Netentry covers a DNS lookup of a host that its host matches. - cli.cap.scope.envAn
Enventry covers the variable it names. An entry that ends in*covers every name that starts with the text before the*. - cli.cap.scope.processA
Processentry covers arun!call whoseprogramargument equals it, as written, before the host looks the program up. - cli.cap.scope.sysA
Sysentry covers the method ofSysthat it names:os,arch,hostname, orcpu_count.
Note. In an integration test,
run!names an executable or a task of the package (cli.test.process), so aProcessentry there names one of them.
#Total Deny
A trait whose grant is false is totally denied. A program that can
reach it never starts:
# hd.toml holds: [capabilities] Console = false
hd run; echo $?
# hd: the program needs Console, which Console = false in hd.toml denies
# 101
- cli.cap.total.needsA Wasm module's needs are the host capability traits whose methods its import list names.
- cli.cap.total.refuseWhen a need of a module is totally denied,
hdrefuses to start it, before its module initialization and beforemain. Error:denied-capability. - cli.cap.total.messageThe refusal names the trait, and the setting that denied it: the
hd.tomlkey or the--capflag. - cli.cap.total.statusA refused command exits with status 101, since no program ran, by
cli.exit.hd-failure. - cli.cap.total.any-moduleThe check reads only the module's import list, so it holds alike for a program built from source and for a prebuilt Wasm module.
- cli.cap.total.test
hd testrefuses an integration test module or a doc test with a totally denied need in the same way, with status 101. - cli.cap.total.check
hd checkmay report a total deny before any run, but the startup refusal is the rule that decides.
Why.
hd FILE.wasmruns prebuilt Wasm, whose sourcehdnever sees. Every host capability call is an import, and dead code is removed, so the import list is exactly what the module can reach. A refusal at startup comes before any side effect.
#Partial Deny
A trait whose grant is a list is partially denied: a call outside the list fails, and the program goes on:
use std.fs.{FsRead, read_text}
use std.path.Path
pub fn main!() -> void $ Console + FsRead:
match read_text!(Path("secrets/key.txt")): # FsRead = ["data/"] does not cover it
.Ok(text) => println(text)
.Err(.NotGranted(_)) => println("not granted")
.Err(_) => println("unreadable")Open in playground| Trait | A refused call returns |
|---|---|
FsRead, FsWrite |
.Err(FsError.NotGranted(path)) |
Http |
.Err(HttpError.NotGranted(host)) |
Net |
.Err(NetError.NotGranted(address)) |
Process |
.Err(ProcessError.NotGranted) |
Sys |
.Err(HostError.NotGranted(name)) |
Env |
.None from get, by Environment Grant |
- cli.cap.partial.refuseA call whose resource lies outside its trait's grant returns the value of the table above, and never panics.
- cli.cap.partial.not-os
NotGrantedreports a refusal by the grant, andPermissionDeniedstays the operating system's refusal. - cli.cap.partial.path.resolvedWhen path resolution changes the target, a filesystem refusal names the resolved path in its message and grant hint.
- cli.cap.partial.path.writtenA filesystem refusal keeps the written path when resolution does not change its target.
Why. The resource is known only at run time, and a program that probes an optional file must be able to recover.
NotGrantedandPermissionDeniedhave different fixes, so an agent must tell them apart, as Deno 2 splitNotCapablefromPermissionDenied.
#Environment Grant
Env.get returns an optional, so a refused read is .None, and hd
names the fix on standard error:
hd --cap Env=HOME notes.hd
# hd: env PATH is set but not granted; run with --cap Env=PATH
- cli.cap.env.getWhen the
Envgrant is a list,get(name)returns.Nonefor a name that no entry covers, even when the variable is set. - cli.cap.env.noticeWhen such a variable is set on the host,
hdwrites one line to standard error, once per name in a run. - cli.cap.env.notice.textThe line is
hd: env NAME is set but not granted; run with --cap Env=NAME, with the variable's name forNAME. - cli.cap.env.names
namesreturns only the set variables that an entry covers.
Why. A silent
.Nonewould send an agent hunting for a variable that is set. The notice names the flag, so one rerun fixes it.
#Building And Checking
hd check # the library and the executables
hd check --tests # also the test code
hd check --all # also the tasks
hd test # every test of the package
hd test src/billing.hd # the tests of module billing
- cli.package.wholeIn package mode,
hd build,hd check, andhd testwithout a FILE work on the whole package. - cli.package.fileWith a FILE in the package, they work on that file's module, linked with the rest of the package.
- cli.package.no-rootIn package mode,
hd check FILEandhd test FILEtreat a FILE under no root as a single-file program, ashd FILEdoes. The roots are the source root, the test root, andtasks. - cli.check.defaultA whole-package
hd checkchecks whathd buildcompiles, the library and the executables, and no test code, as Cargo'scargo checkdoes without--all-targets. - cli.check.tests
hd check --testsalso checks the package's test code: itstests:blocks, test modules, and integration test modules. - cli.check.tests.doc
hd check --testsalso checks the package's doc tests, except compile-fail ones, which onlyhd testjudges. - cli.check.all
hd check --allchecks the library, the executables, the test code, and the package's tasks. - cli.build.directoryThe build directory is the directory
buildin the package directory, wherehdwrites its build and cache output. - cli.build.outputA whole-package
hd buildwrites each executable NAME tobuild/debug/NAME.wasm, or tobuild/release/NAME.wasmwith--release. - cli.build.output.file
hd build FILEwrites FILE's module tobuild/debug/files/STEM.wasm, or tobuild/release/files/STEM.wasmwith--release. STEM is FILE's name without its.hdextension. - cli.build.library-onlyA whole-package
hd buildof a package with no executable, such as a library-only package, checks the package and writes no.wasmfile. - cli.build.library-only.cacheIt still writes the package's interface and its entries in the compiled cache, so a dependent's next command reuses them.
- cli.build.depth-error
hd build,hd run,hd test, andhd FILEreport the one error that only building a program finds: the instantiation limit'sinstantiation-too-deep. - cli.check.no-depth-error
hd checkbuilds no program, so it does not report the instantiation limit. It does report a generic call cycle that grows. A package thathd checkaccepts may still failhd build. - cli.build.facts-at-run-timeChecking or building a program evaluates no fact. A fact's panic is the runtime panic
fact-evaluation-failedof the running program that reads it.
hd check # 0: checking compiles no instantiation
hd build # 101 with instantiation-too-deep, past the instantiation limit
Why. The
filesdirectory keeps a FILE's module apart from the executables, sohd build src/shop.hdnever overwrites the output of executableshop.
Why. Instantiations need the bodies of other modules and packages.
hd checkchecks each module against its dependencies' interfaces alone, so its result never waits on them.
#Check Output
hd check --max-errors 5 # the first five errors, then the summary
hd check --summary # a count per code and file, in place of each diagnostic
- cli.check.max-errors
hd check --max-errors Nstops printing diagnostics after the Nth error. N is a whole number of at least 1. - cli.check.max-errors.summaryThe summary still counts every error and warning, and the exit status is unchanged.
- cli.check.summary-modeIn text output,
hd check --summaryprints, in place of each diagnostic, one line for each pair of a code and a file, with its count. The summary follows. - cli.check.output-options
--max-errorsand--summarychange only what is printed. They change no diagnostic, count, or exit status. - cli.check.report.streamIn text output,
hd checkwrites its diagnostics and its summary to standard error, and nothing to standard output. - cli.check.report.summaryThe last line is the summary:
check result: ok.when the run reported no error, orcheck result: FAILED.when it did, thenerrors: E; warnings: Wwith both counts, zero counts included. - cli.check.summary-mode.lineA
--summaryline is the severity, the file, the code, and the count, each followed by:except the count, as inerror: src/cart.hd: type-mismatch: 2. The file is written as in a diagnostic. - cli.check.summary-mode.order
--summarylines are in file-path order, then in code order.
hd check --summary
# error: src/cart.hd: type-mismatch: 2
# error: src/tax.hd: unknown-name: 1
# check result: FAILED. errors: 3; warnings: 0
Why. An agent with a hundred errors reads the first few, fixes them, and checks again. The counts per code show at a glance whether one mistake caused them all.
#Test Runs
hd test --filter "sums prices" # only the test cases whose name holds it
hd test --filter doc # only the doc tests
hd test --filter text.slugify # only the doc tests of slugify in src/text.hd
hd test --seed 42 # every property test starts from seed 42
- cli.test.file-empty
hd test FILEis an error when FILE registers no test case. - cli.test.package-emptyA whole-package
hd testthat registers no test case passes. - cli.test.filter
hd test --filter PATTERNruns only the test cases whose name contains PATTERN, with a FILE or without one. - cli.test.filter.none
hd test FILE --filter PATTERNis an error when no test case of FILE has a name that contains PATTERN, as when FILE registers none. - cli.test.builds-executables
hd testbuilds the package's executables before it runs any test case, so an integration test may run them withhd_run!. - cli.test.processFor each integration test module,
hd testbinds the host traitProcessto a provider. That provider starts the package's executables, each by its name, ascli.exe.tablenames it. - cli.test.process.tasksThat provider also starts each task of the package by its name, as
hd run NAMEdoes. A name names at most one program, bycli.task.name-clash. - cli.test.process.unknownThat provider returns
.Err(ProcessError.NotFound(program))for a program name that names neither an executable nor a task of the package. - cli.test.process.cwdEach executable that provider starts runs with the package directory, the directory of its
hd.toml, as its working directory. - cli.test.process.decodeThe provider decodes the executable's standard output and standard error as UTF-8, and replaces each byte sequence that is not valid UTF-8 with U+FFFD.
- cli.test.runnerWhen it runs a test case,
hd testbinds the host traitsTestRunnerandPropertyRunnerfor thestd.testingcode around the body, bystd-testing.runner.binding. - cli.test.doc.default
hd testruns the package's doc tests with its other test cases. With a FILE, it runs the doc tests of that file's module. - cli.test.doc.nameA doc test is named
doc <module>.<item>[i], by the table below. - cli.test.doc.filter
--filtermatches a doc test by that name, so--filter docselects every doc test, and--filter text.slugifythe doc tests ofslugifyinsrc/text.hd. - cli.test.doc.locationA diagnostic or a failure of a doc test names the
.hdsource file and a##line of the doc test's block. - cli.test.doc.jsonWith
--format json, a doc test's test object holds that name inname, and its diagnostics give that file and line infileandline. - cli.test.doc.name.rootFor a block in
src/lib.hd,<module>ispkg, which names the package root module bymodule.path.lib-file. - cli.test.doc.name.memberFor a block on a member,
<item>isType.member, so the first block onnewofSluginsrc/text.hdisdoc text.Slug.new[0]. - cli.test.doc.name.moduleA block in a module's module documentation has no
<item>and no dot. The first such block insrc/text.hdisdoc text[0]. - cli.test.doc.update-prefixAn update run, as
hd test --updatemakes, rewrites a failing doc testsnapshot's expected text in place, inside its block's##lines, keeping each line's##prefix and indentation. Thecli.test.snapshot.update.*rules apply to it as to a call in code. - cli.test.snapshot.updateAn update run rewrites the
expectliteral of a failingsnapshotcall in place, to the text that the call compared, ascli.test.doc.update-prefixdoes for a doc test. - cli.test.snapshot.update.passingAn update run leaves a passing
snapshotcall as it is, and writes no file that holds no rewritten literal. - cli.test.snapshot.update.formThe new literal is a single-line string literal in the form of
std-format.debug.source.string, whatever form the old literal had. - cli.test.snapshot.update.omittedA failing
snapshotcall with noexpectargument getsexpect=and the new literal, after its last argument. - cli.test.snapshot.update.rowsAn update run does not rewrite a
snapshotcall that fails in anit_each,it_prop, orit_prop_withtest case, since the rows or inputs of one call share its literal. Those test cases fail as they do without--update. - cli.test.snapshot.update.conflictWhen failing test cases of one update run want different texts at one
snapshotcall, the call is left as it is, and those test cases fail. - cli.test.snapshot.update.continueIn an update run, a failing
snapshotcall of anittest case records its new text, asstd-testing.snapshot.updatesays, and the test case goes on. So one update run rewrites every failingsnapshotcall of the test case. - cli.test.snapshot.update.acceptedA
snapshotcall that an update run rewrites is no failure of its test case. The run's counts and exit status include only its other failures, so a run whose only failures were rewritten snapshots exits 0. The run prints the file and line of each rewritten call. - cli.test.snapshot.update.unfixableA failing
snapshotcall that an update run cannot rewrite fails its test case. That is a call the run cannot find in its file, and a call in a file that changed during the run. - cli.test.every-caseIn every output mode,
hd testruns every selected test case of every file and reports each failure. A failing test case does not stop later ones. - cli.test.seed
hd testderives a property test case's base seed, theseedof its first case, from the test case's name. The same name gives the same base seed on every run. - cli.test.seed.flag
hd test --seed Nmakes N the base seed of every property test case it runs. N is a whole number. - cli.test.seed.reportThe report of a failing property test case names its base seed, so a run with
--seedand that seed draws the same cases. - cli.test.leakA test case whose run otherwise passes fails when a host handle that the run opened is still open at its end.
- cli.test.leak.messageIts failure message names each kind of handle left open, such as
TcpListener, with its count. - cli.test.leak.closed
hd testcloses those handles once the run ends, so a later test case never sees them. - cli.test.leak.runsEach run of a test case counts alone: a row of an
it_eachtest case, and each case of a property test case. - cli.test.leak.abandonedAbandoning a suspension closes no handle, by
req.abandon.host. A handle that an abandoned cleanup left open is still open at the run's end, and fails the test case bycli.test.leak. - cli.test.cancelledA test case whose body turns out cancelled, by
module.testing.cancelled, fails with the messagecancelled.
| Part | Value |
|---|---|
<module> |
the path of the module whose comment holds the block, such as text for src/text.hd, and pkg for src/lib.hd |
<item> |
for a block on a declaration, the name of the documented declaration, such as slugify; for a member, the declaration's name, a dot, and the member's name, such as Slug.new |
[i] |
the block's index among that item's doc tests, from 0, as in the name[i] of an it_each row |
So the first hd block on slugify in src/text.hd is doc text.slugify[0],
and the JSON line of its result is
{"kind":"test","name":"doc text.slugify[0]","outcome":"passed","message":""}.
The first block on slugify in src/lib.hd would be doc pkg.slugify[0].
Why. Naming a FILE asks for its tests, so none is a mistake, while a new package may have none yet. A filter that matches nothing in a named FILE is most often a typo, so it does not pass silently.
Why. A handle left open is reported from the host's own table of handles, never by a finalizer, so the test case that opened it fails, next to its cause. A
usingbinding or an explicit close fixes it.
#Text Report
FAIL tests/cart.hd:18: returns a failed exit code
`report()` returned `ExitCode(3)`
repro: hd test tests/cart.hd --filter "returns a failed exit code"
PANIC tests/cart.hd:24: sums prices
panic: assertion-failed: total: actual 9, expected 10
tests/cart.hd:25 pkg.cart/$test.2
repro: hd test tests/cart.hd --filter "sums prices"
IGNORED tests/cart.hd:28: calls the live service (needs credentials)
test result: FAILED. 1 passed; 2 failed; 1 ignored
- cli.test.report.passIn text output, a passing test case has no result line.
- cli.test.report.headingA failed case starts with
FAILorPANIC, its registration's file and line, and its name. - cli.test.report.failureA
FAILblock prints the failure message, indented by four spaces. - cli.test.report.reproEach failure block ends with
repro: hd test FILE --filter "NAME", indented by four spaces and command-line escaped. - cli.test.report.panicA
PANICblock printspanic: CATEGORY: MESSAGE, indented by four spaces. - cli.test.report.frameIts symbolized frames follow as
FILE:LINE FUNCTION, indented by four spaces and with the panic site first. - cli.test.report.ignoredAn ignored case prints
IGNORED, its registration's file and line, its name, and its reason. - cli.test.report.summaryThe final line gives the run's verdict and its passed, failed, and ignored counts, including zero counts.
- cli.test.report.filterResult blocks and summary counts include only cases selected by
--filter. - cli.test.report.orderResult blocks use file-path order, then registration order, then row-index order, regardless of completion order.
- cli.test.report.streamA ready result block is written once every earlier selected case has finished, without waiting for the whole run.
See also: Machine Output, Exit Status.
#Affected Tests
hd test # every test passes, so each test program's fingerprint is recorded
hd test --affected # after an edit to src/billing.hd: only the programs that reach it
- cli.test.programA test program is a program that
hd testbuilds to run test cases: one per module with unit tests or doc tests, and one per integration test module. - cli.test.fingerprintA test program's fingerprint changes whenever a change could change its outcome: a source file it reads, a dependency, a manifest setting, the
hdversion, or a test option such as--seed. - cli.test.affected
hd test --affectedruns only the test programs whose fingerprint differs from the one recorded when they last passed, or that have no record. - cli.test.affected.record
hd testrecords a test program's fingerprint only when every test case of the program ran and passed. A filtered run, or one in which a test case of the program fails, records nothing for it. - cli.test.affected.failedWhen a test case of a test program fails,
hd testremoves the program's record. So--affectedruns that program until it passes in full. - cli.test.affected.whereThe records live in the build directory, so after
hd clean,hd test --affectedruns every test program.
Why. An agent edits one module and wants its feedback in seconds. A record is written only for a whole pass, so a test that failed or never ran is never skipped.
#Test Environments
The kind of a test case decides what hd test gives it, never where its
file lies:
| Test case | Host providers | Capability grant | Working directory | Program arguments | Standard input |
|---|---|---|---|---|---|
a unit test, in src, tasks, or a single file |
TestRunner alone |
none | not reachable | not reachable | not reachable |
| an integration test case, or a doc test | the default profile, TestRunner, and the test runner's Process |
the test grant | the package directory | none | closed |
- cli.test.env.unit
hd testbindsTestRunneralone for a unit test, in a package or outside one, bymodule.testing.unit-row.places. - cli.test.env.integrationFor an integration test case or a doc test,
hd testbinds the traits of the default profile that the body's row names. It binds them ashd rundoes, except as this list says. - cli.test.env.cwdSuch a test case runs with the package directory, the directory of its
hd.toml, as its working directory. A relative path such asfixtures/orders.csvthen names a file of the package. - cli.test.env.argsIts
Argsprovider holds no program arguments, solistreturns an empty list. - cli.test.env.args.programIts
Args.programreturns the path of the test case's file relative to the package directory, such astests/report.hd. - cli.test.env.stdinIts standard input is closed, so every
read_line!returns.Ok(.None), as at the end of input. - cli.test.env.temp-dir
hd testgives each test case its own fresh, empty temporary directory, whichtemp_dirreturns. No other test case shares it, in the same run or in another run at the same time. - cli.test.env.temp-dir.removed
hd testremoves that directory and everything in it once the test case ends, whether it passed or failed. - cli.test.env.grant.combinedAn integration test case or doc test gets the test grant, combining both root grant tables before Grant Precedence applies command flags.
- cli.test.env.grant.fs-readWhen neither the table nor a flag names
FsRead, it covers the package directory and the test case's temporary directory. - cli.test.env.grant.fs-writeWhen neither the table nor a flag names
FsWrite, it covers the test case's temporary directory. - cli.test.env.grant.fs-addedWhen they give
FsReadorFsWritea list, the entries of the two rules above are added to it.
hd test # tests/report.hd reads fixtures/orders.csv from the package
cd src && hd test # the same: the working directory is still the package directory
# hd.toml
[dev-capabilities]
Http = ["localhost"]
hd test # Http reaches localhost alone; FsRead covers the package and the temporary directory
hd test --cap Http=true # this run reaches every host
Note. A
$.withprovider scope inside an integration test case still covers the calls in it, so$.with(Clock=ManualClock::new(start))replaces the real clock there.
Why. An integration test checks the package on the real system, so it gets the providers that
hd rungives. Fixed arguments, a closed standard input, and the package directory make its result independent of how and wherehd testran. A directory of its own lets two test cases write files without meeting, as Go'st.TempDirdoes.
Why. A fixed test grant reads the package and writes only the test case's own directory, with no setup per test. A unit test runs on fakes alone, so it needs no grant.
#Testing Tasks
- cli.test.tasksA whole-package
hd testalso runs the test cases of thetests:blocks in the package's tasks and shared task modules. They are unit tests. - cli.test.tasks.file
hd test tasks/NAME.hdruns the test cases of taskNAME, bycli.package.file. - cli.test.tasks.no-testsA whole-package
hd testcompiles no test build of a task, or of an executable's entry module, that holds notests:block. - cli.test.tasks.runAn integration test runs a task with
hd_run!, bycli.test.process.tasks.
hd test # also the tests: block of tasks/seed.hd
hd test tasks/seed.hd # only the tests of task seed
Why. A test build runs no entry behavior, so a script's top level must be requirement-free in it (
module.init.tests.initialization-empty-row). A task without tests then never failshd testfor printing at its top level.
#Linting
hd lint is an explicit, read-only request for advice about accepted code.
It keeps advisory findings separate from errors that reject a program.
hd lint
hd lint src/main.hd --format json
hd lint --deny unsigned-comparison-always
hd lint --all --allow unused-local-binding
- cli.lint.select
hd lintselects files, packages, workspace members, tests and tasks by the same selection rules ashd check. - cli.lint.options
hd lintaccepts--format,--summary,--max-errors,--jobs,--tests,--all, and-pas checking does. - cli.lint.prerequisites
hd lintreports ordinary compiler errors before running analyses that need successfully checked code. - cli.lint.read-only
hd lintruns no program and changes no source, manifest, or lockfile. - cli.lint.ordinary-errors
hd check,hd build,hd test, REPL evaluation, and the playground report compiler errors only. - cli.lint.report-commandOnly
hd lintreports the optional findings listed in the diagnostic registry. - cli.lint.defaultEach registered lint runs at its declared default level.
- cli.lint.levelsA lint level is allow, warn, or deny, with the effects in the table below.
- cli.lint.exact-names
--allow CODE,--warn CODE, and--deny CODEtake exact registered lint names and may repeat. - cli.lint.unknown-nameAn unknown lint name is an error, including a name in a site suppression.
- cli.lint.cli-conflictContradictory CLI levels for one lint are an error regardless of argument order.
- cli.lint.manifestThe selected packageโs
[lint]table acceptsallow,warn, anddenylists of exact lint names. - cli.lint.manifest-invalidA non-list policy value, non-string list element, or conflicting manifest levels for one lint is an error.
- cli.lint.cli-overrideAn explicit CLI level overrides the selected packageโs manifest level for that exact lint.
- cli.lint.no-inheritA package inherits no parent-directory or workspace lint policy implicitly.
- cli.lint.standaloneA standalone file uses registered defaults and explicit CLI levels.
- cli.lint.dependency-policyA dependency cannot impose lint policy on its caller.
- cli.lint.site-allowA full-line
# lint-allow: CODEimmediately before a statement suppresses that code for calls directly owned by that statement. - cli.lint.site-scopeA declarationโs site suppression never extends into its nested function or closure body.
- cli.lint.site-precedenceA site suppression overrides manifest and CLI deny for its code.
- cli.lint.errors-unsuppressibleLint levels and suppressions never suppress compiler errors or make invalid code valid.
- cli.lint.orderFindings are ordered by package-relative path, source span, then stable lint code.
- cli.lint.outputLint reuses the diagnostic objects and final summary of machine output.
- cli.lint.text-streamLint text diagnostics and summaries go to standard error; JSON output goes to standard output.
- cli.lint.totalsDiagnostic truncation changes no total count or exit status.
- cli.lint.status-warningA successful lint run with only warning-level findings exits with status 0.
- cli.lint.status-errorA lint run with a compiler error, denied finding, invocation error, or configuration error exits with status 101.
- cli.lint.stable-messagesMoving an existing warning into lint preserves its stable diagnostic code, message, source span and fix-it information.
| Level | Effect |
|---|---|
allow |
Suppresses that lint finding. |
warn |
Reports a warning and does not change an otherwise successful exit status. |
deny |
Reports the finding as an error and makes lint fail. |
[lint]
allow = ["unused-local-binding"]
warn = ["redundant-let-mut"]
deny = ["unsigned-comparison-always"]
Why. Choosing advice is a tooling policy. It must not alter program validity, execution, or the compiler errors that every command reports.
See also: Machine Output, Exit Status, Diagnostics.
#Build Profiles
A build profile decides whether integer overflow panics or wraps:
hd run # debug: overflow panics
hd run --release # release: overflow wraps
hd --release notes.hd # release: overflow wraps
hd test # always a checked build
hd test --release # still checked, with optimized code
- cli.profile.default
hd FILE,hd run,hd build,hd test, and the REPL use the debug profile, which panics on integer overflow bytypes.arith.checked. - cli.profile.release
--releaseonhd buildandhd runselects the release profile, which wraps on integer overflow bytypes.arith.release. - cli.profile.release.file
--releaseonhd FILEselects the release profile too. - cli.profile.test
hd testalways uses a checked build, the test profile, including for the executables it builds bycli.test.builds-executables. - cli.profile.test.release
hd test --releasekeeps the test profile, so integer overflow still panics. The flag selects only the pipeline. - cli.profile.release.commands
hd FILE,hd run,hd build, andhd testtake--release. No other command takes it.
Why. Cargo has the same two profiles, and checks overflow in one only. Release builds stay fast, since a check costs speed on every arithmetic operation. Tests are always checked, so a property test finds an overflow that a release build would hide.
#Pipelines
A pipeline is the way an implementation generates code for a build. It is separate from the profile:
hd test # the test profile; code that builds fast
hd test --release # the test profile; optimized code, the same results
- cli.profile.pipeline.release
--releaseselects the implementation's optimized pipeline on every command that takes it. Without the flag, a command may use a pipeline that builds faster and runs slower. - cli.profile.pipeline.unobservableThe pipeline is unobservable. In one profile, two pipelines give the same output, exit status, panic categories, and panic sites.
- cli.profile.pipeline.limitsOnly speed, memory use, the depth at which the stack runs out, the point at which the heap runs out, and backtrace frames may differ between pipelines.
- cli.profile.pipeline.oneAn implementation may have one pipeline only.
Why. The profile is part of a program's meaning: a release build wraps where a checked build panics. The pipeline is not. So
hd test --releasecan test optimized code without dropping the checks, as Zig'sReleaseSafemode does.
#Engines
An engine is what runs a program's compiled module. An implementation may offer several, and a command picks one by name:
hd run --engine NAME # NAME is an engine the implementation offers
HD_ENGINE=NAME hd test # the same choice, from the environment
hd run --engine no-such-name # a usage error: exits 101 and runs no program
- cli.engine.defineAn engine is the runtime that executes a program's compiled module.
- cli.engine.offeredAn implementation offers one or more engines. Which engines exist, and what they are called, is implementation-defined.
- cli.engine.flag
hd FILE,hd FILE.wasm,hd run,hd build, andhd testtake--engine NAME, where NAME names an engine. - cli.engine.selectA command that takes
--engineuses the engine that the flag names. Without the flag, it uses the engine that the environment variableHD_ENGINEnames. Without both, it uses the default engine. - cli.engine.env-emptyAn empty
HD_ENGINEcounts as unset. - cli.engine.defaultWhich engine is the default is implementation-defined.
- cli.engine.build
hd build --engine NAMEbuilds the module for that engine. A module thathd buildwrote runs on every engine, whichever engine it was built for. - cli.engine.same-behaviorA program's output, exit status, panic categories, and panic sites are the same on every engine, as
cli.profile.pipeline.unobservablesays of pipelines. - cli.engine.differencesBetween engines, only what
cli.profile.pipeline.limitslists may differ. - cli.engine.unknownA NAME that the implementation does not offer is a usage error, by
cli.exit.hd-failure. That includes an engine that this build lacks. - cli.engine.unknown.no-runA command that rejects its engine name runs no program, and never falls back to another engine.
Why. Engines differ in speed and in what they cost, not in what a program means. So a user, CI, or an agent picks one by name, and a program's results do not depend on the choice.
#Code Generation
- cli.codegen.singleEvery command that builds a program uses the implementation's one code generator.
- cli.codegen.no-optionCommands take no code-generator selection option; such an option is a usage error by
cli.exit.hd-failure.
Note. This implementation stores values in linear memory and frees counted objects through reference counting and cycle collection.
#Debug Output
A dbg call writes debug lines,
which each command keeps apart from the program's output:
hd run # dbg lines go to standard error
hd test # a failing test case shows its dbg lines
hd build --release # error: dbg-in-release
- cli.dbg.run
hd FILEandhd runwrite eachdbgline to standard error, never to standard output. - cli.dbg.test
hd testkeeps thedbglines of each test case with that test case's output. It shows them with the failure when the test case fails, and drops them when it passes. - cli.dbg.replIn the REPL, the
dbglines of an input print before the input's value line. - cli.dbg.release
hd build --releaseandhd run --releasereject adbgcall in the user's own code, bymodule.dbg.release, andhd run --releasethen runs nothing. - cli.dbg.release.file
hd --release FILErejects such a call too, and then runs nothing. - cli.dbg.test-release
hd test --releaseaccepts adbgcall, since it builds in the test profile bymodule.dbg.release.debug-build. - cli.dbg.dependency.lintLint reports fetched-package
dbguse once per package, bymodule.dbg.dependency.lint. Warning:dbg-in-dependency.
Note. The browser playground shows
dbglines in its output panel, marked as debug output.
#Installing Executables
hd install builds a package's executables and gives each a command on PATH:
hd install ./tools --root ./my-hd
hd install --list --root ./my-hd
hd uninstall tool --root ./my-hd
- cli.install.source
hd install SOURCEselects the package named by SOURCE independently of the caller's package. - cli.install.resolveSOURCE follows the path, repository and version-query forms of dependency targets, without requiring a library.
- cli.install.executablesInstallation builds every executable of the selected package.
- cli.install.no-executableA source without a package manifest or without executables is an error.
- cli.install.release-wasmEach installed executable is release Wasm with a launcher through the current
hdruntime. - cli.install.no-executionInstallation runs no guest entry, test, package provider or installation hook.
- cli.install.rootThe installation root is
--root DIR, elseHD_INSTALL_ROOT, else.hdin the user's home directory. - cli.install.layoutLaunchers live under
ROOT/bin, and managed artifacts and installation records underROOT/installed. - cli.install.path-guidanceInstallation prints PATH guidance without editing shell startup files.
- cli.install.launcher-runtimeEach launcher selects
hdfrom PATH when invoked. - cli.install.launcher-argumentsEach launcher forwards every argument unchanged as application data.
- cli.install.launcher-streamsInstalled programs inherit the caller's standard input, standard output and standard error.
- cli.install.launcher-statusThe installed command returns the program's exit status.
- cli.install.launcher-cwdThe installed program keeps the caller's working directory.
- cli.install.program-nameIts
Args.programis its installed executable name. - cli.install.no-checkoutThe launcher and loader require no original source checkout at invocation.
- cli.install.namesInstalled executable names must be portable flat filenames, distinct without ASCII case, other than
hdand Windows device names. - cli.install.list
hd install --listlists one row per installed executable, ordered by name. - cli.install.list-fieldsEach row contains name, canonical source identity and revision, separated by tabs.
- cli.install.ownerA local source's identity is its canonical directory; a remote source's identity is its canonical host path.
- cli.install.updateReinstalling the same source updates its commands after every executable builds and validates successfully.
- cli.install.removed-executablesAn update removes formerly installed commands that its source no longer declares.
- cli.install.collisionInstallation refuses an unmanaged command or a command owned by a different source.
- cli.install.modified-launcherInstallation and removal refuse a managed launcher whose contents were replaced or modified.
- cli.install.failed-validationBuild, capability-check and collision failures preserve previously installed commands.
- cli.install.manifest-grantsInstalled programs use the package's installation-time
[capabilities]table, with no additional grant policy. - cli.install.path-scopesRelative filesystem grants resolve against the source package directory at installation.
- cli.install.show-grantsInstallation and update display the package's capability table.
- cli.install.denyRepeatable
--deny CAPrefuses that installation when the package table grants CAP. - cli.install.deny-once
--denyis not stored and affects no later installation or invocation. - cli.install.provider-unavailableInstallation of an executable using package-owned JavaScript providers is refused with a diagnostic naming the unsupported provider.
- cli.install.record-versionAn installed loader validates its installation record format before guest initialization.
- cli.install.abiIt validates an explicit Wasm/runtime ABI revision before guest initialization.
- cli.install.hashIt validates the artifact's SHA256 before guest initialization.
- cli.install.incompatibleAn incompatible ABI or artifact hash mismatch refuses execution and gives an explicit reinstall command.
- cli.install.no-implicit-rebuildInvocation never fetches or rebuilds an incompatible installed program automatically.
- cli.uninstall.one
hd uninstall NAMEremoves one installed executable's managed launcher and record. - cli.uninstall.payloadRemoval reclaims payloads that no installed executable still references.
- cli.uninstall.root
hd uninstalltakes the same root selection ashd install.
Why. A versioned record keeps compatibility checks separate from the Wasm artifact. Package provider installation waits for a portable provider graph; retaining an absolute checkout path would break independent installs.
#Documentation
hd doc writes a package's documentation, built from its declarations and
their ## documentation comments:
hd doc # writes the pages into the build directory's doc/
hd doc --open # also opens index.html, the entry page
hd doc --private # also documents the private items
hd doc --out site # writes the pages into site/
hd doc text.slugify # prints the Markdown of one item
hd doc std.text.split # prints a std item and links to its spec
A module's page looks like this, in Markdown:
# Module `text`
Text helpers for URLs and titles.
- [`slugify`](#slugify): Turns a title into a URL slug.
<a id="slugify"></a>
## `slugify`
```hd
pub fn slugify(title: string) -> string
```
Turns a title into a URL slug: each space becomes a hyphen.
See `Slug` for a checked slug.
Example `doc text.slugify[0]`:
```hd
use pkg.text.{slugify}
use std.testing.assert_equal
assert_equal(slugify("Ship It Now"), "Ship-It-Now", reason="each space becomes a hyphen")
```
#Scope
- cli.doc.command
hd docwrites the HTML and the Markdown of the package's documentation together. No flag selects one format. - cli.doc.package
hd docworks on the package of package mode, and is an error outside any package. - cli.doc.workspaceIn workspace mode,
hd docneeds-p NAME, by Selecting Members, to select one member. Without it,hd docis an error. - cli.doc.check
hd docchecks the package ashd checkdoes. When that reports an error,hd docwrites and prints no documentation. - cli.doc.markdownThe text of a documentation comment is Markdown, as CommonMark defines it.
- cli.doc.items
hd docdocuments eachpubitem of every module under the source root, with thepubmembers of each, as the table below lists. Items are functions, structs, enums, traits, and type aliases. - cli.doc.private
--privatealso documents the private items and members. It applies to the package only, and tohd doc NAMEas to the pages. - cli.doc.mainAn application's
src/main.hdis no library module, so it has a page only with--private. That page is the modulemain, asmain.mdandmain.html. - cli.doc.reexportA
pub useof a module is listed under that module, with a link to the page of the module that declares the item. It copies no signature and no doc, since the item keeps its identity.
| Item | The signature block holds |
|---|---|
| function | the declaration through its result type and requirement row, with no body |
| struct | the declaration head and its pub fields; the line # private fields omitted stands for the private ones |
| enum | the declaration head and its variants with their payloads |
| trait | the declaration head and the signature of each method |
| type alias | the whole declaration |
pub method of an inherent impl |
the method's signature, under the type's heading |
hd doc # writes build/doc, HTML and Markdown together
hd doc --private # the private items and members too
#Output
- cli.doc.dir
hd docwrites into the directorydocof the build directory, asbuild/doc.--out DIRwrites into DIR instead. - cli.doc.open
--openopens the entry pageindex.htmlof the output with the platform's default program, after the files are written. - cli.doc.open.failureFailure to launch the browser for
hd doc --openis a command failure with non-zero status. - cli.doc.open.failure.pathThe failed command still prints the documentation path and retains the files already written.
- cli.doc.filesThe output holds the files of the table below. A module's HTML and Markdown pages sit side by side.
- cli.doc.root-pageThe root module's pages are named
pkg, asmodule.path.reserved-pkgreserves the name, so no user module's page takes their file names. - cli.doc.index-moduleA top-level module named
indexhas its pages atindex/index.htmlandindex/index.md, so the entry page keeps the namesindex.htmlandindex.md. - cli.doc.relativeA link between the output's files is a relative path, so the output works from any base path.
- cli.doc.htmlA module's HTML page holds everything its Markdown page holds, under the same anchors. Its styling and navigation are not specified.
| Files | Hold |
|---|---|
index.md, index.html |
a small entry page: the package name, a link to the root module pkg when src/lib.hd exists, and the list of the other modules |
pkg.md, pkg.html |
the root module pkg, which is src/lib.hd; present only when src/lib.hd exists |
PATH.md, PATH.html |
one pair for each other module under the source root; PATH is the module path with each . a /, so module shop.cart is shop/cart.md; a top-level module index is the exception of cli.doc.index-module |
llms.txt |
the index for agents, by cli.doc.llms |
llms-full.txt |
the whole documentation in one file, by cli.doc.llms-full |
build/doc/index.html # the entry page
build/doc/pkg.md # the root module, beside pkg.html
build/doc/shop/cart.md # module shop.cart, beside shop/cart.html
build/doc/llms.txt # the index for agents
#Pages
- cli.doc.pageA module's Markdown page starts with the heading
# Module `PATH`, which reads# Module `pkg`for the root module. Then come the module's documentation and a list of its items. Each list entry links to the item's anchor and ends with the item's summary. - cli.doc.summaryAn item's summary is the first sentence of its documentation, or empty when it has none.
- cli.doc.itemEach item has a second-level heading with its name in code, and each member a third-level heading with
Type.memberin code. Below the heading come the signature, the documentation, and the examples. - cli.doc.anchorAn item's anchor is its item name, as
cli.test.doc.nameforms<item>, such asslugifyorSlug.new. - cli.doc.signatureA signature is the source text of the declaration, copied as written, in a fenced block whose info string is
hd. - cli.doc.implsA type lists the traits it implements, one line for each implementation head. That includes implementations in other modules of the package. A derived implementation is marked
(derived). - cli.doc.conciseA page holds no function body and no navigation text.
- cli.doc.exampleEach doc test appears in place, as written, in an
hdblock. The wordExampleand the doc test's name in code, ascli.test.doc.nameforms it, come before the block. - cli.doc.example.compile-failA compile-fail doc test is labelled
Does not compile (CODE), with its error code, and its block keeps the# error: CODEline.
# shop/cart.md, the page of module shop.cart:
# # Module `shop.cart`
#
# ## `total`
#
# ```hd
# pub fn total(cart: Cart) -> i64
# ```
#
# Sums the prices.
#Pages For Agents
- cli.doc.llms
llms.txtstarts with a heading of the package's name. Then it holds a blockquote with the summary of the root module's documentation, and a## Moduleslist. - cli.doc.llms.modulesThe list has one entry for each module,
pkgfirst: a link to its.mdpage, a colon, and the summary of the module's documentation. The link of a top-level moduleindexis toindex/index.md. - cli.doc.llms-full
llms-full.txtholds the Markdown of every module page, the root module first and the others in path order.
# build/doc/llms.txt:
# # shop
#
# > Tools for the shop's catalog.
#
# ## Modules
# - [pkg](pkg.md): Tools for the shop's catalog.
#Links
- cli.doc.link.formA documentation comment links to an item with the Markdown shortcut
[`NAME`], as in[`Slug.new`]. A Markdown link with a target is ordinary Markdown and is not checked. - cli.doc.link.scopeNAME resolves in the module scope of the documented item: its own declarations and the names its
usedeclarations bind. A dotted NAME names a member or a path through modules. - cli.doc.link.resolvedA resolved link becomes a link to the target's anchor, on the target's page.
- cli.doc.link.externalA NAME that resolves to a
stditem links to the page for that item in the standard library reference. A NAME that resolves into a dependency is checked, but shows as plain code. - cli.doc.link.broken.lintAn unresolved documentation NAME or an undocumented private target produces a lint finding. Warning:
broken-doc-link. - cli.doc.link.unresolved-textDocumentation renders an unresolved NAME or an undocumented private target as plain code.
# in the doc comments of src/text.hd:
# /// Reads what [`slugify`] wrote. # a link to #slugify on this page
# /// See [`std.json.parse`]. # a link to the standard library reference
# /// See [`sluggify`]. # warning: broken-doc-link
#Looking Up One Item
- cli.doc.name
hd doc NAMEprints the Markdown of one item to standard output, and writes no file. It prints the item's section of the module page, from its heading to the next heading of the same or a higher level. - cli.doc.name.formNAME is a module path and an item name joined by a dot, as in
text.slugifyortext.Slug.new. The module is the longest prefix that names a module, andpkgnames the root module. - cli.doc.name.moduleA NAME that names only a module prints that module's whole Markdown page.
- cli.doc.name.dependencyA NAME that starts with
dep.and a dependency's key, as indep.json.parse, names an item of that dependency. Only itspubitems are found. - cli.doc.name.stdA NAME that starts with
std., as instd.text.split, names an item of the standard library:std, a module, and an item of that module. - cli.doc.name.std.outputIt prints the item's signature in an
hdblock, its summary, and a link to the item's section of the standard library reference. - cli.doc.name.missingA NAME that names no documented item is an error. That includes a
stdNAME that names no item the standard library reference specifies.
hd doc text.slugify # the slugify section of text.md, on standard output
hd doc text # the whole page of module text
hd doc std.json.parse # parse's signature, summary, and reference link
Why. The Markdown pages let an agent fetch one module in one request, and
hd doc NAMEanswers a lookup without any file. A broken link is a lint finding, so a rename does not block a build. Anchors equal doc-test names, so one name finds the item, its page section, and its examples.
See also: Doc Tests, Documentation Comments, Creating A Package.
#Text Output
Without --format json, a diagnostic's fix-its follow it in text output:
hd check shape.hd
# error: shape.hd:4:14: trait-used-as-type: `Shape` is a trait, not a type
# fix: insert `dyn` before the trait name
- cli.text.fix-lineIn text output, each fix-it of a diagnostic prints on its own line after the diagnostic's line: four spaces,
fix:, and the fix-it's message, ascli.json.fix.objectgives it.
Note. The rest of a text diagnostic's layout is the implementation's. Machine Output is the stable form, and it also holds each fix-it's edits.
#Machine Output
hd check --format json
# {"kind":"diagnostic","code":"type-mismatch","severity":"error","message":"...","file":"src/cart.hd","line":3,"column":5}
# {"kind":"summary","errors":1,"warnings":0,"passed":0,"failed":0,"ignored":0,"status":101}
- cli.json.commands
hd build,hd check,hd test,hd run,hd doc,hd FILE, and the dependency commands take--format json. - cli.json.lines.buildWith it,
hd build,hd check,hd test, and the dependency commands write JSON lines to stdout: one JSON object per line, and no other text. - cli.json.runWith it,
hd runandhd FILEwrite onlyhd's own diagnostics and summary as JSON lines, and write them to stderr. - cli.json.run.programThe program's standard output passes through to stdout untouched.
- cli.json.kindEach object's
kindfield is"diagnostic","test", or"summary". - cli.json.diagnosticEach diagnostic is one object, with its stable code, its severity, and its file and position.
- cli.json.diagnostic.fieldsA diagnostic object has the fields
code,severity,message,file,line, andcolumn. - cli.json.diagnostic.fileIn a package,
fileis the path relative to the package root, assrc/cart.hd, whatever the working directory. Outside a package,fileis the path as written on the command line. At a workspace root,fileis relative to the workspace root, aslib/src/lib.hd, so two members' files stay apart. - cli.json.test.resultEach test case's result is one object, with the test case's name and its outcome: passed, failed, or ignored.
- cli.json.test.result.fieldsA test object has the fields
name,outcome, andmessage.outcomeis"passed","failed", or"ignored", andmessageholds the failure or ignore reason, or""when there is none. - cli.json.test.order
hd testlists the test objects in the order of the files' paths, and within one file in declaration order. - cli.json.summary.resultThe last object is a summary, with the count of errors, warnings, and each test outcome, and the command's exit status. It is written on success too.
- cli.json.summary.result.fieldsA summary object has the counts
errors,warnings,passed,failed, andignored, andstatus, the command's exit status. - cli.json.summary.modules-checkedThe summary object of
hd checkalso has the countmodules_checked: the modules whose bodies this run checked, rather than loaded from the compiled cache. - cli.json.diagnostic.fixesA diagnostic object also has the field
fixes: a list of the diagnostic's fix-its. It is[]when the diagnostic has no fix-it. - cli.json.fix.objectA fix-it is an object with the fields
message, which says what the fix-it does, andedits, a list of its edits. - cli.json.fix.editAn edit is an object with the fields
file,start,end, andtext. It replaces the bytes offilefrom offsetstartup to offsetendwithtext. - cli.json.fix.offsets
startandendcount bytes of the file's UTF-8 text from 0, and the byte atendis not replaced.fileis a path ascli.json.diagnostic.filegives it. - cli.json.fix.disjointThe edits of one fix-it never overlap, so applying them together applies the fix-it.
- cli.json.diagnostic.no-positionA diagnostic that no source text causes, such as
denied-capability, hasnullforfile,line, andcolumn.
hd check --format json
# {"kind":"diagnostic","code":"unknown-name","severity":"error","message":"...","file":"src/cart.hd","line":4,"column":12,
# "fixes":[{"message":"replace 'totl' with 'total'","edits":[{"file":"src/cart.hd","start":61,"end":65,"text":"total"}]}]}
# {"kind":"summary","errors":1,"warnings":0,"passed":0,"failed":0,"ignored":0,"status":101,"modules_checked":1}
Note.
modules_checkedcounts work, not results. A second run with no edit can load every module from the cache, and count 0.
Why. A stream lets an agent act on the first error. The summary makes a clean run explicit, as Cargo's
build-finishedmessage does, and a missing summary means thathddid not finish.
#Exit Status
| Status | Meaning |
|---|---|
| 0 | the command succeeded |
| 1 | hd test: a test case failed; hd fmt --check: a file is not formatted |
| 101 | hd itself failed, and ran no program |
| the program's | hd FILE, hd run: the status of the program |
| 130 | hd FILE, hd run: the program was cancelled or stopped, as a Ctrl-C does |
- cli.exit.successA command that completes with no error exits with status 0. Warnings do not change its status.
- cli.exit.hd-failureWhen
hditself fails, it exits with status 101. It fails when it reports an error, rejects its command line or a manifest, or cannot finish for an internal reason. - cli.exit.programOnce its program is built,
hd FILEorhd runexits with the program's own status, by Exit Status andmodule.entry.panic. - cli.exit.test-failure
hd testexits with status 1 when a test case fails andhdreports no error. - cli.exit.fmt-check
hd fmt --checkexits with status 1 when a file is not formatted andhdreports no error. - cli.exit.cancelled
hd FILEorhd runexits with status 130 when its program is cancelled or stopped, by Cancelling A Run.
hd lint; echo $? # 0: warning-level findings do not change it
hd test; echo $? # 1 when a test case failed
hd run; echo $? # the program's own status
Why. Cargo reserves 101 for its own failures. CI and agents then tell "my code did not compile" apart from "my program failed", which most often exits with 1.
Note. A command-line usage error, such as an unknown flag, has no diagnostic code. Cargo's and Go's usage errors have none either. It still exits with status 101.
#Cancelling A Run
hd run # Ctrl-C: cleanup runs, up to ten seconds
hd run --cleanup-limit 30s # Ctrl-C: cleanup runs, up to 30 seconds
hd run --cleanup-limit 0s # Ctrl-C: cleanup is abandoned at once
- cli.cancel.ctrl-cA Ctrl-C while the program of
hd FILE,hd FILE.wasm, orhd runwaits on the host cancels its entry suspension, byreq.entry.cancel. - cli.cancel.boundThe cleanup bound of that cancel is the
--cleanup-limitvalue. - cli.cancel.flag
--cleanup-limit DURATIONtakes a DURATION of the forms in Program Limits. Those three commands take the flag, before the--of the program arguments. - cli.cancel.defaultWithout the flag, the cleanup bound is ten seconds.
- cli.cancel.zeroA cleanup bound of zero abandons the entry at the first moment it waits on the host after the cancel, by
req.entry.cancel.bound.cooperative. - cli.cancel.secondA second Ctrl-C after the cancel abandons the entry at once, by
req.entry.cancel.abandon. A later one does nothing more. - cli.cancel.computingA Ctrl-C while the program computes stops it, as
cli.repl.stopstops an input. It then writesstoppedto standard error. - cli.cancel.endA cancelled entry, by
req.entry.cancelled, writescancelledto standard error when its cleanup ran to its end, andcancelled, cleanup abandonedwhen it was abandoned. - cli.cancel.statusA run that is cancelled or stopped exits with status 130, its cancellation status, on every platform.
- cli.cancel.signalsOn Unix, a SIGINT or a SIGTERM that is sent to
hdalone is a Ctrl-C in every rule above, on every engine. - cli.cancel.forwardOn the Node engine,
hdpasses each Ctrl-C it hears on to the process that runs the program. That process hears each one once, from a terminal or from a signal sent tohdalone. - cli.cancel.windowsOn Windows,
hdpasses nothing on, since the console delivers a Ctrl-C to every process attached to it, the Node process included. A Ctrl-Break is not a Ctrl-C there on the Node engine, and the in-process engine takes it as one.
hd run; echo $? # Ctrl-C while main waits, and its dispose closes in time
# stderr: cancelled
# 130
kill -INT $(pgrep hd) # a supervisor's signal to hd alone: the same cancel
kill -TERM $(pgrep hd) # SIGTERM too: cleanup runs, exit status 130
Why. 130 is 128 plus SIGINT's number 2, the status a POSIX shell reports for a process that SIGINT ended. Scripts and CI already read it as "interrupted", not as "failed", and one status on every platform keeps them portable. Ten seconds is what
docker stopwaits by default before it kills: long enough for a network close, and short enough that a person does not reach for a second Ctrl-C.
docker stopand a service manager send SIGTERM to one process and expect it to clean up before the kill that follows. A terminal's Ctrl-C reaches the whole process group, so the Node process would hear it twice ifhdpassed it on as the same signal. It ignores SIGINT and hears the interrupt fromhdas another signal.
Note. A stop ends the program with no cleanup, as a time limit does. Text the program wrote and the host still held may be lost.
#Rewriting Source
#Formatting
hd fmt rewrites source files in the one standard layout, as gofmt
does:
hd fmt # formats every .hd file of the package in place
hd fmt src/cart.hd # formats one file
hd fmt --check # writes nothing; lists each file that would change
- cli.fmt.packageIn package mode,
hd fmtformats every.hdfile under the package's source root, test root, andtasks, and writes each changed file in place. - cli.fmt.file
hd fmt FILEformats FILE alone, in a package or outside one. Outside any package,hd fmtwithout a FILE is an error. - cli.fmt.meaningFormatting changes only the layout of a file. It keeps the file's meaning and every comment.
- cli.fmt.idempotentFormatting a formatted file changes nothing.
- cli.fmt.check
hd fmt --checkwrites no file. It prints the path of each file that formatting would change. - cli.fmt.syntax-errorA file with a syntax error is left untouched, and
hd fmtreports its syntax diagnostics. It still formats the other files. - cli.fmt.no-check
hd fmtreads no other module and checks no type, so a file with a type error is still formatted. - cli.fmt.width.wrapA code line longer than 100 columns is wrapped at its syntactic grouping boundaries.
- cli.fmt.width.elementsAn overlong struct literal, call, parameter list, or list literal places one element per line.
- cli.fmt.width.comma-groupsOther comma-delimited groups use the same layout, including tuples, maps, use groups, generic parameters, and type arguments.
- cli.fmt.width.indentA broken group's elements are indented four spaces beyond the line holding its opening delimiter.
- cli.fmt.width.closeA broken group's closing delimiter starts a separate line at the opening line's indentation.
- cli.fmt.width.binary-groupingA broken binary chain uses parentheses for implicit line continuation.
- cli.fmt.width.binary-operandsA broken binary chain places one operand per line, with the operator before each later operand.
- cli.fmt.width.protectedComment and string literal text remains as written, even when it exceeds the width.
- cli.fmt.width.trailing-commaA comma-delimited group written one element per line ends its last element with a comma.
Why. Formatting around a syntax error could rewrite code that the parser misread, so the file waits until it parses.
#Fixing
hd fix applies the fix-its that hd check reports, as cargo fix does:
hd fix # applies each safe fix-it, rechecks, and lists the changes and what remains
- cli.fix.command.all-entries
hd fixchecks the package ashd check --alldoes and applies safe source and capability-table fix-its. - cli.fix.safeA fix-it is safe when applying it adds no syntax error and no new diagnostic.
hd fixapplies no other fix-it. - cli.fix.overlapWhen the edits of two safe fix-its overlap,
hd fixapplies only the one whose diagnostic comes first in output order. - cli.fix.roundsAfter it applies fix-its,
hd fixchecks the package again and applies the safe fix-its of the new check. It repeats until a check offers none, or a bounded number of rounds has run. - cli.fix.report
hd fixprints each change it made, with its file and the code of its diagnostic, then the diagnostics that remain. - cli.fix.status
hd fixexits with status 0 when no error remains, and with status 101 otherwise. - cli.fix.manifest.capabilities
hd fixapplies amissing-capabilityfix-it to the package'shd.toml. - cli.fix.manifest.otherIt reports other manifest fix-its without applying them.
Why. An agent fixes a typo or a missing
usein one command. A fix-it that breaks the file or brings a new diagnostic is left for a person to judge.
See also: Machine Output, whose fixes field holds
the same edits.
#Resource Limits
#Program Limits
hd run --time-limit 30s # panics with time-limit past 30 seconds
hd test --time-limit 5s # each test case gets five seconds
- cli.limit.time
--time-limit DURATIONlimits how long the program runs. A program that runs past it panics, byflow.panic.time-limit. - cli.limit.commands
hd FILE,hd FILE.wasm,hd run, andhd testtake the flag, before the--of the program arguments. - cli.limit.noneThe limit has no default. Without the flag, a program is limited only by its host.
- cli.limit.testIn
hd test, the limit applies to each test case on its own, in its own program instance.
| Form | Meaning | Examples |
|---|---|---|
| SIZE | a whole number of bytes, or a whole number followed by K, M, or G, for 1024, 1024ยฒ, or 1024ยณ bytes |
65536, 256M, 2G |
| DURATION | a whole number followed by ms, s, min, or h, the duration suffixes |
500ms, 30s, 2min |
- cli.limit.formsA SIZE or DURATION value has a form of the table above. Any other value is a usage error.
Why. An agent runs code it has not read. A limit turns a runaway loop into a panic with a stable category, in place of a hung process.
#Threads
hd test --jobs 1 # one thread
HD_JOBS=4 hd check # four threads
- cli.jobs
--jobs Nsets the number of threads thathduses to check, build, format, and run tests. N is a whole number of at least 1. - cli.jobs.envThe environment variable
HD_JOBSsets that number when no flag does. - cli.jobs.defaultWithout either,
hduses the number of processor cores, or 8 when there are more. - cli.jobs.resultThe number of threads changes no diagnostic, test outcome, or output order.
Why. Many agents often run
hdat once on one machine. A cap of 8 keeps them from starving each other.
#Workspace Mode
At a workspace root, commands act on the members, as Cargo's do:
cd ws
hd test # the tests of every member
hd run invoice # the one member executable or task named invoice
- cli.workspace.membersIn workspace mode,
hd check,hd test, andhd buildact on every member of the workspace. - cli.workspace.run-nameIn workspace mode,
hd run NAMEruns the executable or task namedNAMEwhen exactly one member has one. - cli.workspace.run-ambiguousWhen several members have one,
hd run NAMEis an error whose message lists those members. - cli.workspace.run-missingWhen no member has one,
hd run NAMEis an error. - cli.workspace.run-bareIn workspace mode,
hd runwith no NAME is an error whose message lists each member's executables and tasks. - cli.workspace.replIn workspace mode, the REPL session may use only
std.
#Selecting Members
hd test -p ui -p shared # the tests of members ui and shared
hd run -p web serve # serve of member web
cd libs/ui && hd test -p shared # from inside member ui, the tests of shared
- cli.workspace.select.anywhere
-p NAMEor--package NAMEselects the member whose package is namedNAME. This works wherever a command finds a workspace: at its root, or inside a member bycli.mode.member. It works onhd run,hd test,hd check, andhd build. - cli.workspace.select.repeatThe flag may be repeated, and the command then acts on the selected members only, by the rules above.
- cli.workspace.select.unknownA
-p NAMEthat names no member of the workspace is an error.
Why. CI and agents usually run from the root. A directory argument would read as
hd run NAME, so a flag selects members, as Cargo's-pdoes.
#Names
- cli.name.hyphenA package's name and an executable's name may contain
-, as Cargo's do. - cli.name.taskA task's name is its file name, so it is an identifier and contains no
-.
hd new my-app # a package's name may contain -
hd run my-tool # an executable's name too
hd run gen # a task's name is its file name: tasks/gen.hd
Note. Where source needs an identifier for such a name, each
-becomes_, as the dependency keymy-appisdep.my_app(module.dep.key-name).
#Creating A Package
hd new is the one command that creates a package:
hd new --app hello # hello/hd.toml, src/main.hd, and tests/hello.hd, in a new git repository
hd new --lib util # util/hd.toml, src/lib.hd, and tests/util.hd
hd new --app # an application in the working directory
hd new hello # asks which kind, or fails without a terminal
cd hello
hd run # runs src/main.hd
hd test # runs tests/hello.hd, which runs the executable
- cli.new.kind
hd newcreates an application with--appand a library with--lib. - cli.new.kind.askWith neither flag,
hd newasks which kind to create when standard input is a terminal. - cli.new.kind.no-terminalWith neither flag and standard input not a terminal,
hd newis an error whose message names--appand--lib. It never picks a kind itself. - cli.new.app
hd new --app PATHcreates the directory PATH, holding anhd.toml, asrc/main.hdwhose program printshello, world, and an integration testtests/NAME.hd. - cli.new.app.testThat test runs the executable with
hd_run!, and checks its output and its exit status. - cli.new.lib
hd new --lib PATHcreates the directory PATH, holding anhd.toml, asrc/lib.hdthat declares one sample public function, and an integration testtests/NAME.hd. - cli.new.lib.testThat test checks the sample function through the package's public interface, with
use pkg. - cli.new.test-nameIn both,
NAMEis the package's name with each-replaced by_, so the file is a valid module path. - cli.new.here.dirWith no PATH, or with
.,hd newdoes the same in the working directory. - cli.new.existing
hd newis an error when any file it would write already exists, and it then writes nothing. - cli.new.no-executable-tableThe
hd.tomlthathd newwrites has no[[executable]]table. With--app,src/main.hdis then the default executable, andhd runruns it right afterhd new. - cli.new.workspace-memberWhen the new package's directory lies under a workspace root,
hd newalso adds that directory to the workspace manifest'smembers, as Cargo does. - cli.new.vcsUnless the new package's directory is already inside a git repository,
hd newrunsgit initthere and writes a.gitignore, as Cargo does. - cli.new.vcs.ignoreThat
.gitignorelists only the build directory, as/build/.hd.sumis not listed, so it is committed. - cli.new.vcs-none
hd new --vcs noneruns nogit initand writes no.gitignore. - cli.new.capabilitiesThe application template lists Console, Args, Clock and Random as
truein[capabilities]. - cli.new.dev-capabilitiesThe application template lists Process as
truein[dev-capabilities].
Note. Running Executables shows the test that
hd new --appwrites.
Why. A new package starts with a passing test in
tests/. Where tests go, and how they reach the package, is then visible from the first run. A package left out ofmemberswould be skipped by every root command without a word.
#GitHub Pages Workflow
- cli.new.pages
hd new --pagesalso writes.github/workflows/docs.ymlin the new package's directory, for an application and for a library. - cli.new.pages.workflowThat workflow runs
hd docand deploys the output directory to GitHub Pages. - cli.new.pages.askWhen
hd newasks which kind to create, bycli.new.kind.ask, it also asks whether to publish the documentation to GitHub Pages. A yes is--pages. - cli.new.pages.defaultWithout
--pages, and without that question,hd newwrites no workflow file.
The workflow file that --pages writes looks like this. Its exact text is
not specified:
# Written by `hd new --pages` (hd 0.4.0).
name: Docs
on:
push:
branches: [main]
workflow_dispatch:
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: pages
cancel-in-progress: false
jobs:
docs:
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deploy.outputs.page_url }}
steps:
- uses: actions/checkout@v5
- uses: hd-lang/setup-hd@0.4.0
- run: hd doc --out _site
- uses: actions/upload-pages-artifact@v4
with:
path: _site
- id: deploy
uses: actions/deploy-pages@v4
Note. The workflow installs
hdwith asetup-hdaction, pinned to the version ofhdthat wrote the file. That action is not published yet, so a workflow written today cannot run until it is. The repository's Pages source must also be set to "GitHub Actions".hd newcannot set it.
Note.
cli.new.existingcovers the workflow file too: when it exists already,hd newwrites nothing.
#REPL
cd shop/src/billing
hd # use self.util names shop/src/util.hd
echo 'println(1 + 2)' | hd # prints 3
- cli.repl.open.terminal
hdwith no arguments opens the REPL when standard input is a terminal. - cli.stdin.programWhen standard input is not a terminal,
hdwith no arguments runs all of it as a single-file program, aspythondoes. This holds in every mode. - cli.repl.package.libIn package mode, the session acts as code inside
src/lib.hd, wherever in the package it starts. It sees the private declarations ofsrc/lib.hd, anduse self.utilnamessrc/util.hd. - cli.repl.package.dependenciesIn package mode, the session may use the package's dependencies and its dev dependencies.
- cli.repl.package.no-libIn a package without
src/lib.hd, the session may still use the public declarations of the package's modules throughself, assrc/main.hddoes. It may also use the package's dependencies, its dev dependencies, andstd. - cli.repl.package.no-lib.main
src/main.hditself stays unusable from the session, as it is from every module bymodule.path.main-no-use. - cli.repl.uses.otherApart from the declarations of
src/lib.hd, names reach the session only through itsusedeclarations. - cli.repl.outsideOutside any package, the session may use only
std. - cli.repl.input-typesEach input of a session is checked on its own, by Open Literal Width. A later input never changes the type of an earlier input's binding.
- cli.repl.host.default-profileA session runs its inputs under the default profile, as
hd FILEdoes, so an input may call each host capability trait that the profile binds. - cli.repl.input.driverAn input's top level is a suspension driver context, as
main!'s body is, so an input may make bang calls. Its row is inferred under the default profile. - cli.repl.host.cwdA session's relative paths resolve from the working directory of the
hdcommand. - cli.repl.host.argsIn a session,
Argsgives an empty program name and no program arguments. - cli.repl.host.onceEach input's host calls happen once, when the input runs. A later input never repeats an earlier input's file writes, reads, clock readings, or random draws.
- cli.repl.valueThe REPL shows an expression input's value as
dbgprints it, then:and its type, as in[3, 6] : List[usize]. So a value of a type withoutDisplayorDebugshows too. An expression input is an input whose only top-level item is an expression statement that binds nothing. The REPL shows no value line when that expression's type isvoidornever. - cli.repl.value.streamThe REPL writes the value line to standard output, after the input's own output and its
dbglines. - cli.repl.panicWhen an input panics, the REPL reports the panic, and the session continues with the next input.
- cli.repl.panic.bindingAn input that panics adds no binding to the session.
- cli.repl.panic.mutationsChanges that the input made before the panic, to values that earlier inputs bound, remain. So do its host calls, by
cli.repl.host.once. - cli.repl.panic.declarationsAn input that panics adds no declaration to the session either.
hd> let mut log: List[string] = []
hd> fn record_then_read(entries: &mut List[string]) -> string:
... entries.push("started")
... entries[5]
hd> line := record_then_read(&mut log) # panics with index-out-of-bounds; binds no line
hd> log # the push remains
["started"] : List[string]
Note. The panic ends only the input that raised it, by
flow.panic.poison. The values of earlier inputs live on in the session.
hd> use std.time.{now}
hd> started := now() # reads the system clock once
hd> use std.path.{Path}
hd> use std.fs.{read_text}
hd> read_text!(Path("notes.txt")) # reads ./notes.txt
Note. An input such as
x := 21has no signed literal, soxis ausize. A session that shows types then showsx * 2as ausize.y := -21is ani32, bytypes.literal.local.default.
Why. An agent that pipes code into
hdgets a run, not a prompt that waits for a terminal.
Why. A session is where a call is tried before a program makes it, so it binds what
hd FILEbinds. Python's REPL likewise reads files and the clock, with an emptysys.argv[0].
#Redefinition
hd> rate := +3
hd> fn price(n: i32) -> i32: n * rate # reads an earlier input's binding
hd> fn total() -> i32: price(+2)
hd> total()
6 : i32
hd> fn price(n: i32) -> i32: n * rate + 1 # error: duplicate-binding, price is already declared
hd> let count = +1
hd> count = count + 1 # a `let` binding is assigned, not declared again
- cli.repl.declarations.bindingsA declaration in an input may read and use the top-level bindings of earlier inputs.
- cli.repl.redeclare.bindingAn input must not declare a function or top-level binding whose name an earlier input of the session declared. Error:
duplicate-binding. - cli.repl.redeclare.typeAn input must not declare a type, trait, or type alias whose name an earlier input of the session declared. Error:
duplicate-type. - cli.repl.redefine.implAn implementation in an input that overlaps an implementation of an earlier input is an error, as in one module. Error:
overlapping-impl.
Note. To change a declaration, edit it in a file and start a new session, or give the new declaration another name. An input that panicked declared nothing, by
cli.repl.panic.binding, so its names stay free.
Note. An implementation of
Debugadded after a value of its type was shown does not change how later values of that type show. A type not yet shown uses it.
Why. A session is one scope, as a module is, so a name means one declaration. Shadowing would leave earlier items and values tied to a declaration that can no longer be named. A value of an old
Pointwould fail against the newPoint, and the old type would keep its implementations.
#Interrupting An Input
- cli.repl.cancelInterrupting an input, as Ctrl-C does, while it waits on the host cancels it by
req.cancel.steps. It then ends as a panicking input does. - cli.repl.stopInterrupting an input while it computes stops it. It then ends as a panicking input does.
Note. "Ends as a panicking input does" means
cli.repl.panic.binding,cli.repl.panic.declarations, andcli.repl.panic.mutationsapply: the input adds nothing, and its earlier changes and host calls remain.
#Commands
- cli.repl.commandAn input that starts with
:is a REPL command, not hd code. - cli.repl.command.help
:helplists the commands. - cli.repl.command.quit
:quit,:qand:exitend the session, as end of input at an empty prompt does. - cli.repl.command.reload
:reloadreads the package's files again and rebuilds the session bycli.repl.rebuild.
#Rebuilding A Session
- cli.repl.rebuildAn implementation may rebuild a session by running its inputs again in order, with each host call's result served from a record of the first run. A rebuild repeats no host call.
- cli.repl.rebuild.prefixA rebuild keeps the longest prefix of inputs that still check and make the same host calls as before. The first input that does not, and every later input, is dropped, and the REPL reports the first one.
- cli.repl.rebuild.stoppedA rebuild skips an input that was stopped, so that input's changes to earlier values may be lost. The REPL reports each stopped input that a rebuild skipped.
- cli.repl.checkpoint.continueWhen the active host-call record exceeds its limit, the REPL checkpoints completed inputs and continues accepting inputs.
- cli.repl.checkpoint.initializedLater inputs import the checkpoint's initialized bindings and code without executing the checkpointed inputs again.
- cli.repl.checkpoint.effectsCreating or restoring a checkpoint repeats no host call.
- cli.repl.checkpoint.historyThe REPL retains the recorded host results needed to rebuild checkpointed inputs by
cli.repl.rebuild.prefix.
Note. A browser session is rebuilt after a Stop of a busy input or a page reload, and a session is rebuilt after its package's files change. A stopped input stopped at an arbitrary point that no record marks, so running it again could differ or never end.
#Session Scripts
hd --repl --engine wasmtime session.in
- cli.repl.script
hd --repl FILEruns a REPL session without a terminal. It reads the session's inputs from FILE, a UTF-8 text file, and ends the session at the end of FILE or at:quit. The session starts in the package around the working directory, ashddoes on a terminal. - cli.repl.script.linesEach line of FILE, without its LF or CRLF line end, is one line typed at the prompt. So a line that continues an input, and the empty line that ends a block, are lines of FILE.
- cli.repl.script.echoBefore the session runs a line it has read, it writes the prompt, one space, and the line to standard output, then LF. The prompt is
hd>for the first line of an input and...for each line that continues it. An empty line shows its prompt alone. Standard output is then the session's transcript. - cli.repl.script.outputEverything else the session writes goes where a terminal session sends it, by
cli.repl.value.streamandcli.dbg.repl, after the echo of the line that started the input. - cli.repl.script.exitThe command exits 0 when the session ends, whether or not an input panicked or was rejected. A FILE that cannot be read, or a session scope that cannot load, is
cli.exit.hd-failure. - cli.repl.script.engine
hd --repltakes--engine NAME, ashd rundoes bycli.engine.flag, andcli.engine.selectpicks the engine, except that with neither--enginenorHD_ENGINEthe session runs on the engine of the terminal REPL. An engine that cannot run a session is refused ascli.engine.unknownsays.
Note. A script is how a conformance CLI case tests the session rules, since a case runs
hdwith a closed standard input and so cannot hold a terminal.
Why. An
hdwith piped standard input runs a program bycli.stdin.program, so a session needs its own way to read inputs from a file. Echoing each line after its prompt makes standard output read as a terminal session does.
#Dependencies
hd fetches the packages that a manifest requires from their repositories,
as Go's go command does:
hd add acme/json@2.1.0 # requires github.com/acme/json as json, fetches it, and records its hash
hd check # fetches each selected version that the cache lacks
hd update json # moves json to the newest 2.x release
hd remove json # deletes json's requirement and tidies hd.sum
hd fetch # fetches everything selected, as CI does before going offline
hd clean --cache # deletes every fetched version; the next command fetches again
Source then uses the dependency through its key, as in
use dep.json.{parse}.
#Fetching
hd check # error: missing-sum-entry, when json has no hd.sum entry
hd fetch # fetches json and adds its entry
hd check # works offline from now on
- cli.dep.implicit-fetchBefore they compile,
hd check,hd build,hd run, andhd testselect the package's dependency versions by Version Selection. They fetch each version they need that the cache lacks. - cli.dep.reachedA command needs every version that a requirement reaches, since selection reads its manifest, by
module.select.reach. - cli.dep.offlineA command that finds every version it needs in the cache uses no network.
- cli.dep.missing-sumIn those commands, a selected version that has no
hd.sumentry is an error at its requirement inhd.toml. The message nameshd addandhd fetch. Error:missing-sum-entry. - cli.dep.no-sum-write
hd check,hd build,hd run, andhd testnever writehd.sumorhd.toml. - cli.dep.verifyA selected version whose tree hash differs from its
hd.sumentry is an error, bymodule.sum.mismatch. Error:sum-mismatch. - cli.dep.pathA path requirement names the package in its directory, by
module.path-dep.form. It is never fetched, and it has nohd.sumentry. - cli.dep.invalidA dependency key or requirement that breaks a rule of Dependency Requirements, Host Paths, or Versions is an error at its line of
hd.toml. A requirement that the command is adding has no line yet, so its error has no file or line. Error:invalid-requirement. - cli.dep.no-libraryA requirement whose package has no
src/lib.hdis an error, bymodule.path.no-lib-dependency. Error:invalid-requirement. - cli.dep.missing-sum.manifestIn those commands, a version that a requirement reaches and that has no manifest line is an error at its requirement. Selection does not read its manifest. Error:
missing-sum-entry. - cli.dep.verify.manifestA reached version whose manifest hash differs from its manifest line is an error, and selection does not read that manifest. Error:
sum-mismatch.
Why.
hd.sumis committed, so the first fetch of a version is the one moment its hash is trusted. Only the commands that change requirements record a hash; a build only checks one. A version that is not selected still decides the selection through its manifest, so its manifest is checked too, as Go checks its/go.modlines.
#Repositories
- cli.dep.repo-url
hdfetches a host path's repository from the URLhttps://followed by its repository part. Sogithub.com/acme/tools/lintis fetched fromhttps://github.com/acme/tools. When the HTTPS fetch fails for any reason,hdtriesssh://git@followed by the repository part, without prompts. Within one run, a host that answered over SSH is tried over SSH first.url.<base>.insteadOfrewrites apply to each URL first. When both URLs fail, one error names both. Error:fetch-failed. - cli.dep.git
hdfetches with the system'sgitcommand, so git's own configuration applies, such as credential helpers, SSH keys, andurl.<base>.insteadOfrewrites. - cli.dep.tagA version's tree is the package's directory in the commit of its tag, by
module.version.tagandmodule.version.tag-prefix. - cli.dep.no-promptFetching never asks a question. A repository that git cannot reach or read, as a private one without credentials, is an error that names the host path. Error:
fetch-failed. - cli.dep.unknown-versionA requirement whose package has no tag for its version is an error, by
module.version.tag-missing. Error:unknown-version. - cli.dep.no-secretA message never shows a credential. A user name or password in a URL that git reports is replaced by
***. - cli.dep.pseudoA pseudo-version's tree is the package's directory in the commit that its
HASHnames.hdfetches the repository's branches and tags to find that commit. - cli.dep.pseudo.unknownA pseudo-version whose
HASHnames no fetched commit, or whoseTIMEis not that commit's committer time in UTC, is an error, bymodule.version.pseudo-missing. Error:unknown-version. - cli.dep.pseudo.baseA pseudo-version's base tag is the tag its form names, by the pseudo-version table, with the package's tag prefix. The table below lists it.
- cli.dep.pseudo.base.missingA pseudo-version whose base tag does not exist is an error. Error:
unknown-version. - cli.dep.pseudo.base.ancestorA pseudo-version whose commit does not descend from the commit of its base tag is an error. Error:
unknown-version. - cli.dep.pseudo.base.taggedA pseudo-version whose commit is the base tag's own commit is an error, and its message names the tag's version. Error:
unknown-version.
| Pseudo-version | Base tag |
|---|---|
0.0.0-TIME-HASH |
none, so no check applies |
X.Y.(Z+1)-0.TIME-HASH |
vX.Y.Z |
X.Y.Z-PRE.0.TIME-HASH |
vX.Y.Z-PRE |
[dependencies]
lint = "github.com/acme/tools/lint@2.4.1" # error: unknown-version, with no tag lint/v2.4.1
pdf = "github.com/acme/pdf@0.5.1-0.20260912081500-3f2c9e1a7b6d" # error: unknown-version, when that commit does not descend from v0.5.0
Why. A pseudo-version orders just above its base tag. Without the check, a manifest could name a commit as
9.0.1-0.TIME-HASHand win every selection, as Go's pseudo-version check prevents. Any earlier tag on the commit's history may be the base, not only the closest, so a tag pushed later never breaks a manifest.0.0.0-TIME-HASHorders below every release, so it needs no base.
Why. git already knows how to reach a private repository, so
hdstores no credentials, bymodule.repo.no-stored-credentials. AninsteadOfrewrite moves a host to SSH or a mirror without a manifest edit.
#Cache
Fetched versions live in one cache directory per user:
hd fetch # into ~/.cache/hd/pkg/github.com/acme/json@2.1.0 on Linux
HD_CACHE=/srv/hd hd fetch # into /srv/hd/pkg/github.com/acme/json@2.1.0
| Platform | Cache directory |
|---|---|
| Linux and other Unix systems | $XDG_CACHE_HOME/hd, or ~/.cache/hd when XDG_CACHE_HOME is unset |
| macOS | ~/Library/Caches/hd |
| Windows | %LocalAppData%\hd |
- cli.cache.directoryThe cache directory is the platform's user cache directory followed by
hd, as the table gives it. When the environment variableHD_CACHEis set, it names the cache directory instead. - cli.cache.entryA fetched version is stored once, in the directory
pkg/HOST_PATH@VERSIONof the cache directory, such as~/.cache/hd/pkg/github.com/acme/json@2.1.0. - cli.cache.entry.treeThat directory holds the version's tree: its manifest, its source, and its other files.
- cli.cache.sharedEvery package of the user shares the cache, so a version is fetched once per machine.
- cli.cache.read-only
hdmakes an entry read-only once it is written, and never changes it afterwards. - cli.cache.completeAn entry appears only once it is complete, so an interrupted fetch leaves no partial entry.
- cli.cache.hash
hdrecords an entry's tree hash when it writes the entry. A command that uses a cached entry compares that record with thehd.sumentry, bycli.dep.verify.
Why. Go's module cache works the same way. A read-only entry keeps an editor's jump to a dependency from changing it in place, and one entry per version serves every project.
#hd.sum
hd.sum holds a tree line per selected version, and a manifest line per
version whose manifest selection reads. No one edits it by hand:
github.com/acme/json@2.0.0/hd.toml h1:x8m2c7S1yR3a4nQpZ0v9LkT6eW5uJbHdFgN1oPq2r3s=
github.com/acme/json@2.1.0 h1:Wk5nB1xKpXz2q8pCw2e1mzXqLhZqS5pDq3K0V2m3s1c=
github.com/acme/json@2.1.0/hd.toml h1:Q3vR8nT2bW7kX1mZ5pL9cJ4sH6dF0gY2aE8uN1oI3qw=
github.com/acme/tools/lint@2.3.0 h1:1y7R7oDpvPj3c9sQm3r3N6cMfJ8pF0Q2b4Lw8d8N3hY=
github.com/acme/tools/lint@2.3.0/hd.toml h1:Zk4mP8sQ1wE6rT3yU9iO2pA5dF7gH0jK1lX4cV6bN8m=
Here lint@2.3.0 requires json@2.0.0, and the root requires json@2.1.0.
Selection reads the manifest of json@2.0.0 but selects 2.1.0.
- cli.sum.lineEach tree line of
hd.sumis a host path,@, a version, one space, and the tree hash of that version. - cli.sum.orderThe lines are sorted by host path, then by version order. A version's tree line comes before its manifest line. Each line, the last included, ends with a newline.
- cli.sum.treeA version's tree is every regular file in its package directory at its tag, by
cli.dep.tag, or in a pseudo-version's commit, bycli.dep.pseudo. A subdirectory that holds its ownhd.tomlis another package, and its files are not part of the tree. - cli.sum.summaryThe tree's summary has one line per file, sorted by path. A line is the file's SHA-256 in lowercase hexadecimal, two spaces, the file's path, and a newline.
- cli.sum.summary.pathThat path is relative to the package directory, with
/between its segments. - cli.sum.hashThe tree hash is
h1:followed by the Base64 encoding, with padding, of the SHA-256 of the summary. - cli.sum.keepNo command replaces an entry with a different hash. A tree whose hash differs from its entry is always the error of
cli.dep.verify. - cli.sum.manifest-lineA manifest line is a host path,
@, a version,/hd.toml, one space, and the manifest hash of that version.hd.sumholds one for each version that selection reads, bymodule.select.reach. - cli.sum.manifest-hashA version's manifest hash is the tree hash, by
cli.sum.hash, of a tree that holds only the version'shd.toml. - cli.sum.entryBoth kinds of line are entries of
hd.sum.cli.sum.keepholds for a manifest line too, whose mismatch is the error ofcli.dep.verify.manifest.
Note. The tree hash has the form of Go's
h1:hash, whose paths start with the module path. In a tree hash they start at the package directory, so a package's hash does not depend on the host path it was fetched by.
#Dependency Commands
hd add acme/json # hd.toml gains json = "github.com/acme/json@2.3.0", its newest release
hd add acme/json@2.0.0 # prints the new line, then: lowered json 2.3.0 -> 2.0.0
hd add --dev acme/fixtures@1.0 # fixtures goes to [dev-dependencies], at its newest 1.0 release
hd add --name json_v1 acme/json@1 # a second key, for the newest 1.x release
hd add ../billing acme/pdf # two targets: a path requirement and a fetched package
hd update json # with a tag v2.4.0, json becomes "github.com/acme/json@2.4.0"
hd update json@main # json becomes the pseudo-version of main's head commit
hd remove json # json's line and its hd.sum entry go
| Command | Effect |
|---|---|
hd add TARGET[@QUERY]... |
adds a requirement for each target, or changes its key's, raising or lowering it, then fetches and records hashes |
hd add --dev TARGET[@QUERY]... |
does the same in [dev-dependencies] |
hd add --name NAME TARGET[@QUERY] |
uses the key NAME in place of the package's name |
hd update |
moves every requirement, dev dependencies too, to the newest release on its compatibility line |
hd update NAME |
moves only NAME's requirement |
hd update NAME@QUERY |
sets NAME's requirement to the version that the query selects |
hd remove NAME |
deletes NAME's requirement, from either table |
hd fetch |
fetches every selected version that the cache lacks, and records missing hashes |
- cli.dep.add.target
hd add TARGET[@QUERY]...sets, for each target, the requirement of its key in[dependencies]. It adds the key when the manifest has none. - cli.dep.add.check
hd addchecks the--namevalue and each target bycli.dep.invalidbefore it fetches anything. - cli.dep.add.lower
hd addmay set a key's requirement to an earlier version of the same host path, asgo getdoes. It then prints that it lowered the requirement, aslowered json 2.1.0 -> 2.0.0. - cli.dep.add.lower.selectionA lowered requirement is still a minimum. Selection keeps a later version when another reached manifest, such as another member's, requires one, by
module.select.largest. - cli.dep.add.dev
hd add --dev TARGET[@QUERY]...sets the requirements in[dev-dependencies]instead, ascli.dep.add.targetdoes for[dependencies]. The other rules ofhd addhold for it. - cli.dep.add.other-tableWhen a target's key is in the other table,
hd addis an error. It reportsNAME is in [dependencies]; run `hd remove NAME` first, or names[dev-dependencies]in the reverse case. It writes nothing, bycli.dep.unchanged-on-error, and fetches nothing when--namegives the key. - cli.dep.add.several
hd addwith several targets changes the manifest once, for all of them. When one target fails, it writes nothing, bycli.dep.unchanged-on-error. - cli.dep.add.reportOnce it has written the manifest,
hd addprints one line per target to stdout, in the targets' order. The line is the key's line ofhd.toml, asjson = "github.com/acme/json@2.3.0". - cli.dep.add.report.pseudoFor a pseudo-version, the line ends with a comment that names the commit and says it has no version tag, as
# commit 3f2c9e1a7b6d has no version tag. - cli.dep.add.report.orderThe line of
cli.dep.add.lowerfollows its target's line. With--format json,hd addprints none of these lines, bycli.json.lines.build. - cli.dep.selectAfter it changes the manifest,
hd add,hd update, orhd removeselects versions again and fetches each version it needs that the cache lacks. - cli.dep.tidyIt then writes
hd.sumwith one tree line for each selected version, one manifest line for each version selection read, and no other entry. A version's existing entry is kept, bycli.sum.keep. - cli.dep.update
hd updatemoves each dependency requirement of the manifest to the newest release tag on its compatibility line.hd update NAMEmoves only NAME's requirement. - cli.dep.update.releaseThe newest release is the greatest tagged version without a pre-release suffix. A requirement already at or above it keeps its version.
- cli.dep.update.network
hd updatelists the tags of each repository it moves, so it uses the network even when the cache holds every version. - cli.dep.update.dev
hd updatemoves the requirements of[dev-dependencies]as it moves those of[dependencies], andhd update NAMEfinds NAME in either table. A NAME that is no key of either table is an error. - cli.dep.update.query
hd update NAME@QUERYsets NAME's requirement to the version that QUERY selects on its host path, by Version Queries. It may raise or lower the requirement, ashd adddoes bycli.dep.add.lower. - cli.dep.update.query.path
hd update NAME@QUERYis an error when NAME holds a path requirement. Error:invalid-requirement. - cli.dep.remove
hd remove NAMEdeletes the key NAME from[dependencies]or[dev-dependencies]. A NAME that is no key of either table is an error. - cli.dep.fetch
hd fetchfetches every version that selection needs and the cache lacks. It adds each tree line and manifest line thatcli.dep.tidywould write andhd.sumlacks, and changes no other entry. - cli.dep.edit.empty-tableWhen an edit deletes a dependency table's last key, it deletes that
[dependencies]or[dev-dependencies]header line too. - cli.dep.unchanged-on-errorWhen a dependency command reports an error, it writes neither
hd.tomlnorhd.sum. - cli.dep.editA dependency command edits
hd.tomlline by line, so its comments and its other lines stay as they are. - cli.dep.package-onlyThe dependency commands work on the package of package mode. Outside any package, each is an error whose message suggests
hd new. - cli.dep.no-questionNo dependency command asks a question.
- cli.dep.no-codeNo dependency command runs code from a dependency. It reads only git's data and the files of the trees it fetches, such as their
hd.toml. - cli.dep.workspace-fetchIn workspace mode,
hd fetchworks on the whole workspace. It fetches what the workspace's selection needs, and adds the missing lines to itshd.sum, bycli.dep.fetch. - cli.dep.workspace-member-onlyIn workspace mode,
hd add,hd update, andhd removeare errors whose message suggests running them in a member's directory. - cli.dep.dev-selectedSelection reads the package's dev dependencies in every command, by
module.select.dev-dependencies. Sohd build,hd check, andhd runfetch them and need theirhd.sumentries, although only test code and tasks may use them. - cli.dep.dev-use.remove-firstCode that is not test code and uses a dev dependency is an error, by
module.test.non-test-use.dev-dependency. Its message says to runhd remove NAMEfirst, thenhd add --name NAME TARGET, to put the key in[dependencies]. Error:test-only-use.
Note. A query may move a requirement to another compatibility line, as
hd update json@latestdoes when json has a newer major release.hd update NAMEwithout a query stays on the requirement's line.
Why. An agent adds a dependency with one command and gets a manifest, a fetched tree, and a hash that agree. CI runs
hd fetchonce, at the workspace root too, and every later command works offline. A requirement lives in one member's manifest, so a command that edits one runs in that member.
Why. The same key in both tables would be a collision.
hd addrefuses to choose whether changing tables was intentional; the explicithd removefirst makes that choice visible.
Why. Lowering a minimum is safe under minimal version selection, so
hd adddoes it as Go'sgo get x@olderdoes. It says so, since a lowered line is easy to miss in a diff.
Why. Several targets either all land or none do, so a failed command leaves nothing to clean up. The printed line shows what
hd addchose for a target the user wrote without a version or a key. Adding a package runs none of its code, unlike an npm install script or a Cargo build script, so reviewing a dependency can wait until it is used.
#Targets
A target names the package that hd add requires, in one of four
forms:
| Target | Requirement | Example |
|---|---|---|
., .., a path that starts with ./ or ../, or an absolute path |
a path requirement on that directory | ../billing is { path = "../billing" } |
| a repository URL, of the table below | the repository's host path | https://github.com/acme/json.git is github.com/acme/json |
| a host path, whose first segment holds a dot | that host path | git.example.com/shop/billing.git |
OWNER/REPO[/DIR...], whose first segment holds no dot |
the host path github.com/OWNER/REPO[/DIR...] |
acme/tools/lint is github.com/acme/tools/lint |
| Repository URL | Host path |
|---|---|
https://HOST/PATH |
HOST/PATH |
ssh://git@HOST/PATH |
HOST/PATH |
git@HOST:PATH |
HOST/PATH |
- cli.dep.target.orderA target takes the first form of the table above that it matches.
- cli.dep.target.splitA target word splits at its first
@that is not thegit@of a repository URL. The text after it is the QUERY. - cli.dep.target.pathA path target is relative to the working directory.
hd addwrites the path relative to the manifest's directory, with/between its segments. - cli.dep.target.path.no-queryA path target with a QUERY, such as
../billing@1.0.0, is an error. Error:invalid-requirement. - cli.dep.target.selfA path target that names the package's own directory is an error. Error:
invalid-requirement. - cli.dep.target.urlA repository URL names a repository, and its target names the package at the repository's root.
- cli.dep.target.url.git-suffixOn
github.com, a.gitat the end of the URL is dropped. On any other host, the last segment gains.gitwhen it lacks one, bymodule.repo.git-suffix. - cli.dep.target.githubA target whose first segment holds no dot names a
github.comhost path without its host. Soacme/jsonisgithub.com/acme/json. - cli.dep.target.github.only
github.comis the only host that a target may leave out, and no setting names another. A target on any other host is written in full. - cli.dep.target.no-manifestA fetched target whose directory holds no
hd.tomlin the selected commit is an error. Its message lists each directory below it that holds anhd.toml, asfound packages at cli/, core/. Error:invalid-requirement.
hd add acme/json # github.com/acme/json
hd add git@github.com:acme/json.git@2.1.0 # github.com/acme/json, at 2.1.0
hd add https://git.example.com/shop/billing # git.example.com/shop/billing.git
hd add gitlab.com/acme/json.git # any other host is written in full
hd add acme/tools # error: invalid-requirement, found packages at cli/, core/
hd add . # error: invalid-requirement, the package itself
Why. Most packages live on
github.com, so the short form saves typing and matches what a repository page shows. A second default host, or a setting for one, would make the same target name different packages on different machines. A pasted URL works because people copy one.
#Version Queries
A query says which version of a fetched target hd add or
hd update requires:
| Query | Selects |
|---|---|
none, or latest |
the newest release; else the newest pre-release; else the pseudo-version of the default branch's head commit |
X.Y.Z, X.Y.Z-PRE, or a pseudo-version |
that version |
X or X.Y |
the newest release on that major, or major and minor; else the newest such pre-release |
| a branch name | the pseudo-version of the branch's head commit |
| a commit hash, or a prefix of one | the pseudo-version of that commit |
- cli.dep.query.orderA query takes the first row of the table above that it matches. So
latestand2.1never name a branch. - cli.dep.query.tagsA query reads only the package's version tags, with its tag prefix, by
module.version.tagandmodule.version.tag-prefix. It ignores every other tag. - cli.dep.query.latestNo query, or
latest, selects the newest release: the greatest version without a pre-release suffix. - cli.dep.query.latest.pre-releaseWhen the package has no release, it selects the greatest pre-release.
- cli.dep.query.latest.untaggedWhen the package has no version tag, it selects the pseudo-version of the head commit of the repository's default branch.
- cli.dep.query.latest.noteIn that case,
hdprints a note to stderr that suggests tagging a version. - cli.dep.query.v-prefixA version or partial version may start with
v, asv2.1.0orv2.hddrops it, so the manifest holds nov, bymodule.dep.requirement-form. - cli.dep.query.exactA version or pseudo-version selects itself. Its tag or commit must exist, by
cli.dep.unknown-versionandcli.dep.pseudo.unknown. - cli.dep.query.partialA query
XorX.Yselects the greatest release whose major, or major and minor, are those. With no such release, it selects the greatest such pre-release. - cli.dep.query.revisionAny other query names a branch, or else a commit by its hash or a prefix of it. It selects that commit's pseudo-version.
- cli.dep.query.revision.taggedA commit that carries a version tag selects that tag's version instead, or the greatest one when it carries several.
- cli.dep.query.unknownA query that selects no version is an error. Error:
unknown-version. - cli.dep.query.networkA query other than an exact version lists the repository's tags or branches, so it uses the network. An exact version that the cache holds needs none.
- cli.dep.query.offlineWhen git cannot reach the repository for such a query, the
fetch-failedmessage ofcli.dep.no-promptalso suggests an exact version.
hd add acme/json # 2.3.0, with the tags v2.3.0, v2.2.0, and v3.0.0-rc.1
hd add acme/json@v2.2 # 2.2.0, the newest 2.2 release; the v is dropped
hd add acme/beta # 0.2.0-rc.2, when every tag is a pre-release
hd add acme/pdf # 0.0.0-20260912081500-3f2c9e1a7b6d, with no version tag, and a note to tag one
hd add acme/json@main # 2.3.1-0.20261002093000-9a1b2c3d4e5f, a commit on main after v2.3.0
hd add acme/json@9a1b2c3 # the same commit, by a prefix of its hash
hd add acme/tools/lint # the newest lint/vX.Y.Z tag
hd add acme/json@2.9 # error: unknown-version, with no 2.9 tag
Why. These are the queries of Go's
go get, by its version queries, andhdfollows Go wherever this list is silent. A release is preferred because a pre-release may change. A package with no tag still works, since many new repositories have none yet, and the note says how to fix it.
#Dependency Keys
hd add takes a target's key from the package's own name unless
--name gives one:
hd add acme/left-pad # key left_pad, from name = "left-pad" in its hd.toml
hd add --name pad acme/left-pad # key pad
hd add acme/json acme/json # error: invalid-requirement, two targets with one key
- cli.dep.key.name-flag
--name NAMEmakes NAME the target's key. - cli.dep.key.name-flag.one
--namewith more than one target is a usage error. - cli.dep.key.inferredWithout
--name, the key is thenameof the[package]table in the target'shd.toml, with each-replaced by_, bymodule.dep.key-name. - cli.dep.key.inferred.invalidAn inferred key that source could not write as
dep.NAME, as9tools, is an error whose message suggests--name. Error:invalid-requirement. - cli.dep.key.sameWhen the key already holds a requirement on the target's host path, or a path requirement on its directory,
hd addchanges that requirement. Its version may rise or fall, also to another compatibility line. - cli.dep.key.clashA key that holds a requirement on another host path or directory, or that two targets share, is an error whose message suggests
--name. Error:invalid-requirement. - cli.dep.key.clash.other-tableWhen an inferred key is in the other table, the message of
cli.dep.add.other-tablealso suggests--name.
Why. A package's manifest already says what it is called, so the key needs no typing, and the
-rule is the one source uses. A key that already names another package is never replaced silently:--namepicks a free key, andhd removemakes a replacement explicit.
See also: Package Manifest, Integrity, Machine Output, Exit Status.
#Cleaning
hd clean removes what hd generated, and never source or a manifest:
hd clean # removes build/ of the package, as cargo clean does
hd clean --cache # removes every cached dependency version and compiled entry, as go clean -modcache does
| Command | Removes |
|---|---|
hd clean |
the build directory of the package, or of each member in workspace mode |
hd clean --cache |
the fetched versions of the cache, and the compiled cache |
- cli.clean.build
hd cleanwithout--cacheremoves the build directory of the package, and printsremoved build. It removes nothing else: nothd.toml,hd.sum, source, or the cache. - cli.clean.build.noneWhen the package has no build directory,
hd cleanprintsnothing to cleanand succeeds. - cli.clean.build.workspaceIn workspace mode,
hd cleanremoves the build directory of every member, and printsremoved DIR/buildfor each, with DIR relative to the workspace root. It takes no-p. - cli.clean.build.package-onlyOutside any package,
hd cleanwithout--cacheis an error whose message suggestshd newandhd clean --cache. - cli.clean.cache
hd clean --cacheremoves the fetched versions from the cache directory ofcli.cache.directory, and touches no build directory. It works inside a package, in a workspace, and outside any package, and it reads no manifest. - cli.clean.cache.writableThe entries are read-only, by
cli.cache.read-only, sohd clean --cachemakes each directory writable before it removes it. - cli.clean.cache.entries
hd clean --cacheremoves only the entriespkg,hash,obj, andtmpof the cache directory, and the cache directory itself stays. It never follows a symbolic link out of the cache directory. - cli.clean.cache.foreign
hd clean --cacheremoves nothing and is an error when the resolved cache directory is the file system root, the user's home directory, or not a directory. It is the same error when the directory holds any entry other thanpkg,hash,obj, andtmp, since that is nohdcache. A relativeHD_CACHEresolves against the working directory. - cli.clean.cache.compiled
hd clean --cachealso removes every entry of the compiled cache, whatever its size. - cli.clean.cache.output
hd clean --cacheprintsremoved HOST_PATH@VERSIONfor each version it removes, thenremoved N versions from DIR. A missing or empty cache directory printsthe cache DIR is emptyand succeeds. - cli.clean.cache.refetch
hd clean --cachechanges nohd.tomlorhd.sum. The next command that needs a removed version fetches it again, bycli.dep.implicit-fetch. - cli.clean.exit
hd cleanexits with status 0 when it succeeds, also when it removes nothing, and with status 101 when it reports an error, bycli.exit.hd-failure. - cli.clean.no-question
hd cleantakes no operand and asks no question.
HD_CACHE=/ hd clean --cache # error: the file system root is no hd cache
HD_CACHE=~/Documents hd clean --cache # error: it holds entries that are not hd's
Why. A command that deletes a user-chosen directory must not be able to delete the wrong one. The layout check keeps a mistyped
HD_CACHEfrom erasing a home directory or a project.
See also: Cache, Build Profiles.
#Compiled Cache
hd keeps what it computes, such as checked modules and compiled code,
in the cache directory, so a later command reuses it:
hd check # fills ~/.cache/hd/obj on Linux
hd check # in another worktree of the same commit: reuses those entries
HD_CACHE_MAX_SIZE=2G hd test # evicts down to 2 GiB when the cache grows past it
hd cache gc # evicts now, and prints the size before and after
- cli.cache.obj
hdstores its compiled entries in the directoryobjof the cache directory. Every package and worktree of the user shares them. - cli.cache.obj.derivedA compiled entry holds only derived data. Removing one changes no command's result, only how long the command takes.
- cli.cache.obj.capThe compiled entries have a size cap of 10 GiB. The environment variable
HD_CACHE_MAX_SIZEsets another cap, as a SIZE of Program Limits. - cli.cache.obj.evictWhen a command finds the compiled entries over the cap, it removes the least recently used ones. That eviction may continue over later commands, so a command does not wait for all of it.
- cli.cache.obj.evict.scopeEviction removes only compiled entries, never a fetched version.
- cli.cache.gc
hd cache gcruns that eviction now, until the compiled entries are within the cap. It prints their size before and after. - cli.cache.gc.anywhere
hd cache gcworks inside a package, in a workspace, and outside any package, and it reads no manifest.
Why. Keys are content hashes, so worktrees on one commit share every entry. A cap with least-recently-used eviction keeps a long-lived machine from filling its disk, as Go's build cache does.
#Package Tooling
A manifest key that no rule gives a meaning is an error on load for every command:
[package]
name = "shop"
version = "1.0.0" # error: unknown-manifest-key, since a tag is the version
| Table | Keys with a meaning |
|---|---|
[package] |
name, hd |
[source] |
root |
[host] |
package-relative host trait paths mapped to provider files |
[[executable]] |
name, module |
[dependencies], [dev-dependencies] |
every key, each a dependency requirement |
[workspace] |
members, exclude |
[toolchain] |
pin |
[lint] |
allow, warn, deny, by Linting |
- cli.manifest.unknown-key.errorEvery command rejects a table or key of
hd.tomlthat the table above does not list, at its line. Error:unknown-manifest-key. - cli.manifest.known-keyA listed key whose value breaks the rule that gives it a meaning stays an error.
- cli.manifest.invalidAn error in the content of
hd.tomlfor which no other rule names a code is an error at its line inhd.toml. Error:invalid-manifest. - cli.manifest.invalid.casesThese errors include a bad key or value of Capability Grants, an
[[executable]]table without a stringnameandmodule, and a[source] rootthat is no source root. They also include a package that its workspace manifest neither lists nor excludes. - cli.manifest.invalid.unlistedFor the unlisted package, the diagnostic's file is the workspace manifest, and its line is that of the manifest's
memberskey, or of its[workspace]header when it has nomembers. - cli.manifest.invalid.syntaxA TOML syntax error in
hd.tomlis also aninvalid-manifesterror, at the line and column where the syntax error starts. - cli.manifest.toolchain-keys
[package] hdis the minimum toolchain version ofmodule.toolchain.minimum, and[toolchain] pinis the exact version ofmodule.toolchain.pin. Each is a version string. - cli.tooling.package-schemaThe complete
hd.tomlschema belongs to package tooling. Dependencies defines thehd.sumformat and the commands that fetch, add, and upgrade dependencies. - cli.tooling.package-laterCompatibility checks at release and upgrade, vendoring, and local-path patches are package tooling that this chapter does not define.
See also: Package Manifest, Workspaces, Test Modules.
#Package Host Declarations
The SDK maps each owned host trait to its provider file:
[host]
"server.HttpServer" = "providers/http.mjs"
"ui.Surface" = "providers/ui.mjs"
- cli.host.declaration.map
[host]must be a table mapping trait paths to nonempty provider-file strings. Error:invalid-manifest. - cli.host.declaration.map-keyEach key must be a package-relative trait path of canonical identifier names separated by dots. Error:
invalid-manifest. - cli.host.declaration.map-uniqueEach trait path must occur at most once in the table. Error:
invalid-manifest. - cli.host.declaration.mappedEach key must name a trait declared with
@hostin its owning package. Error:invalid-manifest. - cli.host.declaration.marker-mappedEach trait marked
@hostmust have an entry in its owning package's[host]table. Error:invalid-manifest. - cli.host.declaration.map-locationsA source/map mismatch diagnostic identifies the trait and its source and manifest locations.
- cli.host.declaration.mapping-no-consentA dependency's host mapping does not grant consent to load its provider.
- cli.host.declaration.map-recheckChanging the host map must recheck source/map agreement even when the source has not changed.
- cli.host.declaration.provider-relativeA provider-file value must be relative to its declaring package. Error:
invalid-manifest.
#Package Host Bindings
The app consents to load a provider through its root manifest:
[capabilities]
"dep.server_sdk.server.HttpServer" = true
hd run --engine node
-
cli.host.package.entryA package-declared host trait named by
mainbelongs to that program's selected runtime profile. -
cli.host.package.javascriptPackage host providers run only on JavaScript hosts.
-
cli.host.package.native-refusalWasmtime refuses a required package host trait before guest initialization, naming the trait and its need for a JavaScript host.
-
cli.host.package.no-fallbackA host unable to supply a package capability must not substitute another engine or provider.
-
cli.host.package.manifest-consentLoading a required package provider needs an explicit
trueentry for its source trait path in the app's[capabilities]table. -
cli.host.package.no-flag-consentA
--capflag cannot replace missing manifest consent for a package provider. -
cli.host.package.invokerOnly the invoker or its trusted adapter loads provider code.
-
cli.host.package.consent-preflightEvery required provider and grant must be checked before any selected provider module loads.
-
cli.host.package.contractProvider identity and required method schemas, including suspension and returned handles, must match before guest initialization.
-
cli.host.package.missingA missing provider causes a startup refusal naming the package, version and trait.
-
cli.host.package.duplicateRegistering multiple providers for one nominal capability is an error.
-
cli.host.package.generatedJS host glue is generated from checked host trait signatures, including value conversion and suspension plumbing.
-
cli.host.package.companionA build requiring package host bindings writes generated
NAME.host.mjsbesideNAME.wasm. -
cli.host.package.no-companionA build requiring no package host binding emits no companion glue file.
-
cli.host.package.completion-formsA suspending provider method may return a ready value, a Promise, or a pollable operation.
-
cli.host.package.abortAborting a pending method signals cancellation and discards its undelivered result, including any later completion.
-
cli.host.package.handle-releaseReleasing the last HD reference to a returned handle drops its host binding without implicitly calling resource methods.
-
cli.host.package.plain-providerA provider module default-exports a plain JavaScript object with an own function property for each required method.
-
cli.host.package.generated-wrapperGenerated glue wraps plain provider objects with the checked nominal identity and method schemas.
-
cli.host.package.provider-containmentLoading refuses a missing provider file or a file whose canonical path leaves its declaring package.
-
cli.host.package.prebuilt-manifestA prebuilt Wasm run requiring package capabilities discovers its app manifest above the Wasm file.
-
cli.host.package.prebuilt-no-manifestA prebuilt Wasm run without that manifest refuses required package capabilities before loading any provider.
-
cli.host.package.value-conversionGenerated bindings apply the JavaScript value conversions of Package Host Traits.
-
cli.host.package.context-pendingThe method context's
pending(poll, done, abort, finish)factory creates a pollable operation. -
cli.host.package.poll-nonblockingA pollable operation's readiness probe must return a boolean without blocking.
-
cli.host.package.poll-resultIts result callback runs once when readiness first succeeds.
-
cli.host.package.poll-deliveryIts optional delivery hook runs only when the guest takes a valid finish.
-
cli.host.package.poll-abortAborting an undelivered pollable operation invokes its cancellation hook at most once.
-
cli.host.package.poll-abort-signalCancellation signals the pending call before invoking its cancellation hook.
-
cli.host.package.poll-closeClosing the instance cancels its outstanding pollable operations.
-
cli.host.package.synchronous-completionA non-suspending provider method cannot return a Promise or a pollable operation. Boundary failure:
host-contract. -
cli.host.package.poll-callback-failureA pollable operation's callback failure is a host-contract failure. Boundary failure:
host-contract. -
cli.host.package.context-httpA JavaScript package-provider context exposes a frozen
httpview of its core runtime's resolved Http policy. -
cli.host.package.context-http-coverageIts
coversHost(host, port)method tests only the Http capability's written host and port. -
cli.host.package.context-http-helpersIts
isAsciiUrl(text)andwrittenHost(text)methods use the core runtime's URL-validation behavior. -
cli.host.package.context-http-readonlyA provider cannot replace the context's
httpview. -
cli.host.package.context-http-missingAccessing
context.httpwithout runtime HTTP policy services fails. Boundary failure:host-contract. -
cli.host.package.context-consoleA JavaScript package-provider context exposes a frozen
consoleview of its core runtime's resolved Console authority. -
cli.host.package.context-console-allowsIts
allows()method reports whether Console access is granted. -
cli.host.package.context-console-readonlyA provider cannot replace the context's
consoleview. -
cli.host.package.context-console-missingAccessing
context.consolewithout runtime Console policy services fails. Boundary failure:host-contract. -
cli.host.package.context-interface-identityA JavaScript package-provider method context exposes a frozen
interfaceIdentitywith its checked nominalpackage,versionandtrait_pathstrings. -
cli.host.package.context-interface-identity-checkedThis identity comes from the method's guest interface after provider identity and schema validation.
-
cli.host.package.context-interface-identity-handleA handle method's
interfaceIdentitynames its own declared handle interface. -
cli.host.package.context-interface-identity-readonlyA provider cannot replace or delete the context's
interfaceIdentity. -
cli.host.package.context-interface-identity-bindingChanging generated descriptor metadata after binding preserves an existing method's identity and bound receiver.
Why. Provider JavaScript has its host realm's authority; an HD grant cannot sandbox arbitrary JavaScript imports. Root consent makes loading dependency code explicit.
Why. The paired declaration makes host interfaces discoverable without scanning source and keeps the provider's responsibility explicit in both places.
See also: Package Host Traits.
#Synchronous Host Driver
Embedding adapters can own the event loop while reusing the core's host completion table. This driver uses the existing root exports; it adds no guest entry convention.
- cli.host.driver.createThe JS core host provides
createDriver(instance, init="hd.init", poll="hd.poll"), returning a synchronous step driver. - cli.host.driver.exportsCreation requires the named initialization and poll exports and
hd.waketo be functions. Boundary failure:host-contract. - cli.host.driver.modeA host selects either asynchronous
rundriving or one synchronous driver. Boundary failure:host-contract. - cli.host.driver.initBefore its first poll, a driver invokes initialization once.
- cli.host.driver.pollEach successful nonterminal step calls the poll export once with context zero.
- cli.host.driver.wakeAfter a pending poll, a successful step publishes currently ready host completions through one
hd.wakecall when their count is nonzero. - cli.host.driver.resultA step returns
{status, wakes}, with the poll's raw signed status and the published completion count. - cli.host.driver.no-backgroundThe driver itself creates no Promise or timer.
- cli.host.driver.terminalAfter a successful terminal step, further steps return its status with zero wakes until the host closes.
- cli.host.driver.failureAn exception during initialization, polling, completion, wake delivery or output flushing permanently fails that driver.
- cli.host.driver.reentrantCalling
step()during a step fails. Boundary failure:host-contract. - cli.host.driver.failed-stepA step on a failed driver fails. Boundary failure:
host-contract. - cli.host.driver.closedCreating or stepping a synchronous driver after closing its host fails. Boundary failure:
host-contract. - cli.host.driver.sync-readyA synchronous host wait in this mode must have a ready completion. Boundary failure:
host-contract. - cli.host.driver.statusThe poll result must be a signed 32-bit integer. Boundary failure:
host-contract.
Why. Request-scoped hosts must keep each operation in its owner's lifetime. Explicit turns avoid anchoring a retained asynchronous driver to the first request's Promise.