Starlark in Rust
repository·main·Indexed 21 days ago
https://github.com/facebook/starlark-rustA deterministic, Python-inspired configuration language implementation written in Rust, used by build systems like Buck2. The project includes the main starlark library for embedding, starlark_lsp for IDE integration, and supporting crates such as starlark_syntax and starlark_map. It also provides utility libraries like Allocative for memory profiling and gazebo for common Rust type extensions.
What's inside starlark-rust
- Starlark in Rust is a deterministic language implementation inspired by Python3, designed for configuration in build systems like Bazel, Buck, and Buck2. It is a Rust-based implementation of the Starlark specification, offering easy interoperability between Rust types and Starlark values. Key features include garbage collection, optional runtime-checked types, a linter, LSP support, and DAP support.
Use Allocative for memory introspection and profiling
mainAllocative is a lightweight memory profiler for Rust that enables object traversal and memory size introspection. Unlike traditional malloc profilers that focus on call stacks, Allocative provides an object-by-object tree view.
Key capabilities include:
- Traversing values to collect their size and the size of referenced objects.
- Identifying memory gaps, such as spare capacity in collections or excessive padding in structs/enums.
- Profiling non-malloc allocations (e.g., within
bumpalobumps). - Profiling specific subsets of process data (e.g., measuring an RPC response before serialization).
To use it, you must implement the
Allocativetrait for the types you wish to measure. This is typically done using a procedural macro provided by the crate.Understand Go evaluation test cases
mainThis directory contains a mirrored set of evaluation test cases from the original Go Starlark project. These test cases are used to verify the correctness of the Starlark implementation.
Note on selection: Not all files from the original source were included. Some files were excluded because they are considered unsuitable tests for Starlark, as determined by the
test_gofunction in the original repository.Use the Dupe trait for cheap clones
mainThe
gazebo::dupemodule provides theDupetrait with adupemethod. This is conceptually similar toClone, but intended for types where cloning is extremely inexpensive (e.g.,Arcorusize).Mental Model: Use
dupeinstead ofcloneto distinguish between cheap operations and expensive reallocations (likeStringorVec). This makes it easier to audit code for performance by making expensiveclonecalls stand out.Compare Allocative with call-stack malloc profilers
mainWhen deciding between Allocative and a tool like the
jemallocheap profiler, consider these differences:Feature Allocative Call-stack Malloc Profiler Primary View Object-by-object tree Call stack Setup Requires Allocativetrait implementationUsually requires no code changes Memory Gaps Shows spare capacity and padding Focuses on allocation sites Allocation Types Supports non-malloc (e.g., bump allocators) Primarily tracks mallocGranularity Can profile specific subsets of data Profiles the entire heap Internal Optimization: Slots and Names
mainTo optimize evaluation, Starlark accesses variables via integer 'slots' rather than string lookups.
Slots
Variables are stored in a
Slotsstructure. A slot can be:Frozen: Used for variables from imported modules (stored behind anArc).Slots: Mutable variables for the current scope (stored behindRc<RefCell<Vec<Option<Value>>>>).
Names
The mapping from a string name to a slot index is managed by the
Namesenum:Frozen(FrozenNames): UsesArc<HashMap<String, usize>>for immutable mappings.Names(Rc<RefCell<HashMap<String, usize>>>): UsesRc<RefCell<...>>for mutable mappings.
Freezing
When a scope is frozen (e.g., when a module is imported), the mutable
NamesandSlotsare consumed and replaced with theirFrozenvariants. Warning: It is unsafe to access the original mutable slots after afreezeoperation has been called.enum Slots { Frozen(FrozenSlots), Slots(Rc<RefCell<Vec<Option<Value>>>>), } struct FrozenSlots(Arc<Vec<Option<FrozenValue>>>); enum Names { Frozen(FrozenNames), Names(Rc<RefCell<HashMap<String, usize>>>), } struct FrozenNames(Arc<HashMap<String, usize>>);How Starlark environments and scoping work
mainStarlark uses a hierarchy of environments (scopes) that allow nested functions to access and mutate variables from outer scopes, but not assign to them.
Scoping Rules
- Access: A nested scope can read variables from any parent scope (e.g., a function can read a module-level variable).
- Mutation: A nested scope can mutate existing objects (e.g.,
list.append(x, 1)wherexis defined in an outer scope). - Assignment (Shadowing): Using the assignment operator (
=) inside a scope creates a new local variable that shadows the outer variable. You cannot assign to a variable defined in an outer scope from within a nested scope. - Reference Errors: Assigning to a variable anywhere in a function makes it local to that function. If you attempt to read it before that assignment line is executed, Starlark will raise a 'referenced before assignment' error.
Environment Hierarchy Example
x = [] # Module environment def foo(): y = True # Environment of foo def bar(): z = 1 # Environment of bar list.append(x, 1) # Accesses x from module, list.append from globalx = [] def foo(): y = True def bar(): z = 1 list.append(x, 1)Understand Starlark Heaps and Heap References
mainStarlark manages memory through three distinct heap types, each with different ownership and sharing rules:
Heap: AllocatesValues. It cannot be cloned or shared.FrozenHeap: AllocatesFrozenValues. It cannot be cloned or shared.FrozenHeapRef: A read-only version of aFrozenHeap. It can be cloned and shared, and it is responsible for keeping the underlyingFrozenHeapalive.
When a value from one heap is used to construct a value in another heap (e.g., putting a
FrozenValuefromh1into a list inh2), you must ensure the source heap is added as a reference to the destination heap to prevent the source from being deallocated while in use.Understand Starlark value representation and freezing
mainStarlark distinguishes between frozen and unfrozen values to allow for safe, parallel sharing of imported modules without expensive copying.
- Frozen values: These are values imported from modules. They are immutable and use atomic reference counting so they can be safely shared across multiple threads.
- Unfrozen values: These are values defined locally within a module. Since module execution is single-threaded, these can be mutated and use non-atomic reference counting for performance.
Once a module finishes executing, its local values are frozen and can be reused freely by other parts of the program.
How thaw-on-write optimization works
mainTo optimize performance when functions return list literals (which are technically unfrozen and mutable), Starlark uses a thaw-on-write strategy.
Instead of returning a fully mutable list, the interpreter can return a shared reference to a frozen list. If a caller attempts to mutate this list, the interpreter 'thaws' the value by copying it into a mutable variant. This allows most callers to treat the returned value as immutable without the overhead of constant copying, only paying the cost if mutation actually occurs.
List Comprehension Environments
mainList comprehensions (e.g.,
[x for x in [1,2,3]]) create a specialized, temporary environment.- The loop variable (e.g.,
x) is immediately initialized and shadows any existingxin the outer scope. - The variable only lives inside the comprehension and is not available at the top-level of the module.
- List comprehensions do not permit assignment statements, only expressions.
- Internally, these are implemented by adding entries to the
Slotsdata type without adding them to theNamesmapping.
[x for x in [1,2,3]]- The loop variable (e.g.,
Immutable containers of mutable data (Pseudo values)
mainCertain data types in Starlark are considered Pseudo values. These are types that are themselves immutable but contain references to mutable data.
Key characteristics:
- They cannot be mutated themselves.
- They can be non-atomically ref-counted.
- All types that can be invoked as functions (such as
lambda,def, or method calls likea.b()) fall into this category.