hd-lang

#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, and hd 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, and hd fetch, which manage and fetch dependencies;
  • hd install and hd uninstall, which manage installed executables;
  • hd clean, which removes the build directory or the dependency cache;
  • hd fmt and hd 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
  1. cli.command.helphd help and hd --help print the command list. It names every command of the table above.
  2. cli.command.help.commandhd help COMMAND and hd COMMAND --help print the command's usage line and each flag it takes. So the help of hd add names --dev and --name, and the help of hd clean names --cache.
  3. cli.command.positional.operandsA positional word denotes the command's declared NAME, FILE, SOURCE, PATH or TARGET.
  4. cli.command.positional.package-directoryA directory operand to hd run or hd test is 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
  1. cli.mode.package.nearestA command works in package mode exactly when the nearest hd.toml in its start directory or a directory above it declares a package. That hd.toml names the package.
  2. cli.mode.startThe start directory is the directory of the FILE that a command names, or the working directory when it names none.
  3. cli.mode.workspaceA command works in workspace mode when that nearest hd.toml is a workspace manifest, which lists members and declares no package.
  4. cli.mode.outsideOtherwise the command works outside any package.
  5. 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.
  6. 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.
  7. cli.mode.member.sharedVersion selection and hd.sum then come from that workspace, by Workspaces, as Cargo's do.
  8. cli.mode.member.unlistedWhen that workspace manifest neither lists the package in members nor in exclude, the command is an error that names the manifest, as Cargo's is.
  9. 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's exclude.
  10. cli.mode.member.excludedA package whose directory the workspace manifest's exclude lists 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 src directory without an hd.toml does 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
  1. cli.file.runhd FILE runs FILE as a single-file program, whether or not FILE lies in a package.
  2. cli.file.in-packageWhen FILE lies in a package, the error for a use of pkg, self, or super in it suggests a task and hd run NAME.
  3. cli.file.entry-hintWhen FILE is the entry module of an executable or a task, that error instead names it and the exact hd run command. For src/main.hd of package shop, it names hd run shop.
  4. cli.file.check-testOutside any package, hd check FILE and hd test FILE check or test FILE as a single-file program.
  5. cli.file.check-test.no-fileOutside any package, hd check or hd test without a FILE is an error whose message suggests passing a FILE or creating a package with hd 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, so hd test FILE binds them TestRunner alone (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
  1. cli.wasm.runWhen FILE's name ends in .wasm, hd FILE instantiates FILE as a module that hd build wrote, and runs it.
  2. cli.wasm.modeOnly the extension picks the mode: hd FILE for a FILE whose name ends in .hd runs a single-file program, by cli.file.run.
  3. cli.wasm.hostThe module runs under the default profile, in the working directory of the hd command.
  4. cli.wasm.host.importshd binds each trait of the default profile that the module's import list names.
  5. cli.wasm.grant.package-contextA prebuilt module with package host capabilities uses the discovered app manifest and command flags for its grants.
  6. cli.wasm.grant.builtin-onlyA prebuilt module without package host capabilities takes grants from command flags alone.
  7. cli.wasm.total-denyThe total deny refusal applies, with the module's needs read from its import list by cli.cap.total.needs.
  8. cli.wasm.argshd FILE.wasm passes the words after -- to the module as its program arguments, by cli.args.pass.
  9. cli.wasm.statusOnce the module starts, hd FILE.wasm exits with the program's own status, by cli.exit.program.
  10. 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.
  11. cli.wasm.not-hdA valid module that hd build did not write is an error, with status 101.
  12. cli.wasm.not-hd.detecthd knows such a module by a missing entry export, or by an import of a host function that hd does not provide.
  13. 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 no hd version that this chapter defines. hd cannot tell which profile built a module, and the module keeps that profile's overflow behavior. A module from an incompatible hd shows 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"
  1. cli.exe.tableEach [[executable]] table declares one executable: name names it, and module names its entry module by its module path under the source root.
  2. cli.exe.severalA package may declare several executables, with unique names.
  3. cli.exe.missing-moduleAn executable whose module names no module of the package is an error. Error: missing-entry-point.
  4. cli.exe.unselected-main.lintA public main or main! in a source-root module named by no executable produces a lint finding. Warning: unselected-main.
  5. cli.exe.unselected-main.functionSuch a main or main! remains an ordinary function.
  6. cli.exe.private-main.missingA run/build request requiring an entry fails when its only main is private, naming that function. Error: missing-entry-point.
  7. cli.exe.private-main.analysisChecking, tests and library-module analysis accept an ordinary private main.
  8. cli.exe.default-mainWith no [[executable]] table in hd.toml, src/main.hd is the package's default executable, as Cargo's src/main.rs is.
  9. cli.exe.main-unlistedWhen hd.toml has an [[executable]] table, a src/main.hd that no table names is an error. Its fix-it adds a table for it, with the package's name and module = "main". Error: unlisted-entry.
  10. cli.exe.other-moduleAn executable's entry module other than src/main.hd, such as src/tools/migrate.hd, resolves relative uses as an ordinary module does: self is its own module.
  11. cli.exe.entry-programEvery executable's entry module is its own program and never part of the package's library, as src/main.hd is by module.path.main-file.
  12. cli.exe.entry-no-useA use of an executable's entry module from another module is an error. Error: unknown-module.
  13. cli.exe.default-nameThe default executable is named after the package, the name of its [package] table, as Cargo names src/main.rs.
  14. 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 main then 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
  1. cli.task.fileEach file tasks/NAME.hd in the package directory is a task named NAME, and it is an entry module.
  2. cli.task.programEach task is its own program, compiled separately from the package's other tasks.
  3. 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.hd never clashes with tasks/zip.hd.
  4. cli.task.sharedA module in a subdirectory of tasks, such as tasks/shared/zip.hd, is a shared task module. Every task of the package may use it.
  5. cli.task.beside-directoryA file directly under tasks beside a directory of the same name, such as tasks/shared.hd beside tasks/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.
  6. cli.task.relativeIn a task or a shared task module, relative lookup works as it does under src, with tasks in place of the package root.
  7. cli.task.root-fileA task resolves relative uses as a root file does, so its lookup starts at tasks. From tasks/build.hd, self.shared.zip names tasks/shared/zip.hd.
  8. cli.task.superA super in a task is an error. Error: unknown-module.
  9. cli.task.above-rootIn a shared task module, a super that moves above tasks is an error. Error: unknown-module.
  10. cli.task.program-useA use of a task from another module is an error. Error: unknown-module.
  11. 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.
  12. cli.task.shared-unused.lintLint reports an unused declaration in a shared task module only when no task uses that declaration.
  13. cli.task.usesA task may use the package's modules through pkg.
  14. cli.task.dev-dependenciesA task may use the package's dependencies and its dev dependencies.
  15. cli.task.cyclic-dev-dependencyA task may use a dev dependency that itself depends on the package, as an integration test module may.
  16. cli.task.not-shippedA task is never part of the package's library or executables, and a dependent package never builds it.
  17. cli.task.name-clashA task and an executable with the same name are an error when hd reads 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.hd beside x/ is a program under that layout, but the folder's parent under src, 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
  1. cli.run.nameIn package mode, hd run NAME runs the executable or task named NAME, and is an error when there is none.
  2. cli.run.default.onehd run alone 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.
  3. cli.run.filehd run FILE is an error whose message suggests hd run or hd run NAME.
  4. cli.run.package-onlyOutside any package, hd run and hd build are errors whose message suggests creating a package with hd new.

#Working Directory

  1. cli.run.cwd.taskA task runs with its package directory, the directory of its hd.toml, as its working directory.
  2. cli.run.cwd.executableAn executable runs in the working directory of the hd run command.
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
  1. cli.args.separatorThe words after the first -- of a command line are arguments of the program, and hd reads none of them as its own.
  2. cli.args.passhd FILE and hd run pass those arguments to the program they run, in order, as Cargo's cargo run -- does.
  3. cli.args.extra-wordA further positional word before --, as in hd 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

  1. cli.host.entry-bindingWhen hd runs an executable, a task, or a single file that has a main, it binds each host capability trait that the row of main or main! 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
  1. cli.host.default-profilehd FILE, hd run, and a task run their program under the default profile, which binds the host capability traits in the table above.
  2. cli.host.script-bindingWhen hd runs 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.
  3. cli.host.default-profile.main-rowThe row of main or main! still limits what the program gets: hd binds only the traits of the default profile that the row names, by cli.host.entry-binding.
  4. 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.
  5. cli.host.default-profile.console-write-closedUnder the default profile, write! and write_error! return .Err(ConsoleError.Closed) when the host cannot write the text. One such case is a pipe whose reader has closed it.
  6. cli.host.default-profile.input-closedread_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.
  7. cli.host.default-profile.grantedEach provider of the default profile touches only the resources that its program's capability grant covers.
  8. cli.host.process.envA program that the default profile's Process provider starts inherits the whole environment of the hd process, whatever the Env grant covers.
  9. cli.host.handles.exitA host handle is a value that a default profile provider opens and keeps in the host, such as a Net socket. When the program of hd FILE, hd run, or a task ends, hd closes every host handle it left open, and writes nothing about them.
  10. cli.host.process.windows-launcherOn the Node engine on Windows, hd supplies its own executable as the processLauncher for 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 println panic, by module.console.println-write-error.either, as Rust's println! does; hd FILE | head -1 stops 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 run closes them silently, and hd test reports them.

Note. A program that Process starts runs with the operating system's authority of hd, and no grant limits what it does. A Process grant 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
  1. cli.cap.tableThe [capabilities] table of hd.toml sets the grant of each host capability trait that it names.
  2. cli.cap.table.keys.with-package-hostIts keys are built-in capability names or quoted package capability paths beginning with pkg. or dep..
  3. cli.cap.value.trueThe value true grants the trait with no limit on its resources.
  4. cli.cap.value.falseThe value false denies the trait totally, by Total Deny.
  5. cli.cap.value.listA list of scope entries grants the trait only the resources that those entries cover, by Grant Scopes.
  6. cli.cap.value.emptyAn empty list covers no resource, so every call of the trait is refused by Partial Deny.
  7. cli.cap.value.unscopedA list is an error for a trait whose row in the table above has no scope entries.
  8. cli.cap.flag--cap NAME=VALUE sets the grant of the trait NAME for one command.
  9. cli.cap.flag.valueVALUE is true, false, or a list of scope entries separated by commas.
  10. cli.cap.flag.commandshd FILE, hd run, hd test, and hd with no FILE take --cap any number of times, before the -- of the program arguments.
  11. cli.cap.flag.unknown.with-package-hostA --cap flag whose NAME is neither a built-in capability name nor a package capability path is an error.
  12. cli.cap.flag.unscopedA --cap flag that gives a list to a trait without scope entries is an error.
  13. cli.cap.no-prompthd never asks whether to grant a resource, so a call outside its grant fails at once.
  14. cli.cap.package.pathA package capability path uses its root source-use spelling, such as pkg.ui.Ui or dep.server_sdk.server.HttpServer.
  15. cli.cap.package.booleanA package capability grant must be true or false.
  16. cli.cap.package.nominalPackage capability paths resolve through the selected dependency graph to complete nominal trait identities.
  17. cli.cap.table.requiredA package's [capabilities] table must list every host capability in each executable main requirement row. Error: missing-capability.
  18. cli.cap.table.required-unusedThis check includes declared row capabilities that the program's linked code does not use.
  19. cli.cap.table.required-builtinsConsole, Args, Clock, Random and ConsoleInput have no exemption from this check.
  20. cli.cap.table.required-packageA package-owned host capability is listed by one of its source capability paths.
  21. cli.cap.table.required-flagsA command-line grant does not replace a required manifest entry.
  22. cli.cap.table.required-entry-messageThe diagnostic names the missing capability and points at its written entry row or test registration.
  23. cli.cap.table.required-entry-fixIts exact fix-it inserts unrestricted grants for missing capabilities into the applicable tables of hd.toml.
  24. cli.cap.table.unusedListing a capability that no main uses is not an error.
  25. cli.cap.table.scriptA standalone script with no hd.toml needs no capability table.
  26. cli.cap.dev.table[dev-capabilities] accepts the same keys and values as [capabilities].
  27. cli.cap.dev.entriesTests and tasks use the root package's production and development grants together.
  28. cli.cap.dev.mainAn executable main uses only production grants.
  29. cli.cap.dev.requiredEvery test or task host capability must appear in either root grant table. Error: missing-capability.
  30. cli.cap.dev.denyA false entry in either table denies the capability for tests and tasks.
  31. cli.cap.dev.allOtherwise, true in either table grants the capability without resource limits.
  32. cli.cap.dev.scopesOtherwise, tests and tasks receive the union of both tables' scope entries before command flags apply.
  33. cli.cap.dev.dependenciesA dependency's development grants never contribute to the root program's grants.
  34. cli.cap.dev.shippingInstallation and deployment adapters read production grants alone.
  35. cli.cap.dev.fixA missing test or task capability receives an exact fix-it inserting its grant into [dev-capabilities].
  36. 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.toml holds 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
  1. cli.cap.source.productionExecutables under hd run, and hd FILE in a package, apply production grants and command flags.
  2. cli.cap.source.flags-onlyhd FILE outside any package, and hd with no FILE, apply only the command's flags, so the REPL takes its grant from them.
  3. cli.cap.source.hd-run.entryA program that hd_run! starts gets its executable or task grant tables, as hd run NAME would, without flags.
  4. cli.cap.buildhd build writes 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"]
  1. cli.cap.scope.pathAn FsRead or FsWrite entry covers the file it names, or every path under the directory it names.
  2. cli.cap.scope.path.resolvedhd resolves .. and symbolic links in the entry and in the accessed path before it compares them, so no path escapes a granted directory.
  3. 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.txt fails with NotFound when data/missing does not exist.
  4. 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.
  5. cli.cap.scope.write-not-readAn FsWrite entry grants no read.
  6. cli.cap.scope.renamerename! needs an FsWrite entry that covers each of its two paths.
  7. cli.cap.scope.hostAn Http entry covers a request whose URL host matches the entry's host, and whose port equals the entry's port when the entry gives one.
  8. cli.cap.scope.host.formsAn entry's host is a name, an IPv4 address, or a bracketed IPv6 address. The entry *.example.com matches every name that ends in .example.com.
  9. cli.cap.scope.redirecthd checks each redirect target of an Http request as a new request.
  10. cli.cap.scope.netA Net entry covers the address of a connect, listen, bind, or send by the host match of an Http entry. An entry without a port covers every port.
  11. cli.cap.scope.net.lookupA Net entry covers a DNS lookup of a host that its host matches.
  12. cli.cap.scope.envAn Env entry covers the variable it names. An entry that ends in * covers every name that starts with the text before the *.
  13. cli.cap.scope.processA Process entry covers a run! call whose program argument equals it, as written, before the host looks the program up.
  14. cli.cap.scope.sysA Sys entry covers the method of Sys that it names: os, arch, hostname, or cpu_count.

Note. In an integration test, run! names an executable or a task of the package (cli.test.process), so a Process entry 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
  1. cli.cap.total.needsA Wasm module's needs are the host capability traits whose methods its import list names.
  2. cli.cap.total.refuseWhen a need of a module is totally denied, hd refuses to start it, before its module initialization and before main. Error: denied-capability.
  3. cli.cap.total.messageThe refusal names the trait, and the setting that denied it: the hd.toml key or the --cap flag.
  4. cli.cap.total.statusA refused command exits with status 101, since no program ran, by cli.exit.hd-failure.
  5. 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.
  6. cli.cap.total.testhd test refuses an integration test module or a doc test with a totally denied need in the same way, with status 101.
  7. cli.cap.total.checkhd check may report a total deny before any run, but the startup refusal is the rule that decides.

Why. hd FILE.wasm runs prebuilt Wasm, whose source hd never 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
  1. cli.cap.partial.refuseA call whose resource lies outside its trait's grant returns the value of the table above, and never panics.
  2. cli.cap.partial.not-osNotGranted reports a refusal by the grant, and PermissionDenied stays the operating system's refusal.
  3. cli.cap.partial.path.resolvedWhen path resolution changes the target, a filesystem refusal names the resolved path in its message and grant hint.
  4. 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. NotGranted and PermissionDenied have different fixes, so an agent must tell them apart, as Deno 2 split NotCapable from PermissionDenied.

#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
  1. cli.cap.env.getWhen the Env grant is a list, get(name) returns .None for a name that no entry covers, even when the variable is set.
  2. cli.cap.env.noticeWhen such a variable is set on the host, hd writes one line to standard error, once per name in a run.
  3. 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 for NAME.
  4. cli.cap.env.namesnames returns only the set variables that an entry covers.

Why. A silent .None would 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
  1. cli.package.wholeIn package mode, hd build, hd check, and hd test without a FILE work on the whole package.
  2. cli.package.fileWith a FILE in the package, they work on that file's module, linked with the rest of the package.
  3. cli.package.no-rootIn package mode, hd check FILE and hd test FILE treat a FILE under no root as a single-file program, as hd FILE does. The roots are the source root, the test root, and tasks.
  4. cli.check.defaultA whole-package hd check checks what hd build compiles, the library and the executables, and no test code, as Cargo's cargo check does without --all-targets.
  5. cli.check.testshd check --tests also checks the package's test code: its tests: blocks, test modules, and integration test modules.
  6. cli.check.tests.dochd check --tests also checks the package's doc tests, except compile-fail ones, which only hd test judges.
  7. cli.check.allhd check --all checks the library, the executables, the test code, and the package's tasks.
  8. cli.build.directoryThe build directory is the directory build in the package directory, where hd writes its build and cache output.
  9. cli.build.outputA whole-package hd build writes each executable NAME to build/debug/NAME.wasm, or to build/release/NAME.wasm with --release.
  10. cli.build.output.filehd build FILE writes FILE's module to build/debug/files/STEM.wasm, or to build/release/files/STEM.wasm with --release. STEM is FILE's name without its .hd extension.
  11. cli.build.library-onlyA whole-package hd build of a package with no executable, such as a library-only package, checks the package and writes no .wasm file.
  12. 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.
  13. cli.build.depth-errorhd build, hd run, hd test, and hd FILE report the one error that only building a program finds: the instantiation limit's instantiation-too-deep.
  14. cli.check.no-depth-errorhd check builds no program, so it does not report the instantiation limit. It does report a generic call cycle that grows. A package that hd check accepts may still fail hd build.
  15. cli.build.facts-at-run-timeChecking or building a program evaluates no fact. A fact's panic is the runtime panic fact-evaluation-failed of 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 files directory keeps a FILE's module apart from the executables, so hd build src/shop.hd never overwrites the output of executable shop.

Why. Instantiations need the bodies of other modules and packages. hd check checks 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
  1. cli.check.max-errorshd check --max-errors N stops printing diagnostics after the Nth error. N is a whole number of at least 1.
  2. cli.check.max-errors.summaryThe summary still counts every error and warning, and the exit status is unchanged.
  3. cli.check.summary-modeIn text output, hd check --summary prints, in place of each diagnostic, one line for each pair of a code and a file, with its count. The summary follows.
  4. cli.check.output-options--max-errors and --summary change only what is printed. They change no diagnostic, count, or exit status.
  5. cli.check.report.streamIn text output, hd check writes its diagnostics and its summary to standard error, and nothing to standard output.
  6. cli.check.report.summaryThe last line is the summary: check result: ok. when the run reported no error, or check result: FAILED. when it did, then errors: E; warnings: W with both counts, zero counts included.
  7. cli.check.summary-mode.lineA --summary line is the severity, the file, the code, and the count, each followed by : except the count, as in error: src/cart.hd: type-mismatch: 2. The file is written as in a diagnostic.
  8. cli.check.summary-mode.order--summary lines 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
  1. cli.test.file-emptyhd test FILE is an error when FILE registers no test case.
  2. cli.test.package-emptyA whole-package hd test that registers no test case passes.
  3. cli.test.filterhd test --filter PATTERN runs only the test cases whose name contains PATTERN, with a FILE or without one.
  4. cli.test.filter.nonehd test FILE --filter PATTERN is an error when no test case of FILE has a name that contains PATTERN, as when FILE registers none.
  5. cli.test.builds-executableshd test builds the package's executables before it runs any test case, so an integration test may run them with hd_run!.
  6. cli.test.processFor each integration test module, hd test binds the host trait Process to a provider. That provider starts the package's executables, each by its name, as cli.exe.table names it.
  7. cli.test.process.tasksThat provider also starts each task of the package by its name, as hd run NAME does. A name names at most one program, by cli.task.name-clash.
  8. 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.
  9. cli.test.process.cwdEach executable that provider starts runs with the package directory, the directory of its hd.toml, as its working directory.
  10. 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.
  11. cli.test.runnerWhen it runs a test case, hd test binds the host traits TestRunner and PropertyRunner for the std.testing code around the body, by std-testing.runner.binding.
  12. cli.test.doc.defaulthd test runs the package's doc tests with its other test cases. With a FILE, it runs the doc tests of that file's module.
  13. cli.test.doc.nameA doc test is named doc <module>.<item>[i], by the table below.
  14. cli.test.doc.filter--filter matches a doc test by that name, so --filter doc selects every doc test, and --filter text.slugify the doc tests of slugify in src/text.hd.
  15. cli.test.doc.locationA diagnostic or a failure of a doc test names the .hd source file and a ## line of the doc test's block.
  16. cli.test.doc.jsonWith --format json, a doc test's test object holds that name in name, and its diagnostics give that file and line in file and line.
  17. cli.test.doc.name.rootFor a block in src/lib.hd, <module> is pkg, which names the package root module by module.path.lib-file.
  18. cli.test.doc.name.memberFor a block on a member, <item> is Type.member, so the first block on new of Slug in src/text.hd is doc text.Slug.new[0].
  19. cli.test.doc.name.moduleA block in a module's module documentation has no <item> and no dot. The first such block in src/text.hd is doc text[0].
  20. cli.test.doc.update-prefixAn update run, as hd test --update makes, rewrites a failing doc test snapshot's expected text in place, inside its block's ## lines, keeping each line's ## prefix and indentation. The cli.test.snapshot.update.* rules apply to it as to a call in code.
  21. cli.test.snapshot.updateAn update run rewrites the expect literal of a failing snapshot call in place, to the text that the call compared, as cli.test.doc.update-prefix does for a doc test.
  22. cli.test.snapshot.update.passingAn update run leaves a passing snapshot call as it is, and writes no file that holds no rewritten literal.
  23. 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.
  24. cli.test.snapshot.update.omittedA failing snapshot call with no expect argument gets expect= and the new literal, after its last argument.
  25. cli.test.snapshot.update.rowsAn update run does not rewrite a snapshot call that fails in an it_each, it_prop, or it_prop_with test case, since the rows or inputs of one call share its literal. Those test cases fail as they do without --update.
  26. cli.test.snapshot.update.conflictWhen failing test cases of one update run want different texts at one snapshot call, the call is left as it is, and those test cases fail.
  27. cli.test.snapshot.update.continueIn an update run, a failing snapshot call of an it test case records its new text, as std-testing.snapshot.update says, and the test case goes on. So one update run rewrites every failing snapshot call of the test case.
  28. cli.test.snapshot.update.acceptedA snapshot call 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.
  29. cli.test.snapshot.update.unfixableA failing snapshot call 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.
  30. cli.test.every-caseIn every output mode, hd test runs every selected test case of every file and reports each failure. A failing test case does not stop later ones.
  31. cli.test.seedhd test derives a property test case's base seed, the seed of its first case, from the test case's name. The same name gives the same base seed on every run.
  32. cli.test.seed.flaghd test --seed N makes N the base seed of every property test case it runs. N is a whole number.
  33. cli.test.seed.reportThe report of a failing property test case names its base seed, so a run with --seed and that seed draws the same cases.
  34. cli.test.leakA test case whose run otherwise passes fails when a host handle that the run opened is still open at its end.
  35. cli.test.leak.messageIts failure message names each kind of handle left open, such as TcpListener, with its count.
  36. cli.test.leak.closedhd test closes those handles once the run ends, so a later test case never sees them.
  37. cli.test.leak.runsEach run of a test case counts alone: a row of an it_each test case, and each case of a property test case.
  38. 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 by cli.test.leak.
  39. cli.test.cancelledA test case whose body turns out cancelled, by module.testing.cancelled, fails with the message cancelled.
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 using binding 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
  1. cli.test.report.passIn text output, a passing test case has no result line.
  2. cli.test.report.headingA failed case starts with FAIL or PANIC, its registration's file and line, and its name.
  3. cli.test.report.failureA FAIL block prints the failure message, indented by four spaces.
  4. cli.test.report.reproEach failure block ends with repro: hd test FILE --filter "NAME", indented by four spaces and command-line escaped.
  5. cli.test.report.panicA PANIC block prints panic: CATEGORY: MESSAGE, indented by four spaces.
  6. cli.test.report.frameIts symbolized frames follow as FILE:LINE FUNCTION, indented by four spaces and with the panic site first.
  7. cli.test.report.ignoredAn ignored case prints IGNORED, its registration's file and line, its name, and its reason.
  8. cli.test.report.summaryThe final line gives the run's verdict and its passed, failed, and ignored counts, including zero counts.
  9. cli.test.report.filterResult blocks and summary counts include only cases selected by --filter.
  10. cli.test.report.orderResult blocks use file-path order, then registration order, then row-index order, regardless of completion order.
  11. 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
  1. cli.test.programA test program is a program that hd test builds to run test cases: one per module with unit tests or doc tests, and one per integration test module.
  2. 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 hd version, or a test option such as --seed.
  3. cli.test.affectedhd test --affected runs only the test programs whose fingerprint differs from the one recorded when they last passed, or that have no record.
  4. cli.test.affected.recordhd test records 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.
  5. cli.test.affected.failedWhen a test case of a test program fails, hd test removes the program's record. So --affected runs that program until it passes in full.
  6. cli.test.affected.whereThe records live in the build directory, so after hd clean, hd test --affected runs 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
  1. cli.test.env.unithd test binds TestRunner alone for a unit test, in a package or outside one, by module.testing.unit-row.places.
  2. cli.test.env.integrationFor an integration test case or a doc test, hd test binds the traits of the default profile that the body's row names. It binds them as hd run does, except as this list says.
  3. 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 as fixtures/orders.csv then names a file of the package.
  4. cli.test.env.argsIts Args provider holds no program arguments, so list returns an empty list.
  5. cli.test.env.args.programIts Args.program returns the path of the test case's file relative to the package directory, such as tests/report.hd.
  6. cli.test.env.stdinIts standard input is closed, so every read_line! returns .Ok(.None), as at the end of input.
  7. cli.test.env.temp-dirhd test gives each test case its own fresh, empty temporary directory, which temp_dir returns. No other test case shares it, in the same run or in another run at the same time.
  8. cli.test.env.temp-dir.removedhd test removes that directory and everything in it once the test case ends, whether it passed or failed.
  9. 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.
  10. 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.
  11. cli.test.env.grant.fs-writeWhen neither the table nor a flag names FsWrite, it covers the test case's temporary directory.
  12. cli.test.env.grant.fs-addedWhen they give FsRead or FsWrite a 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 $.with provider 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 run gives. Fixed arguments, a closed standard input, and the package directory make its result independent of how and where hd test ran. A directory of its own lets two test cases write files without meeting, as Go's t.TempDir does.

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

  1. cli.test.tasksA whole-package hd test also runs the test cases of the tests: blocks in the package's tasks and shared task modules. They are unit tests.
  2. cli.test.tasks.filehd test tasks/NAME.hd runs the test cases of task NAME, by cli.package.file.
  3. cli.test.tasks.no-testsA whole-package hd test compiles no test build of a task, or of an executable's entry module, that holds no tests: block.
  4. cli.test.tasks.runAn integration test runs a task with hd_run!, by cli.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 fails hd test for 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
  1. cli.lint.selecthd lint selects files, packages, workspace members, tests and tasks by the same selection rules as hd check.
  2. cli.lint.optionshd lint accepts --format, --summary, --max-errors, --jobs, --tests, --all, and -p as checking does.
  3. cli.lint.prerequisiteshd lint reports ordinary compiler errors before running analyses that need successfully checked code.
  4. cli.lint.read-onlyhd lint runs no program and changes no source, manifest, or lockfile.
  5. cli.lint.ordinary-errorshd check, hd build, hd test, REPL evaluation, and the playground report compiler errors only.
  6. cli.lint.report-commandOnly hd lint reports the optional findings listed in the diagnostic registry.
  7. cli.lint.defaultEach registered lint runs at its declared default level.
  8. cli.lint.levelsA lint level is allow, warn, or deny, with the effects in the table below.
  9. cli.lint.exact-names--allow CODE, --warn CODE, and --deny CODE take exact registered lint names and may repeat.
  10. cli.lint.unknown-nameAn unknown lint name is an error, including a name in a site suppression.
  11. cli.lint.cli-conflictContradictory CLI levels for one lint are an error regardless of argument order.
  12. cli.lint.manifestThe selected packageโ€™s [lint] table accepts allow, warn, and deny lists of exact lint names.
  13. cli.lint.manifest-invalidA non-list policy value, non-string list element, or conflicting manifest levels for one lint is an error.
  14. cli.lint.cli-overrideAn explicit CLI level overrides the selected packageโ€™s manifest level for that exact lint.
  15. cli.lint.no-inheritA package inherits no parent-directory or workspace lint policy implicitly.
  16. cli.lint.standaloneA standalone file uses registered defaults and explicit CLI levels.
  17. cli.lint.dependency-policyA dependency cannot impose lint policy on its caller.
  18. cli.lint.site-allowA full-line # lint-allow: CODE immediately before a statement suppresses that code for calls directly owned by that statement.
  19. cli.lint.site-scopeA declarationโ€™s site suppression never extends into its nested function or closure body.
  20. cli.lint.site-precedenceA site suppression overrides manifest and CLI deny for its code.
  21. cli.lint.errors-unsuppressibleLint levels and suppressions never suppress compiler errors or make invalid code valid.
  22. cli.lint.orderFindings are ordered by package-relative path, source span, then stable lint code.
  23. cli.lint.outputLint reuses the diagnostic objects and final summary of machine output.
  24. cli.lint.text-streamLint text diagnostics and summaries go to standard error; JSON output goes to standard output.
  25. cli.lint.totalsDiagnostic truncation changes no total count or exit status.
  26. cli.lint.status-warningA successful lint run with only warning-level findings exits with status 0.
  27. cli.lint.status-errorA lint run with a compiler error, denied finding, invocation error, or configuration error exits with status 101.
  28. 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
  1. cli.profile.defaulthd FILE, hd run, hd build, hd test, and the REPL use the debug profile, which panics on integer overflow by types.arith.checked.
  2. cli.profile.release--release on hd build and hd run selects the release profile, which wraps on integer overflow by types.arith.release.
  3. cli.profile.release.file--release on hd FILE selects the release profile too.
  4. cli.profile.testhd test always uses a checked build, the test profile, including for the executables it builds by cli.test.builds-executables.
  5. cli.profile.test.releasehd test --release keeps the test profile, so integer overflow still panics. The flag selects only the pipeline.
  6. cli.profile.release.commandshd FILE, hd run, hd build, and hd test take --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
  1. cli.profile.pipeline.release--release selects 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.
  2. cli.profile.pipeline.unobservableThe pipeline is unobservable. In one profile, two pipelines give the same output, exit status, panic categories, and panic sites.
  3. 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.
  4. 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 --release can test optimized code without dropping the checks, as Zig's ReleaseSafe mode 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
  1. cli.engine.defineAn engine is the runtime that executes a program's compiled module.
  2. cli.engine.offeredAn implementation offers one or more engines. Which engines exist, and what they are called, is implementation-defined.
  3. cli.engine.flaghd FILE, hd FILE.wasm, hd run, hd build, and hd test take --engine NAME, where NAME names an engine.
  4. cli.engine.selectA command that takes --engine uses the engine that the flag names. Without the flag, it uses the engine that the environment variable HD_ENGINE names. Without both, it uses the default engine.
  5. cli.engine.env-emptyAn empty HD_ENGINE counts as unset.
  6. cli.engine.defaultWhich engine is the default is implementation-defined.
  7. cli.engine.buildhd build --engine NAME builds the module for that engine. A module that hd build wrote runs on every engine, whichever engine it was built for.
  8. cli.engine.same-behaviorA program's output, exit status, panic categories, and panic sites are the same on every engine, as cli.profile.pipeline.unobservable says of pipelines.
  9. cli.engine.differencesBetween engines, only what cli.profile.pipeline.limits lists may differ.
  10. 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.
  11. 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

  1. cli.codegen.singleEvery command that builds a program uses the implementation's one code generator.
  2. 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
  1. cli.dbg.runhd FILE and hd run write each dbg line to standard error, never to standard output.
  2. cli.dbg.testhd test keeps the dbg lines 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.
  3. cli.dbg.replIn the REPL, the dbg lines of an input print before the input's value line.
  4. cli.dbg.releasehd build --release and hd run --release reject a dbg call in the user's own code, by module.dbg.release, and hd run --release then runs nothing.
  5. cli.dbg.release.filehd --release FILE rejects such a call too, and then runs nothing.
  6. cli.dbg.test-releasehd test --release accepts a dbg call, since it builds in the test profile by module.dbg.release.debug-build.
  7. cli.dbg.dependency.lintLint reports fetched-package dbg use once per package, by module.dbg.dependency.lint. Warning: dbg-in-dependency.

Note. The browser playground shows dbg lines 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
  1. cli.install.sourcehd install SOURCE selects the package named by SOURCE independently of the caller's package.
  2. cli.install.resolveSOURCE follows the path, repository and version-query forms of dependency targets, without requiring a library.
  3. cli.install.executablesInstallation builds every executable of the selected package.
  4. cli.install.no-executableA source without a package manifest or without executables is an error.
  5. cli.install.release-wasmEach installed executable is release Wasm with a launcher through the current hd runtime.
  6. cli.install.no-executionInstallation runs no guest entry, test, package provider or installation hook.
  7. cli.install.rootThe installation root is --root DIR, else HD_INSTALL_ROOT, else .hd in the user's home directory.
  8. cli.install.layoutLaunchers live under ROOT/bin, and managed artifacts and installation records under ROOT/installed.
  9. cli.install.path-guidanceInstallation prints PATH guidance without editing shell startup files.
  10. cli.install.launcher-runtimeEach launcher selects hd from PATH when invoked.
  11. cli.install.launcher-argumentsEach launcher forwards every argument unchanged as application data.
  12. cli.install.launcher-streamsInstalled programs inherit the caller's standard input, standard output and standard error.
  13. cli.install.launcher-statusThe installed command returns the program's exit status.
  14. cli.install.launcher-cwdThe installed program keeps the caller's working directory.
  15. cli.install.program-nameIts Args.program is its installed executable name.
  16. cli.install.no-checkoutThe launcher and loader require no original source checkout at invocation.
  17. cli.install.namesInstalled executable names must be portable flat filenames, distinct without ASCII case, other than hd and Windows device names.
  18. cli.install.listhd install --list lists one row per installed executable, ordered by name.
  19. cli.install.list-fieldsEach row contains name, canonical source identity and revision, separated by tabs.
  20. cli.install.ownerA local source's identity is its canonical directory; a remote source's identity is its canonical host path.
  21. cli.install.updateReinstalling the same source updates its commands after every executable builds and validates successfully.
  22. cli.install.removed-executablesAn update removes formerly installed commands that its source no longer declares.
  23. cli.install.collisionInstallation refuses an unmanaged command or a command owned by a different source.
  24. cli.install.modified-launcherInstallation and removal refuse a managed launcher whose contents were replaced or modified.
  25. cli.install.failed-validationBuild, capability-check and collision failures preserve previously installed commands.
  26. cli.install.manifest-grantsInstalled programs use the package's installation-time [capabilities] table, with no additional grant policy.
  27. cli.install.path-scopesRelative filesystem grants resolve against the source package directory at installation.
  28. cli.install.show-grantsInstallation and update display the package's capability table.
  29. cli.install.denyRepeatable --deny CAP refuses that installation when the package table grants CAP.
  30. cli.install.deny-once--deny is not stored and affects no later installation or invocation.
  31. cli.install.provider-unavailableInstallation of an executable using package-owned JavaScript providers is refused with a diagnostic naming the unsupported provider.
  32. cli.install.record-versionAn installed loader validates its installation record format before guest initialization.
  33. cli.install.abiIt validates an explicit Wasm/runtime ABI revision before guest initialization.
  34. cli.install.hashIt validates the artifact's SHA256 before guest initialization.
  35. cli.install.incompatibleAn incompatible ABI or artifact hash mismatch refuses execution and gives an explicit reinstall command.
  36. cli.install.no-implicit-rebuildInvocation never fetches or rebuilds an incompatible installed program automatically.
  37. cli.uninstall.onehd uninstall NAME removes one installed executable's managed launcher and record.
  38. cli.uninstall.payloadRemoval reclaims payloads that no installed executable still references.
  39. cli.uninstall.roothd uninstall takes the same root selection as hd 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

  1. cli.doc.commandhd doc writes the HTML and the Markdown of the package's documentation together. No flag selects one format.
  2. cli.doc.packagehd doc works on the package of package mode, and is an error outside any package.
  3. cli.doc.workspaceIn workspace mode, hd doc needs -p NAME, by Selecting Members, to select one member. Without it, hd doc is an error.
  4. cli.doc.checkhd doc checks the package as hd check does. When that reports an error, hd doc writes and prints no documentation.
  5. cli.doc.markdownThe text of a documentation comment is Markdown, as CommonMark defines it.
  6. cli.doc.itemshd doc documents each pub item of every module under the source root, with the pub members of each, as the table below lists. Items are functions, structs, enums, traits, and type aliases.
  7. cli.doc.private--private also documents the private items and members. It applies to the package only, and to hd doc NAME as to the pages.
  8. cli.doc.mainAn application's src/main.hd is no library module, so it has a page only with --private. That page is the module main, as main.md and main.html.
  9. cli.doc.reexportA pub use of 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

  1. cli.doc.dirhd doc writes into the directory doc of the build directory, as build/doc. --out DIR writes into DIR instead.
  2. cli.doc.open--open opens the entry page index.html of the output with the platform's default program, after the files are written.
  3. cli.doc.open.failureFailure to launch the browser for hd doc --open is a command failure with non-zero status.
  4. cli.doc.open.failure.pathThe failed command still prints the documentation path and retains the files already written.
  5. cli.doc.filesThe output holds the files of the table below. A module's HTML and Markdown pages sit side by side.
  6. cli.doc.root-pageThe root module's pages are named pkg, as module.path.reserved-pkg reserves the name, so no user module's page takes their file names.
  7. cli.doc.index-moduleA top-level module named index has its pages at index/index.html and index/index.md, so the entry page keeps the names index.html and index.md.
  8. cli.doc.relativeA link between the output's files is a relative path, so the output works from any base path.
  9. 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

  1. 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.
  2. cli.doc.summaryAn item's summary is the first sentence of its documentation, or empty when it has none.
  3. cli.doc.itemEach item has a second-level heading with its name in code, and each member a third-level heading with Type.member in code. Below the heading come the signature, the documentation, and the examples.
  4. cli.doc.anchorAn item's anchor is its item name, as cli.test.doc.name forms <item>, such as slugify or Slug.new.
  5. cli.doc.signatureA signature is the source text of the declaration, copied as written, in a fenced block whose info string is hd.
  6. 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).
  7. cli.doc.conciseA page holds no function body and no navigation text.
  8. cli.doc.exampleEach doc test appears in place, as written, in an hd block. The word Example and the doc test's name in code, as cli.test.doc.name forms it, come before the block.
  9. 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: CODE line.
