| author | Alan Dipert
<alan@dipert.org> 2026-11-11 14:29:22 UTC |
| committer | Alan Dipert
<alan@dipert.org> 2026-11-11 14:29:22 UTC |
| parent | 786d596043105d75b97684573be22a166b36159d |
| .forgejo/workflows/check.yml | +1 | -3 |
| README.md | +2 | -5 |
| bin/check | +0 | -1 |
| build.clj | +2 | -9 |
| deps.edn | +2 | -5 |
| doc/language.md | +2 | -2 |
| src-java/com/tailrecursion/blacktide/runtime/KeywordSite.java | +0 | -40 |
| src/com/tailrecursion/blacktide/compiler/jvm.clj | +2 | -2 |
| src/com/tailrecursion/blacktide/compiler/languages.clj | +14 | -17 |
| src/com/tailrecursion/blacktide/compiler/passes/boundaries.clj | +0 | -7 |
| src/com/tailrecursion/blacktide/compiler/passes/box_origins.clj | +0 | -56 |
| src/com/tailrecursion/blacktide/compiler/passes/collections.clj | +1 | -1 |
| src/com/tailrecursion/blacktide/compiler/passes/graph.clj | +5 | -5 |
| src/com/tailrecursion/blacktide/compiler/passes/host_emit.clj | +1 | -2 |
| src/com/tailrecursion/blacktide/compiler/passes/joins.clj | +1 | -1 |
| src/com/tailrecursion/blacktide/compiler/passes/native.clj | +2 | -6 |
| src/com/tailrecursion/blacktide/compiler/passes/source.clj | +3 | -19 |
| src/com/tailrecursion/blacktide/compiler/passes/storage.clj | +17 | -17 |
| src/com/tailrecursion/blacktide/compiler/passes/structure.clj | +15 | -46 |
| src/com/tailrecursion/blacktide/compiler/passes/value_facts.clj | +2 | -2 |
| src/com/tailrecursion/blacktide/runtime.clj | +0 | -7 |
| test/com/tailrecursion/assignment_model.clj | +0 | -76 |
| test/com/tailrecursion/blacktide/aot_test.clj | +14 | -5 |
| test/com/tailrecursion/blacktide/assignment_model.clj | +53 | -0 |
| test/com/tailrecursion/blacktide/assignment_test.clj | +3 | -3 |
| test/com/tailrecursion/blacktide/backend_test.clj | +11 | -15 |
| test/com/tailrecursion/blacktide/generated_test.clj | +5 | -1 |
| test/com/tailrecursion/blacktide/interop_values_test.clj | +50 | -0 |
| test/com/tailrecursion/blacktide/jvm_fixture.clj | +1 | -15 |
| test/com/tailrecursion/blacktide/lookup_test.clj | +40 | -70 |
| test/com/tailrecursion/blacktide/native_sites_test.clj | +0 | -101 |
| test/com/tailrecursion/blacktide/numeric_test.clj | +10 | -3 |
| test/com/tailrecursion/blacktide/package_test.clj | +4 | -4 |
| test/com/tailrecursion/blacktide/source_loops_test.clj | +1 | -10 |
| test/com/tailrecursion/nanopass/annotations.clj | +1 | -4 |
| test/com/tailrecursion/nanopass/assignment_test.clj | +0 | -64 |
| test/com/tailrecursion/nanopass/check.clj | +2 | -32 |
| test/com/tailrecursion/nanopass/clients/assignment.clj | +0 | -46 |
| test/com/tailrecursion/test_runner.clj | +1 | -2 |
diff --git a/.forgejo/workflows/check.yml b/.forgejo/workflows/check.yml index 3bfd93b..b1796b4 100644 --- a/.forgejo/workflows/check.yml +++ b/.forgejo/workflows/check.yml @@ -41,9 +41,7 @@ jobs: bash linux-install-1.12.4.1618.sh --prefix "$RUNNER_TEMP/clojure" echo "$RUNNER_TEMP/clojure/bin" >> "$GITHUB_PATH" - name: Test - run: | - clojure -T:build compile-java - clojure -M:test + run: clojure -M:test - name: Check pass types if: matrix.java == '25' || matrix.java == '27' run: clojure -M:check diff --git a/README.md b/README.md index 608dd15..cfe7895 100644 --- a/README.md +++ b/README.md @@ -52,18 +52,15 @@ To use that neighboring checkout, add this dependency to your project's `deps.ed {:deps {com.tailrecursion/blacktide {:local/root "../blacktide"}}} ``` -Run `clojure -X:deps prep` in the client project to compile the bundled JVM keyword-cache helper. Library JARs already include the helper. - -Clojure is the only runtime dependency. Typed Clojure checks compiler passes during development. +Clojure is the only dependency. Generated code needs no Blacktide runtime helpers. Typed Clojure checks compiler passes during development. ## Develop | Command | Purpose | | --- | --- | -| `clojure -T:build compile-java` | Compile the Java helper after checkout or changes to `src-java`. | | `clojure -M:test` | Run semantic, generative, differential and AOT tests. | | `clojure -M:check` | Check language contracts and production pass bodies. | -| `bin/check` | Run all three steps above; accepts CLI `-S` options. | +| `bin/check` | Run tests and type checks; accepts CLI `-S` options. | | `clojure -T:build jar` | Build `target/blacktide-0.1.0-SNAPSHOT.jar`. | Select a JDK with `JAVA_CMD=/path/to/jdk/bin/java bin/check`. Development targets JDK 25/27; 17/21 are compatibility targets. Forgejo CI tests all four JDKs, checks pass types on 25/27, and builds the JAR. diff --git a/bin/check b/bin/check index f6bf105..5ff27b4 100755 --- a/bin/check +++ b/bin/check @@ -1,6 +1,5 @@ #!/usr/bin/env bash set -euo pipefail cd "$(dirname "$0")/.." -clojure "$@" -T:build compile-java clojure "$@" -M:test clojure "$@" -M:check diff --git a/build.clj b/build.clj index 53402b1..b0f0f75 100644 --- a/build.clj +++ b/build.clj @@ -1,20 +1,13 @@ (ns build - "Build the JVM helper and a source library JAR; the compiler stays in Clojure." + "Package the Clojure source library and its resources." (:require [clojure.tools.build.api :as b] [clojure.java.io :as io])) -(defn compile-java [_] - (b/delete {:path "target/classes"}) - (b/javac {:src-dirs ["src-java"] :class-dir "target/classes" - :basis (b/create-basis {:project "deps.edn"}) - :javac-opts ["--release" "17" "-Xlint:all" "-Werror"]})) - (defn jar [{:keys [version] :or {version "0.1.0-SNAPSHOT"}}] - (compile-java nil) (let [lib 'com.tailrecursion/blacktide class-dir "target/jar-classes" basis (b/create-basis {:project "deps.edn"})] (b/delete {:path class-dir}) - (b/copy-dir {:src-dirs ["src" "resources" "target/classes"] :target-dir class-dir}) + (b/copy-dir {:src-dirs ["src" "resources"] :target-dir class-dir}) ;; Editor state may be nested beneath namespace directories. (doseq [file (file-seq (io/file class-dir)) :when (.startsWith (.getName file) ".")] diff --git a/deps.edn b/deps.edn index bb54034..e2e184c 100644 --- a/deps.edn +++ b/deps.edn @@ -1,7 +1,5 @@ -{:paths ["src" "resources" "target/classes"] +{:paths ["src" "resources"] :deps {org.clojure/clojure {:mvn/version "1.12.6"}} - :deps/prep-lib {:alias :build :fn compile-java - :ensure "target/classes/com/tailrecursion/blacktide/runtime/KeywordSite.class"} :aliases {:build {:deps {io.github.clojure/tools.build {:mvn/version "0.10.14"}} :ns-default build} @@ -10,7 +8,6 @@ :jvm-opts ["-Xmx768m"] :main-opts ["-m" "com.tailrecursion.test-runner"]} :check {:extra-paths ["test" "test-resources"] - :extra-deps {org.typedclojure/typed.clj.checker {:mvn/version "1.3.0"} - org.clojure/test.check {:mvn/version "1.1.3"}} + :extra-deps {org.typedclojure/typed.clj.checker {:mvn/version "1.3.0"}} :jvm-opts ["-Xmx768m"] :main-opts ["-m" "com.tailrecursion.check-runner"]}}} diff --git a/doc/language.md b/doc/language.md index 91084a0..c2a3331 100644 --- a/doc/language.md +++ b/doc/language.md @@ -141,7 +141,7 @@ Unreachable code still receives syntax and JVM compilation checks. Very large re Mutable locals retain reference identity through assignment and immutable snapshots. Ordinary primitive locals retain Clojure's semantics: separate Object uses may box separately. -Literal pooling is unspecified. Embedded constants, macro-supplied quoted data, regex literals and inlined `^:const` values may share differently from ordinary Clojure. Bind a reference once when its identity matters. Runtime construction sites and cached lookup sites are not duplicated. +Literal pooling is unspecified. Embedded constants, macro-supplied quoted data, regex literals and inlined `^:const` values may share differently from ordinary Clojure. The concrete classes generated for method values and functional-interface adapters are also unspecified. Bind a reference once when its identity matters; copying that reference preserves identity. ```clojure (let [token (Object.)] @@ -150,7 +150,7 @@ Literal pooling is unspecified. Embedded constants, macro-supplied quoted data, (identical? token y))) ; => true ``` -Literal keyword calls retain Clojure's cached `IKeywordLookup` behavior, including reentrant target evaluation. An assigned target uses a bundled cache helper; that cache never stores mutable region locals. +Literal keyword calls use Clojure's cached lookup; indirect keyword calls retain ordinary invocation. Arguments evaluate exactly once before lookup, including when they assign locals or recursively invoke the containing function. Cache layout, installation counts and cache-miss probes are unspecified. Custom `IKeywordLookup` implementations must return equivalent results regardless of cache history; effects or exceptions that depend on that history are outside this contract. ### Errors diff --git a/src-java/com/tailrecursion/blacktide/runtime/KeywordSite.java b/src-java/com/tailrecursion/blacktide/runtime/KeywordSite.java deleted file mode 100644 index 189e645..0000000 --- a/src-java/com/tailrecursion/blacktide/runtime/KeywordSite.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.tailrecursion.blacktide.runtime; - -import clojure.lang.AFn; -import clojure.lang.ILookupThunk; -import clojure.lang.IPersistentVector; -import clojure.lang.IType; -import clojure.lang.Keyword; -import clojure.lang.KeywordLookupSite; -import clojure.lang.PersistentVector; -import clojure.lang.Symbol; - -/** KeywordInvokeExpr's cache, snapshotted before evaluating an effectful target. */ -public final class KeywordSite extends AFn implements IType { - public final Keyword keyword; - private final KeywordLookupSite site; - private ILookupThunk thunk; - - public KeywordSite(Keyword keyword) { - this.keyword = keyword; - this.site = new KeywordLookupSite(keyword); - this.thunk = site; - } - - // Clojure emits IType constants through this basis and the public constructor, - // giving each emitted call site a cache in both REPL and AOT compilation. - public static IPersistentVector getBasis() { - return PersistentVector.create((Object) Symbol.intern("keyword")); - } - - public ILookupThunk snapshot() { return thunk; } - - @Override public Object invoke(Object saved, Object target) { - ILookupThunk before = (ILookupThunk) saved; - Object value = before.get(target); - if (value != before) return value; - ILookupThunk next = site.fault(target); - thunk = next; - return next.get(target); - } -} diff --git a/src/com/tailrecursion/blacktide/compiler/jvm.clj b/src/com/tailrecursion/blacktide/compiler/jvm.clj index ef77400..79e2c7d 100644 --- a/src/com/tailrecursion/blacktide/compiler/jvm.clj +++ b/src/com/tailrecursion/blacktide/compiler/jvm.clj @@ -160,7 +160,7 @@ (np/defpass host-type {:from [l/JVM Operation] :to [l/JVM Type] :args [l/JVM-Types]} [types operation] - :keyword-site com.tailrecursion.blacktide.runtime.KeywordSite + :keyword :unknown :protocol Object :direct (:return operation) :instanceof Boolean/TYPE @@ -189,7 +189,7 @@ [types operation] (let [unchanged {:operation operation :conversions (mapv (fn [_] nil) types)}] (case (:op operation) - (:adapt :instanceof :method-value :static-field :protocol :keyword-site) unchanged + (:adapt :instanceof :method-value :static-field :protocol :keyword) unchanged :direct (let [params (:parameters operation) fixed (if (:variadic? operation) (pop params) params)] {:operation operation diff --git a/src/com/tailrecursion/blacktide/compiler/languages.clj b/src/com/tailrecursion/blacktide/compiler/languages.clj index 9e46358..ab660d7 100644 --- a/src/com/tailrecursion/blacktide/compiler/languages.clj +++ b/src/com/tailrecursion/blacktide/compiler/languages.clj @@ -19,7 +19,7 @@ Mode (t/U ':method ':member ':field) Protocol (t/HMap :mandatory {:class (t/Option java.lang.Class) :member (t/Option t/Sym) :parameters (t/Vec java.lang.Class)} :complete? true) - Operation (t/U (t/HMap :mandatory {:op ':keyword-site :key t/Kw} :complete? true) + Operation (t/U (t/HMap :mandatory {:op ':keyword :key t/Kw} :complete? true) (t/HMap :mandatory {:op ':protocol :method Protocol} :complete? true) (t/HMap :mandatory {:op ':direct :class java.lang.Class :parameters (t/Vec java.lang.Class) :variadic? t/Bool :return java.lang.Class} :complete? true) @@ -185,8 +185,7 @@ ':zero-guard ':select ':compare ':narrow-int ':narrow-char ':narrow-checked ':widen-char) :parameters (t/Vec java.lang.Class) :result java.lang.Class :origins (t/Vec t/Int)} :complete? true) - Graph (t/HMap :mandatory {:entry Label :blocks (t/Map Label Block)} - :optional {:slots (t/Vec t/Sym) :live (t/Map Label (t/Set t/Sym))} :complete? true)}}) + Blocks (t/Map Label Block)}}) ;; Numeric value facts and control plans are independent backend analyses. (np/deflanguage Analysis @@ -210,29 +209,27 @@ ;; Structured recovery produces Joined expressions. Representation selection ;; produces Represented; lexical and numeric lowering follow it. ;; Returns carry one public result or the zero/many values of a lexical join. -(np/deflanguage Structured +(np/deflanguage Joined {Expr (t/U (t/HMap :mandatory {:op ':bind :binding l/Numeric-Operation :body Expr} :complete? true) (t/HMap :mandatory {:op ':branch :test l/Machine-Atom :yes Expr :no Expr} :complete? true) (t/HMap :mandatory {:op ':switch :test l/Machine-Atom :arms (t/Vec Expr)} :complete? true) (t/HMap :mandatory {:op ':loop :slots (t/Vec t/Sym) :body Expr} :complete? true) (t/HMap :mandatory {:op ':recur :slots (t/Vec t/Sym)} :complete? true) (t/HMap :mandatory {:op ':return :values (t/Vec l/Machine-Atom)} :complete? true) - (t/HMap :mandatory {:op ':throw :value l/Machine-Atom} :complete? true)) - Build (t/U (t/HMap :mandatory {:op ':built :budget t/Int :blocks (t/Vec l/Machine-Label) :expr Expr} :complete? true) + (t/HMap :mandatory {:op ':throw :value l/Machine-Atom} :complete? true) + (t/HMap :mandatory {:op ':join :targets (t/Vec t/Sym) :value Expr :body Expr} :complete? true)) + Build (t/U (t/HMap :mandatory {:op ':built :budget t/Int :expr Expr} :complete? true) (t/HMap :mandatory {:op ':exhausted :reason Fallback} :complete? true)) - Arms (t/U (t/HMap :mandatory {:op ':built :budget t/Int :blocks (t/Vec l/Machine-Label) :arms (t/Vec Expr)} :complete? true) + Arms (t/U (t/HMap :mandatory {:op ':built :budget t/Int :arms (t/Vec Expr)} :complete? true) (t/HMap :mandatory {:op ':exhausted :reason Fallback} :complete? true)) - Fallback (t/U ':cycles ':validation ':budget ':native-sites) + Fallback (t/U ':cycles ':validation ':budget) Decision (t/U (t/HMap :mandatory {:op ':structured :body Expr} :complete? true) - (t/HMap :mandatory {:op ':fallback :reason Fallback} :complete? true))}) - -(np/derive-language Joined Structured - {:add {Expr [(t/HMap :mandatory {:op ':join :targets (t/Vec t/Sym) :value Expr :body Expr} :complete? true)]} - :replace {Context (t/HMap :mandatory {:machine l/Numeric-Program :header (t/Option l/Machine-Label) - :slots (t/Vec t/Sym) :joins l/Planned-Joins - :loops l/Planned-LoopBodies :exits l/Planned-Joins - :active (t/Set l/Machine-Label) - :stop (t/Option (t/HVec [l/Machine-Label (t/Vec t/Sym)]))} :complete? true)}}) + (t/HMap :mandatory {:op ':fallback :reason Fallback} :complete? true)) + Context (t/HMap :mandatory {:machine l/Numeric-Program :header (t/Option l/Machine-Label) + :slots (t/Vec t/Sym) :joins l/Planned-Joins + :loops l/Planned-LoopBodies :exits l/Planned-Joins + :active (t/Set l/Machine-Label) + :stop (t/Option (t/HVec [l/Machine-Label (t/Vec t/Sym)]))} :complete? true)}) (np/derive-language Represented Joined {:replace {Types (t/Map t/Sym java.lang.Class) diff --git a/src/com/tailrecursion/blacktide/compiler/passes/boundaries.clj b/src/com/tailrecursion/blacktide/compiler/passes/boundaries.clj index 8257cf4..51c8f44 100644 --- a/src/com/tailrecursion/blacktide/compiler/passes/boundaries.clj +++ b/src/com/tailrecursion/blacktide/compiler/passes/boundaries.clj @@ -57,13 +57,6 @@ [expr] (let [node (:node expr)] (cond - ;; KeywordInvokeExpr snapshots its cache before evaluating the target. - ;; Retain the whole call when the target needs no DSL control or writes; - ;; saving the target first would change reentrant lookup behavior. - (and (= :invoke (:op node)) (= 1 (count (:args node))) - (= :literal (:op (:node (:function node)))) - (keyword? (:form (:site (:function node)))) (not (effects expr))) - (assoc expr :node {:op :native :value (native expr)}) (and (= :loop (:op node)) (not (effects expr))) (assoc expr :node {:op :native :value (native expr)}) (= :loop (:op node)) (let [expr (loops/lower expr)] (assoc expr :node (classify-node (:node expr)))) diff --git a/src/com/tailrecursion/blacktide/compiler/passes/box_origins.clj b/src/com/tailrecursion/blacktide/compiler/passes/box_origins.clj deleted file mode 100644 index c7a8233..0000000 --- a/src/com/tailrecursion/blacktide/compiler/passes/box_origins.clj +++ /dev/null @@ -1,56 +0,0 @@ -(ns ^:typed.clojure com.tailrecursion.blacktide.compiler.passes.box-origins - "Find values that can be reboxed at return, following copies. Primitive - computations create boxes; compiled small Long literals use the guaranteed - cache. Storage analysis must still exclude all other identity observations." - (:require [clojure.set :as set] - [com.tailrecursion.nanopass :as np] - [com.tailrecursion.blacktide.compiler.languages :as l] - [com.tailrecursion.blacktide.compiler.passes.value-facts :as facts] - [com.tailrecursion.blacktide.compiler.passes.numeric :as numeric] - [typed.clojure :as-alias t])) - -(np/defpass copy-source {:from [l/Numeric Rhs] :to (t/Option t/Sym) :body true} - [rhs] - (when (and (= :value (:op rhs)) (= :local (:op (:value rhs)))) (:id (:value rhs)))) - -(np/defpass origin-values - {:from [l/Numeric Rhs] :to (t/Vec l/Machine-Atom) :args [l/Analysis-Types] :body true} - [types rhs] - (case (:op rhs) - :value [(:value rhs)] - :numeric (if-let [plan (numeric/plan types rhs)] - (numeric/origin-values plan rhs) []) - [])) - -(np/defpass reboxable? {:from [l/Numeric Rhs] :to t/Bool :args [l/Analysis-Types] :body true} - [types rhs] - (let [origins (origin-values types rhs)] - (if (seq origins) - ;; Selected references (min/max and NaN division) inherit all possible - ;; origins, just like copies. The fixed point checks local dependencies. - (every? (fn [^{::t/- l/Machine-Atom} value] - (or (= :local (:op value)) - (and (= :literal (:op value)) (instance? Long (:value value)) - (<= -128 (:value value) 127)))) origins) - (case (:op rhs) - :numeric (or (when-let [plan (numeric/plan types rhs)] (empty? (:origins plan))) - (class? (facts/rhs-type {} (numeric/source-call rhs)))) - (:host :convert) (class? (facts/rhs-type {} rhs)) - false)))) - -(np/defpass reboxable-values {:from [l/Analysis Program] :to (t/Set t/Sym) :body true} - [analysis] - (let [definitions (facts/definitions (:reachable analysis) (:machine analysis)) - initial (reduce-kv - (fn [^{::t/- (t/Set t/Sym)} out ^{::t/- t/Sym} id ^{::t/- (t/Vec l/Numeric-Rhs)} values] - (if (some #(not (reboxable? (:types analysis) %)) values) (conj out id) out)) - #{} definitions) - denied ((fn ^{::t/- [(t/Set t/Sym) :-> (t/Set t/Sym)]} solve [denied] - (let [next (reduce-kv - (fn [^{::t/- (t/Set t/Sym)} out ^{::t/- t/Sym} id ^{::t/- (t/Vec l/Numeric-Rhs)} values] - (if (some (fn [rhs] - (some #(and (= :local (:op %)) (contains? denied (:id %))) - (origin-values (:types analysis) rhs))) values) (conj out id) out)) - denied definitions)] - (if (= next denied) denied (recur next)))) initial)] - (set/difference (set (keys definitions)) denied))) diff --git a/src/com/tailrecursion/blacktide/compiler/passes/collections.clj b/src/com/tailrecursion/blacktide/compiler/passes/collections.clj index e21d2d7..5428756 100644 --- a/src/com/tailrecursion/blacktide/compiler/passes/collections.clj +++ b/src/com/tailrecursion/blacktide/compiler/passes/collections.clj @@ -24,7 +24,7 @@ (if (<= (count items) 6) (list* 'clojure.lang.Tuple/create items) ;; An Object expression prevents constant folding without unboxing the - ;; first literal, which would lose its original numeric width or identity. + ;; first item or changing its numeric width. (list 'do (assoc items 0 (host/box-form (nth items 0))))) (list (if (= :set kind) 'clojure.lang.RT/set (if unique? 'clojure.lang.RT/mapUniqueKeys 'clojure.lang.RT/map)) diff --git a/src/com/tailrecursion/blacktide/compiler/passes/graph.clj b/src/com/tailrecursion/blacktide/compiler/passes/graph.clj index c64e4f5..6058c78 100644 --- a/src/com/tailrecursion/blacktide/compiler/passes/graph.clj +++ b/src/com/tailrecursion/blacktide/compiler/passes/graph.clj @@ -55,9 +55,9 @@ {:reads (set/union reads (set/difference (term-reads (:term block)) defs)) :defs defs :successors (successors (:term block))})) -(np/defpass collect-facts {:from [l/Numeric Graph] :to [l/CFG Facts] :body true} - [program] - (update-vals (:blocks program) block-facts)) +(np/defpass collect-facts {:from [l/Numeric Blocks] :to [l/CFG Facts] :body true} + [blocks] + (update-vals blocks block-facts)) (np/defpass fact-at {:from [l/CFG Label] :to [l/CFG BlockFacts] :args [l/CFG-Facts] :body true} [facts id] @@ -129,12 +129,12 @@ (np/defpass allocate {:from [l/CFG Program] :to [l/Machine Program] :body true} [program] - (let [live (liveness (collect-facts program))] + (let [live (liveness (collect-facts (:blocks program)))] (assoc program :live live :slots (vec (sort (reduce set/union #{} (vals live))))))) (np/defpass validate {:from [l/CFG Program] :to [l/CFG Program] :body true} [program] - (let [facts (collect-facts program) + (let [facts (collect-facts (:blocks program)) program (validate-edges facts program) seen (reachable facts (:entry program)) reachable-facts (select-keys facts seen)] diff --git a/src/com/tailrecursion/blacktide/compiler/passes/host_emit.clj b/src/com/tailrecursion/blacktide/compiler/passes/host_emit.clj index 178c380..c276041 100644 --- a/src/com/tailrecursion/blacktide/compiler/passes/host_emit.clj +++ b/src/com/tailrecursion/blacktide/compiler/passes/host_emit.clj @@ -3,7 +3,6 @@ explicitly; Object prevents boxed storage from inventing a more specific type." (:require [com.tailrecursion.nanopass :as np] [com.tailrecursion.blacktide.compiler.languages :as l] - [com.tailrecursion.blacktide.runtime] [typed.clojure :as-alias t])) (np/defpass global-form {:from [l/Machine Name] :to [l/Machine Code] :body true} @@ -51,7 +50,7 @@ (np/defpass host-form {:from [l/JVM Operation] :to [l/Machine Code] :args [(t/Vec l/Machine-Code)]} [args operation] - :keyword-site (list 'com.tailrecursion.blacktide.runtime/keyword-site (:key operation)) + :keyword (list (:key operation) (first args)) :protocol (let [locals (mapv (fn [_] (gensym "protocol__")) args) receiver (first locals) function (second locals) arguments (subvec locals 2) diff --git a/src/com/tailrecursion/blacktide/compiler/passes/joins.clj b/src/com/tailrecursion/blacktide/compiler/passes/joins.clj index 9f9c7d0..5d5eab2 100644 --- a/src/com/tailrecursion/blacktide/compiler/passes/joins.clj +++ b/src/com/tailrecursion/blacktide/compiler/passes/joins.clj @@ -91,7 +91,7 @@ (np/defpass plan {:from [l/Numeric Program] :to [l/Planned Program] :body true} [machine] - (let [facts (graph/collect-facts machine) + (let [facts (graph/collect-facts (:blocks machine)) known (postdominators facts) bodies (loops/analyze (:entry machine) facts)] {:machine machine diff --git a/src/com/tailrecursion/blacktide/compiler/passes/native.clj b/src/com/tailrecursion/blacktide/compiler/passes/native.clj index b23496f..38989a7 100644 --- a/src/com/tailrecursion/blacktide/compiler/passes/native.clj +++ b/src/com/tailrecursion/blacktide/compiler/passes/native.clj @@ -42,12 +42,8 @@ (with-meta id (when-let [hint (:hint (:jvm expr))] {:tag hint})) (host/typed-form (:jvm expr) (host/box-form id)))) :host (hinted-form (:hint (:jvm expr)) (host/host-form (mapv #(form bound %) (:args node)) (:operation node))) - :invoke (let [function (:function node) source (:form (:site function)) - ;; Quote changes KeywordExpr into ConstantExpr, bypassing the - ;; source call site's IKeywordLookup cache. Preserve its syntax. - callee (if (and (keyword? source) (= :literal (:op (:node function)))) - source (form bound function))] - (hinted-form (:hint (:jvm expr)) (list* callee (mapv #(form bound %) (:args node))))) + :invoke (hinted-form (:hint (:jvm expr)) + (list* (form bound (:function node)) (mapv #(form bound %) (:args node)))) :if (list 'if (form bound (:test node)) (form bound (:then node)) (form bound (:else node))) :case (cases/arms-form (mapv #(form bound %) (:arms node)) (form bound (:selector node))) :dispatch (cases/selector-form (form bound (:value node)) (:spec node)) diff --git a/src/com/tailrecursion/blacktide/compiler/passes/source.clj b/src/com/tailrecursion/blacktide/compiler/passes/source.clj index 874510a..2105890 100644 --- a/src/com/tailrecursion/blacktide/compiler/passes/source.clj +++ b/src/com/tailrecursion/blacktide/compiler/passes/source.clj @@ -9,7 +9,6 @@ [com.tailrecursion.blacktide.compiler.passes.source-jvm :as facts] [com.tailrecursion.blacktide.compiler.passes.loop-facts :as loops] [com.tailrecursion.blacktide.compiler.passes.source-case :as cases] - [com.tailrecursion.blacktide.compiler.passes.effects :refer [effects]] [com.tailrecursion.blacktide.compiler.passes.source-host :as host])) (np/defpass fresh @@ -107,21 +106,6 @@ [supply (expression ctx seed {:op :let :bindings [{:local local :init receiver}] :body (expression ctx seed {:op :host :operation {:op :protocol :method method} :args args})})])) -(np/defpass keyword-call - {:from [l/Bound Expr] :to (t/HVec [l/Bound-Supply l/Bound-Expr]) - :args [l/Bound-Supply l/Bound-Context l/JVM-Seed t/Kw] :body true} - [supply ctx seed key target] - (let [plain {:hint nil :var-meta {}} [supply id] (fresh "keyword-site" supply) - site (expression ctx plain {:op :host :operation {:op :keyword-site :key key} :args []}) - local {:id id :name id :mutable? false :external? false :jvm (:jvm site)} - read (expression ctx plain {:op :local :local local}) - snapshot (expression ctx plain {:op :host :operation {:op :instance :class com.tailrecursion.blacktide.runtime.KeywordSite - :member 'snapshot :mode :method :tags []} :args [read]})] - ;; The first argument snapshots the cache before the target runs. - ;; An IFn call retains KeywordInvokeExpr's unknown result type. - [supply (expression ctx seed {:op :let :bindings [{:local local :init site}] - :body (expression ctx seed {:op :invoke :function read :args [snapshot target]})})])) - (np/defpass parse-marker {:from [l/Bound Forms] :to [l/Bound Expr] :args [l/Bound-Context (t/U ':label ':goto)] :body true} [ctx kind forms] @@ -277,9 +261,9 @@ (cond (and method (= :global (:op fnode))) (protocol-call supply ctx {:hint hint :var-meta {}} method (:name fnode) args) - (and (= :literal (:op fnode)) (keyword? key) (= 1 arity) - (effects (nth args 0))) - (keyword-call supply ctx {:hint hint :var-meta {}} key (nth args 0)) + (and (= :literal (:op fnode)) (keyword? key) (= 1 arity)) + [supply (expression ctx {:hint hint :var-meta {}} + {:op :host :operation {:op :keyword :key key} :args args})] :else [supply (expression ctx {:hint hint :var-meta {}} node)])))) (parse-host diff --git a/src/com/tailrecursion/blacktide/compiler/passes/storage.clj b/src/com/tailrecursion/blacktide/compiler/passes/storage.clj index 78d58c3..50cf93f 100644 --- a/src/com/tailrecursion/blacktide/compiler/passes/storage.clj +++ b/src/com/tailrecursion/blacktide/compiler/passes/storage.clj @@ -1,8 +1,8 @@ (ns ^:typed.clojure com.tailrecursion.blacktide.compiler.passes.storage "Select primitive storage only when every producer and consumer agrees. Object observations and reference-valued native reads retain identity; loop slots - specialize only to long/double. Observed copies share boxing requirements; - reboxable results may be boxed at return when no other observer requires a box." + specialize only to long/double. Copies and numeric plans propagate boxing + requirements; the public return boundary boxes the selected representation." (:require [clojure.set :as set] [com.tailrecursion.nanopass :as np] [com.tailrecursion.blacktide.compiler.languages :as l] @@ -10,7 +10,6 @@ [com.tailrecursion.blacktide.compiler.passes.native :as native] [com.tailrecursion.blacktide.compiler.passes.numeric :as numeric] [com.tailrecursion.blacktide.compiler.passes.value-facts :as facts] - [com.tailrecursion.blacktide.compiler.passes.box-origins :as origins] [typed.clojure :as-alias t])) (np/defpass loop-slots {:from [l/Joined Expr] :to (t/Set t/Sym)} @@ -33,14 +32,17 @@ [types observed operation] (let [rhs (:value operation)] (case (:op rhs) - :value (if-not (contains? observed (:target operation)) #{} - (if (contains? types (:target operation)) - (if-let [source (origins/copy-source rhs)] - (if (contains? types source) #{} #{(:target operation)}) #{}) - (graph/atom-reads (:value rhs)))) + :value (let [value (:value rhs) target (:target operation)] + (cond + (not (contains? observed target)) #{} + (not (contains? types target)) (graph/atom-reads value) + (and (= :local (:op value)) (not (contains? types (:id value)))) #{target} + :else #{})) :numeric (if-let [plan (numeric/plan types rhs)] - (into (if (contains? types (:target operation)) #{} - (into #{} (mapcat graph/atom-reads) (numeric/origin-values plan rhs))) + (into (if (= (get types (:target operation)) (:result plan)) #{} + ;; Boxing an operand can change the selected overload's + ;; result representation. Its target must follow suit. + (into #{(:target operation)} (mapcat graph/atom-reads) (numeric/origin-values plan rhs))) (mapcat (fn [arg ^Class parameter] (if (.isPrimitive parameter) #{} (graph/atom-reads (:value arg)))) (:args rhs) (:parameters plan))) @@ -69,11 +71,10 @@ (if (= observed next) observed (recur next)))) initial))) (np/defpass term-boxes - {:from [l/Machine Term] :to (t/Set t/Sym) :args [l/Represented-Types (t/Set t/Sym)] :body true} - [types reboxable term] + {:from [l/Machine Term] :to (t/Set t/Sym) :args [l/Represented-Types] :body true} + [types term] (case (:op term) - :return (if (and (= :local (:op (:value term))) (contains? reboxable (:id (:value term)))) - #{} (graph/term-reads term)) + :return #{} :branch (if (= Boolean/TYPE (facts/atom-type types (:test term))) #{} (graph/term-reads term)) :switch (if (= Long/TYPE (facts/atom-type types (:test term))) #{} (graph/term-reads term)) (graph/term-reads term))) @@ -82,8 +83,7 @@ {:from [l/Joined Expr] :to [l/Represented Program] :args [l/Analysis-Program] :body true} [analysis expr] ;; Emission preserves the public Object boundary at every exit. - (let [reboxable (origins/reboxable-values analysis) - slots (loop-slots expr) + (let [slots (loop-slots expr) initial (reduce-kv (fn [^{::t/- l/Represented-Types} out ^{::t/- t/Sym} id ^{::t/- l/Analysis-Type} type] (if (and (class? type) (or (not (contains? slots id)) (#{Long/TYPE Double/TYPE} type))) @@ -93,7 +93,7 @@ blocks (vals (:blocks machine)) types ((fn ^{::t/- [l/Represented-Types :-> l/Represented-Types]} solve [types] (let [boxed (into (into #{} (mapcat #(operation-boxes types observed %)) (mapcat :ops blocks)) - (mapcat #(term-boxes types reboxable (:term %))) blocks) + (mapcat #(term-boxes types (:term %))) blocks) next (apply dissoc types boxed)] (if (= types next) types (recur next)))) initial)] {:body expr :types types})) diff --git a/src/com/tailrecursion/blacktide/compiler/passes/structure.clj b/src/com/tailrecursion/blacktide/compiler/passes/structure.clj index ff5a566..c95c706 100644 --- a/src/com/tailrecursion/blacktide/compiler/passes/structure.clj +++ b/src/com/tailrecursion/blacktide/compiler/passes/structure.clj @@ -1,7 +1,7 @@ (ns ^:typed.clojure com.tailrecursion.blacktide.compiler.passes.structure "Recover lexical control from reducible loop regions. Nested loops yield their live results; tail loops keep their continuation without a packet. - Bounded duplication preserves native sites and dead-code validation." + Recovery bounds code duplication and preserves dead-code validation." (:require [com.tailrecursion.nanopass :as np] [com.tailrecursion.blacktide.compiler.languages :as l] [com.tailrecursion.blacktide.compiler.passes.graph :as graph] @@ -45,11 +45,11 @@ {:from [l/Machine Term] :to [l/Joined Arms] :args [l/Joined-Context t/Int] :body true} [ctx budget term] (if (= :switch (:op term)) - ((fn ^{::t/- [t/Int t/Int (t/Vec l/Joined-Expr) (t/Vec l/Machine-Label) :-> l/Joined-Arms]} build [index budget out blocks] - (if (= index (count (:targets term))) {:op :built :budget budget :arms out :blocks blocks} + ((fn ^{::t/- [t/Int t/Int (t/Vec l/Joined-Expr) :-> l/Joined-Arms]} build [index budget out] + (if (= index (count (:targets term))) {:op :built :budget budget :arms out} (let [result (edge ctx budget (nth (:targets term) index))] (if (= :built (:op result)) - (recur (inc index) (:budget result) (conj out (:expr result)) (into blocks (:blocks result))) result)))) 0 budget [] []) + (recur (inc index) (:budget result) (conj out (:expr result))) result)))) 0 budget []) {:op :exhausted :reason :budget})) (terminator @@ -57,16 +57,16 @@ [ctx budget term] :jump (edge ctx budget (:target term)) :return (if (:stop ctx) {:op :exhausted :reason :cycles} - {:op :built :budget budget :blocks [] :expr {:op :return :values [(:value term)]}}) - :throw {:op :built :budget budget :blocks [] :expr term} + {:op :built :budget budget :expr {:op :return :values [(:value term)]}}) + :throw {:op :built :budget budget :expr term} :switch (let [result (arms ctx budget term)] (if (= :built (:op result)) - {:op :built :budget (:budget result) :blocks (:blocks result) :expr {:op :switch :test (:test term) :arms (:arms result)}} result)) + {:op :built :budget (:budget result) :expr {:op :switch :test (:test term) :arms (:arms result)}} result)) :branch (let [yes (edge ctx budget (:yes term))] (if (= :exhausted (:op yes)) yes (let [no (edge ctx (:budget yes) (:no term))] (if (= :exhausted (:op no)) no - {:op :built :budget (:budget no) :blocks (into (:blocks yes) (:blocks no)) + {:op :built :budget (:budget no) :expr {:op :branch :test (:test term) :yes (:expr yes) :no (:expr no)}}))))) (joined @@ -77,8 +77,7 @@ (if (= :built (:op result)) (let [tail (edge ctx (:budget result) (:target join))] (if (= :built (:op tail)) - (assoc tail :blocks (into (:blocks result) (:blocks tail)) - :expr {:op :join :targets (:results join) :value (:expr result) :body (:expr tail)}) tail)) + (assoc tail :expr {:op :join :targets (:results join) :value (:expr result) :body (:expr tail)}) tail)) result))) (body @@ -91,7 +90,7 @@ result (if (and join (within-stop? ctx join)) (joined ctx budget (:term block) join) (terminator ctx budget (:term block)))] - (if (= :built (:op result)) (assoc result :blocks (conj (:blocks result) id) :expr (wrap (:expr result) block)) result))))) + (if (= :built (:op result)) (assoc result :expr (wrap (:expr result) block)) result))))) (loop-form {:from [l/Machine Label] :to [l/Joined Build] :args [l/Joined-Context t/Int] :body true} @@ -108,10 +107,10 @@ (let [stop (:stop ctx)] (cond (and stop (= id (first stop))) - {:op :built :budget budget :blocks [] + {:op :built :budget budget :expr {:op :return :values (mapv (fn [id] {:op :local :id id}) (second stop))}} (= id (:header ctx)) - (if (pos? budget) {:op :built :budget (dec budget) :blocks [] :expr {:op :recur :slots (:slots ctx)}} + (if (pos? budget) {:op :built :budget (dec budget) :expr {:op :recur :slots (:slots ctx)}} {:op :exhausted :reason :budget}) (contains? (:active ctx) id) {:op :exhausted :reason :cycles} (contains? (:loops ctx) id) @@ -122,45 +121,16 @@ (if (= :built (:op result)) (let [tail (edge ctx (:budget result) (:target join))] (if (= :built (:op tail)) - (assoc tail :blocks (into (:blocks result) (:blocks tail)) - :expr {:op :join :targets (:results join) :value (:expr result) :body (:expr tail)}) tail)) result)) + (assoc tail :expr {:op :join :targets (:results join) :value (:expr result) :body (:expr tail)}) tail)) result)) {:op :exhausted :reason :cycles}) {:op :exhausted :reason :cycles}) (loop-form ctx budget id)) :else (body ctx budget id))))) -(np/defpass construction-site? - {:from [l/Numeric Rhs] :to t/Bool :body true} - [rhs] - (case (:op rhs) - :native true - :convert (let [conversion (:conversion rhs)] - (and (:parameter? conversion) - (.isAnnotationPresent ^Class (:type conversion) java.lang.FunctionalInterface))) - :host (let [operation (:operation rhs)] - (case (:op operation) - (:keyword-site :method-value) true - :adapt (.isAnnotationPresent ^Class (:class operation) java.lang.FunctionalInterface) - :protocol (boolean (some (fn [^Class type] (.isAnnotationPresent type java.lang.FunctionalInterface)) - (:parameters (:method operation)))) - false)) - false)) - -(np/defpass site-failure - {:from [l/Numeric Program] :to (t/Option ':native-sites) - :args [(t/Vec l/Machine-Label)] :body true} - [blocks machine] - (let [copies (frequencies blocks) - repeated (mapv (fn [[id _]] (block machine id)) (filter (fn [[_ n]] (> n 1)) copies))] - (when (some (fn [block] - (some (fn [operation] (construction-site? (:value operation))) - (:ops block))) repeated) - :native-sites))) - (np/defpass recover {:from [l/Planned Program] :to [l/Joined Decision] :body true} [plan] (let [machine (:machine plan) - reachable (graph/reachable (graph/collect-facts machine) (:entry machine)) + reachable (graph/reachable (graph/collect-facts (:blocks machine)) (:entry machine)) dead (apply dissoc (:blocks machine) reachable) loops (:loops plan)] (cond @@ -172,6 +142,5 @@ budget (* 4 (reduce + 0 (map #(inc (count (:ops %))) (vals (select-keys (:blocks machine) reachable))))) result (edge ctx budget (:entry machine))] (if (= :built (:op result)) - (if-let [reason (site-failure (:blocks result) machine)] - {:op :fallback :reason reason} {:op :structured :body (:expr result)}) + {:op :structured :body (:expr result)} {:op :fallback :reason (:reason result)}))))) diff --git a/src/com/tailrecursion/blacktide/compiler/passes/value_facts.clj b/src/com/tailrecursion/blacktide/compiler/passes/value_facts.clj index 297acfa..31d61ff 100644 --- a/src/com/tailrecursion/blacktide/compiler/passes/value_facts.clj +++ b/src/com/tailrecursion/blacktide/compiler/passes/value_facts.clj @@ -20,7 +20,7 @@ (np/defpass atom-type {:from [l/Machine Atom] :to [l/Analysis Type] :args [l/Analysis-Types]} [types node] :local (get types (:id node) :bottom) - ;; Quoted Integer constants must keep their boxed identity and width. + ;; Quoted Integer constants stay boxed to preserve their width. :literal (cond (instance? Long (:value node)) (primitive Long/TYPE) (instance? Double (:value node)) (primitive Double/TYPE) :else :boxed)) @@ -67,5 +67,5 @@ (np/defpass analyze {:from [l/Numeric Program] :to [l/Analysis Program] :body true} [machine] - (let [reachable (graph/reachable (graph/collect-facts {:entry (:entry machine) :blocks (:blocks machine)}) (:entry machine))] + (let [reachable (graph/reachable (graph/collect-facts (:blocks machine)) (:entry machine))] {:machine machine :reachable reachable :types (solve (definitions reachable machine))})) diff --git a/src/com/tailrecursion/blacktide/runtime.clj b/src/com/tailrecursion/blacktide/runtime.clj deleted file mode 100644 index 9e6c76b..0000000 --- a/src/com/tailrecursion/blacktide/runtime.clj +++ /dev/null @@ -1,7 +0,0 @@ -(ns com.tailrecursion.blacktide.runtime - "Internal emission support. Cache literals are created during final Clojure - analysis, so macros duplicating an expansion still get distinct call sites." - (:import [com.tailrecursion.blacktide.runtime KeywordSite])) - -(defmacro keyword-site [keyword] - (list 'quote (KeywordSite. keyword))) diff --git a/test/com/tailrecursion/assignment_model.clj b/test/com/tailrecursion/assignment_model.clj deleted file mode 100644 index 54b1be9..0000000 --- a/test/com/tailrecursion/assignment_model.clj +++ /dev/null @@ -1,76 +0,0 @@ -(ns com.tailrecursion.assignment-model - "Independent assignment semantics, event traces, and bounded source generators. - Shared by the nanopass client tests and the real Loophole compiler tests." - (:require [clojure.test.check.generators :as gen])) - -(defn lit [value] {:op :const :value value}) -(defn read-var [name] {:op :read :binding name}) -(defn batch [op & writes] {:op op :writes (mapv vec (partition 2 writes))}) -(defn sequence-of [& forms] {:op :do :forms (vec forms)}) -(defn event [tag value] {:op :event :tag tag :value value}) - -(defn evaluate [form state] - (let [run #(evaluate % %2) - binding (:binding form)] - (case (:op form) - :const [(:value form) state] - :read [(get-in state [:vars binding]) state] - :temp (let [id (:id form)] - (when-not (contains? (:temps state) id) - (throw (ex-info "Unbound temporary" {:id id}))) - [(get-in state [:temps id]) state]) - :add (let [[a state] (run (:left form) state) - [b state] (run (:right form) state)] [(+ a b) state]) - :do (reduce (fn [[_ state] form] (run form state)) [nil state] (:forms form)) - :if (let [[test state] (run (:test form) state)] - (run (if test (:then form) (:else form)) state)) - :event (let [[value state] (run (:value form) state)] - [value (update state :trace conj [(:tag form) value])]) - :throw (throw (ex-info "Source exception" {:status :throw :tag (:tag form) :state state})) - ;; Source assignments define their semantics directly, independent of - ;; the pass's generated binding tree and the output's write primitive. - :setq (reduce (fn [[_ state] [name rhs]] - (let [[value state] (run rhs state)] - [value (assoc-in state [:vars name] value)])) - [nil state] (:writes form)) - :psetq (let [[values state] - (reduce (fn [[values state] [_ rhs]] - (let [[value state] (run rhs state)] - [(conj values value) state])) - [[] state] (:writes form))] - [nil (update state :vars merge (zipmap (map first (:writes form)) values))]) - ;; The output interpreter knows only a single write and lexical temps. - :write (let [[value state] (run (:value form) state)] - [value (assoc-in state [:vars binding] value)]) - :bind (let [[value state] (run (:value form) state) - temps (:temps state) - [result state] (run (:body form) (assoc-in state [:temps (:id form)] value))] - [result (assoc state :temps temps)])))) - -(defn execute [form vars] - (let [state {:vars vars :temps {} :trace []}] - (try - (let [[value state] (evaluate form state)] - (assoc (select-keys state [:vars :trace]) :value value :status :return)) - (catch clojure.lang.ExceptionInfo e - (let [{:keys [state status tag]} (ex-data e)] - (if status - (cond-> (assoc (select-keys state [:vars :trace]) :status status) - tag (assoc :tag tag)) - (throw e))))))) - -(defn expression [depth] - (let [leaf (gen/one-of [(gen/fmap lit (gen/choose -5 5)) - (gen/fmap read-var (gen/elements '[x y]))])] - (if (zero? depth) leaf - (let [child (expression (dec depth)) - writes (gen/vector (gen/tuple (gen/elements '[x y]) child) 0 3)] - (gen/one-of - [leaf - (gen/let [a child b child] {:op :add :left a :right b}) - (gen/let [a child b child] (sequence-of a b)) - (gen/let [test gen/boolean then child else child] - {:op :if :test (lit test) :then then :else else}) - (gen/let [tag (gen/elements [:a :b :c]) value child] (event tag value)) - (gen/let [writes writes] (sequence-of {:op :setq :writes writes} (read-var 'x))) - (gen/let [writes writes] (sequence-of {:op :psetq :writes writes} (read-var 'y)))]))))) diff --git a/test/com/tailrecursion/blacktide/aot_test.clj b/test/com/tailrecursion/blacktide/aot_test.clj index eac31f7..a7b77f5 100644 --- a/test/com/tailrecursion/blacktide/aot_test.clj +++ b/test/com/tailrecursion/blacktide/aot_test.clj @@ -21,11 +21,6 @@ (try (is (zero? (:exit compiled)) (:output compiled)) (when (zero? (:exit compiled)) - ;; A library JAR includes this Java class; AOT does not copy dependencies. - (let [path "com/tailrecursion/blacktide/runtime/KeywordSite.class" - target (io/file directory path)] - (io/make-parents target) - (with-open [source (io/input-stream (io/resource path))] (io/copy source target))) (let [loaded (subprocess (str/join java.io.File/pathSeparator (cons (.getPath directory) dependencies)) '(do (require 'com.tailrecursion.blacktide.aot-fixture) (assert (= 9 (com.tailrecursion.blacktide.aot-fixture/sample 4))) @@ -39,4 +34,18 @@ (:k (com.tailrecursion.blacktide/setq x {:k 11})))))) (println "AOT client and source-free macro expansion passed")))] (is (zero? (:exit loaded)) (:output loaded)))) + ;; Execute generated function classes with only Clojure and the client. + ;; Loading the client's namespace would also execute its macro require. + (when (zero? (:exit compiled)) + (doseq [file (file-seq directory) + :when (and (.isFile file) (not (.startsWith (.getName file) "aot_fixture")))] + (io/delete-file file)) + (let [loaded (subprocess (str/join java.io.File/pathSeparator (cons (.getPath directory) dependencies)) + '(do + (assert (= 9 (clojure.lang.Reflector/invokeStaticMethod + "com.tailrecursion.blacktide.aot_fixture$sample" "invokeStatic" (to-array [4])))) + (assert (= 7 (clojure.lang.Reflector/invokeStaticMethod + "com.tailrecursion.blacktide.aot_fixture$keyword_read" "invokeStatic" (to-array [{:k 7}])))) + (println "Generated code runs without compiler classes")))] + (is (zero? (:exit loaded)) (:output loaded)))) (finally (doseq [file (reverse (file-seq directory))] (io/delete-file file)))))) diff --git a/test/com/tailrecursion/blacktide/assignment_model.clj b/test/com/tailrecursion/blacktide/assignment_model.clj new file mode 100644 index 0000000..468c5a9 --- /dev/null +++ b/test/com/tailrecursion/blacktide/assignment_model.clj @@ -0,0 +1,53 @@ +(ns com.tailrecursion.blacktide.assignment-model + "Independent assignment semantics, event traces, and bounded source generators. + The interpreter models source assignments without using compiler lowering." + (:require [clojure.test.check.generators :as gen])) + +(defn lit [value] {:op :const :value value}) +(defn read-var [name] {:op :read :binding name}) +(defn batch [op & writes] {:op op :writes (mapv vec (partition 2 writes))}) +(defn sequence-of [& forms] {:op :do :forms (vec forms)}) +(defn event [tag value] {:op :event :tag tag :value value}) + +(defn evaluate [form state] + (case (:op form) + :const [(:value form) state] + :read [(get-in state [:vars (:binding form)]) state] + :add (let [[a state] (evaluate (:left form) state) + [b state] (evaluate (:right form) state)] [(+ a b) state]) + :do (reduce (fn [[_ state] form] (evaluate form state)) [nil state] (:forms form)) + :if (let [[test state] (evaluate (:test form) state)] + (evaluate (if test (:then form) (:else form)) state)) + :event (let [[value state] (evaluate (:value form) state)] + [value (update state :trace conj [(:tag form) value])]) + :setq (reduce (fn [[_ state] [name rhs]] + (let [[value state] (evaluate rhs state)] + [value (assoc-in state [:vars name] value)])) + [nil state] (:writes form)) + ;; Snapshot every parallel RHS before applying any pending write. + :psetq (let [[values state] + (reduce (fn [[values state] [_ rhs]] + (let [[value state] (evaluate rhs state)] + [(conj values value) state])) + [[] state] (:writes form))] + [nil (update state :vars merge (zipmap (map first (:writes form)) values))]))) + +(defn execute [form vars] + (let [[value state] (evaluate form {:vars vars :trace []})] + (assoc state :value value))) + +(defn expression [depth] + (let [leaf (gen/one-of [(gen/fmap lit (gen/choose -5 5)) + (gen/fmap read-var (gen/elements '[x y]))])] + (if (zero? depth) leaf + (let [child (expression (dec depth)) + writes (gen/vector (gen/tuple (gen/elements '[x y]) child) 0 3)] + (gen/one-of + [leaf + (gen/let [a child b child] {:op :add :left a :right b}) + (gen/let [a child b child] (sequence-of a b)) + (gen/let [test gen/boolean then child else child] + {:op :if :test (lit test) :then then :else else}) + (gen/let [tag (gen/elements [:a :b :c]) value child] (event tag value)) + (gen/let [writes writes] (sequence-of {:op :setq :writes writes} (read-var 'x))) + (gen/let [writes writes] (sequence-of {:op :psetq :writes writes} (read-var 'y)))]))))) diff --git a/test/com/tailrecursion/blacktide/assignment_test.clj b/test/com/tailrecursion/blacktide/assignment_test.clj index a235757..f488f69 100644 --- a/test/com/tailrecursion/blacktide/assignment_test.clj +++ b/test/com/tailrecursion/blacktide/assignment_test.clj @@ -7,7 +7,7 @@ [clojure.test.check.properties :as prop] [com.tailrecursion.blacktide :as lh :refer [loophole setq psetq]] [com.tailrecursion.blacktide.backend-fixture :as backend] - [com.tailrecursion.assignment-model :as oracle])) + [com.tailrecursion.blacktide.assignment-model :as oracle])) (defn add [a b] (+ a b)) (defn event! [events tag value] (swap! events conj [tag value]) value) @@ -83,11 +83,11 @@ (list 'let ['events '(atom [])] (list 'loophole '[x (long x) y (long y)] (list 'let ['result (render node)] - {:status :return :value 'result :trace '(deref events) + {:value 'result :trace '(deref events) :vars {(list 'quote 'x) 'x (list 'quote 'y) 'y}})))) :default) nil)) (deftest generated-assignment-programs - ;; Reuse only the independent source interpreter, not its lowering algorithm. + ;; Compare generated source with the independent interpreter. (let [property (prop/for-all [node (oracle/expression 3)] (let [compiled (compile-node node)] (every? true? diff --git a/test/com/tailrecursion/blacktide/backend_test.clj b/test/com/tailrecursion/blacktide/backend_test.clj index a430055..35d6f31 100644 --- a/test/com/tailrecursion/blacktide/backend_test.clj +++ b/test/com/tailrecursion/blacktide/backend_test.clj @@ -8,7 +8,6 @@ [com.tailrecursion.blacktide.compiler.passes.storage :as storage] [com.tailrecursion.blacktide.compiler.passes.compact :as compact] [com.tailrecursion.blacktide.compiler.passes.value-facts :as facts] - [com.tailrecursion.blacktide.compiler.passes.box-origins :as origins] [com.tailrecursion.blacktide.compiler.jvm :as jvm] [com.tailrecursion.blacktide.compiler.passes.prepare :as prepare] [com.tailrecursion.blacktide.compiler.passes.lower :as lower] @@ -85,7 +84,7 @@ (deftest integer-conversion-overloads-are-not-interchangeable (let [outcome (fn [f x] (try (let [v (f x)] [v (class v)]) - (catch Throwable e [(class e) (ex-message e)]))) + (catch Throwable e [(class e)]))) reference (fn [x] (int (identity x)))] (doseq [[cast values] [['long [Long/MIN_VALUE (dec (long Integer/MIN_VALUE)) Integer/MIN_VALUE @@ -105,21 +104,18 @@ :seed 173205)] (is (:pass? result) (pr-str [mode result]))))))) -(deftest cached-long-origins-retain-value-class-and-identity - ;; Compiler.NumberExpr initializes constants with Long.valueOf, including - ;; distinct Long objects supplied by macros. Only its guaranteed cache is safe. - (doseq [n [-129 -128 -1 0 1 127 128 1000]] - (let [literal (Long. (long n)) cached? (<= -128 n 127) - form (list 'loophole ['i 0 'x literal] +(deftest return-boxing-preserves-values-and-observed-references + (doseq [literal [-129 0 1000 -0.0 1.5]] + (let [form (list 'loophole ['i 0 'x literal] '(while (< i 2) (setq i (inc i))) 'x)] - (is (= cached? (origins/reboxable? {} {:op :value :value (m/literal literal)}))) + (is (some (fn [[id type]] (and (.endsWith (name id) "__x") + (= type (if (instance? Long literal) Long/TYPE Double/TYPE)))) + (:types (capture #'storage/select-storage form)))) (doseq [mode [:default :dispatcher]] - (let [f (compile-program form mode) first-value (f nil)] - (is (= n first-value)) - (is (= Long (class first-value))) - (is (identical? first-value (f nil))) - (is (= cached? (identical? (Long/valueOf (long n)) first-value))))))) - ;; Small external boxes and constructor results are not cached literals. + (let [value ((compile-program form mode) nil)] + (is (= (Double/doubleToRawLongBits (double literal)) (Double/doubleToRawLongBits (double value)))) + (is (= (class literal) (class value))))))) + ;; Externally supplied and observed boxes retain identity regardless of value. (doseq [mode [:default :dispatcher] value [(Long. 0) (Long. 127) (Long. 128) (Integer. 0) (Double. 0.0)]] (let [f (compile-program '(loophole [i 0 x events] diff --git a/test/com/tailrecursion/blacktide/generated_test.clj b/test/com/tailrecursion/blacktide/generated_test.clj index 994c284..8ad379a 100644 --- a/test/com/tailrecursion/blacktide/generated_test.clj +++ b/test/com/tailrecursion/blacktide/generated_test.clj @@ -15,7 +15,7 @@ (defn three-steps [x k] (+ x (* 3 k))) -(def operations [:parallel :if :case :snapshot :arguments :shadow :helper :loop :check :truth :cast]) +(def operations [:parallel :if :case :snapshot :arguments :shadow :helper :loop :check :truth :cast :lookup]) (defn model-step [{:keys [x y] :as state} i index [op k]] (if (and (= op :check) (= i k)) (reduced (assoc state :throw ArithmeticException)) @@ -30,6 +30,7 @@ :loop [(+ x (* 2 k)) y] :truth [(+ x (if (even? k) k (- k))) y] :cast [(+ x k) y] + :lookup [(+ x k) (+ x y)] :check [x y])] (assoc state :x x :y y :trace (conj (:trace state) [i index x y]))))) @@ -63,6 +64,9 @@ :truth `(setq ~'x (if ~(if (even? k) (if (pos? k) 0 :selected) (if (pos? k) false nil)) (+ ~'x ~k) (- ~'x ~k))) :cast `(setq ~'x (+ (long ~'x) ~k)) + :lookup `(setq ~'y (:value (case (mod ~'i 3) + 0 (let [~'old ~'x] (setq ~'x (+ ~'x ~k)) {:value (+ ~'old ~'y)}) + (do (setq ~'x (+ ~'x ~k)) {:value (+ (- ~'x ~k) ~'y)})))) :check `(when (= ~'i ~k) (+ Long/MAX_VALUE 1))) (swap! ~'events conj [~(observed numeric? 'i) ~index ~(observed numeric? 'x) ~(observed numeric? 'y)]))) diff --git a/test/com/tailrecursion/blacktide/interop_values_test.clj b/test/com/tailrecursion/blacktide/interop_values_test.clj new file mode 100644 index 0000000..fbe97e0 --- /dev/null +++ b/test/com/tailrecursion/blacktide/interop_values_test.clj @@ -0,0 +1,50 @@ +(ns com.tailrecursion.blacktide.interop-values-test + "Method values and adapters retain their behavior across recovered control. + Bound references preserve identity; generated wrapper classes may differ." + (:require [clojure.test :refer [deftest is]] + [com.tailrecursion.blacktide.compiler.passes.structure :as structure] + [com.tailrecursion.blacktide.backend-fixture :as backend] + [com.tailrecursion.blacktide.jvm-fixture])) + +(def flags [true false false true]) + +(deftest java-function-values-across-joins + (let [target (proxy [java.util.HashMap] [] (compute [_ callback] callback)) + callback (fn [x y] [x y])] + (doseq [[tail invoke] + [['String/.length #(% "abc")] + [(with-meta 'String/valueOf {:param-tags ['Object]}) #(% "abc")] + [(with-meta 'String/new {:param-tags ['String]}) #(% "abc")] + ['(let [^java.util.function.BiFunction f callback] f) + #(.apply ^java.util.function.BiFunction % :left :right)] + ['(java.util.Map/.compute target :key callback) + #(.apply ^java.util.function.BiFunction % :left :right)] + ['(com.tailrecursion.blacktide.jvm-fixture/map-compute target :key callback) + #(.apply ^java.util.function.BiFunction % :left :right)]] + mode [:default :dispatcher]] + (let [ordinary (list 'let ['values '(if flag [1 2] [3 4])] (list 'vector 'values tail)) + compiled (list 'loophole '[a 0 b 0] + '(if flag (setq a 1 b 2) (setq a 3 b 4)) (list 'vector '[a b] tail)) + observe (fn [body mode] + (let [f ((backend/compile-program (list 'fn '[flag target callback] body) mode) nil) + results (mapv #(f % target callback) flags)] + (mapv (fn [[pair f]] [pair (invoke f)]) results)))] + (is (= :structured (:op (backend/capture #'structure/recover + (list 'fn '[flag target callback] compiled))))) + (is (= (observe ordinary :ordinary) (observe compiled mode)) (pr-str [mode tail])))))) + +(deftest bound-adapters-retain-identity-and-effects + (doseq [mode [:default :dispatcher] flag [false true]] + (let [events (atom []) callback (fn [x y] (swap! events conj [x y]) (+ x y)) + f ((backend/compile-program + '(fn [flag callback] + (loophole [a 0 b 0 saved nil] + (if flag (setq a 1 b 2) (setq a 3 b 4)) + (let [^java.util.function.BiFunction f (do (swap! events conj :adapt) callback)] + (setq saved f) + [(identical? saved f) (.apply f a b) saved]))) mode) events) + [same? result adapter] (f flag callback)] + (is same?) + (is (= (if flag 3 7) result)) + (is (= 9 (.apply ^java.util.function.BiFunction adapter 4 5))) + (is (= [:adapt (if flag [1 2] [3 4]) [4 5]] @events))))) diff --git a/test/com/tailrecursion/blacktide/jvm_fixture.clj b/test/com/tailrecursion/blacktide/jvm_fixture.clj index 7db9df1..cd7ef1d 100644 --- a/test/com/tailrecursion/blacktide/jvm_fixture.clj +++ b/test/com/tailrecursion/blacktide/jvm_fixture.clj @@ -1,5 +1,5 @@ (ns com.tailrecursion.blacktide.jvm-fixture - "Shared JVM dispatch and lookup fixtures, with a native Clojure outcome oracle." + "Shared JVM dispatch fixtures, with a native Clojure outcome oracle." (:require [clojure.java.io :as io] [com.tailrecursion.blacktide :refer [loophole setq psetq label goto]] [com.tailrecursion.blacktide.backend-fixture :as backend])) @@ -38,20 +38,6 @@ (catch Throwable error {:error (class (backend/root-error error))}))) -(deftype ObservedLookup [events entries] - clojure.lang.ILookup - (valAt [_ key] (swap! events conj [:val key]) (get entries key)) - (valAt [_ key default] (swap! events conj [:default key]) (get entries key default)) - clojure.lang.IKeywordLookup - (getLookupThunk [_ key] - (swap! events conj [:install key]) - (reify clojure.lang.ILookupThunk - (get [thunk target] - (if (instance? ObservedLookup target) - (do (swap! (.-events ^ObservedLookup target) conj [:get key]) - (get (.-entries ^ObservedLookup target) key)) - thunk))))) - (def java-map {:on 'java.util.Map :method-map {:map-compute :compute}}) (defn map-compute [_ k f] (f k :fallback)) (alter-meta! #'map-compute assoc :protocol #'java-map) diff --git a/test/com/tailrecursion/blacktide/lookup_test.clj b/test/com/tailrecursion/blacktide/lookup_test.clj index 67dc025..4f9627a 100644 --- a/test/com/tailrecursion/blacktide/lookup_test.clj +++ b/test/com/tailrecursion/blacktide/lookup_test.clj @@ -1,17 +1,29 @@ (ns com.tailrecursion.blacktide.lookup-test - "Keyword call syntax determines cached IKeywordLookup versus ordinary ILookup - dispatch. Native emission must preserve call sites, effects and cache faults." + "Keyword syntax retains lookup dispatch, values and source effects. + Cache installation and unsuccessful probes are not observable contracts." (:require [clojure.test :refer [deftest is]] [com.tailrecursion.blacktide.backend-fixture :as backend] - [com.tailrecursion.blacktide.jvm-fixture]) - (:import [com.tailrecursion.blacktide.jvm_fixture ObservedLookup])) + [com.tailrecursion.blacktide.compiler.passes.structure :as structure])) + +(deftype ObservedLookup [events entries] + clojure.lang.ILookup + (valAt [_ key] (swap! events conj [:val key]) (get entries key)) + (valAt [_ key default] (swap! events conj [:default key]) (get entries key default)) + clojure.lang.IKeywordLookup + (getLookupThunk [_ key] + (reify clojure.lang.ILookupThunk + (get [thunk target] + (if (instance? ObservedLookup target) + (do (swap! (.-events ^ObservedLookup target) conj [:get key]) + (get (.-entries ^ObservedLookup target) key)) + thunk))))) (deftype UncachedLookup [events entries] clojure.lang.ILookup (valAt [_ key] (swap! events conj [:val key]) (get entries key)) (valAt [_ key default] (swap! events conj [:default key]) (get entries key default)) clojure.lang.IKeywordLookup - (getLookupThunk [_ key] (swap! events conj [:no-thunk key]) nil)) + (getLookupThunk [_ _] nil)) (def ^:const constant-key :k) (defmacro lookup [target] `(:k ~target)) @@ -23,7 +35,7 @@ targets [a a b c c {:k 11} nil a {} (java.util.HashMap. {:k 13})]] [(mapv #(function % events) targets) @events])) -(deftest native-keyword-call-syntax +(deftest keyword-call-syntax (doseq [body '[(:k (do (swap! events conj :target) target)) (:missing target) (:k (do (swap! events conj :target) target) (do (swap! events conj :default) 17)) @@ -41,36 +53,7 @@ :let [compiled ((backend/compile-program (list 'fn '[target events] (list 'loophole [] (wrap body))) mode) nil)]] (is (= (trace reference) (trace compiled)) (pr-str [body mode (wrap body)])))) -(deftest keyword-sites-in-mutable-regions - (doseq [body '[(:k (do (swap! events conj :target) target)) - (:missing target) - ((quote :k) target) - ((do :k) target) - (com.tailrecursion.blacktide.lookup-test/constant-key target) - (com.tailrecursion.blacktide.lookup-test/lookup target)] - mode [:default :dispatcher] - :let [reference (list 'fn '[target events] body) - compiled (list 'fn '[target events] - (list 'loophole ['target 'target 'unused 0] - '(setq unused (inc unused)) body))]] - (is (= (trace ((backend/compile-program reference :default) nil)) - (trace ((backend/compile-program compiled mode) nil))) - (pr-str [body mode])))) - -(deftest region-keyword-site-keeps-the-thunk-snapshot - (let [body '(:k (do (swap! events conj :target) - (if (vector? target) - (do (read-key (first target) events) (second target)) target))) - run (fn [body mode] - (let [f ((backend/compile-program (list 'fn 'read-key '[target events] body) mode) nil) - events (atom []) a (ObservedLookup. events {:k 7}) - b (UncachedLookup. events {:k 9})] - [(f [a b] events) (f [b a] events) (f a events) @events]))] - (doseq [mode [:default :dispatcher]] - (is (= (run body :default) - (run (list 'loophole [] body) mode)) (str mode))))) - -(deftest keyword-sites-across-backward-jumps +(deftest keyword-lookups-across-backward-jumps (let [reference '(fn [target events] (loop [i 0 out []] (if (< i 3) (recur (inc i) (conj out (:k target))) out))) @@ -83,19 +66,22 @@ (is (= (trace ((backend/compile-program reference :default) nil)) (trace ((backend/compile-program source mode) nil))) (str mode))))) -(deftest lookup-faults-are-not-bypassed - (let [target (reify - clojure.lang.ILookup - (valAt [_ _] :wrong-path) - (valAt [_ _ _] :wrong-path) - clojure.lang.IKeywordLookup - (getLookupThunk [_ _] (throw (ex-info "lookup fault" {:phase :install}))))] - (doseq [mode [:default :dispatcher] - source '[(fn [target] (loophole [] (:k target))) - (fn [target] (loophole [saved nil] (:k (setq saved target))))] - :let [f ((backend/compile-program source mode) nil)]] - (is (= {:phase :install} - (try (f target) (catch clojure.lang.ExceptionInfo e (ex-data e)))) (str mode))))) +(deftest argument-and-lookup-failures + (doseq [mode [:default :dispatcher] fail-argument? [false true]] + (let [events (atom []) + target (reify clojure.lang.IKeywordLookup + (getLookupThunk [_ _] + (reify clojure.lang.ILookupThunk + (get [_ _] (swap! events conj :lookup) (throw (ex-info "lookup" {:phase :lookup})))))) + f ((backend/compile-program + '(fn [target fail?] + (loophole [saved nil] + (:k (do (swap! events conj :argument) + (when fail? (throw (ex-info "argument" {:phase :argument}))) + (setq saved target))))) mode) events)] + (is (= {:phase (if fail-argument? :argument :lookup)} + (try (f target fail-argument?) (catch clojure.lang.ExceptionInfo e (ex-data e))))) + (is (= (if fail-argument? [:argument] [:argument :lookup]) @events))))) (deftest assigned-keyword-targets (doseq [operator '[:k :missing (quote :k) (do :k) @@ -120,7 +106,7 @@ (is (= (trace ((backend/compile-program reference :default) nil)) (trace ((backend/compile-program source mode) nil))) (str mode)))) -(deftest assigned-target-keeps-reentrant-snapshot +(deftest reentrant-argument-evaluation (let [reference '(fn read-key [target events] (:k (do (swap! events conj :target) (if (vector? target) @@ -134,8 +120,9 @@ (let [f ((backend/compile-program form mode) nil) events (atom []) a (ObservedLookup. events {:k 7}) b (UncachedLookup. events {:k 9})] [(f [a b] events) (f [b a] events) (f a events) @events]))] - (doseq [mode [:default :dispatcher]] - (is (= (run reference :default) (run source mode)) (str mode))))) + (doseq [mode [:default :dispatcher] + form [source (list 'fn 'read-key '[target events] (list 'loophole [] (last reference)))]] + (is (= (run reference :default) (run form mode)) (str mode))))) (deftest skipped-lookup-and-reentry (let [reference '(fn [target events] @@ -153,24 +140,6 @@ (is (= (trace ((backend/compile-program reference :default) nil)) (trace ((backend/compile-program source mode) nil))) (str mode))))) -(defmacro twice-expanded [form] - (let [expanded (macroexpand form)] `(do ~expanded ~expanded))) - -(deftest cache-sites-survive-expansion-and-closure-instances - (doseq [[reference source] - '[[(fn [target events] - (com.tailrecursion.blacktide.lookup-test/twice-expanded (:k target))) - (fn [target events] - (com.tailrecursion.blacktide.lookup-test/twice-expanded - (loophole [saved nil] (:k (setq saved target)))))] - [(let [factory (fn [] (fn [target events] (:k target)))] - (fn [target events] ((factory) target events))) - (let [factory (fn [] (fn [target events] (loophole [saved nil] (:k (setq saved target)))))] - (fn [target events] ((factory) target events)))]] - mode [:default :dispatcher]] - (is (= (trace ((backend/compile-program reference :default) nil)) - (trace ((backend/compile-program source mode) nil))) (pr-str [source mode])))) - (deftest assigned-lookup-after-multiple-scope-entries (let [reference '(fn [flag target events] [(if flag 3 7) (:k target)]) source '(fn [flag target events] @@ -183,5 +152,6 @@ (let [f ((backend/compile-program form mode) nil) events (atom []) target (ObservedLookup. events {:k 42})] [(mapv #(f % target events) [true false false true]) @events]))] + (is (= :structured (:op (backend/capture #'structure/recover source)))) (doseq [mode [:default :dispatcher]] (is (= (run reference :default) (run source mode)) (str mode))))) diff --git a/test/com/tailrecursion/blacktide/native_sites_test.clj b/test/com/tailrecursion/blacktide/native_sites_test.clj deleted file mode 100644 index 2372b13..0000000 --- a/test/com/tailrecursion/blacktide/native_sites_test.clj +++ /dev/null @@ -1,101 +0,0 @@ -(ns com.tailrecursion.blacktide.native-sites-test - "Control recovery retains Java function construction and cached lookup sites. - Generated loops preserve bound references without promising literal pooling." - (:require [clojure.test :refer [deftest is]] - [clojure.test.check.clojure-test :refer [defspec]] - [clojure.test.check.generators :as gen] - [clojure.test.check.properties :as prop] - [com.tailrecursion.blacktide.compiler.passes.structure :as structure] - [com.tailrecursion.blacktide.backend-fixture :as backend] - [com.tailrecursion.blacktide :refer [loophole setq label goto]] - [com.tailrecursion.blacktide.jvm-fixture :as jvm])) - -(def flags [true false false true]) -(defn identity-pattern [values] - (mapv (fn [a] (mapv #(identical? a %) values)) values)) -(deftest java-function-construction-sites - ;; Qualified method values and implicit functional-interface adapters also - ;; construct classes. A join must not split one source site across its paths. - (let [target (proxy [java.util.HashMap] [] (compute [_ callback] callback)) - callback (fn [x y] [x y])] - (doseq [[tail invoke] - [['String/.length #(% "abc")] - [(with-meta 'String/valueOf {:param-tags ['Object]}) #(% "abc")] - [(with-meta 'String/new {:param-tags ['String]}) #(% "abc")] - ['(let [^java.util.function.BiFunction f callback] f) - #(.apply ^java.util.function.BiFunction % :left :right)] - ['(java.util.Map/.compute target :key callback) - #(.apply ^java.util.function.BiFunction % :left :right)] - ['(com.tailrecursion.blacktide.jvm-fixture/map-compute target :key callback) - #(.apply ^java.util.function.BiFunction % :left :right)]] - mode [:default :dispatcher]] - (let [ordinary (list 'let ['values '(if flag [1 2] [3 4])] (list 'vector 'values tail)) - compiled (list 'loophole '[a 0 b 0] - '(if flag (setq a 1 b 2) (setq a 3 b 4)) (list 'vector '[a b] tail)) - observe (fn [body mode] - (let [f ((backend/compile-program (list 'fn '[flag target callback] body) mode) nil) - results (mapv #(f % target callback) flags)] - [(identity-pattern (mapv (comp class second) results)) - (mapv (fn [[pair f]] [pair (invoke f)]) results)]))] - (is (= (observe ordinary :ordinary) (observe compiled mode)) (pr-str [mode tail])))))) - -(defn cached-lookups [mode wrapped?] - (let [lookup '(:k target) - body `(do (if ~'flag (swap! ~'events conj :yes) (swap! ~'events conj :no)) ~lookup) - events (atom []) target (jvm/->ObservedLookup events {:k 42}) - f ((backend/compile-program (list 'fn '[flag target] (if wrapped? (list 'loophole [] body) body)) mode) events) - values (mapv #(f % target) flags)] - [values @events])) - -(deftest cached-lookup-sites-through-joins - (doseq [mode [:default :dispatcher]] - (is (= (cached-lookups mode false) (cached-lookups mode true)) - (pr-str [mode])))) - -(defn loop-program [tail real-backedge?] - `(loophole [~'a 0 ~'b 0 ~'i 0] - (if ~'flag (setq ~'a 1 ~'b 2) (setq ~'a 3 ~'b 4)) - (label ~'again) - ~(if real-backedge? - '(when (< i 1) (setq i (inc i)) (goto again)) - '(when (neg? a) (goto again))) - (+ ~'a ~'b) - ~tail)) - -(deftest bound-references-across-generated-loops - (doseq [real-backedge? [false true] context [:tail :expression] - mode [:default :dispatcher]] - (let [body (loop-program 'input real-backedge?) - source (list 'fn '[flag input] (if (= context :tail) body (list 'identity body))) - f ((backend/compile-program source mode) nil) - inputs [(Object.) (object-array [nil]) (Long/valueOf 1000)] - inputs (conj inputs (first inputs))] - (is (every? true? (map (fn [flag input] (identical? input (f flag input))) flags inputs)) - (pr-str [real-backedge? context mode]))))) - -(defn selection-tree [depth path] - (if (zero? depth) - (gen/fmap #(list 'do (list 'swap! 'events 'conj [:leaf path]) (list 'quote %)) - (gen/elements [nil false 1000 1.5 :selected "text"])) - (let [test (gen/fmap #(list 'do (list 'swap! 'events 'conj [:test path]) - (list 'mod (list '+ 'input %) 3)) (gen/choose 0 4)) - arm #(selection-tree (dec depth) (conj path %))] - (gen/one-of - [(gen/let [test test a (arm 0) b (arm 1)] (list 'if (list 'zero? test) a b)) - (gen/let [test test a (arm 0) b (arm 1) c (arm 2)] - (list 'let ['choice test] (list 'case 'choice 0 a 1 b c)))])))) - -(defspec nested-joins-preserve-one-continuation {:num-tests 32 :seed 331662} - ;; Java method values after a join retain one class construction site. - (prop/for-all [tree (gen/bind (gen/choose 1 3) #(selection-tree % []))] - (let [body (list 'let ['picked tree] '(swap! events conj :joined) '[picked String/.length]) - form (list 'loophole [] body) - source (fn [body] (list 'fn '[input] body)) - observe (fn [body mode] - (let [events (atom []) f ((backend/compile-program (source body) mode) events) - values (mapv f (range 6))] - [(mapv (fn [[picked f]] [picked (f "abc")]) values) - (identity-pattern (mapv (comp class second) values)) @events])) - expected (observe body :default)] - (and (= :structured (:op (backend/capture #'structure/recover (source form)))) - (every? #(= expected (observe form %)) [:default :dispatcher]))))) diff --git a/test/com/tailrecursion/blacktide/numeric_test.clj b/test/com/tailrecursion/blacktide/numeric_test.clj index 3c66f6b..8cd51c4 100644 --- a/test/com/tailrecursion/blacktide/numeric_test.clj +++ b/test/com/tailrecursion/blacktide/numeric_test.clj @@ -11,6 +11,14 @@ (def modes [:default :generic :dispatcher]) +(deftest numeric-results-retain-observed-origins + (doseq [expression '[(min x x) (max x x) (/ x 1) (/ 1 x)] mode modes + initial [Double/NaN '(double Double/NaN)]] + (let [form (list 'loophole ['x initial 'y '(double 0)] + (list 'setq 'y expression) '(reset! events x) 'y) + f (backend/compile-program form mode) saved (atom nil) value (f saved)] + (is (identical? value @saved) (pr-str [mode initial expression]))))) + (deftest accumulator-recurrence-is-primitive (let [form '(loophole [sum 0] (dotimes [i 10] (when (= i 5) (goto done)) (setq sum (+ sum i))) @@ -62,9 +70,8 @@ (instance? Float value) (Float/floatToIntBits value) (instance? BigDecimal value) (.toString ^BigDecimal value) :else value)]) - (catch Throwable error - ;; Helpful NPE text includes generated local names, which differ by backend. - [(class error) (when-not (instance? NullPointerException error) (ex-message error))]))) + ;; HotSpot may omit built-in exception messages after repeated failures. + (catch Throwable error [(class error)]))) (defn boxed [form] (list (with-meta 'clojure.lang.RT/box {:param-tags [Object]}) form)) diff --git a/test/com/tailrecursion/blacktide/package_test.clj b/test/com/tailrecursion/blacktide/package_test.clj index 720be9e..eefdd65 100644 --- a/test/com/tailrecursion/blacktide/package_test.clj +++ b/test/com/tailrecursion/blacktide/package_test.clj @@ -10,13 +10,14 @@ write! (fn [path value] (let [file (io/file directory path)] (io/make-parents file) (spit file value)))] (try - (doseq [root ["deps.edn" "build.clj" "LICENSE" "src" "src-java"] + (doseq [root ["deps.edn" "build.clj" "LICENSE" "src"] file (file-seq (io/file root)) :when (.isFile file)] (let [target (io/file directory (.getPath file))] (io/make-parents target) (io/copy file target))) (write! "src/com/tailrecursion/.clj-kondo/hooks.clj" "(ns private-editor-hook)") (write! "src/com/tailrecursion/blacktide/.lsp/cache.json" "private editor state") (write! "resources/application.edn" "{:library-resource true}") + (write! "target/classes/stale.class" "unrelated build output") (let [java (str (io/file (System/getProperty "java.home") "bin" "java")) result (shell/sh "clojure" "-T:build" "jar" :dir directory :env (assoc (into {} (System/getenv)) "JAVA_CMD" java))] @@ -25,8 +26,7 @@ (with-open [jar (java.util.jar.JarFile. (io/file directory "target/blacktide-0.1.0-SNAPSHOT.jar"))] (let [names (set (map #(.getName ^java.util.jar.JarEntry %) (enumeration-seq (.entries jar))))] (is (not-any? #(re-find #"(?:^|/)\." %) names)) - (is (every? names ["com/tailrecursion/blacktide.clj" - "com/tailrecursion/blacktide/runtime/KeywordSite.class" - "META-INF/LICENSE" "application.edn"])) + (is (every? names ["com/tailrecursion/blacktide.clj" "META-INF/LICENSE" "application.edn"])) + (is (not-any? #(.endsWith ^String % ".class") names)) (is (not-any? #(re-find #"(?:^|/)(?:test|tools|examples)/" %) names)))))) (finally (doseq [file (reverse (file-seq directory))] (io/delete-file file)))))) diff --git a/test/com/tailrecursion/blacktide/source_loops_test.clj b/test/com/tailrecursion/blacktide/source_loops_test.clj index 4159d51..832129c 100644 --- a/test/com/tailrecursion/blacktide/source_loops_test.clj +++ b/test/com/tailrecursion/blacktide/source_loops_test.clj @@ -18,15 +18,6 @@ (sequential? value) (mapv canonical value) :else value)) -(defn error-message [error] - (let [message (ex-message error)] - ;; Prepared reference conversions use Class.cast. JVM checkcast has different - ;; wording; retain both failing types, not just the exception class. - (if (and message (instance? ClassCastException error)) - (or (some-> (re-matches #"class (\S+) cannot be cast to class (\S+)(?: .*)?" message) rest vec) - (some-> (re-matches #"Cannot cast (\S+) to (\S+)" message) rest vec) - message) - message))) (defn enclose [body mode] ;; The helper functions belong to the caller. Only the loop is the region. (if (#{'let 'clojure.core/let} (first body)) @@ -44,7 +35,7 @@ (if (map? f) f (let [events (atom [])] (try (let [value (f events input n)] {:value (canonical value) :class (class value) :trace (canonical @events)}) - (catch Throwable e {:throw (class e) :message (error-message e) :trace (canonical @events)}))))) + (catch Throwable e {:throw (class e) :trace (canonical @events)}))))) (def bodies '[(let [step (fn ^long [^long x] (unchecked-inc x))] (loop [i 0 x 1000] (if (< i n) (recur (inc i) (step x)) [x (class x) (identical? x x)]))) diff --git a/test/com/tailrecursion/nanopass/annotations.clj b/test/com/tailrecursion/nanopass/annotations.clj index 142b410..8133305 100644 --- a/test/com/tailrecursion/nanopass/annotations.clj +++ b/test/com/tailrecursion/nanopass/annotations.clj @@ -4,11 +4,8 @@ [com.tailrecursion.nanopass :as np] [com.tailrecursion.nanopass.pipeline :as pipeline] [com.tailrecursion.nanopass.fixtures :as f] - [com.tailrecursion.nanopass.passes :as p] - [com.tailrecursion.nanopass.clients.assignment :as assignment])) + [com.tailrecursion.nanopass.passes :as p])) (np/ann-languages f/Source f/Core f/Literals f/Mutual f/Stopped) (np/ann-passes pipeline/map-value) (np/ann-passes p/lower p/lower-binding p/rename p/rename-binding p/count-step p/lower-envelope) -(np/ann-languages assignment/Source assignment/Core) -(np/ann-passes assignment/lower assignment/sequential-writes assignment/parallel-writes) diff --git a/test/com/tailrecursion/nanopass/assignment_test.clj b/test/com/tailrecursion/nanopass/assignment_test.clj deleted file mode 100644 index 918b62c..0000000 --- a/test/com/tailrecursion/nanopass/assignment_test.clj +++ /dev/null @@ -1,64 +0,0 @@ -(ns com.tailrecursion.nanopass.assignment-test - "Execute both grammars against explicit traces and deterministic generated inputs. - The interpreter is test instrumentation; the compiler pass only constructs data." - (:require [clojure.test :refer [deftest is testing]] - [clojure.test.check :as tc] - [clojure.test.check.properties :as prop] - [com.tailrecursion.nanopass.clients.assignment :as lower] - [com.tailrecursion.assignment-model :refer [lit read-var batch sequence-of event execute expression]])) - -(deftest values-and-sequencing - (let [x (read-var 'x) y (read-var 'y)] - (doseq [[name source value vars trace] - [[:empty-sequential (batch :setq) nil {'x 1 'y 2} []] - [:empty-parallel (batch :psetq) nil {'x 1 'y 2} []] - [:sequential (batch :setq 'x (lit 3) 'y x) 3 {'x 3 'y 3} []] - [:swap (batch :psetq 'x y 'y x) nil {'x 2 'y 1} []] - [:repeated (batch :psetq 'x (event :first (lit 9)) 'x (event :second y)) - nil {'x 2 'y 2} [[:first 9] [:second 2]]] - [:nested (batch :psetq 'x y 'y (batch :setq 'x (lit 9))) - nil {'x 2 'y 9} []] - [:nested-parallel (batch :psetq 'x (sequence-of (batch :psetq 'x y 'y x) y) 'y x) - nil {'x 1 'y 2} []] - [:sequential-trace (batch :setq 'x (event :first y) 'y (event :second x)) - 2 {'x 2 'y 2} [[:first 2] [:second 2]]] - [:parallel-trace (batch :psetq 'x (event :first y) 'y (event :second x)) - nil {'x 2 'y 1} [[:first 2] [:second 1]]] - [:branch {:op :if :test (batch :psetq 'x (lit false)) - :then (event :wrong (lit 0)) - :else (batch :setq 'y (event :chosen x))} - false {'x false 'y false} [[:chosen false]]] - [:snapshot {:op :add :left x :right (batch :setq 'x (lit 10))} - 11 {'x 10 'y 2} []] - [:runtime-type (batch :setq 'x (lit "string") 'y x) - "string" {'x "string" 'y "string"} []] - [:quoted-ir (lit {:op :psetq :writes :data}) - {:op :psetq :writes :data} {'x 1 'y 2} []]]] - (testing (str name) - (let [expected {:value value :vars vars :trace trace :status :return}] - (is (= expected (execute source {'x 1 'y 2}))) - (is (= expected (execute (lower/lower source) {'x 1 'y 2})))))))) - -(deftest abandoned-writes - (doseq [op [:setq :psetq] nested? [false true]] - (let [source (batch op 'x (event :first (lit 10)) - 'y (sequence-of - (if nested? (batch :setq 'x (lit 9)) (lit nil)) - {:op :throw :tag :failed})) - expected {:status :throw :tag :failed - :vars {'x (cond nested? 9 (= op :setq) 10 :else 1) 'y 2} - :trace [[:first 10]]}] - (testing (pr-str [op nested?]) - (is (= expected (execute source {'x 1 'y 2}))) - (is (= expected (execute (lower/lower source) {'x 1 'y 2}))))))) - -(deftest generated-assignment-combinations - (let [property (prop/for-all [source (expression 4)] - (let [lowered (lower/lower source)] - (every? true? - (for [[x y] [[0 0] [1 2] [-7 5] [10 -3]] - :let [vars {'x x 'y y}]] - (= (execute source vars) (execute lowered vars))))))] - (doseq [seed [712381 314159]] - (let [result (tc/quick-check 200 property :seed seed)] - (is (:pass? result) (pr-str result)))))) diff --git a/test/com/tailrecursion/nanopass/check.clj b/test/com/tailrecursion/nanopass/check.clj index 5b6892c..0db0ce9 100644 --- a/test/com/tailrecursion/nanopass/check.clj +++ b/test/com/tailrecursion/nanopass/check.clj @@ -7,9 +7,7 @@ [com.tailrecursion.nanopass.pipeline :as pipeline] [com.tailrecursion.nanopass.annotations] [com.tailrecursion.nanopass.fixtures :as f] - [com.tailrecursion.nanopass.passes :as p] - [com.tailrecursion.nanopass.clients.assignment :as assignment] - [com.tailrecursion.assignment-model :as semantics])) + [com.tailrecursion.nanopass.passes :as p])) (defn check-form [form expected] (binding [*ns* (the-ns 'com.tailrecursion.nanopass.check) @@ -60,8 +58,7 @@ (deftest checked-passes (is (t/check-ns-clj 'com.tailrecursion.nanopass.pipeline)) - (is (t/check-ns-clj 'com.tailrecursion.nanopass.passes)) - (is (t/check-ns-clj 'com.tailrecursion.nanopass.clients.assignment))) + (is (t/check-ns-clj 'com.tailrecursion.nanopass.passes))) (deftest pipeline-contracts (doseq [[accepted form] @@ -122,30 +119,3 @@ (macroexpand-1 (cons 'np/defpasses definitions))) form (list head bindings signatures 'expression)] (checks? '[f/Source-Binding :-> f/Core-Binding] form accepted)))) - -(deftest assignment-contract-probes - (doseq [form - '[(fn [node] node) - (fn [_] {:op :psetq :writes []}) - (fn [node] {:op :if :test {:op :const :value true} :then node :else {:op :const :value nil}}) - (fn [_] {:op :write :binding 'x}) - (fn [_] {:op :write :binding 42 :value {:op :const :value nil}}) - (fn [_] {:op :write :binding 'x :value {:op :const :value nil} :extra true}) - (fn [node] (assignment/lower (assignment/lower node))) - (fn [node] (case (:op node) :const node - ((fn [^{:typed.clojure/- typed.clojure/Nothing} missing] - (throw (ex-info "unhandled" {:node missing}))) node)))]] - (checks? '[assignment/Source-Expr :-> assignment/Core-Expr] form false))) - -(deftest structural-types-do-not-prove-order - (let [form '(fn [node] - (if (= :psetq (:op node)) - {:op :do :forms [(assignment/sequential-writes (:writes node)) {:op :const :value nil}]} - (assignment/lower node))) - {:keys [ex delayed-errors]} (check-form form '[assignment/Source-Expr :-> assignment/Core-Expr]) - wrong (binding [*ns* (the-ns 'com.tailrecursion.nanopass.check)] (eval form)) - source (semantics/batch :psetq 'x (semantics/read-var 'y) 'y (semantics/read-var 'x))] - (is (nil? ex) (str ex)) - (is (empty? delayed-errors)) - (is (= {'x 2 'y 2} (:vars (semantics/execute (wrong source) {'x 1 'y 2})))) - (is (= {'x 2 'y 1} (:vars (semantics/execute (assignment/lower source) {'x 1 'y 2})))))) diff --git a/test/com/tailrecursion/nanopass/clients/assignment.clj b/test/com/tailrecursion/nanopass/clients/assignment.clj deleted file mode 100644 index e090ffb..0000000 --- a/test/com/tailrecursion/nanopass/clients/assignment.clj +++ /dev/null @@ -1,46 +0,0 @@ -(ns ^:typed.clojure com.tailrecursion.nanopass.clients.assignment - "A language client lowering sequential/parallel assignment to writes and temps. - This compiler constructs immutable data and has no dependency on Loophole." - (:require [com.tailrecursion.nanopass :as np] - [typed.clojure :as-alias t])) - -(np/deflanguage Source - {Expr (t/U (t/HMap :mandatory {:op ':const :value t/Any} :complete? true) - (t/HMap :mandatory {:op ':read :binding t/Sym} :complete? true) - (t/HMap :mandatory {:op ':throw :tag t/Keyword} :complete? true) - (t/HMap :mandatory {:op ':add :left Expr :right Expr} :complete? true) - (t/HMap :mandatory {:op ':do :forms (t/Vec Expr)} :complete? true) - (t/HMap :mandatory {:op ':if :test Expr :then Expr :else Expr} :complete? true) - (t/HMap :mandatory {:op ':event :tag t/Keyword :value Expr} :complete? true) - (t/HMap :mandatory {:op ':setq :writes Writes} :complete? true) - (t/HMap :mandatory {:op ':psetq :writes Writes} :complete? true)) - Writes (t/Vec (t/HVec [t/Sym Expr]))}) - -(np/derive-language Core Source - {:remove {Expr #{:setq :psetq}} - :add {Expr [(t/HMap :mandatory {:op ':write :binding t/Sym :value Expr} :complete? true) - (t/HMap :mandatory {:op ':temp :id t/Sym} :complete? true) - (t/HMap :mandatory {:op ':bind :id t/Sym :value Expr :body Expr} :complete? true)]}}) - -(np/defpasses - (sequential-writes {:from [Source Writes] :to [Core Expr] :body true} - [writes] - {:op :do :forms (mapv (fn [[target rhs]] {:op :write :binding target :value (lower rhs)}) writes)}) - - (parallel-writes {:from [Source Writes] :to [Core Expr] :body true} - [writes] - (let [names (mapv (fn [_] (gensym "rhs__")) writes) - body {:op :do - :forms (conj (mapv (fn [[target _] id] - {:op :write :binding target :value {:op :temp :id id}}) - writes names) - {:op :const :value nil})}] - (reduce (fn [^{::t/- Core-Expr} body ^{::t/- (t/HVec [t/Sym Source-Expr])} entry] - (let [[id rhs] entry] - {:op :bind :id id :value (lower rhs) :body body})) - body (reverse (mapv (fn [id [_ rhs]] [id rhs]) names writes))))) - - (lower {:from [Source Expr] :to [Core Expr]} - [node] - :setq (sequential-writes (:writes node)) - :psetq (parallel-writes (:writes node)))) diff --git a/test/com/tailrecursion/test_runner.clj b/test/com/tailrecursion/test_runner.clj index abd6467..55c6415 100644 --- a/test/com/tailrecursion/test_runner.clj +++ b/test/com/tailrecursion/test_runner.clj @@ -5,7 +5,6 @@ (def suites '[com.tailrecursion.nanopass.language-test com.tailrecursion.nanopass.pass-test - com.tailrecursion.nanopass.assignment-test com.tailrecursion.blacktide.machine-test com.tailrecursion.blacktide.backend-test com.tailrecursion.blacktide.numeric-test @@ -26,7 +25,7 @@ com.tailrecursion.blacktide.case-test com.tailrecursion.blacktide.lookup-test com.tailrecursion.blacktide.invocation-test - com.tailrecursion.blacktide.native-sites-test + com.tailrecursion.blacktide.interop-values-test com.tailrecursion.blacktide.algorithms-test]) (defn -main [& selected]