git » blacktide.git » master » tree

[master] / doc / language.md

Language reference

The public namespace is com.tailrecursion.blacktide. It provides loophole, setq, psetq, label and goto. A loophole form defines a region where assignment and jumps are allowed. The other four macros report an error outside that region.

Qualified calls and namespace aliases work. Lexical bindings can shadow macro names.

Bindings and assignment

loophole takes an even vector of simple symbols and initializers, followed by a body. Initializers run once, sequentially, and may read or assign earlier bindings. The result is the last body value, or nil for an empty body.

(require '[com.tailrecursion.blacktide :refer [loophole setq psetq label goto]])
(loophole [x 1 y x]
  (let [x 3] [x y])) ; => [3 1]

(loophole [x 1 y 2]
  (psetq x y y x)
  [(setq x 10) y]) ; => [10 1]
Form Evaluation and assignment Result
setq Evaluate a right-hand side and write its target before proceeding to the next pair. Last value, or nil for no pairs.
psetq Evaluate all right-hand sides in pair order, then write all targets in pair order. nil

These rules also apply to repeated targets.

Nested assignments take effect while right-hand sides are evaluated. If one throws an exception, pending parallel writes are abandoned.

Only bindings introduced by loophole are assignable. Shadowing with an ordinary let makes the inner binding immutable. Native set! is not allowed.

Statements and jumps

The placement rules distinguish labels from jumps:

A jump cannot occur anywhere within an operand or initializer. This includes discarded expressions nested in do.

(loophole [x 0]
  (label again)
  (let [next (inc x)]
    (setq x next)
    (when (< x 3) (goto again)))
  (goto done)
  (setq x 99)
  (label done) x) ; => 3

Backward jumps rerun initializers they encounter. They cannot enter a lexical scope or bypass its initialization. These forms are errors:

(loophole [] (let [] (label inside)))
(loophole [] (+ 1 (do (goto done) 2)) (label done))
(loophole [x (goto done)] (label done) x)

Clojure subset

Supported forms

Category Supported forms
Values Literals, quote, var, symbol reads and collection literals with executable metadata.
Bindings let and loop, including destructuring.
Control do, if, throw, case and loop/recur.
Calls Function calls, including external function values.
Java interop Method calls, constructors, field reads and arrays.

External functions called from the region can use unrestricted Clojure.

Macro expansion

Macros expand in the caller namespace with lexical shadowing and visibility checks. The macro environment, &env, provides binding names, hints and inferred immutable types. It does not provide private Compiler$LocalBinding objects.

Common supported macros include when, when-not, cond, and, or, if-let, when-let, threading, dotimes, while and doseq. A macro works only when its entire expansion uses supported forms. Quoted data is never analyzed as code.

Forms that belong outside the region

The region rejects these forms, including forms produced by macroexpansion:

Put helper functions, resource management and dynamic bindings outside:

(letfn [(step [n] (inc n))]
  (loophole [x 0]
    (dotimes [_ 3] (setq x (step x)))
    x)) ; => 3

(try
  (loophole [] (throw (Exception. "example")))
  (catch Exception e (.getMessage e))) ; => "example"

(loophole [total 0]
  (dotimes [i 4] (dotimes [j i] (setq total (+ total j))))
  total) ; => 4

Loops and case dispatch

Original recur preserves:

recur cannot target a loop outside the region.

Source primitive inference is preserved. This includes int→long and float→double initializer promotion. Incompatible backedges, where a recur returns to its loop, require boxing. Even an unreachable recur contributes to inference.

case preserves Clojure's constant matching, grouping, hash collisions and missing-default exception:

(loophole [x 0]
  (case (do (setq x (inc x)) x)
    1 (setq x 10)
    2 (setq x 20)
    (setq x 30))
  x) ; => 10

JVM semantics and limits

Calls and compilation

Source facts determine overloads and conversion order. Clojure 1.12 qualified method values, functional-interface adaptation and :param-tags are supported. Arithmetic respects *unchecked-math*. Proven numeric operations may use primitive storage and calls when their value, promotion, overflow, conversion and identity behavior agree with the source. This includes preserving the distinctions between = and ==, and between generic and primitive floating-point operations. Calls respect :direct-linking and direct protocol dispatch.

Unreachable code still receives syntax and JVM compilation checks. Very large regions remain subject to JVM method-size limits.

Identity and lookup sites

Mutable locals retain reference identity through assignment and immutable snapshots. Ordinary primitive locals retain Clojure's semantics: separate Object uses may box separately.

Literal pooling is unspecified. Embedded constants, macro-supplied quoted data, regex literals and inlined ^:const values may share differently from ordinary Clojure. The concrete classes generated for method values and functional-interface adapters are also unspecified. Bind a reference once when its identity matters; copying that reference preserves identity.

(let [token (Object.)]
  (loophole [x token y nil]
    (psetq y x x x)
    (identical? token y))) ; => true

Literal keyword calls use Clojure's cached lookup; indirect keyword calls retain ordinary invocation. Arguments evaluate exactly once before lookup, including when they assign locals or recursively invoke the containing function. Cache layout, installation counts and cache-miss probes are unspecified. Custom IKeywordLookup implementations must return equivalent results regardless of cache history; effects or exceptions that depend on that history are outside this contract.

Errors

Explicit reference conversions may use Class.cast. Its exception message differs from JVM checkcast. Invalid protocol fallback roots can also produce different error text. Failing types and evaluation order are preserved.

DSL errors carry :kind, :form, :file, available :line/:column, and relevant binding or label data. Clojure normally wraps them in CompilerException.