hd-lang

#The hd Command

This page is the task-oriented guide to the hd command. The normative reference is Command Line; this page shows each job the way you would do it, with real output.

#Start A Project

You want a new package. hd new needs to know which kind: --app for an application, --lib for a library:

$ hd new orders --app
created application package `orders` in /home/you/orders

The layout is small: an hd.toml manifest, src/main.hd for the entry point, and tests/ for integration tests:

orders/hd.toml
orders/src/main.hd
orders/tests/orders.hd

The manifest names the package; everything else is convention:

[package]
name = "orders"

Read next: Package Mode.

#Run A Program

You want to see it run. Inside a package, hd run runs its executable; hd FILE runs one file anywhere:

$ hd run
hello, world

With several executables or a task, name it: hd run NAME runs the executable or the tasks/NAME.hd script. hd run --release runs the release build. In a workspace, -p NAME selects the member, from the root or inside another member:

$ hd run seed    # runs tasks/seed.hd
seeded

Read next: Running A Package and Tasks.

An implementation may offer more than one engine to run programs on. hd FILE, hd run, hd build and hd test take --engine NAME, and HD_ENGINE gives the name when the flag is absent. Which engines exist, and which is the default, depends on your hd. A program behaves the same on each; only speed and memory use differ. One difference is security: the wasmtime engine opens files relative to each granted directory, so a grant holds even if another process swaps a link inside it while the program runs. The node engine checks a path and then opens it by name, so a local process that swaps a link between the two can reach outside the grant. Run untrusted programs on wasmtime. A name your hd does not offer is a usage error that exits 101 and runs nothing:

$ hd run --engine no-such-name
$ echo $?
101

Read next: Engines.

#Parse Your Program's Arguments

std.cli.ArgParser declares your program's options and provides -h and --help. Return error.exit() from main to use its command-line policy: help goes to standard output with status 0, and usage errors go to standard error with status 2.

Save this as volume.hd:

use std.cli.{Arg, ArgParser}
use std.host.Args
use std.process.ExitCode

pub fn main() -> ExitCode $ Args + Console:
    parser := ArgParser::new("volume").arg(Arg { name: "level", help: "how loud" })
    parsed := match parser.parse_args():
        .Ok(parsed) => parsed
        .Err(error) => return error.exit()
    println(parsed.value("level").unwrap_or("quiet"))
    ExitCode(0)
Open in playground

Pass its arguments after --:

$ hd volume.hd -- --level 3
3
$ hd volume.hd -- --levl 3
unknown option --levl
$ echo $?
2
$ hd volume.hd -- --help >help.txt
$ echo $?
0

help.txt contains the generated usage. The exit policy belongs to CliError, and main selects it by returning error.exit(). Propagating a parse error with ? instead uses the ordinary Result entry behavior.

Read next: Argument Parser and Exit Status.

#Check And Build

You want to know it compiles, without running it. hd check type-checks the package, and hd build writes the Wasm:

$ hd check
check result: ok. errors: 0; warnings: 0
$ hd build
$ hd build --release

The outputs land in build/debug/ and build/release/:

build/debug/orders.wasm
build/release/orders.wasm

Read next: Building And Checking.

#Test

You want the tests. hd test runs every test of the package; --filter runs only the cases whose name contains a word:

$ hd test
test result: ok. 1 passed; 0 failed; 0 ignored
$ hd test --filter "prints"
test result: ok. 1 passed; 0 failed; 0 ignored

A unit test runs on fakes alone; an integration test under tests/ gets the real system and can run the built program with hd_run!. A doc test is a fenced hd block in a ## doc comment. A snapshot test compares with a recorded file, and hd test --update records it. For agents, hd test --format json prints one JSON line per case plus a summary:

$ hd test --format json
{"kind":"test","name":"prints a greeting","outcome":"passed","message":""}
{"kind":"summary","errors":0,"warnings":0,"passed":1,"failed":0,"ignored":0,"status":0}

You edit one module and want feedback in seconds, not the whole suite. After a full pass, hd records each test program's fingerprint: its sources, dependencies, manifest settings, and hd version. hd test --affected then runs only the programs whose fingerprint changed, and the summary counts the rest:

$ hd test --affected
test result: ok. 0 passed; 0 failed; 0 ignored; 1 program skipped

A filtered run or a failing program records nothing, so --affected never skips a test that failed or did not run. hd clean removes the records.

A runaway loop should fail, not hang. --time-limit gives each test case, and hd run and hd FILE give the program, a deadline; past it the program panics with time-limit:

$ hd test --time-limit 5s
$ hd run --time-limit 30s
panic: time-limit: the program exceeded its time limit

A Ctrl-C asks a waiting program to stop and lets its cleanup finish: using disposes and defer suites run, for up to ten seconds, then whatever is left is abandoned. A second Ctrl-C abandons it at once. --cleanup-limit changes the wait, and the run exits with status 130:

$ hd run --cleanup-limit 2s
^C
cancelled

Read next: Test Runs, Affected Tests, Program Limits, and Cancelling A Run.

#Dependencies And Workspaces

You want another package's code. A requirement is a minimum version of a host path, or a local path while you develop:

[dependencies]
json = "github.com/acme/json@2.1.0"
billing = { path = "../billing" }

hd add, hd update, hd remove, and hd fetch manage the table and hd.sum, the recorded hashes beside the manifest. Several packages of one repository share one selection through a workspace manifest:

[workspace]
members = ["libs/ui", "libs/shared"]

Read next: Dependency Commands and Workspaces.

#Debug And Explore

You want to see a value while it runs, or read what a function promises. dbg(x) prints the value with its source location on standard error. hd doc NAME prints one item's documentation, and no file is written:

$ hd doc std.text.split
```hd
pub fn split(self, separator: string) -> List[string]
```

Splits the string around each occurrence of `separator`, in order.

Reference: std.text.string.split in the standard library reference

NAME is a module path and an item, as in text.slugify for your own package, or std.text.split for the standard library, where the output is the signature, a summary, and a reference link. A module name alone, as in hd doc std.text, prints the module's page. hd help COMMAND prints one command's usage and flags.

hd with no arguments on a terminal opens the interactive session; each line is evaluated and printed with its type. Text piped into hd runs as a program instead, so a session cannot be scripted through standard input. A later input may use the top-level bindings and declarations of earlier ones, but may not redeclare a name an earlier input declared (duplicate-binding, duplicate-type); to change a declaration, start a new session or give the new one another name. An input that panics adds nothing to the session, and the session goes on. An input that starts with : is a command: :help lists them, :quit (or :q, :exit) ends the session, and :reload reads the package's files again after you edit them, keeping the inputs that still check.

$ hd
1 + 2
3 : usize

Every diagnostic command takes --format json for one JSON object per diagnostic. Read next: Machine Output.

#Common Errors And Their Fixes

  • missing-requirement: the call needs a capability, such as Console, that the enclosing signature does not grant. Add it to the row, as in pub fn main() -> void $ Console, or bind a provider in a test.
  • invalid-result-propagation: ? needs the function's own result type to be a compatible Result; the note names the type to write.
  • non-reassignable-binding: := bindings are fixed; write let for a name you reassign.
  • type-mismatch on usize versus i32: literals default to usize; write +N for a signed one.
  • mixed-signedness: an unsigned usize, which len() and literals give, does not mix with a signed i32. Declare the configurable count usize, or cast one side with usize(n) or i32(n).
  • bang-call-outside-suspension: the call suspends, so its caller must be a suspending function: fn!, or main!.

Each message names its code, and the rule IDs it cites are in the specification.