Bend Programming Language

repository·main·Indexed 12 days ago

https://github.com/higherorderco/bend

A high-level, massively parallel programming language powered by the HVM2 runtime. Bend allows developers to write expressive code that automatically scales to GPUs without explicit concurrency primitives, using a divide-and-conquer approach to achieve parallelism. It includes multiple interpreters (C, Rust, and CUDA) and a compiler that transforms high-level terms into HVM interaction net nodes.

Tokens
33.3K
Snippets
148
Records
179
Agent score
94%

What's inside Bend

  1. Recursive Data Structures with Bend

    main

    The bend keyword creates recursive data structures by binding a variable to the return of an inline recursive function.

    Inside the when arm, the fork function is available to call the function recursively. fork must receive the same number of arguments as the number of state variables defined in the bend statement.

    main =
      bend x = 0 {
        when (< x 3):
          (Tree/Node (fork (+ x 1)) (fork (+ x 1)))
        else:
          (Tree/Leaf x)
      }

    You can initialize multiple state variables:

    bend x = 0, y = 1 ... {
      when (condition x y ...):
        ...
    }
    main =
      bend x = 0 {
        when (< x 3):
          (Tree/Node (fork (+ x 1)) (fork (+ x 1)))
        else:
          (Tree/Leaf x)
      }
  2. Implement Church-encoded booleans

    main

    In Bend/HVM, you can encode boolean datatypes as $\lambda$-calculus terms using Church encoding.

    • true is represented as λt λf t (takes two arguments and returns the first).
    • false is represented as λt λf f (takes two arguments and returns the second).

    To perform conditional logic (matching), simply call the boolean value with the case_true and case_false branches as arguments.

    true = λt λf t
    false = λt λf f
    
    # Matching logic
    if boolean case_true case_false = (boolean case_true case_false)
    
    # Example usage
    main = (if true 42 37)
    # Outputs 42
  3. What are scopeless lambdas and how to use them

    main

    Scopeless lambdas are a powerful feature of Bend/HVM where variables bound by a lambda can be accessed outside the lambda's body. This is achieved by prefixing the variable name with a dollar symbol ($).

    While regular lambdas restrict their bound variables to their own scope, a scopeless lambda allows the bound variable to persist in the environment after the lambda is applied.

    Key constraints:

    • Bound variables are local to each term; they do not bind across definitions (e.g., a global variable cannot be bound by a scopeless lambda defined elsewhere).
    • In the imp syntax, scopeless lambdas are written using the lambda $x: body pattern.
    # Standard syntax
    main = (((λ$x 1) 2), $x)
    # Outputs (1, 2)
    
    # imp syntax
    def main() -> _:
      f = lambda $x: 1
      return (f(2), $x)
  4. Use Constructor Literals for types

    main

    Constructors are functions. You can use two syntaxes to create a constructor expression:

    1. Constructor syntax: Requires all field names.
    2. Function syntax: Uses parentheses and can use named or positional arguments.
    # Constructor syntax
    Type/Ctr { field1: 4, field2: 8 }
    
    # Function syntax
    Type/Ctr(field1 = 4, field2 = 8)
    Type/Ctr(4, field2 = 8)
    Type/Ctr(4)
    Type/Ctr { field1: 4, field2: 8 }
    Type/Ctr(field1 = 4, field2 = 8)
    Type/Ctr(4, field2 = 8)
    Type/Ctr(4)
  5. How CLI arguments are applied to the entry point

    main

    When you run bend run <file> arg1 arg2 ..., Bend treats the program as a function and applies the provided arguments to it.

    For example, if your program uses the following Imp syntax:

    def main(x1, x2, x3):
      return MainBody(x1 x2 x3)

    Executing bend run <file> arg1 arg2 arg3 is equivalent to the following fun syntax application:

    main = (λx1 λx2 λx3 (MainBody x1 x2 x3) arg1 arg2 arg3)
    # Imp syntax
    def main(x1, x2, x3):
      return MainBody(x1 x2 x3)
    
    # Calling with `bend run <file> arg1 arg2 arg3`, it becomes:
    main = (λx1 λx2 λx3 (MainBody x1 x2 x3) arg1 arg2 arg3)
  6. Use Map Literals and access keys

    main

    Bend has a built-in binary tree map data structure where keys are u24 (numbers, characters, or symbols). Maps are defined using { key: value } syntax.

    # Map definition
    my_map = { 0: 4, `hi`: "bye", 'c': 2 + 3 }
    
    # Assignment and access
    my_map[0] = 5
    val = my_map[0]
    { 0: 4, `hi`: "bye", 'c': 2 + 3 }
    x[0] = 5
    return x[0]
  7. Define custom data types with Constructors

    main

    The Ctr(...) syntax represents a constructor type. This is used to define custom data types or algebraic data types (ADTs), and constructors can contain other types as parameters.

    def head(list: List(T)) -> Option(T):
      match list:
        case List/Nil:
          return Option/None
        case List/Cons:
          return Option/Some(list.head)
  8. Importing relative and absolute paths

    main

    Bend uses two types of path resolution for imports:

    1. Relative Paths: Paths starting with ./ or ../ are resolved relative to the file containing the import statement.
    2. Absolute Paths: Paths that do not start with ./ or ../ are resolved relative to the folder of the main file of the project.

    Note: When importing a file, names from that file are bound using the file name as a prefix (e.g., helper/calc).

    # Relative imports
    from ./utils import helper
    import ../utils/helper
    
    # Absolute imports (relative to main file)
    from utils import math
    import utils/math
  9. Understand pattern matching priority and wildcards

    main

    Patterns in Bend are evaluated from top to bottom. This means more specific patterns should be placed above more general patterns or wildcards (*). A wildcard can cover multiple specific cases if placed correctly in the order of definition.

    # Patterns are checked top-to-bottom
    pred_if Bool/False * if_false = if_false
    pred_if Bool/True  p *        = (- p 1)
    pred_if Bool/True  0 *        = 0
  10. Prevent infinite expansion in recursive definitions

    main

    In Bend's strict mode (HVM2), certain recursive terms can cause infinite reduction sequences (hanging) because they unroll indefinitely. To prevent this, you must ensure that recursive parts of a function are part of a combinator that is not in an active position, allowing them to be lifted into a top-level function. This transforms the recursive call into a lazy reference.

    Strategies for Lazy Recursion:

    1. Linearize Lambdas: By linearizing the function (passing arguments like f into the inner lambdas), you allow the compiler to lift the inner combinators to the top level.
    2. Use Supercombinators: Use supercombinator patterns to ensure lazy unrolling of recursive terms.
    3. Partial Laziness: In mutually recursive functions, you only need to make one step lazy to prevent the infinite loop, which can be used for micro-optimizations.

    Note: Making a definition lazy prevents infinite expansion of the term itself, but it does not guarantee that the program will terminate if the logic is inherently infinite.

    // Example of a problematic recursive Map that hangs in strict mode
    Map = λf λlist
      let cons = λx λxs (Cons (f x) (Map f xs))
      let nil = Nil
      (list cons nil)
    
    // Example of a linearized Map that works by allowing combinators to float
    Map = λf λlist
      let cons = λx λxs λf (Cons (f x) (Map f xs))
      let nil = λf Nil
      (list cons nil f)
  11. Define recursive datatypes in Bend

    main

    In Bend, recursive datatypes are defined using the type keyword. To allow a field to be recursive, prefix the field name with a tilde ~.

    Example of a binary tree definition:

    type Tree:
      Node { ~left, ~right }
      Leaf { value }

    Bend provides dedicated syntax for common recursive types like binary trees:

    • ![a, b] is equivalent to Tree/Node { left: a, right: b }
    • !x is equivalent to Tree/Leaf { value: x }
    type Tree:
      Node { ~left, ~right }
      Leaf { value }
    
    # Using dedicated syntax for a tree:
    tree = ![![!1, !2],![!3, !4]]