Skip to main content
Halis
v1.0.1
v1.0.1MIT9 effect0 GCself-hosted

Safety is the default.

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.

examples/effects_demo.hls HLS
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
}
❯ $ hlc --audit examples/effects_demo.hls
function declared computed
-------------- ---------------------- ------
pure_compute (none - pure) (none)
read_config Fs Fs
elapsed_ms Clock Clock
program_args Args Args
main IO, Fs, Clock, Args, Exit IO, Fs, Clock, Args, Exit
verdict: OK
Declared capabilities vs. what the compiler computed by walking the call graph. Read the full effect model →
The guarantees

Seven guarantees. Seven things you no longer have to remember.

Each guarantee below is pinned in a machine-checked ledger. The published bug-bounty scope is that same ledger.

01

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.

02

Every operation is checked

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.

03

No null. Nothing hidden

No null, no uninitialised state, no globals, no hidden anything. Everything is explicit so that it can be audited line by line.

04

Memory safety without a GC

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.

05

Data races are impossible by construction

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.

06

Untrusted input is tainted by default

tainted[T] values cannot reach a sink — print, files, exit, DNS, subprocess — until they pass through std.sanitize or an explicit untaint.

07

The whole program is auditable

hlc --audit prints the capability and taint-flow report of every function: declared versus computed, with the violation named.

The effect system

Nine effects, declared where they are needed.

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 →

What the language says

The language says exactly what it does.

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
}

Adapted from the examples/ directory of halis-lang

let p: Point = Point { x: 3, y: 4 }
The type is always declared. No implicit inference, no implicit casts.
fn translate(mut self: Point, …)
mut on a parameter allows reassigning the binding inside the body.
self.x * self.x
int multiplication is overflow-checked: overflow halts safely, it never silently wraps.
Self-hosting

The fixed point, proven on every build.

The compiler is written in Halis, so it must compile itself — and land on exactly the same result.

snapshot/hlc.c committed seed bin/hlc native compiler src/hlc.hls compiler snapshot cc compiles must re-derive byte for byte

A fixed point, not a promise

Every build must re-derive the committed snapshot byte for byte. A stale or hand-edited snapshot fails the build loudly.

Two independent ignitions

`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.

No Python on the lifecycle path

Nothing on any build, release or snapshot path reads Python. The interpreter is lab tooling for the differential suites only.

The ecosystem

The whole toolchain is written in Halis.

Not "a compiler that supports Halis" — Halis compiles Halis. Every new language feature is used inside the compiler itself before it ships.

Toolchain

Every tool is written in Halis and compiles itself. (30 công cụ native)

Compile & check
hlchlfmthllinthlbindgenhls-spec-checkhlfront
Editor & debugger
hls-lsphls-daphls-replhlservehls-wasm-pack
Test & measure
hltesthls-benchhls-rthls-dethls-rsshls-perf
Supply chain & governance
hls-pkghls-sbomhls-signhls-tloghls-freezehls-guaranteehls-lts

Standard library

Pure HLS modules, grouped by the three application families Halis set out to serve. (64 std + 18 core)

Core 13
strmathfmtlistsetcollectionsbitsoptionresulthashregextemplatelog
System 12
iofsenvprocessthreadsynctimestreamasyncarchiveuuidnet
Web 16
httphttp2http_routerhttp_serverwebsocketssecookiesessioncsrfgraphqlopenapidomjsffijsffi_callbackurlhtml
Data 10
jsonjson_streamcsvconfigcryptosha1sha256base64hexsimd
CLI 8
clituicolorprogresscompletemanhlsdochlscli
Security 2
taintsanitize
Testing 3
testquickcheckbench
Bare metal 18
core.alloccore.asmcore.atomicscore.bootcore.clonecore.dmacore.eqcore.interruptcore.itercore.memcore.mmiocore.optioncore.paniccore.portcore.resultcore.rtcore.sectioncore.stack
Targets

One language, four kinds of target.

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.
#![freestanding]#![no_std]page-level memory controlvolatile I/O
Verification

Evidence, not assertion.

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.

15 soundness ledger rows each with an anchor and negative probes
9 static effects from parser.hls and checker.hls
82 standard library modules 64 std + 18 core
247 negative probes under tests/fail/

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

Questions

Straight answers.

The questions people actually ask about a language this young.

Does Halis have closures?

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.

Does taint tracking stop SQL injection?

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.

Can I use it from C?

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.

How does the performance compare to Rust and C?

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.

Who maintains the project?

One person, under the MIT licence. The main branch is protected and every change lands through a pull request with green CI.

Which release is stable?

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.

Get started

Install Halis in one command.

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.

curl -fsSL https://raw.githubusercontent.com/mhieuhonda/halis-lang/main/install.sh | sh
SHA-256 verified against the release’s own SHA256SUMS before anything is unpacked.
The installer runs its own acceptance gate: it compiles a std.result program and formats it twice — the second pass must equal the first.
--uninstall removes exactly what the install recorded, nothing else.