# 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

  1. cli.doc.llmsllms.txt starts with a heading of the package's name. Then it holds a blockquote with the summary of the root module's documentation, and a ## Modules list.
  2. cli.doc.llms.modulesThe list has one entry for each module, pkg first: a link to its .md page, a colon, and the summary of the module's documentation. The link of a top-level module index is to index/index.md.
  3. cli.doc.llms-fullllms-full.txt holds 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.
  1. 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.
  2. cli.doc.link.scopeNAME resolves in the module scope of the documented item: its own declarations and the names its use declarations bind. A dotted NAME names a member or a path through modules.
  3. cli.doc.link.resolvedA resolved link becomes a link to the target's anchor, on the target's page.
  4. cli.doc.link.externalA NAME that resolves to a std item 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.
  5. cli.doc.link.broken.lintAn unresolved documentation NAME or an undocumented private target produces a lint finding. Warning: broken-doc-link.
  6. 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

  1. cli.doc.namehd doc NAME prints 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.
  2. cli.doc.name.formNAME is a module path and an item name joined by a dot, as in text.slugify or text.Slug.new. The module is the longest prefix that names a module, and pkg names the root module.
  3. cli.doc.name.moduleA NAME that names only a module prints that module's whole Markdown page.
  4. cli.doc.name.dependencyA NAME that starts with dep. and a dependency's key, as in dep.json.parse, names an item of that dependency. Only its pub items are found.
  5. cli.doc.name.stdA NAME that starts with std., as in std.text.split, names an item of the standard library: std, a module, and an item of that module.
  6. cli.doc.name.std.outputIt prints the item's signature in an hd block, its summary, and a link to the item's section of the standard library reference.
  7. cli.doc.name.missingA NAME that names no documented item is an error. That includes a std NAME 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 NAME answers 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
  1. 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, as cli.json.fix.object gives 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}
  1. cli.json.commandshd build, hd check, hd test, hd run, hd doc, hd FILE, and the dependency commands take --format json.
  2. 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.
  3. cli.json.runWith it, hd run and hd FILE write only hd's own diagnostics and summary as JSON lines, and write them to stderr.
  4. cli.json.run.programThe program's standard output passes through to stdout untouched.
  5. cli.json.kindEach object's kind field is "diagnostic", "test", or "summary".
  6. cli.json.diagnosticEach diagnostic is one object, with its stable code, its severity, and its file and position.
  7. cli.json.diagnostic.fieldsA diagnostic object has the fields code, severity, message, file, line, and column.
  8. cli.json.diagnostic.fileIn a package, file is the path relative to the package root, as src/cart.hd, whatever the working directory. Outside a package, file is the path as written on the command line. At a workspace root, file is relative to the workspace root, as lib/src/lib.hd, so two members' files stay apart.
  9. cli.json.test.resultEach test case's result is one object, with the test case's name and its outcome: passed, failed, or ignored.
  10. cli.json.test.result.fieldsA test object has the fields name, outcome, and message. outcome is "passed", "failed", or "ignored", and message holds the failure or ignore reason, or "" when there is none.
  11. cli.json.test.orderhd test lists the test objects in the order of the files' paths, and within one file in declaration order.
  12. 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.
  13. cli.json.summary.result.fieldsA summary object has the counts errors, warnings, passed, failed, and ignored, and status, the command's exit status.
  14. cli.json.summary.modules-checkedThe summary object of hd check also has the count modules_checked: the modules whose bodies this run checked, rather than loaded from the compiled cache.
  15. 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.
  16. cli.json.fix.objectA fix-it is an object with the fields message, which says what the fix-it does, and edits, a list of its edits.
  17. cli.json.fix.editAn edit is an object with the fields file, start, end, and text. It replaces the bytes of file from offset start up to offset end with text.
  18. cli.json.fix.offsetsstart and end count bytes of the file's UTF-8 text from 0, and the byte at end is not replaced. file is a path as cli.json.diagnostic.file gives it.
  19. cli.json.fix.disjointThe edits of one fix-it never overlap, so applying them together applies the fix-it.
  20. cli.json.diagnostic.no-positionA diagnostic that no source text causes, such as denied-capability, has null for file, line, and column.
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_checked counts 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-finished message does, and a missing summary means that hd did 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
  1. cli.exit.successA command that completes with no error exits with status 0. Warnings do not change its status.
  2. cli.exit.hd-failureWhen hd itself 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.
  3. cli.exit.programOnce its program is built, hd FILE or hd run exits with the program's own status, by Exit Status and module.entry.panic.
  4. cli.exit.test-failurehd test exits with status 1 when a test case fails and hd reports no error.
  5. cli.exit.fmt-checkhd fmt --check exits with status 1 when a file is not formatted and hd reports no error.
  6. cli.exit.cancelledhd FILE or hd run exits 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
  1. cli.cancel.ctrl-cA Ctrl-C while the program of hd FILE, hd FILE.wasm, or hd run waits on the host cancels its entry suspension, by req.entry.cancel.
  2. cli.cancel.boundThe cleanup bound of that cancel is the --cleanup-limit value.
  3. cli.cancel.flag--cleanup-limit DURATION takes a DURATION of the forms in Program Limits. Those three commands take the flag, before the -- of the program arguments.
  4. cli.cancel.defaultWithout the flag, the cleanup bound is ten seconds.
  5. 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.
  6. 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.
  7. cli.cancel.computingA Ctrl-C while the program computes stops it, as cli.repl.stop stops an input. It then writes stopped to standard error.
  8. cli.cancel.endA cancelled entry, by req.entry.cancelled, writes cancelled to standard error when its cleanup ran to its end, and cancelled, cleanup abandoned when it was abandoned.
  9. cli.cancel.statusA run that is cancelled or stopped exits with status 130, its cancellation status, on every platform.
  10. cli.cancel.signalsOn Unix, a SIGINT or a SIGTERM that is sent to hd alone is a Ctrl-C in every rule above, on every engine.
  11. cli.cancel.forwardOn the Node engine, hd passes 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 to hd alone.
  12. cli.cancel.windowsOn Windows, hd passes 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 stop waits 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 stop and 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 if hd passed it on as the same signal. It ignores SIGINT and hears the interrupt from hd as 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
  1. cli.fmt.packageIn package mode, hd fmt formats every .hd file under the package's source root, test root, and tasks, and writes each changed file in place.
  2. cli.fmt.filehd fmt FILE formats FILE alone, in a package or outside one. Outside any package, hd fmt without a FILE is an error.
  3. cli.fmt.meaningFormatting changes only the layout of a file. It keeps the file's meaning and every comment.
  4. cli.fmt.idempotentFormatting a formatted file changes nothing.
  5. cli.fmt.checkhd fmt --check writes no file. It prints the path of each file that formatting would change.
  6. cli.fmt.syntax-errorA file with a syntax error is left untouched, and hd fmt reports its syntax diagnostics. It still formats the other files.
  7. cli.fmt.no-checkhd fmt reads no other module and checks no type, so a file with a type error is still formatted.
  8. cli.fmt.width.wrapA code line longer than 100 columns is wrapped at its syntactic grouping boundaries.
  9. cli.fmt.width.elementsAn overlong struct literal, call, parameter list, or list literal places one element per line.
  10. cli.fmt.width.comma-groupsOther comma-delimited groups use the same layout, including tuples, maps, use groups, generic parameters, and type arguments.
  11. cli.fmt.width.indentA broken group's elements are indented four spaces beyond the line holding its opening delimiter.
  12. cli.fmt.width.closeA broken group's closing delimiter starts a separate line at the opening line's indentation.
  13. cli.fmt.width.binary-groupingA broken binary chain uses parentheses for implicit line continuation.
  14. cli.fmt.width.binary-operandsA broken binary chain places one operand per line, with the operator before each later operand.
  15. cli.fmt.width.protectedComment and string literal text remains as written, even when it exceeds the width.
  16. 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
  1. cli.fix.command.all-entrieshd fix checks the package as hd check --all does and applies safe source and capability-table fix-its.
  2. cli.fix.safeA fix-it is safe when applying it adds no syntax error and no new diagnostic. hd fix applies no other fix-it.
  3. cli.fix.overlapWhen the edits of two safe fix-its overlap, hd fix applies only the one whose diagnostic comes first in output order.
  4. cli.fix.roundsAfter it applies fix-its, hd fix checks 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.
  5. cli.fix.reporthd fix prints each change it made, with its file and the code of its diagnostic, then the diagnostics that remain.
  6. cli.fix.statushd fix exits with status 0 when no error remains, and with status 101 otherwise.
  7. cli.fix.manifest.capabilitieshd fix applies a missing-capability fix-it to the package's hd.toml.
  8. cli.fix.manifest.otherIt reports other manifest fix-its without applying them.

