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}