Bolt Language Documentation

repository·main·Indexed 21 days ago

https://github.com/beariish/bolt

Bolt is a high-performance, type-safe, embeddable language optimized for real-time applications and low-latency inter-language communication. It features a register-based VM, nanboxing, and type-accelerated bytecode to minimize overhead. The documentation covers embedding Bolt into C applications, using the Bolt API (bt_ prefix), binding C functions to Bolt, and language syntax including its static typing system and fundamental types.

Tokens
17.5K
Snippets
68
Records
81
Agent score
74%

What's inside Bolt

  1. Overview of Bolt

    main

    Bolt is a lightweight, lightning-fast, type-safe embeddable language designed for real-time applications. It features an embed-first design that prioritizes inter-language performance and agility, making it suitable for integration into existing host applications with minimal overhead.

    import print, error, Error from core
    import abs, epsilon from math
    
    // The return type of safe_divide is inferred to be `Error | number`
    fn safe_divide(a: number, b: number) {
        if abs(b) < epsilon {
            return error("Cannot divide by zero!")
        }
    
        return a / b
    }
    
    match let result = safe_divide(10, 5) {
        is Error {
            // The type of result is narrowed in this branch!
            print("Failed to divide:", result.what)
        }
    
        is number {
            print("The answer is", result)
        }
    }
  2. Introduction to Bolt language syntax and typing

    main

    Bolt is a high-level, embeddable, statically typed language. It uses a C-family syntax but omits semicolons and parentheses around expressions.

    Key characteristics:

    • Static Typing: The type of a binding cannot change after declaration. Bolt uses a mature type inference mechanism, so types are often omitted in code but are still strictly enforced.
    • Performance: Designed to be fast by leveraging types at runtime to omit dynamic typing steps.
    • Syntax Style: Strictly C-family styled, designed to be unambiguous without semicolons or parentheses.
    import print, error, Error from core
    import abs, epsilon from math
    
    // The return type of safe_divide is inferred to be `Error | number`
    fn safe_divide(a: number, b: number) {
        return if abs(b) < epsilon
            then error("Cannot divide by zero!")
            else a / b
    }
    
    match let result = safe_divide(10, 5) {
        is Error {
            print("Failed to divide:", result.what)
        }
    
        is number {
            print("The answer is", result)
        }
    }
  3. How string utility functions are accessed in Bolt

    main

    In Bolt, all string utility functions are available in two ways: as direct imports from the strings module or as methods on the string prototype. This allows you to call functions either by passing the string as the first argument or by calling them directly on a string instance.

    Example usage:

    import strings
    
    let str = "hello!"
    
    strings.length(str) // 6
    str.length() // 6
    import strings
    
    let str = "hello!"
    
    strings.length(str) // 6
    str.length() // 6
  4. Handle null with null operators

    main

    Bolt uses null to express the lack of a value. Several operators facilitate null safety:

    • ? (Postfix):
      • On a type: Creates a union between the type and null (e.g., number? is equivalent to number | null).
      • On an expression: Tests for existence (returns true if non-null).
    • ! (Postfix/Non-null assertion): Explicitly strips null from a binding. Raises a runtime error if the value is actually null.
    • ?? (Null-coalescing): Selects between two values. Returns the first if it is non-null, otherwise the second.
    • ?. (Null-indexing): Allows walking a tree of table properties safely. If any part of the chain is null, the whole expression returns null.
    let num: number? = 10
    let num2: number? = null
    
    if num? {
        // num is narrowed to number here
    }
    
    let n = num ?? 20 // 10
    let n2 = num2 ?? 20 // 20
    
    let n_force: number = num! // 10
    let n_err: number = num2! // Runtime error
    
    let t = { x: { y: 10 } }
    let t_opt: typeof(t)? = t
    let val = t_opt?.x.y // number | null
  5. Use union types and nullability

    main

    A union type allows a binding to contain one of a subset of specified types using the | operator. Like any, you cannot perform operations on a union directly; you must use match or casting to narrow the type.

    Nullable types: A type is considered 'nullable' if it is a union containing null. You can use the ? operator to append | null to a type (e.g., string?).

    type NumBoolString = number | bool | string
    
    let x: NumBoolString = 10
    
    fn takes_uni(u: NumBoolString) {
        match u {
            is number { ... }
            is bool { ... }
            is string { ... }
        }
    }
    
    // Recursive union example
    type JsonValue = number | string | bool | null | [JsonValue] | { ..string: JsonValue }
  6. Use the `match` statement for pattern matching

    main

    The match statement compares an expression against multiple branches. It walks down the list of expressions until a branch is taken.

    Key features:

    • Equality matching: By default, it implicitly generates x == <branch_expression>.
    • Fallback: Use an else branch as a fallback if no other branches match.
    • Multiple expressions: Use comma-separated lists for a single branch (e.g., 1, 2, 3).
    • Custom operators: You can use operators like < or > instead of equality.
    • Type matching: Use is <type> to match based on the type of a union binding (this also performs type narrowing).
    • Custom conditions: Wrap conditions in parentheses (condition) to omit the implicit equality check.
    • Branch bodies: Use a block { ... } or the then keyword for single-expression bodies (use a trailing comma with then).
    let x = get_random_number()
    match x {
        < 5 { print("x is small!") }
        5 { print("x is exactly five!") }
        > 5 { print("x is large!") }
    }
    
    // Using type narrowing with union types
    let y: number | bool | string = get_union()
    match y {
        is number { y += 10 } // y is narrowed to number
        is bool { print("y is", y) }
        is string { print("y is a string!") }
    }
    
    // Using 'then' for single expressions
    match x {
        1 then print("x is 1!"),
        2 then print("x is 2!"),
        else print("unknown")
    }
  7. How modules and importing work in Bolt

    main

    In Bolt, every source file is treated as an isolated module. To use functionality from other modules, you use the import keyword. There are several ways to import depending on your needs:

    1. Namespace Import: Imports the entire module under its name.
    2. Aliased Import: Imports the module under a custom name using as to avoid conflicts.
    3. Specific Import: Extracts specific identifiers from a module into the local scope.
    4. Wildcard Import: Imports all names from a module into the local scope using *.

    For local files, you can use dot-notation (e.g., engine.graphics.draw) or path-based strings (e.g., "../parent") to resolve modules relative to the current file.

    // Namespace import
    import core
    core.print("hello!")
    
    // Aliased import
    import core as c
    c.print("this is okay!")
    
    // Specific imports
    import print, write, error from core
    print("This acts as a local, now!")
    
    // Wildcard import
    import * from core
    print(error("Everything is in scope!"))
    
    // Path-based import
    import * from "../parent"
  8. How to use the Regex module

    main

    The Regex module embeds picomatch functionality with Bolt type-safety. Functions in this module are available in two ways: as direct imports from the regex module or as methods on the Regex prototype. This allows for both functional and object-oriented styles of usage.

    Example of dual usage:

    import regex
    
    let reg = regex.compile("^[a-z]+$")!
    
    regex.groups(reg) // Functional style
    reg.groups()     // Prototype style
  9. Understand the Bolt API naming conventions

    main

    The Bolt API follows a consistent naming pattern to simplify exploration:

    • Prefixes: All names are prefixed with bt_. Functions use lower_snake_case and types use PascalCase (e.g., bt_make_number(), bt_Context).
    • Object Creation: All objects are instantiated via bt_make_ functions (e.g., bt_make_string(), bt_make_thread()).
    • Object Operations: Operations on specific types follow the bt_objectype_do_thing pattern (e.g., bt_table_set(), bt_table_get()).
    • Context Requirement: Almost all functions that perform allocation require a bt_Context* as their first parameter.
  10. Implement prototype functions and metamethods

    main

    You can associate functions with a tableshape by using the type's name in the function declaration.

    • Member Access: If the first argument of a prototype function matches the type, it can be called using dot notation (e.g., v.func()). The object is implicitly passed as the first argument.
    • this keyword: Use this as syntactic sugar for the first argument in prototype functions.
    • Metamethods: Functions starting with @ override default Bolt behavior. Supported metamethods include:
      • Arithmetic: @add, @sub, @mul, @div (+, -, *, /)
      • Comparison: @lt, @lte (<, <=), @eq, @neq (==, !=)
      • Stringification: @format (used by print() and to_string())

    Note: Prototype functions can refer to each other, but must follow their declaration order.

    type Vec2 = { x: number, y: number }
    
    fn Vec2.new(x: number, y: number) {
        return Vec2 => { x: x, y: y }
    }
    
    fn Vec2.length(this) {
        return math.sqrt(this.x * this.x + this.y * this.y)
    }
    
    fn Vec2.@add(this, other: Vec2) {
        return Vec2.new(this.x + other.x, this.y + other.y)
    }
    
    let v = Vec2.new(10, 20)
    let l = v.length()
  11. Use `match let` for temporary bindings

    main

    The match let variation allows you to create a temporary binding for the expression being matched. This binding is only available within the scope of the chosen branch and is not accessible outside the match block.

    match let x = get_random_number() {
        < 5 { print("x is small!") }
        (is_even(x)) { print("x is even!") }
    }
    
    print(x) // Error: no binding named x
  12. How to use Bolt array utility functions

    main

    The array module provides utility functions for working with arrays. These functions are available in two ways: as direct imports from the arrays module or as methods on the array prototype. These two approaches are functionally equivalent.

    Example of equivalent usage:

    import arrays
    
    let arr = [1, 2, 3]
    
    // Using the module import
    arrays.push(arr, 4)
    
    // Using the prototype method
    arr.push(4)
    import arrays
    
    let arr = [1, 2, 3]
    
    // Using the module import
    arrays.push(arr, 4)
    
    // Using the prototype method
    arr.push(4)