Why. An agent fixes a typo or a missing use in 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
  1. cli.limit.time--time-limit DURATION limits how long the program runs. A program that runs past it panics, by flow.panic.time-limit.
  2. cli.limit.commandshd FILE, hd FILE.wasm, hd run, and hd test take the flag, before the -- of the program arguments.
  3. cli.limit.noneThe limit has no default. Without the flag, a program is limited only by its host.
  4. 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
  1. 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
  1. cli.jobs--jobs N sets the number of threads that hd uses to check, build, format, and run tests. N is a whole number of at least 1.
  2. cli.jobs.envThe environment variable HD_JOBS sets that number when no flag does.
  3. cli.jobs.defaultWithout either, hd uses the number of processor cores, or 8 when there are more.
  4. cli.jobs.resultThe number of threads changes no diagnostic, test outcome, or output order.

Why. Many agents often run hd at 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
  1. cli.workspace.membersIn workspace mode, hd check, hd test, and hd build act on every member of the workspace.
  2. cli.workspace.run-nameIn workspace mode, hd run NAME runs the executable or task named NAME when exactly one member has one.
  3. cli.workspace.run-ambiguousWhen several members have one, hd run NAME is an error whose message lists those members.
  4. cli.workspace.run-missingWhen no member has one, hd run NAME is an error.
  5. cli.workspace.run-bareIn workspace mode, hd run with no NAME is an error whose message lists each member's executables and tasks.
  6. 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
  1. cli.workspace.select.anywhere-p NAME or --package NAME selects the member whose package is named NAME. This works wherever a command finds a workspace: at its root, or inside a member by cli.mode.member. It works on hd run, hd test, hd check, and hd build.
  2. cli.workspace.select.repeatThe flag may be repeated, and the command then acts on the selected members only, by the rules above.
  3. cli.workspace.select.unknownA -p NAME that 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 -p does.

