I/O is a declared effect
Nine effects — IO, Fs, Clock, Args, Exit, Net, Rand, Proc, Conc — are declared on the function that needs them and verified through the whole call graph. There is no escape through an indirect call.
Effects are declared. Every operation is checked. Nothing hides.
Halis is a secure, self-hosting systems language that compiles ahead of time to native code. A function with no uses clause is guaranteed pure by the compiler — not by convention, not by review, by construction.
Only a shell and curl. No compiler toolchain, no Python.
fn read_config(path: str) -> str uses Fs {
return read_file(path)
}
fn elapsed_ms() -> int uses Clock {
return clock_ms()
}
fn pure_compute(n: int) -> int {
# No uses clause -> pure by default
let mut s: int = 0
let mut i: int = 0
while i < n {
s = s + i
i = i + 1
}
return s
}
fn main() -> int uses IO {
let n: int = pure_compute(10)
println("sum(0..10) = " + n.to_str())
return 0
}Each guarantee below is pinned in a machine-checked ledger. The published bug-bounty scope is that same ledger.
Nine effects — IO, Fs, Clock, Args, Exit, Net, Rand, Proc, Conc — are declared on the function that needs them and verified through the whole call graph. There is no escape through an indirect call.
Integer overflow, division by zero and out-of-bounds access halt safely. There is no switch that disables the checks; the only way a check disappears is a formal proof that it is dead.
No null, no uninitialised state, no globals, no hidden anything. Everything is explicit so that it can be audited line by line.
Use-after-move is a compile error; clone() deep-copies; the runtime frees exactly at scope exit. The memory-stress suite runs hundreds of thousands of allocation rounds with a flat RSS, under a 256 MB address-space limit.
Values only cross task boundaries through channels; sharing a variable with a task outside a channel is a compile error. The language has no locks — that is the actor model.
tainted[T] values cannot reach a sink — print, files, exit, DNS, subprocess — until they pass through std.sanitize or an explicit untaint.
hlc --audit prints the capability and taint-flow report of every function: declared versus computed, with the violation named.
A function names the capabilities it needs. Anything it does not name is impossible for it to do. The compiler verifies this through the whole call graph — there is no escape through an indirect call.
| Effect | Group | Builtins | Taint sink |
|---|---|---|---|
| IO | Input / output | print, println, read_line
+6 | print, println, eprint |
| Fs | Input / output | read_file, write_file, file_exists, read_file_tainted
+8 | read_file, write_file, file_exists |
| Clock | Time & randomness | clock_ms, instant_now_ns, thread_sleep_ms
+4 | — |
| Args | Input / output | args, tainted_args
+2 | — |
| Exit | Termination | exit
+1 | exit |
| Net | Process & network | net_lookup, net_tcp_connect, net_tls_get
+11 | net_lookup, net_tcp_connect |
| Rand | Time & randomness | rand_int, rand_float, rand_seed
+3 | — |
| Proc | Process & network | proc_exec, env_get, cwd_get, sys_hostname
+16 | proc_exec |
| Conc | Concurrency | chan_new, spawn, select, task.join, thread_yield
+31 | — |
The effect and builtin lists are generated from the compiler source (src/hlc/parser.hls and src/hlc/checker.hls) — not hand-written. Quick reference →
No uses clause means pure — not by convention, but because the compiler proves it. Remove uses IO from the last function and the program does not compile. That is not a lint warning; it is a proof.
struct Point {
x: int,
y: int
}
impl Point {
fn dist2(self: Point) -> int {
return self.x * self.x + self.y * self.y
}
fn translate(mut self: Point, dx: int) -> void {
self.x = self.x + dx
}
}
fn main() -> int uses IO {
let p: Point = Point { x: 3, y: 4 }
p.translate(1)
println("dist2 = " + p.dist2().to_str())
return 0
}fn read_config(path: str) -> str uses Fs {
return read_file(path)
}
fn elapsed_ms() -> int uses Clock {
return clock_ms()
}
fn pure_compute(n: int) -> int {
# No `uses` clause -> pure by construction.
let mut s: int = 0
let mut i: int = 0
while i < n {
s = s + i
i = i + 1
}
return s
}
fn main() -> int uses IO {
let n: int = pure_compute(10)
println("sum(0..10) = " + n.to_str())
return 0
}import "std.sanitize"
fn main() -> int uses Args, IO {
# Every argv arrives tainted by default.
let argv: list[tainted[str]] = tainted_args()
if argv.len() > 1 {
# println is a sink: tainted data cannot reach it.
println("hello, " + sanitize_filename(argv.get(1)))
}
return 0
}fn fib(n: int) -> int {
if n < 2 {
return n
}
return fib(n - 1) + fib(n - 2)
}
fn worker(job: int, req: Chan[int], rep: Chan[int]) -> int uses Conc {
let answer: int = fib(job)
rep.send(answer)
return req.recv()
}
fn main() -> int uses IO, Conc {
let t: Task[int] = spawn(fib, 25)
let r: int = t.join()
println("fib(25) = " + r.to_str())
let req: Chan[int] = chan_new()
let rep: Chan[int] = chan_new()
let w: Task[int] = spawn(worker, 20, clone(req), clone(rep))
req.send(0)
let answer: int = rep.recv()
return 0
}The compiler is written in Halis, so it must compile itself — and land on exactly the same result.
Every build must re-derive the committed snapshot byte for byte. A stale or hand-edited snapshot fails the build loudly.
`make snapshot-chain` runs the bootstrap twice, byte-compares every hop and prints the chain digest. `CC2=clang` proves a different compiler lands on the same bytes.
Nothing on any build, release or snapshot path reads Python. The interpreter is lab tooling for the differential suites only.
Not "a compiler that supports Halis" — Halis compiles Halis. Every new language feature is used inside the compiler itself before it ships.
Every tool is written in Halis and compiles itself. (30 công cụ native)
Pure HLS modules, grouped by the three application families Halis set out to serve. (64 std + 18 core)
The same source tree: compiled to C and then to native machine code, emitted as LLVM IR, packaged as WebAssembly, or emitted as a bare-metal image. Halis does not build an operating system; it builds the language in which others do.
| Native C | x86_64 / aarch64 / riscv64 — Linux, macOS, Windows, FreeBSD. The default path: AOT through the system C compiler. |
| LLVM IR | Text emission for any LLVM-supported target. |
| WebAssembly | wasm32 with std.jsffi and std.dom, optimised and packaged by hls-wasm-pack. |
| Bare metal | x86_64 / aarch64 / riscv64 -unknown-none — Multiboot2/Limine headers, IDT/GDT syntax, MMIO, DMA-safe buffers, custom link scripts and sections, DWARF 5. |
The guarantees above are not marketing copy — they are rows in a soundness ledger, each with an enforcement mechanism, an anchor in the source tree, and negative probes that must fail exactly as expected.
Each ledger row has an enforcement mechanism, an anchor in the source tree, and negative probes that must fail exactly as expected. The published bug-bounty scope is that same ledger.
Committed C snapshot: 43,610 lines, sha256 5943322a69d3ee1c…
G-MEM-01 · G-MEM-02 · G-TYPE-01 · G-ARITH-01 · G-EFF-01 · G-TAINT-01 · G-CONC-01 · G-MATCH-01 · G-CAP-01 · G-SBX-01 · G-CT-01 · G-FFI-01 · G-SPEC-01 · G-DET-01 · G-CORPUS-01
The questions people actually ask about a language this young.
No. With closures, spawn would have to capture variables by context, and the rule "values only cross task boundaries through channels" would stop being statically checkable. The absence of closures is a deliberate consequence of the data-safety rule.
It stops unsanitised data from reaching a sink. It is a single label: tainted[str, Html] and tainted[str, Sql] are not implemented. Today, one sanitize call drops the label for every sink.
Yes, extern "C" blocks come with ABI size checks. But headers are trusted: a wrong declaration (int for a function returning char*) is not detected. hlbindgen mitigates this by generating extern blocks from a header with ABI size assertions.
Halis compiles ahead of time through C, with no VM and no GC in the runtime. We do not publish comparison benchmarks until there is a public benchmark infrastructure; four regression dashboards (perf, mem, ctime, size) track every commit to catch drift.
One person, under the MIT licence. The main branch is protected and every change lands through a pull request with green CI.
v1.0.1 is the current stable release. The release/1.0 line is the LTS stream: 3 years of bug fixes and 5 years of security support, counted from the day the line was cut.
Nothing compiles. Every release archive carries the whole toolchain — the compiler and the formatter as binaries, the standard library as source — so installing Halis is a download, a checksum and a copy.
SHA256SUMS before anything is unpacked. std.result program and formats it twice — the second pass must equal the first. --uninstall removes exactly what the install recorded, nothing else.