Luce
Luce BaseSourceLuciaOS

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. 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 (base.md §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 7; 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. 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.

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#

LuceBaseCrossing
inti64by value
floatf64by value
boolboolby value
strstrlent as Base's view of the bytes; a Base result is copied into an owned str
bytesconst u8[]lent; a Base result is copied
list[T] of int, float, bool, str or handlesconst T[]lent for the call; texts and handles use temporary converted arrays. Base must retain individual resources it keeps; a Base span never crosses back
struct with copyable native storagethe same struct, declared in Basecomplete native value plus owned public text/data
declared owned objectinterop.Reference[T]shared native owner; parameter borrows and result transfers a reference
declared borrowed viewinterop.View[T]shared checked lease; no public construction
native interfaceinterop.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 Basethat enumby value
T?T?by value
T!T!the code and the message
func(A) -> R of scalars, str and bytes answering a scalar or nothingfunc(A) -> Ra named function, never a closure; Base calls a thunk that copies the texts for the call
handlepub handleas an object (§16.4)

Nothing else crosses in either direction. A usize, u32, or u64 in Base is an int in Luce, and a negative or oversized value traps at the crossing; a list of them 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 [native] libraries, frameworks, and pkg_config requirements are preserved in compiled and explicitly emitted Base workspaces. Native source and search paths currently require a Base build and are rejected explicitly by Luce.

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.