Full Code of hyperfiddle/rcf for AI

master 5d81fde08ce3 cached
38 files
93.2 KB
28.3k tokens
1 requests
Download .txt
Repository: hyperfiddle/rcf
Branch: master
Commit: 5d81fde08ce3
Files: 38
Total size: 93.2 KB

Directory structure:
gitextract_sxhcwvab/

├── .cljfmt.edn
├── .github/
│   └── workflows/
│       ├── tests_babashka.yml
│       ├── tests_browser.yml
│       ├── tests_clj.yml
│       └── tests_node.yml
├── .gitignore
├── License.md
├── README.md
├── bb/
│   └── runner.clj
├── bb.edn
├── build.clj
├── build.md
├── ci/
│   ├── run_tests_all.sh
│   ├── run_tests_browser.sh
│   ├── run_tests_jvm.sh
│   └── run_tests_node.sh
├── deps.edn
├── example/
│   ├── dev_entrypoint.cljc
│   └── example.cljc
├── out/
│   └── index.html
├── package.json
├── pom.xml
├── run_dev_repl.sh
├── shadow-cljs.edn
├── src/
│   └── hyperfiddle/
│       ├── rcf/
│       │   ├── analyzer.clj
│       │   ├── impl.clj
│       │   ├── queue.clj
│       │   ├── queue.cljs
│       │   ├── reporters.clj
│       │   ├── reporters.cljs
│       │   ├── time.cljc
│       │   └── unify.cljc
│       └── rcf.cljc
└── test/
    └── hyperfiddle/
        ├── rcf/
        │   ├── analyzer_test.clj
        │   ├── cljs_test.cljs
        │   ├── example_test.clj
        │   └── unify_test.cljc
        └── rcf_test.cljc

================================================
FILE CONTENTS
================================================

