Wado

Research: Language Survey — Hike

Survey against the rubric.

kanryu/hike-lang — 115 commits / first 2026-08-27 / surveyed at fac6525a 2026-09-11. One author, sixteen consecutive days.

Arbiter: "Hike is an experimental systems language combining Go-style syntax and ergonomics with C-equivalent execution, direct C-ABI compatibility, and no garbage collection." — README.md, opening the Overview. Four goals joined by "and", in no order.

A Go-shaped language whose compiler is a frontend only: hikec emits LLVM IR and hands every backend concern — object format, optimization, linking — to Clang. 19,528 lines of Go against 3,761 lines of Hike in the whole repository.

Examined: README.md, concurrency.md, encoding.md, wasm.md, without_cgo.md; the whole of std/ by count and sampled reading, with std/encoding/json/json.hike and std/maps/maps.hike read in full; pkg/token/token.go, pkg/target/target.go, pkg/backend/llvm/runtime.go and runtime/runtime.ll in full; pkg/sema/sema.go, pkg/parser/parser.go, pkg/backend/llvm/emitter.go and pkg/compiler/wasmjs.go in the regions the rows below cite; tests/e2e_setup_test.go in full and the other nine test files by name and count; examples/ by listing, with shared/libcalc.hike read.

Nothing was run, and that bounds everything here. This environment has no Go toolchain and no Clang, so the compiler was never built and no Hike program was ever compiled. Every "Reality" below is read from the implementation or counted with a command. None is observed behaviour.

Not examined: pkg/lower/ (4,411 lines) and pkg/transform/ beyond the cited call sites, and pkg/codegen/debug.go.

A. The language

Axis Claim Reality Holds in self-application Unimplemented / Rejected
A1 Canonicity Not claimed anywhere Synonyms at every layer. token.go:123 maps both Async and async to one token. A C import has two spellings, extern func f(…) and a bodyless func f(…), which sema treats identically (IsExtern: (fd.Body == nil), sema.go:788, against sema.go:820). float is an undocumented alias of float64 (sema.go:67). runtime.ll carries os_sleep_ms and c_os_sleep_ms as byte-identical definitions, likewise os_now_ns / c_os_now_ns. The darwin triple is arm64-apple-darwin in target.go and arm64-apple-macosx in emitter.go No — the stdlib uses the bodyless form throughout, and spells the void return both ways: func free(ptr *byte) in json.hike, func free(ptr *byte) void in maps.hike —
A2 Type vocabulary Go's, mapped to fixed widths: int→i64, byte→u8, bool→i1, string→i8* Go's, plus cstring for the raw null-terminated *byte that C-interop needs (encoding.md §1). PointerSize and the width of int/string are mutable package globals set once per process from the target (sema.go:32, sema.go:80), so the type system's Size() answers depend on process state — rune is accepted by the parser's builtin-name list (parser_expr.go:92) and absent from sema.BuiltinTypes, while encoding.md §2 recommends []rune traversal
A3 Effects None, and function colouring is named as the thing being avoided (concurrency.md §4) None, not even one bit. Nothing in the type system distinguishes a function that allocates, blocks, or calls C — Refused, with the reason written: Async is a property of the call site, never of the callee
A4 Errors Go's: values, multi-return, error as a builtin interface error is one interface registered in sema_context.go:58. No propagation operator; every error is branched on by hand or dropped. No panic / recover in the keyword table. errors.New takes a *byte and errorString.Error() returns *byte, so the error message travels in the FFI type, not in string No — the two parsers that matter return nil on failure, not an error: json.Parse(jsonStr string) *Value, toml.Parse(tomlStr string) *Value No stated rule for when an error is worth branching on
A5 Concurrency Async(f) / <-task / chan; 1:1 OS thread-pool offload, explicitly not green threads (concurrency.md §5) Hand-written LLVM IR over kernel32 alone: QueueUserWorkItem, CreateEventA, SetEvent, WaitForSingleObject, CloseHandle, Sleep, GetTickCount64. A grep for pthread, clock_gettime, nanosleep and usleep across pkg/ and std/ returns nothing — Not built yet for four of the five targets the CLI accepts. Async, chan and std/time cannot link on linux, darwin or wasm32
A6 Boundary mechanisms Four generators, no macros, no reflection The widest axis in the project. C in: a bodyless func becomes a declare, no wrapper. C out: cfunc plus -header emits the matching .h (cgen/header.go, 256 lines). Go out without cgo: passthrough cfunc emits a .syso and a Plan 9 assembly stub (compiler/go_decl.go, 274 lines). JS out: jfunc plus a generated runtime.js (compiler/wasmjs.go, 169 lines) — Nothing comes in but a C signature retyped by hand. No IDL, no schema, no serialization framework: std/encoding/json and toml are hand-written DOM parsers
A7 Hidden operations No list At least six, none enumerated: escape-driven heap promotion (sema.go:1182), monomorphization, implicit numeric and interface casts (ast.ImplicitCastExpr, synthesized at sema_context.go:1192), itab boxing on interface assignment, memcpy insertion for struct copies (lower_call.go:190, :218), and the alloca a range loop issues for a custom container's iterator state — —