#Names

  1. cli.name.hyphenA package's name and an executable's name may contain -, as Cargo's do.
  2. 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 key my-app is dep.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
  1. cli.new.kindhd new creates an application with --app and a library with --lib.
  2. cli.new.kind.askWith neither flag, hd new asks which kind to create when standard input is a terminal.
  3. cli.new.kind.no-terminalWith neither flag and standard input not a terminal, hd new is an error whose message names --app and --lib. It never picks a kind itself.
  4. cli.new.apphd new --app PATH creates the directory PATH, holding an hd.toml, a src/main.hd whose program prints hello, world, and an integration test tests/NAME.hd.
  5. cli.new.app.testThat test runs the executable with hd_run!, and checks its output and its exit status.
  6. cli.new.libhd new --lib PATH creates the directory PATH, holding an hd.toml, a src/lib.hd that declares one sample public function, and an integration test tests/NAME.hd.
  7. cli.new.lib.testThat test checks the sample function through the package's public interface, with use pkg.
  8. cli.new.test-nameIn both, NAME is the package's name with each - replaced by _, so the file is a valid module path.
  9. cli.new.here.dirWith no PATH, or with ., hd new does the same in the working directory.
  10. cli.new.existinghd new is an error when any file it would write already exists, and it then writes nothing.
  11. cli.new.no-executable-tableThe hd.toml that hd new writes has no [[executable]] table. With --app, src/main.hd is then the default executable, and hd run runs it right after hd new.
  12. cli.new.workspace-memberWhen the new package's directory lies under a workspace root, hd new also adds that directory to the workspace manifest's members, as Cargo does.
  13. cli.new.vcsUnless the new package's directory is already inside a git repository, hd new runs git init there and writes a .gitignore, as Cargo does.
  14. cli.new.vcs.ignoreThat .gitignore lists only the build directory, as /build/. hd.sum is not listed, so it is committed.
  15. cli.new.vcs-nonehd new --vcs none runs no git init and writes no .gitignore.
  16. cli.new.capabilitiesThe application template lists Console, Args, Clock and Random as true in [capabilities].
  17. cli.new.dev-capabilitiesThe application template lists Process as true in [dev-capabilities].

