ADR 0007 — M8: hybrid core library, raw pointers, unsafe contexts
- Status: Accepted (project owner, 2026-10-24, when starting M8: "Hybrid" for the core library). Decisions 2–5 are the agent's defaults under that instruction, following the pattern of ADR 0004; the owner may revise them.
- Scope: how M8 provides the core library, raw pointers and unsafe code (
milestone-8.md). FFI, profiles and MMIO details are specified with their increments (8b–8d).
Context
Master prompt Milestone 8 asks for a core library, freestanding compilation, a C FFI, raw-pointer unsafe operations, custom entry points and target capabilities. Every library type today (Option, Result, Box, Vector, Console, File, …) is built into the compiler and the C runtime (ADR 0003, 0005); Nova has no library source files. The grammar reserves unsafe and extern, and ownership.md U1–U3 sketch the rules, but nothing is specified or implemented.
Decision
Core library: hybrid (owner). The existing built-in types stay built in. The "split" is a per-profile capability table (hosted-only names such as
Console,FileandSystemHeapare rejected in freestanding code) plus the runtime split into a freestanding core and a hosted part. New systems types that unsafe Nova can express (Volatile<T>,MmioPtr<T>,PhysAddr, memory helpers) are written in Nova, in alib/corebundled with the compiler (8c, 8d). Rejected: everything built in (lib/corewould stay empty, and safe wrappers over unsafe code would be exercised only by tests); porting the M3–M6 built-ins to Nova (a large rewrite with regression risk, not needed for C4).Raw pointers are
*Tand*mut T, Copy, with no loans (U3). Making and converting them is safe; using them is unsafe. Conversions useas, so no new operator is needed; the operations are methods (p.read(),p.write(v),p.add(n)) andPtr.*/Slice.fromRawbuilt-ins. There is no*pdereference operator, since Nova has no prefix dereference at all (references and boxes auto-dereference). Rejected: a*pplace operator (new syntax, and*is already multiplication);nullas a pointer literal (nullis the optional's empty value, §22.1); the null pointer is0 as *T.Pointer reads and writes need a Copy pointee in v0. Moving a value out of or into raw memory would need the drop elaborator to track raw places. It can come later, with the allocator interface.
Unsafe contexts are
unsafe { }blocks,unsafe { e }expressions (Ex17:let n = unsafe { write(…) };, since Nova has no block expressions) andunsafe fnbodies. TheSafety:doc comment (U2) is an error when missing, not a warning: Nova has no warnings, and the rule is cheap to satisfy.Generated C has no undefined pointer arithmetic: raw pointers are plain
T *(noconst), and arithmetic and casts go throughuintptr_t(unsafe.md§4).FFI (8b):
- Extern declarations name C types exactly, through the mapping of
type-system.md§28.3 (usizeissize_t,*Tisconst T *, and the pointee-onlyCCharischar), and calls go through generated wrappers. Calling a C function through an incompatible prototype is undefined in C11, and clang and GCC reject redeclared library functions with other types. @exportalone makes a function C-callable. Ex17'spublic extern "C" fnspelling is not used: the attribute already says everything, and anexterndefinition would read like a declaration.@repr(C)structs keep their Nova names in C, so C code usingnova_exports.hhas the same struct type. Arrays and other non-FFI-safe types are not allowed in them yet.- The interpreter models a few C library functions and faults on any other C call.
- Function pointers move to 8d: a C callback needs a C-typed wrapper, which exports provide.
- Extern declarations name C types exactly, through the mapping of
Systems capabilities (8d):
lib/coreis Nova source embedded innovacand part of every package; unused parts are not emitted, so programs that do not use it compile to the same C as before.Volatile<T>is a cell written throughborrow mut this, not an interior-mutable cell: writing a register needs exclusive access to the register block, whichMmioPtr<T>.getMut()gives.- Only C function pointers exist (
extern "C" fn(…)); their values are extern and exported functions, whose C addresses already exist. Calling one is unsafe. Target.isX86_64()and friends are decided by the C compiler, because one generated C file serves every LP64 target; there is no conditional compilation in Nova yet.
Consequences
ownership.mdU3'sborrowFromRaw(p)is spelledPtr.toBorrow(p)/Ptr.toBorrowMut(p)(borrowis a keyword, soPtr.borrowwould not parse).- The interpreter becomes the dynamic oracle for unsafe code too (null, dead, out-of-bounds, misaligned, uninitialised accesses), but it does not track loans.
- M5's rule 1c (address exposed to unsafe) is met conservatively: a function that turns references into pointers keeps its boxes on the heap.
- Pointee types excluded type parameters in 8a. 8d lifts this (
lib/core'sVolatile<T>needs*T); a scoped type argument for such aTis N0274, as for boxes.