Skip to content

Your first run

Put something on screen.

Create a file hello.hl:

fn main() {
println("Hello from Hale.");
}

Run it:

Terminal window
hale run hello.hl
Hello from Hale.

hale run compiles your program and runs it in one step — it’s the same native code hale build produces, just executed immediately and not left on disk. When you want the artifact to keep and ship, build it:

Terminal window
hale build hello.hl
./hello

Same compiler, same output: run is the fast inner-loop shape, build is for the binary you deploy. There’s no separate interpreter, so anything that runs under build runs identically under run.

  • fn main() is the entry point, the same as it is in C, Go, or Rust. A Hale program starts by calling it.

  • println(...) prints its arguments followed by a newline. It takes any number of arguments and concatenates them — there’s no format string:

    fn main() {
    let name = "Hale";
    println("Hello from ", name, ".");
    }
  • Statements end with ;. Newlines are just whitespace — they don’t end statements. Source is ASCII outside of string literals and comments.

Comments are C-style:

// a line comment
/* a block comment */

That’s the whole surface you need to start. The next chapter introduces variables and the value types — the vocabulary every Hale program is built from.

hale run and imports. import "..." as ...; directives are resolved by hale run just as hale build resolves them, for a single file and for a directory (hale run ./dir, which takes the directory’s files as one program).

A few switches worth knowing from day one:

  • The rest of the loop: hale fmt keeps your code canonical (zero config), hale test runs *_test.hl, hale verify is check with teeth (any advisory fails — what CI runs), and hale doc renders API references from /// comments. Agent hosts without a shell get all of it via hale mcp.
  • Faster iteration: hale build --dev (or HALE_DEV=1) uses a lighter optimization pipeline — noticeably quicker builds while you’re in an edit-run loop. Release builds default to -O3 tuned for your CPU.
  • Where did the build time go? HALE_TIME=1 hale build app.hl prints per-phase wall times.
  • What did the lifecycle do? HALE_LIFECYCLE_TRACE=1 hale run app.hl builds with the lifecycle trace: one lc line on stderr per step (a birth, a run(), a drain, a dissolve, a reclaim, the pool join), naming the instance, its restart incarnation and the thread it ran on. Concurrent events for an instance wait for its trace identity to be initialized; this synchronization and writing the trace can affect scheduling. A debug aid: the format is not a contract, and a build without the switch carries none of it.
  • Editor & agent integration: hale lsp is a stdio Language Server — point any LSP-speaking editor (or agent harness) at it and you get live diagnostics: type errors as errors, the advisory analyses (unbounded-alloc survey, hot-path lint, placement warnings) as warnings, refreshed as you edit. The server loads the seed exactly as hale check <dir> does — every file of the open file’s directory, every import followed, your unsaved buffers in place of the files on disk — so the two never disagree; a seed file that cannot be read is an error on the file you have open, not a clean result. On a program that declares laws the diagnostics arrive in two steps: the type check’s findings first, then the laws’ verdicts added to the files they concern. The first step is incremental: when an edit changes only what bodies do, the server type-checks again just the declarations you edited and the ones that read them, and keeps the rest from the last check. An edit to a signature, a field, or which declarations exist checks the whole seed. Either way the findings are the ones a full check gives. Diagnostics use your import aliases when naming library declarations, with a stable spelling across checks. Repeated imports of the same library share their rename entries, and each check reuses its name index across diagnostic publications. Hover shows signatures with their contracts (fallibility, @hot/@budget status, a topic’s routing key), completion covers self. members, the std:: surface, and your seed’s symbols, go-to-definition and references work across the seed — go-to-definition on a std:: path jumps into the stdlib’s own source (materialized read-only from the compiler’s embedded copy, so it works even when nothing is installed on disk) — and the custom requests hale/busGraph, hale/placement, and hale/allocSummary return the pub/sub topology, the thread/pool map of the main locus the build deploys (your own, never an imported library’s), and the allocation survey’s leak sites. If the compiler itself panics on a half-typed file the server stays up: that file gets one error, “the compiler hit an internal error on this file: …; please report it with the file” (a request gets a JSON-RPC internal error), and the next edit is checked as usual. No configuration. Prefer plain JSON? hale check app.hl --json emits one object per diagnostic (file, line, col, severity, kind, message) on stdout — a save-hook is all a minimal integration needs.
  • Real debugging: binaries carry full DWARF by default — gdb ./app, break app.hl:42, backtraces with real file:line, info locals / print x with typed values (Strings print their text), and ASAN reports that point at the exact source line. Zero runtime cost; opt out with LOTUS_NO_DEBUGINFO=1.