16. The Base boundary
16.1 What a Luce module sees of a Base module#
A Luce module imports a Base module by the same import, a .lucb file under the source root, a public module of a Base package it depends on, or one of Base's standard modules (import io), which luce-base describes from the source it carries; a sandboxed program imports no Base module. It sees the module's pub functions, pub let constants, pub structs and integer-backed enums whose fields are crossable, and pub handle types, through the description luce-base prints for the module (Luce Base §17.7): the compiler never parses Base. It does not see pointers, spans, arrays, unions, atomics, c types, extern declarations, generic declarations, or any function, constant or struct whose signature mentions one of those: those are the Base package's own, and the package writes the function a Luce program can call. A program that imports a Base module is built; the interpreter runs Luce alone and refuses it (§17.1).
The current description begins with description 9; a mismatched compiler is rejected before declarations are read. There is one current format. Field mutability and default availability are explicit in the records. Named arguments keep their parameter association. Omitted defaults are evaluated by Base in their original module; supported caller facts name the Luce call site. Nested tuples and optionals preserve declaring-module identity and native scalar conversions.
Imported structs use Type(args) to call the actual Base initializer. Aliases keep that constructor and the canonical type identity. Instance and static methods are callable directly or as function values. Mutating value methods require a var; a bound value method keeps its own copy, including private state. A mutation made before a recoverable error is preserved.
A native value retains its complete representation and owns copies of supported public text/data fields. Before a native call, those fields are rebased into a fresh native copy. Private scalar state survives copying, equality and worker transfer. A hidden borrow requires an explicit ownership contract and is unavailable as a copied value. A private initializer prevents construction but allows returned values and their public methods. Unsupported methods are unavailable individually; an unsupported initializer cannot become an implicit memberwise constructor.
Base structs declared with interop.Type[T] import as owning Luce objects. An object's close() is Base's own close when that takes nothing and cannot fail; otherwise it is the dispose function its interop.Type names, whatever Base calls it, and a Base close that fails or takes arguments stays Base's own. Aliases and bound methods share one native owner. Real initializers run in stable allocated storage; failed construction releases that storage. Explicit close is visible through every alias, while active native calls finish before disposal. Other methods and public fields reject closed access. A garbage cycle's native objects are disposed after every deinit in it has run, each before the objects it refers to, so a dispose function may still use what its object holds.
interop.ViewType[T] imports a nonconstructible borrowed reference with a checked lease. Retaining a view, binding a method or capturing it keeps validity checks alive without extending the valid period. After the Base owner ends the lease or closes, access fails; is_valid() still reports the state. Returned text is copied into an owned Luce string and can outlive the view. Native owners and views cannot be transferred to workers.
Native interfaces dispatch to Base or Luce implementations with shared ownership. Base uses interop.Interface[I] to retain an actual native witness. A Luce class can implement that interface and be retained in a Base container. Native conformers keep their real witness and complete native storage. Class-backed interface values preserve the concrete object's identity, including comparisons through another interface or the concrete class. Value-backed interfaces own a copied value.
A native interop.Owned[T] result appears as T; interop.Outcome[T] appears as T!. These carriers retain borrowed result storage and dynamic failure text until the receiver consumes/releases them. Luce implementers require these contracts for borrowed or fallible native results. This lets Base handle a managed failure without leaving a pending Luce error allocation. Retained interfaces trace their owners, so mixed native/managed cycles can be collected. Interface views keep their source lease and reject access after expiry.
16.2 Crossable types#
| Luce | Base | Crossing |
|---|---|---|
int | i64, and i8 to u64, isize, usize | by value; an int outside a narrower type's range traps as it crosses in, and a u64 or usize past int's range traps as it crosses out |
float | f64, f32 | by value; an f32 is rounded on the way in |
bool | bool | by value |
str | str | lent as Base's view of the bytes; a Base result is copied into an owned str |
str | c.str | lent as a zero-terminated copy that lives until the end of the calling statement; a text holding a zero byte traps; a result is copied |
bytes | const u8[] | lent; a Base result is copied |
list[T] of int, float, bool, str or handles | const T[] | lent for the call; texts and handles use temporary converted arrays. Base must retain individual resources it keeps; a borrowed Base span never crosses back |
list[T] | interop.Owned[const T[]], as a result | the elements converted into a new list, then the owner released |
| struct with copyable native storage | the same struct, declared in Base | complete native value plus owned public text/data |
| declared owned object | interop.Reference[T] | shared native owner; parameter borrows and result transfers a reference |
| declared borrowed view | interop.View[T] | shared checked lease; no public construction |
| native interface | interop.Interface[I] | witness and explicit owner/lease; both implementation directions |
T, T! | interop.Owned[T], interop.Outcome[T] | explicit backing storage and owned failures |
(T, U) | (T, U) | each member crosses recursively |
integer-backed enum declared in Base | that enum | by value |
T? | T? | by value |
T! | T! | the code and the message |
func(A) -> R of scalars, str and bytes answering a scalar or nothing | func(A) -> R | a named function, never a closure; Base calls a thunk that copies the texts for the call |
| handle | pub handle | as an object (§16.4) |
Nothing else crosses in either direction. A list of narrower integers is copied into a converted array for the call. A Base enum value that names no declared case traps as it crosses.
Public handles retain their declaring module's identity when another Base module uses them in a signature or exports a type alias. Lists of handles are supported as function arguments, not aggregate fields or callback parameters. Closed handles are checked while unwrapping each element, just as for an individual argument.
The project's def native inputs (sources, search paths, libraries, frameworks and pkg_config requirements) are merged into compiled and explicitly emitted Base workspaces; an absolute source or search path keeps the declaring package's root.
16.3 Errors and traps#
A Base T! failing is a Luce failure with the same code and message. A Base trap is a Luce trap. A Base function that returns unit returns unit.
16.4 Handles#
# in base: files.lucb
pub handle File:
destroy close
pub func open(path: str) -> File!: ...
pub func read_line(file: File) -> str?: ...# in luce
with files.open(path) as file:
while let line = files.read_line(file):
print(line)A Base module declares a handle for a resource it owns: a pointer-sized value whose destroy names the Base function that releases it. A Luce program sees the handle as a class with identity, no fields, and a close() that calls destroy once; the runtime calls it at the last reference if the program did not. Calling the declared destroy function from Luce is another spelling of close(): it shares the same closed state, including through aliases of the handle, and repeated calls do nothing. A handle is never constructed in Luce, only answered by the module's functions; a closed handle handed back to Base traps. Opaque APIs may use this form; ordinary Base value APIs use their structs, constructors and methods.
16.5 What Base sees of Luce#
Nothing. A Base module cannot name a Luce declaration, call a Luce function, or hold a Luce object; a callback into Luce is a capture-free Luce function passed as a function value.