Chenhao Zuo
nero@meta.com
90d · built 2026-05-28
90-day totals
- Commits
- 239
- Grow
- 14.0
- Maintenance
- 13.3
- Fixes
- 1.1
- Total ETV
- 28.4
Where this dev ranks
Percentile against the global top-100 leaderboard (all-time totals).
- By commits
- Top 30 %
- By Growth share
- Top 12 %
30-day trajectory
Last 30 days vs. the 30 days before. Up arrows on Growth and ETV mean improvement; up arrow on Fixes share means more time on fixes (worse).
Daily performance
Daily ETV, stacked by Growth, Maintenance and Fixes.
Work-mix over time
Share of Growth / Maintenance / Fixes over a rolling 7-day window. Reads as 'where is effort flowing right now'.
Bug flow over time
Monthly bug flow attributed to this developer. The left bar (red) is bug impact this dev authored that was addressed in the given month — combining bugs others fixed for them and bugs they fixed themselves. The right bar is fixes they personally shipped that month, split between self-fixes (overlap with the red bar) and fixes done for someone else. X-axis is fix-time, not introduction-time — the Navigara API attributes bugs backward to the author at the moment the fix lands.
- Self-fix share
- 38%
- Bugs you introduced
- 1.4
- Bugs you fixed
- 2.2
Repository spread
Where this developer's commits land. Concentrated work (top1 > 80%) vs polymath spread (top1 < 30%).
Most impactful commits
Top 20 by ETV in the 90-day window.
- 1.9ETVAdd `--agent-context` CLI flag with buckconfig-driven validation Summary: RFC: https://fb.workplace.com/groups/buck2dev/permalink/4302138863407486/ Adds a new `--agent-context key=value` CLI flag (modeled after `--client-metadata`) that lets AI agents pass structured context like `intent=fix`, `attempt=2`, or `prior_error=missing_target`. On the server side, a buckconfig-driven schema (`[agent_context]` / `[agent_context#field]` sections) allows enforcing required fields, allowed values, and unknown-key rejection for specific client IDs, while non-enforced clients pass data through without validation. Reviewed By: jtbraun Differential Revision: D99735398 fbshipit-source-id: 1c3e7d0eacae1d92b18068ad5e8fd472fd4d2d39github.com-facebook-buck2 · 9d9dc6c0 · 2026-04-21
- 0.8ETVAdd PagableCursor and position-tracking postcard flavors Summary: Introduces `PagableCursor` and `position()`/`seek()` methods on `PagableSerializer`/`PagableDeserializer`. `PagableCursor` captures both the byte stream position and the arc list index, because pagable serialization uses two parallel streams: the byte buffer (for inline data) and the arc list (for nested `Arc` references). Seeking to a saved position must restore both to stay in sync. Replaces postcard's built-in `StdVec` and `Slice` flavors with custom `PagableVecFlavor` and `PagableSlice` that expose the current read/write position. This is the infrastructure needed for random-access deserialization — the next diff uses `seek()` to jump to a value's data when it's referenced before being reached in linear order. Reviewed By: christolliday Differential Revision: D96243746 fbshipit-source-id: 5d9b16406a5b5aaf30aa15ea503706387d9dd7eegithub.com-facebook-buck2 · 74c10182 · 2026-04-15
- 0.6ETVAdd FrozenValue serialization via arena offset maps Summary: Starlark values often contain `FrozenValue` fields that point to other values in the same heap. This diff adds `serialize_frozen_value` and `deserialize_frozen_value` to the Starlark ser/de context, using per-heap offset maps to convert raw pointers to/from `ArenaOffset` (bump kind + byte offset). Only same-heap `FrozenValue` references are supported for now; cross-heap references are added in a later diff. The serializer builds a `HashMap<usize, ArenaOffset>` from the arena, and the deserializer resolves offsets back to pointers using stored bump base addresses. Reviewed By: christolliday Differential Revision: D96243757 fbshipit-source-id: a0593483f0aa23d3f53691143526241d0c2239f8github.com-facebook-buck2 · 4f856e12 · 2026-04-10
- 0.5ETVpagable_typetag for TypeMatcher dispatch Summary: `TypeMatcherBox` is `Box<dyn TypeMatcherDyn>`. Deserialize needs to know the concrete type behind the `dyn` so it can pick the right `pagable_deserialize`. Wire up `pagable::typetag` to provide that dispatch: - **`TypeMatcherDyn`** becomes a `#[pagable_typetag]` trait with a `PagableTagged` supertrait, so a per-trait registry exists for it. - **Each concrete matcher** is tagged so it lands in the registry: `#[pagable_typetag(TypeMatcherDyn)]` for non-generic ones (`IsStr`, `EnumTypeMatcher`, `UserProviderMatcher`, …) and `#[pagable_tagged(TypeMatcherDyn)]` + `register_type_matcher!` for generic wrappers (`IsListOf<I>`, `IsDictOf<K, V>`, `IsTupleElems2<A, B>`, …). - **`TypeMatcherBox`** gets a manual ser/de that writes `tag + payload` on the way out and reads the tag to dispatch through `<dyn TypeMatcherDyn>::deserialize_box` on the way in. A later diff replaces this with the `Pagable` derive once tag handling is generalized. - **`TypeMatcherAlloc`** allocators (`any_of_two_matcher`, `list_of_matcher`, `dict_of_matcher`, `set_of_matcher`) switch from `impl TypeMatcher` to explicit generic params with `IsXxx<…>: PagableRegisteredFor<dyn TypeMatcherDyn>` bounds — this forces callers to register the concrete instantiations they construct. Reviewed By: christolliday Differential Revision: D103624117 fbshipit-source-id: 7efc071cd349c5282ce8b9aa1ed166f7584c94efgithub.com-facebook-buck2 · d985b3f7 · 2026-05-06
- 0.5ETVMigrate all `StarlarkValueAsType` consts to `#[starlark_types]` attribute Summary: **Motivation**: With the infrastructure in place, migrate all existing `StarlarkValueAsType` constants from inline `const` declarations to the new `#[starlark_types]` attribute. This is a mechanical migration across ~30 files in buck2 app crates. Each `const Foo: StarlarkValueAsType<Bar> = StarlarkValueAsType::new()` inside a `#[starlark_module]` function is replaced with `#[starlark_types(Bar as Foo)]` on the function. Types with lifetimes use explicit `<'_>` (e.g., `StarlarkDynamicActions<'_> as DynamicActions`). `SelectDict`/`SelectConcat` registrations are moved to `buck2_build_api` where the types are defined (Rust orphan rule). Reviewed By: christolliday Differential Revision: D99029993 fbshipit-source-id: 0fe940c3b102d0ae842dc988556299df0d93b0fbgithub.com-facebook-buck2 · a38eb9ad · 2026-04-14
- 0.5ETVAdd `recover_from_pagable` bridges between starlark and pagable serialization layers Summary: When two starlark values share an `Arc<T>`, we want that `Arc` to dedup on the wire — round-trip back to a single allocation. Pagable already has an Arc-identity dedup mechanism, but it lives in the lower `pagable::PagableSerialize` / `PagableDeserialize` layer, not in the starlark `StarlarkSerialize` / `StarlarkDeserialize` layer. So for an `Arc` field on a starlark value, we have to switch that field's serialization off the starlark layer and onto the pagable layer. The catch: the inside of that `Arc<T>` may itself contain `FrozenValue`s that need to resolve against the currently-(de)serializing starlark heap. Once we've switched into the pagable layer, the starlark heap context is gone — so the body can't be serialized correctly without recovering it. This diff adds two associated-function bridges: - `StarlarkSerializerImpl::recover_from_pagable(serializer)` - `StarlarkDeserializerImpl::recover_from_pagable(deserializer)` Each reads the heap context that was stashed in the `SessionContext` before the pagable hop and returns a fully-wired `StarlarkSerializerImpl` / `StarlarkDeserializerImpl`. A type's `PagableSerialize` / `PagableDeserialize` impl can therefore recover the starlark context and delegate its body back to `StarlarkSerialize` / `StarlarkDeserialize`, completing the round-trip. Plumbing changes that fall out of this: - `current_heap_deser_state` becomes `Arc<Mutex<HeapDeserializationState>>` so a nested deserializer entered through `recover_from_pagable` shares the **same** forward-reference work queue as the outer flow. - New `CurrentHeapId` / `CurrentHeapDeserState` wrappers stored in the `SessionContext` so the heap id (and deser state) survives trips through pure pagable layers. This is a prerequisite for the next diff. Reviewed By: cjhopman Differential Revision: D102555402 fbshipit-source-id: e6ccc281473e80f43b6718d2c91170549c7314eagithub.com-facebook-buck2 · b487d3dd · 2026-05-06
- 0.5ETVAdd StarlarkPagable derive macro and common type impls Summary: Adds `#[derive(StarlarkPagable)]` which generates `StarlarkSerialize` and `StarlarkDeserialize` impls by serializing each field in order. Fields use their `StarlarkSerialize`/`StarlarkDeserialize` impls (not `PagableSerialize`), so `FrozenValue` fields are handled correctly through the context. Also adds blanket `StarlarkSerialize`/`StarlarkDeserialize` impls for primitive types (bool, integers, floats, String), `Vec<T>`, `Box<T>`, and `Option<T>` in a new `starlark_pagable_impls` module. Replaces all hand-written impls in tests with `#[derive(StarlarkPagable)]`. Reviewed By: christolliday Differential Revision: D96243760 fbshipit-source-id: 6b106510762766bda240b36cc2d01bfcc49f63a4github.com-facebook-buck2 · 3c0ed18e · 2026-04-15
- 0.5ETVAdd UserContext typed map to serializer/deserializer traits Summary: The Starlark layer needs to pass its own state (offset maps, heap base addresses) through the pagable serializer/deserializer, but the pagable layer shouldn't know about Starlark-specific context. This adds a `UserContext` — a `TypeId`-keyed `HashMap<TypeId, Box<dyn Any>>` — to `PagableSerializer` and `PagableDeserializer`, letting each layer store/retrieve its own opaque state without coupling. The design is asymmetric: serializers own `UserContext` directly and expose `&mut UserContext`, while deserializers access it through `PagableStorage` via `&Mutex<UserContext>` (the `Mutex` is needed because `PagableStorage: Send + Sync`). Reviewed By: christolliday Differential Revision: D96243754 fbshipit-source-id: 7552d55563155eae1e7c8d6463cc10eec282c6acgithub.com-facebook-buck2 · 48553e8f · 2026-04-15
- 0.5ETVTyStarlarkValue: foundation, HasTyVTable trait, register_ty_starlark_value! macro Summary: ## Why we need this `TyStarlarkValue` is the typing-system handle to a `StarlarkValue` impl. Before this diff it held a raw `&'static TyStarlarkValueVTable`. To make `TyStarlarkValue` pagable round-trippable, the inner pointer has to become a `pagable::StaticValue<TyStarlarkValueVTable>`. That gets us most of the way, but creates a new problem: `TyStarlarkValue::new::<T>()` needs the right `StaticValue` *for that specific `T`*. This diff goes with: the **`HasTyVTable`** trait. Each `T` that can flow through `TyStarlarkValue::new` is required to implement `HasTyVTable` with a `const TY_VTABLE_STATIC: StaticValue<TyStarlarkValueVTable>` filled in by the per-`T` registration. `TyStarlarkValue::new::<T>` now reads `T::Canonical::TY_VTABLE_STATIC` and any unregistered type is a compile error. A `register_ty_starlark_value!` macro is the standard way to populate the trait — it emits the static, the `pagable::static_value!` registration, and the trait impl in one block. Two macro forms exist because V-parameterized types (`Foo<'v, V: ValueLike<'v>>`) instantiate to two distinct Rust types — `Foo<'v, Value<'v>>` and `Foo<'static, FrozenValue>` — that share a single `'static` vtable content but need separate `HasTyVTable` impls. `#[starlark_value]` is updated to emit the macro automatically for the common cases, so most types pick up registration without a manual call. The cases the macro can't handle — types generic over non-`ValueLike` parameters (`StarlarkAny<T>`, `AnyArray<T>`, `StarlarkAnyComplex<T>`, `CallEnter<'v, D>`, `StarlarkTargetSet<Node>`) — get a temporary `UNREGISTERED_VTABLE_STATIC` placeholder so the trait bound compiles; following diffs then replace each placeholder with proper per-`T` registration, and in diff 67 deletes the placeholder. ## What changes - **`HasTyVTable` trait** + `TyStarlarkValue::new::<T>` requires `T::Canonical: HasTyVTable` and reads the vtable from the trait const. - **`register_ty_starlark_value!` macro** — simple `($ty)` form for non-generic and lifetime-only types; `(generic = <…>, elided_ty = …, impl_ty = …)` form for V-parameterized types so live and frozen impls share one content static. - **`#[starlark_value]` macro** — auto-emits the registration for cases it can resolve. - **`PagableSerialize`/`PagableDeserialize` for `TyStarlarkValue`** — delegate to the inner `StaticValue<TyStarlarkValueVTable>`. - **`UNREGISTERED_VTABLE_STATIC` sentinel** — temporary placeholder pointing at `NoneType`'s vtable, used by the placeholder `HasTyVTable` impls for `StarlarkAny<T>` / `AnyArray<T>` / `StarlarkAnyComplex<T>` / `CallEnter<'v, D>` / `StarlarkTargetSet<Node>`. Removed in diff 67. - One-line `Self: HasTyVTable` bound additions on a handful of types (`partial.rs`, `list/value.rs`, `compiled.rs`, `any_array.rs`, `any.rs`, `any_complex.rs`, `targetset.rs`) to satisfy the new trait-bound contract. Reviewed By: cjhopman Differential Revision: D102509848 fbshipit-source-id: b02d06e7aa59f4537f8bba68016d4905403409f2github.com-facebook-buck2 · 28765092 · 2026-05-06
- 0.4ETVExtend #[derive(StarlarkPagable)] with skip, bound, and enums Summary: Three orthogonal gaps in the derive macro, all hit by later diffs in this stack: - **`#[starlark_pagable(skip)]` / `skip = "expr"`**: some fields — caches, profiling data, handles to non-serializable runtime state — have no meaningful serialized form. On serialize they become a no-op (with a `let _ = &self.field;` to silence unused-field lints); on deserialize they are reconstructed via `Default::default()` or a caller-supplied expression. This is the Starlark analog of `#[pagable(skip)]`. - **`#[starlark_pagable(bound = "...")]`**: when a generic type has impl-level constraints that don't match the literal type parameters (e.g. `DictGen<T>` actually needs `'v, T: DictLike<'v>`), the user can override the `impl<...>` parameter list verbatim. Without this, the derive emits bounds copied from the type definition and those don't always typecheck. - **Enum support**: previously the macro rejected enums. Now it emits a `u8` discriminant tag (variant index, capped at 255) followed by each variant's payload fields, reusing the same `pagable` / `skip` / `skip_expr` handling as structs. Required by `CodeMapImpl` and `SerializedFrozenValue`-like enums downstream. - **`#[starlark_pagable(impl_for = "...")]`**: Imp starlark pagable for some specific generic T. Reviewed By: cjhopman Differential Revision: D102186975 fbshipit-source-id: a2d3dd0ef92167814dfc14a748a135b26588909cgithub.com-facebook-buck2 · 273da329 · 2026-05-01
- 0.4ETVMigrate ArcStr/ArcOrStatic to StaticStr identity Summary: Fields holding `&'static str` (e.g. native function parameter names) lose their identity across pagable round-trips. To fix this: - `ArcOrStatic<T>` switches its `Static` arm from `&'static T` to `pagable::StaticValue<T>`, so the static lives in the registry and round-trips by index. - `ArcStr::new_static` now takes `StaticStr` instead of `&'static str`. - `ArcOrStatic<T>` gets manual `PagableSerialize`/`PagableDeserialize`: a 1-byte tag picks between the indexed-static path and `serializer.serialize_arc(...)` for the heap-allocated path. - All call sites — native function param specs (via the `starlark_module` derive macro), `ArcStr::EMPTY_STR`, internal/test usages — are updated to register strings via `pagable::static_str!` and pass the resulting `StaticStr`. - `TyCallable` drops the `ArcOrStatic` wrapping in favor of plain `Arc<TyCallableInner>` plus a `OnceLock<Arc<...>>` for the static-`any()` case. Same end result, but without the now-unneeded enum. This is the load-bearing change that makes the rest of the stack possible. Reviewed By: cjhopman Differential Revision: D102487030 fbshipit-source-id: 7ebea25fdea44a69ae9a1eb67e436a9733c28195github.com-facebook-buck2 · ed3bd48a · 2026-05-06
- 0.4ETVDrop `starlark` dep from `buck2_core`, `buck2_fs`, `buck2_util` Summary: `buck2_common` can't depend on `starlark` (banned in `app_dep_graph_rules/rules.bzl`) — the buck2 client binaries depend on `buck2_common` and shouldn't pull in the starlark interpreter. Adding `starlark` to any foundation crate (`buck2_core`, `buck2_fs`, `buck2_util`) creates a banned transitive path via `buck2_common`. This drops the `starlark` dep from those three crates: removes the `StarlarkPagableViaPagable` derive from their types, and removes the orphan `ThinBoxSlice<T>: StarlarkSerialize/StarlarkDeserialize` impls from `buck2_util`. Affected downstream `StarlarkPagable` derives are updated at the use sites — either with `#[starlark_pagable(pagable)]` (pagable-only fields) or `#[starlark_pagable(serialize_with = "...", deserialize_with = "...")]` helpers (mixed pagable/starlark fields). The `StarlarkPagable` derive in `starlark_derive` is extended to accept `serialize_with`/`deserialize_with` on enum variant fields, matching the existing struct-field behavior. Foundation types touched (kept `Pagable`, dropped `StarlarkPagableViaPagable`): - `buck2_core`: `ImportPath`, `ProjectRoot`, `ProjectRelativePathBuf`, `ProviderId`, `ProvidersLabel`, `ConfiguredTargetLabel` - `buck2_fs`: `ForwardRelativePathBuf` - `buck2_util`: removed the `StarlarkSerialize`/`StarlarkDeserialize` orphan impls on `ThinBoxSlice<T>` Consumer bridging is added in `buck2_build_api` (artifact/value, cmd_args/options, provider/collection, resolve_query_macro) and `buck2_transition`. Reviewed By: christolliday Differential Revision: D104865040 fbshipit-source-id: f3b2d79caf9bd581219782fcace59cd3c3c18feagithub.com-facebook-buck2 · 25f4757c · 2026-05-13
- 0.4ETVFix asymmetric Box/Arc<dyn Trait> wire format via pagable_serialize_body split Summary: # The `Arc<dyn Trait>` / `Box<dyn Trait>` round-trip bug ## Setup A struct deriving `Pagable` holds a trait-object field: ```rust #[derive(Pagable)] pub struct AnimalHolder { pub animal: Arc<dyn Animal>, } ``` The `Animal` trait is a `#[pagable_typetag]` trait with `PagableTagged` as a supertrait, and (before the fix) `PagableTagged: PagableSerialize` as a further supertrait. A round-trip of `AnimalHolder` corrupts the wire and panics on deserialize: - "Hit the end of buffer, expected more data". ## Serialize path ```rust AnimalHolder::pagable_serialize (from `derive(Pagable)`) → calls pagable_serialize on each field Arc<dyn Animal>::pagable_serialize (blanket Arc<T> impl) → serializer.serialize_arc(self) TestingSerializer::serialize_arc → write Arc identity (usize) → for first occurrence: arc.serialize(self) ArcEraseDyn::serialize for Arc<dyn Animal> → ArcErase::serialize_inner(self, ser) ArcErase::serialize_inner for Arc<T>, T = dyn Animal → T::pagable_serialize(self, ser) → i.e. <dyn Animal as PagableSerialize>::pagable_serialize(self, ser) ← KEY HOP <dyn Animal as PagableSerialize>::pagable_serialize (compiler auto-generated) → dynamic-dispatch to the concrete type's PagableSerialize::pagable_serialize Wrapper<Cat>::pagable_serialize (from `derive(Pagable)`) → writes body fields only — NO TAG ``` ## Deserialize path ```rust AnimalHolder::pagable_deserialize (from `derive(Pagable)`) → calls pagable_deserialize on each field Arc<dyn Animal>::pagable_deserialize (blanket Arc<T> impl) → deserialize_arc::<Self, _>(de) deserialize_arc → reads Arc identity → for first occurrence: ArcErase::deserialize_inner(...) ArcErase::deserialize_inner for Arc<T>, T = dyn Animal → T::deserialize_box(deser) → i.e. <dyn Animal>::deserialize_box(deser) <dyn Animal>::deserialize_box (from `#[pagable_typetag]` on the trait) → reads tag (string) + body via the typetag registry ``` ## Where the bug is The asymmetry sits at one logical hop, present in both the `Box<dyn T>` and `Arc<dyn T>` paths: - **Serialize**: vtable-dispatched call to `<dyn Animal as PagableSerialize>::pagable_serialize`. - **Deserialize**: explicit call to `<dyn Animal>::deserialize_box`, which reads `tag + body`. The deserialize side does what's expected. The bug is on the serialize side: the vtable dispatch went somewhere wrong. ### Why the serialize side wrote body-only The chain of supertraits before the fix: - `trait Animal: PagableTagged + Send + Sync + Debug` - `trait PagableTagged: PagableSerialize + Send + Sync` So transitively `Animal: PagableSerialize`. The Rust language feature that bites: > For any object-safe trait `T`, the compiler automatically generates `impl T for dyn T`. By extension, also `impl Supertrait for dyn T` for each supertrait in the chain. So the compiler silently emits, in addition to `impl Animal for dyn Animal` and `impl PagableTagged for dyn Animal`, also: ```rust // Compiler-generated, no source code anywhere. impl PagableSerialize for dyn Animal { fn pagable_serialize(&self, ser: &mut dyn PagableSerializer) -> Result<()> { // dynamic-dispatch to the concrete type's PagableSerialize::pagable_serialize } } ``` So the serialized data would not have tag. ## The fix Three coordinated changes: ### 1. **Drop `PagableSerialize` from `PagableTagged`'s supertraits.** ```rust pub trait PagableTagged: Send + Sync { ... } // was: + PagableSerialize ``` Now `Animal: PagableSerialize` is no longer transitively required. The compiler stops auto-generating `impl PagableSerialize for dyn Animal`. ### 2. **Have `#[pagable_typetag]` emit an explicit `impl PagableSerialize for dyn Trait`.** This is now legal because the auto-impl is gone (otherwise E0371 — "the object type automatically implements the trait" — would fire). The explicit impl writes `tag + body` via `serialize_tagged`: ```rust impl PagableSerialize for dyn Animal { fn pagable_serialize(&self, ser) -> Result<()> { PagableTagged::serialize_tagged(self, ser) // writes tag + body } } ``` Now the "key hop" in the serialize path lands here, writing the matching wire format. ### 3. **Add `pagable_serialize_body` as a required method on `PagableTagged`.** `serialize_tagged` writes the tag, then needs to write the body. The naive approach — `<Self as PagableSerialize>::pagable_serialize(self, ser)` — recurses forever on `&dyn Animal`: that path resolves to the macro's impl from step 2, which calls `serialize_tagged` again. Adding `where Self: Sized` to dodge it doesn't help — `serialize_tagged` is meant to be called on `&dyn Animal`. Fix: give `PagableTagged` its own body-writing slot. Each concrete type forwards `pagable_serialize_body` to its own `PagableSerialize::pagable_serialize` (body only). `serialize_tagged` calls `self.pagable_serialize_body(...)`, which on `&dyn Animal` dispatches through the `PagableTagged` vtable to the concrete type's forwarder — bypassing `<dyn Animal as PagableSerialize>::pagable_serialize` entirely. No recursion; the wire is exactly `tag + body`. ## Before this change The test `test_pagable_derive_arc_dyn_trait_field_roundtrip` would fail Test UI: https://www.internalfb.com/intern/testinfra/testrun/11258999238093744 Reviewed By: cjhopman Differential Revision: D102513000 fbshipit-source-id: 2626f7a7229a9d68929600fbc3480d6477ee4595github.com-facebook-buck2 · 5dae95a4 · 2026-05-06
- 0.4ETVAdd FrozenAnyValue<T> newtype for serializable heap references Summary: Add `FrozenAnyValue<T>` as a type alias for `FrozenValueTyped<'static, StarlarkAny<T>>`. This is the replacement for `FrozenRef<T>` when `T` is allocated via `StarlarkAny<T>`. Unlike `FrozenRef`, `FrozenValueTyped` stores a `FrozenValue` internally, which can be directly serialized — enabling the starlark serde project. `FrozenAnyValue<T>` is a drop-in replacement for `FrozenRef<T>` when `T` is allocated via `StarlarkAny<T>`. It wraps a `FrozenValueTyped<'static, StarlarkAny<T>>` and can recover the underlying `FrozenValue` for serialization. `FrozenAnyValue` is a **newtype struct** rather than a type alias because: - A type alias inherits `FrozenValueTyped`'s `Display`, which goes through the Starlark repr path and prints `<any>` instead of the inner value - A type alias is covered by the blanket `BcInstrArg for FrozenValueTyped<T>` impl, which also prints `<any>` via `TruncateValueRepr`; a newtype allows a custom `BcInstrArg` that delegates to `T::Display` - The newtype provides `Deref<Target = T>` directly (skipping `StarlarkAny`), matching `FrozenRef`'s ergonomics Key changes: - Add `#[repr(transparent)]` and `Deref<Target = T>` to `StarlarkAny<T>` - Add `FrozenHeap::alloc_any_value` as the allocation entry point - Add `AllocStaticSimple<StarlarkAny<T>>::unpack_any()` for static allocations - Add `BcInstrArg` and `VisitSpanMut` impls for `FrozenAnyValue<T>` --- The benchmark of comparing this with ``` pub struct FrozenAnyValue<T: Debug + Send + Sync + 'static> { /// Direct pointer to the `T` inside `StarlarkAny<T>` on the frozen heap. ptr: &'static T, } ``` the cost is negligible. https://www.internalfb.com/phabricator/paste/view/P2266956733#summary Reviewed By: JakobDegen Differential Revision: D98410881 fbshipit-source-id: 7f3d707634954a8e9a6b28800c905c357157d560github.com-facebook-buck2 · 8b215c49 · 2026-04-23
- 0.4ETVAdd support for StarlarkAny vtable registration Summary: Adds `StarlarkAnyRegistered` marker trait, `StarlarkAnyBound` trait alias, and `register_starlark_any!` macro to properly register types for use with `StarlarkAny<T>` when the pagable feature is enabled. - `StarlarkAnyBound` is a trait alias: `Debug + Send + Sync + 'static` (+ `StarlarkAnyRegistered` when pagable) - `register_starlark_any!` implements `StarlarkAnyRegistered` and registers the vtable entry - `VtableRegistered` impl for `StarlarkAny<T>` now delegates to `T: StarlarkAnyBound` Differential Revision: D91365857 fbshipit-source-id: c72531c15d8d13b70bd29fe4052fb5a0f147baddgithub.com-facebook-buck2 · 11ec153f · 2026-04-21
- 0.4ETVAdd cross-heap FrozenValue reference support Summary: When a heap references values in a dependency heap (via `FrozenHeapRef`), those `FrozenValue` pointers won't be found in the current heap's offset map. This diff adds `CrossHeapPtr { heap_id, offset, is_str }` to the wire format and moves per-heap offset/base maps into shared state (`StarlarkSerState`/`StarlarkDeserState`) stored in `UserContext`. Refs (referred frozen heaps, inner is arc) are serialized via `serialize_arc`, which could defer actual serialization later — so there's no guarantee that a referenced heap is serialized before the current heap's arena values. So the context is not guaranteed to set up the offset map. To handle this, `ensure_offset_maps_registered_inner` recursively ensures offset maps are registered for the heap and all its transitive dependencies before serialization begins. This is needed because `FrozenValue` pointers in the current heap may reference values in dependency heaps, and the serializer needs their offset maps to resolve those pointers. The serializer tries the current heap first, then searches all referenced heaps. Referenced heaps (`refs`) are serialized and deserialized before the current heap's values, ensuring that cross-heap `FrozenValue` targets are always valid by the time they are resolved. Includes a cross-heap round-trip test. Reviewed By: christolliday Differential Revision: D96243750 fbshipit-source-id: 9062d0c3f96ab0d736a0b1b181eeb1f82ead5e17github.com-facebook-buck2 · 69a7dd11 · 2026-04-15
- 0.4ETV`StarlarkAny<T>` ser/de Summary: Adds `StarlarkSerialize`/`StarlarkDeserialize` for `StarlarkAny<T>` (delegating to `T`'s impl) and tightens `StarlarkAnyRegistered` to require `T: StarlarkPagable`. Derives `StarlarkPagable` on the codemap/frame types that flow through `FrozenAnyValue` (`FrameSpan`, `FrozenFileSpan`, `InlinedFrame`, `InlinedFrames`, `CopySlotFromParent`) and `Pagable` on bc index types (`BcSlot`, `BcSlotOut`, `LocalSlotIdCapturedOrNot`). Adds a manual `StarlarkSerialize`/`Deserialize` bridge for `CodeMap` (foreign type). Tightening `StarlarkAnyRegistered` forces a few transitive holdouts (`Globals`, `FrozenModuleData`, `DefInfo`, `UserProviderCallableData`) to satisfy `StarlarkPagable`; this diff stubs them with `PagablePanic` placeholders. `UserProviderCallableData` gets a real impl in [87/n], `FrozenModuleData` in [91/n], and `DefInfo` in [92/n]; `Globals` remains `PagablePanic` and will be handled in a later diff. Reviewed By: cjhopman Differential Revision: D102558116 fbshipit-source-id: 1abc6bb2d58a3cba118424fa99919cf55886b96fgithub.com-facebook-buck2 · 3839eb94 · 2026-05-08
- 0.3ETV`FrozenDynamicLambdaParamsStorageImpl` real `StarlarkPagable` via `#[starlark_pagable_typetag]` Summary: `FrozenDynamicLambdaParamsStorageImpl` participates in pagable typetag dispatch via `dyn FrozenDynamicLambdaParamsStorage`, but its body holds `FrozenValue`s in `lambda_params` that must resolve against the currently-(de)serializing starlark heap. `#[starlark_pagable_typetag]` emits the recovery bridge alongside the typetag registration, so starlark-domain typetag impls don't need a hand-written `PagableSerialize`/`PagableDeserialize` bridge. `#[starlark_pagable_typetag]` mirrors `#[pagable::pagable_typetag]`: - **Trait def**: wraps `#[pagable_typetag]` and emits a sealed marker `StarlarkTypetagTraitMarker for dyn Trait`. - **`impl Trait for Foo`**: wraps `#[pagable_typetag]`, emits the recovery bridge (`recover_from_pagable` + delegate to `StarlarkSerialize`/`StarlarkDeserialize`), and asserts the trait carries the marker. Reviewed By: christolliday Differential Revision: D102558111 fbshipit-source-id: c5be176554c42df25fb42ebdb79f2447460e8b6dgithub.com-facebook-buck2 · 9012710c · 2026-05-13
- 0.3ETVAdd `declare_starlark_value_as_type!` macro and `#[starlark_types]` attribute Summary: **Motivation**: `StarlarkValueAsType<T>` constants inside `#[starlark_module]` functions are static values that need inventory registration for pagable serialization. Previously these were defined as inline `const` items — but since they're generated by a proc macro, registration must also be generated. This diff takes a new approach compared to the original review: instead of having the proc macro detect `StarlarkValueAsType` constants and rewrite them, it introduces: 1. A `declare_starlark_value_as_type!` macro that creates a module-level static + `inventory::submit!` registration. 2. A `#[starlark_types(RustType as StarlarkName)]` attribute on `#[starlark_module]` functions, which the proc macro expands into `declare_starlark_value_as_type!` calls **outside** the function (generating registered module-level statics) + `globals_builder.set()` statements **inside** the function body (exposing the type names in the Starlark globals). Also adds `Clone`/`Copy` impls and `to_frozen_value()` to `StarlarkValueAsType` to support static usage, and adds a `StarlarkValueAsType` re-export via `__derive_refs`. Reviewed By: christolliday Differential Revision: D91644300 fbshipit-source-id: c9556778e10bb386e3a72c4c5c77cc611f0b3e62github.com-facebook-buck2 · 1275f890 · 2026-04-14
- 0.3ETVAdd SmallMap ser/de and wire ensure_initialized into FrozenValue deserialization Summary: Adds `StarlarkSerialize`/`StarlarkDeserialize` impls for `SmallMap<K, V>` with a `SmallMapKeyDeserialize` trait that bridges hashing for different key types (standard `Hash` types vs Starlark values that use `get_hashed()`). Also wires `ensure_initialized` into `deserialize_frozen_value` for same-heap pointers, so any FrozenValue reference automatically triggers deserialization of its target. Includes tests for `SmallMap<String, FrozenValue>`, `SmallMap<FrozenValue, FrozenValue>`, and `SmallMap<FrozenStringValue, FrozenValue>`. Note: `ensure_initialized` is currently not exposed on the `StarlarkDeserializeContext` trait since `deserialize_frozen_value` handles it automatically. It's kept on the impl for now in case it's needed elsewhere; if it turns out we don't need it, we'll clean it up. Reviewed By: christolliday Differential Revision: D96243765 fbshipit-source-id: 257c9da576c2e72047989575555c3127e44e8876github.com-facebook-buck2 · 07e501bb · 2026-04-15