Vale Programming Language

repository·master·Indexed 24 days ago

https://github.com/valelang/vale

A high-performance, memory-safe, statically-typed programming language compiled to LLVM. Vale utilizes generational references instead of a garbage collector or borrow checker to achieve memory safety. The documentation covers language features, compiler architecture (Frontend, Backend, and Coordinator), build instructions for Ubuntu, macOS, and Windows, and detailed memory layouts for Sparse Arrays, Sparse Lists, and Sparse Stable Lists.

Tokens
32.8K
Snippets
67
Records
175
Agent score
84%

What's inside Vale

  1. What is Vale

    master

    Vale is a fast, memory-safe, and easy-to-use programming language. It is AOT (Ahead-of-Time) compiled to LLVM and is statically-typed.

    Key features include:

    • Memory Safety: Achieved through generational references and Fearless FFI, providing single ownership without a garbage collector or a traditional borrow checker.
    • Performance: High speed due to LLVM compilation and upcoming region borrow checking.
    • Ease of Use: Designed to be flexible while maintaining safety.
  2. Detect Territory Changes using FFOP

    master

    A territory change occurs when a final owning reference inside a varying inline object is changed (e.g., replacing one Spaceship with another). While the type remains stable, the indirectly owned memory area changes.

    To detect this, you can use the First Final Owned Pointer (FFOP). By remembering the FFOP of an inline object, you can check if it has changed. If the FFOP differs, it indicates that the underlying hierarchy (the 'territory') has been replaced.

  3. Understand the Default Region and Mutable Regions

    master

    Every function in Vale receives a mutable region, often referred to as the default region.

    • Mutable Regions: By default, every function receives a mutable region. When a user names the default region in a function body, they are naming this incoming mutable region.
    • Pure Functions: Pure functions receive a default region that is guaranteed to contain none of the function's parameters.
    • Caller Control: While the default region is implicit, a caller can gain control over which region is used by passing a specific region as a parameter. The implementation or override can then switch to using that passed-in region as its own default region.
  4. Implicit Default Regions for Functions and Structs

    master

    Every 'denizen' (function or struct) in Vale has an implicit self-region. While you can omit region annotations, they are conceptually present to track ownership and lifetime.

    For a function, the default region acts as the ambient region for its local variables. For a struct, the default region identifies the region in which the struct's own fields reside.

    Example: Implicit Function Regions

    // Explicit version of a standard function
    func main<m' rw>(args m'[]str) m' {
      list = m'List<int>(0);
      println(list.len());
    }

    Example: Implicit Struct Regions

    // Explicit version of a struct
    struct HashMap<K, V, x'> x' {
      size x'int;
      arr x'[]HashMapNode<K, V>;
    }
    // Standard function with implicit regions
    func main(args []str) {
      list = List<int>(0);
      println(list.len());
    }
    
    // Standard struct with implicit regions
    struct HashMap<K, V> {
      size int;
      arr []HashMapNode<K, V>;
    }
  5. Understand how instantiation bounds are resolved

    master

    In Vale, generics use instantiation bounds to ensure that types passed to generic functions or structs satisfy specific requirements (e.g., implementing a certain function like drop).

    Key behaviors regarding bounds:

    1. Namespace Consistency: To avoid ambiguity during overload resolution and substitution, bounds are renamed to reside in the original denizen's (the function or struct being defined) namespace. This ensures all bounds have a consistent full ID regardless of their origin.
    2. Bound Matching: For a function FunctionT, the instantiationBoundParams (of type InstantiationBoundArgumentsT) must match the runes used at the call site. This ensures that the arguments provided during a call align with the requirements defined in the function's signature.
    3. Ambiguity in Overlapping Bounds: If multiple function overloads could satisfy a bound requirement, the compiler may face ambiguity. For example, if a bound requires the existence of a function moo(P, Q), and there are two different overloads of moo that could match the provided types, the resolution may be ambiguous. The system aims to perform checks at the call site to mitigate this.
  6. How spans and shared generations work for arrays in HGM v13

    master

    HGM v13 introduces the concept of a "span" for arrays, where a generation is shared every $N$ bytes.

    Implementation Details:

    • Span Width: For example, a span could be 491,520 bytes (480kb), which is 3,840 chunks wide (less than $2^{12}$ chunks, requiring a 12-byte offset).
    • Indexing Logic: To access an element, calculate the beginning of the span using (index / 2^11). Read the generation at that location, move 16 bytes past it, and then add (index % 2^11) to locate the specific element.
    • Random Access: To support random access within these structures, 8 bytes are reserved before every object.
  7. Use Weakables with LGT in HGM V14

    master
    Because HGM V14 allows skipping generation increments for certain inline elements, the generation number can no longer be used as a reliable indicator of whether a weakable object is dead. To track the lifecycle of weakable objects in this version, you must use an LGT.
  8. How Vale handles serialized offsets in linear buffers

    master

    When reading structs from a linear buffer that uses offsets instead of pointers (e.g., when writing to a file), Vale does not immediately translate these offsets into pointers using a Serialized Address Adjuster upon loading.

    Instead, Vale follows a 'lazy translation' approach: when a reference is read from the serialized buffer, it may remain as an offset. The translation from an offset to a valid pointer occurs only when the program eventually attempts to load from the specific struct or array containing that reference. This avoids the complexity of recursively translating entire hierarchies of fat pointers, interfaces, or enums immediately upon loading.

  9. Understanding HGM (Heap Generation Management) and Regions

    master

    Vale primarily uses regions for memory safety, which has largely obviated the need for HGM. However, HGM is considered for scenarios where more precise control is needed within a region to eliminate generation checks (gen checks).

    A common problem occurs when passing a non-owning reference to a function (like push in a collection) where the compiler cannot guarantee the reference remains alive, potentially triggering a generation check.

    Proposed strategies to handle this include:

    • Inlining: Allowing the compiler to see that an owning reference is still in scope.
    • Live Refs: Using a live keyword on parameters to monomorphize functions (creating one version for when the value is known to be alive and one for when it isn't).
    • Trackable Types: Marking types as trackable to include metadata (like a boolean bit) within the type or its padding to facilitate live references.
    • Trackable Instances: Overriding specific instances to be trackable even if the type itself is not inherently trackable.
  10. Green Threads and Compaction in HGM

    master

    The HGM architecture provides two key benefits for runtime execution:

    • Green Threads: Because referenceable data resides in a linked stack off to the side and nothing refers to the regular execution stack, implementing green threads is highly efficient. An executor can manage multiple green threads, and since an executor is typically owned by a single core, these threads can share memory easily.
    • Compaction: Because the system defines its own structs within the linked stack, it can perform memory compaction as a last resort. This involves tracing from the linked stack through all reachable objects to create a list and compacting the memory to reduce fragmentation.
  11. How dot-access substitution works (SBITAFD)

    master

    When accessing a member of a generic struct using dot notation (e.g., self.table), Vale performs a substitution to rephrase the accessed type in terms of the current denizen's placeholders. This ensures that nested generic types are correctly mapped to the local generic context.

    #!DeriveStructDrop
    struct HashMapNode<X Ref imm> {
      key X;
    }
    
    #!DeriveStructDrop
    struct HashMap<T Ref imm> {
      table! Array<mut, HashMapNode<T>>;
    }
    
    func keys<K Ref imm>(self &HashMap<K>) {
      // Accessing self.table substitutes the internal HashMapNode<T> 
      // with HashMapNode<keys$K> to match the current context.
      self.table.len();
    }
  12. How generic placeholders are resolved (IRAGP)

    master

    Vale uses an incremental approach to resolving generic placeholders. Instead of populating all placeholders at once, the compiler populates them one at a time and performs a 'solve' step in between each new placeholder. This prevents conflicts where multiple placeholders might otherwise violate constraints (e.g., when two placeholders are required to be equal).

    Example of a function that requires incremental solving to avoid conflicts:

    func bork<T, Y>(a T) Y where T = Y { return a; }

    If T and Y were populated simultaneously, they would be treated as distinct placeholders, causing the T = Y constraint to fail. By populating them incrementally, the solver can unify them.