1. Introduction
Luce Base is C with a different organisation. It keeps the properties that make C the language other languages are implemented in: values with predictable layout, pointers, manual memory, a plain calling convention, and no runtime library (§1.3). It replaces the parts of C that are accidents of its history: header files, the preprocessor, null, integer promotion, switch fallthrough, return-code error handling, and void* generics.
A Base program is made of structs with functions and initialisers inside them, modules instead of headers, generics instead of macros, tagged unions with exhaustive matching, interfaces, defer, optionals instead of null, and a fallible result type instead of return codes. Memory is managed by hand: new and free are part of the language and go to an allocator the program chose. There is no reference counting, no garbage collector, and no hidden allocation.
Base is a profile of the Luce language, compiled by the same compiler as full Luce. A Base module is a file ending in .lucb. Full Luce, with its reference-counted classes and collections, can import a Base module as an ordinary module; Chapter 18 states that contract. A Base program that never touches full Luce needs nothing from it.
The three sentences that summarise the design:
Base gives up nothing that portable C can do. Where a C capability is unsafe, Base admits it with a restriction that makes it checkable; it never omits it.
Values copy. Pointers point and are never null unless they say so. Spans carry their length. Allocation is written. Arithmetic, bounds, and shifts are checked. Failure is visible. Nothing runs that you did not write.
A better C with Luce's syntax, that calls C in both directions without adapters, and is the language Luce's own runtime is written in.
The rule that decided most spellings: Base is allowed to be safer than C, but never slower and never more convoluted. Where a check costs one predicted branch, Base has it and marks the unchecked form. Where a check would cost more than that, or would change what the hardware does, C's semantics stand.
1.1 How to read this document#
Each chapter states its rules first, then the reasons under the heading Why. The rules are normative. The reasons are there so that the next person to change a rule knows what the rule was for. Chapter 21 is the grammar; Chapter 22 is a one-page translation table from C; Chapter 23 lists the places where Base and full Luce differ and why; Chapter 24 is a set of complete programs.
Code in this document is Base source unless marked otherwise. A fenced block marked c is C, shown for comparison.
Builtin names are single words, and few. The language's own operations are keywords (new, free, with), operators (+%, +?), or one of four core functions (print, assert, error, trap). What else the compiler answers lives in a standard module and is imported like the rest of it: C's sizeof is memory.size_of, formatting into a buffer is strings.format. Compile-time source facts live on the luce module (luce.location, luce.file, luce.line, luce.function) and are replaced at the use site, not called. A compound name such as format_into never appears in the builtin surface; user code may name things as it likes.
1.2 What it looks like#
let past_end = ErrorCode.package(1)
struct Cursor:
var data: const u8[]
var offset: usize
func advance(count: usize) -> !:
if self.offset + count > self.data.length:
error(past_end, "advance past the end of the input")
self.offset += count
func remaining() -> const u8[]:
return self.data[self.offset..]
func first_line(text: str) -> str:
var cursor = Cursor(data = text.bytes, offset = 0)
while let byte = cursor.remaining().first():
if byte == '\n': break
cursor.advance(1) catch failure:
recover ()
return (str)text.bytes[0..<cursor.offset]Everything in that example has a C counterpart and costs what the C would cost on the native backends. The span const u8[] is a pointer and a length. offset + count traps on overflow instead of wrapping. error(...) returns a two-word error value; catch handles it. The final cast reinterprets bytes as text without a check, because the bytes came from a str. Nothing allocates.
1.3 The runtime, and why Base has none#
Every language links some code into every program it compiles, code the programmer did not write but the language's features depend on. C's is small: the startup code that calls main, and malloc, printf, and the rest of the C library. Full Luce's is larger, because full Luce has features that need code to exist at execution time. This document calls that code the Luce runtime. It is a library, written in Luce, that provides:
- Reference counting. A
classvalue in full Luce is a heap object with a count of the references to it. Every copy of the reference increments the count, every release decrements it, and the object is freed when the count reaches zero. The code that allocates the object, adjusts the count, tracks weak references, and runsdeinitis the runtime. - Collection storage.
list,map,set, and the ownedstrandbytesare growable heap structures. The code that grows them, copies them on write, hashes keys, and keeps their iteration guards is the runtime. - Closures. A closure that captures a local needs an environment that outlives the frame; the runtime allocates and reference-counts it.
- Workers.
spawncopies a value graph into an isolated worker; the runtime does the copying, scheduling, and joining. - Interface values. A full Luce interface value boxes its payload; the runtime allocates the box and detaches it before mutation.
- The heap. Underneath all of the above, an allocator over a memory arena the backend provides.
- Trap reporting. The code that prints a trap's message and source trace and stops the program.
Full Luce needs all of this because its central promise, values copy and references share identity with automatic lifetime, is implemented by it. A full Luce program cannot run without the runtime any more than a C program can run without its C library.
Base has none of those features. There are no classes, so nothing is reference-counted. There are no built-in collections; a Base list is a library type that allocates with new from the allocator it was given (§12.3). There are no closures, no workers, and no boxed interface values. There is no hidden heap; every allocation is written as new or alloc. What remains from the list above is trap reporting, and that is a few hundred bytes.
So a Base executable links what a C executable links: a startup shim that builds main's arguments, sets the initial allocator, and calls main; a trap reporter; and, unless the program is built --freestanding, the host C library for process start and output. There is no runtime library. This is not a restriction placed on Base; it is a consequence of the features Base does not have, and the compiler checks it: a Base artifact that reaches any function needing the Luce runtime fails to build, naming the function (§16.2).
The relationship runs the other way too. The Luce runtime is itself written in Luce, today in the audited native tier (§16.2), and the plan is for it to become a Base package (§18.13): the code that implements reference counting and collection storage for full Luce is code of the kind Base is designed for.
Two uses of the word are kept apart in this document. The Luce runtime is the library above. At runtime means at execution time, as in "checked at runtime", and says nothing about the library.
1.4 Glossary#
Terms this document uses with a fixed meaning, each defined again where it first matters.
- Tier, kind: one of the three module kinds, safe Luce, native Luce, or Base, told apart by file suffix (§16.2).
- Profile: Base as a mode of the Luce compiler, selected by the
.lucbsuffix. Not to be confused with a build profile, the choice between the default and diagnostic builds (§19.4). - Span: a pointer and a length,
T[](§5.4). - View: a value that refers to storage it does not own: a span, a
str, an interface view. - Niche: a bit pattern that is never a valid value of a type, reused to represent
noneso that the optional costs no extra word. Address zero is the niche of every pointer type (§5.3). - Zeroable: a type whose all-zero bit pattern is a valid value (§6.1).
- Plain: a type whose representation is copied data with no pointer and no reference identity (§18.3).
- Lent: passed for the duration of one call, to be read and not retained (§18.4).
- Witness table: the static table of function pointers through which an interface view dispatches (§14.3).
- Current allocator: the thread-local allocator that
newandfreeuse unless told otherwise (§12.3). - Slot: a parameter or result position in a signature that crosses the C boundary (§17.1).
- Sealed: usable only by the runtime package, under its own identity (§18.13).
- Recipe: the file
luce bindreads for the facts a C header does not state (§17.5).
1.5 Failure in four sentences#
Failure appears from the first example on and is specified in Chapter 11. The short version: a function whose result type ends in ! returns either its value or an Error. try e checks every fallible operation in the expression e, yielding its success value or forwarding its error to the nearest enclosing handler or the current fallible function. error(code, message) returns an error. e catch name: handles the error of e in place, and the handler must recover a value, return, or terminate.