Your first run
Put something on screen. Create a file hello.hl: Run it: hale run compiles your program and runs it in one step — it's the same native code hale build…
Put something on screen.
Create a file hello.hl:
fn main() { println("Hello from Hale.");}Run it:
hale run hello.hlHello 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:
hale build hello.hl./helloSame 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.
What’s here
Section titled “What’s here”-
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 runand imports.import "..." as ...;directives are resolved byhale runjust ashale buildresolves them, for a single file and for a directory (hale run ./dir, which takes the directory’s files as one program).
Build modes, diagnostics, and debugging
Section titled “Build modes, diagnostics, and debugging”A few switches worth knowing from day one:
- The rest of the loop:
hale fmtkeeps your code canonical (zero config),hale testruns*_test.hl,hale verifyischeckwith teeth (any advisory fails — what CI runs), andhale docrenders API references from///comments. Agent hosts without a shell get all of it viahale mcp. - Faster iteration:
hale build --dev(orHALE_DEV=1) uses a lighter optimization pipeline — noticeably quicker builds while you’re in an edit-run loop. Release builds default to-O3tuned for your CPU. - Where did the build time go?
HALE_TIME=1 hale build app.hlprints per-phase wall times. - What did the lifecycle do?
HALE_LIFECYCLE_TRACE=1 hale run app.hlbuilds with the lifecycle trace: onelcline on stderr per step (a birth, arun(), 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 lspis 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 ashale check <dir>does — every file of the open file’s directory, everyimportfollowed, 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/@budgetstatus, a topic’s routing key), completion coversself.members, thestd::surface, and your seed’s symbols, go-to-definition and references work across the seed — go-to-definition on astd::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 requestshale/busGraph,hale/placement, andhale/allocSummaryreturn the pub/sub topology, the thread/pool map of themain locusthe 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 --jsonemits 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 xwith typed values (Strings print their text), and ASAN reports that point at the exact source line. Zero runtime cost; opt out withLOTUS_NO_DEBUGINFO=1.