18. Working with full Luce
This chapter is the contract between a full Luce program and the Base modules it imports. It names full Luce types; a reader who does not use full Luce may skip it. Full Luce's own definitions are in its specification; the one-line glosses here are enough to read the tables.
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 to be reported by luce build --costs, the cost report (planned).
18.2 Shared types#
| Base | Full Luce | Gloss | Where full Luce may use it |
|---|---|---|---|
T* | native_mut_ptr[T] | full Luce's raw pointer | .lucn modules only |
const T* | native_ptr[T] | raw read-only pointer | .lucn modules only |
void*, const void* | foreign | untyped raw pointer | .lucn modules and extern signatures |
T*? | native_mut_ptr[T]? | nullable raw pointer; the null niche in both | .lucn modules |
func(A) -> R | cfunc(A) -> R | C function pointer | anywhere cfunc is admitted |
T[N] | array[T, N] | fixed array | anywhere |
integer-backed enum | export c enum | enum with a fixed integer representation | anywhere |
Types with two representations, crossed only through adapters: str (a view here, an owned reference-counted string there); Error; T[] against slice[T] (an owner-retaining view) and list[T] (a growable collection); interface views against interface values (boxed). usize and isize convert (§18.4). Types that never cross: c.str, union, @T, volatile T*, fmt, 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, c.str, 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 check that a value may be sent to a worker, and the compiler names the field that fails.
18.4 Full Luce calls Base: parameters#
| Base parameter | Full Luce argument | Crossing |
|---|---|---|
plain T | plain T | by value |
usize / isize | u64 / i64 | by value; a checked narrowing that traps on a 32-bit target |
const T[], T plain | slice[T], list[T], array[T, N], bytes | lent: the existing dense storage is viewed, nothing is copied |
T[], T plain | list[T] | lent under the list's mutation guard: structural change through any alias traps until the call returns, and element storage does not move |
str | str | lent: the owned string's bytes are viewed |
c.str | str | lent as a NUL-terminated temporary |
| interface view | a conforming value, class instance, or interface value | lent: the payload's address and the same static witness table; for a conformer whose implementations change their receiver (§14.3) the argument must be a var or a class instance |
func(...) -> R, plain signature | a capture-free function or lambda | by value |
T*, const T*, void*, T*? | the native pointer from a .lucn module | by value; a safe module cannot produce one |
union, @T, volatile T*, fmt | nothing | rejected |
"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 result | Full Luce receives | Crossing |
|---|---|---|
plain T | T | by value |
usize / isize | u64 / i64 | exact widening |
str | owned str | copied 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*?, func | the native type | only into a .lucn module |
T[], c.str, union, @T, interface view | nothing | rejected: no owner for a view |
The rule in both tables is the same: 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, the method full Luce runs when the last reference is released, 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. A .lucn module obtains one from a class reference with native.retain(object) -> foreign, which increments the reference count and hands out the address; reads it back during a callback with native.borrow[T](handle) -> T, a reference valid for the callback; and ends the ownership with native.release[T](handle) -> T. These three intrinsics 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, memory.heap is the runtime's heap, and it is the current allocator for Base code called from full Luce. 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 required: 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 requires no copy 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 differ in representation, str and Error, is handled by adapters rather than by treating the representations as the same.
18.13 The runtime#
The runtime that implements full Luce's classes and collections becomes a Base package. The two intrinsics only it may use, the arena provider that asks the backend for committed memory and the storage-service binding through which generated code reaches the allocator, stay sealed to .lucn modules under its package identity; everything else it does, it does in ordinary Base. The port needs only pointers, spans, globals, and unions, which are the first Base slices to be implemented, and it is the first real Base program: it must compile freestanding, produce the machine code the current tier produces, and pass the differential corpus that already checks every runtime behaviour through three executions. Exposing the runtime's heap to other Base code as memory.heap needs interface views (§14.3) and comes after.