Everything the four generators produce is a text file you can open, diff, and hand to another toolchain: the .ll, the .h, the .s, the runtime.js. None of it needs hikec to be read.

The self-application cross-check

There is no self-hosting; it is the last unchecked box on the README roadmap. So the hardest program in Hike is its own standard library: 3,001 lines across 17 files, the largest being std/fmt/sprintf/sprintf.hike at 501.

The privileged prefix is the bodyless func, which is how a C symbol enters. 9 of the 17 stdlib files declare and call C directly, 53%. 5 of them also perform unchecked pointer casts, 24 sites. There is no module a libc declaration can live in, so each one is retyped where it is wanted: malloc and free appear five times each. All five land in one entry, because ctx.Functions is a flat map keyed by bare name (sema.go:823) with no package qualification for externs. They agree today. If two ever disagreed, the last one read would win in silence.

The floor is not a typed generic operation. It is malloc with a constant the author worked out by hand:

v := (*Value)(malloc(128))                // json.hike:43, ×6; toml.hike:45, ×6
b := (*Builder)(malloc(24))               // builder.hike:18
var newEntry *Entry[K, V] = malloc(32)    // maps.hike:116
m.buckets = calloc(initialCap, 8)         // maps.hike:88

The language has no sizeof: the string appears nowhere in pkg/ or std/. So every one of those numbers is a literal that nothing rechecks. Adding a field to json.Value is a silent heap overflow. Entry[K, V] is generic and allocated at a fixed 32 bytes, correct only while K and V are both 8-byte types, which is every use the repository contains.

The compiler computes the same fact twice, too. StructType.Size() (sema.go:170) is an unpadded sum of field sizes, while the struct it emits, %struct.X = type { … }, is not packed, so LLVM pads it. That Size() supplies the element stride for slice indexing and the byte count for make. The two answers agree only for structs whose fields are uniformly wide, which every struct in the stdlib happens to be.

The headline generics claim does not reach the stdlib either. The README's example is func Min[T int | float64](a T, b T) T, and ast.TypeParam has two fields, Token and Name, with no constraint node at all. parseTypeParams (parser.go:580) reads an identifier, then breaks and calls expectPeek(RBRACKET) on the next token. The repository's own copy of the example the README quotes, examples/shared/libcalc.hike, writes func Add[T](a T, b T) T and calls it as Add[int](a, b). Nothing in tests/, examples/ or std/ uses a union constraint.

One more thing the stdlib author lives with: a type error in Hike source is reported as a Go panic(). There are 23 panic sites in pkg/, 9 of them in sema_context.go.

Count the implementations: two maps

Hike ships a builtin map[K]V and a std/maps.Map[K, V], and they do not agree about what a key is.

The builtin lowers to __hike_map_* in runtime.ll: __hike_hash_str is FNV-1a over the bytes, and __hike_map_key_eq compares pointers first, then falls through to strcmp. It is correct.

std/maps hashes with func hashKey[K](key K) int { return int(key) }, which for a string key is the pointer value. It compares with ==, which does compare contents. Two equal strings at different addresses therefore land in different buckets and never match. The README documents this one, in §7, as the way to use a hash map. Its example works because both the insert and the lookup name the key as a literal, and literals are interned into one @.str global.

Nothing holds the two together: there is no CI, no differential test, and no e2e case for std/maps at all.

Where the reality is a bug

NewNull, NewBool, NewNumber and NewString (json.hike:42–67, mirrored in toml.hike) allocate 128 bytes with malloc, not calloc, and then set one or two fields. Value's three slice headers — ArrVal, ObjKeys, ObjVals — keep whatever the allocator handed back. Any code that asks a scalar node for len(v.ArrVal) reads that.

