Roadmap
- Type checker (static types with inference) and typed JIR.
- Differential tests across both backends.
- Sized integers, pointers with loads and stores, structs, arrays,
size_of, inline asm. comptimeon the VM, generic functions.- Expression macros, generic structs, bitwise operators, modules and an
alloclibrary,pub, statement and item macros,unique/ident. - GC hardening (one root set, generation-checked references, stress mode); function values.
- Sum types and
match;ref Tfor recursive data; nested patterns, guards,|patterns andmatchexpressions. - Closures, captured by value and lifted into functions.
- Tasks and channels on the VM: one OS thread, a deterministic scheduler;
select. - Comptime closures: closures passed to
comptimeparameters, one instance per closure, captured values as hidden arguments. - Coroutines for freestanding code, as a library on top of function values
and
asm. - A native profile: LLVM-compiled programs for an OS, linked with libc,
calling C through
extern fn. - Floats:
f32andf64, identical on the VM and natively, and across into C. - Targets: x86_64 and aarch64 Linux with
--target, recorded in JIR; per-target modules (sys.x86_64.jh); the standard library on both, with coroutines on libc’s contexts. - Macro hygiene: a template’s locals are its own, and its other names resolve where the macro is defined.
- Field visibility:
pubfields, also through macros. - Float builtins, the same in every profile:
sqrt,floor,ceil,trunc,round,abs,copysign, and a float’s bits (to_bits,from_bits). printin native programs, with the VM’s output to the byte.- Structs by value to and from C, lowered per target (x86_64 System V,
AAPCS64), and
export fnfor functions C calls. - A Rust-side JIR reader:
jihoo run file.jir,jihoo build file.jir. - A hosted standard library: strings as bytes with
len,s[i], slices and ordering; lists[T]withpushandpop, in place when unshared;read_line; the prelude (Option,Result);lib/strings.jhandlib/lists.jh. JIR version 5. trap()in every profile (JIR version 6), and a checkedalloc: bounds checks onVec, traps on misuse, overflow-safe sizes, private fields,arena_free.- Threads and atomics (JIR version 7):
atomic_load,atomic_store,atomic_swap,atomic_addand the other read-modify-writes,atomic_casandfence, sequentially consistent;lib/sync.jh, futex locks (Mutex,Cond,Group) for native and freestanding code;lib/thread.jh, POSIX threads for native code;lib/channel.jh, channels between threads withclose, andSelectover them (sendandrecvare now names a module may take);go f(x)in native code, starting a detached thread (JIR version 8);channel.wait_ms, a Select with a timeout, andthread.now_ns. - Memory safety without a GC or a borrow checker: paths
(
path(Root).a[_],root[p],path_index), references that hold indices instead of addresses; lists in native programs, freed by an ownership pass over JIR that moves values at their last use, clones them where the original is still used, reads parts as views, and makes consuming versions of functions forxs = f(xs)(JIR version 9:cloneanddrop). Checked by hosted-versus-native tests and a list mode of the fuzzer, under valgrind. str, capturing closures,ref,cell, tasks, channels andselectin native programs, then native as the default profile: a program without an attribute is native,#![hosted]asks for the VM, andjihoo runcompiles and runs native and freestanding programs. Native tasks are threads and share no cells;comptimeand macros still run on the VM.forloops in every profile: rangeslo..hi, and lists, arrays and strings, with the index (for i, x in xs). The loop walks the value it started with; natively that is a view of the list, which the ownership pass now also makes of any copy whose original is still used, unless one of them changes. The libraries useforwhere it fits.- Tuples in every profile:
(A, B)types,(a, b),t.0, tuple patterns andlet (a, b) = .... A tuple type is a struct named by its spelling, so JIR and both backends are unchanged. - Inferred type arguments: a call to a generic function may leave out its
comptime T: typearguments, which are matched from the other arguments (lists.sort_by(xs, fn(a, b) { return a < b })); literals and anonymous functions take what those decide. The first step of traits. - Traits:
trait,impland bounds (comptime T: Ord + Hash), with default bodies, method calls (x.f()),Trait.f(x)andType.f(). An impl’s functions are compiled per type like generic functions and called directly, so JIR is unchanged; impls live in the trait’s or the type’s module, and every one is checked. The prelude hasOrd, for integers andstr, andlists.sort(xs)sorts by it. Generic impls (impl(T: Ord) Ord for Option(T)), matched against a type’s shape and applying when their parameters’ bounds hold; the prelude orders options, lists and tuples. The standard traits:Ord(floats in total order),Hashwithhash_of, andShow, whichprintuses;lib/maps.jh, hash mapsMap(K, V)for keys withHashand==.<,<=,>and>=on any type with anOrd, throughcompare.meta.derive!(Show, Hash, Ord, T)for structs and enums, withtype_shapeandto_itemsfor macros;lists.minandlists.max. The fuzzer declares random traits, impls and derives and calls them. - Tail calls of a function to itself are jumps, in every profile and at
every
-Olevel: sema assigns the arguments to the parameters and jumps back, so JIR only sees a loop and natively the ownership pass frees each round as for any loop. - Associated types (
type Item,T.Item), supertraits (trait Greet: Named), and the prelude’sIterator, whichforwalks (traits). Natively, the parts of a value that is only taken apart, such asSome((rest, x)), move out of it instead of being copied, so an iterator over a list copies nothing; a function whose parameter is only taken apart gets a consuming version, so iterator adapters (lib/iters.jh:map,filter,take,zip) copy nothing either.meta.derive!for a generic type’s name writes generic impls. - Moving a field out of a struct that is used again (JIR 16:
take): natively a field read that would be copied moves out when nothing reads the field before it is written or the struct dies, and the struct keeps the field empty. Sob.xs = grow(b.xs)and an iterator’ss.inner = restcopy nothing. - Tail calls between functions (JIR 17:
tailcall):return g(...)does not nest, so mutual recursion and state machines run in constant stack in every profile, at every-Oand on both targets. The VM runs the callee in the caller’s frame; LLVM getsmusttailcalls betweentailccbodies, which allows any two signatures; the ownership pass hands the arguments over to a version of the callee that owns them. jihoo build --cost: where a native program copies lists, strings and closures, and why, from source positions in JIR (JIR 18:file,loc), which sema writes when asked and the backend reads past.takefor any path of fields and elements (JIR 19):rows[i] = f(rows[i])andshelves[i].right.xs = f(shelves[i].right.xs)move the part out and back. Paths are compared step by step; elements at different index registers, or an index written in between, count as the same element, so the part is copied.- Contracts (Contracts):
requiresandensuresclauses on functions, checked at run time in every profile, and tried while compiling: each function with a contract runs on the VM with special and seeded random inputs, and the smallest input that makes anensuresfalse, breaks a callee’srequiresor traps is a compile error that shows the call. JIR is unchanged: a false clause calls a function named after it that traps. - Copy-free functions (Copy-free functions):
nocopyafter a signature makes a copy that the function or anything it calls makes natively a compile error, which says where, why and through which calls. Function values made inside are followed. On the way, the ownership pass learned to hand a list on through functions that only pass it along (they get consuming versions too), to move out a part handed on while the rest is still used, and to take two parts of one value. Withlists.sort_byreading its length first, iterator chains andlists.sortcopy nothing over a list handed to them; the examples and list tests copy 132 times instead of 153. - Rewrite rules (Rewrite rules): a function with
replaces patternis a rule. Calls matching the pattern call it instead, with the matched parts evaluated once, in order, and the rule applied only where their types fit. Before that, the compiler runs every rule next to its pattern on the VM, with the inputs contracts are tried with and functions it writes for parameters of function type, and refuses a rule that gives a different value, prints differently or traps differently. Generic rules are also checked ati64. - Rewrite rules keep effects: the checks only try functions without
effects, so a rule now applies only where moving code cannot be seen.
The functions a call passes must be inert (no effect, and certain to
return: no print, cell, pointer, unknown call, integer division,
bounds check, loop or recursion, through every call), and so must any
part evaluated after one of the pattern’s own calls that is not, and
any part right of
&&or||. Elsewhere the call is compiled as written.map_mapstill fusesmaps of inert functions. - Arguments, files and floats in text, in hosted and native programs alike
(JIR 20):
args(),read_file,write_file,append_file, with Linux’s error numbers on the VM too, andlib/files.jhover them;parse_f64andstrings.parse_float, correctly rounded everywhere (Rust’s parser on the VM,strtodnatively, after the same check of the syntax). - A real program,
examples/json.jh(a smalljq), and what it showed about native memory: each arm of amatchnow takes apart the value it matches, soDone(v, rest)moves its parts out instead of copying them; a part read stays a view while another part of the same value changes in place (let t = s.textass.at += 1); and a variable replaced by a value made from its old one (v = items[k]withitemspart ofv) frees the old one, which leaked before. Parsing a 0.9 MB document went from 19 s, quadratic, to 0.03 s natively (the VM takes 0.8 s). - A call hands over what it can: when some of the parameters a function
copies get values at their last use and others do not, the call still
goes to the consuming version, and the caller copies the others. Before,
m = maps.set(m, w, n)with a wordwstill in a list copied the whole map; counting 300,000 words took 129 s natively, now 0.13 s. - The ownership pass is checked (design): it writes down what
it decided (JIR 21:
own,views,moves), and a checker follows every value that owns memory along every path of every native function, so a mistake in the pass (a leak, a double free, a use after a move, a view outliving its value) is an internal compiler error, not memory corruption. With thev = items[k]leak put back into the pass, it rejects the programs that hit it, at the instruction. - Counted list and string buffers (design): natively a copy of a list or a string shares its memory and counts one more holder, and the first write to memory held more than once copies that list, one level. Flags that remember which lists a function knows to be unique let LLVM fold the checks away, so loops of writes cost what they did. Keeping 2,000 versions of a 200 x 200 grid went from 0.51 s and 643 MB to 0.02 s and 14 MB; programs that copied nothing take the same time.
lists.mapto the same type andlists.filterwrite into the list they are given, through rules inlib/lists.jh(map_in_place,filter_in_place) that the compiler checks:xs = lists.map(xs, f)allocates nothing (0.05 s and 17 MB instead of 0.10 s and 33 MB on 2 million integers). Dividing by a constant other than 0 and -1 counts as inert for rules, sox % 3 == 0can be given to them.- Rules that keep the order of calls apply to any function: the first
time a rule would apply to a function with effects, the compiler runs
it next to its pattern with functions that record each call and trap
at one of the first few, and the rule applies when the calls, the
output and where each side stops are all the same.
map_in_placeandfilter_in_placepass, solists.mapwithstrings.to_upper, or with a function that prints, writes into the list too;map_mapdoes not, and still fuses only inert functions. - Payloads move out of values used again: a value is taken apart in each
block that reads only its parts and where it dies, whatever the rest of
the function does with it, unless it is a view or a view of it is still
used. Rebuilding a state in a variable (
st = State.Busy(xs)afterpush(xs, i)) went from 0.87 s to 0.12 s for 30 million rounds; out of a field every arm writes again, from O(n²) to O(n) (15.3 s to 2 ms for 100,000 rounds). The fuzzer now takes apart and rebuilds aSlot(T)of random payload types.
-
Code as data (design). Done: functions on code, which macros build their code with, macros taking any number of code arguments, and
meta.nodeto look inside code (JIR 14:codetext), with whichexamples/pipe.jhfusesmap,filterand sums into one loop, three times as fast aslists; blocks as expressions, so that a fused pipeline is a value;type_of(JIR 15), so that it works for any element type;lists.fuse!in the library, withsum,countandcollect; and#![comptime]modules, sometacosts programs nothing and works in freestanding ones; random pipelines in the fuzzer, each checked against the same stages written out as a loop. -
Traits (design): dynamic dispatch, when a program needs it.
-
Rules: patterns that are operators (
x * 1) or methods. -
Contracts: clauses on trait functions, which every impl would keep; invariants of a module’s structs, so that
pubfunctions taking them can be tried;old(e)for the value of an expression at the start; and telling apart runtime contract failures from other traps natively. -
Memory (design): taking a payload out of a field that some arm of a
matchleaves as it is (takeof paths through payloads), or putting it back on that path. -
Floats:
fma, which IEEE also defines exactly, but freestanding code on x86_64 would need an exact software version. -
Tail calls through function values (
return f(x)withfa parameter), which need a convention for every function value. -
Native code: structs by value through function values handed to C (callbacks take scalars only);
printin freestanding programs. -
Concurrency: weaker memory orders (acquire/release) for code that needs the speed; threads in freestanding programs, over
clone; tasks on more than one core in the VM, which needs a GC that threads can share. -
Library: directories, and reading a file a piece at a time; the environment.
Library layers
Section titled “Library layers”The standard library is meant to grow into three layers:
std hosted only: GC types, I/O, collectionsalloc any compiled profile, given an allocator: Vec(T), stringscore everywhere: integers, pointers, slices, OptionToday std is the prelude, strings and lists, on the VM’s str and [T],
and alloc is a bump arena with Vec(T) (lib/alloc.jh); the goal is for
freestanding code to bring its own allocator, written in jihoo on top of
syscall, and use the same collections with it.