================================================
FILE: .cljfmt.edn
================================================
{:indents ^:replace {#"^." [[:inner 0]]}
 :test-code [(sui/ui-grid {:columns 2}
               (sui/ui-grid-row {}
                 (sui/ui-grid-column {:width 12}
                   ...)))
             (let [foo bar]
               (str "foo"
                 "bar"))]}

================================================
FILE: .github/workflows/tests_babashka.yml
================================================
name: Babashka
on: [push, pull_request]
jobs:
  babashka:
    runs-on: [ubuntu-latest]
    steps:
      - name: Checkout
        uses: actions/checkout@v2

      - name: Prepare java
        uses: actions/setup-java@v1
        with:
          java-version: 1.8

      - name: Install clojure tools
        uses: DeLaGuardo/setup-clojure@10.1
        with:
          cli: 1.10.1.727
          bb: latest

      - name: Run tests
        run: bb test:bb



================================================
FILE: .github/workflows/tests_browser.yml
================================================
name: Browser
on: [push, pull_request]
jobs:
  browser:
    runs-on: [ubuntu-latest]
    steps:
      - name: Checkout
        uses: actions/checkout@v2

      - name: Prepare java
        uses: actions/setup-java@v1
        with:
          java-version: 1.8

      - name: Install clojure tools
        uses: DeLaGuardo/setup-clojure@3.4
        with:
          cli: 1.10.1.727

      - name: Use Node.js
        uses: actions/setup-node@v2
        with:
          node-version: 17.8.0

      - name: Install Shadow
        run: npm install --also dev

      - name: Run Tests
        run: ./ci/run_tests_browser.sh



================================================
FILE: .github/workflows/tests_clj.yml
================================================
name: JVM
on: [push, pull_request]
jobs:
  clojure:
    runs-on: [ubuntu-latest]
    steps:
      - name: Checkout
        uses: actions/checkout@v2

      - name: Prepare java
        uses: actions/setup-java@v1
        with:
          java-version: 1.8

      - name: Install clojure tools
        uses: DeLaGuardo/setup-clojure@3.4
        with:
          cli: 1.10.1.727

      - name: Run tests
        run: ./ci/run_tests_jvm.sh



================================================
FILE: .github/workflows/tests_node.yml
================================================
name: NodeJS
on: [push, pull_request]
jobs:
  node:
    runs-on: [ubuntu-latest]
    steps:
      - name: Checkout
        uses: actions/checkout@v2

      - name: Prepare java
        uses: actions/setup-java@v1
        with:
          java-version: 1.8

      - name: Install clojure tools
        uses: DeLaGuardo/setup-clojure@3.4
        with:
          cli: 1.10.1.727

      - name: Use Node.js
        uses: actions/setup-node@v2
        with:
          node-version: 17.8.0

      - name: Install Shadow
        run: npm install shadow-cljs

      - name: Run Tests
        run: ./ci/run_tests_node.sh



================================================
FILE: .gitignore
================================================
.cpcache/
.nrepl-port
.cljs_node_repl/
.shadow-cljs/
.rebel_readline_history
.idea
out/js
*.js
*.js.map
*.DS_Store
node_modules
/target
.clj-kondo/
.lsp/
scratch/
.calva/


================================================
FILE: License.md
================================================
Copyright 2021 Hyperfiddle, Inc.

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.



================================================
FILE: README.md
================================================
# RCF – REPL-first async test macro for Clojure/Script

RCF turns your Rich Comment Forms into tests (in the same file as your functions). Send form or file to REPL to run tests and it squirts dopamine ✅✅✅. It's good, try it!

![](https://i.imgur.com/nBOOZq7.png)

Features
* Clojure/Script
* async tests
* zero boilerplate
* natural REPL workflow
* one key-chord to run tests, no hotkey configuring
* same-file tests (examples are better than docstrings)
* no file watchers, no extra windows, no beeping, no latency
* notebook support – [example NextJournal notebook](https://nextjournal.com/dustingetz/missionary-relieve-backpressure)
* it's fun! ✅✅✅

RCF is specifically engineered to support [Electric Clojure](https://github.com/hyperfiddle/electric), which we test, document and teach with RCF.

Hype quotes:
* "RCF has changed my habits with regards to tests. It is so much easier than flipping back and forth between files, you get my preferred work habits - work in a comment block until something works. But before RCF I never took the time to turn comment blocks into an automated test"
* "I use RCF to do leetcode style questions as 'fun practice.' It certainly didn't feel fun before!"
* "I think people make the mistake of comparing this with other methods of inlining tests near their function definitions. The integration with the REPL, low syntax/interface, reduces friction and makes testing more attractive as a language of communication and verification."
* "I used RCF in a successful interview. RCF was a massive help in communication and a fast tool for thought whilst under the conditions of technical interview."

# Dependency

Project maturity: CLJ is stable, CLJS is experimental, bb is experimental.

```clojure 
{:deps {com.hyperfiddle/rcf {:mvn/version "20220926-202227"}}}
```

Changelog
* :throws
* babashka support (experimental)
* **breaking** don't return final result return nil like comment
* `20220926-202227` `!` is deprecated, use `tap` instead
* `20220827-151056` async test forms no longer guaranteed return final result
* `20220405` maven group-id renamed from `hyperfiddle` to `com.hyperfiddle` for security
* 2021 Dec 18: clojurescript dependency is now under the :cljs alias, see #25
* 2021 Oct 20: custom reporters now dispatch on qualified keywords, see #19

Current dev priority is improving complex async tests in ClojureScript.

[![JVM](https://github.com/hyperfiddle/rcf/actions/workflows/tests_clj.yml/badge.svg?branch=master)](https://github.com/hyperfiddle/rcf/actions/workflows/tests_clj.yml)
[![NodeJS](https://github.com/hyperfiddle/rcf/actions/workflows/tests_node.yml/badge.svg?branch=master)](https://github.com/hyperfiddle/rcf/actions/workflows/tests_node.yml)
[![Browser](https://github.com/hyperfiddle/rcf/actions/workflows/tests_browser.yml/badge.svg?branch=master)](https://github.com/hyperfiddle/rcf/actions/workflows/tests_browser.yml)

# Usage

`(tests)` blocks erase by default (macroexpanding to nothing), which avoids a startup time performance penalty as well as keeps tests out of prod.

It's an easy one-liner to turn on tests in your dev entrypoint:

```clojure
(ns user ; user ns is loaded by REPL startup
  (:require [hyperfiddle.rcf]))

(hyperfiddle.rcf/enable!)
```
Tests are run when you send a file or form to your Clojure/Script REPL.

```clojure
(ns example
  (:require [hyperfiddle.rcf :refer [tests tap %]]))

(tests
  "equality"
  (inc 1) := 2

  "wildcards"
  {:a :b, :b [2 :b]} := {:a _, _ [2 _]}

  "unification"
  {:a :b, :b [2 :b]} := {:a ?b, ?b [2 ?b]}

  "unification on reference types"
  (def x (atom nil))
  {:a x, :b x} := {:a ?x, :b ?x}
  
  "multiple tests on one value"
  (def xs [:a :b :c])
  (count xs) := 3
  (last xs) := :c
  (let [xs (map identity xs)]
    (last xs) := :c
    (let [] (last xs) := :c))

  "exceptions"
  (assert false "boom") :throws java.lang.AssertionError

  (tests
    "nested tests (is there a strong use case?)"
    1 := 1)

  (tests
    "REPL bindings work"
    (keyword "a") := :a
    (keyword "b") := :b
    (keyword "c") := :c
    *1 := :c
    *2 := :b
    *3 := :a
    *1 := :c                   ; inspecting history does not affect history

    (keyword "d") := :d
    *1 := :d
    *2 := :c
    *3 := :b
    (symbol *2) := 'c          ; this does affect history
    (symbol *2) := 'd))
```
```text
Loading src/example.cljc...
✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅Loaded
```

# Async tests

```Clojure
(ns example
  (:require [clojure.core.async :refer [chan >! go go-loop <! timeout close!]]
            [hyperfiddle.electric :as e]
            [hyperfiddle.rcf :as rcf :refer [tests tap % with]]
            [missionary.core :as m]))

(rcf/set-timeout! 100)

(tests
  "async tests"
  #?(:clj  (tests
             (future
               (tap 1) (Thread/sleep 10)        ; tap value to queue
               (tap 2) (Thread/sleep 200)
               (tap 3))
             % := 1                               ; pop queue
             % := 2
             % := ::rcf/timeout)
     :cljs (tests
             (defn setTimeout [f ms] (js/setTimeout ms f))
             (tap 1) (setTimeout 10 (fn []
             (tap 2) (setTimeout 200 (fn []
             (tap 3)))))
             % := 1
             % := 2
             % := ::rcf/timeout)))

(tests 
  "electric"
  (def !x (atom 0))
  (def dispose
    (e/run
      (let [x (e/watch !x)
            a (inc x)
            b (inc x)]
        (tap (+ a b)))))
  % := 2
  (swap! !x inc)
  % := 4
  (swap! !x inc)
  % := 6
  (dispose))

(tests
  "core.async"
  (def c (chan))
  (go-loop [x (<! c)]
    (when x
      (<! (timeout 10))
      (tap x)
      (recur (<! c))))
  (go (>! c :hello) (>! c :world))
  % := :hello
  % := :world
  (close! c))

(tests
  "missionary"
  (def !x (atom 0))
  (def dispose ((m/reactor (m/stream! (m/ap (! (inc (m/?< (m/watch !x)))))))
                (fn [_] #_(prn ::done)) #(prn ::crash %)))
  % := 1
  (swap! !x inc)
  (swap! !x inc)
  % := 2
  % := 3
  (dispose)))
```

# CI

To run in CI, configure a JVM flag for RCF to generate clojure.test deftests, and then run them with clojure.test. [Github actions example](https://github.com/hyperfiddle/rcf/tree/master/.github/workflows).

```Clojure
; deps.edn
{:aliases {:test {:jvm-opts ["-Dhyperfiddle.rcf.generate-tests=true"]}}}
```
```bash
% clj -M:test -e "(require 'example)(clojure.test/run-tests 'example)"

Testing example
✅✅✅✅✅✅✅✅
Ran 1 tests containing 8 assertions.
0 failures, 0 errors.
{:test 1, :pass 8, :fail 0, :error 0, :type :summary}
```

# ClojureScript configuration

For CLJS tests to run, `rcf/enable!` must be true **in both CLJ (shadow-cljs macroexpansion time) and CLJS (JS runtime)**. Reports may be printed to **browser console instead of the REPL**, because browser REPLs donn't intercept the async println.

```Clojure
(ns dev-entrypoint
  (:require [example] ; transitive inline tests will erase
            [hyperfiddle.rcf :refer [tests]]))

; wait to enable tests until after app namespaces are loaded
(hyperfiddle.rcf/enable!)

; subsequent REPL interactions will run tests

; prevent test execution during cljs hot code reload
#?(:cljs (defn ^:dev/before-load stop [] (hyperfiddle.rcf/enable! false)))
#?(:cljs (defn ^:dev/after-load start [] (hyperfiddle.rcf/enable!)))
```

# FAQ

*One of my tests threw an exception, but the stack trace is empty?* — you want `{:jvm-opts ["-XX:-OmitStackTraceInFastThrow"]}` [explanation](https://web.archive.org/web/20190416091616/http://yellerapp.com/posts/2015-05-11-clojure-no-stacktrace.html) (this may be JVM specific)

*I see no output* — RCF is off by default, run `(hyperfiddle.rcf/enable!)`

*Emacs has no output and tests are enabled* — check if your emacs supports emojis

*How do I customize what’s printed at the REPL?* — see [reporters.clj](https://github.com/hyperfiddle/rcf/blob/03c821c3875c3dfe647c945430ecdc5a7c8b594f/src/hyperfiddle/rcf/reporters.clj), [reporters.cljs](https://github.com/hyperfiddle/rcf/blob/03c821c3875c3dfe647c945430ecdc5a7c8b594f/src/hyperfiddle/rcf/reporters.cljs)

# Community

&#35;hyperfiddle @ clojurians.net

![Scroll Of Truth meme saying "you do not really understand something until you can explain it as a passing test".](./doc/meme.jpg)


================================================
FILE: bb/runner.clj
================================================
(ns runner
  (:require
   [clojure.test :as test]
   [hyperfiddle.rcf :as rcf]))

(defn run-tests [_]
  (alter-var-root (var rcf/*generate-tests*) (constantly true))
  (let [test-nses '[hyperfiddle.rcf-test
                    hyperfiddle.rcf.analyzer-test
                    ;; needs missionary, doesn't work due to missing reactive streams Java lib:
                    #_hyperfiddle.rcf.unify-test
                    ;; needs missionary:
                    #_hyperfiddle.rcf.example-test
                    ]]
    (apply require test-nses)
    (let [{:keys [fail error]}
          (apply test/run-tests test-nses)]
      (when (and fail error (pos? (+ fail error)))
        (throw (ex-info "Tests failed" {:babasha/exit 1}))))))


================================================
FILE: bb.edn
================================================
{:paths ["bb" "src"]
 :tasks {test:bb {:extra-paths ["test"]
                  :extra-deps {nubank/matcher-combinators {:mvn/version "3.5.1"}}
                  :task (exec 'runner/run-tests)}}}


================================================
FILE: build.clj
================================================
(ns build
  (:require [org.corfield.build :as bb]))

(def defaults
  {:src-pom "pom.xml"
   :lib     'com.hyperfiddle/rcf})                          ; maven group-id and library name

(defn clean [opts]
  (bb/clean opts))

(defn jar [opts]
  (bb/jar (merge defaults opts)))

(defn install [opts]
  (bb/install (merge defaults opts)))

(defn deploy [opts]
  (bb/deploy (merge defaults opts)))

================================================
FILE: build.md
================================================
Build
```shell
HYPERFIDDLE_RCF_DATE=`date -u '+%Y%m%d-%H%M%S'`
git tag v$HYPERFIDDLE_RCF_DATE
clojure -T:build clean
clojure -T:build jar :version '"'$HYPERFIDDLE_RCF_DATE'"'
clojure -T:build install :version '"'$HYPERFIDDLE_RCF_DATE'"'
```

Test local maven repo:
```clojure
{:deps    {org.clojure/clojure {:mvn/version "1.10.3"}
           com.hyperfiddle/rcf {:mvn/version $HYPERFIDDLE_RCF_DATE}}}
```

# echo clj :replace-deps '{:deps {com.hyperfiddle/photon {:mvn/version "'$HYPERFIDDLE_RCF_DATE'"}}}'

Deploy
```shell
CLOJARS_PASSWORD= \
CLOJARS_USERNAME=dustingetz \
clojure -T:build deploy :version '"'$HYPERFIDDLE_RCF_DATE'"'
```


================================================
FILE: ci/run_tests_all.sh
================================================
#!/bin/bash

./run_tests_jvm.sh
./run_tests_node.sh
./run_tests_browser.sh

================================================
FILE: ci/run_tests_browser.sh
================================================
#!/bin/sh

echo "Running Browser tests"
./node_modules/.bin/shadow-cljs -A:cljs:test release :browser-test
./node_modules/.bin/karma start --single-run

================================================
FILE: ci/run_tests_jvm.sh
================================================
#!/bin/sh

echo "Running JVM tests"
clojure -X:test :dirs "[\"test\" \"example\"]" :patterns "[\"example.*\" \"hyperfiddle.rcf.*-test\"]"

================================================
FILE: ci/run_tests_node.sh
================================================
#!/bin/sh

echo "Running NodeJS tests"
./node_modules/.bin/shadow-cljs -A:cljs:test release :test
node out/node-tests.js

================================================
FILE: deps.edn
================================================
{:paths   ["src"]
 :deps    {org.clojure/clojure {:mvn/version "1.10.3"}}
 :aliases {:cljs {:extra-deps {org.clojure/clojurescript {:mvn/version "1.10.896"}}}
           :dev  {:extra-deps {thheller/shadow-cljs {:mvn/version "2.16.8"}}}
           :test {:jvm-opts    ["-XX:-OmitStackTraceInFastThrow"
                                "-Dhyperfiddle.rcf.generate-tests=true"]
                  :extra-paths ["test" "example"]
                  :extra-deps  {thheller/shadow-cljs   {:mvn/version "2.16.8"}
                                nubank/matcher-combinators {:mvn/version "3.5.1"}
                                missionary/missionary  {:mvn/version "b.26"}
                                org.clojure/core.async {:mvn/version "1.5.648"}
                                io.github.cognitect-labs/test-runner
                                {:git/url "https://github.com/cognitect-labs/test-runner.git"
                                 :sha     "cc75980b43011773162b485f46f939dc5fba91e4"}}
                  :exec-fn     cognitect.test-runner.api/test}
           :build {:deps {io.github.seancorfield/build-clj {:git/tag "v0.8.0" :git/sha "9bd8b8a"}}
                   :ns-default build}}}


================================================
FILE: example/dev_entrypoint.cljc
================================================
(ns dev-entrypoint
  (:require [example]
            [hyperfiddle.rcf :as rcf :refer [tests]]))

; enable tests after app namespaces are loaded (intended for subsequent REPL interactions)
(comment
  (rcf/enable!)
  )

; prevent test execution during cljs hot code reload
#?(:cljs (defn ^:dev/before-load stop [] (rcf/enable! false)))
#?(:cljs (defn ^:dev/after-load start [] (rcf/enable! true)))


================================================
FILE: example/example.cljc
================================================
(ns example
  (:require [clojure.core.async :refer [chan >! go go-loop <! timeout close!]]
            [clojure.test :as t]
            [hyperfiddle.rcf :as rcf :refer [tests tap %]]
            [missionary.core :as m]))

(defn get-extension [path]
  (let [found (last (re-find #"(\.[a-zA-Z0-9]+)$" path))
        ext (and found (subs found 1))]
    (or ext "")))

(tests
  "empty"
  (get-extension "") := ""
  (get-extension ".") := ""
  (get-extension "..") := ""
  (get-extension "image") := ""
  (get-extension "image.") := ""
  (get-extension "image..") := ""

  "found"
  (get-extension "image.png") := "png"
  (get-extension "image.blah.png") := "png"
  (get-extension "image.blah..png") := "png")


(tests
  "equality"
  (inc 1) := 2

  "wildcards"
  {:a :b, :b [2 :b]} := {:a _, _ [2 _]}

  "unification"
  {:a :b, :b [2 :b]} := {:a ?b, ?b [2 ?b]}

  "unification on reference types"
  (def x (atom nil))
  {:a x, :b x} := {:a ?x, :b ?x}

  (tests
    "nested tests (is there a strong use case?)"
    1 := 1

    "tests form returns final result"
    (tests (inc 1) := 2 (inc *1)) := 3)

  (tests
    "REPL bindings work"
    (keyword "a") := :a
    (keyword "b") := :b
    (keyword "c") := :c
    *1 := :c
    *2 := :b
    *3 := :a
    *1 := :c                                                  ; inspecting history does not affect history

    (keyword "d") := :d
    *1 := :d
    *2 := :c
    *3 := :b
    (symbol *2) := 'c                                         ; this does affect history
    (symbol *2) := 'd))

(tests
  (rcf/set-timeout! 100)
  "async tests"
  #?(:clj  (tests
            (future
              (rcf/tap 1) (Thread/sleep 10)
              (rcf/tap 2) (Thread/sleep 200)
              (rcf/tap 3))
            % := 1
            % := 2
            % := ::rcf/timeout)
     :cljs (tests
            (defn set-timeout [f ms] (js/setTimeout ms f))
            (rcf/tap 1) (set-timeout 10 (fn []
                                       (rcf/tap 2) (set-timeout 200 (fn []
                                                                   (rcf/tap 3)))))
            % := 1
            % := 2
            % := ::rcf/timeout
            )))



(tests
  "core.async"
  (def c (chan))
  (go-loop [x (<! c)]
    (when x
      (<! (timeout 10))
      (tap x)
      (recur (<! c))))
  (go (>! c :hello) (>! c :world))
  % := :hello
  % := :world
  (close! c))

(tests
  "missionary"
  (def !x (atom 0))
  (def dispose ((m/reactor (m/stream! (m/ap (tap (inc (m/?< (m/watch !x)))))))
                (fn [_] #_(prn ::done)) #(prn ::crash %)))
  % := 1
  (swap! !x inc)
  (swap! !x inc)
  % := 2
  % := 3
  (dispose))


================================================
FILE: out/index.html
================================================
<!DOCTYPE html>
<html>
    <head></head>
    <body>
        <h1>Check your javascript console.</h1>
        <script type="text/javascript" src="./js/main.js"></script>
    </body>
</html>


================================================
FILE: package.json
================================================
{
    "name": "rcf",
    "dependencies": {},
    "devDependencies": {
        "karma": "6.3.17",
        "karma-chrome-launcher": "3.1.1",
        "karma-cljs-test": "0.1.0",
        "puppeteer": "13.5.2",
        "shadow-cljs": "2.16.8"
    }
}


================================================
FILE: pom.xml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <packaging>jar</packaging>
  <groupId>><!-- generated --></groupId>
  <artifactId>><!-- generated --></artifactId>
  <version>><!-- generated --></version>
  <name>rcf</name>
  <description>RCF – a REPL-first, async test macro for Clojure/Script</description>
  <scm>
    <url>https://github.com/hyperfiddle/rcf</url>
    <connection>scm:git:git://github.com/hyperfiddle/rcf.git</connection>
    <developerConnection>scm:git:ssh://git@github.com/hyperfiddle/rcf.git</developerConnection>
    <tag><!-- generated --></tag>
  </scm>
  <dependencies>
    <!-- list regenerated from deps.edn -->
    <dependency>
      <groupId>org.clojure</groupId>
      <artifactId>clojure</artifactId>
      <version>1.10.3</version>
    </dependency>
  </dependencies>
  <build>
    <sourceDirectory>src</sourceDirectory>
    <!--<sourceDirectory>examples</sourceDirectory>-->
  </build>
  <repositories>
    <repository>
      <id>clojars</id>
      <url>https://repo.clojars.org/</url>
    </repository>
  </repositories>
</project>


================================================
FILE: run_dev_repl.sh
================================================
#!/bin/bash

npx shadow-cljs -d nrepl/nrepl:0.9.0-beta4 -d cider/cider-nrepl:0.27.2 -A:dev:test:cljs server


================================================
FILE: shadow-cljs.edn
================================================
{:deps     {:aliases [:cljs]}
 :dev-http {8000 "out"}
 :builds   {:browser {:target     :browser
                      :modules    {:main {:entries [dev-entrypoint]}}
                      :output-dir "out/js"}
            :test    {:target    :node-test
                      :output-to "out/node-tests.js"
                      :ns-regexp "^(hyperfiddle.rcf.*-test|example)"
                      }
            :browser-test {:target    :karma
                           :output-to "out/karma-tests.js"
                           :ns-regexp "^(hyperfiddle.rcf.*|example.*)"}}}


================================================
FILE: src/hyperfiddle/rcf/analyzer.clj
================================================
;; A simpler tools.analyzer for the restricted use case of RCF
;; Adapted from  https://github.com/clojure/tools.analyzer
(ns hyperfiddle.rcf.analyzer
  (:refer-clojure :exclude [macroexpand-1 macroexpand update-vals])
  (:import (clojure.lang IObj)))

(defn cljs? [env] (some? (:js-globals env)))

(defn empty-env
  "Returns an empty env"
  []
  {:locals     {}
   :namespaces {}
   :ns         (ns-name *ns*)})

(defn to-env [&env]
  (if (:js-globals &env)
    &env
    (assoc (empty-env) :locals (or &env {}))))



(defn build-ns-map []
  {:namespaces (into {} (mapv #(vector (ns-name %)
                                       {:mappings (merge (ns-map %) {'in-ns #'clojure.core/in-ns})
                                        :aliases  (reduce-kv (fn [a k v] (assoc a k (ns-name v)))
                                                             {} (ns-aliases %))
                                        :ns       (ns-name %)})
                              (all-ns)))})

(def ^:dynamic *global-env* nil)

(defn global-env [] (or *global-env* (build-ns-map)))

(defn resolve-local [env sym] (get-in env [:locals sym]))

(defn resolve-ns
  "Resolves the ns mapped by the given sym in the global env"
  [ns-sym {:keys [ns]}]
  (when ns-sym
    (let [namespaces (:namespaces (global-env))]
      (or (get-in namespaces [ns :aliases ns-sym])
          (:ns (namespaces ns-sym))))))

(defn var?' [maybe-var] (or (var? maybe-var) (= ::var (type maybe-var))))

(defn to-var [{:keys [macro meta ns name]}]
  (with-meta {:ns ns, :name name} (assoc meta :type ::var)))

(defmacro no-warn
  "Localy disable a set of cljs compiler warning.
  Usage: `(no-warn #{:undeclared-ns} (cljs/resolve env sym))`"
  [disabled-warnings & body]
  ;; Cannot use `cc/binding` as it relies on var which does a read-time resolve,
  ;; while we want a runtime var resolve.
  `(do (push-thread-bindings {(resolve 'cljs.analyzer/*cljs-warnings*)
                            (reduce (fn [r# k#] (assoc r# k# false))
                              (deref (resolve 'cljs.analyzer/*cljs-warnings*))
                              ~disabled-warnings)})
       (try ~@body
            (finally (pop-thread-bindings)))))

(defn cljs-resolve [env sym]
  (require '[cljs.analyzer.api])
  (require '[cljs.analyzer])
  ;; RCF should try to resolve like the repl does, but is not in charge of
  ;; handling invalid userland forms.
  (no-warn #{:undeclared-ns} ((resolve 'cljs.analyzer.api/resolve) env sym)))

(defn resolve-sym
  "Resolves the value mapped by the given sym in the global env"
  [sym {:keys [ns] :as env}]
  (when (symbol? sym)
    (if (cljs? env)
      (let [resolved (cljs-resolve env sym)]
        (if (or (:macro resolved) (= :var (:op resolved)))
          (resolve-sym (:name resolved) (dissoc env :js-globals))
          (to-var resolved)))
      (let [sym-ns  (when-let [ns (namespace sym)] (symbol ns))
            full-ns (resolve-ns sym-ns env)]
        (when (or (not sym-ns) full-ns)
          (let [name (if sym-ns (-> sym name symbol) sym)]
            (-> (global-env) :namespaces (get (or full-ns ns)) :mappings (get name))))))))

(def specials "Set of special forms common to every clojure variant" ;; TODO replace with cc/special-symbol?
  '#{do if new quote set! try var catch throw finally def . let* letfn* loop* recur fn*})

(defn var-sym [v] 
  (cond
    (var? v) (symbol v)
    (var?' v) (symbol (str (:ns v)) (str (:name v)))))

(defmulti macroexpand-hook (fn [the-var _&form _&env _args] (var-sym the-var)))
(defmethod macroexpand-hook :default [the-var &form &env args]
  (if (cljs? &env)
    (if (:cljs.analyzer/numeric (meta the-var))
      (reduced &form)
      (let [mform (apply the-var &form &env args)]
        (if (and (seq? mform) (= 'js* (first mform)))
          (reduced &form)
          mform)))
    (apply the-var &form (:locals &env) args)))

(defn has-meta? [o] (instance? clojure.lang.IMeta o))

(defmulti -parse (fn [_env form] (and (seq? form) (first form))))

(defn parse
  ([]    (parse (empty-env)))
  ([env] (partial parse env))
  ([env form]
   {:pre (map? env)}
   (-parse env form)))

(defn classify
  "Returns a keyword describing the form type"
  [form]
  (cond
    ;; (nil? form)     :nil
    ;; (boolean? form) :bool
    ;; (keyword? form) :keyword
    (symbol? form)  :symbol
    ;; (string? form)  :string
    ;; (number? form)  :number
    ;; (type? form)    :type
    ;; (record? form)  :record
    (map? form)     :map
    (vector? form)  :vector
    (set? form)     :set
    (seq? form)     :seq
    ;; (char? form)    :char
    ;; (regex? form)   :regex
    ;; (class? form)   :class
    (var? form)     :var
    :else           :const))

(defmulti -analyze (fn [env form]
                     {:pre (map? env)}
                     (classify form)))

(defn analyze
  ([env]      (partial analyze env))
  ([env form] (-analyze env form)))

(defn obj? [x] (instance? IObj x))

(defn- analyze-const [env form]
  {:op   :const
   :env  env
   :form form})

(defmethod -analyze :const [env form] (analyze-const env form))

(defn analyze-body [env body]
  ;; :body is used by emit-form to remove the artificial 'do
  (assoc (parse env (cons 'do body)) :body? true)) ;; default

(defn wrapping-meta
  [{:keys [form env] :as expr}]
  (let [meta (meta form)]
    (if (and (obj? form)
             (seq meta))
      {:op       :with-meta
       :env      env
       :form     form
       :meta     meta
       :expr     expr
       :children [:meta :expr]}
      expr)))

(defmethod -analyze :vector [env form]
  (let [items (mapv (analyze env) form)]
    (wrapping-meta
     {:op       :vector
      :env      env
      :items    items
      :form     form
      :children [:items]})))

(defmethod -analyze :map [env form]
  (wrapping-meta
   {:op       :map
    :env      env
    :keys     (mapv (analyze env) (keys form))
    :vals     (mapv (analyze env) (vals form))
    :form     form
    :children [:keys :vals]}))

(defmethod -analyze :set [env form]
  (wrapping-meta
   {:op       :set
    :env      env
    :items    (mapv (analyze env) form)
    :form     form
    :children [:items]}))

(defmethod -analyze :seq [env form]
  (if (not (seq form))
    (analyze-const env form)
    (parse env form)))

(defmethod -analyze :symbol [env sym]
  (let [local? (some? (resolve-local env sym))]
    {:op :symbol
     :local? local?
     :env  env
     :form sym
     :ns   (namespace sym)
     :name (name sym)}))

(defmethod -analyze :var [env form]
  {:op :the-var
   :env env
   :var form})

;; --------

(defn valid-binding-symbol? [s]
  (and (symbol? s)
       (not (namespace s))
       (not (re-find #"\." (name s)))))

(defn source-info
  "Returns the available source-info keys from a map"
  [m]
  (when (:line m)
    (select-keys m #{:file :line :column :end-line :end-column :source-span})))

(defn -source-info
  "Returns the source-info of x"
  [env x]
  (merge (source-info env)
         (source-info (meta x))
         (when-let [file (and (not= *file* "NO_SOURCE_FILE")
                              *file*)]
           {:file file})))

(defn validate-bindings
  [[op bindings & _ :as form] env]
  (when-let [error-msg
             (cond
               (not (vector? bindings))
               (str op " requires a vector for its bindings, had: "
                    (class bindings))

               (not (even? (count bindings)))
               (str op " requires an even number of forms in binding vector, had: "
                    (count bindings)))]
    (throw (ex-info error-msg
                    (merge {:form     form
                            :bindings bindings}
                           (-source-info env form))))))

(defn dissoc-env [ast] (dissoc ast :env))

(defn analyze-let [env [_ bindings & body :as form]]
  (validate-bindings env form)
  (loop [bindings bindings
         env      env
         binds    []]
    (if-let [[name init & bindings] (seq bindings)]
      (if (not (valid-binding-symbol? name))
        (throw (ex-info (str "Bad binding form: " name)
                        (merge {:form form
                                :sym  name}
                               (-source-info form env))))
        (let [bind-expr {:op       :binding
                         :env      env
                         :name     name
                         :init     (analyze env init)
                         :form     name
                         :local    :let
                         :children [:init]}]
          (recur bindings
                 (assoc-in env [:locals name] (dissoc-env bind-expr))
                 (conj binds bind-expr))))
      {:body     (analyze-body env body)
       :bindings binds
       :children [:bindings :body]})))

(defn analyze-fn-method [{:keys [locals local] :as env} [params & body :as form]]
  (when-not (vector? params)
    (throw (ex-info "Parameter declaration should be a vector"
                    (merge {:params params
                            :form   form}
                           (-source-info form env)
                           (-source-info params env)))))
  (when (not-every? valid-binding-symbol? params)
    (throw (ex-info (str "Params must be valid binding symbols, had: "
                         (mapv class params))
                    (merge {:params params
                            :form   form}
                           (-source-info form env)
                           (-source-info params env))))) ;; more specific
  (let [variadic?    (boolean (some '#{&} params))
        params-names (if variadic? (conj (pop (pop params)) (peek params)) params)
        env          (dissoc env :local)
        arity        (count params-names)
        params-expr  (mapv (fn [name id]
                             {:op        :binding
                              :env       env
                              :form      name
                              :name      name
                              :variadic? (and variadic?
                                              (= id (dec arity)))
                              :arg-id    id
                              :local     :arg})
                           params-names (range))
        fixed-arity  (if variadic?
                       (dec arity)
                       arity)
        body-env (update-in env [:locals] merge (zipmap params-names (map dissoc-env params-expr)))
        body         (analyze-body body-env body)]
    (when variadic?
      (let [x (drop-while #(not= % '&) params)]
        (when (contains? #{nil '&} (second x))
          (throw (ex-info "Invalid parameter list"
                          (merge {:params params
                                  :form   form}
                                 (-source-info form env)
                                 (-source-info params env)))))
        (when (not= 2 (count x))
          (throw (ex-info (str "Unexpected parameter: " (first (drop 2 x))
                               " after variadic parameter: " (second x))
                          (merge {:params params
                                  :form   form}
                                 (-source-info form env)
                                 (-source-info params env)))))))
    (merge
     {:op          :fn-method
      :form        form
      :env         env
      :variadic?   variadic?
      :params      params-expr
      :fixed-arity fixed-arity
      :body        body
      :children    [:params :body]}
     (when local
       {:local (dissoc-env local)}))))

(defmethod -parse 'do [env [_ & exprs :as form]]
  {:op         :do
   :env        env
   :form       form
   :statements (mapv (analyze env) exprs)
   :children   [:statements]})

(defmethod -parse 'if [env [_ test then else :as form]]
  {:op       :if
   :form     form
   :env      env
   :test     (analyze env test)
   :then     (analyze env then)
   :else     (analyze env else)
   :children [:test :then :else]})

(defmethod -parse 'quote [env form]
  {:op       :quote
   ;; :expr     (analyze env expr) ;; maybe not needed
   :form     form
   :env      env
   :children []})

(defmethod -parse 'try [env [_ & body :as form]]
  (let [catches (filter (every-pred seq? #(= 'catch (first %))) body)
        finallies (filter (every-pred seq? #(= 'finally (first %))) body)
        body    (remove (into #{} (concat catches finallies)) body)]
    {:op       :try
     :env      env
     :form     form
     :body     (analyze-body env body)
     :catches  (mapv (partial -parse env) catches)
     :finally  (mapv (partial -parse env) finallies)
     :children [:body :catches :finally]}))

(defmethod -parse 'catch [env [_ etype ename & body :as form]]
  (let [local {:op   :binding
               :env  env
               :form ename
               :name ename
               :local :catch ;; maybe not needed
               }]
    {:op       :catch
     :class    (analyze (assoc env :locals {}) etype)
     :local    local
     :env      env
     :form     form
     :body     (analyze-body (assoc-in env [:locals ename] (dissoc-env local)) body)
     :children [:local :body]}))

(defmethod -parse 'let* [env form]
  (into {:op   :let
         :form form
         :env  env}
        (analyze-let env form)))

(defmethod -parse 'loop* [env form]
  (into {:op      :loop
         :form    form
         :env     env}
        (analyze-let env form)))

(defmethod -parse :default [env form]
  (if (seq? form)
    (let [[f & args] form]
      {:op       :invoke
       :form     form
       :env      env
       :fn       (analyze env f)
       :args     (mapv (analyze env) args)
       :children [:fn :args]})
    (analyze env form)))

(defn ns-sym [ns] (cond (symbol? ns) ns
                        (map? ns) (:name ns)
                        :else (ns-name ns)))

(defn unquote' [form]
  (if (and (seq? form) (= 'quote (first form)))
    (second form)
    form))

(defn update-vals
  "Applies f to all the vals in the map"
  [m f]
  (reduce-kv (fn [m k v] (assoc m k (f v))) {} (or m {})))

(defmacro if-bb [then else] (if (System/getProperty "babashka.version") then else))

(defn create-var
  "Creates a Var for sym and returns it.
   The Var gets interned in the env namespace."
  [sym {:keys [ns] :as env}]
  (let [v (get-in (global-env) [:namespaces ns :mappings (symbol (name sym))])]
    (if (some? v)
      (cond
        (class? v) v
        (and (var? v) (= ns (ns-name (if-bb (:ns (meta v)) (.ns ^clojure.lang.Var v)))))
        (do (when-some [m (meta sym)]
              (if-bb (alter-meta! v (constantly (update-vals m unquote')))
                (.setMeta v (update-vals m unquote')))) v)
        :else (throw (ex-info (str "(def " sym " ...) resolved to an existing mapping of an unexpected type.")
                              {:sym         sym
                               :ns          ns
                               :resolved-to v
                               :type        (type v)})))
      (let [meta (-> (dissoc (meta sym) :inline :inline-arities #_:macro)
                     (update-vals unquote'))
            #_#_meta (if-let [arglists (:arglists meta)]
                       (assoc meta :arglists (qualify-arglists arglists))
                       meta)]
        (intern (ns-sym ns) (with-meta sym meta))))))

(defn- to-cljs-var [var]
  (let [m (-> (meta var))
        m (as-> m $ 
            (update $ :ns ns-name)
            (assoc $ :name (symbol (str (:ns $)) (str (:name $)))))]
    (assoc m :meta m)))

(defn- intern-cljs-var! [cljs-var]
  (require 'cljs.env)
  (let [ns (:ns cljs-var)
        name (symbol (name (:name cljs-var)))
        *compiler* (deref (resolve 'cljs.env/*compiler*))]
    (swap! *compiler* assoc-in [:cljs.analyzer/namespaces ns :defs name] cljs-var)
    nil))

(defmethod -parse 'def [{:keys [ns] :as env} [_ sym & expr :as form]]
  (let [pfn  (fn
               ([])
               ([init]
                {:init init})
               ([doc init]
                {:pre [(string? doc)]}
                {:init init :doc doc}))
        args (apply pfn expr)
        env (if (some? (namespace sym))
              env ;; Can't intern namespace-qualified symbol, ignore
              (let [var (create-var sym env)] ;; side effect, FIXME should be a pass  
                (when (cljs? env)
                  (intern-cljs-var! (to-cljs-var var)))
                (assoc-in env [:namespaces ns :mappings sym] var)))
        args (when-let [[_ init] (find args :init)]
               (assoc args :init (analyze env init)))]
    (merge {:op       :def
            :env      env
            :form     form
            :name     sym
            :doc      (or (:doc args) (-> sym meta :doc))
            :children (into [] (when (:init args) [:init]))}
           args)))

(defmethod -parse 'fn* [env [op & args :as form]]
  (wrapping-meta
   (let [[n meths]       (if (symbol? (first args))
                           [(first args) (next args)]
                           [nil (seq args)])
         name-expr       {:op    :binding
                          :env   env
                          :form  n
                          :local :fn
                          :name  n}
         e               (if n (assoc (assoc-in env [:locals n] (dissoc-env name-expr)) :local name-expr) env)
         once?           (-> op meta :once boolean)
         menv            (assoc e :once once?)
         meths           (if (vector? (first meths)) (list meths) meths) ;;turn (fn [] ...) into (fn ([]...))
         methods-exprs   (mapv #(analyze-fn-method menv %) meths)]
     (merge {:op              :fn
             :env             env
             :form            form
             :methods         methods-exprs
             :once            once?}
            (when n
              {:local name-expr})
            {:children (conj (if n [:local] []) :methods)}))))

(defmethod -parse 'letfn* [env [_ bindings & body :as form]]
  (validate-bindings env form)
  (let [bindings (apply array-map bindings)            ;; pick only one local with the same name, if more are present.
        fns      (keys bindings)
        binds    (reduce (fn [binds name]
                           (assoc binds name
                                  {:op    :binding
                                   :env   env
                                   :name  name
                                   :form  name
                                   :local :letfn}))
                         {} fns)
        e        (update-in env [:locals] merge binds) ;; pre-seed locals
        binds    (reduce-kv (fn [binds name bind]
                              (assoc binds name
                                     (merge bind
                                            {:init     (analyze e (bindings name))
                                             :children [:init]})))
                            {} binds)
        e        (update-in env [:locals] merge (update-vals binds dissoc-env))]
    {:op       :letfn
     :env      env
     :form     form
     :bindings (vec (vals binds)) ;; order is irrelevant
     :body     (analyze-body e body)
     :children [:bindings :body]}))

;;;;;;;;;;
;; EMIT ;;
;;;;;;;;;;

(def ^:dynamic *emit-options* {:simplify-do false}) ;; FIXME :simplify-* should be passes

(defmulti -emit (fn [ast] (:op ast)))

(defn emit [ast] (-emit ast))

(defmethod -emit :const [ast] (:form ast))

(defmethod -emit :symbol [ast] (:form ast))
(defmethod -emit :var [ast] (:form ast))

(defn emit-invoke [ast] (list* (emit (:fn ast)) (mapv emit (:args ast))))
(defmethod -emit :invoke [ast] (emit-invoke ast))

(defmethod -emit :do [ast]
  (if (and (:simplify-do *emit-options*)
           (= 1 (count (:statements ast))))
    (emit (first (:statements ast)))
    (list* 'do (mapv emit (:statements ast)))))

(defmethod -emit :vector [ast] (mapv emit (:items ast)))
(defmethod -emit :set [ast] (set (mapv emit (:items ast))))
(defmethod -emit :map [ast] (zipmap (mapv emit (:keys ast))
                                    (mapv emit (:vals ast))))

(defmethod -emit :with-meta [ast] (with-meta (emit (:expr ast)) (:meta ast)))

(defmethod -emit :try [ast] (list* 'try (emit (:body ast))
                                   (concat (mapv emit (:catches ast))
                                           (mapv emit (:finally ast)))))
(defmethod -emit :catch [ast] (list 'catch (emit (:class ast)) (emit (:local ast)) (emit (:body ast))))

(defmethod -emit :binding [ast]
  (case (:local ast)
    :catch        (:form ast)
    (:let :letfn) [(:name ast) (emit (:init ast))]
    :fn           (:name ast)
    :arg          (if (:variadic? ast)
                    ['& (:name ast)]
                    [(:name ast)])))

(defmethod -emit :quote [ast] (:form ast))

(defmethod -emit :if [ast] (list 'if (emit (:test ast)) (emit (:then ast)) (emit (:else ast))))

(defmethod -emit :def [ast]
  (if-let [init (:init ast)]
    (list 'def (:name ast) (emit init))
    (list 'def (:name ast))))

(defmethod -emit :let [ast]
  (list* 'let* (vec (mapcat identity (mapv emit (:bindings ast))))
         (if (:simplify-do *emit-options*) ;; FIXME should be a pass
           (mapv emit (:statements (:body ast)))
           (list (emit (:body ast))))))

(defmethod -emit :loop [ast]
  (list 'loop* (vec (mapcat identity (mapv emit (:bindings ast)))) (emit (:body ast))))

(defmethod -emit :fn [ast]
  (let [methods (mapv emit (:methods ast))]
    (if-let [name (some-> (:local ast) emit)]
      `(~'fn* ~name ~@methods)
      `(~'fn* ~@methods))))

(defmethod -emit :fn-method [ast]
  (list (vec (mapcat emit (:params ast))) (emit (:body ast))))

(defmethod -emit :letfn [ast]
  (list 'letfn* (vec (mapcat identity (mapv emit (:bindings ast)))) (emit (:body ast))))

;; AST walk

(defn walk [inner outer ast]
  (when (some? ast)
    (if (reduced? ast) ast
        (outer (reduce (fn [ast child-key]
                         (if (sequential? (get ast child-key))
                           (update ast child-key (partial mapv inner))
                           (update ast child-key inner)))
                       ast (:children ast))))))

(defn postwalk [f ast] (unreduced (walk (partial postwalk f) f ast)))
(defn prewalk [f ast] (unreduced (walk (partial prewalk f) identity (f ast))))

(defn only-nodes [pred f] ;; use with *walk to skip some nodes
  (let [pred (if (set? pred) (comp pred :op) pred)]
    (fn [ast] (if (pred ast) (f ast) ast))))

(defn children [ast]
  (when (some? ast)
    (mapcat (fn [child]
              (let [child-ast (get ast child)]
                (if (sequential? child-ast)
                  child-ast
                  (list child-ast))))
            (:children ast))))
(defn ast-seq
  "Equivalent of `cc/tree-seq` on AST nodes"
  [ast]
  (when (some? ast)
    (cons ast (mapcat ast-seq (children ast)))))

;; Passes

(defn resolve-sym-node [{:keys [env] :as ast}]
  (assert (= :symbol (:op ast)))
  (if (:local? ast)
    ast
    (if-let [v (resolve-sym (:form ast) env)]
      (if (var?' v)
        (assoc ast :op :var, :var v)
        ast)
      ast)))

(defn resolve-syms-pass [ast] (prewalk (only-nodes #{:symbol} resolve-sym-node) ast))

(defn- tag-with-form [ast parent form] (assoc ast :raw-forms (conj (:raw-forms parent ()) (list 'quote form))))

(defn macroexpand-node [{:keys [env] :as ast}]
  (let [{:keys [op var]} (:fn ast)
        [f & args :as form] (:form ast)]
    (if (and (= :var op) (:macro (meta var)) (not (::prevent-macroexpand (meta f))))
      (let [
            mform (macroexpand-hook var form env args)
            var'  (when (seq? mform) (resolve-sym (first mform) env))]
        (cond
          (= form mform)   (reduced ast)
          (reduced? mform) (reduced (tag-with-form (parse env (unreduced mform)) ast form))
          (= var var')     (let [[f & args] mform
                                 f (if (contains? (methods macroexpand-hook) f)
                                     (vary-meta f assoc ::prevent-macroexpand true)
                                     f)]
                             (tag-with-form (analyze env (cons f args)) ast form))
          :else            (tag-with-form (analyze env mform) ast form)))
      ast)))

(defn macroexpand-pass
  ([ast] (macroexpand-pass ##Inf ast))
  ([n ast]
   (let [state (atom n)]
     (prewalk (only-nodes #{:invoke} (fn rec [ast]
                                       (if-not (pos? @state)
                                         (reduced ast) ;; stop walking
                                         (let [ast' (macroexpand-node ast)]
                                           (binding [*global-env* (build-ns-map)]
                                             (let [ast'-resolved (resolve-syms-pass (unreduced ast'))]
                                               (cond
                                                 (reduced? ast') (reduced ast'-resolved)
                                                 (= ast ast')    ast'-resolved
                                                 :else           (if (pos? (swap! state dec))
                                                                   (if (= :invoke (:op ast'-resolved))
                                                                     (rec ast'-resolved)
                                                                     ast'-resolved)
                                                                   ast'-resolved))))))))
              ast))))

(defn macroexpand-n
  ([n form] (macroexpand-n n (empty-env) form))
  ([n env form]
   (binding [*global-env* (build-ns-map)]
     (->> (analyze env form)
          (resolve-syms-pass)
          (macroexpand-pass n)
          (emit)))))

(defn macroexpand-all
  ([form] (macroexpand-all (empty-env) form))
  ([env form] (macroexpand-n ##Inf env form)))

(defn macroexpand-1
  ([form] (macroexpand-1 (empty-env) form))
  ([env form] (macroexpand-n 1 env form)))


================================================
FILE: src/hyperfiddle/rcf/impl.clj
================================================
(ns hyperfiddle.rcf.impl
  (:require [clojure.string :as str]
            [clojure.test :as t]
            [clojure.walk :as walk]
            [hyperfiddle.rcf.analyzer :as ana]
            [hyperfiddle.rcf.queue :as q]
            [hyperfiddle.rcf.time :as time]
            [hyperfiddle.rcf.unify :as u]
            [hyperfiddle.rcf.reporters]))

(defn rewrite-doc [env ast]
  (ana/prewalk
   (ana/only-nodes #{:do}
                   (fn [do-ast]
                     (assoc do-ast :statements
                            (loop [[s & ss] (:statements do-ast)
                                   r []]
                              (if (nil? s) r
                                  (if (and (string? (:form s)) (seq ss))
                                    (let [testing-ast (ana/analyze env `(~`t/testing ~(:form s)))]
                                      (->> (assoc do-ast :statements (vec ss))
                                           (update testing-ast :args conj)
                                           (conj r)))
                                    (recur ss (conj r s))))))))
   ast))

(defn rewrite-star [env ast]
  (ana/postwalk
   (ana/only-nodes #{:var}
                   (fn [var-ast]
                     (if-let [index (condp = (:var var-ast) #'*1 0, #'*2, 1 #'*3 2, nil)]
                       (-> (ana/analyze env `(~'RCF__peek! ~index))
                           (assoc :raw-forms (list (:form var-ast)))
                           (assoc ::star true))
                       var-ast)))
   ast))


(defn star? [ast] (or (::star ast)
                      (and (= :var (:op ast))
                           (#{#'*1 #'*2 #'*3} (:var ast)))))

(defn has-stars? [ast] (some? (first (filter star? (ana/ast-seq ast)))))

(defn maybe-add-stars-support [env ast]
  (if (has-stars? ast)
    (-> (ana/analyze env `(let [[~'RCF__push! ~'RCF__peek!] (hyperfiddle.rcf/binding-queue)]))
        (ana/resolve-syms-pass)
        (ana/macroexpand-pass)
        (update-in [:body :statements] conj ast))
    ast))

(defn make-queue [timeout-value]
  `(let [q#       (q/queue)
         start#   (time/current-time)
         timeout# (atom hyperfiddle.rcf/*timeout*)]
     [(fn [x#] (q/offer! q# x#) x#)
      (fn
        ([] (q/poll! q# start# (deref timeout#) ~timeout-value))          ; blocking
        ([n# cb#] (q/poll-n! q# start# (deref timeout#) ~timeout-value n# cb#)) ; non blocking
        )
      (partial reset! timeout#)]))

(defn %? [ast] (and (= :var (:op ast)) (= 'hyperfiddle.rcf/% (ana/var-sym (:var ast)))))
(defn has-%? [ast] (some? (first (filter %? (ana/ast-seq ast)))))
(defn maybe-add-queue-support [env ast]
  (if (has-%? ast)
    (-> (ana/analyze env `(let [[~'RCF__tap ~'RCF__% ~'RCF__set-timeout!] (hyperfiddle.rcf/make-queue :hyperfiddle.rcf/timeout)]))
        (ana/resolve-syms-pass)
        (ana/macroexpand-pass)
        (update-in [:body :statements] conj ast))
    ast))

(defn rewrite-tap-% [env ast]
  (if-not (has-%? ast)
    ast
    (ana/postwalk
     (ana/only-nodes #{:var}
                     (fn [var-ast]
                       (condp = (ana/var-sym (:var var-ast))
                         'hyperfiddle.rcf/tap (assoc var-ast :form 'RCF__tap)
                         'hyperfiddle.rcf/! (assoc var-ast :form 'RCF__tap)
                         'hyperfiddle.rcf/set-timeout! (assoc var-ast :form 'RCF__set-timeout!)
                         'hyperfiddle.rcf/%  (if (ana/cljs? env)
                                               var-ast
                                               (-> (ana/analyze env `(~'RCF__%))
                                                   (assoc :raw-forms (list (:form var-ast)))))
                         var-ast)))
     ast)))

(defn- inspect-star-only? [ast]
  (and (= `t/is (:form (:fn ast)))
       (let [[left right] (some-> ast :args first :args)]
         (or (and (star? left) (not (has-stars? right)))
             (and (star? right) (not (has-stars? left)))))))

(defn rewrite-repl [env ast]
  (ana/prewalk (ana/only-nodes #{:do}
                               (fn [do-ast]
                                 (if-not (has-stars? do-ast)
                                   do-ast
                                   (assoc do-ast :statements
                                          (loop [[s & ss] (:statements do-ast)
                                                 r        []]
                                            (cond
                                              (nil? s) r
                                              (empty? ss) (recur ss (conj r s))
                                              :else
                                              (if-not (inspect-star-only? s)
                                                (let [invoke-ast (-> (ana/analyze env '(RCF__push!))
                                                                     (update :args conj s))]
                                                  (recur ss (conj r invoke-ast)))
                                                (recur ss (conj r s)))))))))
               ast))


(defmulti replace-sigil identity)
(defmethod replace-sigil :default [sym] sym)
(defmethod replace-sigil := [_sym] :hyperfiddle.rcf/=)
(defmethod replace-sigil 'thrown? [_sym] 'hyperfiddle.rcf/thrown?)
(defmethod replace-sigil :throws [_sym] 'hyperfiddle.rcf/thrown?)

(defn replace-sigil* [sym]
  (let [sym' (replace-sigil sym)]
    (if (= sym sym')
      sym' (recur sym'))))

(defn make-is [env a b c]
  (let [sigil     (replace-sigil* (:form b))
        inner-ast (-> (ana/analyze env `(~sigil))
                      (update :args conj a c))]
    (-> (ana/analyze env `(t/is))
        (update :args conj inner-ast))))

(defn lvar? [ast]
  (and (#{:var :symbol} (:op ast))
       (or (= '_ (:form ast))
           (str/starts-with? (str (:form ast)) "?"))))
(defn has-lvars? [ast] (some? (first (filter lvar? (ana/ast-seq ast)))))

(defn simplify-sigil [left center right]
  (cond
    (and (= := (:form center))
         (not (has-lvars? left))
         (not (has-lvars? right))) 'hyperfiddle.rcf/=
    :else (:form center)))

(defmulti rewrite-infix (fn [_env _left center _right] (:form center)))
(defmethod rewrite-infix :default [env l c r]
  (let [sigil-ast  (ana/analyze env (replace-sigil* (simplify-sigil l c r)))]
    (make-is env l sigil-ast r)))

(defn sigil? [ast]
  (let [methods (methods t/assert-expr)
        sigil   (replace-sigil* (if (= :var (:op ast))
                                  (ana/var-sym (:var ast))
                                  (:form ast)))]
    (or (contains? methods sigil)
        (when (or (keyword? sigil) (symbol? sigil))
          (contains? methods (symbol (name sigil)))))))

(defn rewrite-infix-pass [env ast]
  (ana/prewalk
   (ana/only-nodes #{:do}
                   (fn [do-ast]
                     (assoc do-ast :statements
                            (loop [ss (:statements do-ast)
                                   r []]
                              (if (>= (count ss) 3)
                                (let [[?actual ?op ?expected] ss]
                                  (if (sigil? ?op)
                                    (recur (drop 3 ss) (conj r (rewrite-infix env ?expected ?op ?actual)))
                                    (recur (rest ss) (conj r ?actual))))
                                (into r ss))))))
   ast))

(defn autoquote-lvars [env ast]
  ;; Rewrites ?a and _ in t/is assertions
  ;; It could be handled by t/is directly, but we already have a full AST here, 
  ;; no need to serialize it only to reparse it all on the next macroexpand.
  (ana/prewalk
   (ana/only-nodes #{:invoke}
                   (fn [ast]
                     (if-not (and (= `t/is (:form (:fn ast)))
                                  (sigil? (-> ast :args first :fn))                                  )
                       ast
                       (ana/postwalk
                        (ana/only-nodes #{:var :symbol}
                                        (fn [ast]
                                          (if (lvar? ast)
                                            (-> (update ast :form #(list 'quote %))
                                                (update :raw-forms (fnil conj ()) (:form ast)))
                                            ast))) ast)))) ast))

(defn rewrite-is-support [env ast]
  (ana/prewalk
   (ana/only-nodes #{:invoke}
                   (fn [ast]
                     (if-not (= `t/is (:form (:fn ast)))
                       ast
                       (update-in ast [:args 0 :fn :form] replace-sigil*)))) ast))

(defn rewrite-cljs-test [env ast]
  (if-not (ana/cljs? env)
    ast
    (ana/prewalk
     (ana/only-nodes #{:var :symbol}
                     (fn [ast]
                       (if (and (symbol? (:form ast))
                                (= "clojure.test" (namespace (:form ast))))
                         (assoc ast :form (symbol "cljs.test" (name (:form ast))))
                         ast))) ast)))

(defn make-poll-n [n env ast]
  (let [syms (take n (map #(symbol (str "%-" (inc %))) (range n)))
        ast  (let [!syms (atom syms)]
               (ana/postwalk (ana/only-nodes #{:var}
                                             (fn [var-ast]
                                               (if (empty? @!syms)
                                                 (reduced var-ast)
                                                 (condp = (ana/var-sym (:var var-ast))
                                                   'hyperfiddle.rcf/% (let [sym (first @!syms)]
                                                                        (swap! !syms rest)
                                                                        (-> (ana/analyze env sym)
                                                                            (assoc :raw-forms (list (:form var-ast)))))
                                                   var-ast))))
                             ast))
        f    (-> (ana/analyze env `(fn [~@syms]))
                 (ana/resolve-syms-pass)
                 (ana/macroexpand-pass)
                 (update-in [:methods 0 :body :statements] conj ast))]
    (-> (ana/analyze env `(~'RCF__% ~n))
        (update-in [:args] conj f))))


(defn assertion? [ast]
  (and (= :invoke (:op ast))
       (= `t/is (:form (:fn ast)))))

(defn rewrite-async-assert [env ast]
  (if-not (ana/cljs? env)
    ast
    (ana/prewalk
     (ana/only-nodes #{:do}
                     (fn [ast]
                       (assoc ast :statements
                              (loop [r        []
                                     [s & ss] (:statements ast)]
                                (cond
                                  (nil? s)             r
                                  (not (assertion? s)) (recur (conj r s) ss)
                                  :else (let [%-count (count (filter %? (ana/ast-seq s)))]
                                          (if (zero? %-count)
                                            (recur (conj r s) ss)
                                            (let [s' (-> (ana/analyze (ana/empty-env) '(do))
                                                         (update :statements into (cons s ss)))]
                                              (conj r (make-poll-n %-count env s')))))))))) ast)))

(defn rewrite-overload-is [env ast]
  (ana/prewalk
   (ana/only-nodes #{:invoke}
                   (fn [ast]
                     (if (#{`t/is 'cljs.test/is} (:form (:fn ast)))
                       (assoc-in ast [:fn :form] 'hyperfiddle.rcf/is)
                       ast))) ast))

(defn add-done-support [env ast]
  (let [count-is  (count (filter (fn [ast] (and (= :invoke (:op ast)) (= `t/is (:form (:fn ast))))) (ana/ast-seq ast)))]
    (if (zero? count-is)
      ast
      (let [done-sym (gensym "done-")
            body (-> (ana/analyze env `(let [~'RCF__done! (hyperfiddle.rcf/async-notifier ~count-is ~done-sym)]))
                     (ana/resolve-syms-pass)
                     (ana/macroexpand-pass)
                     (update-in [:body :statements] conj ast))]
        (-> (ana/analyze env `(~'hyperfiddle.rcf/async ~done-sym))
            (update-in [:args] conj body))))))

(defn rewrite [env ast]
  (->> ast
       (maybe-add-stars-support env)
       (maybe-add-queue-support env)
       (rewrite-tap-% env)
       (rewrite-infix-pass env)
       (rewrite-async-assert env)
       (rewrite-doc env)
       (rewrite-star env)
       (rewrite-repl env)
       (autoquote-lvars env)
       (rewrite-is-support env)
       (add-done-support env)
       (rewrite-cljs-test env)
       (rewrite-overload-is env)))

(defn tests-cljs* [env exprs]
  (let [env (ana/to-env env)]
    (binding [ana/*emit-options* {:simplify-do true}]
      (->> (cons 'do exprs)
           (ana/analyze env)
           (ana/resolve-syms-pass)
           (ana/macroexpand-pass)
           (rewrite env)
           (ana/emit)))))

(defn tests*
  ([exprs] (tests* nil exprs))
  ([env exprs]
   (if (ana/cljs? env)
     (tests-cljs* env exprs)
     `(binding [*ns* ~*ns*]
        ~(let [env (ana/to-env env)]
           (binding [ana/*emit-options* {:simplify-do true}]
             (->> (cons 'do exprs)
                  (ana/analyze env)
                  (ana/resolve-syms-pass)
                  (ana/macroexpand-pass)
                  (rewrite env)
                  (ana/emit))))))))

;; Nested test support
(defmethod ana/macroexpand-hook `hyperfiddle.rcf/tests [_the-var _&form _&env args] `(do ~@args))
;; clojure.test/is support
(defmethod ana/macroexpand-hook `t/is [_the-var _&form _&env args] `(t/is ~@args))

;; Skip these DSLs, their macroexpansion is not rewritable as clojure. 
(defmethod ana/macroexpand-hook 'clojure.core/case [_ _ _ args] `(case ~@args))
(defmethod ana/macroexpand-hook 'cljs.core/case [_ _ _ args] `(case ~@args))
(defmethod ana/macroexpand-hook 'clojure.core.async/go [_ _ _ args] (reduced `(clojure.core.async/go (do ~@args))))
(defmethod ana/macroexpand-hook 'cljs.core.async/go [_ _ _ args] (reduced `(cljs.core.async/go (do ~@args))))

(defn quoted? [form] (and (seq? form) (= 'quote (first form))))

(defn original-form [form]
  (walk/prewalk (fn [form]
                  (if (ana/has-meta? form)
                    (if-some [form (::ana/macroexpanded (meta form))]
                      (if (quoted? form) (second form) form)
                      form)
                    form))
                form))

;; Tag asserted forms with the original user input (form before macroexpand)
;; so t/is can report it as typed by user, not as rewritten by RCF.
(defmethod ana/-emit :invoke [ast]
  (if (sigil? (:fn ast))
    (list* (ana/emit (:fn ast)) (map (fn [arg] (let [form (ana/emit arg)]
                                                 (if-some [original-form (some-> arg :raw-forms seq last)]
                                                   (with-meta form {::ana/macroexpanded original-form})
                                                   form))) (:args ast)))
    (ana/emit-invoke ast)))

(defn- stacktrace-file-and-line
  [stacktrace]
  (if (seq stacktrace)
    (let [^StackTraceElement s (first stacktrace)
          file-name (.getFileName s)
          file-name (if (= "NO_SOURCE_FILE" file-name) (str (ns-name *ns*)) file-name)]
      {:file file-name :line (.getLineNumber s)})
    {:file nil :line nil}))


(defn do-report* [m]
  (t/report
   (case
    (:type m)
     (:fail :hyperfiddle.rcf/fail) (merge (stacktrace-file-and-line (drop-while
                                                                     #(let [cl-name (.getClassName ^StackTraceElement %)]
                                                                        (or (str/starts-with? cl-name "java.lang.")
                                                                            (str/starts-with? cl-name "clojure.test$")
                                                                            (str/starts-with? cl-name "clojure.core$ex_info")
                                                                            (str/starts-with? cl-name "hyperfiddle.rcf")))
                                                                     (.getStackTrace (Thread/currentThread)))) m)
     (:error :hyperfiddle.rcf/error) (merge (stacktrace-file-and-line (.getStackTrace ^Throwable (:actual m))) m)
     m)))

(defmacro do-report [m]
  (if (:js-globals &env)
    `(cljs.test/do-report ~m)
    `(do-report* ~m)))

(defmethod t/assert-expr 'hyperfiddle.rcf/thrown? [msg form]
  ;; (is (thrown? c expr))
  ;; Asserts that evaluating expr throws an exception of class c.
  ;; Returns the exception thrown.
  (let [[klass body] (rest form)]
    `(try ~body
          (do-report {:type :hyperfiddle.rcf/fail, :message ~msg,
                      :expected '~form, :actual nil})
          (catch ~klass e#
            (do-report {:type :hyperfiddle.rcf/pass, :message ~msg,
                        :expected '~form, :actual e#})
            e#))))

(defn test-var
  "Like `clojure.test/test-var` but return actual result."
  [v]
  (when-let [t (:test (meta v))]
    (binding [t/*testing-vars* (conj t/*testing-vars* v)]
      (do-report {:type :begin-test-var, :var v})
      (t/inc-report-counter :test)
      (try (t)
           (catch Throwable e
             (do-report {:type :error, :message "Uncaught exception, not in assertion."
                         :expected nil, :actual e}))
           (finally (do-report {:type :end-test-var, :var v}))))))


================================================
FILE: src/hyperfiddle/rcf/queue.clj
================================================
(ns hyperfiddle.rcf.queue
  (:require [hyperfiddle.rcf.time :as time])
  (:import (java.util.concurrent LinkedBlockingQueue TimeUnit)))

(defn queue [] (LinkedBlockingQueue.))

(defn get-queue [^LinkedBlockingQueue q]
  (map :value q))

(defn poll!
  ([^LinkedBlockingQueue q, start, timeout, timeout-value]
   (let [now (time/current-time)]
     (if (time/timeout? now start timeout)
       timeout-value
       (:value (.poll q (time/remaining now start timeout) TimeUnit/MILLISECONDS) timeout-value))))
  ([^LinkedBlockingQueue q, start, timeout, timeout-value, callback]
   ;; TODO leverage this arity for non-blocking poll? call callback in (cc/future …)?
   (callback (poll! q start timeout timeout-value))))

(defn poll-n! [q start timeout missing-value n callback]
  (assert (nat-int? n))
  (poll! q start timeout missing-value
         (fn [x]
           (if (= 1 n)
             (callback x)
             (poll-n! q start timeout missing-value (dec n) (partial callback x))))))

(defn offer! [^LinkedBlockingQueue q, val]
  (.offer q {:value val})
  val)


================================================
FILE: src/hyperfiddle/rcf/queue.cljs
================================================
(ns hyperfiddle.rcf.queue
  (:require [hyperfiddle.rcf.time :as time]))

(defprotocol IObservableQueue
  (put! [this val])
  (take! [this])
  (-empty? [this])
  (observe! [this callback])
  (unobserve! [this callback]))

(deftype ObservableArray [^js arr, observers]
  IObservableQueue
  (put! [this val]
    (if-let [observer (.shift observers)]
      (observer val)
      (.push arr val))
    this)
  (take! [_this] (.shift arr))
  (-empty? [_this] (= 0 (.-length arr)))
  (observe! [_this callback] (.push observers callback))
  (unobserve! [_this callback]
    (let [idx (.indexOf observers callback)]
      (when (> idx -1)
        (.splice observers idx 1)))))

(defn queue [] (ObservableArray. #js [] #js []))

(defn get-queue [^js q]
  (seq (.-arr q)))

(defn poll! 
  ([_ _ _ _] (throw (ex-info "Blocking poll not available on a JS runtime." {})))
  ([^js q start timeout missing-value callback]
   (let [now (time/current-time)]
     (if (time/timeout? now start timeout)
       (callback missing-value)
       (let [resolved? (volatile! false)
             resolve   (fn [val] (when-not @resolved?
                                   (vreset! resolved? true)
                                   (callback val)))]
         (if (-empty? q)
           (do (observe! q resolve)
               (js/setTimeout (fn []
                                (unobserve! q resolve)
                                (resolve missing-value))
                              (time/remaining now start timeout)))
           (resolve (take! q))))))))

(defn poll-n! [^js q start timeout missing-value n callback]
  (assert (nat-int? n))
  (poll! q start timeout missing-value
         (fn [x]
           (if (= 1 n)
             (callback x)
             (poll-n! q start timeout missing-value (dec n) (partial callback x))))))

(defn offer! [q v]
  (put! q v)
  v)


================================================
FILE: src/hyperfiddle/rcf/reporters.clj
================================================
(ns hyperfiddle.rcf.reporters
  (:require [clojure.test :as t]
            [clojure.string :as str]))

(defmethod t/report :hyperfiddle.rcf/pass [_m]
  (t/with-test-out
    (t/inc-report-counter :pass)
    (print "✅")
    (flush)))

(defmethod t/report :hyperfiddle.rcf/fail [m]
  (print "❌ ")
  (print (str/triml (with-out-str
                      (binding [t/*test-out* *out*]
                        (t/report (assoc m :type :fail)))))))


================================================
FILE: src/hyperfiddle/rcf/reporters.cljs
================================================
(ns hyperfiddle.rcf.reporters
  (:require [cljs.test :as t]))

(defn testing-vars-str
  "Returns a string representation of the current test.  Renders names
  in *testing-vars* as a list, then the source file and line of
  current assertion."
  [m]
  (let [{:keys [file line column]} m]
    (str file ":" line (when column (str ":" column)))))

;; For js console.
(defmethod t/report [::t/default :hyperfiddle.rcf/pass] [m]
  (t/inc-report-counter! :pass)
  (js/console.log "✅"))

(defmethod t/report [::t/default :hyperfiddle.rcf/fail] [m]
  (t/report (assoc m :type :fail)))

;; Shadow cljs + karma test runner
(defmethod t/report [:shadow.test.karma/karma :hyperfiddle.rcf/pass] [m]
  (t/report (assoc m :type :pass)))

(defmethod t/report [:shadow.test.karma/karma :hyperfiddle.rcf/fail] [m]
  (t/report (assoc m :type :fail)))


================================================
FILE: src/hyperfiddle/rcf/time.cljc
================================================
(ns hyperfiddle.rcf.time)

(defn current-time []
  #?(:clj (System/currentTimeMillis)
     :cljs (js/Date.now)))

(defn timeout? [now start timeout]
  (> now (+ start timeout)))

(defn remaining [now start timeout]
  (- (+ start timeout) now))


================================================
FILE: src/hyperfiddle/rcf/unify.cljc
================================================
;; Adapted from `https://github.com/clojure/core.unify` which was not cljs
;; compatible out of the box.

(ns hyperfiddle.rcf.unify
  (:require [clojure.walk :as walk]
            [clojure.set :as set]))

(defn wildcard? [x] (= '_ x))

(defn &? [form] (and (seqable? form) (= '& (first form))))

(defn lvar? [x] (and (symbol? x) (= \? (first (name x)))))

(defn failed? [env] (contains? env ::fail))

(defn unify-in-env [x y env]
  (if (contains? env x)
    (let [y' (get env x)]
      (if (= y y')
        env
        (if (lvar? y')
          (unify-in-env y' y env)
          (assoc env ::fail {x [y' y]}))))
    (assoc env x y)))

(defn wildcard-in-env [v env]
  (if (contains? env '_)
    (update-in env ['_] conj v)
    (assoc env '_ [v])))

(defn resolve*
  ([env k]
   (resolve* [] env k))
  ([path env k]
   (if (= k (first path))
     ::cycle
     (let [v (get env k)]
       (if (lvar? v)
         (resolve* (conj path k) env v)
         v)))))

(defn ground [env]
  (if (map? env)
    (let [env (reduce-kv (fn [env k _v] (assoc env k (resolve* env k))) env env)]
      (if (contains? env '_)
        (update env '_ (fn [xs] (mapv (fn [x] (if (lvar? x) (get env x) x)) xs)))
        env))
    env))

(declare unify)

(defn unify-set [xs ys env]
  (if (seq (set/intersection xs ys))
    (unify-set (set/difference xs ys) (set/difference ys xs) env)
    (let [env (unify (first xs) (first ys) env)]
      (if (failed? env)
        env
        (unify (rest xs) (rest ys) env)))))

(defn replace-keys [m ks-map]
  (reduce-kv (fn [r k v]
               (-> (dissoc r k)
                   (assoc (if (= k '_)
                            (or (first (set/difference (set (get ks-map '_)) (set (keys r))))
                                '_)
                            (get ks-map k k))
                          v)))
             m m))

(defn unify-map [xs ys env]
  (let [env (unify-set (set (keys xs)) (set (keys ys)) env)
        xs  (replace-keys xs env)
        ys  (replace-keys ys env)]
    (if (= xs ys)
      env
      (reduce (fn [env k]
                (let [env (unify (find xs k) (find ys k) env)]
                  (if (failed? env)
                    (reduced env)
                    env)))
              env (set/union (set (keys xs)) (set (keys ys)))))))

;; Javascript do not have chars. So iterating a string always produce more strings -> StackOverflow.
(defn collection? [x] (and (seqable? x) (not (string? x))))

(defn unify
  ([x y] (unify x y {}))
  ([x y env]
   (let [env (cond
               (failed? env)              env
               (wildcard? x)              (wildcard-in-env y env)
               (wildcard? y)              (wildcard-in-env x env)
               (= x y)                    env
               (&? x)                     (if (seq y)
                                            (unify (second x) (seq y) env)
                                            env)
               (&? y)                     (if (seq x)
                                            (unify (second y) (seq x) env)
                                            env)
               (lvar? x)                  (unify-in-env x y env)
               (lvar? y)                  (unify-in-env y x env)
               (and (set? x) (set y))     (unify-set x y env)
               (and (map? x) (map? y))    (unify-map x y env)
               (every? collection? [x y]) (let [env (unify (first x) (first y) env)]
                                            (if (failed? env)
                                              env
                                              (unify (rest x) (rest y) env)))
               :else                      (assoc env ::fail {::root [x y]}))]
     (if (failed? env)
       (update env ::path (fnil conj ()) x)
       env))))

(defn subst [form env]
  (let [idx      (volatile! -1)
        get-idx! (fn [] (vswap! idx inc))]
    (if (map? env)
      (walk/prewalk (fn [expr] (cond
                                 (lvar? expr)     (get env expr expr)
                                 (wildcard? expr) (get-in env ['_ (get-idx!)] '_)
                                 :else            expr))
                    form)
      form)))

(defn unifier* [x y]
  (let [env (unify x y)]
    (if (failed? env)
      [::fail env]
      (let [env (ground env)]
        [(subst y env) env]))))

(def unifier (comp first unifier*))

(defn explain [env]
  (when-let [fail (::fail env)]
    (str "Failed to unify "
         (if-some [[a b] (::root fail)]
           (str (pr-str a) " and " (pr-str b))
           (let [[lvar [a b]] (first fail)]
             (str (pr-str lvar) " with " (pr-str a) " and " (pr-str b))))
         (when-some [path (seq (->> (::path env) (filter map-entry?) (map key)))]
           (str " in " (into [] path))))))

================================================
FILE: src/hyperfiddle/rcf.cljc
================================================
(ns hyperfiddle.rcf
  (:refer-clojure :exclude [=])
  #?(:cljs (:require-macros [hyperfiddle.rcf :refer [tests deftest async]]
                            [hyperfiddle.rcf.impl :refer [make-queue]]))
  (:require #?(:clj [hyperfiddle.rcf.impl :as impl])
            #?(:clj [clojure.test :as t]
               :cljs [cljs.test :as t])
            #?(:clj [hyperfiddle.rcf.analyzer :as ana])
            #?(:clj [clojure.walk :as walk])
            #?(:clj [clojure.java.io :as io])
            [clojure.string :as str]
            [hyperfiddle.rcf.reporters]
            [hyperfiddle.rcf.queue]
            [hyperfiddle.rcf.time]
            [hyperfiddle.rcf.unify :as u]))

(def = clojure.core/=)

#?(:cljs (goog-define ^boolean ENABLED false))
#?(:cljs (goog-define ^boolean TIMEOUT 400))

;; "Set to true if you want to generate clojure.test compatible tests. This
;; will define testing functions in your namespace using `deftest`. Defaults to
;; `false`.
#?(:clj  (defonce ^:dynamic *enabled* (= "true" (System/getProperty "hyperfiddle.rcf.enabled")))
   :cljs (def ^boolean ^:dynamic *enabled* ENABLED))

(defn enable! [& [v]]
  #?(:clj  (alter-var-root #'*enabled* (constantly (if (some? v) v true)))
     :cljs (set! *enabled* (if (some? v) v true))))

#?(:clj (def ^:dynamic *timeout* (or (System/getProperty "hyperfiddle.rcf.timeout") 1000))
   :cljs (def ^:dynamic *timeout* TIMEOUT))

(defn set-timeout! [ms]
  #?(:clj (alter-var-root #'*timeout* (constantly ms))
     :cljs (set! *timeout* ms)))

#?(:clj  (def ^:dynamic *generate-tests*  (= "true" (System/getProperty "hyperfiddle.rcf.generate-tests"))))

(def ^{:doc "
Function to push value to async queue, e.g. `(tap 42)`. RCF redefines this var in tests context. For REPL
convenience, defaults to println outside of tests context."}
  tap (fn [x] (doto x println)))

(def ^{:doc "Deprecated alias for `tap`." :deprecated true} ! tap)

(comment
  "tap outside of tests macro"
  (is (= (with-out-str (tap 1)) "1\n")))

(def ^{:doc "Queue behaving as a value. Assert `% := _` to pop from it. Async, will time out after `:timeout` option, default to 1000 (ms)."}
  %)

(defn- push-binding [q d] (let [[c b _a] q] [d c b]))

(defn binding-queue []
  (let [!q    (atom [nil nil nil])
        push! (partial swap! !q push-binding)
        peek! #(get (deref !q) %)]
    [push! peek!]))

(defn gen-name [form]
  (let [{:keys [line _column]} (meta form)
        file (str/replace (name (ns-name *ns*)) #"[-\.]" "_")]
    (symbol (str "generated__" file "_" line))))

(defn ns-filename "Given a symbol identifying a namespace, return the corresponding file path"
  [sym]
  (-> (name sym)
    (str/replace #"\." "/")
    (str/replace #"-" "_")))

#?(:clj (defn find-file [relative-path]
          (when-let [res (io/resource relative-path)]
            (try (io/file res)
                 (catch IllegalArgumentException _
                   ;; resource is not a file on the classpath. E.g. jar:// sources are
                   ;; not files. We also don’t want to reload them.
                   nil)))))
#?(:clj
   (defn resolve-file
     "Resolve a source file from namespace symbol.
      Precedence:
      - cljc,
      - cljs if we are compiling clojurescript,
      - clj otherwise."
     [env ns-sym]
     (let [file-name (ns-filename ns-sym)
           cljc      (find-file (str file-name ".cljc"))
           clj       (find-file (str file-name ".clj"))
           cljs      (find-file (str file-name ".cljs"))]
       (if (and cljc (.exists cljc))
         cljc
         (if (:js-globals env)
           (when (and cljs (.exists cljs))
             cljs)
           (when (and clj (.exists clj))
             clj))))))
#?(:clj
   (defn is-ns-in-current-project? [env ns-sym]
     (let [current-dir (System/getProperty "user.dir")]
       (when-some [file (resolve-file env ns-sym)]
         (str/starts-with? (.getPath file) current-dir)))))

(defmacro tests [& body]
  (let [body `(~@body nil) ; return nil like comment, unlike do
        name (gen-name &form)
        ns (if (:js-globals &env)
             (:name (:ns &env))
             (:ns &env (ns-name *ns*)))]
    (cond
      (and *generate-tests* (is-ns-in-current-project? &env ns)) `(deftest ~name ~@body)
      *enabled*         (if (:js-globals &env)
                          `(do (defn ~name [] ~(impl/tests* &env body))
                               (when *enabled* (cljs.test/run-block (cljs.test/test-var-block* (var ~name) ~name))))
                          (impl/tests* &env body))
      :else             nil)))

(defmacro deftest
  "When *load-tests* is false, deftest is ignored."
  [name & body]
  (if (:js-globals &env)
    `(do (cljs.test/deftest ~name ~(impl/tests* &env body))
         (when *enabled* (~name)))
    (when t/*load-tests*
      `(do (def ~(vary-meta name assoc :test `(fn [] ~(impl/tests* &env body)))
             (fn [] (impl/test-var (var ~name))))
           (when *enabled* (~name))))))

(defn done [])

(defmacro async [done & body]
  (if (ana/cljs? &env)
    `(cljs.test/async ~done ~@body)
    `(let [~done (constantly nil)]
       ~@body)))

(defn async-notifier [n done]
  (let [!seen (atom 0)]
    (fn []
      (swap! !seen inc)
      (when (= @!seen n)
        (done)))))

(defmacro make-queue [& args] (apply impl/make-queue args))

(defmacro is
  ([form] `(is ~form nil))
  ([form msg] `(try-expr ~msg ~form)))

(defmacro try-expr
  [msg form]
  (let [cljs? (ana/cljs? &env)
        {:keys [file line end-line column end-column]} (meta form)]
    `(try ~(t/assert-expr msg form)
          (catch ~(if cljs? :default 'Throwable) t#
            (do-report {:type :error, :message ~msg,
                        :file ~file :line ~line :end-line ~end-line :column ~column :end-column ~end-column
                        :expected '~form, :actual t#}))
          (finally
            (~'RCF__done!)))))

;; Same as default `=` behavior, but returns the first argument instead of a boolean.

(defmacro do-report [m]
  (if (:js-globals &env)
    `(cljs.test/do-report ~m)
    `(impl/do-report ~m)))

#?(:clj (defn- assert-= [menv msg form]
          (let [[_= & args] form
                form        (cons '= (map impl/original-form args))]
            `(let [values# (list ~@args)
                   result# (apply = values#)]
               (if result#
                 (do-report {:type     :hyperfiddle.rcf/pass
                             :message  ~msg,
                             :expected '~form
                             :actual   (cons '= values#)})
                 (do-report {:type     :hyperfiddle.rcf/fail
                             :message  ~msg,
                             :expected '~form
                             :actual   (list '~'not (cons '~'= values#))}))
               (first values#)))))

#?(:clj (defmethod t/assert-expr 'hyperfiddle.rcf/= [msg form] (assert-= nil msg form)))

#?(:clj
   (defn- assert-unify [menv msg form]
     (let [[_= & args] form
           form        (cons := (map impl/original-form args))]
       `(let [lhs#           (identity ~(first args))
              rhs#           (identity ~(second args))
              [result# env#] (u/unifier* lhs# rhs#)]
          (if-not (u/failed? env#)
            (do (do-report {:type     :hyperfiddle.rcf/pass
                            :message  ~msg,
                            :expected '~form
                            :actual   result#})
                result#)
            (do (do-report {:type     :hyperfiddle.rcf/fail
                            :message  ~msg,
                            :expected '~form
                            :actual   (u/explain env#)})
                lhs#))))))

#?(:clj (defmethod t/assert-expr :hyperfiddle.rcf/= [msg form] (assert-unify nil msg form)))

(defmacro with
  "Resource cleanup helper, based on missionary's dependency-free Task protocol, see https://github.com/leonoel/task"
  [dispose-fn & body]
  `(let [dispose# ~dispose-fn]
     (try (do ~@body) (finally (dispose#)))))


================================================
FILE: test/hyperfiddle/rcf/analyzer_test.clj
================================================
(ns hyperfiddle.rcf.analyzer-test
  (:require [hyperfiddle.rcf.analyzer :as ana]
            [clojure.test :as t :refer [deftest are testing]]))

(defn roundtrip
  ([form] (roundtrip (ana/empty-env) form))
  ([env form] (ana/emit (ana/analyze env form))))

(deftest roundtrips
  (testing "hf.analyzer should parse and unparse clojure code transparently."
    (are [x y] (= y x)
      (roundtrip 1)   '1
      (roundtrip '1)  1
      (roundtrip ''1) ''1
      (roundtrip :ns/a) :ns/a
      (roundtrip '(inc 1)) '(inc 1)
      (roundtrip [1]) [1]
      (roundtrip #{1}) #{1}
      (roundtrip ()) ()
      (roundtrip '(1)) '(1)
      (roundtrip {:a 1}) {:a 1}
      (roundtrip '(do 1)) '(do 1)
      (roundtrip '(do (def a 1) a)) '(do (def a 1) a)
      (roundtrip '((def a identity) 1)) '((def a identity) 1)

      (-> (roundtrip '(def ^:macro a 1))
          (second) (meta))
      {:macro true}

      (roundtrip '(if true a b)) '(if true a b)
      (roundtrip '(if true a)) '(if true a nil)
      (roundtrip '(let [a 1
                        b 2] a)) '(let [a 1, b 2] a)

      (roundtrip '(loop* [a 1] (recur a))) '(loop* [a 1] (do (recur a)))

      (roundtrip '(fn* [])) '(fn* ([] (do)))
      (roundtrip '(fn* f [])) '(fn* f ([] (do)))
      (roundtrip '(fn* [a] a)) '(fn* ([a] (do a)))

      (roundtrip '(fn* ([a] a) ([a b] a))) '(fn* ([a] (do a)) ([a b] (do a)))

      (roundtrip '(fn [a] a)) '(fn [a] a)
      (roundtrip '(fn f [])) '(fn f [])

      (roundtrip '(letfn* [foo (fn* foo ([a] (inc a)))] 1)) '(letfn* [foo (fn* foo ([a] (do (inc a))))] (do 1))

      (roundtrip '(try 1)) '(try (do 1))
      (roundtrip '(try 1 (catch Err e# 2))) '(try (do 1) (catch Err e# (do 2)))

      (roundtrip '(try 1 (catch Err e# 2)
                       (catch Err2 e# 3)
                       (finally 4))) 
      '(try (do 1) (catch Err e# (do 2)) (catch Err2 e# (do 3)) (finally 4))
 
      )))

(comment
  (ana/analyze (ana/empty-env) '(do 1))
  )

================================================
FILE: test/hyperfiddle/rcf/cljs_test.cljs
================================================
(ns hyperfiddle.rcf.cljs-test
  (:require [clojure.core.async :refer [chan >! go go-loop <! timeout close!]]
            [hyperfiddle.rcf :as rcf :refer [tests tap %]]
            [missionary.core :as m]))

(tests
   "equality"
   (inc 1) := 2)

(tests
   "nested tests"
   (tests
    "are sometimes convenient"
    1 := 1))

(tests
   "Macros are supported"
   (with-out-str
     "print is captured"
     (print "hello")
     1 := 1)
   := "hello")

(tests
   "let body"
   (let [a 1, b 2] [a b] := [1 2]))

(tests
   "def in same form (Gilardi scenario)"
   (def a 1)
   a := 1

   "inline def"
   ((def b identity) 1) := 1)


(tests
   ((fn [a] a := 1) 1) := 1)

(tests
   "REPL bindings work"
   (inc 1) := 2
   (dec *1) := 1

   (tests 1 2 3 *3 := 1, *2 := 2, *1 := 3))

(tests
   "wildcards"
   {:a :b, :b [2 :b]} := {:a _, _ [2 _]})

(tests
   "unification"
   {:a :b, :b [2 :b]} := {:a ?b, ?b [2 ?b]})

(tests
   "unification on reference types"
   (def x (atom nil))
   {:a x, :b x} := {:a ?x, :b ?x})

(tests
 (rcf/set-timeout! 100)
 "async tests"
 (tests
  (defn set-timeout [f ms] (js/setTimeout ms f))
  (rcf/tap 1) (set-timeout 10 (fn []
                              (rcf/tap 2) (set-timeout 200 (fn []
                                                           (rcf/tap 3)))))
  % := 1
  % := 2
  % := ::rcf/timeout))

(tests
 "core.async"
 (def c (chan))
 (go-loop [x (<! c)]
   (when x
     (<! (timeout 10))
     (tap x)
     (recur (<! c))))
 (go (>! c :hello) (>! c :world))
 % := :hello
 % := :world
 (close! c))

(tests
 "missionary"
 (def !x (atom 0))
 (def dispose ((m/reactor (m/stream! (m/ap (tap (inc (m/?< (m/watch !x)))))))
               (fn [_] #_(prn ::done)) #(prn ::crash %)))
 % := 1
 (swap! !x inc)
 (swap! !x inc)
 % := 2
 % := 3
 (dispose))


================================================
FILE: test/hyperfiddle/rcf/example_test.clj
================================================
(ns hyperfiddle.rcf.example-test
  (:require [clojure.core.async :refer [chan >! go go-loop <! timeout close!]]
            [hyperfiddle.rcf :as rcf :refer [tests tap % with]]
            [missionary.core :as m]))

(rcf/enable!)

(tests
  "equality"
  (inc 1) := 2)

(tests
 "nested tests"
 (tests
  "are sometimes convenient"
  1 := 1))

(tests
 "Macros are supported"
 (with-out-str 
   "print is captured"
   (print "hello")
   1 := 1)
 := "hello")

(tests
 "let body"
 (let [a 1, b 2] [a b] := [1 2]))

(tests
 "def in same form (Gilardi scenario)"
 (def a 1)
 a := 1

 "inline def"
 ((def b identity) 1) := 1)

(tests
 ((fn [a] a := 1) 1) := 1)

(tests
 "REPL bindings work"
 (inc 1) := 2
 (dec *1) := 1
 
 (tests 1 2 3 *3 := 1, *2 := 2, *1 := 3))

(tests
 "wildcards"
 {:a :b, :b [2 :b]} := {:a _, _ [2 _]})

(tests
 "unification"
 {:a :b, :b [2 :b]} := {:a ?b, ?b [2 ?b]})

(tests
 "unification on reference types"
 (def x (atom nil))
 {:a x, :b x} := {:a ?x, :b ?x})

(tests
 "Async queue"
 (tap 1) % := 1
 (future (Thread/sleep 300) (tap 2))
 % := 2
 )

(tests
 (tests (future (Thread/sleep 100) (tap :a) :b) %)
 := :a)

(tests
  ":throws"
  (throw (ex-info "" {})) :throws clojure.lang.ExceptionInfo
  (assert false "boom") :throws java.lang.AssertionError)

(tests
 "missionary"
 (def !x (atom 0))
 (def dispose ((m/reactor (m/stream! (m/ap (tap (inc (m/?< (m/watch !x)))))))
               (fn [_] (prn ::done)) #(prn ::crash %)))
 % := 1
 (swap! !x inc)
 (swap! !x inc)
 % := 2
 % := 3
 (dispose))


(tests
 "core.async"
 (def c (chan))
 (go-loop [x (<! c)]
   (when x
     (<! (timeout 10))
     (tap x)
     (recur (<! c))))
 (go  (>! c :hello) (>! c :world))
 % := :hello
 % := :world
 (close! c))

(tests
  (def task (fn [success! failure!] (success! 1) (fn cancel [] (tap ::dispose))))
  (with (task tap tap)
    % := 1)
  % := ::dispose)


================================================
FILE: test/hyperfiddle/rcf/unify_test.cljc
================================================
(ns hyperfiddle.rcf.unify-test
  (:require [hyperfiddle.rcf :refer [deftest tests]]
            [hyperfiddle.rcf.unify :as u])
  #?(:cljs (:require-macros [hyperfiddle.rcf :refer [deftest tests]])))

(tests
 '(do 1) := '(do 1))

(tests
 (u/unify 1 _) := '{_ [1]})

(tests
 "Basics"
 (u/unify 1 1) := {}
 (u/failed? (u/unify 1 2)) := true
 (u/unify 1 _) := '{_ [1]}
 (u/unify 1 ?a) := '{?a 1}
 (u/unify [1 2 3] '[_ _ ?a]) := '{_ [1 2], ?a 3}

 "Both sides"
 (u/unify '[?a 1] '[?a ?a]) := '{?a 1}

 "Cycle"
 (u/unify '[?a ?b] '[?b ?a]) := '{?a ?b, ?b ?a}

 "Transitive"
 (u/unify '[?a ?b 1] '[?b _ ?a]) := '{?a ?b, _ [?b], ?b 1})

(tests
 "Ground"
 "Cycle"
 (u/ground (u/unify '[?a ?b] '[?b ?a])) := '{?a ::u/cycle, ?b ::u/cycle}

 "Transitive"
 (u/ground (u/unify '[?a ?b 1] '[?b _ ?a])) := '{?a 1, _ [1], ?b 1})


(tests
 "Composite"
 (u/unify {:first ?first  :last ?last    :genre :giallo}
          {:first "Dario"  :last "Argento" :genre :giallo}) := '{?first "Dario", ?last "Argento"}
 (u/unify '[(?a * ?x | 2) + (?b * ?x) + ?c]
          '[?z + (4 * 5) + 3])                              := '{?c 3, ?x 5, ?b 4, ?z (?a * ?x | 2)}
 (u/unify '[(?a * ?x | 2) + (?b * ?x) + ?c]
          '[(?a * 5 | 2) + (4 * 5) + 3])                    := '{?c 3, ?b 4, ?x 5}
 (u/unify '[(?a * 5 | 2) + (4 * 5) + 3]
          '[?z + (4 * 5) + 3])                              := '{?z (?a * 5 | 2)}
 (u/unify '[?a ?a] [1 2])                                   := {'?a 1, ::u/fail {'?a [1 2]} ::u/path _}

 "Spread"
 (u/unify [1 2 3] '[?x & ?more])                            := '{?more (2 3), ?x 1}
 (u/unify [1 2 3] '[_ _ _ & ?more])                         := '{_ [1 2 3]}
 (u/unify [1 2 3 4 5] '[_ _ _ & ?more])                     := '{_ [1 2 3] ?more (4 5)}
 (u/unify [1 2 3 4 5] '[_ ?b _ & ?more])                    := '{_ [1 3], ?more (4 5), ?b 2}
 (u/unify [:foo 1 2] '[?head & _])                          := '{?head :foo, _ [(1 2)]})

(tests
 (u/unifier '[?first "Argento"]
            '["Dario" ?last])             := ["Dario" "Argento"]

 (u/unifier '[(?a * ?x | 2) + (?b * ?x) + ?c]
            '[?z + (4 * 5) + 3])          := '[(?a * 5 | 2) + (4 * 5) + 3]

 (u/unifier '{?a 1 :b :a} '{?b 1 :b ?a})  := {:a 1, :b :a}
 (u/unifier {:a 1, :b 2} '{?a ?b, ?b ?a}) := ::u/fail
 (u/unifier '[?a ?b] '[?b ?a])            := [::u/cycle ::u/cycle])


================================================
FILE: test/hyperfiddle/rcf_test.cljc
================================================
(ns hyperfiddle.rcf-test
  (:require [clojure.test :as t :refer [deftest is testing]]
            [hyperfiddle.rcf :as rcf :refer [tests]]
            [matcher-combinators.test]
            #_[hyperfiddle.rcf.analyzer :as ana])
  #?(:clj (:import [clojure.lang ExceptionInfo]))
  #?(:cljs (:require-macros [hyperfiddle.rcf-test])))

(deftest tap-outside-tests
  (is (= (with-out-str (rcf/tap 1)) "1\n"))
  (is (= (rcf/tap 1) 1)))

(defmacro my-def [x]
  `(def ~(vary-meta x assoc 
                    :form3 (inc 1)         ; evaluated now
                    :form4 (quote (inc 1)) ; interpreted as if evaluated after code emission  
                    :form5 (quote (quote (inc 1))) ; escaping interpretation
                    )))

#?(:clj
   (tests
     "Custom var meta on def symbol are interpreted as if they were evaluated after emission."
     ;; CLJ only, no vars in cljs.
     (my-def ^{:form1 (quote (inc 1))
               :form2 (inc 1)}        ; read and evaluated as usual
       x)
     (:form1 (meta #'x)) := '(inc 1)
     (:form2 (meta #'x)) := 2
     (:form3 (meta #'x)) := 2
     (:form4 (meta #'x)) := 2
     (:form5 (meta #'x)) := '(inc 1)))


(tests
  "Support for variadic fn" ; issue #65
  (let [f (fn ([a]        [a])          ; also check multi arity
            ([a b]      [a b])
            ([a b & cs] [a b cs]))
        g (fn [& args] args)
        h #(vector %&)]
    (f 1)       := [1]
    (f 1 2)     := [1 2]
    (f 1 2 3 4) := [1 2 '(3 4)]
    (g 1 2)     := [1 2]
    (h 1 2)     := ['(1 2)]
    ))

(tests
  "Inline letfn support"
  (letfn [(descent  [x] (cond (pos? x) (dec x)
                              (neg? x) (inc x)
                              :else    x))
          (is-even? [x] (if (zero? x) true  (is-odd?  (descent x))))
          (is-odd?  [x] (if (zero? x) false (is-even? (descent x))))]
    [(is-even? 0) (is-even? 1) (is-even? 2) (is-even? -2)] := [true false true true]
    [(is-odd?  0) (is-odd?  2) (is-odd?  3) (is-odd? -3)]  := [false false true true]))

(tests
  "! still works"
  (rcf/! 5)
  rcf/% := 5)

(tests
  ":throws works in clj(s)"
  ;; inlining `thrower` leads to "unreachable code" warning
  (let [thrower #(throw (ex-info "boom" {}))]
    (thrower) :throws ExceptionInfo))

; fixme
#_(tests "matcher-combinators match? works infix" 
  {:a {:b 1}} match? {:a {:b int?}})

(tests "`with` discards in presence of exceptions, too"
  (try
    (rcf/with #(rcf/tap :ran)
      (throw (ex-info "" {})))
    (catch ExceptionInfo _))
  rcf/% := :ran
  )

;; For an unknown reason, `macroexpand-1` acts as identity when runnning
;; tests without a repl.

;; (defn disable-ci [f]
;;   (let [gen rcf/*generate-tests*
;;         enabled rcf/*enabled*]
;;     (alter-var-root #'rcf/*generate-tests* (constantly (not gen)))
;;     (alter-var-root #'rcf/*enabled* (constantly (not enabled)))
;;     (f)
;;     (alter-var-root #'rcf/*generate-tests* (constantly gen))
;;     (alter-var-root #'rcf/*enabled* (constantly enabled))))
;; 
;; (t/use-fixtures :once disable-ci)
;; 
;; (defn tests' [& [body]]
;;   (apply #'tests nil nil body))
;; 
;; (deftest block-behavior
;;   (testing "`tests` behaves like"
;;     (testing "`cc/comment` when RCF is disabled."
;;       (binding [rcf/*enabled* false]
;;         (is (nil? (tests' '(1 := 1))))))
;;     (testing "`cc/do` when RCF is enabled."
;;       (is (= '(do) (tests')))
;;       (is (= '1 (tests' '(1))))
;;       (is (= '(do 1 2) (tests' '(1 2)))))))
;; 
;; (deftest nesting
;;   (testing "Nested `tests` flattens"
;;     (is (= '(do) (tests' '((tests)))))
;;     (is (= '(do 1 2) (tests' '(1 (tests 2)))))))
;; 
;; #_(deftest documentation-behavior
;;   (testing "`tests` behaves like `t/testing` when a string litteral is followed by an expression."
;;     (is (= `(t/testing "a" 1)
;;            (macroexpand-1 '(tests "a" 1))))))
;; 
;; #_(deftest basic-assertion
;;   (testing "Assertion sigils are listed by `t/assert-expr`."
;;     (is (contains? (methods t/assert-expr) :default)))
;;   (testing "Infix sigils desugares to `is`"
;;     (is (= `(t/is (:default 1 1))
;;            (macroexpand-1 '(tests 1 :default 1)))))
;;   (testing "RCF rewrites some sigils to avoid conflicts" ;; extensible by a multimethod
;;     (is (= '(clojure.test/is (:hyperfiddle.rcf/= '_ '_))
;;            (macroexpand-1 '(tests _ := _))))))
;; 
;; (comment
;;   (alter-var-root #'rcf/*generate-tests* (constantly false))
;;   (rcf/enable! true))

;; (deftest repl-behavior
;;   (testing "`tests` behaves like a REPL"
;;     (testing "where *1, *2, *3 referers to previous results."
;;       (is (= '(i/repl 1 *1 *2 *3) (tests* 1 *1 *2 *3))))
;;     (testing "where a `def` form is immediately available after being evaluated."
;;       (is (= '(i/repl (def a 1) a) (tests* (def a 1) a)))
;;       (is (= '(i/repl ((def a identity) 1))
;;              (tests* ((def a identity) 1)))))))
;; 
;; (deftest async-behavior
;;   (testing "`tests` can probe for values using `!`"
;;     (is (= '(let [[! %] (i/queue)]
;;               (! 1))
;;            (tests* (! 1)))))
;;   (testing "`tests` can inspect probed values using `%`"
;;     (is (= '(let [[! %] (i/queue)]
;;               (! 1)
;;               (%))
;;            (tests* (! 1) %)))
;;     (is (= '(let [[! %] (i/queue)]
;;               (! 1)
;;               (% (fn [result] (i/is (:= result 1)))))
;;            (tests* (! 1) % := 1)))))
;; 
;; (deftest repl-macro
;;   (testing "RCF repl behavior"
;;     (testing "supports *1 *2 *3"
;;       (is (= '(let [[push! peek] (i/binding-queue)]
;;                 (push! 1)
;;                 (push! (peek 0))
;;                 (push! (peek 1))
;;                 (peek 2))
;;              '(i/repl 1 *1 *2 *3))))
;;     (testing "handles the Gilardi scenario"
;;       (is (= '(do (def a 1) a)
;;              (tests* (def a 1) a)))
;;       (is (= '(do (def a identity) (a 1))
;;              (tests* ((def a identity) 1)))))))
;; 
;; (macroexpand-1 `(tests* 1 := 1))

;; (macroexpand-1 `(tests* (do (rcf/! 1) rcf/% := 1)))

Download .txt
gitextract_sxhcwvab/

├── .cljfmt.edn
├── .github/
│   └── workflows/
│       ├── tests_babashka.yml
│       ├── tests_browser.yml
│       ├── tests_clj.yml
│       └── tests_node.yml
├── .gitignore
├── License.md
├── README.md
├── bb/
│   └── runner.clj
├── bb.edn
├── build.clj
├── build.md
├── ci/
│   ├── run_tests_all.sh
│   ├── run_tests_browser.sh
│   ├── run_tests_jvm.sh
│   └── run_tests_node.sh
├── deps.edn
├── example/
│   ├── dev_entrypoint.cljc
│   └── example.cljc
├── out/
│   └── index.html
├── package.json
├── pom.xml
├── run_dev_repl.sh
├── shadow-cljs.edn
├── src/
│   └── hyperfiddle/
│       ├── rcf/
│       │   ├── analyzer.clj
│       │   ├── impl.clj
│       │   ├── queue.clj
│       │   ├── queue.cljs
│       │   ├── reporters.clj
│       │   ├── reporters.cljs
│       │   ├── time.cljc
│       │   └── unify.cljc
│       └── rcf.cljc
└── test/
    └── hyperfiddle/
        ├── rcf/
        │   ├── analyzer_test.clj
        │   ├── cljs_test.cljs
        │   ├── example_test.clj
        │   └── unify_test.cljc
        └── rcf_test.cljc
Condensed preview — 38 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (101K chars).
[
  {
    "path": ".cljfmt.edn",
    "chars": 270,
    "preview": "{:indents ^:replace {#\"^.\" [[:inner 0]]}\n :test-code [(sui/ui-grid {:columns 2}\n               (sui/ui-grid-row {}\n     "
  },
  {
    "path": ".github/workflows/tests_babashka.yml",
    "chars": 453,
    "preview": "name: Babashka\non: [push, pull_request]\njobs:\n  babashka:\n    runs-on: [ubuntu-latest]\n    steps:\n      - name: Checkout"
  },
  {
    "path": ".github/workflows/tests_browser.yml",
    "chars": 618,
    "preview": "name: Browser\non: [push, pull_request]\njobs:\n  browser:\n    runs-on: [ubuntu-latest]\n    steps:\n      - name: Checkout\n "
  },
  {
    "path": ".github/workflows/tests_clj.yml",
    "chars": 436,
    "preview": "name: JVM\non: [push, pull_request]\njobs:\n  clojure:\n    runs-on: [ubuntu-latest]\n    steps:\n      - name: Checkout\n     "
  },
  {
    "path": ".github/workflows/tests_node.yml",
    "chars": 612,
    "preview": "name: NodeJS\non: [push, pull_request]\njobs:\n  node:\n    runs-on: [ubuntu-latest]\n    steps:\n      - name: Checkout\n     "
  },
  {
    "path": ".gitignore",
    "chars": 171,
    "preview": ".cpcache/\n.nrepl-port\n.cljs_node_repl/\n.shadow-cljs/\n.rebel_readline_history\n.idea\nout/js\n*.js\n*.js.map\n*.DS_Store\nnode_"
  },
  {
    "path": "License.md",
    "chars": 1058,
    "preview": "Copyright 2021 Hyperfiddle, Inc.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this s"
  },
  {
    "path": "README.md",
    "chars": 8200,
    "preview": "# RCF – REPL-first async test macro for Clojure/Script\n\nRCF turns your Rich Comment Forms into tests (in the same file a"
  },
  {
    "path": "bb/runner.clj",
    "chars": 736,
    "preview": "(ns runner\n  (:require\n   [clojure.test :as test]\n   [hyperfiddle.rcf :as rcf]))\n\n(defn run-tests [_]\n  (alter-var-root "
  },
  {
    "path": "bb.edn",
    "chars": 195,
    "preview": "{:paths [\"bb\" \"src\"]\n :tasks {test:bb {:extra-paths [\"test\"]\n                  :extra-deps {nubank/matcher-combinators {"
  },
  {
    "path": "build.clj",
    "chars": 391,
    "preview": "(ns build\n  (:require [org.corfield.build :as bb]))\n\n(def defaults\n  {:src-pom \"pom.xml\"\n   :lib     'com.hyperfiddle/rc"
  },
  {
    "path": "build.md",
    "chars": 639,
    "preview": "Build\n```shell\nHYPERFIDDLE_RCF_DATE=`date -u '+%Y%m%d-%H%M%S'`\ngit tag v$HYPERFIDDLE_RCF_DATE\nclojure -T:build clean\nclo"
  },
  {
    "path": "ci/run_tests_all.sh",
    "chars": 74,
    "preview": "#!/bin/bash\n\n./run_tests_jvm.sh\n./run_tests_node.sh\n./run_tests_browser.sh"
  },
  {
    "path": "ci/run_tests_browser.sh",
    "chars": 151,
    "preview": "#!/bin/sh\n\necho \"Running Browser tests\"\n./node_modules/.bin/shadow-cljs -A:cljs:test release :browser-test\n./node_module"
  },
  {
    "path": "ci/run_tests_jvm.sh",
    "chars": 137,
    "preview": "#!/bin/sh\n\necho \"Running JVM tests\"\nclojure -X:test :dirs \"[\\\"test\\\" \\\"example\\\"]\" :patterns \"[\\\"example.*\\\" \\\"hyperfidd"
  },
  {
    "path": "ci/run_tests_node.sh",
    "chars": 120,
    "preview": "#!/bin/sh\n\necho \"Running NodeJS tests\"\n./node_modules/.bin/shadow-cljs -A:cljs:test release :test\nnode out/node-tests.js"
  },
  {
    "path": "deps.edn",
    "chars": 1195,
    "preview": "{:paths   [\"src\"]\n :deps    {org.clojure/clojure {:mvn/version \"1.10.3\"}}\n :aliases {:cljs {:extra-deps {org.clojure/clo"
  },
  {
    "path": "example/dev_entrypoint.cljc",
    "chars": 396,
    "preview": "(ns dev-entrypoint\n  (:require [example]\n            [hyperfiddle.rcf :as rcf :refer [tests]]))\n\n; enable tests after ap"
  },
  {
    "path": "example/example.cljc",
    "chars": 2635,
    "preview": "(ns example\n  (:require [clojure.core.async :refer [chan >! go go-loop <! timeout close!]]\n            [clojure.test :as"
  },
  {
    "path": "out/index.html",
    "chars": 188,
    "preview": "<!DOCTYPE html>\n<html>\n    <head></head>\n    <body>\n        <h1>Check your javascript console.</h1>\n        <script type"
  },
  {
    "path": "package.json",
    "chars": 246,
    "preview": "{\n    \"name\": \"rcf\",\n    \"dependencies\": {},\n    \"devDependencies\": {\n        \"karma\": \"6.3.17\",\n        \"karma-chrome-l"
  },
  {
    "path": "pom.xml",
    "chars": 1302,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2"
  },
  {
    "path": "run_dev_repl.sh",
    "chars": 108,
    "preview": "#!/bin/bash\n\nnpx shadow-cljs -d nrepl/nrepl:0.9.0-beta4 -d cider/cider-nrepl:0.27.2 -A:dev:test:cljs server\n"
  },
  {
    "path": "shadow-cljs.edn",
    "chars": 579,
    "preview": "{:deps     {:aliases [:cljs]}\n :dev-http {8000 \"out\"}\n :builds   {:browser {:target     :browser\n                      :"
  },
  {
    "path": "src/hyperfiddle/rcf/analyzer.clj",
    "chars": 25865,
    "preview": ";; A simpler tools.analyzer for the restricted use case of RCF\n;; Adapted from  https://github.com/clojure/tools.analyze"
  },
  {
    "path": "src/hyperfiddle/rcf/impl.clj",
    "chars": 17500,
    "preview": "(ns hyperfiddle.rcf.impl\n  (:require [clojure.string :as str]\n            [clojure.test :as t]\n            [clojure.walk"
  },
  {
    "path": "src/hyperfiddle/rcf/queue.clj",
    "chars": 1065,
    "preview": "(ns hyperfiddle.rcf.queue\n  (:require [hyperfiddle.rcf.time :as time])\n  (:import (java.util.concurrent LinkedBlockingQu"
  },
  {
    "path": "src/hyperfiddle/rcf/queue.cljs",
    "chars": 1851,
    "preview": "(ns hyperfiddle.rcf.queue\n  (:require [hyperfiddle.rcf.time :as time]))\n\n(defprotocol IObservableQueue\n  (put! [this val"
  },
  {
    "path": "src/hyperfiddle/rcf/reporters.clj",
    "chars": 442,
    "preview": "(ns hyperfiddle.rcf.reporters\n  (:require [clojure.test :as t]\n            [clojure.string :as str]))\n\n(defmethod t/repo"
  },
  {
    "path": "src/hyperfiddle/rcf/reporters.cljs",
    "chars": 832,
    "preview": "(ns hyperfiddle.rcf.reporters\n  (:require [cljs.test :as t]))\n\n(defn testing-vars-str\n  \"Returns a string representation"
  },
  {
    "path": "src/hyperfiddle/rcf/time.cljc",
    "chars": 244,
    "preview": "(ns hyperfiddle.rcf.time)\n\n(defn current-time []\n  #?(:clj (System/currentTimeMillis)\n     :cljs (js/Date.now)))\n\n(defn "
  },
  {
    "path": "src/hyperfiddle/rcf/unify.cljc",
    "chars": 4780,
    "preview": ";; Adapted from `https://github.com/clojure/core.unify` which was not cljs\n;; compatible out of the box.\n\n(ns hyperfiddl"
  },
  {
    "path": "src/hyperfiddle/rcf.cljc",
    "chars": 8009,
    "preview": "(ns hyperfiddle.rcf\n  (:refer-clojure :exclude [=])\n  #?(:cljs (:require-macros [hyperfiddle.rcf :refer [tests deftest a"
  },
  {
    "path": "test/hyperfiddle/rcf/analyzer_test.clj",
    "chars": 1955,
    "preview": "(ns hyperfiddle.rcf.analyzer-test\n  (:require [hyperfiddle.rcf.analyzer :as ana]\n            [clojure.test :as t :refer "
  },
  {
    "path": "test/hyperfiddle/rcf/cljs_test.cljs",
    "chars": 1780,
    "preview": "(ns hyperfiddle.rcf.cljs-test\n  (:require [clojure.core.async :refer [chan >! go go-loop <! timeout close!]]\n           "
  },
  {
    "path": "test/hyperfiddle/rcf/example_test.clj",
    "chars": 1857,
    "preview": "(ns hyperfiddle.rcf.example-test\n  (:require [clojure.core.async :refer [chan >! go go-loop <! timeout close!]]\n        "
  },
  {
    "path": "test/hyperfiddle/rcf/unify_test.cljc",
    "chars": 2337,
    "preview": "(ns hyperfiddle.rcf.unify-test\n  (:require [hyperfiddle.rcf :refer [deftest tests]]\n            [hyperfiddle.rcf.unify :"
  },
  {
    "path": "test/hyperfiddle/rcf_test.cljc",
    "chars": 6041,
    "preview": "(ns hyperfiddle.rcf-test\n  (:require [clojure.test :as t :refer [deftest is testing]]\n            [hyperfiddle.rcf :as r"
  }
]

About this extraction

This page contains the full source code of the hyperfiddle/rcf GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 38 files (93.2 KB), approximately 28.3k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!