A small, unsafe language for writing unsafe things safely.
Hemlock is a systems scripting language that combines the power of C with the ergonomics of modern scripting languages. It embraces manual memory management and explicit control while providing structured async concurrency built-in.
The checked-in documentation starts at docs/README.md. Standard library module references live in stdlib/docs/, and the release history is in CHANGELOG.md.
- Explicit over implicit - No hidden behavior, no magic
- Manual memory management - You allocate, you free
- Dynamic by default, typed by choice - Optional type annotations with runtime checking
- Unsafe is a feature - Full control when you need it, safety tools when you want them
print("Hello, World!");
// Raw pointer (dangerous but flexible)
let p: ptr = alloc(64);
memset(p, 0, 64);
free(p);
// Safe buffer (bounds checked)
let buf: buffer = buffer(64);
buf[0] = 65;
free(buf);
async fn compute(n: i32): i32 {
let sum = 0;
for (let i = 0; i < n; i = i + 1) {
sum = sum + i;
}
return sum;
}
// Spawn tasks on separate OS threads (real pthreads!)
let t1 = spawn(compute, 1000);
let t2 = spawn(compute, 2000);
// Wait for results (both computing in parallel)
let r1 = join(t1);
let r2 = join(t2);
import "libc.so.6";
extern fn strlen(s: string): i32;
extern fn getpid(): i32;
let len = strlen("Hello!");
let pid = getpid();
| Category | Highlights |
|---|---|
| Types | i8-i64, u8-u64, f32/f64, bool, string, rune, ptr, buffer, array, object |
| Memory | alloc, free, memset, memcpy, realloc, talloc, sizeof |
| Strings | UTF-8, mutable, 19 methods (substr, split, trim, replace, etc.) |
| Arrays | Dynamic, 23 methods (push, pop, map, filter, reduce, sort, fill, etc.) |
| Concurrency | async/await, real OS threads (pthreads), channels |
| FFI | Call C functions from shared libraries, export extern |
| Error Handling | try/catch/finally/throw, panic() |
| I/O | File API, signal handling, command execution |
| Stdlib | 53 modules (math, net, crypto, signal, atomic, ffi, vector, and more) |
| Packages | hpm package manager with GitHub registry |
macOS:
brew install libffi openssl@3 libwebsocketsUbuntu/Debian:
sudo apt-get install libffi-dev libssl-dev libwebsockets-devWindows: build natively under MSYS2 or cross-compile from Linux with
make mingw — see docs/advanced/windows.md.
make # Build hemlock
make test # Run all testsQuick Install (recommended):
curl -fsSL https://raw.githubusercontent.com/hemlang/hemlock/main/install.sh | bashFrom source:
sudo make install # Install to /usr/local
make install PREFIX=~/.local # Install to custom prefix
sudo make uninstall # Remove installationHemlock can run in a web browser by compiling the interpreter to WebAssembly via Emscripten.
# Install Emscripten (one-time setup)
git clone https://github.com/emscripten-core/emsdk.git
cd emsdk && ./emsdk install latest && ./emsdk activate latest && source ./emsdk_env.sh
cd ..
# Build the WASM interpreter
make wasm-interpreter
# Serve the browser example (builds WASM if needed)
make wasm-browser-example
# Open http://localhost:8080/examples/wasm-browser/index.htmlOr use it from JavaScript:
var Module = {
print: function(text) { console.log(text); },
onRuntimeInitialized: function() {
var hemlockEval = Module.cwrap('hemlock_eval', 'number', ['string']);
hemlockEval('print("Hello from Hemlock WASM!");');
},
noInitialRun: true
};See Installation - WASM Build for full details and examples/wasm-browser/ for a complete browser integration example.
./hemlock program.hml # Run a program
./hemlock program.hml arg1 arg2 # With arguments
./hemlock # Start REPLHemlock provides tools to bundle multi-file projects and create self-contained executables.
Resolve all imports and create a single distributable file:
./hemlock --bundle app.hml # Create app.hmlc
./hemlock --bundle app.hml --compress # Create app.hmlb (smaller)
./hemlock --bundle app.hml -o dist/app.hmlc # Custom output pathCreate a standalone executable that includes the interpreter:
./hemlock --package app.hml # Create ./app executable
./hemlock --package app.hml -o myapp # Custom name
./hemlock --package app.hml --no-compress # Faster startup, larger fileThe packaged executable runs anywhere without needing Hemlock installed.
See Bundling & Packaging for details.
Hemlock v2.4.1 is the current checked-in release. Highlights include:
- v2.4.1 socket FD_CLOEXEC + binary file read + exec_argv stdin - TCP listener and accepted-client socket fds now set
FD_CLOEXECsoposix_spawn'd children don't inherit (and pin) the parent's listener after a crash. Newfile.read_binary()returns a buffer that preserves 0x00 bytes when reading/proc/<pid>/cmdlineor other binary content.exec_argv()gained astdinoption that pipes a string into the child's stdin without ash -c "cmd < file"wrapper. - v2.4.0 HTTP/auth + ergonomics batch -
@stdlib/httpPOST/PUT/DELETE/PATCH now actually send custom headers (Authorization,X-Request-Id, etc. were silently dropped before — onlyContent-Typemade it past the wrapper). Interpreter named-module imports are live bindings instead of import-time snapshots, so reassigning an exportedletpropagates to importers and to spawned tasks. Flow null-narrowing follows?.field access. Type error labels report the parameter name instead of'positional'.string.lower()/upper()aliases.get_binaryfollows 3xx redirects. C codegen no longer prefixes call symbols with surrounding-scope tokens. macOS libwebsockets picks up Homebrew CA bundles automatically. Default LWS HTTP timeout dropped from ~30s to 5s. - v2.3.1 binary HTTP fixes -
@stdlib/http.download(url, path)now actually writes the buffer body (was silently writing zero-byte files in compiled binaries); newdownload_streaming(url, path)for bounded-memory pulls of large artifacts; newstream.read_binary()preserves 0x00 bytes thatread()would have truncated at. - v2.3.0 streaming HTTP -
@stdlib/httpnow exportsstream(),stream_get(),stream_post(),post_json_stream(), andstream_sse()for chunked HTTP and Server-Sent Events. Built on the already-bundled libwebsockets, so no new dependency. The compiler runtime now sends POST/PUT/PATCH bodies for streaming requests and throws the same catchable exceptions as the interpreter on invalid arguments — full interpreter/compiler parity. - v2.2.3 hardening release - Socket connect/bind failures are catchable in compiled binaries,
/procand/syspseudo-files read correctly, buffer memory builtins validate ranges, optional-chain null guards narrow in the compiler, CLI help parsing is stricter, and@stdlib/fsnow includes recursivemake_dirs(). - v2.2.2 hardening release - Runtime
file_stat()failures are catchable, typed-array fast-path assignments have stricter checks, non-default install prefixes find their stdlib/runtime layout, and documentation audit tooling is checked in. - v2.2.1 HTTP body fix - Compiled binaries now send
Content-Type,Content-Length, and request body bytes for POST/PUT/PATCH/DELETE, matching interpreter behavior. - v2.2.0 process and filesystem additions -
@stdlib/processexportsposix_spawn(argv, opts?);@stdlib/fsexports raw file-descriptor helpersopen_fd()andfileno(). - Safer module/codegen behavior - Fixes include two-library FFI import collision handling, module
initsymbol-collision prevention, and stricter compiler/interpreter parity checks. - Reduced builtin conflicts since v2.0.0 - 63 former globals moved to
@stdlibmodules (math, signal, net, process, fs, atomic, debug, ffi) to reduce global namespace pollution. - Full type system with 64-bit integers and Unicode support.
- Pattern matching with destructuring, guards, and rest syntax.
- Expression-bodied functions, type aliases, named arguments, null coalescing, string-literal object keys, and safe dynamic indexing (
obj?[key]). - Manual memory management with safe (
buffer) and unsafe (ptr) options. - Async/await with pthread-backed task execution and channels.
- 53 stdlib modules with one API document per module.
- FFI for C interop with
export extern fnfor reusable library wrappers. - Compiler backend (C code generation), formatter, bundler/packager, and LSP tooling.
- hpm package manager with GitHub-based registry.
- A 1,400+ file Hemlock test corpus, including interpreter/compiler parity tests.
"We give you the tools to be safe (
buffer, type annotations, bounds checking) but we don't force you to use them (ptr, manual memory, unsafe operations)."
Hemlock is NOT memory-safe. Dangling pointers, use-after-free, and buffer overflows are your responsibility. We provide the tools to help, but we don't force you to use them.
MIT License
Hemlock is experimental and evolving. If you're interested in contributing, please read CLAUDE.md first to understand the design philosophy.
