joker

repository·master·Indexed 23 days ago

https://github.com/candid82/joker

A lightweight Clojure interpreter, linter, and formatter written in Go, designed for fast-starting scripting tasks. It provides a REPL, support for .joke scripts, and built-in tooling for linting and formatting Clojure, ClojureScript, and EDN code. While designed for scripting, it features a reduced set of persistent data structures, is single-threaded, and maps primitive types to Go types.

Tokens
7.5K
Snippets
18
Records
46
Agent score
83%

What's inside joker

  1. Understand Joker's primitive types

    master

    Because Joker is implemented in Go, its primitive types map to Go types rather than Java types.

    Joker typeGo type
    BigFloatbig.Float
    BigIntbig.Int
    Booleanbool
    Charrune
    Doublefloat64
    Intint
    Ratiobig.Rat
    Regexregexp.Regexp
    Stringstring
    Timetime.Time

    Note: Nil is a type with a single value: nil.

  2. Understand the difference between Core and Standard-library-wrapping (std) namespaces

    master

    Joker namespaces are categorized into two types based on how they are integrated into the executable:

    1. Core Namespaces (e.g., joker.core): These provide essential functions and macros required for basic Joker operation. They are defined in .joke files in core/data/ and are compiled into native Go code during the build process. They are available immediately upon startup.

    2. Standard-library-wrapping ("std") Namespaces (e.g., joker.math, joker.string): These provide Clojure-like interfaces to Go standard library APIs. They are defined in std/*.joke files and are lazily loaded on-demand. They act as "thin" wrappers where the actual logic is implemented in Go, rather than Joker code.

  3. Understand Joker's Library (Namespace) Loading Behavior

    master

    Joker's library loader (joker.core/load), which powers (ns ... :require ...) and related macros, assumes that namespace names correspond to their filesystem location.

    Specifically:

    • The last part of the namespace (after the last dot) must match the filename (excluding the .joke extension).
    • The preceding parts of the namespace must correspond to the directory path, with dots separating directories.

    Example structure:

    ├── core.joke
    └── utils
        ├── a.joke
        └── b.joke

    Corresponding namespaces:

    • core.joke $\rightarrow$ ttt.core (if in directory ttt)
    • utils/a.joke $\rightarrow$ ttt.utils.a (if in directory ttt/utils)
    • utils/b.joke $\rightarrow$ ttt.utils.b (if in directory ttt/utils)

    Note: Joker resolves paths relative to the file currently being executed, not the current working directory. You can override this using joker.core/*classpath* and joker.core/*ns-sources* variables.

    ;; core.joke
    (ns ttt.core
      (:require [ttt.utils.a :refer [a]]))
    
    (a)
    
    ;; utils/a.joke
    (ns ttt.utils.a
      (:require [ttt.utils.b :refer [b]]))
    
    (defn a []
      (println "I am A")
      (b))
    
    ;; utils/b.joke
    (ns ttt.utils.b)
    
    (defn b []
      (println "I am B"))
  4. Use the *classpath* variable to load deployed libraries

    master

    The *classpath* variable (found in joker.core/*classpath*) allows you to specify a list of directories to search for .joke files when a namespace is required.

    Each component in *classpath* is separated by colons (:) on most OSes or semicolons (;) on Windows. Joker searches these components in order. For a namespace biz.logic, if a component is /usr/lib/joker, Joker will attempt to open /usr/lib/joker/biz/logic.joke.

    Limitations of classpath:

    • It does not provide a delivery mechanism (you must manually place files in the paths).
    • It lacks explicit version management.
    • Dependencies are external to the source code, making them hard to track visually.
  5. Use the BigFloat type for high-precision floating-point numbers

    master

    In Joker, you can create high-precision floating-point numbers by appending the M suffix to a constant. These values are of type BigFloat.

    When parsing decimal constants (like 1.3M), Joker assigns a minimum precision of 53 (equivalent to a float64/Double) and a maximum precision based on the number of digits provided.

    Note that base-10 encoding may not represent values exactly due to the underlying binary representation. For exact precision, use binary, octal, or hexadecimal encodings (e.g., 0x1.fM).

    user=> 2.71828182845904523536028747135266249775724709369995957496696763M
    2.71828182845904523536028747135266249775724709369995957496696763M
  6. Key differences between Joker and Clojure

    master

    Joker is a lightweight interpreter designed for scripting. Users should be aware of the following constraints compared to standard Clojure:

    • Single-threaded: No support for parallelism (no refs, agents, futures, promises, locks, etc.). It does support core.async style concurrency via the go macro.
    • Reduced Data Structures: The set of persistent data structures is smaller (includes ArrayMap, MapSet, HashMap, List, Vector).
    • Limited Interop: Joker does not have the same level of host (Go) interoperability as Clojure/Java. Dot notation for calling methods is not supported.
    • Missing Features: Protocols, records, structmaps, transients, transducers, and several clojure.core functions (like subseq, iterator-seq, etc.) are not implemented.
    • Namespace Prefixing: Built-in namespaces use the joker prefix (e.g., joker.core, joker.string, joker.json).
    • Execution Model: Joker reads and executes s-expressions sequentially. It does not support AOT compilation. To run code only when a file is the main entry point, use the (when (= *main-file* *file*) ...) idiom.
  7. Understand Joker's default namespace loading behavior

    master

    By default, Joker locates namespace source files on the local filesystem by converting the namespace name into a relative path (subpath). Each component of the namespace is treated as a directory, and the final component is appended with a .joke extension.

    Example Conversion:

    • Namespace a.b.c $\rightarrow$ a/b/c.joke

    Joker also performs relative lookups based on the file currently being evaluated. When a namespace seeks to load another, it removes the components corresponding to its own namespace from the current pathname and appends the new subpath.

    Example: If /Users/somebody/mylibs/a/b/c.joke is running as namespace a.b.c and attempts to load d.e, Joker calculates the path by removing a/b/c and appending d/e.joke, resulting in /Users/somebody/mylibs/d/e.joke.

  8. Understand Joker namespace states and lifecycle

    master

    Joker organizes code into namespaces that transition through three distinct states:

    1. Available: The namespace's source code is either compiled into the Joker executable (built-in) or exists as a Joker source file that the executable can locate on disk. An available but unmapped namespace cannot be found via (the-ns 'name) until it is first referenced.
    2. Mapped: The namespace is registered in the current global environment and will appear in the output of (all-ns). A mapped namespace is known to the system but may not yet be initialized.
    3. Loaded: The namespace's internal code and data structures are fully initialized. This happens lazily when the namespace is first required via (ns ... :require [...]), via (require 'name), or when a symbol is qualified by that namespace (e.g., namespace/symbol).

    While Joker manages these transitions automatically for most users, understanding them is useful for debugging namespace resolution or managing large library deployments.

  9. Configure HTTP-based library loading via :url

    master

    The :url value in an *ns-sources* map can be an HTTP URL (starting with http:// or https://). This allows Joker to fetch libraries from the web and cache them locally.

    How HTTP caching works:

    1. Joker calculates the subpath for the namespace (e.g., a.b.c $\rightarrow$ a/b/c.joke).
    2. It attempts to find the file in the local cache directory: $HOME/.jokerd/deps/<url_domain_and_path>/<subpath>.joke.
    3. If the file is missing, Joker performs an HTTP GET request to retrieve the file.
      • If the :url ends in .joke, it is used directly.
      • Otherwise, the subpath is appended to the URL.
    4. The retrieved file is saved to the $HOME/.jokerd cache for future use.

    Example: If :url is https://example.com/joker/libs and the namespace is a.b.c:

    • Retrieved URL: https://example.com/joker/libs/a/b/c.joke
    • Cached Path: $HOME/.jokerd/deps/example.com/joker/libs/a/b/c.joke
  10. Build Joker with fast-startup optimization

    master

    Joker supports two build modes: a "slow" version and a "fast" version. The fast-startup version uses statically initialized Go source files (generated via run.sh and go generate) to reduce runtime initialization overhead for core namespaces.

    Running run.sh builds both versions, creating joker.slow and joker.fast executables. The build process uses gen_code.go to generate a_*code.go files which contain static variables representing core data structures.