Note. Running Executables shows the test that hd new --app writes.

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 of members would be skipped by every root command without a word.

#GitHub Pages Workflow

  1. cli.new.pageshd new --pages also writes .github/workflows/docs.yml in the new package's directory, for an application and for a library.
  2. cli.new.pages.workflowThat workflow runs hd doc and deploys the output directory to GitHub Pages.
  3. cli.new.pages.askWhen hd new asks which kind to create, by cli.new.kind.ask, it also asks whether to publish the documentation to GitHub Pages. A yes is --pages.
  4. cli.new.pages.defaultWithout --pages, and without that question, hd new writes 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 hd with a setup-hd action, pinned to the version of hd that 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 new cannot set it.

Note. cli.new.existing covers the workflow file too: when it exists already, hd new writes nothing.

#REPL

cd shop/src/billing
hd                # use self.util names shop/src/util.hd
echo 'println(1 + 2)' | hd    # prints 3
  1. cli.repl.open.terminalhd with no arguments opens the REPL when standard input is a terminal.
  2. cli.stdin.programWhen standard input is not a terminal, hd with no arguments runs all of it as a single-file program, as python does. This holds in every mode.
  3. 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 of src/lib.hd, and use self.util names src/util.hd.
  4. cli.repl.package.dependenciesIn package mode, the session may use the package's dependencies and its dev dependencies.
  5. cli.repl.package.no-libIn a package without src/lib.hd, the session may still use the public declarations of the package's modules through self, as src/main.hd does. It may also use the package's dependencies, its dev dependencies, and std.
  6. cli.repl.package.no-lib.mainsrc/main.hd itself stays unusable from the session, as it is from every module by module.path.main-no-use.
  7. cli.repl.uses.otherApart from the declarations of src/lib.hd, names reach the session only through its use declarations.
  8. cli.repl.outsideOutside any package, the session may use only std.
  9. 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.
  10. cli.repl.host.default-profileA session runs its inputs under the default profile, as hd FILE does, so an input may call each host capability trait that the profile binds.
  11. 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.
  12. cli.repl.host.cwdA session's relative paths resolve from the working directory of the hd command.
  13. cli.repl.host.argsIn a session, Args gives an empty program name and no program arguments.
  14. 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.
  15. cli.repl.valueThe REPL shows an expression input's value as dbg prints it, then : and its type, as in [3, 6] : List[usize]. So a value of a type without Display or Debug shows 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 is void or never.
  16. cli.repl.value.streamThe REPL writes the value line to standard output, after the input's own output and its dbg lines.
  17. cli.repl.panicWhen an input panics, the REPL reports the panic, and the session continues with the next input.
  18. cli.repl.panic.bindingAn input that panics adds no binding to the session.
  19. 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.
  20. 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 := 21 has no signed literal, so x is a usize. A session that shows types then shows x * 2 as a usize. y := -21 is an i32, by types.literal.local.default.

