Hylo Programming Language Documentation

repository·main·Indexed 23 days ago

https://github.com/hylo-lang/hylo

Documentation for Hylo (formerly Val), a programming language for high-level systems programming featuring mutable value semantics and generic programming. This resource covers the hc compiler CLI, the multi-stage compilation pipeline (Tokenisation, Parsing, Type-checking, IR-lowering, and LLVM generation), AST structure, and project conventions for contributors.

Tokens
12.3K
Snippets
35
Records
65
Agent score
82%

What's inside Hylo

  1. What is a generic environment in Hylo?

    main

    In Hylo, a generic signature is a set of generic parameter declarations and their associated constraints. To make sense of these signatures, the compiler constructs a generic environment.

    A generic environment is a set of axioms used by the type system to prove properties about type or value terms. For example, if a signature constrains a type T to conform to Collection, the environment allows the compiler to prove that T.Element exists and has certain members (like infix+).

    Key points:

    • Trait declarations also define generic environments. All traits include an implicit Self parameter.
    • Constraints on associated types and values add further axioms to the environment.
    • The environment is used during name resolution to determine trait conformance and associated type/value lookups.
  2. What is an Addressor subscript?

    main

    An Addressor is a specific type of subscript that does not synthesize (create) the value it projects. Instead, it exposes a stored part of one of its arguments.

    Because it is exposing an existing address rather than creating new data, an Addressor does not require "slide" functions. It can simply compute the address of the projected value within its ramp and clean up its stack before returning.

  3. What is Definite (De)Initialization (DI)?

    main

    Definite (De)Initialization (DI) is a mandatory transformation pass applied to Hylo's intermediate representation (IR) before code generation. It ensures that all objects are treated as linear resources by enforcing two primary guarantees:

    1. Definite Initialization: All objects must be fully initialized before they are used (e.g., via loading, starting a let binding, or an inout binding).
    2. Definite Deinitialization: All objects must be deinitialized before the end of their storage's lifetime (e.g., before deallocating the storage or starting a set borrow on uninitialized/consumed storage).

    If the compiler cannot guarantee these properties, the DI pass will fail with a diagnostic error.

  4. Understand the Hylo generics compilation model

    main

    Hylo's generics design balances algorithm efficiency with the ability to maintain opaque module boundaries.

    • Separate Type Checking: Generic functions are type-checked independently. This prevents users from encountering type errors inside a generic function's body that only trigger during specific usage patterns.
    • Opaque Module Boundaries: A module's private implementation details (including type layouts and function implementations) can change without requiring clients to recompile. This enables binary-compatible library evolution.
    • Optimization: While separate compilation can introduce costs when crossing boundaries, Hylo supports an optimization step that eliminates these costs without changing semantics. This optimization can be applied globally for performance or selectively at module boundaries to preserve binary compatibility.
  5. How subscripts are implemented in Hylo IR

    main

    In Hylo, a subscript is lowered as a single Hylo IR function. This function has no return value but must follow the 1-yield rule: it is expected to execute exactly one yield instruction on every possible execution path from the entry to each return instruction.

    At the call site, a subscript is invoked using the project instruction. The lifetime pass ensures that a projection is closed with an end_project instruction after its last use.

    subscript sum(_ x: Int, _ y: Int): Int { x + y }
    
    fun foo(a: Int, b: Int) {
      let x = sum(a, b)
      print(x)
    }
  6. Understand generic type identity and conformance constraints

    main

    In Hylo, a generic type's identity is tied to the conformances of its arguments. If a generic type's arguments satisfy constraints using different conformances, the generic type itself is treated as a different type.

    To maintain soundness, a generic type with a concrete argument cannot "escape" into a context where:

    1. It would depend on different conformances for that argument.
    2. The conformances the argument depends on are not in scope or are not satisfied.

    Common Error Scenarios:

    • Conformance Mismatch: Attempting to use a generic type from a module where the concrete argument's conformance differs from the one used to construct it in the original module.
    • Visibility Errors: Attempting to use a generic type where the required conformance for its arguments is not in scope (e.g., a private conformance).
    // module A
    public trait P { fun boo() -> Int }
    public type X<T: P> { public memberwise init }
    
    // module B
    import A
    private conformance Int: P {
      public fun boo() -> Int { return 3 }
    }
    let x = X<Int>()
    
    // module C
    import B
    private conformance Int: P {
      public fun boo() -> Int { return 4 }
    }
    let x = X<Int>() // OK
    let bx = B.x     // Error: X<Int> depends on a different conformance Int: P
    
    // module D
    import B
    
    // Error: X<Int> depends on conformance Int: P, which is not in scope
    let bx = B.x
  7. Define traits and conformances for generics

    main

    Hylo's generic model is built on traits and conformances:

    • Traits: Define requirements for a concept.
      • Method requirements: Signatures that must be implemented in a conformance.
      • Associated type requirements: Types defined within the trait (e.g., type Element).
      • Trait refinement: A trait can inherit from or refine another trait.
      • Default implementations: Traits can provide default logic for requirements.
    • Conformances: Explicitly specify how a type satisfies a trait's requirements.
      • Conditional (Bounded) Conformance: A type can conform to a trait only if its own type parameters meet certain criteria (e.g., conformance<T> Y<T>: Equatable where T: Equatable).
      • Type Equality Bounds: Constraints that require two types to be identical (e.g., where Element == M.Element).
    trait Equatable {
      fun equals(_: Self) -> Bool
    }
    
    trait Hashable: Equatable {
      fun hash(into: inout Hasher)
    }
    
    type Y<T> {
      var a: T
    }
    
    conformance<T> Y<T>: Equatable where T: Equatable {
      fun equals(_ other: Y)-> Bool {
        return self.a.equals(other.a)
      }
    }
  8. Understand the Hylo compilation pipeline

    main

    The Hylo compiler follows a multi-stage pipeline to transform source code into machine code. The process is orchestrated by the Driver module. Depending on the flags provided to the compiler, execution can exit early at any of these stages:

    1. Tokenisation: Converts Hylo source strings into a stream of tokens (see Lexer.swift, Token.swift).
    2. Parsing: Generates an Abstract Syntax Tree (AST) from tokens (see Parser.swift).
    3. Type-checking: Validates the AST for type errors (see TypeChecking module).
    4. IR-lowering: Generates Hylo Intermediate Representation (IR) from the AST (see IR module, Emitter.swift).
    5. LLVM IR generation: Converts Hylo IR into LLVM IR (see Transpilation.swift).
    6. Machine Code Generation: Handled entirely by the LLVM backend.
  9. How Hylo IR is generated and validated

    main

    Hylo IR is composed of instructions defined in the Instruction module.

    Generation Workflow:

    1. The Emitter component creates the IR.
    2. The Emitter inserts instructions into an IR/Module module-by-module.
    3. These modules are aggregated into an IR/Program.

    Validation: Hylo IR is only considered valid after it has undergone mandatory analysis passes defined in the IR/Analysis module (specifically within Module+* files). Once these passes are complete, the IR is valid and executable by a theoretical Hylo VM.

  10. Subscripts-as-sessions compilation approach

    main

    The subscripts-as-sessions approach also uses a ramp and slide structure, but the ramp returns the yielded value directly instead of calling a continuation.

    Key Characteristics:

    • Memory Management: To avoid dynamic allocation, the ramp accepts a pointer to a pre-allocated buffer representing its stack. The caller pushes this onto its own stack before the call.
    • Lifecycle: The caller initiates a "session" by calling the ramp and terminates it by calling a slide.
    • Trade-off: This approach requires code transformation and, to maintain ABI stability, requires careful management of frame sizes (often requiring a helper function to return size and alignment).
  11. How Local Storage works in Hylo

    main
    A local binding has 'storage' if its value can be stored at a memory location allocated specifically for that binding. The lifetime of this storage is tied to the lexical scope of the binding. In Hylo's IR, this lifetime corresponds to the live-range of an alloc_stack instruction. In a well-formed program, every alloc_stack must be paired with exactly one dealloc_stack on every possible execution path.
  12. How specialization and dispatching work in Hylo generics

    main

    Hylo uses customization points to allow algorithms to optimize their behavior based on the properties of the data structures they operate on. This is achieved through traits and conformances.

    For example, a generic reverse() algorithm might have a default $O(N \log N)$ implementation, but can be specialized to $O(N)$ for types that conform to BidirectionalCollection. Similarly, a Matrix multiplication algorithm can dispatch to high-performance libraries like BLAS when the Element type is a Float.

    trait MutableCollection {
      // ...
      fun reverse() inout { /* O(N log N) implementation */ }
    }
    
    conformance<T: MutableCollection & BidirectionalCollection> T: MutableCollection {
      fun reverse() inout { /* O(N) implementation */ }
    }