luaur_common/records/f_value.rs
1//! Faithful port of Luau's FastFlag value `FValue<T>`. Reference:
2//! `luau/Common/include/Luau/Common.h` (the `FValue<T>` template, the
3//! `LUAU_FASTFLAG*` macros, `FValueVersionSetter`) and the list walkers in
4//! `luau/CLI/src/Flags.cpp`. Oracle: `/tmp/fastflag_proto.rs` (reads, enumerate,
5//! runtime-set, version-set-by-name, unknown-flag guard — all pass).
6//!
7//! Flags read as their `value` (the C++ `operator T()`, see [`FValue::get`]); a
8//! per-type intrusive list lets the host enumerate flags and set them / their
9//! version by name.
10//!
11//! Deviations from C++ (documented, behavior-faithful):
12//! - The per-`T` `static FValue<T>* list` is inexpressible in Rust (no generic
13//! statics), so the head is supplied by the [`FValueList`] trait, implemented
14//! for exactly the instantiated types (`bool`, `i32`).
15//! - The C++ ctor self-registers (`list = this`). Rust statics are
16//! const-initialized with no ctor side effects, so registration is the explicit
17//! [`FValue::register`], called once on a flag's `'static` instance. Reading a
18//! flag does NOT require registration — only enumeration / set-by-name does.
19//! - Public mutable fields become `UnsafeCell` + `unsafe impl Sync`, matching the
20//! C++ contract (flags configured before worker threads start, read-only after;
21//! C++ has no synchronization here either).
22//!
23//! Downstream `LUAU_FASTFLAGVARIABLE(Foo)` becomes a
24//! `static FOO: FValue<bool> = FValue::new(c"Foo", false, false);` registered at
25//! startup; `FFlag::Foo` reads become `FOO.get()`.
26
27use core::cell::UnsafeCell;
28use core::ffi::{c_char, c_uint};
29use core::sync::atomic::{AtomicBool, AtomicPtr, Ordering};
30
31// ---------------------------------------------------------------------------
32// Thread-local flag overrides (test isolation)
33//
34// C++ doctest runs single-threaded, so `ScopedFastFlag`/`ScopedFastInt` (which
35// mutate a process-global flag) are safe. Rust's libtest runs tests in PARALLEL
36// threads, so a global mutation by one test leaks into another reading the same
37// flag — producing nondeterministic failures (and, when a recursion/length
38// LIMIT leaks, runaway recursion that overflows a test thread's stack).
39//
40// Fix: a per-thread override layer. A scoped guard pushes its value onto a
41// thread-local stack for that flag; `get()` returns the current thread's
42// override when present, else the global. Mutations are thus private to the
43// thread (and the scope) that made them — parallel tests no longer interfere,
44// and the production path is unchanged.
45//
46// Cost in production: `get()` does one relaxed atomic load of `OVERRIDES_ACTIVE`
47// (set the first time any scoped guard runs — i.e. never, outside tests) before
48// the normal read. No cargo feature needed; the flag stays false in real runs.
49// ---------------------------------------------------------------------------
50
51/// Set true the first time a scoped override is pushed. Until then `get()` skips
52/// the thread-local lookup entirely.
53static OVERRIDES_ACTIVE: AtomicBool = AtomicBool::new(false);
54
55/// Has any thread-local override ever been installed in this process?
56#[inline]
57pub fn overrides_active() -> bool {
58 OVERRIDES_ACTIVE.load(Ordering::Relaxed)
59}
60
61/// Per-type thread-local override stacks, keyed by the flag's `'static` address.
62/// A `Vec` (stack) so nested scopes restore correctly and the entry is removed
63/// when the outermost scope ends (no stale leak across tests reusing a thread).
64pub trait FValueOverridable: Copy {
65 fn with_overrides<R>(
66 f: impl FnOnce(&mut std::collections::HashMap<usize, Vec<Self>>) -> R,
67 ) -> R;
68
69 fn override_top(addr: usize) -> Option<Self> {
70 Self::with_overrides(|m| m.get(&addr).and_then(|s| s.last().copied()))
71 }
72 fn override_push(addr: usize, value: Self) {
73 OVERRIDES_ACTIVE.store(true, Ordering::Relaxed);
74 Self::with_overrides(|m| m.entry(addr).or_default().push(value));
75 }
76 fn override_pop(addr: usize) {
77 Self::with_overrides(|m| {
78 if let Some(stack) = m.get_mut(&addr) {
79 stack.pop();
80 if stack.is_empty() {
81 m.remove(&addr);
82 }
83 }
84 });
85 }
86}
87
88thread_local! {
89 static BOOL_OVERRIDES: core::cell::RefCell<std::collections::HashMap<usize, Vec<bool>>> =
90 core::cell::RefCell::new(std::collections::HashMap::new());
91 static INT_OVERRIDES: core::cell::RefCell<std::collections::HashMap<usize, Vec<i32>>> =
92 core::cell::RefCell::new(std::collections::HashMap::new());
93}
94
95impl FValueOverridable for bool {
96 fn with_overrides<R>(
97 f: impl FnOnce(&mut std::collections::HashMap<usize, Vec<Self>>) -> R,
98 ) -> R {
99 BOOL_OVERRIDES.with(|c| f(&mut c.borrow_mut()))
100 }
101}
102
103impl FValueOverridable for i32 {
104 fn with_overrides<R>(
105 f: impl FnOnce(&mut std::collections::HashMap<usize, Vec<Self>>) -> R,
106 ) -> R {
107 INT_OVERRIDES.with(|c| f(&mut c.borrow_mut()))
108 }
109}
110
111impl<T: FValueOverridable> FValue<T> {
112 /// Install a thread-local override for this flag (used by the test scope
113 /// guard `ScopedFValue`). Visible only to the current thread until popped.
114 pub fn push_test_override(&self, value: T) {
115 T::override_push(self as *const FValue<T> as usize, value);
116 }
117
118 /// Remove the most recent thread-local override for this flag.
119 pub fn pop_test_override(&self) {
120 T::override_pop(self as *const FValue<T> as usize);
121 }
122}
123
124pub struct FValue<T> {
125 pub(crate) value: UnsafeCell<T>,
126 pub(crate) dynamic: bool,
127 pub(crate) name: *const c_char,
128 pub(crate) next: UnsafeCell<*const FValue<T>>,
129 pub(crate) version: UnsafeCell<c_uint>,
130}
131
132// See the module deviation note: flags are configured before threads start and
133// treated as read-only afterwards.
134unsafe impl<T: Sync> Sync for FValue<T> {}
135
136/// Supplies the per-type intrusive-list head, replacing the inexpressible C++
137/// `static FValue<T>* list`. Implemented for exactly the instantiated types.
138
139/// C++ `setLuauFlagsDefault(bool)` analog (CLI default-on behavior): walk the
140/// bool-flag registry and set every non-Debug flag. Call before threads start.
141pub fn set_luau_bool_flags(value: bool) {
142 unsafe {
143 let mut cur = <bool as FValueList>::head().load(Ordering::Relaxed) as *const FValue<bool>;
144 while !cur.is_null() {
145 let name = core::ffi::CStr::from_ptr((*cur).name);
146 if !name.to_bytes().starts_with(b"Debug") {
147 *(*cur).value.get() = value;
148 }
149 cur = *(*cur).next.get();
150 }
151 }
152}
153
154pub trait FValueList: Sized {
155 fn head() -> &'static AtomicPtr<FValue<Self>>;
156}
157
158static FVALUE_LIST_BOOL: AtomicPtr<FValue<bool>> = AtomicPtr::new(core::ptr::null_mut());
159impl FValueList for bool {
160 fn head() -> &'static AtomicPtr<FValue<bool>> {
161 &FVALUE_LIST_BOOL
162 }
163}
164
165static FVALUE_LIST_INT: AtomicPtr<FValue<i32>> = AtomicPtr::new(core::ptr::null_mut());
166impl FValueList for i32 {
167 fn head() -> &'static AtomicPtr<FValue<i32>> {
168 &FVALUE_LIST_INT
169 }
170}
171
172impl<T: Copy> FValue<T> {
173 /// Runtime flag set (the CLI/host path mutates the public `value` field).
174 pub fn set(&self, value: T) {
175 unsafe { *self.value.get() = value };
176 }
177
178 /// Current `version` (0 unless a `LUAU_FLAGVERSION` setter ran).
179 pub fn version(&self) -> c_uint {
180 unsafe { *self.version.get() }
181 }
182
183 pub(crate) fn set_version(&self, version: c_uint) {
184 unsafe { *self.version.get() = version };
185 }
186}
187
188impl<T: FValueList> FValue<T> {
189 /// The C++ ctor side effect `next = list; list = this;`. Call once, on the
190 /// flag's `'static` instance, after construction.
191 ///
192 /// # Safety
193 /// Must be called at most once per flag, before any concurrent list walk
194 /// (registration is single-threaded startup work, as in C++).
195 pub unsafe fn register(&'static self) {
196 let head = T::head();
197 let old = head.load(Ordering::Relaxed);
198 *self.next.get() = old as *const FValue<T>;
199 head.store(
200 self as *const FValue<T> as *mut FValue<T>,
201 Ordering::Relaxed,
202 );
203 }
204}
205
206impl<T: FValueList + Copy + 'static> FValue<T> {
207 /// Walk the per-type `FValue<T>::list` and set every flag whose `name`
208 /// matches to `value` (the C++ test harness `setFastValue<T>(name, value)`
209 /// in `tests/main.cpp`). Configured at startup before worker threads — the
210 /// same single-threaded contract as flag construction.
211 pub fn set_value_by_name(name: &str, value: T) {
212 unsafe {
213 let mut cur = T::head().load(Ordering::Relaxed) as *const FValue<T>;
214 while !cur.is_null() {
215 let fvalue = &*cur;
216 if core::ffi::CStr::from_ptr(fvalue.name).to_bytes() == name.as_bytes() {
217 *fvalue.value.get() = value;
218 }
219 cur = *fvalue.next.get();
220 }
221 }
222 }
223
224 /// Walk the per-type `FValue<T>::list` and set every flag to `value` unless
225 /// the host-supplied `skip` predicate (matched on the flag's UTF-8 name)
226 /// returns true. Models the bool `--fflags=true|false` branch of the C++
227 /// test harness `setFastFlags`, which sets every non-skipped flag. Startup-
228 /// only, single-threaded — the same contract as flag construction.
229 pub fn set_all_unless(value: T, skip: impl Fn(&str) -> bool) {
230 unsafe {
231 let mut cur = T::head().load(Ordering::Relaxed) as *const FValue<T>;
232 while !cur.is_null() {
233 let fvalue = &*cur;
234 let name = core::ffi::CStr::from_ptr(fvalue.name)
235 .to_str()
236 .unwrap_or("");
237 if !skip(name) {
238 *fvalue.value.get() = value;
239 }
240 cur = *fvalue.next.get();
241 }
242 }
243 }
244}