Why. An agent that pipes code into hd gets 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 FILE binds. Python's REPL likewise reads files and the clock, with an empty sys.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
  1. cli.repl.declarations.bindingsA declaration in an input may read and use the top-level bindings of earlier inputs.
  2. 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.
  3. 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.
  4. 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 Debug added 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 Point would fail against the new Point, and the old type would keep its implementations.

#Interrupting An Input

  1. 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.
  2. 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, and cli.repl.panic.mutations apply: the input adds nothing, and its earlier changes and host calls remain.

#Commands

  1. cli.repl.commandAn input that starts with : is a REPL command, not hd code.
  2. cli.repl.command.help:help lists the commands.
  3. cli.repl.command.quit:quit, :q and :exit end the session, as end of input at an empty prompt does.
  4. cli.repl.command.reload:reload reads the package's files again and rebuilds the session by cli.repl.rebuild.

#Rebuilding A Session

  1. 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.
  2. 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.
  3. 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.
  4. cli.repl.checkpoint.continueWhen the active host-call record exceeds its limit, the REPL checkpoints completed inputs and continues accepting inputs.
  5. cli.repl.checkpoint.initializedLater inputs import the checkpoint's initialized bindings and code without executing the checkpointed inputs again.
  6. cli.repl.checkpoint.effectsCreating or restoring a checkpoint repeats no host call.
  7. 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
  1. cli.repl.scripthd --repl FILE runs 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, as hd does on a terminal.
  2. 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.
  3. 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.
  4. cli.repl.script.outputEverything else the session writes goes where a terminal session sends it, by cli.repl.value.stream and cli.dbg.repl, after the echo of the line that started the input.
  5. 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.
  6. cli.repl.script.enginehd --repl takes --engine NAME, as hd run does by cli.engine.flag, and cli.engine.select picks the engine, except that with neither --engine nor HD_ENGINE the session runs on the engine of the terminal REPL. An engine that cannot run a session is refused as cli.engine.unknown says.

Note. A script is how a conformance CLI case tests the session rules, since a case runs hd with a closed standard input and so cannot hold a terminal.

Why. An hd with piped standard input runs a program by cli.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
  1. cli.dep.implicit-fetchBefore they compile, hd check, hd build, hd run, and hd test select the package's dependency versions by Version Selection. They fetch each version they need that the cache lacks.
  2. cli.dep.reachedA command needs every version that a requirement reaches, since selection reads its manifest, by module.select.reach.
  3. cli.dep.offlineA command that finds every version it needs in the cache uses no network.
  4. cli.dep.missing-sumIn those commands, a selected version that has no hd.sum entry is an error at its requirement in hd.toml. The message names hd add and hd fetch. Error: missing-sum-entry.
  5. cli.dep.no-sum-writehd check, hd build, hd run, and hd test never write hd.sum or hd.toml.
  6. cli.dep.verifyA selected version whose tree hash differs from its hd.sum entry is an error, by module.sum.mismatch. Error: sum-mismatch.
  7. cli.dep.pathA path requirement names the package in its directory, by module.path-dep.form. It is never fetched, and it has no hd.sum entry.
  8. 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.
  9. cli.dep.no-libraryA requirement whose package has no src/lib.hd is an error, by module.path.no-lib-dependency. Error: invalid-requirement.
  10. 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.
  11. 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.sum is 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.mod lines.

#Repositories

  1. cli.dep.repo-urlhd fetches a host path's repository from the URL https:// followed by its repository part. So github.com/acme/tools/lint is fetched from https://github.com/acme/tools. When the HTTPS fetch fails for any reason, hd tries ssh://git@ followed by the repository part, without prompts. Within one run, a host that answered over SSH is tried over SSH first. url.<base>.insteadOf rewrites apply to each URL first. When both URLs fail, one error names both. Error: fetch-failed.
  2. cli.dep.githd fetches with the system's git command, so git's own configuration applies, such as credential helpers, SSH keys, and url.<base>.insteadOf rewrites.
  3. cli.dep.tagA version's tree is the package's directory in the commit of its tag, by module.version.tag and module.version.tag-prefix.
  4. 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.
  5. cli.dep.unknown-versionA requirement whose package has no tag for its version is an error, by module.version.tag-missing. Error: unknown-version.
  6. cli.dep.no-secretA message never shows a credential. A user name or password in a URL that git reports is replaced by ***.
  7. cli.dep.pseudoA pseudo-version's tree is the package's directory in the commit that its HASH names. hd fetches the repository's branches and tags to find that commit.
  8. cli.dep.pseudo.unknownA pseudo-version whose HASH names no fetched commit, or whose TIME is not that commit's committer time in UTC, is an error, by module.version.pseudo-missing. Error: unknown-version.
  9. 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.
  10. cli.dep.pseudo.base.missingA pseudo-version whose base tag does not exist is an error. Error: unknown-version.
  11. 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.
  12. 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-HASH and 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-HASH orders below every release, so it needs no base.

