Luce Base
LuceEngineeringLuciaOS

18. Working with full Luce

This chapter is the contract between a full Luce program and the Base modules it imports. It is written so that the compiler enforces every rule and so that a reader of either tier can predict what a crossing costs.

18.1 One program, two representations#

A full Luce program that imports a Base package compiles both into one intermediate representation. A call from full Luce into Base whose signature uses only shared types (§18.2) or plain types (§18.3) is an ordinary call: no thunk, no marshalling. A call whose signature involves a type with two representations, str, Error (and so every T!), spans, or interface views, goes through an adapter the compiler generates at the call site, which lends or copies as §18.4 and §18.5 state. Every adapter is reported by the cost inspector.

18.2 Shared types#

BaseFull LuceWhere full Luce may use it
T*native_mut_ptr[T].lucn modules only
const T*native_ptr[T].lucn modules only
void*, const void*foreign.lucn modules and extern signatures
T*?native_mut_ptr[T]?.lucn modules; the null niche in both
func(A) -> Rcfunc(A) -> Ranywhere cfunc is admitted
T[N]array[T, N]anywhere
integer-backed enumexport c enumanywhere

Types with two representations, crossed only through adapters: str (view here, owned there); Error; T[] against slice[T] and list[T]; interface views against interface values. usize and isize convert (§18.4). Types that never cross: cstr, union, @T, volatile T*, thread_local globals.

18.3 Plain types#

A type is plain when its representation is copied data with no reference identity and no pointer: scalars, bool, char; enums, tuples, arrays, structs, and optionals of plain types; function pointers with plain signatures. usize, pointers, spans, str, cstr, unions, @T, interface views, and every runtime-dependent full Luce type are not plain. Plain is a compiler-derived marker constraint, derived structurally with the same machinery as full Luce's sendability check, and the compiler names the field that fails.

18.4 Full Luce calls Base: parameters#

Base parameterFull Luce argumentCrossing
plain Tplain Tby value
usize / isizeu64 / i64by value; a checked narrowing that traps on a 32-bit target
const T[], T plainslice[T], list[T], array[T, N], byteslent: the existing dense storage is viewed, nothing is copied
T[], T plainlist[T]lent under the list's mutation guard: structural change through any alias traps until the call returns, and element storage does not move
strstrlent: the owned string's bytes are viewed
cstrstrlent as a NUL-terminated temporary
interface viewa conforming value, class instance, or interface valuelent: the payload's address and the same static witness table; for a mutating requirement the argument must be a var or a class instance
func(...) -> R, plain signaturea capture-free function or lambdaby value
T*, const T*, void*, T*?the native pointer from a .lucn moduleby value; a safe module cannot produce one
union, @T, volatile T*nothingrejected

"Lent" means the callee may read (or, for T[], write elements of) the storage until it returns and may not retain the pointer. Retaining it is undefined by contract. The compiler cannot check the callee; it reports the crossing.

18.5 Base returns to full Luce: results#

Base resultFull Luce receivesCrossing
plain TTby value
usize / isizeu64 / i64exact widening
strowned strcopied into a fresh owned string by the adapter; the allocation is reported
T!T!T by the rules above; on failure the adapter builds a full Luce Error with the same code and a copied message
T*, const T*, void*, T*?, functhe native typeonly into a .lucn module
T[], cstr, union, @T, interface viewnothingrejected: no owner for a view

The asymmetry is the whole design: owned values are lent into Base; views are copied out of Base. Nothing crosses that could dangle in safe code.

18.6 Wrapping a Base resource for safe Luce#

A Base type that owns memory (an arena, a parser, a device) is exposed to safe Luce the way a C library is: a .lucn module holds the Base pointer in a final class whose deinit calls the Base destroy function, and whose methods lend and copy by the tables above. Base does not need to know it is wrapped.

18.7 Base calls full Luce#

A Base module cannot name a full Luce declaration. It reaches full Luce only through function pointers it was handed, and only inside a program that contains the runtime. A full Luce module converts a capture-free function with a plain signature to a Base func and passes it down. State crosses as a void* context that a .lucn module obtains from a class reference with native.retain_handle(object) -> foreign, reads back during a callback with native.borrow_handle[T](handle) -> T, and releases with native.release_handle[T](handle) -> T; these are the bridging-retain pattern and are sealed to .lucn modules. The callback runs synchronously on the thread that entered Base. A thread Luce never entered may not call full Luce; the runtime stops the process when it detects one. A Base artifact built without the runtime cannot reach full Luce at all, and reachability analysis reports the first full Luce function reached with its call path.

18.8 Globals#

A pub var in a Base module is not accessible from full Luce, which has no mutable globals; Base exposes accessor functions. A pub let is accessible when plain.

18.9 Allocation#

Inside a full Luce program the runtime's heap is available to Base as runtime.heap(), an Allocator. Memory Base allocates from it belongs to Base structures; full Luce sees those only through §18.6 wrappers. Memory owned by full Luce is never freed by Base.

18.10 Errors and traps#

ErrorCode is one type in both tiers. Error is two types with one meaning, converted by the T! adapter. A trap in Base inside a full Luce program is full Luce's trap, with the same reporter and trace. A trap in a freestanding Base artifact is reported by the Base trap reporter and ends the process.

18.11 Generics and interfaces across the boundary#

A full Luce generic instantiated with a plain Base type is ordinary. A Base generic called from full Luce is instantiated with a type argument that has a Base spelling or is plain, and with nothing else; a Base first[T](items: const T[]) called with a list[Point] instantiates T = Point and lends the list as the span. A full Luce generic cannot be instantiated inside Base. An interface declared in either tier may be implemented in either when every requirement signature uses plain or shared types; static dispatch crosses free, and the witness table a Base view dispatches through is the same static table full Luce's boxed values use. Display and Writer are the exceptions: their Base signatures are not plain, so a full Luce conformer or sink reaches Base through a .lucn adapter.

18.12 Why the contract is shaped this way#

Two properties were non-negotiable: Base must need nothing from the runtime, and safe Luce must never receive a value that can dangle. Everything else follows. Lending owned values into Base costs nothing because the runtime already stores every plain element type densely at its C width. Copying views out of Base costs an allocation, which is reported, because the alternative is a view into storage nobody owns. The one place the two tiers genuinely differ in representation, str and Error, is handled by adapters rather than by pretending the representations are the same; an earlier draft claimed "same type, no thunk" and the compiler's own type table proved it false.

18.13 The runtime#

The runtime that full Luce's classes and collections stand on becomes a Base package. The two intrinsics only it may use, the arena provider and the storage-service binding, stay sealed to .lucn modules under its package identity; everything else it does, it does in ordinary Base. That migration is scheduled after every other Base slice, because it depends on the adapters of §18.1 and the views of §14.3.