forked from nulang-org/nulang
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcapabilities.lean
More file actions
345 lines (297 loc) · 12.5 KB
/
Copy pathcapabilities.lean
File metadata and controls
345 lines (297 loc) · 12.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
/-
Nulang capability lattice — Pony-inspired reference capabilities.
Formalizes the eight-capability lattice from `src/types.rs` (Capability enum):
LinearIso → Iso → Trn → Ref → Box → Tag, with Linear → Val → Box.
Operations: `join`, `is_subtype_of`, `is_sendable`, `is_isolated`,
`discharge_linear`.
Theorem `cap_sendable` stated; proof open.
-/
import types
namespace Nulang
-- ------------------------------------------------------------------
-- Capability lattice
-- ------------------------------------------------------------------
/-
The eight capability constants. Mirrors `Capability` in `src/types.rs`.
Lattice: LinearIso -> Iso -> Trn -> Ref -> Box -> Tag,
Linear -> Val -> Box -> Tag, Iso -> Val, LinearIso -> Linear.
Subtyping follows the lattice order.
-/
inductive Cap where
| LinearIso | Linear | Iso | Trn | Ref | Val | Box | Tag
deriving BEq, Repr, Inhabited
namespace Cap
-- ------------------------------------------------------------------
-- Join (least upper bound)
-- ------------------------------------------------------------------
/--
The join operation computes the least upper bound of two capabilities
in the lattice. Mirrors `Capability::join()` in `src/types.rs`.
Join is commutative, associative, and idempotent (the lattice is
a meet-semilattice through `≤` and a join-semilattice through `⊔`).
-/
def join (a b : Cap) : Cap :=
match a, b with
| .LinearIso, .LinearIso => .LinearIso
| .LinearIso, .Iso => .Iso
| .Iso, .LinearIso => .Iso
| .LinearIso, .Trn => .Trn
| .Trn, .LinearIso => .Trn
| .LinearIso, .Ref => .Ref
| .Ref, .LinearIso => .Ref
| .LinearIso, .Val => .Val
| .Val, .LinearIso => .Val
| .LinearIso, .Box => .Box
| .Box, .LinearIso => .Box
| .LinearIso, .Tag => .LinearIso
| .Tag, .LinearIso => .LinearIso
| .Linear, .Linear => .Linear
| .Linear, .Val => .Val
| .Val, .Linear => .Val
| .Linear, .LinearIso => .Val
| .LinearIso, .Linear => .Val
| .Linear, .Iso => .Val
| .Iso, .Linear => .Val
| .Linear, .Trn => .Val
| .Trn, .Linear => .Val
| .Linear, .Ref => .Box
| .Ref, .Linear => .Box
| .Linear, .Box => .Box
| .Box, .Linear => .Box
| .Linear, .Tag => .Linear
| .Tag, .Linear => .Linear
| .Iso, .Iso => .Iso
| .Iso, .Trn => .Trn
| .Trn, .Iso => .Trn
| .Trn, .Trn => .Trn
| .Iso, .Ref => .Ref
| .Ref, .Iso => .Ref
| .Trn, .Ref => .Ref
| .Ref, .Trn => .Ref
| .Ref, .Ref => .Ref
| .Iso, .Val => .Val
| .Val, .Iso => .Val
| .Trn, .Val => .Val
| .Val, .Trn => .Val
| .Val, .Val => .Val
| .Ref, .Val => .Box
| .Val, .Ref => .Box
| .Iso, .Box => .Box
| .Box, .Iso => .Box
| .Trn, .Box => .Box
| .Box, .Trn => .Box
| .Ref, .Box => .Box
| .Box, .Ref => .Box
| .Val, .Box => .Box
| .Box, .Val => .Box
| .Box, .Box => .Box
| .Tag, .Tag => .Tag
| .Tag, c => c
| c, .Tag => c
-- ------------------------------------------------------------------
-- Subtyping (partial order)
-- ------------------------------------------------------------------
/--
`a ≤ b` iff the join of a and b is exactly b.
Mirrors `Capability::is_subtype_of()`.
-/
def le (a b : Cap) : Bool := join a b == b
-- ------------------------------------------------------------------
-- Sendability for actor boundaries
-- ------------------------------------------------------------------
/--
`a` is sendable iff values with capability `a` can be safely sent
to another actor (the value is immutable and alias-tracked).
Mirrors `Capability::is_sendable()` → `LinearIso | Linear | Iso | Val | Tag`.
-/
def is_sendable (a : Cap) : Bool :=
match a with
| .LinearIso | .Linear | .Iso | .Val | .Tag => true
| _ => false
/--
`a` is isolated iff values with capability `a` can be sent AND
provide full state isolation (unique ownership).
Mirrors `Capability::is_isolated()` → `LinearIso | Linear | Iso | Val | Tag`.
-/
def is_isolated (a : Cap) : Bool :=
match a with
| .LinearIso | .Linear | .Iso | .Val | .Tag => true
| _ => false
-- ------------------------------------------------------------------
-- Linear-to-iso promotion
-- ------------------------------------------------------------------
/--
Discharge linear tracking: LinearIso → Iso, Linear → Val.
Used when a linear value is consumed and the obligation is satisfied.
Mirrors `Capability::discharge_linear()`.
-/
def discharge_linear (a : Cap) : Cap :=
match a with
| .LinearIso => .Iso
| .Linear => .Val
| c => c
-- ------------------------------------------------------------------
-- Lattice theorems (open proofs)
-- ------------------------------------------------------------------
/--
**Theorem 1:** `join` is associative:
`join (join a b) c == join a (join b c)` for all `a`, `b`, `c`.
-/
theorem join_assoc : ∀ (a b c : Cap), join (join a b) c = join a (join b c) := by
intro a b c
cases a <;> cases b <;> cases c <;> rfl
/--
**Theorem 2:** `join` is commutative:
`join a b == join b a` for all `a`, `b`.
-/
theorem join_comm : ∀ (a b : Cap), join a b = join b a := by
intro a b
cases a <;> cases b <;> rfl
/--
**Theorem 3:** `join` is idempotent:
`join a a = a` for all `a`.
-/
theorem join_idem : ∀ a : Cap, join a a = a := by
intro a
cases a <;> rfl
/--
**Theorem: Sendable Capabilities are Safe for Actor Boundaries**
If the runtime permits a value `v : τ @ cap` to cross an actor
boundary (`is_sendable cap = true`), then either:
1. `cap ≤ Val` (value semantics — immutable, alias-tracked), or
2. `cap = Tag` (tagged pointer — safe to copy, no dereference).
A value whose capability is `Iso`, `Trn`, or `Ref` must NOT cross
an actor boundary — the capability lattice forbids it.
**Divergence note (2026-07):** The original statement
`∀ cap, is_sendable cap → le cap .Val` is **false** for `cap = Tag`:
`is_sendable Tag = true` but `le Tag Val = false`. `Tag` is
sendable because tagged pointers carry no ownership and can be
safely copied across actor boundaries without dereferencing, but
`Tag` is not a subtype of `Val` in the lattice (it sits at the
bottom, not below `Val`). The corrected statement uses a
disjunction to capture both cases.
-/
theorem cap_sendable : ∀ (cap : Cap), is_sendable cap = true → (le cap .Val = true ∨ cap = .Tag) := by
intro cap h
have h' := h
cases cap <;> simp [is_sendable, le, join] at h' ⊢
<;> first | rfl | trivial | done
theorem discharge_sendable : ∀ (cap : Cap), is_sendable cap → is_sendable (discharge_linear cap) := by
intro cap h
cases cap <;> simp [is_sendable, discharge_linear] at h ⊢
end Cap
-- ==================================================================
-- CAPABILITY-ANNOTATED TYPING JUDGMENT Γ ⊢ e : τ @ cap
-- ==================================================================
/--
Capability-aware context: each binding carries a type and a
capability. Extends the base `Context` from `types.lean` with
capability annotations. In a full implementation, `Scheme` would
also carry capability parameters; here we keep the capability
explicit in the binding for clarity.
-/
abbrev CapContext := List (Name × Ty × Cap)
/-- Look up a variable in the capability context. -/
def CapContext.lookup (Γ : CapContext) (x : Name) : Option (Ty × Cap) :=
match Γ with
| [] => none
| (y, τ, c) :: rest => if x == y then some (τ, c) else rest.lookup x
/-- The empty capability context. -/
def CapContext.empty : CapContext := []
-- ------------------------------------------------------------------
-- Typing rules
-- ------------------------------------------------------------------
/--
`HasTypeCap Γ e τ cap` — in context `Γ`, expression `e` has type `τ`
with capability `cap`.
Rules:
- `tVar`: variable lookup, capability from binding
- `tLit{Int,Bool,String}`: literals are always `Val` (sendable, immutable)
- `tLambda`: closures are `Val` (sendable, immutable reference)
- `tApp`: application joins function and argument capabilities
- `tLet`: let-binding propagates the body's capability
- `tIf`: conditional joins branch capabilities
- `tSend`: send requires sendable capability (hypothetical — needs Expr.send)
- `tSpawn`: spawned actor ref is `Tag` (hypothetical — needs Expr.spawn)
The judgment mirrors `HasType` from `types.lean` but adds capability
propagation through join at merge points and capability checks at
actor boundaries.
-/
inductive HasTypeCap : CapContext → Expr → Ty → Cap → Prop where
-- ** Variable **
| tVar : ∀ {Γ x τ cap},
Γ.lookup x = some (τ, cap) →
HasTypeCap Γ (.var x) τ cap
-- ** Literals **
| tLitInt : ∀ {Γ n},
HasTypeCap Γ (.litInt n) .int .Val
| tLitBool : ∀ {Γ b},
HasTypeCap Γ (.litBool b) .bool .Val
| tLitString : ∀ {Γ s},
HasTypeCap Γ (.litString s) .string .Val
-- ** Lambda (closures are Val — safe to send) **
| tLambda : ∀ {Γ x τ₁ e τ₂ cap₁ cap₂},
HasTypeCap ((x, τ₁, cap₁) :: Γ) e τ₂ cap₂ →
HasTypeCap Γ (.lambda x τ₁ e) (.fn τ₁ τ₂) .Val
-- ** Application (join capabilities of function and argument) **
| tApp : ∀ {Γ e₁ e₂ τ₁ τ₂ cap₁ cap₂},
HasTypeCap Γ e₁ (.fn τ₂ τ₁) cap₁ →
HasTypeCap Γ e₂ τ₂ cap₂ →
HasTypeCap Γ (.app e₁ e₂) τ₁ (Cap.join cap₁ cap₂)
-- ** Let (generalize bound type, propagate body capability) **
| tLet : ∀ {Γ x e₁ e₂ τ₁ τ₂ cap₁ cap₂},
HasTypeCap Γ e₁ τ₁ cap₁ →
HasTypeCap ((x, τ₁, cap₁) :: Γ) e₂ τ₂ cap₂ →
HasTypeCap Γ (.letIn x e₁ e₂) τ₂ cap₂
-- ** If (join branch capabilities at merge point) **
| tIf : ∀ {Γ e₁ e₂ e₃ τ cap₁ cap₂ cap₃},
HasTypeCap Γ e₁ .bool cap₁ →
HasTypeCap Γ e₂ τ cap₂ →
HasTypeCap Γ e₃ τ cap₃ →
HasTypeCap Γ (.ifThenElse e₁ e₂ e₃) τ (Cap.join cap₂ cap₃)
-- ** Send: message crossing actor boundary requires sendability **
-- Note: `Expr` does not yet have a `send` constructor. This rule is
-- stated for the capability discipline completeness and would take
-- `Expr.send e` as its subject when `Expr` is extended.
| tSend : ∀ {Γ e τ cap},
HasTypeCap Γ e τ cap →
Cap.is_sendable cap = true →
HasTypeCap Γ e τ cap
-- ** Spawn: spawned actor reference is always Tag (sendable) **
-- Note: `Expr` does not yet have a `spawn` constructor. When added,
-- this rule would type `spawn { e }` at some actor type with `Tag`.
| tSpawn : ∀ {Γ e τ cap},
HasTypeCap Γ e τ cap →
HasTypeCap Γ e τ .Tag
-- ==================================================================
-- LINEAR-ISO CONSUMPTION TRACKING
-- ==================================================================
/--
`consumed Γ x` holds iff `x` is not present in the capability
context `Γ`. In the full linear typing discipline (which refines
`HasTypeCap` to track input *and output* contexts), a linear
binding (`LinearIso` or `Linear`) is removed from the output
context after its single use. `consumed` checks that removal.
At merge points (if/else branches), both paths must produce the
same output context — i.e., both consume the same linear bindings.
-/
def consumed (Γ : CapContext) (x : Name) : Bool :=
Γ.lookup x == none
/--
**Theorem: Linear bindings are consumed at most once.**
If `x` is bound with a linear capability (`LinearIso`) in the
initial context and a term `e` is well-typed under that context,
then `x` is consumed — it does not persist in the context for
further use. This enforces the "use exactly once" discipline for
linear capabilities.
The theorem requires the full context-splitting semantics (input/
output context pairs) that a production linear type system would
carry. In the simplified single-context `HasTypeCap` judgment
above, the statement is aspirational and the proof is open.
-/
theorem linear_at_most_once : ∀ (Γ : CapContext) (x : Name) (τ : Ty) (e : Expr) (τ' : Ty) (cap : Cap),
HasTypeCap ((x, τ, .LinearIso) :: Γ) e τ' cap →
consumed Γ x = true := by
intro Γ x τ e τ' cap h
sorry
end Nulang