Why. git already knows how to reach a private repository, so hd stores no credentials, by module.repo.no-stored-credentials. An insteadOf rewrite 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
  1. cli.cache.directoryThe cache directory is the platform's user cache directory followed by hd, as the table gives it. When the environment variable HD_CACHE is set, it names the cache directory instead.
  2. cli.cache.entryA fetched version is stored once, in the directory pkg/HOST_PATH@VERSION of the cache directory, such as ~/.cache/hd/pkg/github.com/acme/json@2.1.0.
  3. cli.cache.entry.treeThat directory holds the version's tree: its manifest, its source, and its other files.
  4. cli.cache.sharedEvery package of the user shares the cache, so a version is fetched once per machine.
  5. cli.cache.read-onlyhd makes an entry read-only once it is written, and never changes it afterwards.
  6. cli.cache.completeAn entry appears only once it is complete, so an interrupted fetch leaves no partial entry.
  7. cli.cache.hashhd records an entry's tree hash when it writes the entry. A command that uses a cached entry compares that record with the hd.sum entry, by cli.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.

  1. cli.sum.lineEach tree line of hd.sum is a host path, @, a version, one space, and the tree hash of that version.
  2. 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.
  3. 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, by cli.dep.pseudo. A subdirectory that holds its own hd.toml is another package, and its files are not part of the tree.
  4. 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.
  5. cli.sum.summary.pathThat path is relative to the package directory, with / between its segments.
  6. cli.sum.hashThe tree hash is h1: followed by the Base64 encoding, with padding, of the SHA-256 of the summary.
  7. 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.
  8. cli.sum.manifest-lineA manifest line is a host path, @, a version, /hd.toml, one space, and the manifest hash of that version. hd.sum holds one for each version that selection reads, by module.select.reach.
  9. cli.sum.manifest-hashA version's manifest hash is the tree hash, by cli.sum.hash, of a tree that holds only the version's hd.toml.
  10. cli.sum.entryBoth kinds of line are entries of hd.sum. cli.sum.keep holds for a manifest line too, whose mismatch is the error of cli.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
  1. cli.dep.add.targethd add TARGET[@QUERY]... sets, for each target, the requirement of its key in [dependencies]. It adds the key when the manifest has none.
  2. cli.dep.add.checkhd add checks the --name value and each target by cli.dep.invalid before it fetches anything.
  3. cli.dep.add.lowerhd add may set a key's requirement to an earlier version of the same host path, as go get does. It then prints that it lowered the requirement, as lowered json 2.1.0 -> 2.0.0.
  4. 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.
  5. cli.dep.add.devhd add --dev TARGET[@QUERY]... sets the requirements in [dev-dependencies] instead, as cli.dep.add.target does for [dependencies]. The other rules of hd add hold for it.
  6. cli.dep.add.other-tableWhen a target's key is in the other table, hd add is an error. It reports NAME is in [dependencies]; run `hd remove NAME` first, or names [dev-dependencies] in the reverse case. It writes nothing, by cli.dep.unchanged-on-error, and fetches nothing when --name gives the key.
  7. cli.dep.add.severalhd add with several targets changes the manifest once, for all of them. When one target fails, it writes nothing, by cli.dep.unchanged-on-error.
  8. cli.dep.add.reportOnce it has written the manifest, hd add prints one line per target to stdout, in the targets' order. The line is the key's line of hd.toml, as json = "github.com/acme/json@2.3.0".
  9. 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.
  10. cli.dep.add.report.orderThe line of cli.dep.add.lower follows its target's line. With --format json, hd add prints none of these lines, by cli.json.lines.build.
  11. cli.dep.selectAfter it changes the manifest, hd add, hd update, or hd remove selects versions again and fetches each version it needs that the cache lacks.
  12. cli.dep.tidyIt then writes hd.sum with 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, by cli.sum.keep.
  13. cli.dep.updatehd update moves each dependency requirement of the manifest to the newest release tag on its compatibility line. hd update NAME moves only NAME's requirement.
  14. 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.
  15. cli.dep.update.networkhd update lists the tags of each repository it moves, so it uses the network even when the cache holds every version.
  16. cli.dep.update.devhd update moves the requirements of [dev-dependencies] as it moves those of [dependencies], and hd update NAME finds NAME in either table. A NAME that is no key of either table is an error.
  17. cli.dep.update.queryhd update NAME@QUERY sets NAME's requirement to the version that QUERY selects on its host path, by Version Queries. It may raise or lower the requirement, as hd add does by cli.dep.add.lower.
  18. cli.dep.update.query.pathhd update NAME@QUERY is an error when NAME holds a path requirement. Error: invalid-requirement.
  19. cli.dep.removehd remove NAME deletes the key NAME from [dependencies] or [dev-dependencies]. A NAME that is no key of either table is an error.
  20. cli.dep.fetchhd fetch fetches every version that selection needs and the cache lacks. It adds each tree line and manifest line that cli.dep.tidy would write and hd.sum lacks, and changes no other entry.
  21. cli.dep.edit.empty-tableWhen an edit deletes a dependency table's last key, it deletes that [dependencies] or [dev-dependencies] header line too.
  22. cli.dep.unchanged-on-errorWhen a dependency command reports an error, it writes neither hd.toml nor hd.sum.
  23. cli.dep.editA dependency command edits hd.toml line by line, so its comments and its other lines stay as they are.
  24. 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.
  25. cli.dep.no-questionNo dependency command asks a question.
  26. 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.
  27. cli.dep.workspace-fetchIn workspace mode, hd fetch works on the whole workspace. It fetches what the workspace's selection needs, and adds the missing lines to its hd.sum, by cli.dep.fetch.
  28. cli.dep.workspace-member-onlyIn workspace mode, hd add, hd update, and hd remove are errors whose message suggests running them in a member's directory.
  29. cli.dep.dev-selectedSelection reads the package's dev dependencies in every command, by module.select.dev-dependencies. So hd build, hd check, and hd run fetch them and need their hd.sum entries, although only test code and tasks may use them.
  30. 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 run hd remove NAME first, then hd 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@latest does when json has a newer major release. hd update NAME without 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 fetch once, 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 add refuses to choose whether changing tables was intentional; the explicit hd remove first makes that choice visible.

Why. Lowering a minimum is safe under minimal version selection, so hd add does it as Go's go get x@older does. 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 add chose 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
  1. cli.dep.target.orderA target takes the first form of the table above that it matches.
  2. cli.dep.target.splitA target word splits at its first @ that is not the git@ of a repository URL. The text after it is the QUERY.
  3. cli.dep.target.pathA path target is relative to the working directory. hd add writes the path relative to the manifest's directory, with / between its segments.
  4. cli.dep.target.path.no-queryA path target with a QUERY, such as ../billing@1.0.0, is an error. Error: invalid-requirement.
  5. cli.dep.target.selfA path target that names the package's own directory is an error. Error: invalid-requirement.
  6. cli.dep.target.urlA repository URL names a repository, and its target names the package at the repository's root.
  7. cli.dep.target.url.git-suffixOn github.com, a .git at the end of the URL is dropped. On any other host, the last segment gains .git when it lacks one, by module.repo.git-suffix.
  8. cli.dep.target.githubA target whose first segment holds no dot names a github.com host path without its host. So acme/json is github.com/acme/json.
  9. cli.dep.target.github.onlygithub.com is the only host that a target may leave out, and no setting names another. A target on any other host is written in full.
  10. cli.dep.target.no-manifestA fetched target whose directory holds no hd.toml in the selected commit is an error. Its message lists each directory below it that holds an hd.toml, as found 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
  1. cli.dep.query.orderA query takes the first row of the table above that it matches. So latest and 2.1 never name a branch.
  2. cli.dep.query.tagsA query reads only the package's version tags, with its tag prefix, by module.version.tag and module.version.tag-prefix. It ignores every other tag.
  3. cli.dep.query.latestNo query, or latest, selects the newest release: the greatest version without a pre-release suffix.
  4. cli.dep.query.latest.pre-releaseWhen the package has no release, it selects the greatest pre-release.
  5. 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.
  6. cli.dep.query.latest.noteIn that case, hd prints a note to stderr that suggests tagging a version.
  7. cli.dep.query.v-prefixA version or partial version may start with v, as v2.1.0 or v2. hd drops it, so the manifest holds no v, by module.dep.requirement-form.
  8. cli.dep.query.exactA version or pseudo-version selects itself. Its tag or commit must exist, by cli.dep.unknown-version and cli.dep.pseudo.unknown.
  9. cli.dep.query.partialA query X or X.Y selects the greatest release whose major, or major and minor, are those. With no such release, it selects the greatest such pre-release.
  10. 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.
  11. 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.
  12. cli.dep.query.unknownA query that selects no version is an error. Error: unknown-version.
  13. 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.
  14. cli.dep.query.offlineWhen git cannot reach the repository for such a query, the fetch-failed message of cli.dep.no-prompt also 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, and hd follows 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
  1. cli.dep.key.name-flag--name NAME makes NAME the target's key.
  2. cli.dep.key.name-flag.one--name with more than one target is a usage error.
  3. cli.dep.key.inferredWithout --name, the key is the name of the [package] table in the target's hd.toml, with each - replaced by _, by module.dep.key-name.
  4. cli.dep.key.inferred.invalidAn inferred key that source could not write as dep.NAME, as 9tools, is an error whose message suggests --name. Error: invalid-requirement.
  5. cli.dep.key.sameWhen the key already holds a requirement on the target's host path, or a path requirement on its directory, hd add changes that requirement. Its version may rise or fall, also to another compatibility line.
  6. 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.
  7. cli.dep.key.clash.other-tableWhen an inferred key is in the other table, the message of cli.dep.add.other-table also 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: --name picks a free key, and hd remove makes 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
  1. cli.clean.buildhd clean without --cache removes the build directory of the package, and prints removed build. It removes nothing else: not hd.toml, hd.sum, source, or the cache.
  2. cli.clean.build.noneWhen the package has no build directory, hd clean prints nothing to clean and succeeds.
  3. cli.clean.build.workspaceIn workspace mode, hd clean removes the build directory of every member, and prints removed DIR/build for each, with DIR relative to the workspace root. It takes no -p.
  4. cli.clean.build.package-onlyOutside any package, hd clean without --cache is an error whose message suggests hd new and hd clean --cache.
  5. cli.clean.cachehd clean --cache removes the fetched versions from the cache directory of cli.cache.directory, and touches no build directory. It works inside a package, in a workspace, and outside any package, and it reads no manifest.
  6. cli.clean.cache.writableThe entries are read-only, by cli.cache.read-only, so hd clean --cache makes each directory writable before it removes it.
  7. cli.clean.cache.entrieshd clean --cache removes only the entries pkg, hash, obj, and tmp of the cache directory, and the cache directory itself stays. It never follows a symbolic link out of the cache directory.
  8. cli.clean.cache.foreignhd clean --cache removes 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 than pkg, hash, obj, and tmp, since that is no hd cache. A relative HD_CACHE resolves against the working directory.
  9. cli.clean.cache.compiledhd clean --cache also removes every entry of the compiled cache, whatever its size.
  10. cli.clean.cache.outputhd clean --cache prints removed HOST_PATH@VERSION for each version it removes, then removed N versions from DIR. A missing or empty cache directory prints the cache DIR is empty and succeeds.
  11. cli.clean.cache.refetchhd clean --cache changes no hd.toml or hd.sum. The next command that needs a removed version fetches it again, by cli.dep.implicit-fetch.
  12. cli.clean.exithd clean exits with status 0 when it succeeds, also when it removes nothing, and with status 101 when it reports an error, by cli.exit.hd-failure.
  13. cli.clean.no-questionhd clean takes 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_CACHE from 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
  1. cli.cache.objhd stores its compiled entries in the directory obj of the cache directory. Every package and worktree of the user shares them.
  2. cli.cache.obj.derivedA compiled entry holds only derived data. Removing one changes no command's result, only how long the command takes.
  3. cli.cache.obj.capThe compiled entries have a size cap of 10 GiB. The environment variable HD_CACHE_MAX_SIZE sets another cap, as a SIZE of Program Limits.
  4. 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.
  5. cli.cache.obj.evict.scopeEviction removes only compiled entries, never a fetched version.
  6. cli.cache.gchd cache gc runs that eviction now, until the compiled entries are within the cap. It prints their size before and after.
  7. cli.cache.gc.anywherehd cache gc works 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.