Both files declare free and never call it. That is the general case, not an oversight in two files: the compiler emits malloc for closure environments, append growth, string concatenation, substrings and slice-to-string, and it never emits free anywhere (grep '"free"' pkg/lower pkg/backend finds only the symbol-table entry). %struct.Arena and %struct.Allocator are declared in runtime.ll and used by nothing; the arena allocator is a roadmap box. "No garbage collection" turns out to mean no deallocation.

The libc dependency is imported from the host rather than removed. wasm.md §1 tables it as "Completely Zero (LLVM IR built-in)", and runtime.ll does define memcpy, memcmp, strlen and strcmp as define internal. Three lines above them it declares malloc, calloc and free as external. The wasm link passes -Wl,--allow-undefined, and the generated runtime.js implements malloc as a JavaScript bump allocator whose free is an empty function (wasmjs.go:36–56). The allocator that the table counts as zero is in the glue that the same document counts as generated.

B. The practice

Axis Present / Practised / Refused / Absent Scope or doctrine
B1 Arbiter Absent The Overview sentence joins four values with "and" and never ranks them: Go ergonomics, C-equivalent execution, C-ABI compatibility, no GC. Nothing in the repository says which one yields
B2 Accept/reject criteria Absent No CONTRIBUTING, no PR template, no CI. go test ./... is the README's stated check and pkg/ contains zero test files
B3 The "why" record Practised Four topic documents each argue their design in prose: concurrency.md §5 tables Async against go func() and gives three reasons for the rename; without_cgo.md §2 explains the .syso and Plan 9 mechanisms; encoding.md §1–2 states the UTF-8 invariant and why slicing breaks it. No index, no template, no per-decision record
B4 Falsifier Absent No decision template exists to carry one
B5 Rejection record Practised, one entry concurrency.md §5 is a full rejection of Go's go keyword with the reasoning kept: the green-thread fallacy, and what a 1:1 pool does when a user spawns a hundred thousand tasks. It is a section of a feature document, not a list, and there is no rule for citing it
B6 Sync gate Absent No CI of any kind. The README roadmap still lists "Dynamic interface dispatch (vtable)" as an open box while tests/e2e_interface_test.go exercises it through an itab and emitter.go has an emitItabs stage. Nothing in the repository could have caught that
B7 Self-reported violations Practised, scoped to one feature without_cgo.md §6 lists two standing constraints of passthrough cfunc with the mechanism and the consequence, including "silent stack corruption" on a ~2 KB goroutine stack. encoding.md §2 does the same for string indexing. The README's Status note scopes all testing to Windows/MinGW. No file:line, no ledger
B8 Who decides No rule One author, 115 commits, no co-author trailers

B7 and B6 point opposite ways. Where this project writes a document about a mechanism, it is unusually candid about that mechanism's failure modes: §6 of without_cgo.md names the corruption before anyone hits it. Where a claim lives in a list rather than in an argument, nothing maintains it. The roadmap contradicts the test suite, and the CLI offers four targets the runtime cannot serve.

The Almide survey met the same split from the other side. There, the rule with nothing on screen to prompt it was the one that leaked.

C. What outlives it

Kind What Externality Contender Wins
C1 Artifact None. The largest non-stdlib, non-test Hike program is examples/toml/main.hike, 110 lines — — —
C2 Method Cgo-free native FFI: compile to a .syso the Go linker merges automatically, bridge the ABI with a generated Plan 9 assembly stub, and keep the object free of libc refs High — the recipe needs no Hike cgo On call cost and build deps
C2 Method The 2-pass stack iterator protocol: InitIterator(nil) returns the state size, the caller allocas it, InitIterator(buf) initializes it in place High — a way to give a container a zero-allocation range loop with no GC and no lifetimes An iterator object on the heap On allocation, not on safety
C4 Corpus 74 e2e cases, each an inline Hike program with its expected stdout and exit code, run end-to-end through Clang Low — —
C4 Corpus runtime/runtime.ll: 1,106 lines of hand-written LLVM IR — a thread pool, channels, a hash map, string ops, in both 64-bit and 32-bit forms Medium — readable by anyone emitting IR Writing the same runtime in C Not obviously

C1 is empty, and here that means "not built yet" rather than a consequence of the design. Hike's boundary is wide open in four directions, and on day sixteen nobody has walked through it yet. Almide's empty C1 was the opposite case: nothing external could have been built there, because there was no way in.

C2 is where the project's spare effort went. It does not follow from B1, because B1 does not exist. Both methods came out of one instinct rather than a stated arbiter: refuse the runtime, and pay for it at the boundary with generated code you can read.

For Wado

Learned:

Take:

Refuse: