code wiki / (root) / linear_type.nx

linear_type.nx source

↩ module page · 150 lines · 4942 B

1// linear_type.nx -- runtime tracking for linear/affine resources. 2// 3// Phase A of the full linear-type system (EFFICIENCY_ROADMAP ยง3.2). 4// Today's NishiLang has no compile-time borrow checker, so we 5// approximate the safety benefit at runtime: an owning handle 6// struct that tracks "has this been used yet?" + panics on 7// double-use. Callers that adopt this pattern get the benefit 8// immediately; the compiler-side move/borrow checker can 9// retrofit without breaking the API later. 10// 11// The canonical failure this prevents: double-free / use-after- 12// free / duplicate-consume bugs -- the same class that tonight's 13// "duplicate main in nxc.s" + "alloca clobbered by callee-saved 14// reuse" bugs both lived in. Linear types at compile time would 15// reject both without a test suite. 16// 17// Usage: 18// let h: *Linear = linear_new(ptr as i64, LK_FILE_HANDLE) 19// let taken: i64 = linear_take(h) // consume exactly once 20// linear_take(h) // -> LINEAR_ERR_REUSE 21// 22// Also exposes a RefCount type for when true linearity is too 23// strict and you want shared ownership with explicit counting. 24// 25// Invariants: 26// LT1 `linear_take` on a fresh handle returns its payload + 0. 27// LT2 Subsequent `linear_take` returns LINEAR_ERR_REUSE 28// deterministically. 29// LT3 `refcount_clone` always succeeds + bumps count. 30// LT4 `refcount_drop` decrements; when count hits 0, calls 31// the user-registered free hook exactly once. 32 33import "syscalls.nx" 34 35// Kinds catalog for diagnostic output. 36const LK_GENERIC: i64 = 0 37const LK_FILE_HANDLE: i64 = 1 38const LK_SOCKET: i64 = 2 39const LK_MEMORY_ARENA: i64 = 3 40const LK_MUTEX_GUARD: i64 = 4 41const LK_CAPABILITY: i64 = 5 42 43const LINEAR_ERR_REUSE: i64 = -1001 44const LINEAR_ERR_NULL: i64 = -1002 45 46struct Linear { 47 payload: i64, // opaque value -- pointer or fd or index 48 kind: i64, 49 consumed: i64, 50} 51 52// Allocate a fresh linear handle wrapping `payload`. 53func linear_new(payload: i64, kind: i64) -> *Linear { 54 let raw: *u8 = sys_mmap(32) 55 let h: *Linear = raw as *Linear 56 h.payload = payload 57 h.kind = kind 58 h.consumed = 0 59 return h 60} 61 62// Consume the handle; returns the payload. Second call fails. 63func linear_take(h: *Linear) -> i64 { 64 if h == (0 as *Linear) { return LINEAR_ERR_NULL } 65 if h.consumed == 1 { return LINEAR_ERR_REUSE } 66 h.consumed = 1 67 return h.payload 68} 69 70// Peek without consuming (reference semantics). 71func linear_peek(h: *Linear) -> i64 { 72 if h == (0 as *Linear) { return LINEAR_ERR_NULL } 73 return h.payload 74} 75 76// Is the handle still valid? 77func linear_alive(h: *Linear) -> i64 { 78 if h == (0 as *Linear) { return 0 } 79 if h.consumed == 1 { return 0 } 80 return 1 81} 82 83// ===== Ref-counted ownership ========================================== 84// 85// When linearity is too strict and you want shared ownership with 86// explicit drop semantics (Rust's Arc / C++'s shared_ptr style). 87// Use when the resource is legitimately shared -- e.g. a parsed 88// AST consumed by multiple analysis passes. 89 90struct RefCount { 91 payload: i64, 92 count: i64, 93 free_hook: i64, // function pointer; 0 = no hook 94} 95 96func refcount_new(payload: i64, free_hook: i64) -> *RefCount { 97 let raw: *u8 = sys_mmap(32) 98 let r: *RefCount = raw as *RefCount 99 r.payload = payload 100 r.count = 1 101 r.free_hook = free_hook 102 return r 103} 104 105// Bump refcount -- caller now holds a clone. Always succeeds. 106func refcount_clone(r: *RefCount) -> i64 { 107 r.count = r.count + 1 108 return 0 109} 110 111// Decrement -- when count hits 0, return 1 (caller frees payload 112// via the hook); else return 0. We DON'T call the hook directly 113// here because cross-module function-pointer calls need extra 114// wiring that parse.nx doesn't cleanly provide yet; phase B will 115// add it. 116func refcount_drop(r: *RefCount) -> i64 { 117 r.count = r.count - 1 118 if r.count == 0 { return 1 } 119 return 0 120} 121 122func refcount_count(r: *RefCount) -> i64 { 123 return r.count 124} 125 126// Compile-only smoke: exercise linear + refcount invariants. 127func main() -> i64 { 128 // Linear: first take OK, second take fails. 129 let h: *Linear = linear_new(42, LK_FILE_HANDLE) 130 if linear_alive(h) != 1 { return 1 } 131 132 let v: i64 = linear_take(h) 133 if v != 42 { return 2 } 134 if linear_alive(h) != 0 { return 3 } 135 136 let v2: i64 = linear_take(h) 137 if v2 != LINEAR_ERR_REUSE { return 4 } 138 139 // Refcount: clone + drop arithmetic. 140 let r: *RefCount = refcount_new(99, 0) 141 if refcount_count(r) != 1 { return 5 } 142 143 refcount_clone(r) 144 if refcount_count(r) != 2 { return 6 } 145 146 if refcount_drop(r) != 0 { return 7 } // count = 1, not zero yet 147 if refcount_drop(r) != 1 { return 8 } // count = 0 -> free signal 148 149 return 0 150}