Starlark in Rust

repository·main·Indexed 21 days ago

https://github.com/facebook/starlark-rust

A 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.

Tokens
50K
Snippets
164
Records
236
Agent score
76%

What's inside starlark-rust

  1. Overview of Starlark in Rust

    main
    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.
  2. Use Allocative for memory introspection and profiling

    main

    Allocative 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 bumpalo bumps).
    • Profiling specific subsets of process data (e.g., measuring an RPC response before serialization).

    To use it, you must implement the Allocative trait for the types you wish to measure. This is typically done using a procedural macro provided by the crate.

  3. Understand Go evaluation test cases

    main

    This 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_go function in the original repository.

  4. Use the Dupe trait for cheap clones

    main

    The gazebo::dupe module provides the Dupe trait with a dupe method. This is conceptually similar to Clone, but intended for types where cloning is extremely inexpensive (e.g., Arc or usize).

    Mental Model: Use dupe instead of clone to distinguish between cheap operations and expensive reallocations (like String or Vec). This makes it easier to audit code for performance by making expensive clone calls stand out.

  5. Compare Allocative with call-stack malloc profilers

    main

    When deciding between Allocative and a tool like the jemalloc heap profiler, consider these differences:

    FeatureAllocativeCall-stack Malloc Profiler
    Primary ViewObject-by-object treeCall stack
    SetupRequires Allocative trait implementationUsually requires no code changes
    Memory GapsShows spare capacity and paddingFocuses on allocation sites
    Allocation TypesSupports non-malloc (e.g., bump allocators)Primarily tracks malloc
    GranularityCan profile specific subsets of dataProfiles the entire heap
  6. Internal Optimization: Slots and Names

    main

    To optimize evaluation, Starlark accesses variables via integer 'slots' rather than string lookups.

    Slots

    Variables are stored in a Slots structure. A slot can be:

    • Frozen: Used for variables from imported modules (stored behind an Arc).
    • Slots: Mutable variables for the current scope (stored behind Rc<RefCell<Vec<Option<Value>>>>).

    Names

    The mapping from a string name to a slot index is managed by the Names enum:

    • Frozen(FrozenNames): Uses Arc<HashMap<String, usize>> for immutable mappings.
    • Names(Rc<RefCell<HashMap<String, usize>>>): Uses Rc<RefCell<...>> for mutable mappings.

    Freezing

    When a scope is frozen (e.g., when a module is imported), the mutable Names and Slots are consumed and replaced with their Frozen variants. Warning: It is unsafe to access the original mutable slots after a freeze operation 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>>);
  7. How Starlark environments and scoping work

    main

    Starlark 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) where x is 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 global
    x = []
    def foo():
        y = True
        def bar():
            z = 1
            list.append(x, 1)
  8. Understand Starlark Heaps and Heap References

    main

    Starlark manages memory through three distinct heap types, each with different ownership and sharing rules:

    • Heap: Allocates Values. It cannot be cloned or shared.
    • FrozenHeap: Allocates FrozenValues. It cannot be cloned or shared.
    • FrozenHeapRef: A read-only version of a FrozenHeap. It can be cloned and shared, and it is responsible for keeping the underlying FrozenHeap alive.

    When a value from one heap is used to construct a value in another heap (e.g., putting a FrozenValue from h1 into a list in h2), 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.

  9. Understand Starlark value representation and freezing

    main

    Starlark 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.

  10. How thaw-on-write optimization works

    main

    To 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.

  11. List Comprehension Environments

    main

    List 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 existing x in 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 Slots data type without adding them to the Names mapping.
    [x for x in [1,2,3]]
  12. Immutable containers of mutable data (Pseudo values)

    main

    Certain 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 like a.b()) fall into this category.