Unison Language Documentation

repository·trunk·Indexed 25 days ago

https://github.com/unisonweb/unison

Unison is a statically-typed functional language featuring content-addressed code, where functions are identified by hashes of their implementation. This architecture enables perfect incremental compilation, non-breaking renames, and semantically-aware version control. The documentation covers building from source using Stack, Cabal, or Nix, configuring the Nix Cachix cache, debugging SQLite queries, and setting up the Local UI, LSP, and AI Agent Server (MCP).

Tokens
88.4K
Snippets
317
Records
553
Agent score
92%

What's inside Unison

  1. Overview of the Unison language

    trunk

    Unison is a statically-typed functional language featuring type inference, an effect system, and advanced tooling. It is built on the principle of content-addressed code, where functions are identified by a hash of their implementation rather than by name, and code is stored as its AST in a database.

    Key benefits include:

    • No builds: Perfect incremental compilation via a shared compilation cache.
    • Non-breaking renaming: Instant renaming of definitions.
    • Test caching: Deterministic tests are only rerun if dependencies change.
    • Semantically-aware version control: Avoids merge conflicts caused by whitespace, import order, or formatting.

    Unison can be used as a general-purpose language or with Unison Cloud for distributed systems.

  2. Overview of the Unison Runtime

    trunk

    The Unison runtime is responsible for evaluating Unison code that has been parsed and typechecked. It converts computations (reducible expressions like 1 + 1) into values (normal forms like 42).

    Key design characteristics include:

    • Value Capabilities: The runtime enables hashing, serialization, deserialization, and dependency computation for any value, including functions. This supports Unison's distributed programming API for shipping arbitrary values over a network.
    • Cycle Handling: The runtime reliably detects and encodes cycles (e.g., in recursive functions) during serialization and hashing to prevent runtime failures.
    • Decompilation: Any value can be decompiled back into a Unison term, allowing developers to see terms in normal form during evaluation.
    • Algebraic Effects: The runtime supports algebraic effects by providing the ability to manipulate continuations of a running program.
    • Modularity: The runtime is designed to be modular, allowing for future transitions from direct interpretation to JIT compilation (e.g., via LLVM) without replacing the entire system.
  3. Overview of unison-share-api

    trunk
    The unison-share-api package is a centralized repository for all API definitions and types used by Share. It is designed to facilitate the generation of API clients using servant-client, ensuring that clients remain synchronized with the current state of Share's APIs.
  4. Understand Unison Locations and Abilities

    trunk

    A Loc {e} (Location) represents a computing context with access to specific resources. The type parameter {e} defines the abilities available at that location.

    • A Loc {} supports only pure computations.
    • A Loc {Remote, GPU} provides both Remote and GPU abilities.

    At runtime, a Location is a composite of cryptographic tokens that authorize the use of specific hosts, ports, and abilities. This allows for secure, distributed execution where the receiving host validates the accompanying tokens before running the computation.

  5. Understand Unison Distributed Programming Semantics

    trunk

    Unison's distributed model follows a 'dumb runtime' principle: the runtime never contacts another node unless explicitly instructed by the user's program. All intelligence for discovery, upkeep (like DHT maintenance), and peer selection must be implemented within regular Unison libraries.

    Key Concepts

    • Task Lifecycle: A Task returned by Remote.fork controls the entire computation tree. Stopping that Task stops all associated subtasks.
    • Node Isolation: The association between a Name and a Box is local to a node. Nodes are isolated and must communicate explicitly. There is no global storage concept; multi-node storage must be implemented using Unison libraries.
    • Durable Storage:
      • durable name blah : Name Number acts like a typed file name. It can be resolved on any node to a Box Number.
      • The state of a Box Number (empty or full) survives node restarts.
    • Static Node Declaration: Use the node node-name block to declare a node statically, which is used for bootstrapping systems.
  6. Use MVar for mutable, sharable storage

    trunk
    An MVar is a concurrency primitive used for mutable, sharable storage of a single value. It can be empty or contain a value. MVars are thread-safe, allowing multiple threads to attempt simultaneous reading and writing. They serve as building blocks for other primitives like Futures, Queues, and Run-at-most-once initializers.
  7. Understand the v2 Codebase SQLite Schema

    trunk

    The v2 codebase format is a sqlite3 database that stores Unison objects using a mix of relational tables and binary blobs.

    Core Tables

    • hash: Stores Unison hashes in base32hex format. It is indexed for efficient case-insensitive prefix lookups.
    • text: Stores all strings (definition names, builtin names, user-defined strings) for deduplication. It is indexed for exact match lookups.
    • object: Stores objects identified by hash and represented as binary blobs (e.g., patches, namespace slices, terms, and decl components). The type_id field links to the object_type_description table.
    • causal & causal_parent: Manage causal relationships. The causal table links a hash to its root namespace slice, while causal_parent tracks the hierarchy.
  8. Understand Distributed Garbage Collection in Unison

    trunk

    Unison uses a distributed garbage collection mechanism to manage memory across nodes. It relies on two primary tracking structures:

    1. B_map: A weak map used to track local boxes. Entries are automatically removed by the weak map once they are no longer referenced in the local heap or boxes.
    2. C_set: A weak set used to track all remote boxes referenced by the local heap or boxes.

    To prevent premature garbage collection during value transfers (like continuations or Box.put), nodes use a Keepalive mechanism to propagate references and ensure remote boxes remain alive while being used by a new node.

  9. Understand the Unison codebase repo format (v1)

    trunk

    The Unison codebase uses a specific repository format stored under the .unison/v1/ directory. This format organizes terms, types, paths, and dependency information using content-addressed hashes.

    Note: This format is currently in DRAFT and is subject to change until a formal release.

  10. Understand the Unison runtime evaluation phases

    trunk

    To evaluate a successfully typechecked Unison term (p : AnnotatedTerm v a), the runtime executes the following sequence of phases:

    1. let rec minimization: Reduces cycle sizes and eliminates needless cycles to prepare for ability requests.
    2. lambda lifting: Eliminates lambdas with free variables by converting them into ordinary function parameters.
    3. A-normal form (ANF) conversion: Moves function calls and ability requests into the body of a let or let rec to simplify runtime handling.
    4. compilation: Converts ANF code into an Intermediate Representation (IR).
    5. evaluation: Interprets the IR to produce a value V.
    6. decompilation: Converts the value V back into a term for display in the codebase editor.
  11. Quickstart with EasyTest

    trunk

    EasyTest is a simple testing toolkit for Haskell designed to replace frameworks like QuickCheck, HUnit, and Tasty. It uses ordinary monadic Haskell code to define tests, providing explicit control over randomness, I/O, failure, logging, and parallelism.

    To create a test suite, use the tests function (which is msum) to combine Test values. You can run the entire suite with run or run specific sub-tests using runOnly with a scope prefix.

    module Main where
    
    import EasyTest
    import Control.Applicative
    import Control.Monad
    
    suite :: Test ()
    suite = tests
      [ scope "addition.ex1" $ expect (1 + 1 == 2)
      , scope "addition.ex2" $ expect (2 + 3 == 5)
      , scope "list.reversal" . fork $ do
          -- generate lists from size 0 to 10, of Ints in (0,43)
          ns <- [0..10] `forM` \n -> replicateM n (int' 0 43)
          ns `forM_` \ns -> expect (reverse (reverse ns) == ns)
      , scope "addition" . scope "ex3" $ expect (3 + 3 == 6)
      , scope "always passes" $ do
          note "I'm running this test, even though it always passes!"
          ok
      , scope "failing test" $ crash "oh noes!!" ]
    
    -- Run only tests whose scopes are prefixed by "addition"
    main = runOnly "addition" suite