Cinch runtime-fault inventory
Status: inventory taken 2026-10-29 by a read-only scout over the tree at
the time (before slices S1/S2 were integrated; line numbers drift). It
feeds design/no-faults.md.
Closed since the inventory
- Guards and literal matches (rows 70 and 140 below, item 1 of the
escape-hatch list): closed 2026-10-30. A
casewhose rows can all fail by guard, and a literal match without a catch-all, are compile errors; the pattern compiler is the coverage check and can no longer emitCore::Fault, andrun_outis deleted.specifications/surface-grammar-local-amendments.mdsection 16,tests/guard_coverage.rs, CONFORMANCE D401. - Resource exhaustion (all of section 5 except the I/O row): closed
2026-10-30. The heap cap (now on live bytes), a real allocation
failure, a native stack overflow on the program’s or a task’s thread,
the evaluator’s nesting bound, and a refused thread are one defined
outcome: exit 73 with
cinch: exhausted: heap (<n> bytes),cinch: exhausted: stackorcinch: exhausted: threads(design/no-faults.mdsection 4.1,compiler/runtime/src/stack.rs,tests/exhaustion.rs). Nothing in the runtime aborts or dies on a signal for resource reasons any more. - Division and remainder by zero (
DivByZero, thenat-div/nat-rem/int-div/int-remrows below): closed 2026-10-30. The divisor is aNonZero Nat/NonZero Int(type NonZero a = refine (n :: a) with NonZero n, declared inAlgebra.Arithmetic), and so is theDiv/Remclasses’ ((/) :: a -> NonZero a -> a). A numeral divisor decides its predicate at compile time with nothing written (0is a compile error); any other divisor is decided (decide (NonZero d), a plainif d == 0guard,non-zero/non-zero-nat) or threaded as aNonZeroparameter, andchecked-div/checked-remanswerNone. The runtime’s zero check is a compiler-bug trap (exit 71,compiler bug:), exit 72 andcinch_fault_div_zeroare gone.tests/safe_division.rs. Natsubtraction below zero (NegativeResult, thenat-suband-atNatrows below): closed 2026-10-30.nat-sub :: using (b <= a) => (a :: Nat) -> (b :: Nat) -> Nat, and theSub_Natinstance states the same obligation in a method signature inside the instance block ((-) :: using (y <= x) => (x :: Nat) -> (y :: Nat) -> Nat), so theSubclass and-atIntare unchanged. Numerals decide the obligation at compile time (3 - 5is refused naming5 <= 3); any other subtraction takes it from a branch fact,Decevidence, or the enclosing definition’s ownusingobligation. TheSub_Natdictionary never reaches generic code (using (Sub a)), a sub-dictionary or a module argument, and its method is never a value. The truncating form issaturating-sub, andchecked-subanswersNone;Data.Enum’spredandto-enumanswerOption. The runtime’s negative check is a compiler-bug trap (exit 71,compiler bug:). ANatindex difference reaches runtime only as a folded constant.tests/safe_subtraction.rs.- The
I64conversions (NegativeInput,OutOfRange; thenat-from-i64,nat-to-i64/int-to-i64rows below): closed 2026-10-30.nat-from-i64 :: using (0 <= x) => (x :: I64) -> refined (n == x) Nat,nat-to-i64 :: using (n <= 9223372036854775807) => ...,int-to-i64 :: using (-9223372036854775808 <= x, x <= 9223372036854775807) => ...;checked-nat-from-i64,checked-nat-to-i64andchecked-int-to-i64answerNone, andnat-from-int/nat-to-intconvertIntandNatof any size with noI64in between. Results in a known range state it (design/refinement-types.md, “refined results”): every widening integer conversion (InRange lo hi r && r == x),mask-i64,char-code,nat-rem, the integer bounds,bytes-get/bytes-len/bytes-new, and the runtime clock readings, so converting a value non-negative by construction needs nothing written.Hash_Inthashes the magnitude as aNat(no conversion),Data.Rational.to-f64assembles a huge numerator from 32-bit limbs, and every intrinsic unit’sNatcount or index crosses throughcinch_nat_to_len(a bignum clamps toi64::MAX: an out-of-range index, or the heapExhaustedexit for a count). Both runtime checks are compiler-bug traps.tests/range_facts.rs,tests/small_nats.rs. - Byte buffers (
OutOfBounds,ReadOnly; thebytes-get/bytes-set/bytes-copy-range/bytes-newrows,argv, the IO buffer paths): closed 2026-10-30 (NF5).bytes-get/bytes-set :: using (i < bytes-len b) => ..., a byte is anInRange 0 255 Natboth ways (a read’s result carries it, a write’s value proves it),bytes-copy-range :: using (di + n <= bytes-len dst, si + n <= bytes-len src) => ...,bytes-new :: (n :: Nat) -> refined (bytes-len out == n) Bytes, andbytes-lenis at most2^63 - 1. A buffer’s length never changes (the runtime’slenis private and cleared only as the block is freed), so facts overbytes-len bsurvive writes tob. Loops discharge through their own guard; recursions carry their bound as their ownusingobligation; a buffer sized by a sum over a list is made at the end of a structural recursion whose result statespos <= bytes-len out(concat-from,pack-from,words-from,bytes-from-list). Three-term bounds (every copy range) discharge through the linear pass added to the fact checker (design/proof-automation.mdsection 5).bytes-from-listtakesList (InRange 0 255 Nat);FromList BytestakesU8elements.raw-argtakesusing (i < arg-count). A string literal’s read-only backing exists only inside aString(theStrBufconstructor and its reader are module-private, every conversion out copies), so noBytesvalue is ever one. The word-buffer copy loops ofEffect.FsandEffect.Processanswer a runtime error code asIoError/ProcessIoErrorinstead of reading it as a byte. The runtime’s index, range, value and read-only checks are compiler-bug traps; a failed stdout write isOutputFailed(still reachable, below).tests/safe_bytes.rs.
Runtime exits classified (2026-10-30)
Every runtime exit that is neither Exhausted (73) nor a program result,
at the tree of 2026-10-30.
Compiler-bug traps (an invariant the compiler guarantees; exit 71,
message starting compiler bug:): the null or wrong-kind word checks
abi.rs:174,182,295,304,368,383 (Error::CorruptValue, whose message
now says so) and cinch_nat_to_decimal/cinch_int_to_decimal‘s null
buffer (abi.rs:685,853); cinch_panic with a corrupt message pointer
(abi.rs:416); the allocators’ negative sizes and bad kinds
(abi.rs:435,438,492,506,511) and a corrupt layout table
(abi.rs:525); the join handle count (abi.rs:1027); a pointer and
length from generated code (cinch_bytes_from_data, abi.rs:968,973;
cinch_sym_intern, sym.rs:85,90; cinch_utf8_encode’s null buffer,
string.rs:73); a malformed quote block or program stream
(Error::CorruptValue throughout eval.rs, reported at eval.rs:1149);
the block layout and header checks (block.rs:322,327,332,342,368,391,402);
freed or corrupt counts (rc.rs:139); the program thread unwinding
(platform.rs:577); the cooperative join’s handle count (coop.rs:510,
now CorruptValue).
Faults a well-typed program can still reach (left for the migration, not fixed here):
: closed (see above); the check is now a compiler-bug trap.abi.rs:608cinch_nat_sub(NegativeResult), andabi.rs:337for the bignum path of the same operation: closed (see above); the check is now a compiler-bug trap.abi.rs:631,643,782,795cinch_nat_div/rem,cinch_int_div/rem(DivByZero, exit 72): closed (see above); the checks are compiler-bug traps.abi.rs:566cinch_nat_from_i64(NegativeInput);abi.rs:579,729cinch_nat_to_i64/cinch_int_to_i64(OutOfRange)text.rs:446cinch_text_f64_from_decimal(MalformedDecimal, the float slice’s).abi.rs:667,674,829,842cinch_nat_from_decimal/cinch_int_from_decimalare reached only by the code generator’s big-numeral constants (codegen.rsaround 5271), whose digits are a numeral’s by construction: no surface name exposes them, so no well-typed program reaches the malformed path (2026-10-30 audit).: closed (see above); the checks are compiler-bug traps.abi.rscinch_bytes_new/get/set/copy(OutOfBounds),as_bytes_mut(ReadOnly),platform.rscinch_argv_get(OutOfBounds),cinch_io_readwith a negative lengthcinch_bytes_from_datatakes its pointer and length from generated code only: a compiler-bug trap already.abi.rscinch_io_writeon a write or flush failure (OutputFailed, exit 71): an operating-system outcome (a closed pipe, a full device) that no program can rule out; making it aResultofio-writeis a separate API change.sym.rs:96cinch_sym_internon non-UTF-8 bytes (MalformedName, through the rawsym-intern-bytes).coop.rs:422the cooperative scheduler’s deadlock (awaiting a task suspended on the call stack): no static rule yet rules it out.eval.rsError::BlackHole(a quoted program’sFAULTopcode, memo re-entrancy), exit 70, and the codegen-emitted sites of section 2.abi.rs:688,856BufferTooSmall: reachable only from a foreign C caller (no generated code calls the*_to_decimalexterns), so it belongs with section 6.
Goal: every way a compiled, well-typed Cinch program can stop abnormally today. Two structurally different fault regimes exist:
- Defined faults:
cinch_panic/cinch_fault_div_zero/cinch_blackhole/heap-limit exit, a deliberateexit()with a message on stderr. Exit codes:70BlackHole,71panic (generic),72DivByZero,73HeapLimit (compiler/runtime/src/abi.rs:17-28,compiler/runtime/src/error.rs:48-60). - Undefined termination: native stack overflow (SIGSEGV/SIGABRT), OS thread-spawn
panic, or anything a
foreigncall does. These bypass the exit-code table entirely.
1. Runtime fault paths (compiler/runtime/src/*.rs)
Grouped by Error variant (compiler/runtime/src/error.rs:12-45) / exit code.
Error variant |
exit | site(s) | triggering operation | reachable by well-typed program |
|---|---|---|---|---|
DivByZero |
cinch_nat_div/rem, cinch_int_div/rem |
nat-div/nat-rem/int-div/int-rem with a zero divisor |
No (closed 2026-10-30): the divisor is a NonZero; the runtime check is a compiler-bug trap |
|
BlackHole |
70 | abi.rs:358 cinch_blackhole; eval.rs:1044-1048 cinch_eval on Err(BlackHole); codegen’s __cinch_demand/BoxGet re-entrancy check |
memo cell (~) demanded while its own thunk is running; a quoted Code a’s own FAULT opcode; a letrec box read before write |
Yes for memo re-entrancy and eval’s embedded fault opcode (ordinary program logic, no static proof of acyclic demand order); no for the letrec box case: guarded by the static guarded-letrec rule (check_terms.rs:9306-9341), defense-in-depth only |
CorruptValue |
71 | abi.rs:112-121,201-211,220-229 (as_sym/as_nat/as_int/as_bytes/as_bytes_mut) on null/wrong-kind block; eval.rs quote-block/program-stream validation |
a null or wrong-kind heap word reaching an extern entry point; a hand-built or corrupted quote block | No for ordinary generated code (the compiler only ever emits well-formed words); reachable only via raw-pointer foreign/FFI corruption or a compiler bug |
NegativeInput |
71 | abi.rs:463-466 cinch_nat_from_i64, cinch_int_to_nat |
nat-from-i64/nat-from-int on a negative value |
No (closed 2026-10-30): the argument carries 0 <= x evidence; the runtime check is a compiler-bug trap |
NegativeResult |
71 | abi.rs cinch_nat_sub (immediate and bignum path via nat::sub_mag) |
nat-sub a b with b > a |
No (closed 2026-10-30): a - b at Nat requires evidence that b <= a; the runtime check is a compiler-bug trap |
OutOfRange |
71 | abi.rs:473-479,616-620 cinch_nat_to_i64/cinch_int_to_i64 |
converting a bignum Nat/Int outside i64 range to I64 |
No (closed 2026-10-30): the argument carries range evidence (or the caller uses a checked-* form); the runtime check is a compiler-bug trap |
MalformedDecimal |
71 | abi.rs:552-562,666-677; eval.rs BIG opcode decode; text.rs cinch_text_f64_from_decimal |
a big numeral’s constant (well-formed by construction); f64-from-decimal on non-numeral bytes |
No for Nat/Int (only generated constants reach the decimal parsers; no surface name exposes them); yes for f64-from-decimal on untrusted text (the float slice) |
BufferTooSmall |
71 | abi.rs:566-578,695-706 cinch_nat_to_decimal/cinch_int_to_decimal |
caller-supplied buffer shorter than decimal_len |
No in practice: generated/prelude code always sizes the buffer from decimal-len first, but nothing in the type system proves it; a hand-written caller can still trigger it |
MalformedName |
71 | sym.rs:38-39 intern |
a Sym name that is not valid UTF-8 |
No: the only surface entry (Data.Sym.intern :: String -> Sym, Sym.cinch:39-40) converts through string-to-bytes, which is always valid UTF-8; dead for well-typed programs unless a raw sym-intern-bytes :: Bytes -> Sym is called directly with non-UTF-8 bytes (exposed but undocumented as a lower-level escape) |
OutOfBounds |
71 | bytes.rs Bytes::new/get/set, copy_range; abi.rs cinch_bytes_new/get/set/copy; platform.rs cinch_argv_get |
a bytes-get/bytes-set index outside 0..len, a byte value outside 0..=255, a bytes-copy-range range past either end, a negative length, an argv index past the count |
No (closed 2026-10-30, NF5): every index, range and value carries its evidence; the checks are compiler-bug traps. A failed stdout write is now OutputFailed (still reachable, section 5) |
ReadOnly |
71 | abi.rs as_bytes_mut on KIND_BYTES_STATIC |
bytes-set/bytes-copy-range targeting a string literal’s constant backing |
No (closed 2026-10-30, NF5): the backing exists only inside a String (private constructor and reader); every conversion out copies; the check is a compiler-bug trap |
NestingTooDeep |
71 | eval.rs:162,288-299 MAX_NESTING = 1024 |
a quoted program (Code a) whose expression tree + captured holes nests past 1024 evaluator-recursion units |
Yes: eval/quotation depth is a runtime property, not type-checked |
HeapLimit |
73 | heap.rs:118-130,175-179 account_alloc/heap_limit_exit |
cumulative heap allocation exceeding CINCH_HEAP_LIMIT (default 8 GiB, every cinch_alloc/cinch_alloc_raw/Rust Box/Vec call) |
Yes: resource exhaustion, see §5 |
Additional direct std::process::exit/panic! sites not routed through Error:
abi.rs:125,130,359:panic_exit,div_zero_exit,cinch_blackholethemselves (the exit mechanics, not new triggers).task.rs:152-153:Scope::spawncalls.unwrap_or_else(|e| panic!(...))ifstd::thread::Builder::spawnfails (OS thread-creation failure). Yes, reachable under resource exhaustion (too many OS threads/processes); this is a genuine Rustpanic!: on anextern "C"boundary a Rust panic aborts the process (no unwind across FFI), not a defined exit code (§5).platform.rs:555-566cinch_run_main: if the 1 GiB stack reservation thread fails to spawn, it degrades gracefully by runningentryon the calling thread instead of faulting; if the spawned thread’sjoin()fails it maps toEXIT_PANIC(71).
2. Codegen-emitted fault sites (compiler/src/codegen.rs)
| site | emits | source construct | reachable by well-typed program |
|---|---|---|---|
codegen.rs:4429-4440 |
call void @__cinch_blackhole() + unreachable for Core::Fault |
a case/match whose guards all fail on every pattern-covered row (§3) |
Yes |
codegen.rs:4588-4590 |
call void @exit(i32 70) + unreachable |
Core::BoxGet on an uninitialized letrec fixpoint box |
dead by the static guarded-letrec rule in ordinary cases (§3) |
codegen.rs:6764-6766,7003-7005 |
call void @__cinch_blackhole() + unreachable |
a CASE dispatch over a block whose discriminant tag matches none of the compiled arms (an internal invariant for boundary-layout/flat-return call dispatch) |
No for well-typed source: only reachable through a compiler-internal mismatch or corrupted data |
codegen.rs:2032-2040 |
fault_when → cinch_panic |
utf8CountChars on a malformed UTF-8 buffer |
Yes if the Bytes wasn’t validated with utf8Validate first: Data.String’s utf8-count-chars/utf8-decode-at contract treats a non-UTF-8 buffer as undefined and faults rather than returning Option/Result |
codegen.rs:2070-2082 |
fault_when → cinch_panic |
utf8Encode on a value that is not a Unicode scalar value |
Yes: nothing in the type system restricts Nat/I64 arguments to the scalar-value range; the prelude’s char-to-u32/u32-to-char round trip (total, Option-returning) is the only safe front door, but the raw utf8-encode-into intrinsic is directly callable |
codegen.rs:3496-3513 |
check_non_null/ffi_fault_block → cinch_panic |
a foreign import/export boundary pointer parameter declared non-null receiving a null pointer at runtime |
Yes for any foreign call with a non-owned/raw pointer argument sourced from outside the type checker’s view (§6) |
codegen.rs:1904-1905,2556,2680,2829,3184,3366 |
Rust-side unreachable!() |
exhaustive matches over the compiler’s own closed enums (ArithMode, binop tags, ConvMode, field shapes) |
No: these are compiler-internal invariants, not reachable from any Cinch source; a hit here is a compiler bug, not a program fault |
Machine-scalar arithmetic (I8..I64/U8..Usize) is not a fault path at codegen level:
ArithMode::Checked/CheckedDiv/CheckedRem/Shift{checked}/Neg{Checked} all lower to
ret_option (an Option result, total, codegen.rs:1904-1912,2654-2684); only ArithMode::Wrapping
silently wraps (defined, non-faulting) and ArithMode::Saturating clamps (also non-faulting).
3. Checker-emitted Fault (ECore::Fault/Core::Fault, compiler/src/check_terms.rs, check.rs, check_decls.rs, core.rs)
| production site | source construct | reachable by well-typed program |
|---|---|---|
check_terms.rs:6647-6654 run_out |
A case/match row matrix runs out at the fallback: pattern coverage is proven exhaustive by matrix_covered (check_terms.rs:6955-..., rejects non-exhaustive matrices at check_terms.rs:6052-6054,6297-6299), but guard coverage is never proven: compile_rows (check_terms.rs:5686-5745) falls through a guarded row that fails to the next row, and run_out is the terminal fallback once every row’s guard has failed |
YES: this is the primary live hole. Any case/match whose alternatives are all guarded (`pat |
check_terms.rs:7858-7999 infer_unhandled_op/dispatch_op |
an effect operation call for which no enclosing handle matches its label at the matching lexical extent (decision D23’s “defense in depth”) |
No: “effect honesty rejects unhandled operations at check time before any program can run one” (check_terms.rs:7737-7740): the effect-row type system is supposed to make this statically dead; it is kept only as a second line of defense |
check_decls.rs:18182-18186 eval_core on Core::Fault |
a compile-time staged computation (macro/quote/const-eval) whose body reduces to Core::Fault |
N/A at runtime: this rejects the compilation, it never reaches a running program; listed for completeness since it shares the Fault constructor |
4. Prelude exposure of faulting primitives
For each faulting primitive: the .cinch functions that expose it with no total alternative, and existing total escapes.
| primitive | partial function(s) | total alternative in prelude | approximate call-site density |
|---|---|---|---|
Nat subtraction below zero |
closed 2026-10-30: nat-sub and - at Nat require b <= a |
saturating-sub (truncating), checked-sub (Option) |
all sites migrated (see the closing note above) |
Nat/Int division or remainder by zero |
closed 2026-10-30: nat-div/nat-rem/int-div/int-rem and //% take a NonZero divisor |
checked-div/checked-rem (Option), non-zero/non-zero-nat, decide (NonZero d); Data.BigDecimal.div now answers Option, Data.Rational.div keeps its Result |
all sites migrated (see the closing note above) |
Bytes out-of-bounds index/value |
closed 2026-10-30 (NF5): bytes-get/bytes-set/bytes-copy-range take index and range obligations, a byte is an InRange 0 255 Nat |
a guard (if i < bytes-len b), the buffer’s own bytes-new length, an own using obligation, or a structural recursion whose result states its bound |
all sites migrated (see the closing note above) |
| decimal parse failure | nat-from-decimal/int-from-decimal (raw runtime imports) |
surface parsers in Text.Parse/Text.Json/Text.Toml/Text.Csv validate digit-by-digit (digits-fold, all-digits) before calling the parser, so most prelude use is effectively guarded; no Option-returning Nat/Int decimal parse is exported directly |
low direct exposure (a handful of call sites) because callers pre-validate; still unguarded if a program calls the raw parser on unvalidated text |
Sym interning of non-UTF-8 bytes |
sym-intern-bytes :: Bytes -> Sym (Data/Sym.cinch:33) |
Data.Sym.intern :: String -> Sym (Sym.cinch:39-40) is the documented total front door (a String’s bytes are always valid UTF-8) |
effectively zero exposure through the documented API; the raw foreign import is exposed but not meant to be called directly |
| eval nesting too deep | cinch_eval/eval :: Code a -> a |
none: nesting depth is inherent to the quoted program’s own shape, no static bound | rare (quotation/supercompilation code only) |
| read-only buffer write | closed 2026-10-30 (NF5): no Bytes value is a literal’s backing |
string-to-bytes copies (as before) |
none |
Nat/Int → I64 range overflow |
closed 2026-10-30: nat-to-i64/int-to-i64 state the range they need |
checked-nat-to-i64/checked-int-to-i64 (Option), nat-to-int/nat-from-int (any size) |
all sites migrated (see the closing note above) |
Refinement-typed indices (Dec/decide, design’s total-API pattern) exist for several container-level invariants: NonZero (Data/NonZero.cinch:32-34), NonEmpty (Data/NonEmpty.cinch:43-45), Sorted (Data/Sorted.cinch:30-32), Ascii (Data/Ascii.cinch:30-32): but there is no general Fin n/bounded-index refinement type anywhere in specifications/, design/, or compiler/prelude/; Bytes/List indexing has no refined-index total alternative at all, which is why bytes-get/bytes-set/nat-sub/division dominate the exposure counts above.
5. Resource exhaustion
| mechanism | site | behavior | exit code / termination |
|---|---|---|---|
| Heap cap | heap.rs account_alloc; env CINCH_HEAP_LIMIT (default 8 GiB, 0 disables) |
closed 2026-10-30: the cap is on live bytes (allocated minus freed); a refused system allocation and a request too large for any layout take the same exit | Exhausted, exit 73, cinch: exhausted: heap (<n> bytes) |
| Native stack overflow | platform.rs cinch_run_main, task.rs Scope::spawn, stack.rs guarded |
closed 2026-10-30: a 64 KiB guard band at the low end of every Cinch thread’s stack, a SIGSEGV/SIGBUS handler on an alternate stack, and inline stack probes on every generated unit |
Exhausted, exit 73, cinch: exhausted: stack |
| Eval nesting | eval.rs MAX_NESTING = 1024 |
closed 2026-10-30: the bound stands in for the stack, so passing it is the stack outcome | Exhausted, exit 73, cinch: exhausted: stack |
| OS thread spawn failure | task.rs Scope::spawn, platform.rs cinch_run_main (the old fallback onto the unguarded main thread is gone) |
closed 2026-10-30: stack::spawn_refused |
Exhausted, exit 73, cinch: exhausted: threads (stack when the refusal was ENOMEM) |
| File-descriptor / OS errors | abi.rs:946-951 cinch_io_write maps a write/flush failure to Error::OutOfBounds (exit 71, a slight misnomer but still a defined fault); platform.rs process/pty/net handle operations return negated error codes (-INVALID_ARGUMENT etc.) as ordinary Result-shaped values, not faults |
OS errors are generally surfaced as values, not faults, except cinch_io_write’s stdout failure |
mostly fine; the one write-failure case reuses the OutOfBounds fault rather than returning a Result |
6. Foreign calls
foreign import/foreign exportdeclarations carry no total-pure default: “Aforeigndeclaration has opaque external behavior until an effect and contract envelope is supplied. It does not inherit the total-pure default of an ordinaryfn.” (specifications/foreign-function-interface-specification.md:40).- Pointer arguments/results get one of five roles:
RawPtr,Borrowed,Retained,Owned(linear), plusOut/MaybeNullmodifiers (specifications/foreign-function-interface-specification.md:46,128-132).RawPtris explicitly the “raw unchecked pointer” role; the spec states safe callers “MUST NOT” construct roles by casting integers, but this is a documented convention, not something the type checker fully closes: the FFI boundary is deliberately where native-code crashes (segfault, use-after-free, out-of-bounds write in a foreign callee) can occur with no runtime fault wrapper at all. - A non-null-annotated boundary pointer that is null is turned into the one checked case, the defined
cinch_panicviacheck_non_null/ffi_fault_block(codegen.rs:3496-3513); everything else about what the foreign function itself does: writing past a buffer, dereferencing a dangling pointer, calling back into Cinch incorrectly: is outside the compiler’s and runtime’s reach. #[no-raw-ptr](JPL rule 9,check_decls.rs:12144-12176) is an opt-in safety attribute: with it, a signature may carry noRawPtr/opaquetype and a body may call no foreign pointer operation. Without it (the default), raw pointers and foreign calls are unrestricted. This attribute is the only mechanism that makes the FFI escape “extremely onerous to opt into” rather than freely available, and it is off by default.
7. Explicit escape hatches
- No
fault/panic/unreachable/error/absurdfunction exists in the prelude (grep acrosscompiler/prelude,compiler/src,specificationsforabsurdreturns nothing). The only source-level way to intentionally fault is:- Write a
case/matchwhose alternatives are all guarded and whose guards can all fail (§3): reachesCore::Fault→ exit 70 via__cinch_blackhole. This is accidental-looking but works as a deliberate “crash here” idiom today, and nothing stops it. - Call a known-partial prelude/runtime primitive directly (
nat-sub,nat-div/int-div,bytes-get/bytes-set, etc., §1/§4): the de factopanic/assertidiom in this language, reusing the generic exit-71 path. - Write a
foreign importto a C function that callsabort()/raisesSIGSEGV: fully outside the language’s own fault story (§6).
- Write a
#[assert-min n](JPL rule 5, attribute annotations specification section 10) does not itself introduce a new fault: it is a static compiler check that a function’s body contains at leastnruntimedecidesites (check_decls.rs:12073-12099,13844-13849); it counts existingdecidecalls, it does not insert new ones.decide :: p -> Dec p(the refinement discharge world,check_terms.rs:2068-2241) is total: it returnsYes evidence | No refutation, never faults. It is the mechanism by whichNonZero/NonEmpty/Sorted/Asciiturn a partial construction into anOption-returning one (§4); it is not itself a fault path.- There is no runtime
assertkeyword that can fail at runtime distinct from (1)/(2) above: “asserting” in this language is writing a guarded match or calling a partial primitive.
Top 10 most-used faulting operations by call-site count (prelude-wide, approximate, descending)
| rank | operation | fault | approx. call sites |
|---|---|---|---|
| 1 | bytes-get |
closed (i < bytes-len b evidence) |
100+ |
| 2 | bytes-set |
closed (index evidence, byte-valued argument) | 90+ |
| 3 | nat-div |
closed (divisor is NonZero Nat) |
60+ |
| 4 | nat-rem |
closed (divisor is NonZero Nat) |
60+ |
| 5 | int-div |
closed (divisor is NonZero Int) |
25+ |
| 6 | int-rem |
closed (divisor is NonZero Int) |
25+ |
| 7 | nat-sub |
closed (b <= a evidence) |
15+ |
| 8 | nat-to-i64/int-to-i64 |
OutOfRange, exit 71 |
10+ |
| 9 | guarded case/match with no unguarded catch-all (checker’s run_out) |
Core::Fault, exit 70 |
present wherever guards are used without a trailing unconditional wildcard: not grep-countable, but the single highest-leverage type-system gap since it’s the one path that compiles despite being genuinely unprovable |
| 10 | nat-from-decimal/int-from-decimal (direct, unvalidated) |
MalformedDecimal, exit 71 |
low single digits directly, but the underlying risk recurs anywhere text parsing skips pre-validation |
For the “bulletproof” design goal, the three highest-leverage fixes are: (a) give Nat
subtraction, and Nat/Int division/remainder, total (Option/refined-nonzero-divisor)
alternatives and steer the prelude onto them, closing the single widest exposure; (b) give
Bytes/indexable containers a bounds-refined index type (a general Fin n or an
Option-returning bytes-get/bytes-set used prelude-wide, mirroring Http.byte-at’s
one existing wrapper) to close the second-widest exposure; (c) require guard exhaustiveness
(or forbid an all-guarded match without a trailing unconditional arm) in the checker, closing
the one type-system hole (run_out/Core::Fault) that lets an otherwise “exhaustive” match
still fault at runtime.
