This is the design of the Calx features that aren’t built yet. What’s built is in the reference, which is the normative text for it: when a feature ships, its section here moves to a reference page, and a link is left behind. Each feature lives in exactly one place. The roadmap sums up what’s still here.
The design was first written for Calx’s first large program, Calcite, a Minecraft server with plugins, from its sketches and five sample plugins, so many of the examples are about a game world. Each section settles the requirements that Calcite’s build plan and decision log set for it, and a checklist at the end says where.
The examples use the syntax as it stands now. Unlike the reference’s, they aren’t compiled yet, since the features they use aren’t built. Each is compiled from when its section moves to the reference.
In order:
- Exact where it matters. Integer and float results are defined to the bit on every platform, and the Java-compatible types match Java’s results, hash codes and orders, so code ported from Java can give the same answers. Calcite needs this to match the official server.
- Fast to run, and fast to build. It compiles to C with no garbage collector and no hidden costs, and runs the tick-loop benchmark faster than Rust, which is the target, not just a tie. Unlike Rust, building is quick too: a change rebuilds only what it touched, and
calx runstarts in moments (Build speed). When the two conflict, run time wins in release builds and build time wins in debug builds. - Safe and strict by default. No undefined behavior outside
unsafe, so an untrusted plugin can’t corrupt the program that loads it. The type checker is stricter than Rust’s (Strictness). - Simple to write. No lifetimes, no locks, no null. Errors are values. The compiler checks that parallel work stays apart (region scoping).
Syntax overview
Section titled “Syntax overview”The basics the rest of this spec uses:
module example.basics
import std.collections.List
/// Doc comments start with three slashes, and the reference docs are built from them.pub struct Point { x: i32 y: i32 = 0 // a field default}
pub fn manhattan(a: Point, b: Point) -> i32 { let dx = (a.x - b.x).abs() // let: immutable var dy = a.y - b.y // var: mutable if dy < 0 { dy = -dy } return dx + dy}- Statements end at the end of the line. A line that ends in an operator or an open bracket continues on the next.
- Blocks use braces.
if,matchand blocks are expressions:let x = if c { 1 } else { 2 }. - Comments are
//. Doc comments are///and are required on everypubitem of the Calcite host API. pubmakes an item visible outside its module. Everything else is private to the module.- Attributes are
@nameor@name(args)before a declaration (codegen hooks). - Strings interpolate with
{expr}:"moved {d} blocks". A raw string,#"..."#, has no escapes and interpolates with#{expr}, and one that ends its line is a block. Built: raw strings - Loops are
while cond { },while let pattern = value { },for x in iterable { }andloop { }, withbreakandcontinue. Built: control flow - Logic uses words:
and,or,not. Comparison and arithmetic use the usual symbols. match v { Pattern => expr, ... }matches enum variants,noneandsome(x),ok(v)anderr(e), literals, tuples and_.- A function with no
->returns nothing.fn f() ! Ereturns nothing but can fail with anE. - Calls can name their arguments,
teleport(to: home), and parameters can have defaults,fn f(n: i32 = 3). - A closure passed as the last argument can follow the call:
p.after(ticks: 60) |p| { ... }. - In an
impl Tblock, a function whose first parameter is aTin any mode (self: T,r: mut T,x: own T) is a method, called asvalue.f(...).selfalone means a read-onlyself: T.
Primitive types
Section titled “Primitive types”Built: see Types.
Compound types
Section titled “Compound types”Built: Structs, Enums, Tuples, fixed arrays (Types), Lists, and plain functions as values, fn(A, mut B) -> R ! E (functions as values). Still to build:
- Wrapper structs:
struct Name(String)holds one field, read as.0. - Closures: written
|a| expr, with types of their own, accepted through interfaces such asFn(A) -> B. A plainfn(A) -> Bholds no captures.
Optionals, no null
Section titled “Optionals, no null”Built: see Optionals and Patterns. Still to build:
opt.map(f)andopt.or(default), with closures and generics.?.calling a method,opt?.len(), as well as reading a field.- Binding names in or-patterns,
Circle(r) | Ring(r), when every alternative binds the same names with the same types.
Errors are values
Section titled “Errors are values”Built: see Errors. -> T ! E and fn f() ! E, fail e, try f(), matching ok(v) and err(e) with match, if let and let ... else, f()!, and fn main() ! E, which shows the error and exits with status 1. A result is handled at the call, so it’s never stored or dropped. try converts the call’s error to the function’s through the built-in From<T> when their types differ (converting errors). Still to build:
! Ein closures’ types.
Interfaces and generics
Section titled “Interfaces and generics”Built: see Generics and Interfaces, including generic interfaces such as From<T>, implemented once for each list of arguments, with calls that choose between the impls by their types. Still to build:
dyn Shape: an owned value of any type implementingShape, called through a table, for the places that need it (the host’s handler tables).- Associated types in interfaces.
- Generic functions in interfaces, and defaults for an interface’s constants.
- Operators from standard interfaces (
Add,Sub,Mul,Neg,Index,Equal,Order). Only value types should implement the arithmetic ones, such asVec3. The sketches subtract positions this way.
Derived properties
Section titled “Derived properties”The compiler works these out for each type, without being asked:
| Property | Holds when |
|---|---|
Copy |
Every field is Copy and the type owns no heap memory. Copying is implicit only for these |
Equal, Display, Hash, Order |
Every field has them. A struct or an enum can write its own instead |
JavaHash |
Java’s hashCode: built into numbers, bool, char, rune, text and lists. A struct or an enum has the one it writes, as a Java class does |
Send |
The value can cross between regions: no views, no closures that capture views, nothing that’s only valid in one place. Plain data and handles are Send |
Share |
Deeply immutable while shared, so many parallel iterations can read it at once |
Built: Copy (Types), Equal, Order and Display (Comparing and printing), and Hash (Maps and sets), with their bounds, which generic code compares, orders, prints and hashes through. Order gives <, cmp and sorting. A struct or an enum can write its own Equal, Display, Hash or Order by implementing the built-in interface of that name: to compare only some fields, to show a block position as 1, 64, -3, or to keep a field out of logs. What holds it uses it too, a type with its own Equal writes its own Hash to be a key, and one with its own Order is sorted stably. JavaHash is the key of a JavaHashMap. Still to build:
Share, with shared values.Sendis built: every type isSendexcept one that is or holds a pointer or an arena list, and a generic parameter isSendwhen it has the bound (std.mail).
Handles
Section titled “Handles”Handle<T> is a generational reference into a slot map: a 32-bit index and a 32-bit generation. Using one gives T?-like access:
map.get(h)returnsnoneif the object died or moved away.map[h]panics in that case. Calcite core uses this where a stale handle would be a bug.
Handles are Copy and Send, which makes them the way to refer to an entity, player or chunk from anything that outlives one handler. Calcite’s PlayerRef and EntityRef wrap handles with a UUID, so they still mean the same object after a region handoff.
Integer and float semantics
Section titled “Integer and float semantics”Calx matches Java exactly. It’s all built, and in the reference: Integers, Floats and Conversions. That includes x.mul_add(a, b), Java’s Math.fma; i64.round(d) and i32.round(f), Java’s Math.round; and x.rint(), Java’s Math.rint. "{x}" gives exactly what Java’s Double.toString and Float.toString give, for SNBT and JSON, so there’s no separate java_string method.
Transcendental functions
Section titled “Transcendental functions”-
sqrt,abs,floor,ceil,rintandmul_addare methods on the float types. They’re exact in IEEE, so every implementation agrees. -
Built:
sin,cos,tan,asin,acos,atan,atan2,exp,expm1,log,log10,log1p,pow,cbrt,hypot,sinh,coshandtanh, in two modules with the same names:std.java.mathgives Java’sStrictMathresults bit for bit on every platform, for code that must compute what vanilla computes. It’s ported from fdlibm, the libraryStrictMathis defined by, with fdlibm’s license notice kept. Porting is the surest way to get the same constants and order of operations, which decide the last bit. It matched JDK 25 on 1,043,282 arguments, and the rig checks every function againstStrictMath.std.mathis fast, within about half an ULP, and also the same on every platform, since it’s all Calx. Itsexp,log,pow,sin,cosandtanare its own, faster than glibc and the Windows CRT and more accurate than fdlibm:exp,logandpoware table-driven, andsin,cosandtanshare one kernel with no branch on the quadrant. The rest callstd.java.mathuntil faster algorithms replace them.
-
Vanilla calls
Math, notStrictMath:sqrt102 times,pow70,sin30,cos23,log9,atan29,acos5,asin2, andlog1pandhypotonce each.Mth’s sine table is filled withMath.sinat startup, and everynextGaussian()goes throughMath.log. -
On x86-64, HotSpot replaces some of these with its own intrinsics. The rig measured how far they drift from
StrictMathon JDK 25.0.5 (P1.5, 500,000 arguments per input range):Function Results that differ from StrictMathWorst sin,cos,tan1.7-3.7% 1 ULP pow8.8-9.8% on ordinary inputs 1 ULP log7.2% on (0, 1), 0.9% on [1, 10^6) 1 ULP exp9.4% 1 ULP log101.7% 1 ULP cbrt8.3% 1 ULP tanh0.7-2.8% 2 ULP asin,acos,atan,atan2,hypot,log1p,expm1,sinh,cosh,sqrtnone Mth’s sine table is the same either way, since rounding tof32hides the difference. -
Matching the intrinsics would mean copying HotSpot’s stubs, which are GPL and can’t go in an MIT project. Instead, the rig’s reference is vanilla with HotSpot’s flags
-XX:+UnlockDiagnosticVMOptions -XX:-UseLibmIntrinsic. With them,MathmatchedStrictMathbit for bit on all 22 million arguments, so Calx can match that configuration exactly. The rig also records vanilla with default flags, and reports every case where the two differ.
Strictness
Section titled “Strictness”Calx’s checker is stricter than Rust’s: every rule is an error, no flag turns one off, and the only warnings are for deprecated APIs. The rules that are built are in Strictness, which links each to its feature’s page. These apply to features that aren’t built yet:
- Unused imports are errors, like unused names.
- Unreachable code includes code after
fail. - Closures end with their value, like blocks used as values. Only a function’s body has to use
return.
Memory model
Section titled “Memory model”Ownership
Section titled “Ownership”Built: owners, Copy, moves, .copy(), partial moves, patterns that take values apart, and freeing in reverse order of declaration (Ownership), and Box<T>, which owns a value on the heap, for recursive types (Boxes). Still to build:
Shared<T>is an immutable value with an atomic reference count, for data many regions read: registries, config and the tick snapshot. It’sShare, and nothing can change it.
Built: the three parameter modes, views that can’t be moved, stored or returned, pattern names that are views, the rule against passing a mut value twice in one call, and for loops that keep only what they go over from changing, so a loop over one field lets the body change the others (Functions, Ownership, Control flow). Still to build:
- Views are second-class. Besides not being stored or returned, they can’t be captured by a closure that outlives the call, or sent. So the compiler never needs lifetimes. It only checks each function body on its own.
- No overlapping mutable views. Within a function, a
mutview and any other view of the same value can’t both be in use. The Calcite sketches hit this four times, and each time it was a real aliasing bug. - Projections.
p.data(Homes)orr.chunks[c]can return a view into the receiver, valid for the expression or for aletbinding in the same block. checkout. This is for the element-and-owner case:
fn tick_all(r: mut Region) { for id in r.entities.ids_at_start() { checkout e = r.entities[id] else continue tick_entity(mut e, mut r) // e and r are separate now } // e goes back here, in its place in the order}checkout x = c[key] takes one element out of a collection until the end of the enclosing block, and puts it back at the same position in the collection’s order. While it’s out, c[key] gives none. checkout x = s.field does the same for an optional field, and the compiler stops s.field from being used inside the block.
Memory safety
Section titled “Memory safety”- Bounds checks on every index. The compiler removes the ones it can prove, such as
for i in 0..xs.len(). - No pointers in safe code. No pointer arithmetic, no casts between unrelated types, no uninitialized values: every variable is assigned before use, and struct fields take their defaults.
- No data races. The only parallelism is
parallel for(Region actors), and its capture check makes races impossible. restrict. Sincemutviews never overlap, the emitter marks every view and owned-value pointer parameterrestrictin C, which closes Rust’s main codegen advantage.- Views are always valid. Every function starts by telling clang that each view parameter points at a whole, live, aligned value (
CX_VIEW, from__builtin_assume_dereferenceableand__builtin_assume_aligned). Views can’t be stored or outlive what they view, so that holds for the whole call. Clang can then read a view’s fields once before a loop rather than on every pass, even after inlining, which Rust’sdereferenceableparameter attributes don’t survive. - Lists in locals stay in registers. A list or a deque grows by a cold call given copies of its fields, not their addresses, so clang keeps a local list’s pointer, length and capacity in registers. Had their addresses gone to a call, they’d be read back after every store of a byte or an
i64, either of which might be one of them as far as clang can tell. A function with a loop that’s given a list, a deque or aStringto change, or a struct that holds one, works on a copy in a local, written back as it returns, since through the view the same reads come back,restrictor not. Of a struct, it copies the fields it names, or all of it if it uses it whole, and only up to 8 scalars, a scalar at a time. Nothing else can see the value in between, sincemutviews never overlap and a panic ends the program.
unsafe
Section titled “unsafe”Built: unsafe blocks, unsafe fn, raw pointers and their methods, and calling C functions (Unsafe code and C functions). Still to build:
- Unchecked indexing and reinterpreting bytes inside
unsafe, and pointers inside other types. - It’s allowed only in modules the build marks trusted: Calcite core and trusted plugins.
- The compiler records in every binary whether it contains
unsafeorextern. Calcite refuses to load a native plugin that does unless the server config trusts it. - That check protects against mistakes, not against a hostile binary: a native plugin is machine code, so trusting it is trusting whoever built it. WASM is the sandbox for code nobody vouches for.
Arenas and plugin memory
Section titled “Arenas and plugin memory”Built: Arena, whose lists take memory from its chunks and are freed all at once by reset(), and ArenaList<T>, which can only be a variable or a view parameter, so the checker keeps it from outliving its memory without lifetimes (Arenas). Calcite uses one per region per tick for temporary data. Still to build:
- Other values from an arena, such as maps and strings, once there’s a use that a list doesn’t cover.
- Every plugin instance allocates from its own heap, which the host supplies. A region instance’s heap is reset after each handler, since region code keeps no state and anything lasting is encoded into host-owned bytes. The global instance’s heap lives as long as the instance. Stopping an instance frees its whole heap at once, so a panicking plugin can’t leak memory.
Panics
Section titled “Panics”What a panic does in a plain program is built, its stack trace too: see Printing and panics. The rest of this section isn’t.
- A panic is an unrecoverable bug: a failed bounds check, a division by zero,
opt!onnone,map[h]on a stale handle, orpanic("message"). - Calx can’t catch a panic. Expected failures are errors (
! E). - In a plugin instance: the panic unwinds to the host’s dispatch point, which stops that instance and frees its heap. The region keeps ticking. A plugin that keeps panicking is disabled. Native plugins unwind with a saved jump point at each dispatch, and no destructors are needed because all of the instance’s memory is in its heap. WASM traps.
- In Calcite core: the runtime writes a crash report with the Calx stack trace (stack traces), then saves and stops, as vanilla does.
- Tick budgets for plugins use the same path: a handler over budget is stopped like a panic (
P7.3).
Strings
Section titled “Strings”Built: str and String, literals as either, interpolation into a String, the basic methods, == and string patterns (Strings). for r in s.runes() goes over Unicode scalar values and for c in s.chars() over UTF-16 code units, as Java sees the string. s.java_hash() is Java’s String.hashCode and s.utf16_len() is its length(), both over UTF-16 code units (Java’s view), std.text.mutf8 encodes and decodes Java’s Modified UTF-8, which NBT uses, and std.text.utf8 reads bytes that may not be UTF-8 as Java does, with U+FFFD in place of what isn’t. Slicing is by byte offset: s[a..b] panics unless the offsets are in order, in the text, and where runes start, and a String’s part is copied only where it’s kept (Slicing). to_lower_root and to_upper_root match Java’s toLowerCase(Locale.ROOT) and toUpperCase(Locale.ROOT), final sigma included (Changing the case). Still to build:
Stringis always valid UTF-8, andstrwill be able to view part of aStringpast the statement, once a view parameter can hold one.- Interpolation of a type with a
Displayof its own calls it (Derived properties). Text with a prefix, such ast"...", is handled by a library at compile time (Codegen hooks).
Collections
Section titled “Collections”Two families: Java-compatible ones where vanilla’s order can be seen, and fast native ones everywhere else.
Java-compatible
Section titled “Java-compatible”These iterate in exactly the order the Java collections do, for the same sequence of operations, including across resizes:
| Calx | Java |
|---|---|
JavaHashMap<K, V>, JavaHashSet<T> |
java.util.HashMap, HashSet, including hash spreading, power-of-two tables, the resize split order and tree bins. Built: Java’s hash maps |
LinkedMap<K, V>, LinkedSet<T> |
LinkedHashMap, LinkedHashSet, and fastutil’s linked tables such as Long2ObjectLinkedOpenHashMap: the order keys went in, which no hashing changes, so it’s Calx’s own table with a list through it, with Java’s putFirst and fastutil’s getAndMoveToFirst and removeLast, and access order written as move_to_last. Built: Linked maps and sets |
SortedMap<K, V>, SortedSet<T> |
TreeMap, TreeSet, and fastutil’s tree tables such as LongAVLTreeSet: key order, which no tree’s shape changes, so it’s Calx’s own B+ tree, with the nearest key on either side as floor, ceiling, lower and higher, and subMap, headMap and tailMap written as a loop over range(a..b). A comparator is a key type with that Order. Built: Sorted maps and sets |
PriorityQueue<T> |
PriorityQueue, which vanilla drains its scheduled ticks from: a binary heap whose array is the order its iterator goes in, so it’s Calx’s own heap that moves the elements as Java’s does, with the iterator’s remove written as remove_marked. A comparator is an element type with that Order. Built: Priority queues |
java.ArrayList<T> |
The same, including growth sizes where they can be seen |
Deque<T> |
ArrayDeque, and the Queue and Deque vanilla keeps in a LinkedList or an ArrayDeque: their order is the order elements went in at each end, which growth never changes, so the native Deque is them. Built: Deques |
OpenHashMap<K, V>, OpenHashSet<T> |
fastutil’s open hash maps and sets, Long2ObjectOpenHashMap, Object2IntOpenHashMap, LongOpenHashSet and the rest, the key type picking the class, with fastutil’s hash mixing (HashCommon.mix), load factors, removal by shifting and shrinking, and the iterator’s remove written as remove_marked, which gives the order fastutil’s loop goes in. Built: fastutil’s hash maps |
Keys hash with JavaHash: Java’s hashCode for each type. It’s built into the numbers, bool, char, rune, text and lists, as Java’s boxes, String and List have it, and a struct or an enum writes its own, as a Java class does: Calcite writes it for vanilla’s types, such as BlockPos. Built: JavaHash.
Native
Section titled “Native”| Type | Use |
|---|---|
List<T> |
A growable array. Built: Lists |
Map<K, V>, Set<T> |
A fast open-addressing hash map, for when order can’t be seen. Built: Maps and sets |
LinkedMap<K, V>, LinkedSet<T> |
A map or a set that iterates in the order keys went in, which is Java’s and fastutil’s too. Built: Linked maps and sets |
SortedMap<K, V>, SortedSet<T> |
A B+ tree map or set in key order, with ranges, which is Java’s and fastutil’s order too. Built: Sorted maps and sets |
SlotMap<T> |
Storage behind Handle<T> |
Deque<T> |
A growable ring, added to and taken from at both ends. Built: Deques |
PriorityQueue<T> |
A binary heap, least first, in Java’s order. Built: Priority queues |
Box<T> |
One value on the heap, for recursive types. Built: Boxes |
BitSet |
Native maps are deterministic. There’s no random hash seed per process, so the same operations always give the same iteration order. Code that iterates a native map by mistake where the order shows still gives the same answer with any thread count and in every replay, even if it doesn’t match vanilla.
Region actors and messages
Section titled “Region actors and messages”Actors are the only concurrency model for game state. Network I/O, chunk loading and worldgen workers are Calcite core services that talk to the tick loop through queues, written with unsafe, and plugins never see threads. Built: std.thread. Thread.spawn(task) needs unsafe and starts a thread that runs a Task, and Receiver<T>() and its Sender<T> ends are typed queues whose messages move between threads. A queue can’t be used inside a parallel for.
Actors
Section titled “Actors”Built: std.mail. An actor is a plain struct that a parallel for runs, holding an Outbox<D, M> for what it sends and an Inbox<M> for what it’s sent. At the barrier, Mail collects the outboxes in ascending sender id and gives the letters back in delivery order. It panics if a sender comes out of order, so the order is checked, not just promised. A message is Send.
- Actors are values that live in a collection, not threads, and spawning one is adding it to the collection.
- There’s no
actorkeyword and nosendstatement. An earlier design had both, withsend dest msgappending to the current actor’s outbox. An outbox the actor holds as a field does the same work without hidden state, and the capture check already keeps each outbox to its own iteration. - The destination type
Dis the library’s: Calcite’s is an enum of the global actor, a region, a chunk withload, and a player withkeep. The library decides how a destination is routed at the barrier, as in the tick sketch.
Parallel loops
Section titled “Parallel loops”Built: see Parallel loops and std.pool. parallel for k in a..b { } runs on the runtime’s pool, each iteration changing only places it indexes with k, and the capture check makes the result the same at any thread count. parallel for x in mut xs { } gives each iteration a mutable view of its own element, and nothing else in the body reaches xs (over the elements). A loop inside another gets the threads the outer one has no work for (loops inside loops). Still to build:
fn step(counters: mut SlotMap<Counter>, bonus: Shared<Config>) { parallel for c in mut counters.values() { c.total += bonus.amount // bonus is Share, so every iteration may read it }}- Going over the values of a slot map or a map with
in mut, as above. parallel(pool) forpicks a pool.- Reading values that are
Sharewhile something else changes the collection they came from, once shared values exist.
Lockstep ticks
Section titled “Lockstep ticks”- The runtime provides the barrier, outboxes and ordered delivery. Calcite’s tick loop decides the phases: packet phase, global phase, world phase, barrier. The loop itself is ordinary code; see the tick sketch.
- Delivery order at a barrier is ascending sender ID, then send order. The global actor is sender 0.
- No hidden timing. Nothing in safe Calx can see time, thread IDs or scheduling order. The clock is an
unsafe fn(std.time), and the wall clock comes into Calcite as a recorded input. So region split and merge decisions can only depend on world state.
Region scoping
Section titled “Region scoping”Region scoping is enforced at compile time, for Calcite and for native plugins, by three rules together:
- Views can’t be stored or sent.
- A
parallel forbody can’t reach outside its element. - A message holds only
Sendvalues.
So nothing one region can name belongs to another. WASM plugins get the same rules checked at run time by the host, since their compiler isn’t Calx’s.
Modules and builds
Section titled “Modules and builds”Built: see Modules and the calx command. A library, built with calx build --lib, exports functions marked @export, which a program opens while it runs and finds by name and type: the type is checked down to every struct’s fields, and the library uses the program’s runtime, so its memory, output and panics are the program’s. That’s what a native plugin is. Still to build:
- A package is a folder with a
calx.tomlgiving its name, version and dependencies. Calcite, each plugin and the standard library are packages, and a package’s modules are found under its own root. - Reaching items through a module’s name:
import std.math, thenmath.floor(x). - Merging instances. Each module’s file has its own copy of the generic instances it uses. Release builds can have the linker fold identical copies (
-ffunction-sectionswith identical code folding). - Targets:
x86_64-windows,x86_64-linuxandwasm32from Phase 3 and Phase 7.aarch64-macoscomes later.
Build speed
Section titled “Build speed”Building and running Calx code has to be fast. The design choices that make it so:
- A fast front end. Lexing, parsing, checking and emitting C use arena allocation and interned names, with no heap allocation per node and no whole-program analysis beyond what monomorphization needs. The front end always rechecks everything, because that’s cheaper than tracking what to skip.
- Clang only where the C changed. Each module becomes its own C file. An object is cached under a hash of the C text, the flags and the clang version, so a module whose C is unchanged is never compiled again, and changed modules compile in parallel.
- The runtime is built once per target and build mode, and reused.
- Debug builds use
-O0with line tables only. Release builds use-O3, and ThinLTO, which optimizes the program as a whole when it links, so calls between modules inline as calls within one do. lld caches each file’s machine code, so after an edit only the files it could change are compiled again. - lld links on both platforms, since it’s several times faster than the default linkers.
- Lean C. Emitted code includes only Calx’s small runtime header, never
windows.hor other large system headers. calx runreuses the last executable when nothing changed.
Build time is tracked like run time: bench/build.py times checking and building a large generated program, calx run on a small one, and rebuilding the tick benchmark after a change. CI runs it on every push, and the results on the reference machine are in bench/results/.
Built: extern "C" declarations and calls, with integers, floats, bool and pointers (C functions). Still to build:
- C’s own type names are in
std.c:c.int,c.size_t, andCStrfor C strings. @export_c("name")exports a Calx function with the C ABI, for C code to call. A Calx plugin doesn’t need it:@exportexports a function with Calx’s own ABI, whose type the host checks when it finds it.externis allowed only in trusted modules, the same rule asunsafe. A WASM build has noexternexcept the host’s imports.- Struct layout follows C’s rules for
@repr(c)structs. Other structs’ layout is the compiler’s choice.
Codegen hooks
Section titled “Codegen hooks”Three kinds of generated code:
Library-defined attributes
Section titled “Library-defined attributes”Attributes like @packet, @command, @save and @message are defined by libraries, not built into the compiler. Built (attributes): an attribute is a Calx function marked @attribute, which the compiler runs while it compiles. It reads the declaration it’s on, as a StructDecl, EnumDecl or FnDecl from std.meta, with literal arguments and the attributes on its fields, and returns the code to add as text, which is checked like the rest and shown by calx expand. The compiler builds the attributes into a native program of their own and caches their answers. The compiler itself knows @track_caller, @export and @attribute.
/// Declares a packet. Generates its reader, its writer and its ID lookup.@attribute("varint", "max")pub fn packet(decl: StructDecl, id: i32, state: State, bound: Bound) -> Generated ! String { var g = Generated.new() g.add(try read_impl(decl, id, state, bound)) g.add(try write_impl(decl, id, state, bound)) return g}Still to build:
- Build inputs. An attribute may read files its package lists, such as the protocol report that gives packet IDs, and the cache key includes their text. Until then an attribute is expected to depend only on what it’s given, which the compiler doesn’t enforce.
- Attribute blocks.
name { declarations }at the top level of a module applies the attribute@nameto every declaration inside it. Calcite’s plugin library definesregionandglobalthis way, so the plugin structure in the samples isn’t built into the language. - Hidden parameters. An attribute on a function may add parameters the caller supplies and bind names in the body. That’s how a plugin handler sees
region,configandserverwithout declaring them: the host passes them in. - String prefixes like
t"..."work the same way: a function marked@prefix("t")gets the literal and its interpolations at compile time and returns an expression. The Calcite text library checks the tags this way.
Generated data tables
Section titled “Generated data tables”Phase 2’s converter writes vanilla’s data as neutral JSON tables: block states, registries, items, packet IDs and the command tree (Calcite’s data generator docs). Calcite’s build turns them into Calx source, which compiles like any other module. That needs nothing special from the compiler, only that large constant arrays compile quickly. Block states are stored as arithmetic, each block’s first state ID and its properties’ value counts, so the tables stay small: 1,286 blocks and 121 properties on 26.3, not 35,723 rows.
Debugging
Section titled “Debugging”Built: the emitter writes #line directives and both build modes keep line tables, so debuggers and profilers on Windows and Linux show Calx source lines, not the generated C (build modes). Editors get errors as you type (editor support). A panic writes its Calx stack trace, inlined calls, parallel for bodies and the libraries the program opened included (stack traces).
Non-goals
Section titled “Non-goals”What Calx leaves out, on purpose:
- No garbage collector, no reference counting in safe code except
Shared<T>, and no lifetimes. Ownership and second-class views cover what Calcite needs. - No null and no exceptions. Optionals and error values instead, and panics for bugs.
- No classes or inheritance. Structs, enums and interfaces.
- No implicit conversions, numeric or otherwise, and no implicit copies of anything that owns memory.
- No threads, locks or atomics in safe code. Only
parallel for, and actors that it runs. Starting a thread takesunsafe(std.thread), and threads share no memory, only queues. - No global mutable state outside
unsafe. Constants are fine. - No macros beyond attributes and string prefixes, which are typed and run as Calx.
- No reflection at run time. What plugins need from it, such as encoding saved data, is generated at compile time.
- No
async/awaitin v0. It may come back for plugins that wait across ticks (samples README). - No undefined behavior outside
unsafe. Every case C leaves undefined is either checked or defined above.
Must checklist
Section titled “Must checklist”| Must (from the plan and decision log) | Settled in |
|---|---|
| Generics and interfaces, monomorphized into C | Generics, Interfaces |
| No null: absence is an optional type | Optionals |
| Handle types for game objects, checked on use | Handles |
| Signed overflow wraps | Integers |
Shift counts masked, >>> exists |
Integers |
MIN / -1 and MIN % -1 |
Integers |
| Integer division by zero: pick a behavior | Integers: panics |
| Float to int saturates, NaN to 0 | Floats |
char unsigned 16-bit, byte signed |
Types |
| IEEE floats, no contraction, no fast-math | Floats |
Transcendentals: std.java.math ported from fdlibm, matching StrictMath bit for bit, and a faster std.math |
Transcendental functions |
Memory-safe outside unsafe |
Memory safety |
unsafe and extern only for Calcite core and trusted plugins |
unsafe, FFI |
The emitter proves non-aliasing and emits restrict |
Memory safety |
| A plugin panic stops only that instance | Panics |
| No leaked memory when an instance stops | Arenas and plugin memory |
UTF-8 strings, Java hashCode, UTF-16 length, Modified UTF-8 |
Strings |
Java-compatible HashMap, HashSet, LinkedHashMap and fastutil maps |
Java-compatible |
| Fast native collections where order isn’t observable | Native |
| Actors are the only concurrency model | Region actors |
| Lockstep ticks | Lockstep ticks |
| Phases and delivery order | Lockstep ticks |
| Same result with one thread and many | Parallel loops |
| Split and merge depend only on world state | Lockstep ticks: no hidden timing |
| Region scoping enforced at compile time | Region scoping |
| Each module is its own C translation unit | Modules, Modules and builds |
FFI: extern, C ABI, what trusted plugins unlock |
FFI |
Codegen hooks: packets, data tables, #line |
Codegen hooks |
| What Calx leaves out | Non-goals |
| Stricter types than Rust | Strictness |
Fast builds and fast calx run |
Build speed |