See also: Cache, Cleaning.

#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
  1. cli.manifest.unknown-key.errorEvery command rejects a table or key of hd.toml that the table above does not list, at its line. Error: unknown-manifest-key.
  2. cli.manifest.known-keyA listed key whose value breaks the rule that gives it a meaning stays an error.
  3. cli.manifest.invalidAn error in the content of hd.toml for which no other rule names a code is an error at its line in hd.toml. Error: invalid-manifest.
  4. cli.manifest.invalid.casesThese errors include a bad key or value of Capability Grants, an [[executable]] table without a string name and module, and a [source] root that is no source root. They also include a package that its workspace manifest neither lists nor excludes.
  5. cli.manifest.invalid.unlistedFor the unlisted package, the diagnostic's file is the workspace manifest, and its line is that of the manifest's members key, or of its [workspace] header when it has no members.
  6. cli.manifest.invalid.syntaxA TOML syntax error in hd.toml is also an invalid-manifest error, at the line and column where the syntax error starts.
  7. cli.manifest.toolchain-keys[package] hd is the minimum toolchain version of module.toolchain.minimum, and [toolchain] pin is the exact version of module.toolchain.pin. Each is a version string.
  8. cli.tooling.package-schemaThe complete hd.toml schema belongs to package tooling. Dependencies defines the hd.sum format and the commands that fetch, add, and upgrade dependencies.
  9. 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"
  1. cli.host.declaration.map[host] must be a table mapping trait paths to nonempty provider-file strings. Error: invalid-manifest.
  2. cli.host.declaration.map-keyEach key must be a package-relative trait path of canonical identifier names separated by dots. Error: invalid-manifest.
  3. cli.host.declaration.map-uniqueEach trait path must occur at most once in the table. Error: invalid-manifest.
  4. cli.host.declaration.mappedEach key must name a trait declared with @host in its owning package. Error: invalid-manifest.
  5. cli.host.declaration.marker-mappedEach trait marked @host must have an entry in its owning package's [host] table. Error: invalid-manifest.
  6. cli.host.declaration.map-locationsA source/map mismatch diagnostic identifies the trait and its source and manifest locations.
  7. cli.host.declaration.mapping-no-consentA dependency's host mapping does not grant consent to load its provider.
  8. cli.host.declaration.map-recheckChanging the host map must recheck source/map agreement even when the source has not changed.
  9. 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
  1. cli.host.package.entryA package-declared host trait named by main belongs to that program's selected runtime profile.

  2. cli.host.package.javascriptPackage host providers run only on JavaScript hosts.

  3. cli.host.package.native-refusalWasmtime refuses a required package host trait before guest initialization, naming the trait and its need for a JavaScript host.

  4. cli.host.package.no-fallbackA host unable to supply a package capability must not substitute another engine or provider.

  5. cli.host.package.manifest-consentLoading a required package provider needs an explicit true entry for its source trait path in the app's [capabilities] table.

  6. cli.host.package.no-flag-consentA --cap flag cannot replace missing manifest consent for a package provider.

  7. cli.host.package.invokerOnly the invoker or its trusted adapter loads provider code.

  8. cli.host.package.consent-preflightEvery required provider and grant must be checked before any selected provider module loads.

  9. cli.host.package.contractProvider identity and required method schemas, including suspension and returned handles, must match before guest initialization.

  10. cli.host.package.missingA missing provider causes a startup refusal naming the package, version and trait.

  11. cli.host.package.duplicateRegistering multiple providers for one nominal capability is an error.

  12. cli.host.package.generatedJS host glue is generated from checked host trait signatures, including value conversion and suspension plumbing.

  13. cli.host.package.companionA build requiring package host bindings writes generated NAME.host.mjs beside NAME.wasm.

  14. cli.host.package.no-companionA build requiring no package host binding emits no companion glue file.

  15. cli.host.package.completion-formsA suspending provider method may return a ready value, a Promise, or a pollable operation.

  16. cli.host.package.abortAborting a pending method signals cancellation and discards its undelivered result, including any later completion.

  17. cli.host.package.handle-releaseReleasing the last HD reference to a returned handle drops its host binding without implicitly calling resource methods.

  18. cli.host.package.plain-providerA provider module default-exports a plain JavaScript object with an own function property for each required method.

  19. cli.host.package.generated-wrapperGenerated glue wraps plain provider objects with the checked nominal identity and method schemas.

  20. cli.host.package.provider-containmentLoading refuses a missing provider file or a file whose canonical path leaves its declaring package.

  21. cli.host.package.prebuilt-manifestA prebuilt Wasm run requiring package capabilities discovers its app manifest above the Wasm file.

  22. cli.host.package.prebuilt-no-manifestA prebuilt Wasm run without that manifest refuses required package capabilities before loading any provider.

  23. cli.host.package.value-conversionGenerated bindings apply the JavaScript value conversions of Package Host Traits.

  24. cli.host.package.context-pendingThe method context's pending(poll, done, abort, finish) factory creates a pollable operation.

  25. cli.host.package.poll-nonblockingA pollable operation's readiness probe must return a boolean without blocking.

  26. cli.host.package.poll-resultIts result callback runs once when readiness first succeeds.

  27. cli.host.package.poll-deliveryIts optional delivery hook runs only when the guest takes a valid finish.

  28. cli.host.package.poll-abortAborting an undelivered pollable operation invokes its cancellation hook at most once.

  29. cli.host.package.poll-abort-signalCancellation signals the pending call before invoking its cancellation hook.

  30. cli.host.package.poll-closeClosing the instance cancels its outstanding pollable operations.

  31. cli.host.package.synchronous-completionA non-suspending provider method cannot return a Promise or a pollable operation. Boundary failure: host-contract.

  32. cli.host.package.poll-callback-failureA pollable operation's callback failure is a host-contract failure. Boundary failure: host-contract.

  33. cli.host.package.context-httpA JavaScript package-provider context exposes a frozen http view of its core runtime's resolved Http policy.

  34. cli.host.package.context-http-coverageIts coversHost(host, port) method tests only the Http capability's written host and port.

  35. cli.host.package.context-http-helpersIts isAsciiUrl(text) and writtenHost(text) methods use the core runtime's URL-validation behavior.

  36. cli.host.package.context-http-readonlyA provider cannot replace the context's http view.

  37. cli.host.package.context-http-missingAccessing context.http without runtime HTTP policy services fails. Boundary failure: host-contract.

  38. cli.host.package.context-consoleA JavaScript package-provider context exposes a frozen console view of its core runtime's resolved Console authority.

  39. cli.host.package.context-console-allowsIts allows() method reports whether Console access is granted.

  40. cli.host.package.context-console-readonlyA provider cannot replace the context's console view.

  41. cli.host.package.context-console-missingAccessing context.console without runtime Console policy services fails. Boundary failure: host-contract.

  42. cli.host.package.context-interface-identityA JavaScript package-provider method context exposes a frozen interfaceIdentity with its checked nominal package, version and trait_path strings.

  43. cli.host.package.context-interface-identity-checkedThis identity comes from the method's guest interface after provider identity and schema validation.

  44. cli.host.package.context-interface-identity-handleA handle method's interfaceIdentity names its own declared handle interface.

  45. cli.host.package.context-interface-identity-readonlyA provider cannot replace or delete the context's interfaceIdentity.

  46. 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.

  1. cli.host.driver.createThe JS core host provides createDriver(instance, init="hd.init", poll="hd.poll"), returning a synchronous step driver.
  2. cli.host.driver.exportsCreation requires the named initialization and poll exports and hd.wake to be functions. Boundary failure: host-contract.
  3. cli.host.driver.modeA host selects either asynchronous run driving or one synchronous driver. Boundary failure: host-contract.
  4. cli.host.driver.initBefore its first poll, a driver invokes initialization once.
  5. cli.host.driver.pollEach successful nonterminal step calls the poll export once with context zero.
  6. cli.host.driver.wakeAfter a pending poll, a successful step publishes currently ready host completions through one hd.wake call when their count is nonzero.
  7. cli.host.driver.resultA step returns {status, wakes}, with the poll's raw signed status and the published completion count.
  8. cli.host.driver.no-backgroundThe driver itself creates no Promise or timer.
  9. cli.host.driver.terminalAfter a successful terminal step, further steps return its status with zero wakes until the host closes.
  10. cli.host.driver.failureAn exception during initialization, polling, completion, wake delivery or output flushing permanently fails that driver.
  11. cli.host.driver.reentrantCalling step() during a step fails. Boundary failure: host-contract.
  12. cli.host.driver.failed-stepA step on a failed driver fails. Boundary failure: host-contract.
  13. cli.host.driver.closedCreating or stepping a synchronous driver after closing its host fails. Boundary failure: host-contract.
  14. cli.host.driver.sync-readyA synchronous host wait in this mode must have a ready completion. Boundary failure: host-contract.
  15. 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.