Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions notes/api-redesign-prototype/prototype_findings.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,25 @@ impl<T: Trace> Root<T> {

Catches cross-collector misuse where lifetimes can't help.

### Static Root/Context Binding Experiment

To explore Jedel's feedback about statically preventing root sharing between
contexts, the prototype now includes a scoped variant:

```rust
pub struct ScopedRoot<'gc, T> { ... }
pub fn root_scoped(&self, gc: Gc<'gc, T>) -> ScopedRoot<'gc, T>
```

`ScopedRoot<'gc, T>` is bound to the active mutation lifetime and cannot escape
`mutate()`. Compile-fail tests cover:

- escaping a scoped root out of `mutate()`,
- brand-mismatch usage (A-root with B-context) as a type-level feasibility proof.

This does not replace long-lived `Root<T>` yet; it is an incremental path for
evaluating stronger static guarantees.

### Gc Access Safety

**Q**: How do we prevent `Gc::get()` from accessing dead allocations?
Expand Down
23 changes: 23 additions & 0 deletions oscars/examples/api_prototype/gc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,21 @@ impl<'gc, T: Trace + 'gc> Gc<'gc, T> {
}
}

/// Lifetime-branded root handle tied to a single mutation context.
///
/// Unlike `Root<T>`, this cannot escape the `'gc` mutation lifetime and
/// therefore cannot be used with another collector/context in safe code.
#[must_use = "scoped roots must be used within the active mutation context"]
pub struct ScopedRoot<'gc, T: Trace + ?Sized + 'gc> {
gc: Gc<'gc, T>,
}

impl<'gc, T: Trace + ?Sized + 'gc> ScopedRoot<'gc, T> {
pub fn get(&self, _cx: &MutationContext<'gc>) -> Gc<'gc, T> {
self.gc
}
}

/// Pinned root handle that keeps a GC allocation live across `mutate()` boundaries.
///
/// Uses an intrusive linked list. `#[repr(C)]` with `link` first allows
Expand Down Expand Up @@ -297,6 +312,14 @@ impl<'gc> MutationContext<'gc> {
root
}

/// Creates a root that is statically bound to this mutation lifetime.
///
/// This is a prototype path to evaluate whether root/context pairing can be
/// fully enforced at compile time. The handle cannot escape `'gc`.
pub fn root_scoped<T: Trace + Finalize + 'gc>(&self, gc: Gc<'gc, T>) -> ScopedRoot<'gc, T> {
ScopedRoot { gc }
}

pub fn collector_id(&self) -> u64 {
self.collector.id
}
Expand Down
11 changes: 11 additions & 0 deletions oscars/examples/api_prototype/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,17 @@ mod tests {
});
}

#[test]
fn scoped_root_is_context_bound() {
let ctx = GcContext::new();
ctx.mutate(|cx| {
let obj = cx.alloc(777i32);
let scoped = cx.root_scoped(obj);
let gc = scoped.get(cx);
assert_eq!(*gc.get(), 777);
});
}

#[test]
fn root_rejects_different_collector() {
let ctx1 = GcContext::new();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
//! Compile-fail test: with context branding, a root from brand A cannot be used
//! with a mutation context from brand B.

use core::marker::PhantomData;

struct BrandA;
struct BrandB;

struct GcContext<B>(PhantomData<B>);
struct MutationContext<'gc, B>(&'gc (), PhantomData<B>);
struct Gc<'gc, T>(&'gc T);
struct ScopedRoot<'gc, B, T>(Gc<'gc, T>, PhantomData<B>);

impl<B> GcContext<B> {
fn new() -> Self {
GcContext(PhantomData)
}
fn mutate<R>(&self, f: impl for<'gc> FnOnce(&MutationContext<'gc, B>) -> R) -> R {
f(&MutationContext(&(), PhantomData))
}
}

impl<'gc, B, T> ScopedRoot<'gc, B, T> {
fn get(&self, _cx: &MutationContext<'gc, B>) -> Gc<'gc, T> {
todo!()
}
}

impl<'gc, B> MutationContext<'gc, B> {
fn alloc<T>(&self, _v: T) -> Gc<'gc, T> {
todo!()
}
fn root_scoped<T>(&self, gc: Gc<'gc, T>) -> ScopedRoot<'gc, B, T> {
ScopedRoot(gc, PhantomData)
}
}

fn main() {
let ctx1 = GcContext::<BrandA>::new();
let ctx2 = GcContext::<BrandB>::new();

ctx1.mutate(|cx1| {
let gc = cx1.alloc(42i32);
let root = cx1.root_scoped(gc);

ctx2.mutate(|cx2| {
let _ = root.get(cx2);
});
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
error[E0308]: mismatched types
--> examples/api_prototype/tests/ui/scoped_root_cannot_cross_context.rs:47:30
|
47 | let _ = root.get(cx2);
| --- ^^^ expected `&MutationContext<'_, BrandA>`, found `&MutationContext<'_, BrandB>`
| |
| arguments to this method are incorrect
|
= note: expected reference `&MutationContext<'_, BrandA>`
found reference `&MutationContext<'_, BrandB>`
note: method defined here
--> examples/api_prototype/tests/ui/scoped_root_cannot_cross_context.rs:24:8
|
24 | fn get(&self, _cx: &MutationContext<'gc, B>) -> Gc<'gc, T> {
| ^^^ -----------------------------
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
//! Compile-fail test: ScopedRoot<'gc, T> cannot escape mutate()

struct GcContext;
struct MutationContext<'gc>(&'gc ());
struct Gc<'gc, T>(&'gc T);
struct ScopedRoot<'gc, T>(Gc<'gc, T>);

impl GcContext {
fn new() -> Self {
GcContext
}
fn mutate<R>(&self, f: impl for<'gc> FnOnce(&MutationContext<'gc>) -> R) -> R {
f(&MutationContext(&()))
}
}

impl<'gc> MutationContext<'gc> {
fn alloc<T>(&self, _v: T) -> Gc<'gc, T> {
todo!()
}
fn root_scoped<T>(&self, gc: Gc<'gc, T>) -> ScopedRoot<'gc, T> {
ScopedRoot(gc)
}
}

fn main() {
let ctx = GcContext::new();

let escaped = ctx.mutate(|cx| {
let gc = cx.alloc(42i32);
cx.root_scoped(gc)
});

let _ = escaped;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
error: lifetime may not live long enough
--> examples/api_prototype/tests/ui/scoped_root_cannot_escape_mutate.rs:31:9
|
29 | let escaped = ctx.mutate(|cx| {
| --- return type of closure is ScopedRoot<'2, i32>
| |
| has type `&MutationContext<'1>`
30 | let gc = cx.alloc(42i32);
31 | cx.root_scoped(gc)
| ^^^^^^^^^^^^^^^^^^ returning this value requires that `'1` must outlive `'2`