| author | Alan Dipert
<alan@dipert.org> 2026-11-06 19:16:59 UTC |
| committer | Alan Dipert
<alan@dipert.org> 2026-11-06 19:16:59 UTC |
| .gitignore | +6 | -0 |
| LICENSE | +21 | -0 |
| README.md | +70 | -0 |
| bin/check | +6 | -0 |
| build.clj | +25 | -0 |
| deps.edn | +14 | -0 |
| doc/blacktide-logo.png | +0 | -0 |
| doc/compiler.md | +42 | -0 |
| doc/language.md | +151 | -0 |
| doc/nanopass.md | +64 | -0 |
| examples/blacktide/algorithms.clj | +51 | -0 |
| src-java/com/tailrecursion/blacktide/runtime/KeywordSite.java | +40 | -0 |
| src/com/tailrecursion/blacktide.clj | +41 | -0 |
| src/com/tailrecursion/blacktide/compiler.clj | +48 | -0 |
| src/com/tailrecursion/blacktide/compiler/analysis_languages.clj | +23 | -0 |
| src/com/tailrecursion/blacktide/compiler/assignment.clj | +123 | -0 |
| src/com/tailrecursion/blacktide/compiler/backend.clj | +26 | -0 |
| src/com/tailrecursion/blacktide/compiler/boundaries.clj | +70 | -0 |
| src/com/tailrecursion/blacktide/compiler/box_origins.clj | +38 | -0 |
| src/com/tailrecursion/blacktide/compiler/case_emit.clj | +25 | -0 |
| src/com/tailrecursion/blacktide/compiler/cases.clj | +24 | -0 |
| src/com/tailrecursion/blacktide/compiler/collections.clj | +71 | -0 |
| src/com/tailrecursion/blacktide/compiler/compact.clj | +197 | -0 |
| src/com/tailrecursion/blacktide/compiler/compact_languages.clj | +24 | -0 |
| src/com/tailrecursion/blacktide/compiler/effects.clj | +28 | -0 |
| src/com/tailrecursion/blacktide/compiler/emit.clj | +77 | -0 |
| src/com/tailrecursion/blacktide/compiler/errors.clj | +27 | -0 |
| src/com/tailrecursion/blacktide/compiler/feasible.clj | +22 | -0 |
| src/com/tailrecursion/blacktide/compiler/graph.clj | +143 | -0 |
| src/com/tailrecursion/blacktide/compiler/host_emit.clj | +91 | -0 |
| src/com/tailrecursion/blacktide/compiler/invariants.clj | +28 | -0 |
| src/com/tailrecursion/blacktide/compiler/joins.clj | +71 | -0 |
| src/com/tailrecursion/blacktide/compiler/jvm.clj | +217 | -0 |
| src/com/tailrecursion/blacktide/compiler/jvm_languages.clj | +35 | -0 |
| src/com/tailrecursion/blacktide/compiler/languages.clj | +54 | -0 |
| src/com/tailrecursion/blacktide/compiler/loop_facts.clj | +186 | -0 |
| src/com/tailrecursion/blacktide/compiler/lower.clj | +145 | -0 |
| src/com/tailrecursion/blacktide/compiler/native.clj | +108 | -0 |
| src/com/tailrecursion/blacktide/compiler/order.clj | +116 | -0 |
| src/com/tailrecursion/blacktide/compiler/prepare.clj | +46 | -0 |
| src/com/tailrecursion/blacktide/compiler/source.clj | +414 | -0 |
| src/com/tailrecursion/blacktide/compiler/source_case.clj | +42 | -0 |
| src/com/tailrecursion/blacktide/compiler/source_host.clj | +72 | -0 |
| src/com/tailrecursion/blacktide/compiler/source_jvm.clj | +149 | -0 |
| src/com/tailrecursion/blacktide/compiler/source_languages.clj | +93 | -0 |
| src/com/tailrecursion/blacktide/compiler/source_values.clj | +36 | -0 |
| src/com/tailrecursion/blacktide/compiler/storage.clj | +100 | -0 |
| src/com/tailrecursion/blacktide/compiler/structure.clj | +163 | -0 |
| src/com/tailrecursion/blacktide/compiler/structured_emit.clj | +110 | -0 |
| src/com/tailrecursion/blacktide/compiler/structured_languages.clj | +34 | -0 |
| src/com/tailrecursion/blacktide/compiler/transfers.clj | +34 | -0 |
| src/com/tailrecursion/blacktide/compiler/value_facts.clj | +65 | -0 |
| src/com/tailrecursion/blacktide/runtime.clj | +7 | -0 |
| src/com/tailrecursion/nanopass.clj | +86 | -0 |
| src/com/tailrecursion/nanopass/language.clj | +170 | -0 |
| src/com/tailrecursion/nanopass/pass.clj | +140 | -0 |
| test-resources/typedclojure_config.cljc | +1 | -0 |
| test/com/tailrecursion/blacktide/algorithms_test.clj | +59 | -0 |
| test/com/tailrecursion/blacktide/annotations.clj | +138 | -0 |
| test/com/tailrecursion/blacktide/aot_fixture.clj | +23 | -0 |
| test/com/tailrecursion/blacktide/aot_test.clj | +42 | -0 |
| test/com/tailrecursion/blacktide/assignment_test.clj | +107 | -0 |
| test/com/tailrecursion/blacktide/backend_test.clj | +311 | -0 |
| test/com/tailrecursion/blacktide/case_test.clj | +225 | -0 |
| test/com/tailrecursion/blacktide/check.clj | +86 | -0 |
| test/com/tailrecursion/blacktide/collections_test.clj | +135 | -0 |
| test/com/tailrecursion/blacktide/control_test.clj | +80 | -0 |
| test/com/tailrecursion/blacktide/fixtures/HostFixture.java | +10 | -0 |
| test/com/tailrecursion/blacktide/generated_test.clj | +145 | -0 |
| test/com/tailrecursion/blacktide/graph_test.clj | +109 | -0 |
| test/com/tailrecursion/blacktide/invocation_test.clj | +142 | -0 |
| test/com/tailrecursion/blacktide/jumps_test.clj | +116 | -0 |
| test/com/tailrecursion/blacktide/jvm_fixture.clj | +18 | -0 |
| test/com/tailrecursion/blacktide/jvm_test.clj | +233 | -0 |
| test/com/tailrecursion/blacktide/lookup_test.clj | +199 | -0 |
| test/com/tailrecursion/blacktide/loops_test.clj | +220 | -0 |
| test/com/tailrecursion/blacktide/machine_test.clj | +98 | -0 |
| test/com/tailrecursion/blacktide/native_sites_test.clj | +101 | -0 |
| test/com/tailrecursion/blacktide/package_test.clj | +32 | -0 |
| test/com/tailrecursion/blacktide/source_fixture.clj | +7 | -0 |
| test/com/tailrecursion/blacktide/source_loops_test.clj | +241 | -0 |
| test/com/tailrecursion/blacktide/source_test.clj | +179 | -0 |
| test/com/tailrecursion/blacktide/source_values_test.clj | +42 | -0 |
| test/com/tailrecursion/blacktide/subset_test.clj | +52 | -0 |
| test/com/tailrecursion/check_runner.clj | +11 | -0 |
| test/com/tailrecursion/nanopass/annotations.clj | +17 | -0 |
| test/com/tailrecursion/nanopass/assignment_test.clj | +173 | -0 |
| test/com/tailrecursion/nanopass/check.clj | +123 | -0 |
| test/com/tailrecursion/nanopass/clients/assignment.clj | +48 | -0 |
| test/com/tailrecursion/nanopass/clients/flow.clj | +112 | -0 |
| test/com/tailrecursion/nanopass/clients/flow_languages.clj | +35 | -0 |
| test/com/tailrecursion/nanopass/fixtures.clj | +29 | -0 |
| test/com/tailrecursion/nanopass/flow_test.clj | +101 | -0 |
| test/com/tailrecursion/nanopass/language_test.clj | +55 | -0 |
| test/com/tailrecursion/nanopass/pass_test.clj | +64 | -0 |
| test/com/tailrecursion/nanopass/passes.clj | +40 | -0 |
| test/com/tailrecursion/test_runner.clj | +37 | -0 |
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5ba17be --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.cpcache/ +.clj-kondo/ +.lsp/ +target/ +*.class +__pycache__/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..16be1d1 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Alan Dipert + +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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..6997247 --- /dev/null +++ b/README.md @@ -0,0 +1,70 @@ +<p align="center"> + <img src="doc/blacktide-logo.png" alt="Blacktide: moonlight over ocean waves" width="240"> +</p> + +# Blacktide + +A Clojure-to-Clojure compiler library. Its `loophole` macro provides mutable +lexical locals and explicit jumps for algorithmic code. It compiles to ordinary +Clojure, without mutable cells or a runtime interpreter for local state. + +```clojure +(require '[com.tailrecursion.blacktide :refer [loophole setq psetq label goto]]) + +(loophole [i 0 total 0] + (label again) + (when (< i 5) + (setq total (+ total i) i (inc i)) + (goto again)) + total) ; => 10 + +(loophole [x 1 y 2] + (psetq x y y x) + [(setq x (+ x 10)) y]) ; => [12 1] +``` + +`setq` assigns pairs sequentially and returns the last value. `psetq` evaluates +all right-hand sides before assigning and returns nil. Both work in expressions. +Labels belong to the outer statement sequence; jumps can leave nested statement +bodies. `(goto end)` exits with nil. + +The region supports `let`, destructuring, `if`, `case`, `loop`/`recur`, calls, +collections and Java interop. Ordinary macros such as `when`, `cond`, `dotimes` +and `doseq` work through expansion. Define functions and protect resources +outside the region. Functions, `letfn`, `try`, native `set!` and nested `loophole` +forms are rejected inside it, including through macroexpansion. + +Read the [language reference](doc/language.md) for the exact boundary and +[algorithm examples](examples/blacktide/algorithms.clj) for partitioning and parsing. +This is a control-flow tool; it makes no general speedup guarantee. + +## Use + +Requires Clojure 1.12.6 and JDK 17 or later. From a neighboring checkout: + +```clojure +{:deps {com.tailrecursion/blacktide {:local/root "../loophole"}}} +``` + +Run `clojure -X:deps prep` in the client project to compile the bundled JVM +keyword-cache helper. Library JARs include it. Ordinary use depends only on +Clojure; Typed Clojure checks compiler passes during development. + +## Develop + +```sh +clojure -T:build compile-java # after checkout or changes to src-java +clojure -M:test # semantic, generative, differential and AOT tests +clojure -M:check # language contracts and production pass bodies +bin/check # all of the above; accepts CLI -S options +clojure -T:build jar # 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. Generators use deterministic seeds, +independent models and shrinking; failures report their seed and source. + +See the [compiler architecture](doc/compiler.md) and [nanopass guide](doc/nanopass.md) +for implementation contracts and extension guidance. + +Copyright © 2026 Alan Dipert. Distributed under the [MIT License](LICENSE). diff --git a/bin/check b/bin/check new file mode 100755 index 0000000..f6bf105 --- /dev/null +++ b/bin/check @@ -0,0 +1,6 @@ +#!/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 new file mode 100644 index 0000000..53402b1 --- /dev/null +++ b/build.clj @@ -0,0 +1,25 @@ +(ns build + "Build the JVM helper and a source library JAR; the compiler stays in Clojure." + (: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}) + ;; Editor state may be nested beneath namespace directories. + (doseq [file (file-seq (io/file class-dir)) + :when (.startsWith (.getName file) ".")] + (b/delete {:path (.getPath file)})) + (b/copy-file {:src "LICENSE" :target (str class-dir "/META-INF/LICENSE")}) + (b/write-pom {:lib lib :version version :basis basis :class-dir class-dir + :src-dirs ["src"]}) + (b/jar {:class-dir class-dir :jar-file (str "target/blacktide-" version ".jar")}))) diff --git a/deps.edn b/deps.edn new file mode 100644 index 0000000..c18a050 --- /dev/null +++ b/deps.edn @@ -0,0 +1,14 @@ +{:paths ["src" "resources" "target/classes"] + :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} + :test {:extra-paths ["test" "examples"] + :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"}} + :jvm-opts ["-Xmx768m"] + :main-opts ["-m" "com.tailrecursion.check-runner"]}}} diff --git a/doc/blacktide-logo.png b/doc/blacktide-logo.png new file mode 100644 index 0000000..83dc7a3 Binary files /dev/null and b/doc/blacktide-logo.png differ diff --git a/doc/compiler.md b/doc/compiler.md new file mode 100644 index 0000000..f2102e5 --- /dev/null +++ b/doc/compiler.md @@ -0,0 +1,42 @@ +# Compiler architecture + +Blacktide transforms immutable Clojure data through languages defined with +`com.tailrecursion.nanopass`. Typed Clojure checks production pass bodies; the +checker is absent from ordinary library use. Namespace docstrings state pass +contracts. Compiler namespaces and intermediate representations are internal. + +| Stage | Responsibility | +| --- | --- | +| Source | Expand macros in order, resolve unique lexical identities and source JVM facts, enforce the subset, solve loop representations without repeating expansion. | +| Classification | Preserve analyzed loops without DSL effects and native keyword calls; materialize representations for loops requiring lowering. | +| Normalization | Lower sequential/parallel assignments, case and collections; prepare conversions and save operands in evaluation order. | +| Graph | Resolve outer labels, check destinations and definite initialization, compute liveness and allocate slots. | +| Backend | Fold literal branches, recover structured control with shared joins, choose conservative primitive storage and compact safe intermediates. Fall back to the dispatcher when recovery is ineligible. | +| Emission | Emit Clojure with original conversions, construction sites and an opaque Object result. | + +`compiler/compile-form` orchestrates: + +`Bound → Classified → Sequential → Assigned → Cases → Collections → Prepared → Ordered → Symbolic → CFG → Machine → Code`. + +The outer bindings enter as one lexical scope. `goto end` directly returns nil; +emission seals the whole result with an Object type boundary. `Native` is the +analyzed subtree language without DSL operations. The structured backend uses +`Machine → Planned → Joined → Stored → Compact → Code` and shares graph +validation with the dispatcher. + +Assignments, normalization and graph construction are pure. Locals become +lexical bindings and loop parameters, never mutable cells. A generated +`RT.box(Object)` is an identity operation that preserves an inference boundary; +its appearance alone does not imply allocation. + +Java method values, functional-interface adapters and keyword sites must not be +duplicated during control recovery. Keyword calls with effectful targets save +the cache thunk before evaluating the target. `runtime.KeywordSite` implements +that native cache protocol; an internal macro allocates each site during final +Clojure analysis. The Java helper targets JDK 17 and is bundled in JARs. + +To extend the compiler, change the language and affected passes, then run +`bin/check`. Prefer a compiler-local transformation over new nanopass machinery. +Tests should establish source semantics or a necessary invariant. Generators +use independent models or ordinary Clojure, deterministic seeds and shrinking; +backend comparisons supplement those independent references. diff --git a/doc/language.md b/doc/language.md new file mode 100644 index 0000000..a107484 --- /dev/null +++ b/doc/language.md @@ -0,0 +1,151 @@ +# Language reference + +The public namespace is `com.tailrecursion.blacktide`; its macros are `loophole`, +`setq`, `psetq`, `label` and `goto`. Qualified calls and namespace aliases work. +Lexical bindings can shadow macro names. DSL markers have no meaning outside +`loophole` and report an error there. + +## Bindings and assignment + +`loophole` takes an even vector of simple symbols and initializers, followed by +a body. Initializers run once, sequentially, and may read or assign earlier +bindings. The result is the last body value, or nil for an empty body. + +```clojure +(require '[com.tailrecursion.blacktide :refer [loophole setq psetq label goto]]) +(loophole [x 1 y x] + (let [x 3] [x y])) ; => [3 1] + +(loophole [x 1 y 2] + (psetq x y y x) + [(setq x 10) y]) ; => [10 1] +``` + +`setq` writes each pair immediately and returns its last value (nil for no +pairs). `psetq` evaluates every RHS before writing targets, in pair order, and +returns nil. Repeated targets are written in order. Nested assignments take +effect during RHS evaluation; an exception abandons pending parallel writes. +Only bindings introduced by `loophole` are assignable. Shadowing with an ordinary +`let` makes the inner binding immutable. Native `set!` is not admitted. + +## Statements and jumps + +Labels are simple symbols in the **outer statement sequence**, including nested +transparent `do` forms after macroexpansion. They cannot appear inside `let`, +`loop`, conditional arms, operands or initializers. Names are unique within the +region; `end` is reserved. + +`goto` is a statement. It can occur in `if`/`case` arms and `let`/`loop` bodies +and jump out to an outer label. It is forbidden anywhere within an operand or +initializer, even a discarded expression nested in `do`. `(goto end)` returns +nil immediately. A normal final result can be obtained with a final label: + +```clojure +(loophole [x 0] + (label again) + (let [next (inc x)] + (setq x next) + (when (< x 3) (goto again))) + (goto done) + (setq x 99) + (label done) x) ; => 3 + +(loophole [] (goto end) :unreachable) ; => nil +``` + +Backward jumps rerun initializers they encounter. They cannot enter a lexical +scope or bypass its initialization. These forms are errors: + +```clojure +(loophole [] (let [] (label inside))) +(loophole [] (+ 1 (do (goto end) 2))) +(loophole [x (goto end)] x) +``` + +## Clojure subset + +Supported forms are literals, quote/var, symbol reads, function calls, collection +literals and their executable metadata, `do`, `if`, `throw`, `let`, `case`, +`loop`/`recur`, Java calls, constructors, field reads and arrays. Ordinary `let` +and `loop` support destructuring. Calls may use external function values; +called code can use unrestricted Clojure. + +Macros are expanded in the caller namespace with lexical shadowing and visibility +checks. `&env` provides binding names, hints and inferred immutable types, not +private `Compiler$LocalBinding` objects. Common admitted macros include `when`, +`when-not`, `cond`, `and`, `or`, `if-let`, `when-let`, threading, `dotimes`, `while` +and `doseq`. A macro is supported only when its entire expansion is admitted. +Quoted data is never analyzed as code. + +`fn`/`fn*`, `letfn`/`letfn*`, `try`/`catch`/`finally`, monitor operations, native +`set!`, nested `loophole`, namespace/Var/type definitions and `reify` are rejected. +This also excludes `delay`, `lazy-seq`, `future`, `binding`, `locking`, `with-open` +and `with-redefs` **inside** the region. Put helpers and dynamic extent outside: + +```clojure +(letfn [(step [n] (inc n))] + (loophole [x 0] + (dotimes [_ 3] (setq x (step x))) + x)) ; => 3 + +(try + (loophole [] (throw (Exception. "example"))) + (catch Exception e (.getMessage e))) ; => "example" + +(loophole [total 0] + (dotimes [i 4] (dotimes [j i] (setq total (+ total j)))) + total) ; => 4 +``` + +Original `recur` keeps its local loop target, tail-position requirement and arity. +Arguments evaluate before rebinding. Source primitive inference is preserved, +including int→long and float→double initializer promotion and incompatible +backedges that require boxing. Even unreachable recur contributes to inference. +A recur cannot target a loop outside the region. + +`case` preserves Clojure's constant matching, grouping, hash collisions and +missing-default exception. Dispatch executes once; only the selected arm runs. +Each source arm is analyzed once in written order. Arms may assign or recur, +and may jump when the case itself is a statement. + +```clojure +(loophole [x 0] + (case (do (setq x (inc x)) x) + 1 (setq x 10) + 2 (setq x 20) + (setq x 30)) + x) ; => 10 +``` + +## JVM semantics and limits + +Source facts determine overloads and conversion order. Clojure 1.12 qualified +method values, functional-interface adaptation and `:param-tags` are supported. +Arithmetic respects `*unchecked-math*`; calls respect `:direct-linking` and direct +protocol dispatch. Unreachable code still receives syntax and JVM compilation +checks. Very large regions remain subject to JVM method-size limits. + +Mutable locals retain reference identity, including through assignment and +immutable snapshots. Ordinary primitive locals retain Clojure's semantics: +separate Object uses may box separately. Literal pooling is unspecified; +independently 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. + +```clojure +(let [token (Object.)] + (loophole [x token y nil] + (psetq y x x x) + (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. + +Explicit reference conversions may use `Class.cast`, whose exception message +differs from JVM `checkcast`. Invalid protocol fallback roots can also have +different generated error text. Failing types and evaluation order are preserved. +DSL errors carry `:kind`, `:form`, `:file`, available `:line`/`:column`, and relevant +binding or label data. Clojure normally wraps them in `CompilerException`. diff --git a/doc/nanopass.md b/doc/nanopass.md new file mode 100644 index 0000000..bb1e150 --- /dev/null +++ b/doc/nanopass.md @@ -0,0 +1,64 @@ +# Nanopass library + +Define languages from Typed Clojure syntax without loading the +checker. Local nonterminals are rebound recursively when deriving a language: + +```clojure +(require '[com.tailrecursion.nanopass :as np] + '[typed.clojure :as-alias t]) + +(np/deflanguage Source + {Expr (t/U (t/HMap :mandatory {:op ':literal :value t/Any} :complete? true) + (t/HMap :mandatory {:op ':negate :value Expr} :complete? true))}) + +(np/derive-language Core Source + {:remove {Expr #{:negate}} + :add {Expr [(t/HMap :mandatory {:op ':subtract :left Expr :right Expr} + :complete? true)]}}) +``` + +Descriptions are immutable values. `:replace` replaces or adds a complete +nonterminal definition. Structural types support `U`, closed keyword-field +`HMap`, fixed `HVec`, `Vec`, `Map`, `Set`, and `Option`. Other types are opaque +terminals and cannot hide local nonterminal references. Quoted singleton data +and `Val`/`Value` payloads stay untouched. Tagged productions have a required +`:op` with a singleton keyword type. `clojure.core.typed` spelling is accepted too. +Use explicit `HMap`/`HVec` constructors rather than their quoted collection +shorthand. Declare complex polymorphic terminal types as external aliases. + +An annotation-only namespace can require `typed.clojure` and call +`(np/ann-languages Source Core)` to generate aliases `Source-Expr` and `Core-Expr` +in the languages' defining namespace. Nothing registers types during ordinary +language loading. + +Passes supply clauses for changed productions and inherit checked structural +traversal for unchanged ones: + +```clojure +(np/defpass lower {:from [Source Expr] :to [Core Expr]} + [node] + :negate {:op :subtract + :left {:op :literal :value 0} + :right (lower (:value node))}) +``` + +Register `(np/ann-passes lower)` in the checking-only namespace, then check the +implementation namespace with `typed.clojure/check-ns-clj`. Generated signatures +are contracts, not a replacement for checking function bodies. + +`:processors {Binding lower-binding}` names other nonterminal processors; use +ordinary `declare` for mutual recursion. `:args [ContextType]` types leading +arguments, which generated traversal forwards unchanged. Write explicit clauses +when lexical context changes. With `:body true`, supply one ordinary expression +and any result type in `:to`, including state/result tuples. Automatic clauses +require matching fields and structural containers; removed productions and +incompatible fields require explicit code. Missing cases are checked using a +generated bottom-type guard. `macroexpand-1` exposes the complete generated code. +Use qualified aliases such as `languages/Core-Expr` in `:args` and explicit +result types; checking-only aliases need not exist as runtime Vars. + +Structural traversal preserves metadata and optional-field presence, including +present nil values, and shares unchanged fields. Quoted payloads remain opaque. Map/set traversal is structural; +use vectors and explicit clauses when order or scope has semantic significance. + +Run `bin/check` from the repository root to check pass bodies as well as generated contracts. diff --git a/examples/blacktide/algorithms.clj b/examples/blacktide/algorithms.clj new file mode 100644 index 0000000..24a03d4 --- /dev/null +++ b/examples/blacktide/algorithms.clj @@ -0,0 +1,51 @@ +(ns blacktide.algorithms + "Small parsers and an in-place Hoare partition using mutable lexical locals." + (:require [com.tailrecursion.blacktide :refer [loophole label goto setq]])) + +(defn tokens [^String s] + (loophole [i 0 word "" out []] + (label scan) + (when (= i (count s)) (goto flush)) + (let [c (.charAt s i)] + (setq i (inc i)) + (if (Character/isWhitespace c) + (do (when (seq word) (setq out (conj out word))) + (setq word "")) + (setq word (str word c)))) + (goto scan) + (label flush) + (when (seq word) (setq out (conj out word))) + out)) + +(defn partition! [^longs a] + (when (pos? (alength a)) + (let [pivot (aget a 0)] + (loophole [left -1 right (alength a)] + (label scan) + (setq left (inc left)) + (while (< (aget a left) pivot) (setq left (inc left))) + (setq right (dec right)) + (while (> (aget a right) pivot) (setq right (dec right))) + (when (>= left right) (goto done)) + (let [v (aget a left)] (aset-long a left (aget a right)) (aset-long a right v)) + (goto scan) + (label done) right)))) + +(defn decimal-prefix [^String s] + (loophole [i 0 sign 1 value 0N start 0] + (when (empty? s) (goto end)) + (case (.charAt s 0) + \- (setq sign -1 i 1) + \+ (setq i 1) + nil) + (setq start i) + (label digit) + (when (= i (count s)) (goto done)) + (let [c (.charAt s i)] + (when-not (<= (int \0) (int c) (int \9)) (goto done)) + (setq value (+ (* value 10) (- (int c) (int \0))) + i (inc i))) + (goto digit) + (label done) + (when (= i start) (goto end)) + [(* sign value) i])) diff --git a/src-java/com/tailrecursion/blacktide/runtime/KeywordSite.java b/src-java/com/tailrecursion/blacktide/runtime/KeywordSite.java new file mode 100644 index 0000000..189e645 --- /dev/null +++ b/src-java/com/tailrecursion/blacktide/runtime/KeywordSite.java @@ -0,0 +1,40 @@ +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.clj b/src/com/tailrecursion/blacktide.clj new file mode 100644 index 0000000..ab04ce0 --- /dev/null +++ b/src/com/tailrecursion/blacktide.clj @@ -0,0 +1,41 @@ +(ns com.tailrecursion.blacktide + "Blacktide's loophole macro adds mutable lexical locals and statement jumps + to a bounded subset of Clojure, compiled through typed nanopasses." + (:require [com.tailrecursion.blacktide.compiler.errors :as errors] + [com.tailrecursion.blacktide.compiler :as compiler])) + +(defmacro label + "Mark a position in the outer statement sequence of a loophole region." + [& _] + (errors/fail :outside-region "label is only valid inside loophole" {} + (errors/site {:form &form :file *file* :line nil :column nil} &form))) + +(defmacro goto + "Jump from a statement to an outer label; (goto end) returns nil." + [& _] + (errors/fail :outside-region "goto is only valid inside loophole" {} + (errors/site {:form &form :file *file* :line nil :column nil} &form))) + +(defmacro setq + "Assign symbol/value pairs sequentially; return the last value, or nil." + [& _] + (errors/fail :outside-region "setq is only valid inside loophole" {} + (errors/site {:form &form :file *file* :line nil :column nil} &form))) + +(defmacro psetq + "Evaluate symbol/value pairs before assigning their targets; return nil." + [& _] + (errors/fail :outside-region "psetq is only valid inside loophole" {} + (errors/site {:form &form :file *file* :line nil :column nil} &form))) + +(defmacro loophole + "Initialize simple-symbol bindings sequentially; make them mutable through + setq/psetq. Return the last body value, or nil. Labels belong to the outer + statement sequence; goto is forbidden within operands and initializers. + Supports let, loop/recur, case, calls, collections, Java interop and macros + expanding into these forms. Functions, try, native set! and nested regions + must be outside the macro. See doc/language.md for the complete contract." + [bindings & body] + (compiler/compile-form (or &env {}) + {#'loophole :loophole #'label :label #'goto :goto #'setq :setq #'psetq :psetq} + &form)) diff --git a/src/com/tailrecursion/blacktide/compiler.clj b/src/com/tailrecursion/blacktide/compiler.clj new file mode 100644 index 0000000..21507eb --- /dev/null +++ b/src/com/tailrecursion/blacktide/compiler.clj @@ -0,0 +1,48 @@ +(ns ^:typed.clojure com.tailrecursion.blacktide.compiler + "Orchestrate source analysis, lowering, graph checks and emission. The public + macro supplies marker Vars and the caller's lexical environment." + (:require [com.tailrecursion.blacktide.compiler.errors :as errors] + [com.tailrecursion.nanopass :as np] + [com.tailrecursion.blacktide.compiler.source-languages :as s] + [com.tailrecursion.blacktide.compiler.languages :as l] + [com.tailrecursion.blacktide.compiler.source :as source] + [com.tailrecursion.blacktide.compiler.source-jvm :as facts] + [com.tailrecursion.blacktide.compiler.boundaries :as boundaries] + [com.tailrecursion.blacktide.compiler.cases :as cases] + [com.tailrecursion.blacktide.compiler.assignment :as assignment] + [com.tailrecursion.blacktide.compiler.source-values :as source-values] + [com.tailrecursion.blacktide.compiler.collections :as collections] + [com.tailrecursion.blacktide.compiler.order :as order] + [com.tailrecursion.blacktide.compiler.prepare :as prepare] + [com.tailrecursion.blacktide.compiler.lower :as lower] + [com.tailrecursion.blacktide.compiler.transfers :as transfers] + [com.tailrecursion.blacktide.compiler.graph :as graph] + [com.tailrecursion.blacktide.compiler.backend :as backend] + [com.tailrecursion.blacktide.compiler.host-emit :as host] + [typed.clojure :as-alias t])) + +(np/defpass compile-tree {:from [s/Bound Expr] :to [l/Machine Code] :args [s/Bound-Supply] :body true} + [supply expr] + (let [[supply expr] (assignment/parallel supply (assignment/sequential (source-values/expression (boundaries/classify expr)))) + [supply expr] (order/normalize supply (prepare/prepare (collections/normalize (cases/normalize expr)))) + program (graph/analyze (transfers/resolve-program (lower/compile-graph supply expr)))] + (host/box-form (backend/compile-program program)))) + +(np/defpass caller-locals + {:from [s/Bound Context] :to [s/Bound Context] :body true} + [ctx] + (assoc ctx :locals + (reduce-kv (fn [^{::t/- (t/Map t/Sym s/Bound-Local)} out ^{::t/- t/Sym} name ^{::t/- t/Any} binding] + (assoc out name {:id name :name name :mutable? false :external? true + :jvm (facts/caller-facts name binding)})) {} (:macro-env ctx)))) + +(np/defpass compile-form + {:from [s/Bound Raw] :to [l/Machine Code] + :args [(t/Map t/Sym t/Any) (t/Map t/AnyVar t/Kw)] :body true} + [env markers form] + (let [_ (assert (seq? form) "Expected a loophole macro form") + site (errors/site {:form form :file *file* :line nil :column nil} form) + ctx (caller-locals {:locals {} :macro-env env :markers markers :outer? true :transfer? true + :recur nil :site site :statement? true :tail? false}) + [supply expr] (source/parse-scope {:prefix (str (gensym "loophole__")) :next 0} ctx true (vec form))] + (compile-tree supply expr))) diff --git a/src/com/tailrecursion/blacktide/compiler/analysis_languages.clj b/src/com/tailrecursion/blacktide/compiler/analysis_languages.clj new file mode 100644 index 0000000..9fd8408 --- /dev/null +++ b/src/com/tailrecursion/blacktide/compiler/analysis_languages.clj @@ -0,0 +1,23 @@ +(ns com.tailrecursion.blacktide.compiler.analysis-languages + "Value proofs and join plans for checked control-flow graphs. Bottom is an + unresolved copy cycle; boxed values have no primitive storage proof." + (:require [com.tailrecursion.nanopass :as np] + [com.tailrecursion.blacktide.compiler.languages :as l] + [typed.clojure :as-alias t])) + +(np/deflanguage Analysis + {Type (t/U ':bottom ':boxed java.lang.Class) + Types (t/Map t/Sym Type) + Definitions (t/Map t/Sym (t/Vec l/Machine-Rhs)) + Program (t/HMap :mandatory {:machine l/Machine-Program :types Types + :reachable (t/Set l/Machine-Label)} :complete? true)}) + +(np/derive-language Planned Analysis + {:replace + {Postdominators (t/Map l/Machine-Label (t/Set l/Machine-Label)) + Region (t/HMap :mandatory {:definite (t/Set t/Sym) :changed (t/Set t/Sym)} :complete? true) + Regions (t/Map l/Machine-Label Region) + Join (t/HMap :mandatory {:target l/Machine-Label :result t/Sym} :complete? true) + Joins (t/Map l/Machine-Label Join) + Program (t/HMap :mandatory {:machine l/Machine-Program :types Types + :reachable (t/Set l/Machine-Label) :joins Joins} :complete? true)}}) diff --git a/src/com/tailrecursion/blacktide/compiler/assignment.clj b/src/com/tailrecursion/blacktide/compiler/assignment.clj new file mode 100644 index 0000000..f79276e --- /dev/null +++ b/src/com/tailrecursion/blacktide/compiler/assignment.clj @@ -0,0 +1,123 @@ +(ns ^:typed.clojure com.tailrecursion.blacktide.compiler.assignment + "Lower sequential batches to writes, then parallel batches to saved RHSs and + writes. Pending parallel writes do not execute if an RHS abandons evaluation." + (:require [com.tailrecursion.nanopass :as np] + [com.tailrecursion.blacktide.compiler.source-languages :as l] + [com.tailrecursion.blacktide.compiler.source :as source] + [typed.clojure :as-alias t])) + +(declare sequential sequential-binding sequential-pair sequential-forms sequential-pairs parallel) + +(np/defpass sequential-pair + {:from [l/Classified Assignment] :to [l/Sequential Assignment] + :processors {Local clojure.core/identity Expr sequential}} + [pair]) + +(np/defpass sequential-pairs + {:from [l/Classified Assignments] :to [l/Sequential Assignments] :processors {Assignment sequential-pair}} + [pairs]) + +(np/defpass sequential-forms + {:from [l/Classified Exprs] :to [l/Sequential Exprs] :processors {Expr sequential}} + [forms]) + +(np/defpass sequential-node + {:from [l/Classified Node] :to [l/Sequential Node] + :processors {Local clojure.core/identity Raw clojure.core/identity CollectionKind clojure.core/identity RecurTarget clojure.core/identity CaseSpec clojure.core/identity + Expr sequential Exprs sequential-forms Binding sequential-binding Assignments sequential-pairs}} + [node] + :setq {:op :do :forms + (mapv (fn [pair] + (let [value (sequential (:value pair))] + (assoc value :node {:op :write :target (:target pair) :value value}))) (:pairs node))}) + +(np/defpass sequential-binding + {:from [l/Classified Binding] :to [l/Sequential Binding] + :processors {Local clojure.core/identity Expr sequential}} + [binding]) + +(np/defpass sequential + {:from [l/Classified Expr] :to [l/Sequential Expr] :body true} + [expr] + (assoc expr :node (sequential-node (:node expr)))) + +(np/defpass parallel-forms + {:from [l/Sequential Exprs] :to (t/HVec [l/Bound-Supply l/Assigned-Exprs]) + :args [l/Bound-Supply] :body true} + [supply forms] + (reduce (fn [^{::t/- (t/HVec [l/Bound-Supply l/Assigned-Exprs])} acc ^{::t/- l/Sequential-Expr} expr] + (let [[supply out] acc [supply expr] (parallel supply expr)] [supply (conj out expr)])) + [supply []] forms)) + +(np/defpass parallel-bindings + {:from [l/Sequential Bindings] :to (t/HVec [l/Bound-Supply l/Assigned-Bindings]) + :args [l/Bound-Supply] :body true} + [supply bindings] + (reduce (fn [^{::t/- (t/HVec [l/Bound-Supply l/Assigned-Bindings])} acc ^{::t/- l/Sequential-Binding} binding] + (let [[supply out] acc [supply init] (parallel supply (:init binding))] + [supply (conj out (assoc binding :init init))])) [supply []] bindings)) + +(np/defpass save-parallel-values + {:from [l/Sequential Assignments] + :to (t/HVec [l/Bound-Supply l/Assigned-Bindings l/Assigned-Exprs]) :args [l/Bound-Supply] :body true} + [supply pairs] + (reduce (fn [^{::t/- (t/HVec [l/Bound-Supply l/Assigned-Bindings l/Assigned-Exprs])} acc + ^{::t/- l/Sequential-Assignment} pair] + (let [[supply bindings writes] acc [supply value] (parallel supply (:value pair)) + [supply id] (source/fresh "parallel" supply) + local {:id id :name id :mutable? false :external? false :jvm (:jvm value)} + read (assoc value :node {:op :local :local local}) + write (assoc value :node {:op :write :target (:target pair) :value read})] + [supply (conj bindings {:local local :init value}) (conj writes write)])) [supply [] []] pairs)) + +(np/defpass parallel-node + {:from [l/Sequential Node] :to (t/HVec [l/Bound-Supply l/Assigned-Node]) :args [l/Bound-Supply]} + [supply node] + :literal [supply node] + :local [supply node] + :global [supply node] + :var [supply node] + :label [supply node] + :goto [supply node] + :native [supply node] + :dispatch (let [[supply value] (parallel supply (:value node))] + [supply {:op :dispatch :value value :spec (:spec node)}]) + :case (let [[supply selector] (parallel supply (:selector node)) + [supply arms] (parallel-forms supply (:arms node))] + [supply {:op :case :selector selector :arms arms}]) + :recur (let [[supply args] (parallel-forms supply (:args node))] + [supply {:op :recur :target (:target node) :args args}]) + :loop (let [[supply bindings] (parallel-bindings supply (:bindings node)) + [supply body] (parallel supply (:body node))] + [supply {:op :loop :id (:id node) :bindings bindings :body body}]) + :if (let [[supply test] (parallel supply (:test node)) + [supply yes] (parallel supply (:then node)) + [supply no] (parallel supply (:else node))] + [supply {:op :if :test test :then yes :else no}]) + :throw (let [[supply value] (parallel supply (:value node))] [supply {:op :throw :value value}]) + :write (let [[supply value] (parallel supply (:value node))] + [supply {:op :write :target (:target node) :value value}]) + :do (let [[supply forms] (parallel-forms supply (:forms node))] [supply {:op :do :forms forms}]) + :host (let [[supply args] (parallel-forms supply (:args node))] + [supply {:op :host :operation (:operation node) :args args}]) + :invoke (let [[supply function] (parallel supply (:function node)) + [supply args] (parallel-forms supply (:args node))] + [supply {:op :invoke :function function :args args}]) + :collection (let [[supply items] (parallel-forms supply (:items node)) + [supply metadata] (if-let [metadata (:metadata node)] (parallel supply metadata) [supply nil])] + [supply {:op :collection :kind (:kind node) :items items :metadata metadata}]) + :let (let [[supply bindings] (parallel-bindings supply (:bindings node)) + [supply body] (parallel supply (:body node))] + [supply {:op :let :bindings bindings :body body}]) + :psetq (let [[supply bindings writes] (save-parallel-values supply (:pairs node)) + template (first writes)] + (if template + [supply {:op :let :bindings bindings + :body (assoc template :node {:op :do :forms (conj writes (assoc template :node {:op :literal :value nil}))})}] + [supply {:op :literal :value nil}]))) + +(np/defpass parallel + {:from [l/Sequential Expr] :to (t/HVec [l/Bound-Supply l/Assigned-Expr]) + :args [l/Bound-Supply] :body true} + [supply expr] + (let [[supply node] (parallel-node supply (:node expr))] [supply (assoc expr :node node)])) diff --git a/src/com/tailrecursion/blacktide/compiler/backend.clj b/src/com/tailrecursion/blacktide/compiler/backend.clj new file mode 100644 index 0000000..279f53a --- /dev/null +++ b/src/com/tailrecursion/blacktide/compiler/backend.clj @@ -0,0 +1,26 @@ +(ns ^:typed.clojure com.tailrecursion.blacktide.compiler.backend + "Select checked structured code when eligible, otherwise the general machine. + Independent models and both emitters exercise the same source semantics." + (:require [com.tailrecursion.nanopass :as np] + [com.tailrecursion.blacktide.compiler.languages :as l] + [com.tailrecursion.blacktide.compiler.feasible :as feasible] + [com.tailrecursion.blacktide.compiler.graph :as graph] + [com.tailrecursion.blacktide.compiler.invariants :as invariants] + [com.tailrecursion.blacktide.compiler.value-facts :as facts] + [com.tailrecursion.blacktide.compiler.joins :as joins] + [com.tailrecursion.blacktide.compiler.storage :as storage] + [com.tailrecursion.blacktide.compiler.compact :as compact] + [com.tailrecursion.blacktide.compiler.structure :as structure] + [com.tailrecursion.blacktide.compiler.structured-emit :as structured] + [com.tailrecursion.blacktide.compiler.emit :as emit])) + +(np/defpass compile-program {:from [l/Machine Program] :to [l/Machine Code] :body true} + [machine] + (let [program (-> {:entry (:entry machine) :blocks (:blocks machine)} + feasible/program graph/analyze) + analysis (facts/analyze program) + decision (structure/recover (joins/plan analysis))] + (if (= :structured (:op decision)) + (let [program (compact/program (storage/select-storage analysis (invariants/expression #{} (:body decision))))] + (structured/expression (:types program) (:body program))) + (emit/program-form machine)))) diff --git a/src/com/tailrecursion/blacktide/compiler/boundaries.clj b/src/com/tailrecursion/blacktide/compiler/boundaries.clj new file mode 100644 index 0000000..d7e1d5d --- /dev/null +++ b/src/com/tailrecursion/blacktide/compiler/boundaries.clj @@ -0,0 +1,70 @@ +(ns ^:typed.clojure com.tailrecursion.blacktide.compiler.boundaries + "Classify analyzed native bodies before expression lowering. Native subtrees + exclude DSL effects; lowered loops materialize their source representations." + (:require [com.tailrecursion.blacktide.compiler.effects :refer [effects]] + [com.tailrecursion.nanopass :as np] + [com.tailrecursion.blacktide.compiler.source-languages :as s] + [com.tailrecursion.blacktide.compiler.loop-facts :as loops] + [typed.clojure :as-alias t])) + +(declare native native-binding native-forms classify classify-binding classify-forms classify-pair classify-pairs) + +(np/defpass native-node + {:from [s/Bound Node] :to [s/Native Node] + :processors {Expr native Exprs native-forms Binding native-binding}} + [node] + :literal node :local node :global node :var node + :case {:op :case :selector (native (:selector node)) + :arms (native-forms (:arms node))} + :dispatch {:op :dispatch :value (native (:value node)) :spec (:spec node)} + :collection {:op :collection :kind (:kind node) :items (native-forms (:items node)) + :metadata (when-let [metadata (:metadata node)] (native metadata))} + :recur {:op :recur :target (:target node) :args (mapv native (:args node))} + :loop {:op :loop :id (:id node) :bindings (mapv native-binding (:bindings node)) + :body (native (:body node))} + :setq (if (empty? (:pairs node)) {:op :literal :value nil} (throw (IllegalStateException. "Native local write"))) + :psetq (if (empty? (:pairs node)) {:op :literal :value nil} (throw (IllegalStateException. "Native parallel write"))) + :label (throw (IllegalStateException. "Native label")) + :goto (throw (IllegalStateException. "Native transfer"))) + +(np/defpass native-binding {:from [s/Bound Binding] :to [s/Native Binding] :body true} + [binding] (assoc binding :init (native (:init binding)))) +(np/defpass native-forms {:from [s/Bound Exprs] :to [s/Native Exprs] :processors {Expr native}} + [forms]) +(np/defpass native {:from [s/Bound Expr] :to [s/Native Expr] :body true} + [expr] + (assoc expr :node (native-node (:node expr)))) + +(np/defpass classify-node + {:from [s/Bound Node] :to [s/Classified Node] + :processors {Expr classify Exprs classify-forms Binding classify-binding Assignments classify-pairs + Local clojure.core/identity RecurTarget clojure.core/identity + Raw clojure.core/identity CollectionKind clojure.core/identity CaseSpec clojure.core/identity}} + [node] + :loop {:op :loop :id (:id node) :bindings (mapv classify-binding (:bindings node)) :body (classify (:body node))}) + +(np/defpass classify-pair {:from [s/Bound Assignment] :to [s/Classified Assignment] + :processors {Expr classify Local clojure.core/identity}} + [pair]) +(np/defpass classify-pairs {:from [s/Bound Assignments] :to [s/Classified Assignments] :processors {Assignment classify-pair}} + [pairs]) +(np/defpass classify-binding {:from [s/Bound Binding] :to [s/Classified Binding] + :processors {Expr classify Local clojure.core/identity}} + [binding]) +(np/defpass classify-forms {:from [s/Bound Exprs] :to [s/Classified Exprs] :processors {Expr classify}} + [forms]) +(np/defpass classify {:from [s/Bound Expr] :to [s/Classified Expr] :body true} + [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)))) + :else (assoc expr :node (classify-node node))))) diff --git a/src/com/tailrecursion/blacktide/compiler/box_origins.clj b/src/com/tailrecursion/blacktide/compiler/box_origins.clj new file mode 100644 index 0000000..3c78396 --- /dev/null +++ b/src/com/tailrecursion/blacktide/compiler/box_origins.clj @@ -0,0 +1,38 @@ +(ns ^:typed.clojure com.tailrecursion.blacktide.compiler.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.analysis-languages :as a] + [com.tailrecursion.blacktide.compiler.value-facts :as facts] + [typed.clojure :as-alias t])) + +(np/defpass copy-source {:from [l/Machine Rhs] :to (t/Option t/Sym) :body true} + [rhs] + (when (and (= :value (:op rhs)) (= :local (:op (:value rhs)))) (:id (:value rhs)))) + +(np/defpass reboxable? {:from [l/Machine Rhs] :to t/Bool :body true} + [rhs] + (case (:op rhs) + :value (let [value (:value rhs)] + (boolean (and (= :literal (:op value)) (instance? Long (:value value)) + (<= -128 (:value value) 127)))) + (:host :convert) (class? (facts/rhs-type {} rhs)) + false)) + +(np/defpass reboxable-values {:from [a/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/Machine-Rhs)} values] + (if (some #(and (nil? (copy-source %)) (not (reboxable? %))) 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/Machine-Rhs)} values] + (if (some #(contains? denied (copy-source %)) 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/case_emit.clj b/src/com/tailrecursion/blacktide/compiler/case_emit.clj new file mode 100644 index 0000000..bf670f6 --- /dev/null +++ b/src/com/tailrecursion/blacktide/compiler/case_emit.clj @@ -0,0 +1,25 @@ +(ns ^:typed.clojure com.tailrecursion.blacktide.compiler.case-emit + "Emit native case* dispatch from constant table data. Result arms are separate + from constant matching; selection never substitutes ordinary equality for case." + (:require [com.tailrecursion.nanopass :as np] + [com.tailrecursion.blacktide.compiler.source-languages :as s] + [com.tailrecursion.blacktide.compiler.languages :as l] + [typed.clojure :as-alias t])) + +(np/defpass selector-form + {:from [s/Bound CaseSpec] :to [l/Machine Code] :args [l/Machine-Code] :body true} + [value spec] + (let [local (gensym "dispatch__")] + (list 'let* [local value] + (list 'case* local (:shift spec) (:mask spec) (:default spec) + (into (sorted-map) (:tests spec)) (:switch spec) (:test spec) (:skip spec))))) + +(np/defpass arms-form + {:from [l/Machine Code] :to [l/Machine Code] :args [(t/Vec l/Machine-Code)] :body true} + [arms selector] + (let [local (gensym "selected__") + default (or (first arms) (throw (IllegalStateException. "Case requires an arm"))) + table (into (sorted-map) (map-indexed (fn [index arm] [index [index arm]]) arms))] + ;; The selector is exhaustive. Reusing an arm as the unreachable default + ;; preserves native common-return typing without introducing a nil arm. + (list 'let* [local selector] (list 'case* local 0 0 default table :compact :int)))) diff --git a/src/com/tailrecursion/blacktide/compiler/cases.clj b/src/com/tailrecursion/blacktide/compiler/cases.clj new file mode 100644 index 0000000..422d17e --- /dev/null +++ b/src/com/tailrecursion/blacktide/compiler/cases.clj @@ -0,0 +1,24 @@ +(ns ^:typed.clojure com.tailrecursion.blacktide.compiler.cases + "Separate source case from ordinal arm selection after native constant dispatch. + Each source arm occurs once, including grouped and hash-collision alternatives." + (:require [com.tailrecursion.nanopass :as np] + [com.tailrecursion.blacktide.compiler.source-languages :as s])) + +(declare normalize normalize-binding normalize-forms) + +(np/defpass normalize-node + {:from [s/Assigned Node] :to [s/Cases Node] + :processors {Expr normalize Exprs normalize-forms Binding normalize-binding + Raw clojure.core/identity Local clojure.core/identity CollectionKind clojure.core/identity + RecurTarget clojure.core/identity CaseSpec clojure.core/identity}} + [node] + :case + {:op :select :selector (normalize (:selector node)) :arms (normalize-forms (:arms node))}) + +(np/defpass normalize-binding {:from [s/Assigned Binding] :to [s/Cases Binding] + :processors {Expr normalize Local clojure.core/identity}} + [binding]) +(np/defpass normalize-forms {:from [s/Assigned Exprs] :to [s/Cases Exprs] :processors {Expr normalize}} + [forms]) +(np/defpass normalize {:from [s/Assigned Expr] :to [s/Cases Expr] :body true} + [expr] (assoc expr :node (normalize-node (:node expr)))) diff --git a/src/com/tailrecursion/blacktide/compiler/collections.clj b/src/com/tailrecursion/blacktide/compiler/collections.clj new file mode 100644 index 0000000..02bd5d0 --- /dev/null +++ b/src/com/tailrecursion/blacktide/compiler/collections.clj @@ -0,0 +1,71 @@ +(ns ^:typed.clojure com.tailrecursion.blacktide.compiler.collections + "Preserve native constant collections and construction checks. Metadata keeps + construction dynamic and is evaluated only after that construction succeeds." + (:require [com.tailrecursion.nanopass :as np] + [com.tailrecursion.blacktide.compiler.source-languages :as l] + [com.tailrecursion.blacktide.compiler.languages :as m] + [com.tailrecursion.blacktide.compiler.host-emit :as host] + [typed.clojure :as-alias t])) + +(declare normalize normalize-binding) + +(np/defpass form + {:from [l/Collections CollectionKind] :to [m/Machine Code] + :args [(t/Vec m/Machine-Code) t/Bool] :body true} + [items unique? kind] + (if (= :vector kind) + (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. + (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)) + (list 'clojure.lang.RT/object_array items)))) + +(np/defpass normalize-forms + {:from [l/Cases Exprs] :to [l/Collections Exprs] :processors {Expr normalize}} + [forms]) + +(np/defpass literal-values + {:from [l/Collections Exprs] :to (t/Option (t/Vec t/Any)) :body true} + [items] + (reduce (fn [^{::t/- (t/Option (t/Vec t/Any))} values ^{::t/- l/Collections-Expr} expr] + (let [node (:node expr)] + (if (and values (= :literal (:op node))) (conj values (:value node)) (reduced nil)))) + [] items)) + +(np/defpass constant + {:from [l/Collections CollectionKind] :to t/Any :args [(t/Vec t/Any)] :body true} + [values kind] + (case kind + :vector (vec values) + :map (reduce (fn [^{::t/- (t/Map t/Any t/Any)} out ^{::t/- (t/Seq t/Any)} pair] + (assoc out (first pair) (second pair))) {} (partition 2 values)) + :set (set values))) + +(np/defpass normalize-node + {:from [l/Cases Node] :to [l/Collections Node] + :processors {Expr normalize Exprs normalize-forms Binding normalize-binding Raw clojure.core/identity + Local clojure.core/identity RecurTarget clojure.core/identity CaseSpec clojure.core/identity}} + [node] + :collection + (let [items (mapv normalize (:items node)) metadata (some-> (:metadata node) normalize) + values (literal-values items) + keys (when (= :map (:kind node)) (literal-values (vec (take-nth 2 items)))) + unique? (boolean (and keys (= (count keys) (count (set keys)))))] + (when (and (nil? metadata) keys (not unique?)) + (throw (IllegalArgumentException. "Duplicate constant keys in map"))) + (if (and (nil? metadata) values) + {:op :literal :value (constant values (:kind node))} + {:op :build :kind (:kind node) :items items :metadata metadata :unique? unique?}))) + +(np/defpass normalize-binding + {:from [l/Cases Binding] :to [l/Collections Binding] + :processors {Expr normalize Local clojure.core/identity}} + [binding]) + +(np/defpass normalize + {:from [l/Cases Expr] :to [l/Collections Expr] :body true} + [expr] + (assoc expr :node (normalize-node (:node expr)))) diff --git a/src/com/tailrecursion/blacktide/compiler/compact.clj b/src/com/tailrecursion/blacktide/compiler/compact.clj new file mode 100644 index 0000000..ea027c1 --- /dev/null +++ b/src/com/tailrecursion/blacktide/compiler/compact.clj @@ -0,0 +1,197 @@ +(ns ^:typed.clojure com.tailrecursion.blacktide.compiler.compact + "Discard unused storage without discarding evaluation. Substitute compatible + copies and single-use, total primitive computations only before their inputs + are rebound. Native bodies remain opaque, including their free local reads." + (:require [clojure.set :as set] + [com.tailrecursion.nanopass :as np] + [com.tailrecursion.blacktide.compiler.structured-languages :as s] + [com.tailrecursion.blacktide.compiler.compact-languages :as c] + [com.tailrecursion.blacktide.compiler.native :as native] + [com.tailrecursion.blacktide.compiler.jvm :as jvm] + [typed.clojure :as-alias t])) + +(np/defpass introduce {:from [s/Stored Expr] :to [c/Compact Expr]} + [node] + :bind (assoc node :body (introduce (:body node))) + :join (assoc node :value (introduce (:value node)) :body (introduce (:body node))) + :branch (assoc node :yes (introduce (:yes node)) :no (introduce (:no node))) + :switch (assoc node :arms (mapv introduce (:arms node))) + :loop {:op :loop :bindings (mapv (fn [id] {:target id :value {:op :local :id id}}) (:slots node)) + :body (introduce (:body node))} + :recur {:op :recur :args (mapv (fn [id] {:op :local :id id}) (:slots node))} + :return node :throw node) + +(declare rhs-reads) +(np/defpass atom-reads {:from [c/Compact Atom] :to (t/Vec t/Sym)} + [node] + :local [(:id node)] :literal [] :inline (rhs-reads (:value node))) + +(np/defpass rhs-reads {:from [c/Compact Rhs] :to (t/Vec t/Sym)} + [node] + :value (atom-reads (:value node)) + :global [] :external [] :var [] + :native (vec (native/reads (:value node))) + :dispatch (atom-reads (:value (:value node))) + :convert (atom-reads (:value (:value node))) + :construct (into [] (mapcat atom-reads) (:items node)) + :metadata (into (atom-reads (:value node)) (atom-reads (:metadata node))) + :host (into [] (mapcat #(atom-reads (:value %))) (:args node)) + :invoke (into (atom-reads (:function node)) (mapcat atom-reads) (:args node))) + +(np/defpass reads {:from [c/Compact Expr] :to (t/Vec t/Sym)} + [node] + :bind (into (rhs-reads (:value (:binding node))) (filterv #(not= % (:target (:binding node))) (reads (:body node)))) + :effect (into (rhs-reads (:value (:binding node))) (reads (:body node))) + :join (into (reads (:value node)) (filterv #(not= % (:target node)) (reads (:body node)))) + :branch (into (into (atom-reads (:test node)) (reads (:yes node))) (reads (:no node))) + :switch (into (atom-reads (:test node)) (mapcat reads) (:arms node)) + :loop (reduce (fn [^{::t/- (t/Vec t/Sym)} refs ^{::t/- c/Compact-Binding} binding] + (into (atom-reads (:value binding)) (filterv #(not= % (:target binding)) refs))) + (reads (:body node)) (rseq (:bindings node))) + :recur (into [] (mapcat atom-reads) (:args node)) + :return (atom-reads (:value node)) :throw (atom-reads (:value node))) + +(np/defpass stable? + {:from [c/Compact Expr] :to t/Bool :args [t/Sym (t/Set t/Sym)]} + [id dependencies node] + :bind (if (contains? dependencies (:target (:binding node))) + (not (some #{id} (reads (:body node)))) (stable? id dependencies (:body node))) + :effect (stable? id dependencies (:body node)) + :join (and (stable? id dependencies (:value node)) + (if (contains? dependencies (:target node)) + (not (some #{id} (reads (:body node)))) (stable? id dependencies (:body node)))) + :branch (and (stable? id dependencies (:yes node)) (stable? id dependencies (:no node))) + :switch (every? #(stable? id dependencies %) (:arms node)) + ;; Loop binders also rebind on the backedge. Do not move an outer snapshot + ;; into a loop that changes one of its inputs, even if the first entry agrees. + :loop (and (empty? (set/intersection dependencies (set (map :target (:bindings node))))) + (stable? id dependencies (:body node))) + :recur true :return true :throw true) + +(declare substitute-rhs) +(np/defpass substitute-atom + {:from [c/Compact Atom] :to [c/Compact Atom] :args [t/Sym c/Compact-Atom] + :processors {Rhs substitute-rhs}} + [from replacement node] + :local (if (= from (:id node)) replacement node)) + +(np/defpass substitute-operand + {:from [c/Compact Operand] :to [c/Compact Operand] :args [t/Sym c/Compact-Atom] + :processors {Atom substitute-atom}} [from replacement node]) + +(np/defpass substitute-rhs + {:from [c/Compact Rhs] :to [c/Compact Rhs] :args [t/Sym c/Compact-Atom] + :processors {Atom substitute-atom Operand substitute-operand}} [from replacement node]) + +(np/defpass substitute-operation + {:from [c/Compact Operation] :to [c/Compact Operation] :args [t/Sym c/Compact-Atom] + :processors {Rhs substitute-rhs}} [from replacement node]) + +(np/defpass substitute-binding + {:from [c/Compact Binding] :to [c/Compact Binding] :args [t/Sym c/Compact-Atom] + :processors {Atom substitute-atom}} [from replacement node]) + +(np/defpass substitute + {:from [c/Compact Expr] :to [c/Compact Expr] :args [t/Sym c/Compact-Atom] + :processors {Atom substitute-atom Rhs substitute-rhs Operation substitute-operation Binding substitute-binding}} + [from replacement node] + :bind (assoc node :binding (substitute-operation from replacement (:binding node)) + :body (if (= from (:target (:binding node))) (:body node) + (substitute from replacement (:body node)))) + :join (assoc node :value (substitute from replacement (:value node)) + :body (if (= from (:target node)) (:body node) + (substitute from replacement (:body node)))) + ;; Loop initializers see earlier bindings, but the body sees all loop binders. + :loop ((fn ^{::t/- [t/Int (t/Vec c/Compact-Binding) t/Bool :-> c/Compact-Expr]} bindings [index pairs shadowed?] + (if (< index (count (:bindings node))) + (let [binding (nth (:bindings node) index)] + (recur (inc index) (conj pairs (if shadowed? binding (substitute-binding from replacement binding))) + (or shadowed? (= from (:target binding))))) + (assoc node :bindings pairs :body (if shadowed? (:body node) + (substitute from replacement (:body node)))))) 0 [] false)) + +(np/defpass atom-type + {:from [c/Compact Atom] :to (t/Option java.lang.Class) :args [c/Compact-Types]} + [types node] + :local (get types (:id node)) + :literal (cond (instance? Long (:value node)) Long/TYPE (instance? Double (:value node)) Double/TYPE :else nil) + :inline (:type node)) + +(np/defpass movable? + {:from [c/Compact Rhs] :to t/Bool :args [c/Compact-Types] :body true} + [types rhs] + (and (= :host (:op rhs)) + (let [operation (:operation rhs)] + (and (= :static (:op operation)) (= clojure.lang.Numbers (:class operation)) + (contains? '#{lt lte gt gte equiv unchecked_add unchecked_minus unchecked_multiply + unchecked_inc unchecked_dec unchecked_negate} (:member operation)) + (every? (fn [^{::t/- c/Compact-Operand} operand] + (let [type (atom-type types (:value operand))] + (and (contains? #{Long/TYPE Double/TYPE} type) (= type (:type (:jvm operand)))))) + (:args rhs)) + (let [type (jvm/host-type (mapv #(:type (:jvm %)) (:args rhs)) operation)] + (and (class? type) (.isPrimitive ^Class type) (not= Void/TYPE type))))))) + +(np/defpass replacement + {:from [c/Compact Operation] :to (t/Option c/Compact-Atom) :args [c/Compact-Types t/Int] :body true} + [types uses binding] + (let [rhs (:value binding) ^Class type (get types (:target binding))] + (case (:op rhs) + :value (let [value (:value rhs)] + ;; A bound reference must not become several independently boxed reads. + (when (and (= type (atom-type types value)) + (or type (= :local (:op value))) + (or (not= :inline (:op value)) (= uses 1))) value)) + :convert (let [conversion (:conversion rhs) operand (:value rhs)] + (when (and type (:parameter? conversion) (= type (:type conversion)) + (= type (atom-type types (:value operand))) (= type (:type (:jvm operand)))) + (:value operand))) + :host (let [operation (:operation rhs) operand (first (:args rhs)) + cast-name (when (class? type) (symbol (str (.getName ^Class type) "Cast")))] + ;; RT's same-width primitive casts are identities only after both + ;; storage and source operand types agree. Keep Object results: + ;; those casts may establish a fresh box at a source-local read. + (if (and type (= :static (:op operation)) (= clojure.lang.RT (:class operation)) + (= 1 (count (:args rhs))) operand + (= type (atom-type types (:value operand)) (:type (:jvm operand))) + (= cast-name (:member operation)) + (= [type] (:tags operation))) + (:value operand) + (when (and type (= uses 1) (movable? types rhs)) {:op :inline :value rhs :type type}))) + nil))) + +(np/defpass discardable? {:from [c/Compact Rhs] :to t/Bool :body true} + [rhs] + ;; Keep native validation, including constants that may fail AOT embedding. + (and (= :value (:op rhs)) + (let [value (:value rhs)] + (or (= :local (:op value)) (and (= :literal (:op value)) (nil? (:value value))))))) + +(np/defpass simplify {:from [c/Compact Expr] :to [c/Compact Expr] :args [c/Compact-Types]} + [types node] + :bind (let [binding (:binding node) id (:target binding) body (simplify types (:body node)) + uses (count (filter #{id} (reads body))) + value (replacement types uses binding)] + (cond + (and (= :native (:op (:value binding))) (not (contains? types id)) + (= body {:op :return :value {:op :local :id id}})) + {:op :return :value {:op :inline :value (:value binding) :type nil}} + (zero? uses) (if (discardable? (:value binding)) body {:op :effect :binding binding :body body}) + (and value (stable? id (set (atom-reads value)) body)) + (let [out (substitute id value body)] + ;; Substitution deliberately leaves native code alone. + (if (some #{id} (reads out)) (assoc node :body body) out)) + :else (assoc node :body body))) + :effect (let [body (simplify types (:body node))] + (if (discardable? (:value (:binding node))) body (assoc node :body body))) + :join (let [value (simplify types (:value node)) body (simplify types (:body node)) id (:target node)] + (if (and (not (contains? types id)) (= body {:op :return :value {:op :local :id id}})) + value (assoc node :value value :body body))) + :branch (assoc node :yes (simplify types (:yes node)) :no (simplify types (:no node))) + :switch (assoc node :arms (mapv #(simplify types %) (:arms node))) + :loop (assoc node :body (simplify types (:body node))) + :recur node :return node :throw node) + +(np/defpass program {:from [s/Stored Program] :to [c/Compact Program] :body true} + [program] + {:types (:types program) :body (simplify (:types program) (introduce (:body program)))}) diff --git a/src/com/tailrecursion/blacktide/compiler/compact_languages.clj b/src/com/tailrecursion/blacktide/compiler/compact_languages.clj new file mode 100644 index 0000000..aa881cd --- /dev/null +++ b/src/com/tailrecursion/blacktide/compiler/compact_languages.clj @@ -0,0 +1,24 @@ +(ns com.tailrecursion.blacktide.compiler.compact-languages + "Lexical expressions after storage selection. Discarded results have an effect + destination; total primitive computations may occupy operand positions. + Native results may remain in tail position. Loop initializers and recur + arguments are explicit, separate from binders." + (:require [com.tailrecursion.nanopass :as np] + [com.tailrecursion.blacktide.compiler.languages :as l] + [typed.clojure :as-alias t])) + +(np/derive-language Compact l/Machine + {:add {Atom [(t/HMap :mandatory {:op ':inline :value Rhs :type (t/Option java.lang.Class)} :complete? true)]} + :replace + {Binding (t/HMap :mandatory {:target t/Sym :value Atom} :complete? true) + Expr (t/U (t/HMap :mandatory {:op ':bind :binding Operation :body Expr} :complete? true) + (t/HMap :mandatory {:op ':effect :binding Operation :body Expr} :complete? true) + (t/HMap :mandatory {:op ':join :target t/Sym :value Expr :body Expr} :complete? true) + (t/HMap :mandatory {:op ':branch :test Atom :yes Expr :no Expr} :complete? true) + (t/HMap :mandatory {:op ':switch :test Atom :arms (t/Vec Expr)} :complete? true) + (t/HMap :mandatory {:op ':loop :bindings (t/Vec Binding) :body Expr} :complete? true) + (t/HMap :mandatory {:op ':recur :args (t/Vec Atom)} :complete? true) + (t/HMap :mandatory {:op ':return :value Atom} :complete? true) + (t/HMap :mandatory {:op ':throw :value Atom} :complete? true)) + Types (t/Map t/Sym java.lang.Class) + Program (t/HMap :mandatory {:body Expr :types Types} :complete? true)}}) diff --git a/src/com/tailrecursion/blacktide/compiler/effects.clj b/src/com/tailrecursion/blacktide/compiler/effects.clj new file mode 100644 index 0000000..8562b1b --- /dev/null +++ b/src/com/tailrecursion/blacktide/compiler/effects.clj @@ -0,0 +1,28 @@ +(ns ^:typed.clojure com.tailrecursion.blacktide.compiler.effects + "Identify local writes and transfers that prevent native subtree emission." + (:require [com.tailrecursion.nanopass :as np] + [com.tailrecursion.blacktide.compiler.source-languages :as s] + [typed.clojure :as-alias t])) + +(declare effects) + +(np/defpass effect-node {:from [s/Bound Node] :to t/Bool} + [node] + :literal false :local false :global false :var false + :setq (boolean (seq (:pairs node))) + :psetq (boolean (seq (:pairs node))) + :label true :goto true + :if (or (effects (:test node)) (effects (:then node)) (effects (:else node))) + :case (or (effects (:selector node)) (boolean (some effects (:arms node)))) + :dispatch (effects (:value node)) + :throw (effects (:value node)) + :host (boolean (some effects (:args node))) + :invoke (or (effects (:function node)) (boolean (some effects (:args node)))) + :collection (or (boolean (some effects (:items node))) (boolean (some-> (:metadata node) effects))) + :do (boolean (some effects (:forms node))) + :let (or (boolean (some #(effects (:init %)) (:bindings node))) (effects (:body node))) + :loop (or (boolean (some #(effects (:init %)) (:bindings node))) (effects (:body node))) + :recur (boolean (some effects (:args node)))) + +(np/defpass effects {:from [s/Bound Expr] :to t/Bool :body true} + [expr] (effect-node (:node expr))) diff --git a/src/com/tailrecursion/blacktide/compiler/emit.clj b/src/com/tailrecursion/blacktide/compiler/emit.clj new file mode 100644 index 0000000..7492023 --- /dev/null +++ b/src/com/tailrecursion/blacktide/compiler/emit.clj @@ -0,0 +1,77 @@ +(ns ^:typed.clojure com.tailrecursion.blacktide.compiler.emit + "Emit allocated machines as ordinary Clojure forms. Source effects occur in + sequential lets; edges carry live values through boxed loop bindings." + (:require [com.tailrecursion.nanopass :as np] + [com.tailrecursion.blacktide.compiler.languages :as l] + [com.tailrecursion.blacktide.compiler.native :as native] + [com.tailrecursion.blacktide.compiler.collections :as collections] + [com.tailrecursion.blacktide.compiler.case-emit :as cases] + [typed.clojure :as-alias t] + [com.tailrecursion.blacktide.compiler.host-emit :as host])) + +(np/defpass atom-form {:from [l/Machine Atom] :to [l/Machine Code]} + [node] + :literal (list 'quote (:value node)) + :local (:id node)) + +(np/defpass rhs-form {:from [l/Machine Rhs] :to [l/Machine Code]} + [node] + :value (atom-form (:value node)) + :global (host/global-form (:name node)) + :external (:name node) + :var (list 'var (:name node)) + :native (native/form #{} (:value node)) + :dispatch (cases/selector-form (host/typed-form (:jvm (:value node)) (atom-form (:value (:value node)))) (:spec node)) + :construct (collections/form (mapv atom-form (:items node)) (:unique? node) (:kind node)) + :metadata (let [value (gensym "value__") metadata (gensym "metadata__")] + (list 'let* [(with-meta value {:tag 'clojure.lang.IObj}) (atom-form (:value node)) + (with-meta metadata {:tag 'clojure.lang.IPersistentMap}) (atom-form (:metadata node))] + (list '. value 'withMeta metadata))) + :convert (host/conversion-form (:conversion node) (:jvm (:value node)) (atom-form (:value (:value node)))) + :host (host/host-form + (mapv (fn [arg] (host/typed-form (assoc (:jvm arg) :primitive? false) (atom-form (:value arg)))) + (:args node)) (:operation node)) + :invoke (list* (atom-form (:function node)) (mapv atom-form (:args node)))) + +(np/defpass operation-form + {:from [l/Machine Operation] :to (t/HVec [t/Sym l/Machine-Code]) :body true} + [op] + [(with-meta (:target op) nil) (host/box-form (rhs-form (:value op)))]) + +(np/defpass edge-form + {:from [l/Machine Label] :to [l/Machine Code] + :args [(t/Vec t/Sym) (t/Map l/Machine-Label (t/Set t/Sym))] :body true} + [slots live target] + (list* 'recur target (mapv (fn [id] (when (contains? (get live target #{}) id) (with-meta id nil))) slots))) + +(np/defpass term-form + {:from [l/Machine Term] :to [l/Machine Code] + :args [(t/Vec t/Sym) (t/Map l/Machine-Label (t/Set t/Sym))]} + [slots live term] + :jump (edge-form slots live (:target term)) + :branch (list 'if (atom-form (:test term)) + (edge-form slots live (:yes term)) (edge-form slots live (:no term))) + :switch (cases/arms-form (mapv #(edge-form slots live %) (:targets term)) (atom-form (:test term))) + :return (atom-form (:value term)) + :throw (list 'throw (atom-form (:value term)))) + +(np/defpass block-form + {:from [l/Machine Block] :to [l/Machine Code] + :args [(t/Vec t/Sym) (t/Map l/Machine-Label (t/Set t/Sym))] :body true} + [slots live block] + (let [end (term-form slots live (:term block))] + (if (seq (:ops block)) + (list 'let* (vec (mapcat identity (mapv operation-form (:ops block)))) end) + end))) + +(np/defpass program-form {:from [l/Machine Program] :to [l/Machine Code] :body true} + [program] + (let [pc (gensym "pc__") + slots (mapv #(with-meta % nil) (:slots program)) + bindings (into [pc (host/box-form (list 'quote (:entry program)))] + (mapcat #(vector % nil)) slots) + arms (mapcat (fn [[label block]] [label (block-form slots (:live program) block)]) + (sort-by key (:blocks program)))] + (list 'loop* bindings + (list* 'clojure.core/case pc + (concat arms [(list 'throw (list 'clojure.core/ex-info "Invalid machine PC" {:pc pc}))]))))) diff --git a/src/com/tailrecursion/blacktide/compiler/errors.clj b/src/com/tailrecursion/blacktide/compiler/errors.clj new file mode 100644 index 0000000..caef819 --- /dev/null +++ b/src/com/tailrecursion/blacktide/compiler/errors.clj @@ -0,0 +1,27 @@ +(ns ^:typed.clojure com.tailrecursion.blacktide.compiler.errors + "Source locations and structured errors shared by syntax analysis and lowering. + DSL markers obey the same Var visibility rules as other source forms." + (:require [com.tailrecursion.nanopass :as np] + [com.tailrecursion.blacktide.compiler.source-languages :as s] + [typed.clojure :as-alias t])) + +(np/defpass site {:from [s/Bound Raw] :to [s/Bound Site] :args [s/Bound-Site] :body true} + [parent form] + ((fn ^{::t/- [t/Any t/Any t/Any :-> s/Bound-Site]} location [file line column] + {:form form :file (if (string? file) file (:file parent)) + :line (if (integer? line) line (:line parent)) + :column (if (integer? column) column (:column parent))}) + (:file (meta form)) (:line (meta form)) (:column (meta form)))) + +(np/defpass fail + {:from [s/Bound Site] :to t/Nothing :args [t/Kw t/Str (t/Map t/Kw t/Any)] :body true} + [kind message details site] + (throw (ex-info message (merge site details {:kind kind})))) + +(np/defpass check-visibility + {:from [s/Bound Site] :to nil :args [t/AnyVar] :body true} + [v site] + ;; The public Var field is authoritative; :ns metadata can be changed. + (when (and (not (.isPublic ^clojure.lang.Var v)) + (not= *ns* (clojure.lang.Reflector/getInstanceField v "ns"))) + (fail :private-var "Cannot access a private Var from another namespace" {:binding (.toSymbol ^clojure.lang.Var v)} site))) diff --git a/src/com/tailrecursion/blacktide/compiler/feasible.clj b/src/com/tailrecursion/blacktide/compiler/feasible.clj new file mode 100644 index 0000000..b2a138d --- /dev/null +++ b/src/com/tailrecursion/blacktide/compiler/feasible.clj @@ -0,0 +1,22 @@ +(ns ^:typed.clojure com.tailrecursion.blacktide.compiler.feasible + "Fold literal truth tests after source validation. Keep every block so dead + source still receives JVM validation." + (:require [com.tailrecursion.nanopass :as np] + [com.tailrecursion.blacktide.compiler.languages :as l] + [typed.clojure :as-alias t])) + +(np/defpass terminator {:from [l/CFG Term] :to [l/CFG Term] :body true} + [term] + (if (and (= :branch (:op term)) (= :literal (:op (:test term)))) + {:op :jump :target (if (:value (:test term)) (:yes term) (:no term))} + term)) + +(np/defpass block {:from [l/CFG Block] :to [l/CFG Block] :body true} + [block] (assoc block :term (terminator (:term block)))) + +(np/defpass program {:from [l/CFG Program] :to [l/CFG Program] :body true} + [program] + (assoc program :blocks + (reduce-kv (fn [^{::t/- (t/Map l/CFG-Label l/CFG-Block)} out + ^{::t/- l/CFG-Label} id ^{::t/- l/CFG-Block} node] + (assoc out id (block node))) {} (:blocks program)))) diff --git a/src/com/tailrecursion/blacktide/compiler/graph.clj b/src/com/tailrecursion/blacktide/compiler/graph.clj new file mode 100644 index 0000000..ace2765 --- /dev/null +++ b/src/com/tailrecursion/blacktide/compiler/graph.clj @@ -0,0 +1,143 @@ +(ns ^:typed.clojure com.tailrecursion.blacktide.compiler.graph + "Check initialization on reachable paths; retain all blocks for native Clojure + validation. Liveness carries values through edges without evaluating dead code." + (:require [clojure.set :as set] + [com.tailrecursion.nanopass :as np] + [com.tailrecursion.blacktide.compiler.languages :as l] + [com.tailrecursion.blacktide.compiler.native :as native] + [typed.clojure :as-alias t])) + +(np/defpass atom-reads {:from [l/CFG Atom] :to (t/Set t/Sym)} + [node] + :literal #{} + :local #{(:id node)}) + +(np/defpass rhs-reads {:from [l/CFG Rhs] :to (t/Set t/Sym)} + [node] + :value (atom-reads (:value node)) + :global #{} + :external #{} + :var #{} + :native (native/reads (:value node)) + :dispatch (atom-reads (:value (:value node))) + :construct (reduce set/union #{} (mapv atom-reads (:items node))) + :metadata (set/union (atom-reads (:value node)) (atom-reads (:metadata node))) + :convert (atom-reads (:value (:value node))) + :host (into #{} (mapcat #(atom-reads (:value %))) (:args node)) + :invoke (reduce set/union (atom-reads (:function node)) (mapv atom-reads (:args node)))) + +(np/defpass term-reads {:from [l/CFG Term] :to (t/Set t/Sym)} + [term] + :jump #{} + :branch (atom-reads (:test term)) + :switch (atom-reads (:test term)) + :return (atom-reads (:value term)) + :throw (atom-reads (:value term))) + +(np/defpass successors {:from [l/CFG Term] :to (t/Vec l/CFG-Label)} + [term] + :jump [(:target term)] + :branch [(:yes term) (:no term)] + :switch (if (seq (:targets term)) (:targets term) + (throw (ex-info "Ordinal dispatch requires a destination" {:kind :invalid-graph}))) + :return [] + :throw []) + +(np/defpass block-facts {:from [l/CFG Block] :to [l/CFG BlockFacts] :body true} + [block] + (let [[reads defs] + (reduce (fn [^{::t/- (t/HVec [(t/Set t/Sym) (t/Set t/Sym)])} acc + ^{::t/- l/CFG-Operation} op] + (let [[reads defs] acc] + [(set/union reads (set/difference (rhs-reads (:value op)) defs)) + (conj defs (:target op))])) [#{} #{}] (:ops block))] + {:reads (set/union reads (set/difference (term-reads (:term block)) defs)) + :defs defs :successors (successors (:term block))})) + +(np/defpass collect-facts {:from [l/CFG Program] :to [l/CFG Facts] :body true} + [program] + (reduce-kv (fn [^{::t/- l/CFG-Facts} facts ^{::t/- l/CFG-Label} id ^{::t/- l/CFG-Block} block] + (assoc facts id (block-facts block))) {} (:blocks program))) + +(np/defpass fact-at {:from [l/CFG Label] :to [l/CFG BlockFacts] :args [l/CFG-Facts] :body true} + [facts id] + (or (get facts id) (throw (ex-info "Missing control-flow block" {:kind :invalid-graph :block id})))) + +(np/defpass validate-edges {:from [l/CFG Program] :to [l/CFG Program] :args [l/CFG-Facts] :body true} + [facts program] + (do + (fact-at facts (:entry program)) + (doseq [[id fact] facts target (:successors fact)] + (when-not (contains? facts target) + (throw (ex-info "Dangling control-flow edge" {:kind :invalid-graph :block id :target target})))) + program)) + +(np/defpass reachable {:from [l/CFG Label] :to [l/CFG Labels] :args [l/CFG-Facts] :body true} + [facts entry] + ((fn ^{::t/- [(t/Vec l/CFG-Label) l/CFG-Labels :-> l/CFG-Labels]} visit [pending seen] + (if-let [id (peek pending)] + (if (contains? seen id) + (recur (pop pending) seen) + (recur (into (pop pending) (:successors (fact-at facts id))) (conj seen id))) + seen)) [entry] #{})) + +(np/defpass predecessors {:from [l/CFG Facts] :to [l/CFG Predecessors] :body true} + [facts] + (reduce-kv (fn [^{::t/- l/CFG-Predecessors} out ^{::t/- l/CFG-Label} id ^{::t/- l/CFG-BlockFacts} fact] + (reduce (fn [^{::t/- l/CFG-Predecessors} out ^{::t/- l/CFG-Label} target] + (assoc out target (conj (get out target #{}) id))) out (:successors fact))) + {} facts)) + +(np/defpass initialized + {:from [l/CFG Label] :to [l/CFG Flow] :args [l/CFG-Facts l/CFG-Predecessors] :body true} + [facts predecessors entry] + (let [universe (reduce set/union #{} (map :defs (vals facts))) + initial (reduce (fn [^{::t/- l/CFG-Flow} out ^{::t/- l/CFG-Label} id] + (assoc out id (if (= entry id) #{} universe))) {} (keys facts))] + ;; Greatest fixed point: a backedge does not erase initialization on entry. + ((fn ^{::t/- [l/CFG-Flow :-> l/CFG-Flow]} solve [known] + (let [next (reduce-kv + (fn [^{::t/- l/CFG-Flow} out ^{::t/- l/CFG-Label} id ^{::t/- l/CFG-BlockFacts} _] + (assoc out id + (if (= entry id) #{} + (reduce (fn [^{::t/- (t/Set t/Sym)} acc ^{::t/- l/CFG-Label} pred] + (set/intersection acc + (set/union (get known pred #{}) (:defs (fact-at facts pred))))) + universe (get predecessors id #{}))))) {} facts)] + (if (= known next) known (recur next)))) initial))) + +(np/defpass validate-initialization + {:from [l/CFG Facts] :to [l/CFG Facts] :args [l/CFG-Flow] :body true} + [known facts] + (do + (doseq [[id fact] facts] + (let [missing (set/difference (:reads fact) (get known id #{}))] + (when (seq missing) + (throw (ex-info "Control flow reads uninitialized locals" + {:kind :uninitialized-slot :block id :slots missing}))))) + facts)) + +(np/defpass liveness {:from [l/CFG Facts] :to [l/CFG Flow] :body true} + [facts] + ((fn ^{::t/- [l/CFG-Flow :-> l/CFG-Flow]} solve [live] + (let [next (reduce-kv + (fn [^{::t/- l/CFG-Flow} out ^{::t/- l/CFG-Label} id ^{::t/- l/CFG-BlockFacts} fact] + (let [after (reduce set/union #{} (map #(get live % #{}) (:successors fact)))] + (assoc out id (set/union (:reads fact) (set/difference after (:defs fact)))))) {} facts)] + (if (= live next) live (recur next)))) {})) + +(np/defpass allocate + {:from [l/CFG Program] :to [l/Machine Program] :args [l/CFG-Flow] :body true} + [live program] + (assoc program :live live :slots (vec (sort (reduce set/union #{} (vals live)))))) + +(np/defpass analyze {:from [l/CFG Program] :to [l/Machine Program] :body true} + [program] + (let [facts (collect-facts program) + program (validate-edges facts program) + seen (reachable facts (:entry program)) + reachable-facts (select-keys facts seen)] + (validate-initialization (initialized reachable-facts (predecessors reachable-facts) (:entry program)) reachable-facts) + ;; Even unreachable native bodies must reach Clojure's compiler. Pruning + ;; them here suppresses Java call errors in source code. + (allocate (liveness facts) program))) diff --git a/src/com/tailrecursion/blacktide/compiler/host_emit.clj b/src/com/tailrecursion/blacktide/compiler/host_emit.clj new file mode 100644 index 0000000..ae9cd66 --- /dev/null +++ b/src/com/tailrecursion/blacktide/compiler/host_emit.clj @@ -0,0 +1,91 @@ +(ns ^:typed.clojure com.tailrecursion.blacktide.compiler.host-emit + "Emit host descriptors with source-typed saved operands. Known nil is retained + explicitly; Object prevents boxed storage from inventing a more specific type." + (:require [com.tailrecursion.nanopass :as np] + [com.tailrecursion.blacktide.compiler.jvm-languages :as j] + [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} + [name] + ;; A later macro can change :const or :macro metadata after source analysis. + (let [v (ns-resolve *ns* name) + method (if (and (var? v) (.isDynamic ^clojure.lang.Var v)) 'get 'getRawRoot)] + (list '. (list 'var name) method))) + +(np/defpass box-form {:from [l/Machine Code] :to [l/Machine Code] :body true} + [form] + ;; Select Object explicitly: nil must not choose RT.box(Boolean), and callers + ;; may supply genuine primitive locals that cannot accept an Object hint. + (list (with-meta 'clojure.lang.RT/box {:param-tags ['java.lang.Object]}) form)) + +(np/defpass typed-form + {:from [l/Machine Code] :to [l/Machine Code] :args [j/JVM-Fact] :body true} + [fact form] + (let [^Class type (:type fact) + object-form (with-meta (if (symbol? form) form (apply list form)) {:tag 'java.lang.Object})] + (cond + (or (nil? type) (= Void/TYPE type)) (list 'do form nil) + (and (class? type) (.isPrimitive type) (:primitive? fact)) + (list (with-meta (symbol "clojure.lang.RT" (str (.getName type) "Cast")) {:param-tags ['java.lang.Object]}) object-form) + :else (with-meta (if (symbol? form) form (apply list form)) + {:tag (if (class? type) (symbol (.getName type)) 'java.lang.Object)})))) + +(np/defpass conversion-form + {:from [l/Machine Code] :to [l/Machine Code] :args [j/JVM-Conversion j/JVM-Fact] :body true} + [conversion fact form] + (let [^Class type (:type conversion)] + (cond + (not (:parameter? conversion)) + ;; Validate the receiver before evaluating later operands. + (list 'java.lang.Class/.cast (list 'quote type) (box-form form)) + (.isPrimitive type) + (list (with-meta 'clojure.lang.RT/box {:param-tags [type]}) (typed-form fact form)) + :else + (let [local (gensym "parameter__")] + (list 'let* [(with-meta local {:tag (symbol (.getName type))}) + (list (with-meta 'clojure.lang.RT/box {:param-tags ['java.lang.Object]}) form)] + (list 'java.lang.Class/.cast (list 'quote type) local)))))) + + +(np/defpass host-form + {:from [j/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)) + :protocol + (let [locals (mapv (fn [_] (gensym "protocol__")) args) + receiver (first locals) function (second locals) arguments (subvec locals 2) + method (:method operation) + fallback (list 'if (list 'clojure.core/or (list 'clojure.core/nil? function) (list 'clojure.core/instance? clojure.lang.IFn function)) + (list* function receiver arguments) + (list 'throw (list 'new 'java.lang.IncompatibleClassChangeError "Protocol method root does not implement clojure.lang.IFn"))) + call (if-let [class (:class method)] + (list 'if (list 'clojure.core/instance? class receiver) + (list* (with-meta (symbol (.getName ^Class class) (str "." (:member method))) {:param-tags (:parameters method)}) receiver arguments) + fallback) fallback)] + ;; Even an invalid fallback root is checked only after all source operands. + (list 'let* (vec (mapcat (fn [id value] [id (box-form value)]) locals args)) call)) + :direct (let [params (:parameters operation) + fixed (if (:variadic? operation) (dec (count params)) (count params)) + args (if (:variadic? operation) + (conj (subvec args 0 fixed) + (list 'clojure.lang.ArraySeq/create (list 'clojure.lang.RT/object_array (subvec args fixed)))) args)] + (list* (with-meta (symbol (.getName ^Class (:class operation)) "invokeStatic") {:param-tags params}) args)) + :instanceof (list 'clojure.core/instance? (:class operation) (first args)) + :adapt (let [local (gensym "hinted__")] + (list 'let* [(with-meta local {:tag (symbol (.getName ^Class (:class operation)))}) (first args)] local)) + :method-value (:name operation) + :static-field (symbol (.getName ^Class (:class operation)) (name (:member operation))) + :constructor (if-let [tags (:tags operation)] + (list* (with-meta (symbol (.getName ^Class (:class operation)) "new") {:param-tags tags}) args) + (list* 'new (symbol (.getName ^Class (:class operation))) args)) + :static (list* (with-meta (symbol (.getName ^Class (:class operation)) (name (:member operation))) + (when-let [tags (:tags operation)] {:param-tags tags})) args) + :instance (if-let [class (:class operation)] + (list* (with-meta (symbol (.getName ^Class class) (str "." (:member operation))) + (when-let [tags (:tags operation)] {:param-tags tags})) args) + (case (:mode operation) + :method (list '. (first args) (list* (:member operation) (rest args))) + :member (list '. (first args) (:member operation)) + :field (list '. (first args) (symbol (str "-" (:member operation))))))) diff --git a/src/com/tailrecursion/blacktide/compiler/invariants.clj b/src/com/tailrecursion/blacktide/compiler/invariants.clj new file mode 100644 index 0000000..f6092ae --- /dev/null +++ b/src/com/tailrecursion/blacktide/compiler/invariants.clj @@ -0,0 +1,28 @@ +(ns ^:typed.clojure com.tailrecursion.blacktide.compiler.invariants + "Remove loop parameters that only forward an outer lexical value. Their + initializers remain in place, and recur keeps exactly the retained parameters." + (:require [clojure.set :as set] + [com.tailrecursion.nanopass :as np] + [com.tailrecursion.blacktide.compiler.structured-languages :as s] + [typed.clojure :as-alias t])) + +(np/defpass assigned {:from [s/Joined Expr] :to (t/Set t/Sym)} + [node] + :bind (conj (assigned (:body node)) (:target (:binding node))) + :join (conj (set/union (assigned (:value node)) (assigned (:body node))) (:target node)) + :branch (set/union (assigned (:yes node)) (assigned (:no node))) + :switch (into #{} (mapcat assigned) (:arms node)) + :loop (into (assigned (:body node)) (:slots node)) + :recur #{} :return #{} :throw #{}) + +(np/defpass expression {:from [s/Joined Expr] :to [s/Joined Expr] :args [(t/Set t/Sym)]} + [invariants node] + :bind (assoc node :body (expression invariants (:body node))) + :join (assoc node :value (expression invariants (:value node)) :body (expression invariants (:body node))) + :branch (assoc node :yes (expression invariants (:yes node)) :no (expression invariants (:no node))) + :switch (assoc node :arms (mapv #(expression invariants %) (:arms node))) + :loop (let [invariants (set/difference (set (:slots node)) (assigned (:body node)))] + (assoc node :slots (filterv #(not (contains? invariants %)) (:slots node)) + :body (expression invariants (:body node)))) + :recur (assoc node :slots (filterv #(not (contains? invariants %)) (:slots node))) + :return node :throw node) diff --git a/src/com/tailrecursion/blacktide/compiler/joins.clj b/src/com/tailrecursion/blacktide/compiler/joins.clj new file mode 100644 index 0000000..141cc29 --- /dev/null +++ b/src/com/tailrecursion/blacktide/compiler/joins.clj @@ -0,0 +1,71 @@ +(ns ^:typed.clojure com.tailrecursion.blacktide.compiler.joins + "Share acyclic branch continuations with at most one escaping definition. + Every incoming path must define the result. Postdominators nominate exits; + region proofs reject cycles and incomplete paths before recovery." + (:require [clojure.set :as set] + [com.tailrecursion.nanopass :as np] + [com.tailrecursion.blacktide.compiler.languages :as l] + [com.tailrecursion.blacktide.compiler.analysis-languages :as a] + [com.tailrecursion.blacktide.compiler.graph :as graph] + [typed.clojure :as-alias t])) + +(np/defpass postdominators {:from [l/Machine Program] :to [a/Planned Postdominators] :body true} + [machine] + (let [labels (set (keys (:blocks machine)))] + ((fn ^{::t/- [a/Planned-Postdominators :-> a/Planned-Postdominators]} solve [known] + (let [next (reduce-kv + (fn [^{::t/- a/Planned-Postdominators} out ^{::t/- l/Machine-Label} id + ^{::t/- l/Machine-Block} block] + (let [successors (graph/successors (:term block)) + common (if (seq successors) + (reduce set/intersection labels (map #(get known % #{}) successors)) #{})] + (assoc out id (conj common id)))) {} (:blocks machine))] + (if (= known next) known (recur next)))) (zipmap labels (repeat labels))))) + +(np/defpass region + {:from [l/Machine Label] :to (t/Option a/Planned-Region) + :args [l/Machine-Program l/Machine-Label] :body true} + [machine stop id] + ;; Prove blocks only after every successor is proved. Each block contributes + ;; once; shared paths do not exhaust a traversal budget. Cycles never qualify. + ((fn ^{::t/- [a/Planned-Regions :-> (t/Option a/Planned-Region)]} solve [known] + (let [next (reduce-kv + (fn [^{::t/- a/Planned-Regions} out ^{::t/- l/Machine-Label} label + ^{::t/- l/Machine-Block} block] + (let [successors (graph/successors (:term block))] + (if (and (not (contains? out label)) (seq successors) + (every? #(contains? out %) successors)) + (let [own (set (map :target (:ops block))) + regions (mapv #(get out % {:definite #{} :changed #{}}) successors)] + (assoc out label + {:definite (into own (reduce set/intersection (map :definite regions))) + :changed (into own (mapcat :changed) regions)})) out))) known (:blocks machine))] + (if-let [proof (get next id)] proof + (when (not= known next) (recur next))))) {stop {:definite #{} :changed #{}}})) + +(np/defpass candidate + {:from [l/Machine Label] :to (t/Option a/Planned-Join) + :args [l/Machine-Program a/Planned-Postdominators] :body true} + [machine known id] + (when-let [block (get (:blocks machine) id)] + (let [term (:term block) + exits (sort-by (fn [^{::t/- l/Machine-Label} target] (- (count (get known target #{})))) + (disj (get known id #{}) id))] + (when (contains? #{:branch :switch} (:op term)) + (when-let [target (first exits)] + ;; Only the nearest postdominator can form a nested lexical join. + ;; Ignore the branch's preceding definitions when proving its result. + (let [branch-machine (update machine :blocks assoc id (assoc block :ops []))] + (when-let [proof (region branch-machine target id)] + (let [outputs (set/intersection (:changed proof) (get (:live machine) target #{})) + result (or (first outputs) (first (sort (:definite proof))))] + (when (and (<= (count outputs) 1) (symbol? result) (contains? (:definite proof) result)) + {:target target :result result}))))))))) + +(np/defpass plan {:from [a/Analysis Program] :to [a/Planned Program] :body true} + [analysis] + (let [machine (:machine analysis) known (postdominators machine)] + (assoc analysis :joins + (reduce (fn [^{::t/- a/Planned-Joins} out ^{::t/- l/Machine-Label} id] + (if-let [join (candidate machine known id)] (assoc out id join) out)) + {} (keys (:blocks machine)))))) diff --git a/src/com/tailrecursion/blacktide/compiler/jvm.clj b/src/com/tailrecursion/blacktide/compiler/jvm.clj new file mode 100644 index 0000000..14614fc --- /dev/null +++ b/src/com/tailrecursion/blacktide/compiler/jvm.clj @@ -0,0 +1,217 @@ +(ns ^:typed.clojure com.tailrecursion.blacktide.compiler.jvm + "Resolve source JVM facts without evaluating expressions or running the native + analyzer. Method inference follows the pinned Clojure overload rules." + (:refer-clojure :exclude [methods]) + (:require [com.tailrecursion.nanopass :as np] + [com.tailrecursion.blacktide.compiler.jvm-languages :as l] + [typed.clojure :as-alias t])) + +(np/defpass resolved-class + {:from [l/JVM Raw] :to (t/Option java.lang.Class) :body true} + [name] + (cond (class? name) name + (symbol? name) (let [v (ns-resolve *ns* name)] (when (class? v) v)))) + +(np/defpass source-tag {:from [l/JVM Raw] :to (t/Option t/Sym) :body true} + [form] + ;; Compiler.tagOf ignores all other metadata values, including Class objects. + (let [tag (:tag (meta form))] (cond (symbol? tag) tag (string? tag) (symbol tag)))) + +(np/defpass tag-class + {:from [l/JVM Raw] :to (t/Option java.lang.Class) :body true} + [tag] + (when (some? tag) + (or (when (and (symbol? tag) (nil? (namespace tag))) (clojure.lang.Compiler$HostExpr/maybeSpecialTag tag)) + (when (symbol? tag) (clojure.lang.Compiler$HostExpr/maybeArrayClass tag)) + (when (string? tag) (clojure.lang.RT/classForNameNonLoading tag)) + (resolved-class tag) + (throw (IllegalArgumentException. (str "Unable to resolve classname: " tag)))))) + +(np/defpass fact + {:from [l/JVM Type] :to [l/JVM Fact] :args [t/Bool l/JVM-Seed] :body true} + [primitive? seed type] + (assoc seed :type type :primitive? primitive? :hint-error nil)) + +(np/defpass value-type {:from [l/JVM Fact] :to [l/JVM Type] :body true} + [fact] + (if-let [^String error (:hint-error fact)] (throw (UnsupportedOperationException. error)) (:type fact))) + +(np/defpass hint-conflict + {:from [l/JVM Type] :to (t/Option t/Str) :args [(t/Option java.lang.Class)] :body true} + [^Class hint ^Class type] + (when (and hint (class? type) (.isPrimitive hint) (.isPrimitive type) (not= hint type) + (not (and (#{"int" "byte" "short" "char"} (.getName hint)) + (#{"int" "byte" "short" "char"} (.getName type))))) + (str "Cannot coerce " type " to " hint ", use a cast instead"))) + +(np/defpass visible-class + {:from [l/JVM Type] :to [l/JVM Type] :body true} + [^Class class] + (if (and (class? class) (java.lang.reflect.Modifier/isPublic (.getModifiers class))) class :unknown)) + +(np/defpass literal-type {:from [l/JVM Raw] :to [l/JVM Type] :body true} + [value] + (cond + (nil? value) nil + (or (instance? Long value) (instance? Integer value)) Long/TYPE + (instance? Double value) Double/TYPE + (and (coll? value) (empty? value) (nil? (meta value))) + (cond (vector? value) clojure.lang.IPersistentVector + (map? value) clojure.lang.IPersistentMap + (set? value) clojure.lang.IPersistentSet + :else clojure.lang.IPersistentList) + (instance? clojure.lang.APersistentVector value) clojure.lang.APersistentVector + (instance? clojure.lang.APersistentMap value) clojure.lang.APersistentMap + (instance? clojure.lang.APersistentSet value) clojure.lang.APersistentSet + :else (visible-class (class value)))) + +(np/defpass join-type {:from [l/JVM Type] :to [l/JVM Type] :args [l/JVM-Type] :body true} + [a b] + (cond (= a b) a + (and (nil? a) (class? b) (not (.isPrimitive ^Class b))) b + (and (nil? b) (class? a) (not (.isPrimitive ^Class a))) a + :else :unknown)) + +(np/defpass methods + {:from [l/JVM Raw] :to [l/JVM Methods] :args [java.lang.Class t/Int t/Bool] :body true} + [class arity static? member] + ;; Reflector's raw Java List is an external API boundary; refine its elements. + (reduce (fn [^{::t/- l/JVM-Methods} out ^{::t/- t/Any} item] + (if (instance? java.lang.reflect.Method item) (conj out item) out)) + [] (clojure.lang.Reflector/getMethods class (int arity) (clojure.lang.Compiler/munge (str member)) static?))) + +(np/defpass static-method? + {:from [l/JVM Raw] :to t/Bool :args [java.lang.Class] :body true} + [^Class class member] + (let [methods (.getMethods class) name (clojure.lang.Compiler/munge (str member))] + (boolean + (some (fn [^{::t/- t/Int} index] + (let [^java.lang.reflect.Method method (aget methods (int index))] + (and (= name (.getName method)) (java.lang.reflect.Modifier/isStatic (.getModifiers method))))) + (range (alength methods)))))) + +(np/defpass return-type {:from [l/JVM Method] :to java.lang.Class :body true} + [member] + (if (instance? java.lang.reflect.Method member) + (.getReturnType ^java.lang.reflect.Method member) (.getDeclaringClass ^java.lang.reflect.Constructor member))) + +(np/defpass constructors + {:from [l/JVM Raw] :to [l/JVM Methods] :args [java.lang.Class t/Int] :body true} + [^Class class arity _] + (let [constructors (.getConstructors class)] + (filterv (fn [^{::t/- l/JVM-Method :tag java.lang.reflect.Executable} member] (= arity (.getParameterCount member))) + (mapv (fn [^{::t/- t/Int} index] (aget constructors (int index))) (range (alength constructors)))))) + +(np/defpass parameters + {:from [l/JVM Method] :to (t/Vec java.lang.Class) :body true} + [^java.lang.reflect.Executable method] + (let [params (.getParameterTypes method)] + (mapv (fn [^{::t/- t/Int} index] (aget params (int index))) (range (alength params))))) + +(np/defpass select-method + {:from [l/JVM Methods] :to (t/Option l/JVM-Method) :args [l/JVM-Types l/JVM-Tags] :body true} + [types tags methods] + (if tags + (let [tags (mapv #(when-not (= '_ %) (tag-class %)) tags) + matches (filter (fn [^{::t/- l/JVM-Method} method] + (and (not (.isSynthetic ^java.lang.reflect.Executable method)) (= (count tags) (.getParameterCount ^java.lang.reflect.Executable method)) + (every? true? (map #(or (nil? %1) (= %1 %2)) tags (parameters method))))) methods)] + (if (= 1 (count matches)) (first matches) + (throw (IllegalArgumentException. "param-tags do not resolve a single method")))) + (if (= 1 (count methods)) + (first methods) + (let [result + (reduce + (fn [^{::t/- l/JVM-Selection} selection ^{::t/- l/JVM-Method} method] + (let [params (parameters method) + exact? (= types params) + match? (every? true? (map (fn [^{::t/- Class :tag Class} p ^{::t/- l/JVM-Type} a] + (if (nil? a) (not (.isPrimitive p)) + (clojure.lang.Reflector/paramArgTypeMatch p (if (= a :unknown) Object a)))) params types)) + previous (:method selection)] + (cond + exact? (if (or (not (:exact? selection)) (nil? previous) + (.isAssignableFrom ^Class (return-type previous) (return-type method))) + {:method method :exact? true :tied? false} + (assoc selection :tied? false)) + (and match? (not (:exact? selection))) + (cond (nil? previous) (assoc selection :method method) + (clojure.lang.Compiler/subsumes (.getParameterTypes ^java.lang.reflect.Executable method) (.getParameterTypes ^java.lang.reflect.Executable previous)) + (assoc selection :method method :tied? false) + (= params (parameters previous)) + (if (.isAssignableFrom ^Class (return-type previous) (return-type method)) + (assoc selection :method method) selection) + (not (clojure.lang.Compiler/subsumes (.getParameterTypes ^java.lang.reflect.Executable previous) (.getParameterTypes ^java.lang.reflect.Executable method))) + (assoc selection :tied? true) + :else selection) + :else selection))) {:method nil :exact? false :tied? false} methods)] + (if (:tied? result) (throw (IllegalArgumentException. "More than one matching method found")) (:method result)))))) + +(np/defpass public-member + {:from [l/JVM Raw] :to (t/Option l/JVM-Method) :body true} + [member] + (cond + (instance? java.lang.reflect.Constructor member) member + (instance? java.lang.reflect.Method member) + (let [class (.getDeclaringClass ^java.lang.reflect.Method member)] + (if (java.lang.reflect.Modifier/isPublic (.getModifiers ^Class class)) member + (clojure.lang.Reflector/getAsMethodOfPublicBase class member))))) + +(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 + :protocol Object + :direct (:return operation) + :instanceof Boolean/TYPE + :adapt (:class operation) + :constructor (:class operation) + :method-value clojure.lang.AFn + :static-field (when-let [field (clojure.lang.Reflector/getField (:class operation) (name (:member operation)) true)] + (.getType field)) + :static (if-let [method (select-method types (:tags operation) + (methods (:class operation) (count types) true (:member operation)))] + (return-type method) :unknown) + :instance + (let [class (or (:class operation) (when (class? (first types)) (first types))) + args (subvec types 1)] + (if (class? class) + (let [methods (methods class (count args) false (:member operation)) + field? (and (empty? args) (or (= :field (:mode operation)) + (and (= :member (:mode operation)) (empty? methods))))] + (if field? + (if-let [field (clojure.lang.Reflector/getField class (name (:member operation)) false)] (.getType field) :unknown) + (if-let [method (public-member (select-method args (:tags operation) methods))] (return-type method) :unknown))) + :unknown))) + +(np/defpass prepare-call + {:from [l/JVM Operation] :to [l/JVM Plan] :args [l/JVM-Types] :body true} + [types operation] + (let [unchanged {:operation operation :conversions (mapv (fn [_] nil) types)}] + (case (:op operation) + (:adapt :instanceof :method-value :static-field :protocol :keyword-site) unchanged + :direct (let [params (:parameters operation) + fixed (if (:variadic? operation) (pop params) params)] + {:operation operation + :conversions (into (mapv (fn [type] {:type type :parameter? true}) fixed) + (repeat (- (count types) (count fixed)) nil))}) + :constructor + (if-let [member (select-method types (:tags operation) (constructors (:class operation) (count types) nil))] + {:operation (cond-> operation (:tags operation) (assoc :tags (parameters member))) + :conversions (mapv (fn [type] {:type type :parameter? true}) (parameters member))} unchanged) + :static + (if-let [member (select-method types (:tags operation) (methods (:class operation) (count types) true (:member operation)))] + {:operation (cond-> operation (:tags operation) (assoc :tags (parameters member))) + :conversions (mapv (fn [type] {:type type :parameter? true}) (parameters member))} unchanged) + :instance + (let [class (or (:class operation) (when (class? (first types)) (first types))) args (subvec types 1)] + (if (class? class) + (let [members (methods class (count args) false (:member operation)) + field? (and (empty? args) (or (= :field (:mode operation)) + (and (= :member (:mode operation)) (empty? members))))] + (if field? unchanged + (if-let [member (public-member (select-method args (:tags operation) members))] + (let [class (.getDeclaringClass ^java.lang.reflect.Executable member)] + {:operation (cond-> (assoc operation :class class :mode :method) (:tags operation) (assoc :tags (parameters member))) + :conversions (into [{:type class :parameter? false}] + (mapv (fn [type] {:type type :parameter? true}) (parameters member)))}) unchanged))) unchanged))))) diff --git a/src/com/tailrecursion/blacktide/compiler/jvm_languages.clj b/src/com/tailrecursion/blacktide/compiler/jvm_languages.clj new file mode 100644 index 0000000..7a04a91 --- /dev/null +++ b/src/com/tailrecursion/blacktide/compiler/jvm_languages.clj @@ -0,0 +1,35 @@ +(ns com.tailrecursion.blacktide.compiler.jvm-languages + "JVM facts and host operation descriptors contain no executable source children. + Unknown type, known nil and a known JVM class are distinct facts." + (:require [com.tailrecursion.nanopass :as np] + [typed.clojure :as-alias t])) + +(np/deflanguage JVM + {Raw t/Any + Type (t/U ':unknown nil java.lang.Class) + Seed (t/HMap :mandatory {:hint Raw :var-meta (t/Map t/Any t/Any)} :complete? true) + Fact (t/HMap :mandatory {:hint Raw :var-meta (t/Map t/Any t/Any) + :type Type :primitive? t/Bool :hint-error (t/Option t/Str)} :complete? true) + Types (t/Vec Type) + Tags (t/Option (t/Vec Raw)) + 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) + (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) + (t/HMap :mandatory {:op ':instanceof :class java.lang.Class} :complete? true) + (t/HMap :mandatory {:op ':adapt :class java.lang.Class} :complete? true) + (t/HMap :mandatory {:op ':constructor :class java.lang.Class :tags Tags} :complete? true) + (t/HMap :mandatory {:op ':static :class java.lang.Class :member t/Sym :tags Tags} :complete? true) + (t/HMap :mandatory {:op ':instance :class (t/Option java.lang.Class) :member t/Sym + :mode Mode :tags Tags} :complete? true) + (t/HMap :mandatory {:op ':static-field :class java.lang.Class :member t/Sym} :complete? true) + (t/HMap :mandatory {:op ':method-value :name t/Sym} :complete? true)) + Syntax (t/HMap :mandatory {:operation Operation :operands (t/Vec Raw)} :complete? true) + Method (t/U java.lang.reflect.Method java.lang.reflect.Constructor) + Methods (t/Vec Method) + Conversion (t/HMap :mandatory {:type java.lang.Class :parameter? t/Bool} :complete? true) + Plan (t/HMap :mandatory {:operation Operation :conversions (t/Vec (t/Option Conversion))} :complete? true) + Selection (t/HMap :mandatory {:method (t/Option Method) :exact? t/Bool :tied? t/Bool} :complete? true)}) diff --git a/src/com/tailrecursion/blacktide/compiler/languages.clj b/src/com/tailrecursion/blacktide/compiler/languages.clj new file mode 100644 index 0000000..92f9341 --- /dev/null +++ b/src/com/tailrecursion/blacktide/compiler/languages.clj @@ -0,0 +1,54 @@ +(ns com.tailrecursion.blacktide.compiler.languages + "Loophole's backend languages. Stable operands are separate from evaluations; + allocated machines explicitly name the values live at every block entry." + (:require [com.tailrecursion.nanopass :as np] + [com.tailrecursion.blacktide.compiler.source-languages :as s] + [typed.clojure :as-alias t] + [com.tailrecursion.blacktide.compiler.jvm-languages :as j])) + +(np/deflanguage Machine + {Atom (t/U (t/HMap :mandatory {:op ':literal :value t/Any} :complete? true) + (t/HMap :mandatory {:op ':local :id t/Sym} :complete? true)) + Operand (t/HMap :mandatory {:value Atom :jvm j/JVM-Fact} :complete? true) + Rhs (t/U (t/HMap :mandatory {:op ':convert :conversion j/JVM-Conversion :value Operand} :complete? true) + (t/HMap :mandatory {:op ':dispatch :value Operand :spec s/Bound-CaseSpec} :complete? true) + (t/HMap :mandatory {:op ':native :value s/Native-Expr} :complete? true) + (t/HMap :mandatory {:op ':host :operation j/JVM-Operation :args (t/Vec Operand)} :complete? true) + (t/HMap :mandatory {:op ':value :value Atom} :complete? true) + (t/HMap :mandatory {:op ':global :name t/Sym} :complete? true) + (t/HMap :mandatory {:op ':external :name t/Sym :jvm j/JVM-Fact} :complete? true) + (t/HMap :mandatory {:op ':var :name t/Sym} :complete? true) + (t/HMap :mandatory {:op ':construct :kind s/Bound-CollectionKind :items (t/Vec Atom) :unique? t/Bool} :complete? true) + (t/HMap :mandatory {:op ':metadata :value Atom :metadata Atom} :complete? true) + (t/HMap :mandatory {:op ':invoke :function Atom :args (t/Vec Atom)} :complete? true)) + Operation (t/HMap :mandatory {:target t/Sym :value Rhs} :complete? true) + Term (t/U (t/HMap :mandatory {:op ':jump :target Label} :complete? true) + (t/HMap :mandatory {:op ':switch :test Atom :targets (t/Vec Label)} :complete? true) + (t/HMap :mandatory {:op ':branch :test Atom :yes Label :no Label} :complete? true) + (t/HMap :mandatory {:op ':return :value Atom} :complete? true) + (t/HMap :mandatory {:op ':throw :value Atom} :complete? true)) + Block (t/HMap :mandatory {:ops (t/Vec Operation) :term Term} :complete? true) + Program (t/HMap :mandatory {:entry Label :slots (t/Vec t/Sym) + :blocks (t/Map Label Block) :live (t/Map Label (t/Set t/Sym))} :complete? true) + Label t/Int + Name t/Sym + Code (t/U t/Sym (t/Seq t/Any))}) + +(np/derive-language CFG Machine + {:replace + {Program (t/HMap :mandatory {:entry Label :blocks (t/Map Label Block)} :complete? true) + BlockFacts (t/HMap :mandatory {:reads (t/Set t/Sym) :defs (t/Set t/Sym) + :successors (t/Vec Label)} :complete? true) + Facts (t/Map Label BlockFacts) + Labels (t/Set Label) + Flow (t/Map Label (t/Set t/Sym)) + Predecessors (t/Map Label Labels)}}) + +(np/derive-language Symbolic CFG + {:add {Term [(t/HMap :mandatory {:op ':transfer :name t/Sym :site s/Bound-Site} :complete? true)]} + :replace + {LabelTable (t/Map t/Sym Label) + Program (t/HMap :mandatory {:entry Label :blocks (t/Map Label Block) :labels LabelTable} :complete? true) + Builder (t/HMap :mandatory {:supply s/Bound-Supply :ops (t/Vec Operation) :next Label :current Label + :blocks (t/Map Label Block) :labels LabelTable + :loops (t/Map t/Sym Label)} :complete? true)}}) diff --git a/src/com/tailrecursion/blacktide/compiler/loop_facts.clj b/src/com/tailrecursion/blacktide/compiler/loop_facts.clj new file mode 100644 index 0000000..74c805f --- /dev/null +++ b/src/com/tailrecursion/blacktide/compiler/loop_facts.clj @@ -0,0 +1,186 @@ +(ns ^:typed.clojure com.tailrecursion.blacktide.compiler.loop-facts + "Recover source loop representations using Clojure's monotone boxing analysis. + Reanalyze expanded bindings in order without repeating user macros. Native + syntax retains its initializers; lowered loops materialize the conversions." + (:require [com.tailrecursion.nanopass :as np] + [com.tailrecursion.blacktide.compiler.source-languages :as s] + [com.tailrecursion.blacktide.compiler.jvm-languages :as j] + [com.tailrecursion.blacktide.compiler.jvm :as jvm] + [com.tailrecursion.blacktide.compiler.errors :as errors] + [com.tailrecursion.blacktide.compiler.source-jvm :as facts] + [typed.clojure :as-alias t])) + +(np/defpass children {:from [s/Bound Expr] :to [s/Bound Exprs] :body true} + [expr] + (let [node (:node expr)] + (case (:op node) + :if [(:test node) (:then node) (:else node)] + :case (into [(:selector node)] (:arms node)) + (:dispatch :throw) [(:value node)] + :do (:forms node) + :collection (cond-> (:items node) (:metadata node) (conj (:metadata node))) + (:host :recur) (:args node) + :invoke (into [(:function node)] (:args node)) + (:let :loop) (conj (mapv :init (:bindings node)) (:body node)) + (:setq :psetq) (mapv :value (:pairs node)) + []))) + + +(np/defpass primitive {:from [j/JVM Fact] :to (t/Option java.lang.Class) :body true} + [fact] + (let [type (:type fact)] + (when (and (:primitive? fact) (class? type) (.isPrimitive ^Class type) (not= Void/TYPE type)) type))) + +(np/defpass inferred {:from [s/Bound Expr] :to [s/Bound Expr] :args [s/Bound-Node] :body true} + [node expr] + (assoc expr :node node :jvm (facts/infer {:hint (:hint (:jvm expr)) :var-meta (:var-meta (:jvm expr))} node))) + +(np/defpass converted {:from [s/Bound Expr] :to [s/Bound Expr] :args [java.lang.Class] :body true} + [^Class type expr] + (let [node {:op :host :operation {:op :static :class clojure.lang.RT + :member (symbol (str (.getName type) "Cast")) :tags [type]} + :args [expr]}] + (assoc expr :node node :jvm (facts/infer {:hint nil :var-meta {}} node)))) + +(np/defpass boxed {:from [s/Bound Expr] :to [s/Bound Expr] :body true} + [expr] + (let [node {:op :host :operation {:op :static :class clojure.lang.RT :member 'box :tags nil} :args [expr]}] + ;; Reanalysis can select RT.box(Object), rather than RT.box(long/double). + (assoc expr :node node :jvm (facts/infer {:hint nil :var-meta {}} node)))) + +(np/defpass initializer {:from [s/Bound Expr] :to [s/Bound Expr] :args [t/Bool] :body true} + [box? init] + (let [type (primitive (:jvm init))] + (if box? (boxed init) + (if-let [promoted (cond (= type Integer/TYPE) Long/TYPE (= type Float/TYPE) Double/TYPE)] + (converted promoted init) init)))) + +(declare expression solve) +(np/defpass bindings + {:from [s/Bound Bindings] :to (t/HVec [(t/Map t/Sym s/Bound-Local) s/Bound-Bindings]) + :args [(t/Map t/Sym s/Bound-Local) t/Bool (t/Set t/Sym)] :body true} + [env loop? boxed-ids originals] + (reduce (fn [^{::t/- (t/HVec [(t/Map t/Sym s/Bound-Local) s/Bound-Bindings])} state + ^{::t/- s/Bound-Binding} binding] + (let [[env out] state init (expression env (:init binding)) local (:local binding) + effective (if loop? (initializer (contains? boxed-ids (:id local)) init) init) + value (facts/binding-facts (:name local) (:mutable? local) (:jvm effective)) + local (assoc local :jvm value)] + [(assoc env (:id local) local) (conj out {:local local :init init})])) [env []] originals)) + +(np/defpass expression + {:from [s/Bound Expr] :to [s/Bound Expr] :args [(t/Map t/Sym s/Bound-Local)] :body true} + [env expr] + (let [node (:node expr) + node (case (:op node) + :local (assoc node :local (get env (:id (:local node)) (:local node))) + :if (assoc node :test (expression env (:test node)) + :then (expression env (:then node)) :else (expression env (:else node))) + :case (assoc node :selector (expression env (:selector node)) + :arms (mapv #(expression env %) (:arms node))) + (:dispatch :throw) (assoc node :value (expression env (:value node))) + :do (assoc node :forms (mapv #(expression env %) (:forms node))) + :collection (assoc node :items (mapv #(expression env %) (:items node)) + :metadata (some->> (:metadata node) (expression env))) + :host (assoc node :args (mapv #(expression env %) (:args node))) + :recur (assoc node :args (mapv #(expression env %) (:args node)) + :target (assoc (:target node) :bindings + (mapv #(get env (:id %) %) (:bindings (:target node))))) + :invoke (assoc node :function (expression env (:function node)) + :args (mapv #(expression env %) (:args node))) + :let (let [[inner values] (bindings env false #{} (:bindings node))] + (assoc node :bindings values :body (expression inner (:body node)))) + :loop (:node (first (solve env expr))) + (:setq :psetq) (assoc node :pairs (mapv #(assoc % :value (expression env (:value %))) (:pairs node))) + node)] + (inferred node expr))) + +(np/defpass recurs {:from [s/Bound Expr] :to [s/Bound Exprs] :args [t/Sym] :body true} + [target expr] + (if (and (= :recur (:op (:node expr))) (= target (:id (:target (:node expr))))) + [expr] (into [] (mapcat #(recurs target %)) (children expr)))) + +(np/defpass compatible? {:from [j/JVM Fact] :to t/Bool :args [java.lang.Class] :body true} + [type fact] + (let [actual (primitive fact)] + (or (= type actual) (and (= type Long/TYPE) (contains? #{Integer/TYPE Short/TYPE Byte/TYPE Character/TYPE} actual)) + (and (= type Double/TYPE) (= actual Float/TYPE))))) + +(np/defpass solve + {:from [s/Bound Expr] :to (t/HVec [s/Bound-Expr (t/Set t/Sym)]) + :args [(t/Map t/Sym s/Bound-Local)] :body true} + [env expr] + (let [node (:node expr)] + (if-not (= :loop (:op node)) (throw (IllegalStateException. "Expected source loop")) + ((fn ^{::t/- [(t/Set t/Sym) :-> (t/HVec [s/Bound-Expr (t/Set t/Sym)])]} fixed [boxed-ids] + (let [[env values] (bindings env true boxed-ids (:bindings node)) + body (expression env (:body node)) + mismatches (reduce + (fn [^{::t/- (t/Set t/Sym)} out ^{::t/- s/Bound-Expr} edge] + (into out (keep identity) + (map (fn [^{::t/- s/Bound-Binding} binding ^{::t/- s/Bound-Expr} arg] + (let [local (:local binding) type (primitive (:jvm local))] + (when (and type (contains? #{Long/TYPE Double/TYPE} type) + (not (compatible? type (:jvm arg)))) (:id local)))) + values (:args (:node edge))))) boxed-ids (recurs (:id node) body))] + (if (not= boxed-ids mismatches) (recur mismatches) + [(inferred (assoc node :bindings values :body body) expr) boxed-ids]))) #{})))) + +;; RecurExpr's parser admits byte/short/char to long, but its emitter rejects +;; them. Preserve that validation before replacing recur with graph edges. +(np/defpass recur-error + {:from [s/Bound Expr] :to (t/Option (t/HVec [s/Bound-Local s/Bound-Expr])) :body true} + [expr] + (let [node (:node expr)] + (when (= :loop (:op node)) + (first + (keep (fn [^{::t/- s/Bound-Expr} edge] + (first + (keep identity + (map (fn [^{::t/- s/Bound-Binding} binding ^{::t/- s/Bound-Expr} arg] + (let [local (:local binding) type (primitive (:jvm local)) actual (primitive (:jvm arg))] + (when (and type (not (or (= type actual) + (and (= type Long/TYPE) (= actual Integer/TYPE)) + (and (= type Double/TYPE) (= actual Float/TYPE))))) + [local arg]))) (:bindings node) (:args (:node edge)))))) + (recurs (:id node) (:body node))))))) + +(np/defpass recurrence {:from [s/Bound Expr] :to [s/Bound Expr] :body true} + [expr] + (let [node (:node expr)] + (case (:op node) + :recur (assoc expr :node + (assoc node :args + (mapv (fn [local arg] + (if-let [type (primitive (:jvm local))] + (if (= type (primitive (:jvm arg))) arg (converted type arg)) arg)) + (:bindings (:target node)) (:args node)))) + :if (assoc expr :node (assoc node :then (recurrence (:then node)) :else (recurrence (:else node)))) + :do (assoc expr :node (assoc node :forms (mapv recurrence (:forms node)))) + :case (assoc expr :node (assoc node :arms (mapv recurrence (:arms node)))) + :let (assoc expr :node (assoc node :body (recurrence (:body node)))) + expr))) + +(np/defpass materialize {:from [s/Bound Expr] :to [s/Bound Expr] :args [(t/Set t/Sym)] :body true} + [boxed-ids expr] + (let [node (:node expr)] + (if-not (= :loop (:op node)) (throw (IllegalStateException. "Expected source loop")) + (assoc expr :node + (assoc node :body (recurrence (:body node)) :bindings + (mapv (fn [binding] + (let [local (:local binding) init (:init binding) + effective (initializer (contains? boxed-ids (:id local)) init) + type (primitive (:jvm init)) + ;; Keep primitive provenance available to storage analysis. + init (if (and type (contains? boxed-ids (:id local))) (converted type init) effective) + hint (jvm/tag-class (:hint (:jvm local))) + init (if hint (inferred {:op :host :operation {:op :adapt :class hint} :args [init]} init) init)] + (assoc binding :init init))) (:bindings node))))))) + +(np/defpass lower {:from [s/Bound Expr] :to [s/Bound Expr] :body true} + [expr] + (let [[expr boxed-ids] (solve {} expr)] + (when-let [[local arg] (recur-error expr)] + (errors/fail :invalid-recur-type "recur argument does not match its primitive loop binding" + {:binding (:name local) :expected (:type (:jvm local)) :actual (:type (:jvm arg))} (:site arg))) + (materialize boxed-ids expr))) diff --git a/src/com/tailrecursion/blacktide/compiler/lower.clj b/src/com/tailrecursion/blacktide/compiler/lower.clj new file mode 100644 index 0000000..1d812ea --- /dev/null +++ b/src/com/tailrecursion/blacktide/compiler/lower.clj @@ -0,0 +1,145 @@ +(ns ^:typed.clojure com.tailrecursion.blacktide.compiler.lower + "Lower ordered expressions into a CFG with explicit branch joins and exits. + Operands have explicit bindings; operations retain their execution position." + (:require [com.tailrecursion.blacktide.compiler.errors :as errors] + [com.tailrecursion.nanopass :as np] + [com.tailrecursion.blacktide.compiler.source-languages :as s] + [com.tailrecursion.blacktide.compiler.languages :as l] + [com.tailrecursion.blacktide.compiler.source :as source] + [typed.clojure :as-alias t])) + +(declare lower lower-forms lower-bindings) + +(np/defpass fresh-block + {:from [l/Symbolic Builder] :to (t/HVec [l/Symbolic-Builder l/Symbolic-Label]) :body true} + [builder] + [(assoc builder :next (inc (:next builder))) (:next builder)]) + +(np/defpass start + {:from [l/Symbolic Label] :to [l/Symbolic Builder] :args [l/Symbolic-Builder] :body true} + [builder label] + (assoc builder :current label :ops [])) + +(np/defpass seal + {:from [l/Symbolic Term] :to [l/Symbolic Builder] :args [l/Symbolic-Builder] :body true} + [builder term] + (if (contains? (:blocks builder) (:current builder)) + (throw (ex-info "Control-flow block already has a terminator" {:kind :invalid-graph :block (:current builder)})) + (assoc builder :blocks (assoc (:blocks builder) (:current builder) {:ops (:ops builder) :term term}) :ops []))) + +(np/defpass evaluate + {:from [l/Symbolic Rhs] :to (t/HVec [l/Symbolic-Builder l/Symbolic-Atom]) + :args [l/Symbolic-Builder] :body true} + [builder rhs] + (let [[supply id] (source/fresh "value" (:supply builder))] + [(-> builder (assoc :supply supply) (update :ops conj {:target id :value rhs})) {:op :local :id id}])) + +(np/defpass lower-forms + {:from [s/Ordered Exprs] :to (t/HVec [l/Symbolic-Builder (t/Vec l/Symbolic-Atom)]) + :args [l/Symbolic-Builder] :body true} + [builder forms] + (reduce (fn [^{::t/- (t/HVec [l/Symbolic-Builder (t/Vec l/Symbolic-Atom)])} acc ^{::t/- s/Ordered-Expr} expr] + (let [[builder out] acc [builder value] (lower builder expr)] [builder (conj out value)])) + [builder []] forms)) + +(np/defpass lower-bindings + {:from [s/Ordered Bindings] :to [l/Symbolic Builder] :args [l/Symbolic-Builder] :body true} + [builder bindings] + (reduce (fn [^{::t/- l/Symbolic-Builder} builder ^{::t/- s/Ordered-Binding} binding] + (let [[builder value] (lower builder (:init binding))] + (update builder :ops conj {:target (:id (:local binding)) :value {:op :value :value value}}))) + builder bindings)) + +(np/defpass lower-node + {:from [s/Ordered Node] :to (t/HVec [l/Symbolic-Builder l/Symbolic-Atom]) + :args [l/Symbolic-Builder s/Ordered-Expr]} + [builder expr node] + :literal [builder {:op :literal :value (:value node)}] + :local (let [local (:local node)] + (evaluate builder (if (:external? local) {:op :external :name (:id local) :jvm (:jvm local)} + {:op :value :value {:op :local :id (:id local)}}))) + :global (evaluate builder {:op :global :name (:name node)}) + :var (evaluate builder {:op :var :name (:name node)}) + :native (evaluate builder node) + :dispatch (evaluate builder {:op :dispatch :spec (:spec node) + :value {:value {:op :local :id (:id (:value node))} :jvm (:jvm (:value node))}}) + :select + (let [[builder test] (lower builder (:selector node)) + [builder join] (fresh-block builder) + [supply result] (source/fresh "case_result" (:supply builder)) + [builder targets] (reduce (fn [^{::t/- (t/HVec [l/Symbolic-Builder (t/Vec l/Symbolic-Label)])} acc + ^{::t/- s/Ordered-Expr} _] + (let [[builder targets] acc [builder label] (fresh-block builder)] + [builder (conj targets label)])) + [(assoc builder :supply supply) []] (:arms node)) + builder (seal builder {:op :switch :test test :targets targets}) + builder (reduce (fn [^{::t/- l/Symbolic-Builder} builder ^{::t/- t/Int} index] + (let [[builder value] (lower (start builder (nth targets index)) (nth (:arms node) index))] + (seal (update builder :ops conj {:target result :value {:op :value :value value}}) + {:op :jump :target join}))) builder (range (count targets)))] + [(start builder join) {:op :local :id result}]) + :loop (let [builder (lower-bindings builder (:bindings node)) + [builder header] (fresh-block builder) + builder (assoc builder :loops (assoc (:loops builder) (:id node) header))] + (lower (start (seal builder {:op :jump :target header}) header) (:body node))) + :recur (let [target (:target node) + header (or (get (:loops builder) (:id target)) + (errors/fail :invalid-graph "Missing source recur target" {} (:site expr))) + writes (mapv (fn [local arg] {:target (:id local) :value {:op :value :value {:op :local :id (:id arg)}}}) + (:bindings target) (:args node)) + builder (seal (update builder :ops into writes) {:op :jump :target header}) + [builder next] (fresh-block builder)] + [(start builder next) {:op :literal :value nil}]) + :if (let [[builder test] (lower builder (:test node)) + [builder yes] (fresh-block builder) [builder no] (fresh-block builder) [builder join] (fresh-block builder) + [supply result] (source/fresh "join" (:supply builder)) + builder (seal (assoc builder :supply supply) {:op :branch :test test :yes yes :no no}) + [builder a] (lower (start builder yes) (:then node)) + builder (seal (update builder :ops conj {:target result :value {:op :value :value a}}) {:op :jump :target join}) + [builder b] (lower (start builder no) (:else node)) + builder (seal (update builder :ops conj {:target result :value {:op :value :value b}}) {:op :jump :target join})] + [(start builder join) {:op :local :id result}]) + :throw (let [[builder value] (lower builder (:value node)) + builder (seal builder {:op :throw :value value}) + [builder next] (fresh-block builder)] + [(start builder next) {:op :literal :value nil}]) + :write (let [[builder value] (lower builder (:value node))] + [(update builder :ops conj {:target (:id (:target node)) :value {:op :value :value value}}) value]) + :convert (evaluate builder {:op :convert :conversion (:conversion node) + :value {:value {:op :local :id (:id (:value node))} :jvm (:jvm (:value node))}}) + :host-call (evaluate builder {:op :host :operation (:operation node) + :args (mapv (fn [local] {:value {:op :local :id (:id local)} :jvm (:jvm local)}) (:args node))}) + :invoke (evaluate builder {:op :invoke :function {:op :local :id (:id (:function node))} + :args (mapv (fn [local] {:op :local :id (:id local)}) (:args node))}) + :build (evaluate builder {:op :construct :kind (:kind node) :unique? (:unique? node) + :items (mapv (fn [local] {:op :local :id (:id local)}) (:items node))}) + :metadata (evaluate builder {:op :metadata :value {:op :local :id (:id (:value node))} + :metadata {:op :local :id (:id (:metadata node))}}) + :label (let [key (:name node)] + (when (contains? (:labels builder) key) + (errors/fail :duplicate-label "Duplicate label" {:label (:name node)} (:site expr))) + (let [[builder label] (fresh-block builder) + builder (assoc builder :labels (assoc (:labels builder) key label)) + builder (seal builder {:op :jump :target label})] + [(start builder label) {:op :literal :value nil}])) + :goto (let [builder + (if (= 'end (:name node)) + (seal builder {:op :return :value {:op :literal :value nil}}) + (seal builder {:op :transfer :name (:name node) + :site (:site expr)})) + [builder next] (fresh-block builder)] + [(start builder next) {:op :literal :value nil}]) + :do (let [[builder values] (lower-forms builder (:forms node))] + [builder (or (peek values) {:op :literal :value nil})]) + :let (lower (lower-bindings builder (:bindings node)) (:body node))) + +(np/defpass lower + {:from [s/Ordered Expr] :to (t/HVec [l/Symbolic-Builder l/Symbolic-Atom]) :args [l/Symbolic-Builder] :body true} + [builder expr] + (lower-node builder expr (:node expr))) + +(np/defpass compile-graph + {:from [s/Ordered Expr] :to [l/Symbolic Program] :args [s/Bound-Supply] :body true} + [supply expr] + (let [[builder value] (lower {:supply supply :ops [] :next 1 :current 0 :blocks {} :labels {} :loops {}} expr)] + {:entry 0 :labels (:labels builder) :blocks (:blocks (seal builder {:op :return :value value}))})) diff --git a/src/com/tailrecursion/blacktide/compiler/native.clj b/src/com/tailrecursion/blacktide/compiler/native.clj new file mode 100644 index 0000000..fdc6176 --- /dev/null +++ b/src/com/tailrecursion/blacktide/compiler/native.clj @@ -0,0 +1,108 @@ +(ns ^:typed.clojure com.tailrecursion.blacktide.compiler.native + "Analyze native free reads and recover their source types from machine storage. + Primitive source reads cannot observe the slot's box; reference reads retain + it. Internal bindings keep native inference." + (:require [clojure.set :as set] + [com.tailrecursion.nanopass :as np] + [com.tailrecursion.blacktide.compiler.source-languages :as s] + [com.tailrecursion.blacktide.compiler.languages :as l] + [com.tailrecursion.blacktide.compiler.collections :as collections] + [com.tailrecursion.blacktide.compiler.host-emit :as host] + [com.tailrecursion.blacktide.compiler.case-emit :as cases] + [com.tailrecursion.blacktide.compiler.jvm-languages :as j] + [typed.clojure :as-alias t])) + +(declare boxed-reads form) + +(np/defpass hinted-form {:from [l/Machine Code] :to [l/Machine Code] :args [j/JVM-Raw] :body true} + [hint code] + ;; Var return tags may be Class objects; source form tags cannot be. + (let [hint (if (class? hint) (symbol (.getName ^Class hint)) hint)] + (with-meta (if (symbol? code) code (apply list code)) + (cond-> (meta code) hint (assoc :tag hint))))) + +(np/defpass reads-node {:from [s/Native Node] :to (t/Set t/Sym) + :args [(t/Map t/Sym java.lang.Class)]} + [types node] + :literal #{} :global #{} :var #{} + :local (if (:external? (:local node)) #{} #{(:id (:local node))}) + :host (into #{} (mapcat (partial boxed-reads types)) (:args node)) + :invoke (into (boxed-reads types (:function node)) (mapcat (partial boxed-reads types)) (:args node)) + :if (set/union (boxed-reads types (:test node)) (boxed-reads types (:then node)) (boxed-reads types (:else node))) + :case (into (boxed-reads types (:selector node)) (mapcat (partial boxed-reads types)) (:arms node)) + :dispatch (boxed-reads types (:value node)) + :throw (boxed-reads types (:value node)) + :collection (into (if-let [metadata (:metadata node)] (boxed-reads types metadata) #{}) (mapcat (partial boxed-reads types)) (:items node)) + :do (into #{} (mapcat (partial boxed-reads types)) (:forms node)) + :recur (into #{} (mapcat (partial boxed-reads types)) (:args node)) + :let (set/difference (into (boxed-reads types (:body node)) (mapcat #(boxed-reads types (:init %))) (:bindings node)) + (set (map #(get-in % [:local :id]) (:bindings node)))) + :loop (set/difference (into (boxed-reads types (:body node)) (mapcat #(boxed-reads types (:init %))) (:bindings node)) + (set (map #(get-in % [:local :id]) (:bindings node))))) + +(np/defpass boxed-reads {:from [s/Native Expr] :to (t/Set t/Sym) + :args [(t/Map t/Sym java.lang.Class)] :body true} + [types expr] + (let [node (:node expr) fact (:jvm expr)] + (if (and (= :local (:op node)) (:primitive? fact) + (= (:type fact) (get types (:id (:local node))))) + #{} (reads-node types node)))) + +(np/defpass reads {:from [s/Native Expr] :to (t/Set t/Sym) :body true} + [expr] (boxed-reads {} expr)) + + +(np/defpass literal-values {:from [s/Native Exprs] :to (t/Option (t/Vec t/Any)) :body true} + [items] + (reduce (fn [^{::t/- (t/Option (t/Vec t/Any))} values ^{::t/- s/Native-Expr} expr] + (let [node (:node expr)] + (if (and values (= :literal (:op node))) (conj values (:value node)) (reduced nil)))) [] items)) + +(np/defpass bindings-form + {:from [s/Native Bindings] :to (t/Vec t/Any) :args [(t/Set t/Sym)] :body true} + [bound bindings] + (vec (mapcat (fn [^{::t/- s/Native-Binding} binding] + [(with-meta (:id (:local binding)) (meta (:name (:local binding)))) + (form bound (:init binding))]) bindings))) + + +(np/defpass node-form + {:from [s/Native Node] :to [l/Machine Code] :args [(t/Set t/Sym) s/Native-Expr]} + [bound expr node] + :literal (list 'quote (:value node)) + :global (host/typed-form (:jvm expr) (host/global-form (:name node))) + :var (list 'var (:name node)) + :local (let [local (:local node) id (:id local)] + (if (contains? bound id) + (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))))) + :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)) + :throw (list 'throw (form bound (:value node))) + :do (list* 'do (mapv #(form bound %) (:forms node))) + :recur (list* 'recur (mapv #(form bound %) (:args node))) + :let (let [bound (into bound (map #(:id (:local %))) (:bindings node))] + (list 'let* (bindings-form bound (:bindings node)) (form bound (:body node)))) + :loop (let [bound (into bound (map #(:id (:local %))) (:bindings node))] + (list 'loop* (bindings-form bound (:bindings node)) (form bound (:body node)))) + :collection + (let [items (:items node) metadata (:metadata node) values (literal-values items) + keys (when (= :map (:kind node)) (literal-values (vec (take-nth 2 items)))) + unique? (boolean (and keys (= (count keys) (count (set keys))))) + _ (when (and (nil? metadata) keys (not unique?)) + (throw (IllegalArgumentException. "Duplicate constant keys in map"))) + value (if (and (nil? metadata) values) + (list 'quote (collections/constant values (:kind node))) + (collections/form (mapv #(form bound %) items) unique? (:kind node)))] + (if metadata (list '. value 'withMeta (form bound metadata)) value))) + +(np/defpass form {:from [s/Native Expr] :to [l/Machine Code] :args [(t/Set t/Sym)] :body true} + [bound expr] (node-form bound expr (:node expr))) diff --git a/src/com/tailrecursion/blacktide/compiler/order.clj b/src/com/tailrecursion/blacktide/compiler/order.clj new file mode 100644 index 0000000..ea63a7e --- /dev/null +++ b/src/com/tailrecursion/blacktide/compiler/order.clj @@ -0,0 +1,116 @@ +(ns ^:typed.clojure com.tailrecursion.blacktide.compiler.order + "Make operand evaluation explicit before CFG construction. Calls and collection + construction consume immutable saved locals; source scopes stay unchanged." + (:require [com.tailrecursion.nanopass :as np] + [com.tailrecursion.blacktide.compiler.source-languages :as l] + [com.tailrecursion.blacktide.compiler.source :as source] + [typed.clojure :as-alias t])) + +(declare normalize) + +(np/defpass save + {:from [l/Ordered Expr] :to (t/HVec [l/Bound-Supply l/Ordered-Binding l/Ordered-Local]) + :args [l/Bound-Supply] :body true} + [supply expr] + (let [[supply id] (source/fresh "operand" supply) + local {:id id :name id :mutable? false :external? false :jvm (:jvm expr)}] + [supply {:local local :init expr} local])) + +(np/defpass operands + {:from [l/Prepared Exprs] + :to (t/HVec [l/Bound-Supply (t/Vec l/Ordered-Binding) (t/Vec l/Ordered-Local)]) + :args [l/Bound-Supply] :body true} + [supply exprs] + (reduce (fn [^{::t/- (t/HVec [l/Bound-Supply (t/Vec l/Ordered-Binding) (t/Vec l/Ordered-Local)])} acc + ^{::t/- l/Prepared-Expr} expr] + (let [[supply bindings locals] acc + [supply expr] (normalize supply expr) + [supply binding local] (save supply expr)] + [supply (conj bindings binding) (conj locals local)])) [supply [] []] exprs)) + +(np/defpass normalize-forms + {:from [l/Prepared Exprs] :to (t/HVec [l/Bound-Supply l/Ordered-Exprs]) + :args [l/Bound-Supply] :body true} + [supply exprs] + (reduce (fn [^{::t/- (t/HVec [l/Bound-Supply l/Ordered-Exprs])} acc ^{::t/- l/Prepared-Expr} expr] + (let [[supply out] acc [supply expr] (normalize supply expr)] [supply (conj out expr)])) + [supply []] exprs)) + +(np/defpass normalize-bindings + {:from [l/Prepared Bindings] :to (t/HVec [l/Bound-Supply l/Ordered-Bindings]) + :args [l/Bound-Supply] :body true} + [supply bindings] + (reduce (fn [^{::t/- (t/HVec [l/Bound-Supply l/Ordered-Bindings])} acc ^{::t/- l/Prepared-Binding} binding] + (let [[supply out] acc [supply init] (normalize supply (:init binding))] + [supply (conj out (assoc binding :init init))])) [supply []] bindings)) + +(np/defpass normalize-node + {:from [l/Prepared Node] :to (t/HVec [l/Bound-Supply l/Ordered-Expr]) + :args [l/Bound-Supply l/Prepared-Expr]} + [supply expr node] + :literal [supply (assoc expr :node node)] + :local [supply (assoc expr :node node)] + :global [supply (assoc expr :node node)] + :var [supply (assoc expr :node node)] + :label [supply (assoc expr :node node)] + :goto [supply (assoc expr :node node)] + :native [supply (assoc expr :node node)] + :select (let [[supply selector] (normalize supply (:selector node)) + [supply arms] (normalize-forms supply (:arms node))] + [supply (assoc expr :node {:op :select :selector selector :arms arms})]) + :dispatch (let [[supply value] (normalize supply (:value node)) + [supply binding local] (save supply value)] + [supply (assoc expr :node {:op :let :bindings [binding] + :body (assoc expr :node {:op :dispatch :value local :spec (:spec node)})})]) + :recur (let [[supply bindings args] (operands supply (:args node))] + [supply (assoc expr :node {:op :let :bindings bindings + :body (assoc expr :node {:op :recur :target (:target node) :args args})})]) + :loop (let [[supply bindings] (normalize-bindings supply (:bindings node)) + [supply body] (normalize supply (:body node))] + [supply (assoc expr :node {:op :loop :id (:id node) :bindings bindings :body body})]) + :if (let [[supply test] (normalize supply (:test node)) + [supply yes] (normalize supply (:then node)) + [supply no] (normalize supply (:else node))] + [supply (assoc expr :node {:op :if :test test :then yes :else no})]) + :throw (let [[supply value] (normalize supply (:value node))] + [supply (assoc expr :node {:op :throw :value value})]) + :write (let [[supply value] (normalize supply (:value node))] + [supply (assoc expr :node {:op :write :target (:target node) :value value})]) + :do (let [[supply forms] (normalize-forms supply (:forms node))] + [supply (assoc expr :node {:op :do :forms forms})]) + :let (let [[supply bindings] (normalize-bindings supply (:bindings node)) + [supply body] (normalize supply (:body node))] + [supply (assoc expr :node {:op :let :bindings bindings :body body})]) + :convert + (let [[supply value] (normalize supply (:value node)) + [supply binding local] (save supply value)] + [supply (assoc expr :node {:op :let :bindings [binding] + :body (assoc expr :node {:op :convert :conversion (:conversion node) :value local})})]) + :host-call + (let [[supply bindings args] (operands supply (:args node))] + [supply (assoc expr :node {:op :let :bindings bindings + :body (assoc expr :node {:op :host-call :operation (:operation node) :args args})})]) + :invoke + (let [[supply function] (normalize supply (:function node)) + [supply binding local] (save supply function) + [supply bindings args] (operands supply (:args node))] + [supply (assoc expr :node {:op :let :bindings (into [binding] bindings) + :body (assoc expr :node {:op :invoke :function local :args args})})]) + :build + (let [[supply bindings items] (operands supply (:items node)) + value (assoc expr :node {:op :build :kind (:kind node) :items items :unique? (:unique? node)}) + [supply bindings value] + (if-let [metadata (:metadata node)] + (let [[supply binding local] (save supply value) + [supply metadata] (normalize supply metadata) + [supply meta-binding meta-local] (save supply metadata)] + [supply (into bindings [binding meta-binding]) + (assoc expr :node {:op :metadata :value local :metadata meta-local})]) + [supply bindings value])] + [supply (assoc expr :node {:op :let :bindings bindings :body value})])) + +(np/defpass normalize + {:from [l/Prepared Expr] :to (t/HVec [l/Bound-Supply l/Ordered-Expr]) + :args [l/Bound-Supply] :body true} + [supply expr] + (normalize-node supply expr (:node expr))) diff --git a/src/com/tailrecursion/blacktide/compiler/prepare.clj b/src/com/tailrecursion/blacktide/compiler/prepare.clj new file mode 100644 index 0000000..3d004e4 --- /dev/null +++ b/src/com/tailrecursion/blacktide/compiler/prepare.clj @@ -0,0 +1,46 @@ +(ns ^:typed.clojure com.tailrecursion.blacktide.compiler.prepare + "Make function casts and resolved JVM argument conversions explicit before + ordering operands. A failing conversion must prevent subsequent evaluation." + (:require [com.tailrecursion.nanopass :as np] + [com.tailrecursion.blacktide.compiler.source-languages :as l] + [com.tailrecursion.blacktide.compiler.jvm-languages :as j] + [com.tailrecursion.blacktide.compiler.jvm :as jvm] + [typed.clojure :as-alias t])) + +(declare prepare prepare-binding) + +(np/defpass prepare-forms + {:from [l/Collections Exprs] :to [l/Prepared Exprs] :processors {Expr prepare}} + [forms]) + +(np/defpass conversion + {:from [l/Prepared Expr] :to [l/Prepared Expr] :args [(t/Option j/JVM-Conversion)] :body true} + [conversion expr] + (if conversion + (let [fact (jvm/fact (.isPrimitive ^Class (:type conversion)) {:hint nil :var-meta {}} (:type conversion))] + ;; The consumer still sees Object, but evaluating an Object cast cannot + ;; fail. Operand normalization supplies the required value snapshot. + (if (= Object (:type conversion)) (assoc expr :jvm fact) + (assoc expr :jvm fact :node {:op :convert :conversion conversion :value expr}))) expr)) + +(np/defpass prepare-node + {:from [l/Collections Node] :to [l/Prepared Node] + :processors {Expr prepare Exprs prepare-forms Binding prepare-binding Raw clojure.core/identity Local clojure.core/identity + CollectionKind clojure.core/identity RecurTarget clojure.core/identity CaseSpec clojure.core/identity}} + [node] + :invoke {:op :invoke :function (conversion {:type clojure.lang.IFn :parameter? false} (prepare (:function node))) + :args (mapv prepare (:args node))} + :host + (let [plan (jvm/prepare-call (mapv #(:type (:jvm %)) (:args node)) (:operation node))] + {:op :host-call :operation (:operation plan) + :args (mapv (fn [how value] (conversion how (prepare value))) (:conversions plan) (:args node))})) + +(np/defpass prepare-binding + {:from [l/Collections Binding] :to [l/Prepared Binding] + :processors {Expr prepare Local clojure.core/identity}} + [binding]) + +(np/defpass prepare + {:from [l/Collections Expr] :to [l/Prepared Expr] :body true} + [expr] + (assoc expr :node (prepare-node (:node expr)))) diff --git a/src/com/tailrecursion/blacktide/compiler/source.clj b/src/com/tailrecursion/blacktide/compiler/source.clj new file mode 100644 index 0000000..f7bebcc --- /dev/null +++ b/src/com/tailrecursion/blacktide/compiler/source.clj @@ -0,0 +1,414 @@ +(ns ^:typed.clojure com.tailrecursion.blacktide.compiler.source + "Expand in the caller namespace while discovering lexical bindings. Macro + expansion and binding discovery stay ordered; every child becomes bound IR." + (:require [com.tailrecursion.blacktide.compiler.errors :as errors] + [com.tailrecursion.nanopass :as np] + [com.tailrecursion.blacktide.compiler.source-languages :as l] + [typed.clojure :as-alias t] + [com.tailrecursion.blacktide.compiler.jvm-languages :as j] + [com.tailrecursion.blacktide.compiler.jvm :as jvm] + [com.tailrecursion.blacktide.compiler.source-jvm :as facts] + [com.tailrecursion.blacktide.compiler.loop-facts :as loops] + [com.tailrecursion.blacktide.compiler.source-case :as cases] + [com.tailrecursion.blacktide.compiler.effects :refer [effects]] + [com.tailrecursion.blacktide.compiler.source-host :as host])) + +(declare parse parse-forms parse-list parse-bindings) + +(np/defpass fresh + {:from [l/Bound Supply] :to (t/HVec [l/Bound-Supply t/Sym]) :args [t/Str] :body true} + [stem supply] + ;; Put the unique counter before the user-controlled stem. A source name + ;; must not impersonate another binding's generated identity. + [(assoc supply :next (inc (:next supply))) (symbol (str (:prefix supply) "__" (:next supply) "__" stem))]) + +(np/defpass expression + {:from [l/Bound Node] :to [l/Bound Expr] :args [l/Bound-Context j/JVM-Seed] :body true} + [ctx jvm node] + {:site (:site ctx) :jvm (facts/infer jvm node) :node node}) + +(np/defpass resolve-name + {:from [l/Bound Raw] :to (t/U nil t/AnyVar java.lang.Class) :args [l/Bound-Context] :body true} + [ctx name] + (if (var? name) name + (when (and (symbol? name) (not (contains? (:locals ctx) name))) (ns-resolve *ns* name)))) + +(np/defpass parse-symbol + {:from [l/Bound Raw] :to [l/Bound Expr] :args [l/Bound-Context] :body true} + [ctx form] + (if (symbol? form) + (if-let [local (get (:locals ctx) form)] + (expression ctx {:hint (jvm/source-tag form) :var-meta {}} {:op :local :local local}) + (let [resolved (resolve-name ctx form) qualified (host/qualified form)] + (cond + (var? resolved) + (let [m (or (meta resolved) {})] + (errors/check-visibility resolved (:site ctx)) + (when (:macro m) (errors/fail :invalid-expression "Cannot take the value of a macro" {} (:site ctx))) + (if (:const m) + (expression ctx {:hint nil :var-meta {}} {:op :literal :value (.get ^clojure.lang.Var resolved)}) + (expression ctx {:hint (jvm/source-tag form) :var-meta m} + {:op :global :name (.toSymbol ^clojure.lang.Var resolved)}))) + (class? resolved) (expression ctx {:hint (jvm/source-tag form) :var-meta {}} {:op :literal :value resolved}) + qualified + (let [operation qualified] + (expression ctx {:hint (jvm/source-tag form) :var-meta {}} + {:op :host :operation (if (= :static-field (:op operation)) operation + {:op :method-value :name form}) :args []})) + :else (errors/fail :unresolved-symbol "Unable to resolve symbol" {:binding form} (:site ctx))))) + (errors/fail :invalid-expression "Expected a symbol" {} (:site ctx)))) + +(np/defpass parse-operand + {:from [l/Bound Raw] :to (t/HVec [l/Bound-Supply l/Bound-Expr]) + :args [l/Bound-Supply l/Bound-Context] :body true} + [supply ctx form] + (parse supply (assoc ctx :statement? false :tail? false :outer? false :transfer? false) form)) + +(np/defpass parse-forms + {:from [l/Bound Forms] :to (t/HVec [l/Bound-Supply (t/Vec l/Bound-Expr)]) + :args [l/Bound-Supply l/Bound-Context] :body true} + [supply ctx forms] + (reduce (fn [^{::t/- (t/HVec [l/Bound-Supply (t/Vec l/Bound-Expr)])} acc ^{::t/- l/Bound-Raw} form] + (let [[supply out] acc + last? (= (count out) (dec (count forms))) + ctx (assoc ctx :tail? (and (:tail? ctx) last?)) + [supply expr] (if (:statement? ctx) (parse supply ctx form) (parse-operand supply ctx form))] + [supply (conj out expr)])) + [supply []] forms)) + +(np/defpass parse-body + {:from [l/Bound Forms] :to (t/HVec [l/Bound-Supply l/Bound-Expr]) + :args [l/Bound-Supply l/Bound-Context] :body true} + [supply ctx forms] + (let [[supply forms] (parse-forms supply (assoc ctx :statement? true) forms)] + [supply (expression ctx {:hint nil :var-meta {}} {:op :do :forms forms})])) + +(np/defpass bind-local + {:from [l/Bound Raw] :to (t/HVec [l/Bound-Supply l/Bound-Context l/Bound-Local]) + :args [l/Bound-Supply l/Bound-Context t/Bool j/JVM-Fact] :body true} + [supply ctx mutable? init name] + (if (simple-symbol? name) + (let [[supply id] (fresh (str name) supply) + jvm (facts/binding-facts name mutable? init) + type (:type jvm) + tag (or (:hint jvm) (when (class? type) (symbol (.getName ^Class type)))) + local {:id id :name name :mutable? mutable? :external? false + :jvm jvm}] + [supply (assoc ctx :locals (assoc (:locals ctx) name local) + :macro-env (assoc (:macro-env ctx) name {:name name :tag tag})) local]) + (errors/fail :invalid-binding "Binding targets must be simple symbols" {:binding name} (:site ctx)))) + +(np/defpass parse-bindings + {:from [l/Bound Raw] :to (t/HVec [l/Bound-Supply l/Bound-Context (t/Vec l/Bound-Binding)]) + :args [l/Bound-Supply l/Bound-Context (t/U ':mutable ':immutable ':loop)] :body true} + [supply ctx mode bindings] + (if (and (vector? bindings) (even? (count bindings))) + (reduce (fn [^{::t/- (t/HVec [l/Bound-Supply l/Bound-Context (t/Vec l/Bound-Binding)])} acc + ^{::t/- (t/Seq t/Any)} pair] + (let [[supply ctx out] acc + [supply init] (parse-operand supply ctx (second pair)) + [supply ctx local] (bind-local supply ctx (not= mode :immutable) (:jvm init) (first pair)) + local (cond-> local (= mode :loop) (assoc :mutable? false)) + ctx (assoc ctx :locals (assoc (:locals ctx) (:name local) local)) + hint (jvm/tag-class (:hint (:jvm local))) + init (if (and hint (= mode :immutable)) + (expression ctx {:hint nil :var-meta {}} {:op :host :operation {:op :adapt :class hint} :args [init]}) init)] + [supply ctx (conj out {:local local :init init})])) [supply ctx []] (partition 2 bindings)) + (errors/fail :invalid-bindings "Expected an even binding vector" {} (:site ctx)))) + +(np/defpass parse-scope + {:from [l/Bound Forms] :to (t/HVec [l/Bound-Supply l/Bound-Expr]) + :args [l/Bound-Supply l/Bound-Context t/Bool] :body true} + [supply ctx mutable? forms] + (let [inner (assoc ctx :outer? mutable?) + [supply inner bindings] (parse-bindings supply inner (if mutable? :mutable :immutable) (second forms)) + [supply body] (parse-body supply inner (vec (drop 2 forms)))] + [supply (expression ctx {:hint (jvm/source-tag (:form (:site ctx))) :var-meta {}} + {:op :let :bindings bindings :body body})])) + +(np/defpass parse-loop + {:from [l/Bound Forms] :to (t/HVec [l/Bound-Supply l/Bound-Expr]) + :args [l/Bound-Supply l/Bound-Context] :body true} + [supply ctx forms] + (let [[supply id] (fresh "loop" supply) + inner (assoc ctx :outer? false :recur nil :tail? false) + [supply inner bindings] (parse-bindings supply inner :loop (second forms)) + target {:id id :bindings (mapv :local bindings)} + [supply body] (parse-body supply (assoc inner :recur target :tail? true) (vec (drop 2 forms)))] + [supply (first (loops/solve {} (expression ctx {:hint nil :var-meta {}} {:op :loop :id id :bindings bindings :body body})))])) + +(np/defpass parse-recur + {:from [l/Bound Forms] :to (t/HVec [l/Bound-Supply l/Bound-Expr]) + :args [l/Bound-Supply l/Bound-Context] :body true} + [supply ctx forms] + (let [target (:recur ctx)] + (when-not (and target (:tail? ctx)) + (errors/fail :invalid-recur "recur requires the tail position of a local loop" {} (:site ctx))) + (when-not (= (count (:bindings target)) (dec (count forms))) + (errors/fail :recur-arity "recur argument count does not match its target" {} (:site ctx))) + (let [[supply args] (parse-forms supply (assoc ctx :statement? false :tail? false) (subvec forms 1))] + [supply (expression ctx {:hint nil :var-meta {}} {:op :recur :target target :args args})]))) + +(np/defpass parse-arms + {:from [l/Bound Forms] :to (t/HVec [l/Bound-Supply l/Bound-Exprs]) + :args [l/Bound-Supply l/Bound-Context] :body true} + [supply ctx forms] + (reduce (fn [^{::t/- (t/HVec [l/Bound-Supply l/Bound-Exprs])} acc ^{::t/- t/Any} form] + (let [[supply arms] acc [supply arm] (parse supply (assoc ctx :outer? false) form)] [supply (conj arms arm)])) + [supply []] forms)) + +(np/defpass case-expression + {:from [l/Bound Exprs] :to (t/HVec [l/Bound-Supply l/Bound-Expr]) + :args [l/Bound-Supply l/Bound-Context l/Bound-Expr] :body true} + [supply ctx selector arms] + [supply (expression ctx {:hint nil :var-meta {}} {:op :case :selector selector :arms arms})]) + +(np/defpass parse-case + {:from [l/Bound Forms] :to (t/HVec [l/Bound-Supply l/Bound-Expr]) + :args [l/Bound-Supply l/Bound-Context] :body true} + [supply ctx forms] + (let [{:keys [selector arms]} (cases/expand-case (:macro-env ctx) forms)] + (if (empty? arms) (parse supply ctx selector) + (do + (when-not (and (seq? selector) (= 3 (count selector))) + (errors/fail :invalid-case "Expected core case's selector binding" {} (:site ctx))) + (let [[supply inner bindings] (parse-bindings supply ctx :immutable (second selector)) + [supply selector] (parse-operand supply inner (first (nnext selector))) + [supply arms] (parse-arms supply inner arms) + [supply body] (case-expression supply ctx selector arms)] + [supply (expression ctx {:hint nil :var-meta {}} {:op :let :bindings bindings :body body})]))))) + +(np/defpass parse-dispatch + {:from [l/Bound Forms] :to (t/HVec [l/Bound-Supply l/Bound-Expr]) + :args [l/Bound-Supply l/Bound-Context] :body true} + [supply ctx forms] + (let [{:keys [value spec arms]} (cases/dispatch-syntax forms) + [supply value] (parse-operand supply ctx value)] + (when-not (= :local (:op (:node value))) + (errors/fail :invalid-case "case* dispatch must reference a lexical local" {} (:site ctx))) + (let [selector (expression ctx {:hint nil :var-meta {}} {:op :dispatch :value value :spec spec}) + [supply arms] (parse-arms supply ctx arms)] + (case-expression supply ctx selector arms)))) + +(np/defpass protocol-argument + {:from [l/Bound Expr] :to (t/HVec [l/Bound-Supply l/Bound-Expr]) + :args [l/Bound-Supply l/Bound-Context l/Bound-Expr java.lang.Class] :body true} + [supply ctx direct? type arg] + (let [seed {:hint nil :var-meta {}} [supply id] (fresh "protocol-argument" supply) + local {:id id :name id :mutable? false :external? false :jvm (facts/binding-facts id false (:jvm arg))} + read (expression ctx seed {:op :local :local local}) + boxed (expression ctx seed {:op :host :operation {:op :static :class clojure.lang.RT :member 'box :tags [Object]} :args [read]}) + converted (if (.isPrimitive ^Class type) + (expression ctx seed {:op :host :operation {:op :static :class clojure.lang.RT :member 'box :tags [type]} :args [read]}) + (expression ctx seed {:op :host :operation {:op :instance :class Class :member 'cast :mode :method :tags [Object]} + :args [(expression ctx seed {:op :literal :value type}) + (expression ctx seed {:op :host :operation {:op :adapt :class type} :args [read]})]}))] + [supply (expression ctx seed {:op :let :bindings [{:local local :init arg}] + :body (expression ctx seed {:op :if :test direct? :then converted :else boxed})})])) + +(np/defpass protocol-call + {:from [l/Bound Exprs] :to (t/HVec [l/Bound-Supply l/Bound-Expr]) + :args [l/Bound-Supply l/Bound-Context j/JVM-Seed j/JVM-Protocol t/Sym] :body true} + [supply ctx seed method name args] + (let [plain {:hint nil :var-meta {}} [supply id] (fresh "protocol-receiver" supply) + receiver (expression ctx plain {:op :host :operation {:op :static :class clojure.lang.RT :member 'box :tags [Object]} :args [(nth args 0)]}) + local {:id id :name id :mutable? false :external? false :jvm (:jvm receiver)} + read (expression ctx plain {:op :local :local local}) + direct? (if-let [class (:class method)] + (expression ctx plain {:op :host :operation {:op :instanceof :class class} :args [read]}) + (expression ctx plain {:op :literal :value false})) + root (expression ctx plain {:op :host :operation {:op :instance :class clojure.lang.Var :member 'getRawRoot :mode :method :tags []} + :args [(expression ctx plain {:op :var :name name})]}) + root (expression ctx plain {:op :if :test direct? :then (expression ctx plain {:op :literal :value nil}) :else root}) + [supply args] (reduce (fn [^{::t/- (t/HVec [l/Bound-Supply l/Bound-Exprs])} acc ^{::t/- t/Int} index] + (let [[supply out] acc arg (nth args index) type (get (:parameters method) (dec index) Object) + [supply arg] (if (= Object type) [supply arg] (protocol-argument supply ctx direct? type arg))] + [supply (conj out arg)])) [supply [read root]] (range 1 (count args)))] + [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 j/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-call + {:from [l/Bound Forms] :to (t/HVec [l/Bound-Supply l/Bound-Expr]) + :args [l/Bound-Supply l/Bound-Context] :body true} + [supply ctx forms] + (let [inner (assoc ctx :statement? false :tail? false) + [supply function] (parse-operand supply inner (first forms)) + arity (dec (count forms)) + direct (facts/direct-call arity function) + metadata (:var-meta (:jvm function)) + primitive (when-not direct (facts/primitive-interface arity (:arglists metadata))) + hint (or (jvm/source-tag (:form (:site ctx))) + (jvm/source-tag (facts/signature arity (:arglists metadata))) + (when (and (not direct) (not primitive) (= :global (:op (:node function)))) (:hint (:jvm function))) + (:tag metadata)) + [supply args] (parse-forms supply inner (vec (rest forms))) + class-arg (some-> args first :node) + node (cond + (and (= :global (:op (:node function))) (= 'clojure.core/instance? (:name (:node function))) + (= 2 (count args)) (= :literal (:op class-arg)) (class? (:value class-arg))) + {:op :host :operation {:op :instanceof :class (:value class-arg)} :args [(nth args 1)]} + direct {:op :host :operation direct :args args} + primitive {:op :host :operation {:op :instance :class primitive :member 'invokePrim :mode :method :tags nil} + :args (into [function] args)} + :else {:op :invoke :function function :args args})] + (let [method (when-not (or direct primitive) (facts/protocol-call arity function)) + fnode (:node function) key (:form (:site function))] + (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)) + :else [supply (expression ctx {:hint hint :var-meta {}} node)])))) + +(np/defpass parse-host + {:from [j/JVM Syntax] :to (t/HVec [l/Bound-Supply l/Bound-Expr]) + :args [l/Bound-Supply l/Bound-Context] :body true} + [supply ctx syntax] + (let [[supply args] (parse-forms supply (assoc ctx :statement? false :tail? false) (:operands syntax))] + [supply (expression ctx {:hint (jvm/source-tag (:form (:site ctx))) :var-meta {}} + {:op :host :operation (:operation syntax) :args args})])) + +(np/defpass parse-assignment + {:from [l/Bound Forms] :to (t/HVec [l/Bound-Supply l/Bound-Expr]) + :args [l/Bound-Supply l/Bound-Context (t/U ':setq ':psetq)] :body true} + [supply ctx kind forms] + (if (even? (count forms)) + (let [[supply pairs] + (reduce (fn [^{::t/- (t/HVec [l/Bound-Supply l/Bound-Assignments])} acc ^{::t/- (t/Seq t/Any)} pair] + (let [[supply pairs] acc target (first pair)] + (when-not (simple-symbol? target) + (errors/fail :invalid-assignment "Assignment targets must be simple symbols" {:binding target} (:site ctx))) + (let [local (or (get (:locals ctx) target) + (errors/fail :unknown-assignment "Expected a mutable loophole local" {:binding target} (:site ctx)))] + (when-not (:mutable? local) + (errors/fail :immutable-assignment "Cannot assign to an immutable local" {:binding target} (:site ctx))) + (let [[supply value] (parse-operand supply ctx (second pair))] + [supply (conj pairs {:target local :value value})])))) [supply []] (partition 2 forms))] + [supply (expression ctx {:hint (jvm/source-tag (:form (:site ctx))) :var-meta {}} + (if (= kind :setq) {:op :setq :pairs pairs} {:op :psetq :pairs pairs}))]) + (errors/fail :invalid-assignment "setq/psetq requires symbol/value pairs" {} (:site ctx)))) + +(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] + (let [name (second forms)] + (when-not (and (= 2 (count forms)) (simple-symbol? name)) + (errors/fail :invalid-marker "label/goto requires one simple symbol" {:label name} (:site ctx))) + (when-not (and (:statement? ctx) (:transfer? ctx) (or (= kind :goto) (:outer? ctx))) + (errors/fail :marker-position "Labels require the outer body; goto requires a statement outside operands and initializers" + {:operator kind :label name} (:site ctx))) + (when (and (= kind :label) (= name 'end)) + (errors/fail :reserved-label "end is reserved" {:label name} (:site ctx))) + (expression ctx {:hint nil :var-meta {}} + (if (= kind :label) {:op :label :name name} + {:op :goto :name name})))) + +(np/defpass parse-list + {:from [l/Bound Forms] :to (t/HVec [l/Bound-Supply l/Bound-Expr]) + :args [l/Bound-Supply l/Bound-Context] :body true} + [supply ctx forms] + (let [op (first forms) form (:form (:site ctx)) resolved (resolve-name ctx op) + marker (when (var? resolved) (get (:markers ctx) resolved)) + _ (when (var? resolved) (errors/check-visibility resolved (:site ctx)))] + (cond + (= :loophole marker) + (errors/fail :unsupported-form "Nested loophole regions are unsupported" {:operator op} (:site ctx)) + (= :setq marker) (parse-assignment supply ctx :setq (vec (rest forms))) + (= :psetq marker) (parse-assignment supply ctx :psetq (vec (rest forms))) + (= :label marker) [supply (parse-marker ctx :label forms)] + (= :goto marker) [supply (parse-marker ctx :goto forms)] + (= op 'quote) + (if (= 2 (count forms)) + [supply (expression ctx {:hint nil :var-meta {}} {:op :literal :value (second forms)})] + (errors/fail :invalid-quote "quote requires one argument" {} (:site ctx))) + (= op 'var) + (let [name (second forms) resolved (when (symbol? name) (ns-resolve *ns* name))] + (if (and (= 2 (count forms)) (var? resolved)) + [supply (expression ctx {:hint nil :var-meta {}} + {:op :var :name (.toSymbol ^clojure.lang.Var resolved)})] + (errors/fail :invalid-var "var requires a resolvable Var name" {} (:site ctx)))) + (#{'fn* 'letfn* 'try 'set! 'monitor-enter 'monitor-exit} op) + (errors/fail :unsupported-form "Form is unsupported inside loophole; place it outside the region" {:operator op} (:site ctx)) + (#{'new '.} op) (parse-host supply ctx (or (host/syntax ctx forms) + (errors/fail :invalid-interop "Missing host syntax" {} (:site ctx)))) + (= op 'do) (parse-body supply ctx (vec (rest forms))) + (= op 'let*) (parse-scope supply ctx false forms) + (= op 'loop*) (parse-loop supply ctx forms) + (= op 'recur) (parse-recur supply ctx forms) + (= op 'case*) (parse-dispatch supply ctx forms) + (and (= resolved #'clojure.core/case) (< (count forms) 2)) + (errors/fail :invalid-case "case requires a dispatch expression" {} (:site ctx)) + (= resolved #'clojure.core/case) (parse-case supply ctx forms) + (= op 'if) + (if (<= 3 (count forms) 4) + (let [[supply test] (parse-operand supply ctx (second forms)) + [supply yes] (parse supply (assoc ctx :outer? false) (nth forms 2)) + [supply no] (parse supply (assoc ctx :outer? false) (get forms 3))] + [supply (expression ctx {:hint (jvm/source-tag form) :var-meta {}} {:op :if :test test :then yes :else no})]) + (errors/fail :invalid-if "if requires a test, then, and optional else" {} (:site ctx))) + (= op 'throw) + (if (= 2 (count forms)) + (let [[supply value] (parse-operand supply ctx (second forms))] + [supply (expression ctx {:hint nil :var-meta {}} {:op :throw :value value})]) + (errors/fail :invalid-throw "throw requires one value" {} (:site ctx))) + (and (symbol? op) (special-symbol? op)) + (errors/fail :unsupported-form "Special form is not supported inside loophole" {:operator op} (:site ctx)) + (and (var? resolved) (:macro (meta resolved))) + (let [expanded (.applyTo ^clojure.lang.Var resolved (cons form (cons (:macro-env ctx) (rest forms)))) + expanded (if (instance? clojure.lang.IObj expanded) + (with-meta expanded (merge (meta expanded) (meta form))) expanded)] + (parse supply ctx expanded)) + (and (var? resolved) (ifn? (:inline (meta resolved))) + (let [arity (:inline-arities (meta resolved))] (or (nil? arity) (and (ifn? arity) (.invoke ^clojure.lang.IFn arity (dec (count forms))))))) + (let [inline (:inline (meta resolved)) + expanded (if (ifn? inline) (.applyTo ^clojure.lang.IFn inline (rest forms)) + (errors/fail :invalid-expression "Invalid inline expander" {} (:site ctx))) + expanded (if (instance? clojure.lang.IObj expanded) + (with-meta expanded (merge (meta expanded) (meta form))) expanded)] + (parse supply ctx expanded)) + :else (if-let [syntax (host/syntax ctx forms)] + (parse-host supply ctx syntax) + (parse-call supply ctx forms))))) + +(np/defpass parse-collection + {:from [l/Bound Raw] :to (t/HVec [l/Bound-Supply l/Bound-Expr]) + :args [l/Bound-Supply l/Bound-Context] :body true} + [supply ctx form] + (if (or (vector? form) (map? form) (set? form)) + (let [kind (cond (vector? form) :vector (map? form) :map :else :set) + ;; Freeze source traversal before alpha-renaming changes element hashes. + items (if (map? form) (vec (mapcat identity form)) (vec form)) + inner (assoc ctx :statement? false :tail? false) + [supply items] (parse-forms supply inner items) + [supply metadata] (if (some? (meta form)) (parse-operand supply inner (meta form)) [supply nil])] + [supply (expression ctx {:hint (jvm/source-tag form) :var-meta {}} + {:op :collection :kind kind :items items :metadata metadata})]) + (errors/fail :invalid-expression "Expected a collection" {} (:site ctx)))) + +(np/defpass parse + {:from [l/Bound Raw] :to (t/HVec [l/Bound-Supply l/Bound-Expr]) + :args [l/Bound-Supply l/Bound-Context] :body true} + [supply ctx form] + (let [ctx (assoc ctx :site (errors/site (:site ctx) form))] + (cond + (symbol? form) [supply (parse-symbol ctx form)] + (and (seq? form) (seq form)) (parse-list supply ctx (vec form)) + (or (vector? form) (map? form) (set? form)) + (parse-collection supply ctx form) + :else [supply (expression ctx {:hint (jvm/source-tag form) :var-meta {}} {:op :literal :value form})]))) diff --git a/src/com/tailrecursion/blacktide/compiler/source_case.clj b/src/com/tailrecursion/blacktide/compiler/source_case.clj new file mode 100644 index 0000000..7d565c7 --- /dev/null +++ b/src/com/tailrecursion/blacktide/compiler/source_case.clj @@ -0,0 +1,42 @@ +(ns ^:typed.clojure com.tailrecursion.blacktide.compiler.source-case + "Ask core case to select source-arm ordinals. Constants stay opaque, while + source bodies remain unique. Decode prepared case* tables without evaluating them." + (:require [com.tailrecursion.nanopass :as np] + [com.tailrecursion.blacktide.compiler.source-languages :as s] + [typed.clojure :as-alias t])) + +(np/defpass expand-case + {:from [s/Bound Forms] :to [s/Bound CaseSyntax] :args [(t/Map t/Sym t/Any)] :body true} + [env forms] + (let [_ (when (< (count forms) 2) + (.applyTo ^clojure.lang.Var #'clojure.core/case (cons (apply list forms) (cons env (rest forms))))) + clauses (subvec forms 2) pairs (vec (partition 2 clauses)) default? (odd? (count clauses)) + arms (cond-> (mapv second pairs) default? (conj (peek clauses))) + clauses (into [] (mapcat (fn [^{::t/- t/Int} index] [(first (nth pairs index)) index])) (range (count pairs))) + clauses (cond-> clauses default? (conj (count pairs))) + form (list* 'clojure.core/case (second forms) clauses)] + {:selector (.applyTo ^clojure.lang.Var #'clojure.core/case (cons form (cons env (rest form)))) :arms arms})) + +(np/defpass int32 {:from [s/Bound Raw] :to t/Int :body true} + [value] + (if (instance? Number value) (.intValue ^Number value) + (throw (IllegalArgumentException. "case* expects numeric dispatch data")))) + +(np/defpass dispatch-syntax {:from [s/Bound Forms] :to [s/Bound DispatchSyntax] :body true} + [forms] + (let [_ (when-not (<= 8 (count forms) 9) (throw (IllegalArgumentException. "Malformed case*"))) + [_ value shift mask default table switch test skip] forms] + (when-not (and (map? table) (seq table) (#{:compact :sparse} switch) + (#{:int :hash-equiv :hash-identity} test) (or (nil? skip) (set? skip))) + (throw (IllegalArgumentException. "Malformed case* dispatch table"))) + (let [[tests arms] + (reduce (fn [^{::t/- (t/HVec [(t/Map t/Int (t/HVec [t/Any t/Int])) s/Bound-Forms])} acc + ^{::t/- (t/HVec [t/Any t/Any])} entry] + (let [[tests arms] acc [key pair] entry] + (when-not (and (vector? pair) (= 2 (count pair))) + (throw (IllegalArgumentException. "case* table entries require constant/body pairs"))) + [(assoc tests (int32 key) [(if (= :int test) (int32 (first pair)) (first pair)) (count arms)]) + (conj arms (second pair))])) [{} []] (mapv (fn [[key value]] [key value]) table))] + {:value value :arms (conj arms default) + :spec {:shift (int32 shift) :mask (int32 mask) :default (count arms) :tests tests + :switch switch :test test :skip (when skip (set (map int32 skip)))}}))) diff --git a/src/com/tailrecursion/blacktide/compiler/source_host.clj b/src/com/tailrecursion/blacktide/compiler/source_host.clj new file mode 100644 index 0000000..5e8f5b7 --- /dev/null +++ b/src/com/tailrecursion/blacktide/compiler/source_host.clj @@ -0,0 +1,72 @@ +(ns ^:typed.clojure com.tailrecursion.blacktide.compiler.source-host + "Recognize native JVM syntax with the source lexical environment. Return host + descriptors and ordered source operands; none of those operands is evaluated." + (:require [com.tailrecursion.nanopass :as np] + [com.tailrecursion.blacktide.compiler.source-languages :as s] + [com.tailrecursion.blacktide.compiler.jvm-languages :as l] + [com.tailrecursion.blacktide.compiler.jvm :as jvm] + [typed.clojure :as-alias t])) + +(np/defpass tags {:from [l/JVM Raw] :to [l/JVM Tags] :body true} + [symbol] + (let [tags (:param-tags (meta symbol))] + (if (or (nil? tags) (vector? tags)) tags + (throw (IllegalArgumentException. "param-tags must be a vector"))))) + +(np/defpass qualified + {:from [l/JVM Raw] :to (t/Option l/JVM-Operation) :body true} + [symbol] + (when (and (symbol? symbol) (namespace symbol)) + (when-let [class (jvm/resolved-class (clojure.core/symbol (or (namespace symbol) "")))] + (let [member (name symbol) tags (tags symbol)] + (cond (= member "new") {:op :constructor :class class :tags tags} + (.startsWith member ".") {:op :instance :class class :member (clojure.core/symbol (subs member 1)) + :mode :method :tags tags} + (and (clojure.lang.Reflector/getField class member true) + (not (jvm/static-method? class member))) + {:op :static-field :class class :member (clojure.core/symbol member)} + :else {:op :static :class class :member (clojure.core/symbol member) :tags tags}))))) + +(np/defpass syntax + {:from [s/Bound Forms] :to (t/Option l/JVM-Syntax) :args [s/Bound-Context] :body true} + [ctx forms] + (let [op (first forms) args (subvec forms 1)] + (cond + (= op 'new) + (if-let [class (jvm/resolved-class (first args))] + {:operation {:op :constructor :class class :tags nil} :operands (vec (rest args))} + (throw (IllegalArgumentException. "new requires a class name"))) + (= op '.) + (let [[target member & extra] args + _ (when (< (count forms) 3) (throw (IllegalArgumentException. "Malformed member expression"))) + class (jvm/resolved-class target) + method? (seq? member) + values (if method? (vec (rest member)) (vec extra)) + member (if method? (first member) member)] + (when-not (symbol? member) (throw (IllegalArgumentException. "Expected a Java member name"))) + (let [field? (.startsWith (name member) "-") + member (symbol (if field? (subs (name member) 1) (name member))) + mode (cond field? :field (or method? (seq values)) :method :else :member)] + (if class + (if (and (not= mode :method) + (or field? (empty? (jvm/methods class 0 true member)))) + {:operation {:op :static-field :class class :member member} :operands []} + {:operation {:op :static :class class :member member :tags nil} :operands values}) + {:operation {:op :instance :class nil :member member :mode mode :tags nil} + :operands (into [target] values)}))) + (symbol? op) + (cond + (and (nil? (namespace op)) (.startsWith (name op) ".")) + (let [target (first args) + target (if (jvm/resolved-class target) + (with-meta (list 'clojure.core/identity target) {:tag 'java.lang.Class}) target)] + (when (empty? args) (throw (IllegalArgumentException. "Instance member requires a receiver"))) + (syntax ctx (into ['. target (symbol (subs (name op) 1))] (rest args)))) + (.endsWith (name op) ".") + (syntax ctx (into ['new (symbol (subs (str op) 0 (dec (count (str op)))))] args)) + :else (when-let [operation (qualified op)] + (when (and (= :instance (:op operation)) (empty? args)) + (throw (IllegalArgumentException. "Qualified instance call requires a receiver"))) + (when (and (= :static-field (:op operation)) (seq args)) + (throw (IllegalArgumentException. "Cannot invoke a static field with arguments"))) + {:operation operation :operands args}))))) diff --git a/src/com/tailrecursion/blacktide/compiler/source_jvm.clj b/src/com/tailrecursion/blacktide/compiler/source_jvm.clj new file mode 100644 index 0000000..45d2fc2 --- /dev/null +++ b/src/com/tailrecursion/blacktide/compiler/source_jvm.clj @@ -0,0 +1,149 @@ +(ns ^:typed.clojure com.tailrecursion.blacktide.compiler.source-jvm + "Infer source expression and binding facts before normalization erases native + typing evidence. Mutable locals use declared hints, never initializer guesses." + (:require [com.tailrecursion.nanopass :as np] + [com.tailrecursion.blacktide.compiler.source-languages :as s] + [com.tailrecursion.blacktide.compiler.jvm-languages :as l] + [com.tailrecursion.blacktide.compiler.jvm :as jvm] + [typed.clojure :as-alias t])) + +(np/defpass signature + {:from [l/JVM Raw] :to (t/Option (t/Vec t/Any)) :args [t/Int] :body true} + [arity arglists] + (when-let [entries (when (or (seq? arglists) (vector? arglists)) (seq arglists))] + (let [sig (first entries)] + (if (and (vector? sig) + (or (= arity (count sig)) + (let [offset (count (take-while #(not= '& %) sig))] + (and (< offset (count sig)) (<= offset arity))))) + sig + (signature arity (next entries)))))) + +(np/defpass primitive-interface + {:from [l/JVM Raw] :to (t/Option java.lang.Class) :args [t/Int] :body true} + [arity arglists] + (when-let [sig (signature arity arglists)] + (when (= arity (count sig)) + (when-let [name (clojure.lang.Compiler$FnMethod/primInterface sig)] + (clojure.lang.RT/classForNameNonLoading name))))) + +(np/defpass direct-call + {:from [s/Bound Expr] :to (t/Option l/JVM-Operation) :args [t/Int] :body true} + [arity function] + (let [node (:node function)] + (when (and (:direct-linking *compiler-options*) (= :global (:op node))) + (let [v (ns-resolve *ns* (:name node))] + (when (and (var? v) (not (.isDynamic ^clojure.lang.Var v)) + (not (:redef (:var-meta (:jvm function)))) (.isBound ^clojure.lang.Var v)) + (when-let [value (.get ^clojure.lang.Var v)] + (let [^Class class (.getClass ^Object value) members (.getMethods class) + selected (first (filter + (fn [^{::t/- java.lang.reflect.Method :tag java.lang.reflect.Method} member] + (let [params (jvm/parameters member)] + (and (= "invokeStatic" (.getName member)) + (java.lang.reflect.Modifier/isStatic (.getModifiers member)) + (or (= arity (count params)) + (and (> arity (count params)) (= clojure.lang.ISeq (peek params))))))) + (mapv (fn [^{::t/- t/Int} index] (aget members (int index))) (range (alength members)))))] + (when selected + (let [params (jvm/parameters selected)] + {:op :direct :class class :parameters params :return (jvm/return-type selected) + :variadic? (= clojure.lang.ISeq (peek params))}))))))))) + +(np/defpass protocol-class {:from [l/JVM Raw] :to (t/Option java.lang.Class) :body true} + [on] + (when (or (class? on) (symbol? on)) (clojure.lang.Compiler$HostExpr/maybeClass on false))) + +(np/defpass protocol-call + {:from [s/Bound Expr] :to (t/Option l/JVM-Protocol) :args [t/Int] :body true} + [arity function] + ;; InvokeExpr reads protocol metadata after analyzing the operands. A macro + ;; in an argument may change it; later source must not change this call. + (when (= :global (:op (:node function))) + (let [v (ns-resolve *ns* (:name (:node function))) + protocol (:protocol (meta v))] + (when protocol + (when-not (and (var? protocol) (pos? arity)) + (throw (IllegalArgumentException. "A protocol call requires a protocol Var and receiver"))) + (let [value (.get ^clojure.lang.Var protocol) class (protocol-class (:on value))] + (if class + (let [member (get (:method-map value) (keyword (name (:name (:node function))))) + methods (when (keyword? member) (jvm/methods class (dec arity) false (symbol member)))] + (when-not (and (keyword? member) (= 1 (count methods))) + (throw (IllegalArgumentException. "No single protocol interface method matches the call"))) + {:class class :member (symbol (clojure.lang.Compiler/munge (str (symbol member)))) + :parameters (jvm/parameters (first methods))}) + {:class nil :member nil :parameters []})))))) + +(np/defpass infer + {:from [s/Bound Node] :to [l/JVM Fact] :args [l/JVM-Seed] :body true} + [seed node] + (let [hint (when (#{:local :global :invoke :host} (:op node)) (jvm/tag-class (:hint seed))) + fact (fn ^{::t/- [l/JVM-Type t/Bool :-> l/JVM-Fact]} describe [type primitive?] + (jvm/fact primitive? seed (or hint type))) + inherited (fn ^{::t/- [l/JVM-Fact :-> l/JVM-Fact]} inherit [value] + (assoc (fact (:type value) (:primitive? value)) :hint-error (:hint-error value)))] + (case (:op node) + :literal (let [type (jvm/literal-type (:value node))] + (fact type (boolean (and (class? type) (.isPrimitive ^Class type))))) + :local (let [local (:jvm (:local node))] + (when (and hint (:primitive? local) (not= hint (:type local))) + (throw (UnsupportedOperationException. "Can't type hint a primitive local with a different type"))) + (inherited local)) + :global (fact (or hint (jvm/tag-class (:tag (:var-meta seed))) :unknown) false) + :var (fact clojure.lang.Var false) + :invoke (let [metadata (:var-meta (:jvm (:function node))) + sig (signature (count (:args node)) (:arglists metadata))] + (fact (or hint (jvm/tag-class (jvm/source-tag sig)) + (when (= :global (:op (:node (:function node)))) (:type (:jvm (:function node)))) + :unknown) false)) + :host (let [operation (:operation node) + types (mapv #(jvm/value-type (:jvm %)) (:args node)) + type (jvm/host-type types operation) + value (fact type (boolean (and (class? type) (.isPrimitive ^Class type) + (not (#{:adapt} (:op operation))))))] + (assoc value :hint-error (jvm/hint-conflict hint type))) + :collection (let [constant? (and (seq (:items node)) (nil? (:metadata node)) (every? #(= :literal (:op (:node %))) (:items node)))] + (case (:kind node) + :vector (if constant? (fact clojure.lang.APersistentVector false) (fact clojure.lang.IPersistentVector false)) + :map (if constant? (fact clojure.lang.APersistentMap false) (fact clojure.lang.IPersistentMap false)) + :set (if constant? (fact clojure.lang.APersistentSet false) (fact clojure.lang.IPersistentSet false)))) + :if (let [a (:jvm (:then node)) b (:jvm (:else node))] + (assoc (fact (jvm/join-type (:type a) (:type b)) + (and (:primitive? a) (:primitive? b) (= (:type a) (:type b)))) + :hint-error (or (:hint-error a) (:hint-error b)))) + :case (let [values (reduce (fn [^{::t/- (t/Vec l/JVM-Fact)} values ^{::t/- s/Bound-Expr} arm] + (if (= :throw (:op (:node arm))) values (conj values (:jvm arm)))) [] (:arms node)) + type (some-> values first :type) + type (if (and (class? type) (every? #(and (= type (:type %)) (nil? (:hint-error %))) values)) type :unknown)] + (fact type (boolean (and (class? type) (.isPrimitive ^Class type))))) + :dispatch (fact Long/TYPE true) + :do (if-let [last (peek (:forms node))] (inherited (:jvm last)) (fact nil false)) + :let (inherited (:jvm (:body node))) + :loop (fact :unknown false) + :recur (fact :unknown false) + :setq (if-let [last (peek (:pairs node))] (inherited (:jvm (:value last))) (fact nil false)) + :psetq (fact nil false) + :label (fact nil false) + :goto (fact :unknown false) + :throw (fact :unknown false)))) + +(np/defpass binding-facts + {:from [l/JVM Fact] :to [l/JVM Fact] :args [l/JVM-Raw t/Bool] :body true} + [name mutable? init] + (let [hint (jvm/source-tag name) type (jvm/tag-class hint)] + (when-not mutable? (jvm/value-type init)) + (when (and (not mutable?) hint (:primitive? init)) + (throw (UnsupportedOperationException. "Can't type hint a local with a primitive initializer"))) + (jvm/fact (and (not mutable?) (:primitive? init)) {:hint hint :var-meta {}} + (cond mutable? (or type :unknown) + (and (class? (:type init)) (.isPrimitive ^Class (:type init)) (not (:primitive? init))) :unknown + :else (or type (:type init)))))) + +(np/defpass caller-facts + {:from [l/JVM Raw] :to [l/JVM Fact] :args [t/Sym] :body true} + [name ^clojure.lang.Compiler$LocalBinding binding] + (if (instance? clojure.lang.Compiler$LocalBinding binding) + (jvm/fact (boolean (.getPrimitiveType binding)) {:hint (jvm/source-tag name) :var-meta {}} + (if (.hasJavaClass binding) (.getJavaClass binding) :unknown)) + (jvm/fact false {:hint (jvm/source-tag name) :var-meta {}} (or (jvm/tag-class (jvm/source-tag name)) :unknown)))) diff --git a/src/com/tailrecursion/blacktide/compiler/source_languages.clj b/src/com/tailrecursion/blacktide/compiler/source_languages.clj new file mode 100644 index 0000000..fc43c9c --- /dev/null +++ b/src/com/tailrecursion/blacktide/compiler/source_languages.clj @@ -0,0 +1,93 @@ +(ns com.tailrecursion.blacktide.compiler.source-languages + "Bound source separates executable children from source/provenance data. + Raw forms, macro environments and JVM hints are external compiler inputs." + (:require [com.tailrecursion.nanopass :as np] + [typed.clojure :as-alias t] + [com.tailrecursion.blacktide.compiler.jvm-languages :as j])) + +(np/deflanguage Bound + {Raw t/Any + Forms (t/Vec Raw) + Site (t/HMap :mandatory {:form Raw :file (t/Option t/Str) + :line (t/Option t/Int) :column (t/Option t/Int)} :complete? true) + Jvm j/JVM-Fact + Local (t/HMap :mandatory {:id t/Sym :name t/Sym :mutable? t/Bool :external? t/Bool + :jvm Jvm} :complete? true) + Binding (t/HMap :mandatory {:local Local :init Expr} :complete? true) + Bindings (t/Vec Binding) + Assignment (t/HMap :mandatory {:target Local :value Expr} :complete? true) + Assignments (t/Vec Assignment) + RecurTarget (t/HMap :mandatory {:id t/Sym :bindings (t/Vec Local)} :complete? true) + CaseSpec (t/HMap :mandatory {:shift t/Int :mask t/Int :default t/Int + :tests (t/Map t/Int (t/HVec [Raw t/Int])) + :switch (t/U ':compact ':sparse) + :test (t/U ':int ':hash-equiv ':hash-identity) + :skip (t/Option (t/Set t/Int))} :complete? true) + CaseSyntax (t/HMap :mandatory {:selector Raw :arms Forms} :complete? true) + DispatchSyntax (t/HMap :mandatory {:value Raw :spec CaseSpec :arms Forms} :complete? true) + Exprs (t/Vec Expr) + CollectionKind (t/U ':vector ':map ':set) + Expr (t/HMap :mandatory {:site Site :jvm Jvm :node Node} :complete? true) + Node (t/U (t/HMap :mandatory {:op ':literal :value Raw} :complete? true) + (t/HMap :mandatory {:op ':local :local Local} :complete? true) + (t/HMap :mandatory {:op ':global :name t/Sym} :complete? true) + (t/HMap :mandatory {:op ':var :name t/Sym} :complete? true) + (t/HMap :mandatory {:op ':host :operation j/JVM-Operation :args (t/Vec Expr)} :complete? true) + (t/HMap :mandatory {:op ':invoke :function Expr :args (t/Vec Expr)} :complete? true) + (t/HMap :mandatory {:op ':collection :kind CollectionKind :items Exprs + :metadata (t/Option Expr)} :complete? true) + (t/HMap :mandatory {:op ':setq :pairs Assignments} :complete? true) + (t/HMap :mandatory {:op ':psetq :pairs Assignments} :complete? true) + (t/HMap :mandatory {:op ':if :test Expr :then Expr :else Expr} :complete? true) + (t/HMap :mandatory {:op ':case :selector Expr :arms Exprs} :complete? true) + (t/HMap :mandatory {:op ':dispatch :value Expr :spec CaseSpec} :complete? true) + (t/HMap :mandatory {:op ':throw :value Expr} :complete? true) + (t/HMap :mandatory {:op ':label :name t/Sym} :complete? true) + (t/HMap :mandatory {:op ':goto :name t/Sym} :complete? true) + (t/HMap :mandatory {:op ':do :forms (t/Vec Expr)} :complete? true) + (t/HMap :mandatory {:op ':let :bindings (t/Vec Binding) :body Expr} :complete? true) + (t/HMap :mandatory {:op ':loop :id t/Sym :bindings (t/Vec Binding) :body Expr} :complete? true) + (t/HMap :mandatory {:op ':recur :target RecurTarget :args (t/Vec Expr)} :complete? true)) + Supply (t/HMap :mandatory {:prefix t/Str :next t/Int} :complete? true) + Context (t/HMap :mandatory {:locals (t/Map t/Sym Local) :macro-env (t/Map t/Sym Raw) + :markers (t/Map t/AnyVar t/Kw) :outer? t/Bool :transfer? t/Bool + :site Site + :recur (t/Option RecurTarget) :statement? t/Bool :tail? t/Bool} :complete? true)}) + +(np/derive-language Native Bound + {:remove {Node #{:setq :psetq :label :goto}}}) + +(np/derive-language Classified Bound + {:add {Node [(t/HMap :mandatory {:op ':native :value com.tailrecursion.blacktide.compiler.source-languages/Native-Expr} + :complete? true)]}}) + +(np/derive-language Sequential Classified + {:remove {Node #{:setq}} + :add {Node [(t/HMap :mandatory {:op ':write :target Local :value Expr} :complete? true)]}}) + +(np/derive-language Assigned Sequential + {:remove {Node #{:psetq}}}) + +(np/derive-language Cases Assigned + {:remove {Node #{:case}} + :add {Node [(t/HMap :mandatory {:op ':select :selector Expr :arms Exprs} :complete? true)]}}) + +(np/derive-language Collections Cases + {:remove {Node #{:collection}} + :add {Node [(t/HMap :mandatory {:op ':build :kind CollectionKind :items Exprs :unique? t/Bool + :metadata (t/Option Expr)} :complete? true)]}}) + +(np/derive-language Prepared Collections + {:remove {Node #{:host}} + :add {Node [(t/HMap :mandatory {:op ':host-call :operation j/JVM-Operation :args (t/Vec Expr)} :complete? true) + (t/HMap :mandatory {:op ':convert :conversion j/JVM-Conversion :value Expr} :complete? true)]}}) + +(np/derive-language Ordered Prepared + {:remove {Node #{:invoke :build :host-call :convert :recur :dispatch}} + :add {Node [(t/HMap :mandatory {:op ':convert :conversion j/JVM-Conversion :value Local} :complete? true) + (t/HMap :mandatory {:op ':dispatch :value Local :spec CaseSpec} :complete? true) + (t/HMap :mandatory {:op ':recur :target RecurTarget :args (t/Vec Local)} :complete? true) + (t/HMap :mandatory {:op ':host-call :operation j/JVM-Operation :args (t/Vec Local)} :complete? true) + (t/HMap :mandatory {:op ':invoke :function Local :args (t/Vec Local)} :complete? true) + (t/HMap :mandatory {:op ':build :kind CollectionKind :items (t/Vec Local) :unique? t/Bool} :complete? true) + (t/HMap :mandatory {:op ':metadata :value Local :metadata Local} :complete? true)]}}) diff --git a/src/com/tailrecursion/blacktide/compiler/source_values.clj b/src/com/tailrecursion/blacktide/compiler/source_values.clj new file mode 100644 index 0000000..52713ab --- /dev/null +++ b/src/com/tailrecursion/blacktide/compiler/source_values.clj @@ -0,0 +1,36 @@ +(ns ^:typed.clojure com.tailrecursion.blacktide.compiler.source-values + "Restore primitive source-local reads before compiler temporaries exist. + Clojure boxes each Object use of a primitive local independently; a machine + slot must not turn those reads into aliases of one box. Native trees already + preserve this behavior, and mutable locals have reference semantics." + (:require [com.tailrecursion.nanopass :as np] + [com.tailrecursion.blacktide.compiler.source-languages :as s] + [typed.clojure :as-alias t])) + +(declare expression forms binding-form pair pairs) + +(np/defpass node + {:from [s/Classified Node] :to [s/Classified Node] + :processors {Expr expression Exprs forms Binding binding-form Assignments pairs + Local clojure.core/identity Raw clojure.core/identity + CollectionKind clojure.core/identity RecurTarget clojure.core/identity CaseSpec clojure.core/identity}} + [node] + :native node) + +(np/defpass binding-form {:from [s/Classified Binding] :to [s/Classified Binding] + :processors {Expr expression Local clojure.core/identity}} [binding]) +(np/defpass pair {:from [s/Classified Assignment] :to [s/Classified Assignment] + :processors {Expr expression Local clojure.core/identity}} [assignment]) +(np/defpass pairs {:from [s/Classified Assignments] :to [s/Classified Assignments] + :processors {Assignment pair}} [assignments]) +(np/defpass forms {:from [s/Classified Exprs] :to [s/Classified Exprs] :processors {Expr expression}} [exprs]) + +(np/defpass expression {:from [s/Classified Expr] :to [s/Classified Expr] :body true} + [expr] + (let [expr (assoc expr :node (node (:node expr))) + type (:type (:jvm expr))] + (if (and (= :local (:op (:node expr))) (:primitive? (:jvm expr)) + (class? type) (.isPrimitive ^Class type) (not= Void/TYPE type)) + (assoc expr :node {:op :host :operation {:op :static :class clojure.lang.RT + :member (symbol (str (.getName ^Class type) "Cast")) :tags [type]} + :args [expr]}) expr))) diff --git a/src/com/tailrecursion/blacktide/compiler/storage.clj b/src/com/tailrecursion/blacktide/compiler/storage.clj new file mode 100644 index 0000000..4bb8ea6 --- /dev/null +++ b/src/com/tailrecursion/blacktide/compiler/storage.clj @@ -0,0 +1,100 @@ +(ns ^:typed.clojure com.tailrecursion.blacktide.compiler.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." + (:require [clojure.set :as set] + [com.tailrecursion.nanopass :as np] + [com.tailrecursion.blacktide.compiler.languages :as l] + [com.tailrecursion.blacktide.compiler.analysis-languages :as a] + [com.tailrecursion.blacktide.compiler.structured-languages :as s] + [com.tailrecursion.blacktide.compiler.graph :as graph] + [com.tailrecursion.blacktide.compiler.native :as native] + [com.tailrecursion.blacktide.compiler.value-facts :as facts] + [com.tailrecursion.blacktide.compiler.box-origins :as origins] + [typed.clojure :as-alias t])) + +(np/defpass loop-slots {:from [s/Joined Expr] :to (t/Set t/Sym)} + [node] + :bind (loop-slots (:body node)) + :join (set/union (loop-slots (:value node)) (loop-slots (:body node))) + :branch (set/union (loop-slots (:yes node)) (loop-slots (:no node))) + :switch (into #{} (mapcat loop-slots) (:arms node)) + :loop (into (loop-slots (:body node)) (:slots node)) + :recur #{} :return #{} :throw #{}) + +(np/defpass cyclic? {:from [s/Joined Expr] :to t/Bool} + [node] + :bind (cyclic? (:body node)) + :join (or (cyclic? (:value node)) (cyclic? (:body node))) + :branch (or (cyclic? (:yes node)) (cyclic? (:no node))) + :switch (boolean (some cyclic? (:arms node))) + :loop true :recur true :return false :throw false) + +(np/defpass operand-boxes + {:from [l/Machine Operand] :to (t/Set t/Sym) :args [s/Stored-Types] :body true} + [types operand] + (let [type (facts/atom-type types (:value operand))] + (if (and (class? type) (= type (:type (:jvm operand)))) #{} (graph/atom-reads (:value operand))))) + +(np/defpass operation-boxes + {:from [l/Machine Operation] :to (t/Set t/Sym) :args [s/Stored-Types (t/Set t/Sym)] :body true} + [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)))) + :host (if (#{:adapt} (:op (:operation rhs))) (graph/rhs-reads rhs) + (into #{} (mapcat #(operand-boxes types %)) (:args rhs))) + :convert (let [conversion (:conversion rhs)] + (if (and (:parameter? conversion) (.isPrimitive ^Class (:type conversion))) + (operand-boxes types (:value rhs)) (graph/rhs-reads rhs))) + :dispatch (operand-boxes types (:value rhs)) + :native (native/boxed-reads types (:value rhs)) + (graph/rhs-reads rhs)))) + +(np/defpass observed-values + {:from [l/Machine Program] :to (t/Set t/Sym) :body true} + [machine] + (let [ops (mapcat :ops (vals (:blocks machine))) + initial (into (into #{} (mapcat #(graph/term-reads (:term %))) (vals (:blocks machine))) + (mapcat #(if (= :value (:op (:value %))) #{} (graph/rhs-reads (:value %)))) ops)] + ((fn ^{::t/- [(t/Set t/Sym) :-> (t/Set t/Sym)]} solve [observed] + (let [next (reduce (fn [^{::t/- (t/Set t/Sym)} out ^{::t/- l/Machine-Operation} op] + (if (contains? observed (:target op)) (into out (graph/rhs-reads (:value op))) out)) observed ops)] + (if (= observed next) observed (recur next)))) initial))) + +(np/defpass term-boxes + {:from [l/Machine Term] :to (t/Set t/Sym) :args [s/Stored-Types (t/Set t/Sym)] :body true} + [types reboxable term] + (case (:op term) + :return (if (and (= :local (:op (:value term))) (contains? reboxable (:id (:value term)))) + #{} (graph/term-reads term)) + :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))) + +(np/defpass select-storage + {:from [s/Joined Expr] :to [s/Stored Program] :args [a/Analysis-Program] :body true} + [analysis expr] + ;; Clojure wraps an expression-position loop in an Object-returning call. + ;; Acyclic code has no such boundary: keep its return boxed so outer numeric + ;; overloads and native loop promotions cannot change after storage selection. + (let [reboxable (if (cyclic? expr) (origins/reboxable-values analysis) #{}) + slots (loop-slots expr) + initial (reduce-kv + (fn [^{::t/- s/Stored-Types} out ^{::t/- t/Sym} id ^{::t/- a/Analysis-Type} type] + (if (and (class? type) (or (not (contains? slots id)) (#{Long/TYPE Double/TYPE} type))) + (assoc out id type) out)) {} (:types analysis)) + machine (update (:machine analysis) :blocks #(select-keys % (:reachable analysis))) + observed (observed-values machine) + blocks (vals (:blocks machine)) + types ((fn ^{::t/- [s/Stored-Types :-> s/Stored-Types]} solve [types] + (let [boxed (into (into #{} (mapcat #(operation-boxes types observed %)) (mapcat :ops blocks)) + (mapcat #(term-boxes types reboxable (: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/structure.clj b/src/com/tailrecursion/blacktide/compiler/structure.clj new file mode 100644 index 0000000..5a98727 --- /dev/null +++ b/src/com/tailrecursion/blacktide/compiler/structure.clj @@ -0,0 +1,163 @@ +(ns ^:typed.clojure com.tailrecursion.blacktide.compiler.structure + "Recover acyclic control or a single-header loop with bounded duplication. + Construction records block identities to preserve native construction and + cache sites. Dead native code keeps the dispatcher for source validation." + (:require [com.tailrecursion.nanopass :as np] + [com.tailrecursion.blacktide.compiler.languages :as l] + [com.tailrecursion.blacktide.compiler.analysis-languages :as a] + [com.tailrecursion.blacktide.compiler.structured-languages :as s] + [com.tailrecursion.blacktide.compiler.graph :as graph] + [typed.clojure :as-alias t])) + +(declare edge body) + +(np/defpass block + {:from [l/Machine Label] :to [l/Machine Block] :args [l/Machine-Program] :body true} + [machine id] + (or (get (:blocks machine) id) (throw (IllegalStateException. "Missing checked block")))) + +(np/defpass scan + {:from [l/Machine Label] :to [s/Joined Scan] + :args [l/Machine-Program (t/Set l/Machine-Label) s/Joined-Scan] :body true} + [machine path state id] + (let [[seen headers] state] + (cond (contains? path id) [seen (conj headers id)] + (contains? seen id) state + :else (reduce (fn [^{::t/- s/Joined-Scan} state ^{::t/- l/Machine-Label} next] + (scan machine (conj path id) state next)) + [(conj seen id) headers] + (graph/successors (:term (block machine id))))))) + +(np/defpass harmless-atom? {:from [l/Machine Atom] :to t/Bool} + [atom] + :local true + :literal (nil? (:value atom))) + +(np/defpass harmless-block? {:from [l/Machine Block] :to t/Bool :body true} + [block] + ;; Lowering leaves nil result copies after transfers. No host/native form or + ;; arbitrary quoted constant may disappear before Clojure validates it. + (and (every? (fn [op] (let [rhs (:value op)] + (and (= :value (:op rhs)) (harmless-atom? (:value rhs))))) (:ops block)) + (let [term (:term block)] + (case (:op term) + :jump true + (:return :throw) (harmless-atom? (:value term)) + (:branch :switch) (harmless-atom? (:test term)))))) + +(np/defpass wrap + {:from [l/Machine Block] :to [s/Joined Expr] :args [s/Joined-Expr] :body true} + [end block] + (reduce (fn [^{::t/- s/Joined-Expr} body ^{::t/- l/Machine-Operation} op] + {:op :bind :binding op :body body}) end (reverse (:ops block)))) + +(np/defpass arms + {:from [l/Machine Term] :to [s/Joined Arms] :args [s/Joined-Context t/Bool t/Int] :body true} + [ctx inside? budget term] + (if (= :switch (:op term)) + ((fn ^{::t/- [t/Int t/Int (t/Vec s/Joined-Expr) (t/Vec l/Machine-Label) :-> s/Joined-Arms]} build [index budget out blocks] + (if (= index (count (:targets term))) {:op :built :budget budget :arms out :blocks blocks} + (let [result (edge ctx inside? 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 [] []) + {:op :exhausted})) + +(np/defpass terminator + {:from [l/Machine Term] :to [s/Joined Build] :args [s/Joined-Context t/Bool t/Int]} + [ctx inside? budget term] + :jump (edge ctx inside? budget (:target term)) + :return {:op :built :budget budget :blocks [] :expr term} + :throw {:op :built :budget budget :blocks [] :expr term} + :switch (let [result (arms ctx inside? budget term)] + (if (= :built (:op result)) + {:op :built :budget (:budget result) :blocks (:blocks result) :expr {:op :switch :test (:test term) :arms (:arms result)}} result)) + :branch (let [yes (edge ctx inside? budget (:yes term))] + (if (= :exhausted (:op yes)) yes + (let [no (edge ctx inside? (:budget yes) (:no term))] + (if (= :exhausted (:op no)) no + {:op :built :budget (:budget no) :blocks (into (:blocks yes) (:blocks no)) + :expr {:op :branch :test (:test term) :yes (:expr yes) :no (:expr no)}}))))) + +(np/defpass joined + {:from [a/Planned Join] :to [s/Joined Build] :args [s/Joined-Context t/Bool t/Int l/Machine-Term] :body true} + [ctx inside? budget term join] + (let [inner (assoc ctx :stop [(:target join) (:result join)]) + result (terminator inner inside? budget term)] + (if (= :built (:op result)) + (let [tail (edge ctx inside? (:budget result) (:target join))] + (if (= :built (:op tail)) + (assoc tail :blocks (into (:blocks result) (:blocks tail)) + :expr {:op :join :target (:result join) :value (:expr result) :body (:expr tail)}) tail)) + {:op :exhausted}))) + +(np/defpass body + {:from [l/Machine Label] :to [s/Joined Build] :args [s/Joined-Context t/Bool t/Int] :body true} + [ctx inside? budget id] + (let [block (block (:machine ctx) id) + budget (- budget (inc (count (:ops block))))] + (if (neg? budget) {:op :exhausted} + (let [result (if-let [join (get (:joins ctx) id)] + (joined ctx inside? budget (:term block) join) + (terminator ctx inside? budget (:term block)))] + (if (= :built (:op result)) (assoc result :blocks (conj (:blocks result) id) :expr (wrap (:expr result) block)) result))))) + +(np/defpass edge + {:from [l/Machine Label] :to [s/Joined Build] :args [s/Joined-Context t/Bool t/Int] :body true} + [ctx inside? budget id] + (let [stop (:stop ctx)] + (cond + (and stop (= id (first stop))) + {:op :built :budget budget :blocks [] :expr {:op :return :value {:op :local :id (second stop)}}} + (= id (:header ctx)) + (if inside? (if (pos? budget) {:op :built :budget (dec budget) :blocks [] :expr {:op :recur :slots (:slots ctx)}} + {:op :exhausted}) + (let [result (body ctx true (dec budget) id)] + (if (= :built (:op result)) (assoc result :expr {:op :loop :slots (:slots ctx) :body (:expr result)}) result))) + :else (body ctx inside? budget id)))) + +(np/defpass construction-site? + {:from [l/Machine 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/Machine 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 [a/Planned Program] :to [s/Joined Decision] :body true} + [analysis] + (let [machine (:machine analysis) reachable (:reachable analysis) + [_ headers] (scan machine #{} [#{} #{}] (:entry machine)) + dead (apply dissoc (:blocks machine) reachable) + header (first headers)] + (cond + (> (count headers) 1) {:op :fallback :reason :cycles} + (not (every? harmless-block? (vals dead))) {:op :fallback :reason :validation} + :else + (let [slots (if header (vec (sort (get (:live machine) header #{}))) []) + ctx {:machine machine :header header :slots slots :joins (:joins analysis) :stop nil} + budget (* 4 (reduce + 0 (map #(inc (count (:ops %))) (vals (select-keys (:blocks machine) reachable))))) + result (edge ctx false 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 :fallback :reason :budget}))))) diff --git a/src/com/tailrecursion/blacktide/compiler/structured_emit.clj b/src/com/tailrecursion/blacktide/compiler/structured_emit.clj new file mode 100644 index 0000000..c0c1e5a --- /dev/null +++ b/src/com/tailrecursion/blacktide/compiler/structured_emit.clj @@ -0,0 +1,110 @@ +(ns ^:typed.clojure com.tailrecursion.blacktide.compiler.structured-emit + "Emit lexical control with proven storage classes. Consumer source types stay + distinct from storage; Java argument conversions remain explicit evaluations." + (:require [com.tailrecursion.nanopass :as np] + [com.tailrecursion.blacktide.compiler.languages :as l] + [com.tailrecursion.blacktide.compiler.structured-languages :as s] + [com.tailrecursion.blacktide.compiler.compact-languages :as c] + [com.tailrecursion.blacktide.compiler.compact :as compact] + [com.tailrecursion.blacktide.compiler.collections :as collections] + [com.tailrecursion.blacktide.compiler.jvm :as jvm] + [typed.clojure :as-alias t] + [com.tailrecursion.blacktide.compiler.emit :as emit] + [com.tailrecursion.blacktide.compiler.host-emit :as host] + [com.tailrecursion.blacktide.compiler.case-emit :as cases])) + +(np/defpass primitive-form + {:from [l/Machine Code] :to [l/Machine Code] :args [java.lang.Class] :body true} + [^Class type form] + ;; The parameter conversion is exactly the conversion previously performed by + ;; RT.box(type); the matching primitive cast overload itself is an identity. + (list (with-meta (symbol "clojure.lang.RT" (str (.getName type) "Cast")) {:param-tags [type]}) form)) + +(np/defpass binding-form + {:from [l/Machine Code] :to [l/Machine Code] :args [s/Stored-Types t/Sym (t/Option java.lang.Class)] :body true} + [types id actual form] + (if-let [type (get types id)] (if (= type actual) form (primitive-form type form)) (host/box-form form))) + +(declare rhs-form) +(np/defpass atom-form {:from [c/Compact Atom] :to [l/Machine Code] :args [c/Compact-Types]} + [types node] + :local (:id node) + :literal (list 'quote (:value node)) + :inline (rhs-form types (:value node))) + +(np/defpass operand-form + {:from [c/Compact Operand] :to [l/Machine Code] :args [c/Compact-Types] :body true} + [types operand] + (let [form (atom-form types (:value operand)) actual (compact/atom-type types (:value operand)) + fact (:jvm operand)] + (if (and (class? actual) (= actual (:type fact))) form + (host/typed-form (assoc fact :primitive? false) (if (class? actual) (host/box-form form) form))))) + +(np/defpass rhs-form + {:from [c/Compact Rhs] :to [l/Machine Code] :args [c/Compact-Types]} + [types rhs] + :host (host/host-form (mapv #(operand-form types %) (:args rhs)) (:operation rhs)) + :dispatch (cases/selector-form (operand-form types (:value rhs)) (:spec rhs)) + :convert (let [conversion (:conversion rhs) operand (:value rhs)] + (if (and (:parameter? conversion) (.isPrimitive ^Class (:type conversion))) + (if (= (:type conversion) (compact/atom-type types (:value operand)) (:type (:jvm operand))) + (atom-form types (:value operand)) + (primitive-form (:type conversion) (operand-form types operand))) + (host/conversion-form conversion (:jvm operand) (atom-form types (:value operand))))) + :value (atom-form types (:value rhs)) + :global (emit/rhs-form rhs) :external (emit/rhs-form rhs) :var (emit/rhs-form rhs) :native (emit/rhs-form rhs) + :construct (collections/form (mapv #(atom-form types %) (:items rhs)) (:unique? rhs) (:kind rhs)) + :metadata (let [value (gensym "value__") metadata (gensym "metadata__")] + (list 'let* [(with-meta value {:tag 'clojure.lang.IObj}) (atom-form types (:value rhs)) + (with-meta metadata {:tag 'clojure.lang.IPersistentMap}) (atom-form types (:metadata rhs))] + (list '. value 'withMeta metadata))) + :invoke (list* (atom-form types (:function rhs)) (mapv #(atom-form types %) (:args rhs)))) + +(np/defpass rhs-type + {:from [c/Compact Rhs] :to (t/Option java.lang.Class) :args [c/Compact-Types] :body true} + [types rhs] + (case (:op rhs) + :value (compact/atom-type types (:value rhs)) + :convert (when (:parameter? (:conversion rhs)) (:type (:conversion rhs))) + :host (let [type (jvm/host-type (mapv #(:type (:jvm %)) (:args rhs)) (:operation rhs))] + (when (and (class? type) (not (#{:adapt} (:op (:operation rhs))))) type)) + ;; Other producers retain an explicit storage conversion. + nil)) + +(np/defpass operation-form + {:from [c/Compact Operation] :to [l/Machine Code] :args [c/Compact-Types] :body true} + [types op] + (binding-form types (:target op) (rhs-type types (:value op)) (rhs-form types (:value op)))) + +(declare expression) +(np/defpass let-form {:from [c/Compact Expr] :to [l/Machine Code] :args [c/Compact-Types] :body true} + [types node] + ((fn ^{::t/- [(t/Vec t/Any) c/Compact-Expr :-> l/Machine-Code]} bindings [pairs node] + (case (:op node) + :bind (let [op (:binding node)] + (recur (conj pairs (:target op) (operation-form types op)) (:body node))) + :join (recur (conj pairs (:target node) + (binding-form types (:target node) nil (expression types (:value node)))) (:body node)) + (list 'let* pairs (expression types node)))) [] node)) + +(np/defpass expression {:from [c/Compact Expr] :to [l/Machine Code] :args [c/Compact-Types]} + [types node] + :bind (let-form types node) + :effect (let [binding (:binding node) rhs (:value binding) type (get types (:target binding)) + form (rhs-form types rhs)] + (list 'do (if (and type (not= type (rhs-type types rhs))) (primitive-form type form) form) + (expression types (:body node)))) + :join (let-form types node) + :branch (list 'if (atom-form types (:test node)) (expression types (:yes node)) (expression types (:no node))) + :switch (cases/arms-form (mapv #(expression types %) (:arms node)) (atom-form types (:test node))) + :loop (list 'loop* (vec (mapcat (fn [binding] + [(:target binding) (binding-form types (:target binding) + (compact/atom-type types (:value binding)) + (atom-form types (:value binding)))]) (:bindings node))) + (expression types (:body node))) + :recur (list* 'recur (mapv #(atom-form types %) (:args node))) + :return (let [value (:value node) form (atom-form types value)] + ;; Tail compaction must retain the region's opaque result type. + (if (and (= :inline (:op value)) (= :native (:op (:value value)))) + (host/box-form form) form)) + :throw (list 'throw (atom-form types (:value node)))) diff --git a/src/com/tailrecursion/blacktide/compiler/structured_languages.clj b/src/com/tailrecursion/blacktide/compiler/structured_languages.clj new file mode 100644 index 0000000..9fbdf88 --- /dev/null +++ b/src/com/tailrecursion/blacktide/compiler/structured_languages.clj @@ -0,0 +1,34 @@ +(ns com.tailrecursion.blacktide.compiler.structured-languages + "Lexical control trees contain at most one loop target. General graph shapes remain + machines; bounded construction returns an explicit fallback, never partial code." + (:require [com.tailrecursion.nanopass :as np] + [com.tailrecursion.blacktide.compiler.languages :as l] + [com.tailrecursion.blacktide.compiler.analysis-languages :as a] + [typed.clojure :as-alias t])) + +(np/deflanguage Structured + {Expr (t/U (t/HMap :mandatory {:op ':bind :binding l/Machine-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 :value 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 ':exhausted} :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) + (t/HMap :mandatory {:op ':exhausted} :complete? true)) + Scan (t/HVec [(t/Set l/Machine-Label) (t/Set l/Machine-Label)]) + Fallback (t/U ':cycles ':validation ':budget ':native-sites) + 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 :target t/Sym :value Expr :body Expr} :complete? true)]} + :replace {Context (t/HMap :mandatory {:machine l/Machine-Program :header (t/Option l/Machine-Label) + :slots (t/Vec t/Sym) :joins a/Planned-Joins + :stop (t/Option (t/HVec [l/Machine-Label t/Sym]))} :complete? true)}}) + +(np/derive-language Stored Joined + {:replace {Types (t/Map t/Sym java.lang.Class) + Program (t/HMap :mandatory {:body Expr :types Types} :complete? true)}}) diff --git a/src/com/tailrecursion/blacktide/compiler/transfers.clj b/src/com/tailrecursion/blacktide/compiler/transfers.clj new file mode 100644 index 0000000..3be932b --- /dev/null +++ b/src/com/tailrecursion/blacktide/compiler/transfers.clj @@ -0,0 +1,34 @@ +(ns ^:typed.clojure com.tailrecursion.blacktide.compiler.transfers + "Resolve region-local jumps after all labels exist, including dead source. + Source analysis admits only outer-body labels and statement-position jumps." + (:require [com.tailrecursion.blacktide.compiler.errors :as errors] + [com.tailrecursion.nanopass :as np] + [com.tailrecursion.blacktide.compiler.languages :as l] + [typed.clojure :as-alias t])) + + +(np/defpass resolve-term + {:from [l/Symbolic Term] :to [l/CFG Term] :args [l/Symbolic-LabelTable]} + [labels term] + :jump term + :branch term + :switch term + :return term + :throw term + :transfer + (let [target (or (get labels (:name term)) + (errors/fail :undefined-label "Undefined label" {:label (:name term)} (:site term)))] + {:op :jump :target target})) + +(np/defpass resolve-block + {:from [l/Symbolic Block] :to [l/CFG Block] :args [l/Symbolic-LabelTable] :body true} + [labels block] + (assoc block :term (resolve-term labels (:term block)))) + +(np/defpass resolve-program + {:from [l/Symbolic Program] :to [l/CFG Program] :body true} + [program] + {:entry (:entry program) + :blocks (reduce-kv (fn [^{::t/- (t/Map l/CFG-Label l/CFG-Block)} out + ^{::t/- l/Symbolic-Label} id ^{::t/- l/Symbolic-Block} block] + (assoc out id (resolve-block (:labels program) block))) {} (:blocks program))}) diff --git a/src/com/tailrecursion/blacktide/compiler/value_facts.clj b/src/com/tailrecursion/blacktide/compiler/value_facts.clj new file mode 100644 index 0000000..75d5732 --- /dev/null +++ b/src/com/tailrecursion/blacktide/compiler/value_facts.clj @@ -0,0 +1,65 @@ +(ns ^:typed.clojure com.tailrecursion.blacktide.compiler.value-facts + "Prove primitive results across all reachable definitions, including copy + cycles. Hints describe consumers; only actual primitive producers prove values." + (:require [com.tailrecursion.nanopass :as np] + [com.tailrecursion.blacktide.compiler.languages :as l] + [com.tailrecursion.blacktide.compiler.analysis-languages :as a] + [com.tailrecursion.blacktide.compiler.jvm :as jvm] + [com.tailrecursion.blacktide.compiler.jvm-languages :as j] + [com.tailrecursion.blacktide.compiler.graph :as graph] + [typed.clojure :as-alias t])) + +(np/defpass primitive {:from [j/JVM Type] :to [a/Analysis Type] :body true} + [type] + (if (and (class? type) (.isPrimitive ^Class type) (not= Void/TYPE type)) type :boxed)) + +(np/defpass join {:from [a/Analysis Type] :to [a/Analysis Type] :args [a/Analysis-Type] :body true} + [a b] + (cond (= a :bottom) b (= b :bottom) a (= a b) a :else :boxed)) + +(np/defpass atom-type {:from [l/Machine Atom] :to [a/Analysis Type] :args [a/Analysis-Types]} + [types node] + :local (get types (:id node) :bottom) + ;; Quoted Integer constants must keep their boxed identity and width. + :literal (cond (instance? Long (:value node)) (primitive Long/TYPE) + (instance? Double (:value node)) (primitive Double/TYPE) + :else :boxed)) + +(np/defpass rhs-type {:from [l/Machine Rhs] :to [a/Analysis Type] :args [a/Analysis-Types]} + [types rhs] + :value (atom-type types (:value rhs)) + :external (if (:primitive? (:jvm rhs)) (primitive (:type (:jvm rhs))) :boxed) + :convert (if (:parameter? (:conversion rhs)) (primitive (:type (:conversion rhs))) :boxed) + :dispatch (primitive Long/TYPE) + :host (if (#{:adapt} (:op (:operation rhs))) :boxed + (primitive (jvm/host-type (mapv #(:type (:jvm %)) (:args rhs)) (:operation rhs)))) + :native :boxed + :global :boxed + :var :boxed + :construct :boxed + :metadata :boxed + :invoke :boxed) + +(np/defpass definitions + {:from [l/Machine Program] :to [a/Analysis Definitions] :args [(t/Set l/Machine-Label)] :body true} + [reachable machine] + (reduce (fn [^{::t/- a/Analysis-Definitions} out ^{::t/- l/Machine-Operation} op] + (assoc out (:target op) (conj (get out (:target op) []) (:value op)))) + {} (mapcat :ops (vals (select-keys (:blocks machine) reachable))))) + +(np/defpass solve {:from [a/Analysis Definitions] :to [a/Analysis Types] :body true} + [definitions] + ((fn ^{::t/- [a/Analysis-Types :-> a/Analysis-Types]} fixed-point [types] + (let [next (reduce-kv + (fn [^{::t/- a/Analysis-Types} out ^{::t/- t/Sym} id ^{::t/- (t/Vec l/Machine-Rhs)} values] + (assoc out id + ((fn ^{::t/- [t/Int a/Analysis-Type :-> a/Analysis-Type]} common [index type] + (if (< index (count values)) + (recur (inc index) (join type (rhs-type types (nth values index)))) type)) + 0 :bottom))) {} definitions)] + (if (= types next) next (recur next)))) {})) + +(np/defpass analyze {:from [l/Machine Program] :to [a/Analysis Program] :body true} + [machine] + (let [reachable (graph/reachable (graph/collect-facts {:entry (:entry machine) :blocks (: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 new file mode 100644 index 0000000..9e6c76b --- /dev/null +++ b/src/com/tailrecursion/blacktide/runtime.clj @@ -0,0 +1,7 @@ +(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/src/com/tailrecursion/nanopass.clj b/src/com/tailrecursion/nanopass.clj new file mode 100644 index 0000000..58ff10c --- /dev/null +++ b/src/com/tailrecursion/nanopass.clj @@ -0,0 +1,86 @@ +(ns com.tailrecursion.nanopass + "Define immutable languages and emit their Typed Clojure aliases on demand. + Ordinary language loading has no Typed Clojure dependency." + (:require [com.tailrecursion.nanopass.language :as language] + [com.tailrecursion.nanopass.pass :as pass])) + +(defn- description [name] + (let [v (when (symbol? name) (resolve name))] + (when-not (and (var? v) (::language (meta v))) + (language/fail :unknown-language name "Expected a nanopass language Var")) + @v)) + +(defmacro deflanguage + "Define named nonterminals using Typed Clojure syntax. Local references are + rebound when deriving a language; quoted singleton values remain data." + [name types] + (when-not (and (simple-symbol? name) (map? types)) + (language/fail :invalid-language &form "Expected a simple language name and a type map")) + (let [qualified (symbol (str (ns-name *ns*)) (str name)) + types (language/qualify *ns* (set (keys types)) types) + value (language/language qualified types)] + ;; Keep each descriptor's constants in its own class during AOT compilation; + ;; many derived languages otherwise overflow the namespace initializer. + `(def ~(vary-meta name assoc ::language true) ((fn [] '~value))))) + +(defmacro derive-language + "Derive from a language with :remove tag sets, :add production vectors, and + :replace complete nonterminal definitions. Changes use local nonterminal names." + [name parent changes] + (when-not (and (simple-symbol? name) (map? changes)) + (language/fail :invalid-extension &form "Expected a simple language name and a change map")) + (let [base (description parent) + names (set (concat (keys (:types base)) (keys (:replace changes)))) + changes (language/qualify *ns* names changes) + value (language/derive (symbol (str (ns-name *ns*)) (str name)) base changes)] + `(def ~(vary-meta name assoc ::language true) ((fn [] '~value))))) + +(defmacro ann-languages + "Emit checker declarations from language descriptions in an annotation-only + namespace requiring typed.clojure. This macro is unnecessary in ordinary use." + [& languages] + `(do ~@(language/type-forms (map description languages)))) + +(defmacro defpass + "Define a checked pass. :from and :to identify [language nonterminal]; :to may + instead be a result type. :args types the leading context parameters. Supply + tag/expression clauses; unchanged productions get structural recursion through + :processors (nonterminal -> function). :body true accepts one explicit body." + [name {:keys [from to args processors body] :as options} arguments & clauses] + (when-not (and (simple-symbol? name) (vector? from) (= 2 (count from)) + (symbol? (first from)) (simple-symbol? (second from)) + (contains? options :to) + (or (not (vector? to)) (and (= 2 (count to)) (symbol? (first to)) + (simple-symbol? (second to)))) + (or (nil? args) (vector? args)) + (or (nil? body) (boolean? body)) + (or (nil? processors) (and (map? processors) (every? simple-symbol? (keys processors)) + (every? symbol? (vals processors)))) + (vector? arguments) (every? simple-symbol? arguments) + (= (count arguments) (count (set arguments))) + (= (count arguments) (inc (count args))) + (every? #{:from :to :args :processors :body} (keys options))) + (language/fail :invalid-pass &form "Invalid pass name, signature, or options")) + (let [source (description (first from)) + target (when (vector? to) (description (first to))) + result-type (if target (language/type-name target (second to)) + (language/qualify *ns* #{} to))] + (with-meta (pass/definition + {:name (symbol (str (ns-name *ns*)) (str name)) + :source source :target target :from (second from) :to (when target (second to)) + :params (mapv #(language/qualify *ns* #{} %) args) :arguments arguments + :result-type result-type + :processors (into {} (map (fn [[nt processor]] [nt (language/qualify *ns* #{} processor)])) processors) + :body? body} + clauses) (meta &form)))) + +(defmacro ann-passes + "Emit checker signatures stored by defpass. The generated function bodies + must additionally be checked with Typed Clojure's check-ns-clj." + [& passes] + `(do ~@(for [name passes + :let [v (resolve name) signature (::signature (meta v))]] + (do (when-not signature + (language/fail :unknown-pass name "Expected a defined nanopass function")) + (list 'typed.clojure/ann + (symbol (str (ns-name (:ns (meta v)))) (str (:name (meta v)))) signature))))) diff --git a/src/com/tailrecursion/nanopass/language.clj b/src/com/tailrecursion/nanopass/language.clj new file mode 100644 index 0000000..a042d2b --- /dev/null +++ b/src/com/tailrecursion/nanopass/language.clj @@ -0,0 +1,170 @@ +(ns com.tailrecursion.nanopass.language + "Immutable language descriptions from public Typed Clojure type syntax. + Local nonterminals remain symbolic until aliases are emitted for a language." + (:refer-clojure :exclude [derive]) + (:require [clojure.set :as set])) + +(defn fail [kind form message & [details]] + (throw (ex-info message (merge {:kind kind :form form :file *file*} + (select-keys (meta form) [:line :column]) details)))) + +(defn literal-type? [form] + (and (seq? form) (#{'quote 'typed.clojure/Val 'typed.clojure/Value} (first form)))) + +(defn- qualified-type-name [sym] + (if (= "clojure.core.typed" (namespace sym)) + (symbol "typed.clojure" (name sym)) sym)) + +(defn qualify + "Resolve type names in the defining namespace; leave local nonterminals and + singleton quoted values intact. No checker is loaded or consulted." + [caller nonterminals form] + (cond + (and (seq? form) (= 'quote (first form))) + (if (and (= 2 (count form)) (not (coll? (second form)))) form + (fail :unsupported-type form "Use explicit HMap/HVec types, or Val for a literal collection")) + (symbol? form) + (cond + (nonterminals form) form + (namespace form) (qualified-type-name + (if-let [alias (get (ns-aliases caller) (symbol (namespace form)))] + (symbol (str (ns-name alias)) (name form)) form)) + :else (let [v (ns-resolve caller form)] + (cond (class? v) (symbol (.getName ^Class v)) + (var? v) (symbol (str (ns-name (:ns (meta v)))) (str (:name (meta v)))) + :else (fail :unknown-type form "Unknown type or nonterminal")))) + (seq? form) (let [op (qualify caller nonterminals (first form))] + (if (#{'typed.clojure/Val 'typed.clojure/Value} op) + (cons op (rest form)) + (cons op (map #(qualify caller nonterminals %) (rest form))))) + (vector? form) (mapv #(qualify caller nonterminals %) form) + (map? form) (into (empty form) (map (fn [[k v]] [k (qualify caller nonterminals v)])) form) + :else form)) + +(defn hmap-options [form] + (when (= 'typed.clojure/HMap (first form)) + (let [options (rest form)] + (when (or (odd? (count options)) + (not= (count (take-nth 2 options)) (count (set (take-nth 2 options))))) + (fail :invalid-hmap form "HMap options must be distinct key/value pairs")) + (apply hash-map options)))) + +(defn children + "Immediate type children in supported structural constructors. nil means an + opaque terminal type, whereas an empty vector means a known leaf structure." + [form] + (when (seq? form) + (case (first form) + typed.clojure/HMap (let [{:keys [mandatory optional]} (hmap-options form)] + (vec (concat (vals mandatory) (vals optional)))) + typed.clojure/HVec (second form) + (typed.clojure/U typed.clojure/Vec typed.clojure/Map + typed.clojure/Set typed.clojure/Option) (vec (rest form)) + nil))) + +(defn references [nonterminals form] + (into #{} (filter nonterminals) + (tree-seq #(and (coll? %) (not (literal-type? %))) seq form))) + +(defn tag [form] + (when (and (seq? form) (= 'typed.clojure/HMap (first form))) + (let [value (get-in (hmap-options form) [:mandatory :op])] + (when (and (literal-type? value) (= 2 (count value)) (keyword? (second value))) + (second value))))) + +(defn alternatives [form] + (if (and (seq? form) (= 'typed.clojure/U (first form))) + (vec (mapcat alternatives (rest form))) [form])) + +(defn- validate-type [nonterminals form] + (when (seq? form) + (case (first form) + typed.clojure/HMap + (let [{:keys [mandatory optional complete?] :as options} (hmap-options form)] + (when-not (and (true? complete?) + (every? #{:mandatory :optional :complete?} (keys options)) + (or (nil? mandatory) (map? mandatory)) + (or (nil? optional) (map? optional)) + (every? keyword? (concat (keys mandatory) (keys optional)))) + (fail :invalid-hmap form "Structural maps need keyword fields and :complete? true")) + (when (seq (set/intersection (set (keys mandatory)) (set (keys optional)))) + (fail :invalid-hmap form "Required and optional fields must be disjoint")) + (when (or (contains? optional :op) (and (contains? mandatory :op) (nil? (tag form)))) + (fail :invalid-tag form "An :op field must be a required quoted keyword"))) + typed.clojure/HVec + (when-not (and (= 2 (count form)) (vector? (second form))) + (fail :invalid-tuple form "Use a fixed HVec vector without options")) + (typed.clojure/Vec typed.clojure/Set typed.clojure/Option) + (when-not (= 2 (count form)) (fail :invalid-type form "Expected one type argument")) + typed.clojure/Map + (when-not (= 3 (count form)) (fail :invalid-type form "Expected key and value types")) + typed.clojure/U + (let [tags (keep tag (alternatives form))] + (when-not (= (count tags) (count (set tags))) + (fail :duplicate-tag form "Union production tags must be distinct"))) + nil)) + (if-let [parts (children form)] + (doseq [child parts] (validate-type nonterminals child)) + (when (and (not (nonterminals form)) (seq (references nonterminals form))) + (fail :unsupported-recursion form "Nonterminal occurs in an unsupported type constructor")))) + +(defn language [name types] + (when-not (and (qualified-symbol? name) (map? types) (seq types) + (every? simple-symbol? (keys types))) + (fail :invalid-language types "A language needs a qualified name and named nonterminals")) + (let [nonterminals (set (keys types))] + (doseq [[_ type] types] (validate-type nonterminals type))) + {:name name :types (into (sorted-map) types)}) + +(defn derive [name base {:keys [remove add replace] :as changes}] + (when-not (every? #{:remove :add :replace} (keys changes)) + (fail :invalid-extension changes "Expected :remove, :add, or :replace")) + (let [types (:types base) + removed-or-added (set (concat (keys remove) (keys add)))] + (when (seq (set/intersection removed-or-added (set (keys replace)))) + (fail :invalid-extension changes "Replace cannot also add/remove the same nonterminal")) + (doseq [nt removed-or-added] + (when-not (contains? types nt) + (fail :unknown-nonterminal nt "Add/remove needs an existing nonterminal"))) + (language name + (merge + (reduce + (fn [types nt] + (let [old (alternatives (get types nt)) + tags (set (map tag old)) + removed (get remove nt #{}) + added (get add nt [])] + (when-not (and (set? removed) (vector? added) + (not (contains? tags nil)) (every? tag added)) + (fail :invalid-extension nt "Add/remove applies to tagged productions")) + (when-not (set/subset? removed tags) + (fail :unknown-production removed "Cannot remove a production that is absent")) + (assoc types nt (list* 'typed.clojure/U + (concat (clojure.core/remove #(removed (tag %)) old) added))))) + types removed-or-added) + replace)))) + +(defn type-name [{:keys [name types]} nonterminal] + (when-not (contains? types nonterminal) + (fail :unknown-nonterminal nonterminal "Language has no such nonterminal" {:language name})) + (symbol (namespace name) (str (clojure.core/name name) "-" nonterminal))) + +(defn instantiate + "Replace local nonterminals with this language's aliases, preserving quoted + singleton data. Used for checker declarations and pass signatures." + ([language form] (instantiate language {} form)) + ([{:keys [types] :as language} bound form] + (cond + (literal-type? form) form + (and (symbol? form) (contains? types form)) (get bound form (type-name language form)) + (seq? form) (apply list (map #(instantiate language bound %) form)) + (vector? form) (mapv #(instantiate language bound %) form) + (map? form) (into (empty form) (map (fn [[k v]] [k (instantiate language bound v)])) form) + :else form))) + +(defn type-forms [languages] + ;; Rec prevents container-kind checks from expanding recursive aliases forever. + ;; Cross-nonterminal aliases are registered before any pass is checked. + (for [language languages [nt type] (:types language)] + (list 'typed.clojure/defalias (type-name language nt) + (list 'typed.clojure/Rec [nt] (instantiate language {nt nt} type))))) diff --git a/src/com/tailrecursion/nanopass/pass.clj b/src/com/tailrecursion/nanopass/pass.clj new file mode 100644 index 0000000..c09ce38 --- /dev/null +++ b/src/com/tailrecursion/nanopass/pass.clj @@ -0,0 +1,140 @@ +(ns com.tailrecursion.nanopass.pass + "Generate ordinary checked functions for a declared language transformation. + Structural defaults recurse only through explicit nonterminal positions." + (:require [com.tailrecursion.nanopass.language :as l])) + +(defn- typed [sym type] + (vary-meta sym assoc :typed.clojure/- type)) + +(defn- unexpected [pass value] + (let [bad (gensym "unhandled__")] + `((fn [~(typed bad 'typed.clojure/Nothing)] (throw (ex-info "Unexpected nanopass production" + {:pass '~pass :node ~bad}))) ~value))) + +(declare walk dispatch) + +(defn- preserve-meta [{:keys [target]} type value result] + (let [type (l/instantiate target type) + id (gensym "result__") + attach (with-meta 'clojure.core/with-meta + {:typed.clojure/inst [(list 'typed.clojure/I 'clojure.lang.IObj type)]})] + `(let [~(typed id type) ~result] + (if (instance? clojure.lang.IObj ~id) + (~attach ~id (meta ~value)) + ~id)))) + +(defn- mapper [{:keys [source] :as context} in out] + (let [item (gensym "item__")] + `(fn [~(typed item (l/instantiate source in))] ~(walk context in out item)))) + +(defn- map-fields [{:keys [source target] :as context} in out value] + (let [a (l/hmap-options in) b (l/hmap-options out)] + (when-not (every? #(= (set (keys (% a))) (set (keys (% b)))) [:mandatory :optional]) + (l/fail :explicit-clause-required in "Changed map fields need an explicit clause")) + (let [changed (filter (fn [[key type]] + (or (not= type (get-in b [:optional key])) + (seq (l/references (set (keys (:types source))) type)))) + (sort-by (comp str key) (:optional a))) + base (if (seq changed) `(dissoc ~value ~@(map first changed)) value) + fields (mapcat (fn [[key type]] + (let [before `(~key ~value) + after (walk context type (get-in b [:mandatory key]) before)] + (when (not= before after) [key after]))) (:mandatory a)) + required (if (seq fields) `(assoc ~base ~@fields) base)] + ;; assoc/dissoc preserve metadata. Retain unchanged fields and reconstruct + ;; only recursive or changed fields; no metadata wrapper is needed. + (reduce (fn [result [key type]] + (let [acc (gensym "record__") child (gensym "child__") + other-keys (remove #{key} (concat (keys (:mandatory a)) (keys (:optional a))))] + ;; Project a zero/one-entry map. Its value type excludes + ;; missing-key nil while retaining an explicitly present nil. + `(reduce-kv + (fn [~(typed acc (l/instantiate target out)) ~'_ ~(typed child (l/instantiate source type))] + (assoc ~acc ~key ~(walk context type (get-in b [:optional key]) child))) + ~result (dissoc ~value ~@other-keys)))) + required changed)))) + +(defn- optional [context in out value] + (let [id (gensym "optional__")] + `((fn [~(typed id (list 'typed.clojure/Option (l/instantiate (:source context) in)))] + (if (nil? ~id) nil ~(walk context in out id))) ~value))) + +(defn- walk [{:keys [source target processors params] :as context} in out value] + (cond + (and (symbol? in) (contains? (:types source) in)) + (if-let [processor (and (contains? (:types target) out) (get processors in))] + `(~processor ~@params ~value) + (l/fail :missing-processor in "Recursive field needs a processor" {:target out})) + + (and (= in out) (empty? (l/references (set (keys (:types source))) in))) value + + (not (and (seq? in) (seq? out) (= (first in) (first out)))) + (l/fail :explicit-clause-required in "Changed field types need an explicit clause" {:target out}) + + :else + (case (first in) + typed.clojure/HMap (map-fields context in out value) + typed.clojure/Vec (preserve-meta context out value `(mapv ~(mapper context (second in) (second out)) ~value)) + typed.clojure/Set (preserve-meta context out value `(into #{} (map ~(mapper context (second in) (second out))) ~value)) + typed.clojure/HVec + (do (when-not (= (count (second in)) (count (second out))) + (l/fail :explicit-clause-required in "Changed tuple arity needs an explicit clause")) + (preserve-meta context out value + (mapv (fn [i a b] (walk context a b `(nth ~value ~i))) + (range) (second in) (second out)))) + typed.clojure/Map + (let [[_ ik iv] in [_ ok ov] out + acc (gensym "map__") k (gensym "key__") v (gensym "val__")] + (preserve-meta context out value + `(reduce-kv (fn [~(typed acc (l/instantiate target out)) + ~(typed k (l/instantiate source ik)) + ~(typed v (l/instantiate source iv))] + (assoc ~acc ~(walk context ik ok k) ~(walk context iv ov v))) {} ~value))) + typed.clojure/Option (optional context (second in) (second out) value) + typed.clojure/U + (let [a (l/alternatives in) b (l/alternatives out)] + (if (and (= 2 (count a) (count b)) (some nil? a) (some nil? b)) + (optional context (first (remove nil? a)) (first (remove nil? b)) value) + (dispatch context in out value {}))) + (l/fail :explicit-clause-required in "Cannot derive structural traversal for this type")))) + +(defn- dispatch [{:keys [name] :as context} in out value clauses] + (let [variants (l/alternatives in) + tags (set (map l/tag variants)) + targets (when out (into {} (map (juxt l/tag identity)) (l/alternatives out)))] + (when (contains? tags nil) + (l/fail :ambiguous-dispatch in "Dispatch needs tagged map alternatives; use :body true")) + (when-not (every? tags (keys clauses)) + (l/fail :unknown-production clauses "Pass clause is absent from the input language")) + `(case (:op ~value) + ~@(mapcat (fn [variant] + (let [tag (l/tag variant)] + [tag (if (contains? clauses tag) (get clauses tag) + (if-let [other (get targets tag)] + (walk context variant other value) + (l/fail :missing-clause variant "Removed production needs a clause" + {:pass name :production tag})))])) variants) + ~(unexpected name value)))) + +(defn definition + [{:keys [name source target from to params arguments result-type processors body?]} forms] + (let [node (last arguments) + input-type (l/type-name source from) + signature (vec (concat params [input-type :-> result-type])) + context {:name name :source source :target target + :processors (merge {from name} processors) :params (vec (butlast arguments))} + in (get-in source [:types from]) out (get-in target [:types to]) + body (if body? + (do (when-not (= 1 (count forms)) + (l/fail :invalid-pass forms "A whole-body pass needs one expression")) + (first forms)) + (do (when (or (odd? (count forms)) + (not= (count (take-nth 2 forms)) (count (set (take-nth 2 forms))))) + (l/fail :invalid-pass forms "Pass clauses must be distinct tag/expression pairs")) + (if (and (empty? forms) (not-every? l/tag (l/alternatives in))) + (walk context in out node) + (dispatch context in out node (apply hash-map forms))))) + local-name (symbol (clojure.core/name name))] + `(defn ~local-name + {:com.tailrecursion.nanopass/signature '~signature} + ~arguments ~body))) diff --git a/test-resources/typedclojure_config.cljc b/test-resources/typedclojure_config.cljc new file mode 100644 index 0000000..ee5384c --- /dev/null +++ b/test-resources/typedclojure_config.cljc @@ -0,0 +1 @@ +{:ann [com.tailrecursion.nanopass.annotations com.tailrecursion.blacktide.annotations]} diff --git a/test/com/tailrecursion/blacktide/algorithms_test.clj b/test/com/tailrecursion/blacktide/algorithms_test.clj new file mode 100644 index 0000000..abdbb7f --- /dev/null +++ b/test/com/tailrecursion/blacktide/algorithms_test.clj @@ -0,0 +1,59 @@ +(ns com.tailrecursion.blacktide.algorithms-test + "Partitioning and small parsers compared with independent collection/regex + models. These programs are also used in the public examples." + (:require [clojure.test :refer [deftest is]] + [com.tailrecursion.blacktide :refer [loophole label goto setq]] + [blacktide.algorithms :refer [tokens partition! decimal-prefix]])) + +(defn- partition-reference [xs] + (when (seq xs) + (let [pivot (first xs)] + (loop [xs (vec xs) left -1 right (count xs)] + (let [left (first (drop-while #(< (xs %) pivot) (range (inc left) (count xs)))) + right (first (drop-while #(> (xs %) pivot) (range (dec right) -1 -1)))] + (if (>= left right) [right xs] + (recur (assoc xs left (xs right) right (xs left)) left right))))))) + +(defn- decimal-reference [s] + (when-let [[token sign digits] (re-find #"^([+-]?)([0-9]+)" s)] + [(* (if (= sign "-") -1 1) (bigint (java.math.BigInteger. digits))) (count token)])) + +(defn- even-flips? [xs] + (loophole [i 0 even-state true valid true] + (label scan) + (when (= i (count xs)) (goto done)) + (case (nth xs i) + :flip (setq even-state (not even-state)) + :hold nil + (do (setq valid false) (goto done))) + (setq i (inc i)) (goto scan) + (label done) (and valid even-state))) + +(defn token-reference [s] + (->> s (partition-by #(Character/isWhitespace ^char %)) + (remove #(Character/isWhitespace ^char (first %))) (mapv #(apply str %)))) + +(deftest parsing-and-partition-boundaries + (doseq [s ["" "-" "+" "x" "0" "-0" "+0001" "-9223372036854775809!" + "9999999999999999999999999999999999999999999999999"]] + (is (= (decimal-reference s) (decimal-prefix s)))) + (doseq [s ["" " " "a" " a " "one two\tthree\nfour" "a\u2003b\u00a0c"]] + (is (= (token-reference s) (tokens s)))) + (doseq [xs [[] [1] [2 1] [1 1 1] [0 -1 2 -3] [Long/MAX_VALUE Long/MIN_VALUE 0]]] + (let [a (long-array xs)] + (is (= (partition-reference xs) (when (seq xs) [(partition! a) (vec a)])))))) + +(deftest deterministic-parser-and-mutation-differentials + (let [seed 424242 rng (java.util.Random. seed)] + (dotimes [index 300] + (let [xs (vec (repeatedly (.nextInt rng 35) #(- (.nextInt rng 31) 15))) a (long-array xs) + number (str (nth ["" "-" "+"] (.nextInt rng 3)) + (apply str (repeatedly (.nextInt rng 25) #(char (+ (int \0) (.nextInt rng 10))))) + (nth ["" "x" " 3"] (.nextInt rng 3))) + text (apply str (repeatedly (.nextInt rng 40) #(nth [\a \b \space \tab \newline \u2003 \u00a0] (.nextInt rng 7)))) + states (vec (repeatedly (.nextInt rng 35) #(nth [:flip :hold :hold :bad] (.nextInt rng 4)))) + context (str "seed=" seed " index=" index)] + (is (= (partition-reference xs) (when (seq xs) [(partition! a) (vec a)])) context) + (is (= (decimal-reference number) (decimal-prefix number)) context) + (is (= (token-reference text) (tokens text)) context) + (is (= (and (every? #{:flip :hold} states) (even? (count (filter #{:flip} states)))) (even-flips? states)) context))))) diff --git a/test/com/tailrecursion/blacktide/annotations.clj b/test/com/tailrecursion/blacktide/annotations.clj new file mode 100644 index 0000000..6166da3 --- /dev/null +++ b/test/com/tailrecursion/blacktide/annotations.clj @@ -0,0 +1,138 @@ +(ns com.tailrecursion.blacktide.annotations + "Checking-only aliases and signatures for every production compiler pass." + (:require [typed.clojure] + [clojure.core.typed :as ct] + [com.tailrecursion.nanopass :as np] + [com.tailrecursion.blacktide.compiler.assignment :as assignment] + [com.tailrecursion.blacktide.compiler.backend :as backend] + [com.tailrecursion.blacktide.compiler.boundaries :as boundaries] + [com.tailrecursion.blacktide.compiler.box-origins :as box-origins] + [com.tailrecursion.blacktide.compiler.case-emit :as case-emit] + [com.tailrecursion.blacktide.compiler.cases :as cases] + [com.tailrecursion.blacktide.compiler.collections :as collections] + [com.tailrecursion.blacktide.compiler.compact :as compact] + [com.tailrecursion.blacktide.compiler.compact-languages :as compact-languages] + [com.tailrecursion.blacktide.compiler.effects :as effects] + [com.tailrecursion.blacktide.compiler.emit :as emit] + [com.tailrecursion.blacktide.compiler.errors :as errors] + [com.tailrecursion.blacktide.compiler.feasible :as feasible] + [com.tailrecursion.blacktide.compiler.graph :as graph] + [com.tailrecursion.blacktide.compiler.host-emit :as host-emit] + [com.tailrecursion.blacktide.compiler.invariants :as invariants] + [com.tailrecursion.blacktide.compiler.joins :as joins] + [com.tailrecursion.blacktide.compiler.jvm :as jvm] + [com.tailrecursion.blacktide.compiler.jvm-languages :as jvm-languages] + [com.tailrecursion.blacktide.compiler.languages :as languages] + [com.tailrecursion.blacktide.compiler.loop-facts :as loop-facts] + [com.tailrecursion.blacktide.compiler.lower :as lower] + [com.tailrecursion.blacktide.compiler.native :as native] + [com.tailrecursion.blacktide.compiler.analysis-languages :as analysis-languages] + [com.tailrecursion.blacktide.compiler.order :as order] + [com.tailrecursion.blacktide.compiler.prepare :as prepare] + [com.tailrecursion.blacktide.compiler.source :as source] + [com.tailrecursion.blacktide.compiler.source-case :as source-case] + [com.tailrecursion.blacktide.compiler.source-host :as source-host] + [com.tailrecursion.blacktide.compiler.source-jvm :as source-jvm] + [com.tailrecursion.blacktide.compiler.source-languages :as source-languages] + [com.tailrecursion.blacktide.compiler.source-values :as source-values] + [com.tailrecursion.blacktide.compiler.storage :as storage] + [com.tailrecursion.blacktide.compiler.structure :as structure] + [com.tailrecursion.blacktide.compiler.structured-emit :as structured-emit] + [com.tailrecursion.blacktide.compiler.structured-languages :as structured-languages] + [com.tailrecursion.blacktide.compiler.transfers :as transfers] + [com.tailrecursion.blacktide.compiler.value-facts :as value-facts] + [com.tailrecursion.blacktide.compiler :as compiler])) + +;; Var.toSymbol delegates to Symbol.create; its successful result is non-null. +(ct/non-nil-return clojure.lang.Var/toSymbol :all) +;; Java reflection guarantees these successful results are non-null. +(ct/non-nil-return java.lang.Class/getName :all) +(ct/non-nil-return java.lang.Object/getClass :all) +(ct/non-nil-return java.lang.Class/getConstructors :all) +(ct/non-nil-return java.lang.Class/getMethods :all) +(ct/non-nil-return java.lang.reflect.Method/getName :all) +(ct/non-nil-return java.lang.reflect.Executable/getDeclaringClass :all) +(ct/non-nil-return java.lang.reflect.Executable/getParameterTypes :all) +(ct/non-nil-return java.lang.reflect.Constructor/getDeclaringClass :all) +(ct/non-nil-return java.lang.reflect.Method/getReturnType :all) +(ct/non-nil-return java.lang.reflect.Method/getDeclaringClass :all) +(ct/non-nil-return java.lang.reflect.Method/getParameterTypes :all) + +(np/ann-languages compact-languages/Compact) +(np/ann-languages jvm-languages/JVM) +(np/ann-languages languages/Machine languages/CFG languages/Symbolic) +(np/ann-languages analysis-languages/Analysis analysis-languages/Planned) +(np/ann-languages source-languages/Bound source-languages/Native source-languages/Classified + source-languages/Sequential source-languages/Assigned source-languages/Cases + source-languages/Collections source-languages/Prepared source-languages/Ordered) +(np/ann-languages structured-languages/Structured structured-languages/Joined structured-languages/Stored) +(np/ann-passes assignment/sequential-pair assignment/sequential-pairs assignment/sequential-forms + assignment/sequential-node assignment/sequential-binding assignment/sequential + assignment/parallel-forms assignment/parallel-bindings assignment/save-parallel-values + assignment/parallel-node assignment/parallel) +(np/ann-passes backend/compile-program) +(np/ann-passes boundaries/native-node boundaries/native-binding boundaries/native-forms boundaries/native + boundaries/classify-node boundaries/classify-pair boundaries/classify-pairs + boundaries/classify-binding boundaries/classify-forms boundaries/classify) +(np/ann-passes box-origins/copy-source box-origins/reboxable? box-origins/reboxable-values) +(np/ann-passes case-emit/selector-form case-emit/arms-form) +(np/ann-passes cases/normalize-node cases/normalize-binding cases/normalize-forms cases/normalize) +(np/ann-passes collections/form collections/normalize-forms collections/literal-values collections/constant + collections/normalize-node collections/normalize-binding collections/normalize) +(np/ann-passes compact/introduce compact/atom-reads compact/rhs-reads compact/reads compact/stable? + compact/substitute-atom compact/substitute-operand compact/substitute-rhs + compact/substitute-operation compact/substitute-binding compact/substitute compact/atom-type + compact/movable? compact/replacement compact/discardable? compact/simplify compact/program) +(np/ann-passes effects/effect-node effects/effects) +(np/ann-passes emit/atom-form emit/rhs-form emit/operation-form emit/edge-form emit/term-form emit/block-form + emit/program-form) +(np/ann-passes errors/site errors/fail errors/check-visibility) +(np/ann-passes feasible/terminator feasible/block feasible/program) +(np/ann-passes graph/atom-reads graph/rhs-reads graph/term-reads graph/successors graph/block-facts + graph/collect-facts graph/fact-at graph/validate-edges graph/reachable graph/predecessors + graph/initialized graph/validate-initialization graph/liveness graph/allocate graph/analyze) +(np/ann-passes host-emit/global-form host-emit/box-form host-emit/typed-form host-emit/conversion-form + host-emit/host-form) +(np/ann-passes invariants/assigned invariants/expression) +(np/ann-passes joins/postdominators joins/region joins/candidate joins/plan) +(np/ann-passes jvm/resolved-class jvm/source-tag jvm/tag-class jvm/fact jvm/value-type jvm/hint-conflict + jvm/visible-class jvm/literal-type jvm/join-type jvm/methods jvm/static-method? jvm/return-type + jvm/constructors jvm/parameters jvm/select-method jvm/public-member jvm/host-type + jvm/prepare-call) +(np/ann-passes loop-facts/children loop-facts/primitive loop-facts/inferred loop-facts/converted + loop-facts/boxed loop-facts/initializer loop-facts/bindings loop-facts/expression + loop-facts/recurs loop-facts/compatible? loop-facts/solve loop-facts/recur-error + loop-facts/recurrence loop-facts/materialize loop-facts/lower) +(np/ann-passes lower/fresh-block lower/start lower/seal lower/evaluate lower/lower-forms lower/lower-bindings + lower/lower-node lower/lower lower/compile-graph) +(np/ann-passes native/hinted-form native/reads-node native/boxed-reads native/reads native/literal-values + native/bindings-form native/node-form native/form) +(np/ann-passes order/save order/operands order/normalize-forms order/normalize-bindings order/normalize-node + order/normalize) +(np/ann-passes prepare/prepare-forms prepare/conversion prepare/prepare-node prepare/prepare-binding + prepare/prepare) +(np/ann-passes source/fresh source/expression source/resolve-name source/parse-symbol source/parse-operand + source/parse-forms source/parse-body source/bind-local source/parse-bindings source/parse-scope + source/parse-loop source/parse-recur source/parse-arms source/case-expression source/parse-case + source/parse-dispatch source/protocol-argument source/protocol-call source/keyword-call + source/parse-call source/parse-host source/parse-assignment source/parse-marker + source/parse-list source/parse-collection source/parse) +(np/ann-passes source-case/expand-case source-case/int32 source-case/dispatch-syntax) +(np/ann-passes source-host/tags source-host/qualified source-host/syntax) +(np/ann-passes source-jvm/signature source-jvm/primitive-interface source-jvm/direct-call + source-jvm/protocol-class source-jvm/protocol-call source-jvm/infer source-jvm/binding-facts + source-jvm/caller-facts) +(np/ann-passes source-values/node source-values/binding-form source-values/pair source-values/pairs + source-values/forms source-values/expression) +(np/ann-passes storage/loop-slots storage/cyclic? storage/operand-boxes storage/operation-boxes + storage/observed-values storage/term-boxes storage/select-storage) +(np/ann-passes structure/block structure/scan structure/harmless-atom? structure/harmless-block? + structure/wrap structure/arms structure/terminator structure/joined structure/body + structure/edge structure/construction-site? structure/site-failure structure/recover) +(np/ann-passes structured-emit/primitive-form structured-emit/binding-form structured-emit/atom-form + structured-emit/operand-form structured-emit/rhs-form structured-emit/rhs-type + structured-emit/operation-form structured-emit/let-form structured-emit/expression) +(np/ann-passes transfers/resolve-term transfers/resolve-block transfers/resolve-program) +(np/ann-passes value-facts/primitive value-facts/join value-facts/atom-type value-facts/rhs-type + value-facts/definitions value-facts/solve value-facts/analyze) +(np/ann-passes compiler/compile-tree compiler/caller-locals compiler/compile-form) diff --git a/test/com/tailrecursion/blacktide/aot_fixture.clj b/test/com/tailrecursion/blacktide/aot_fixture.clj new file mode 100644 index 0000000..0ed7f27 --- /dev/null +++ b/test/com/tailrecursion/blacktide/aot_fixture.clj @@ -0,0 +1,23 @@ +(ns com.tailrecursion.blacktide.aot-fixture + "Fresh AOT client exercising both compiler loading and generated code." + (:require [com.tailrecursion.blacktide :refer [loophole setq label goto]])) + +(defn keyword-read [target] + (loophole [saved nil] (:k (setq saved target)))) + +(defn sample [n] + (loophole [^long i 0 total 0] + (while (< i n) + (setq total (case i 0 3 + (+ total 2)) + i (inc i))) + total)) + +(defn reference-site [flag value] + (let [result (loophole [a 0 b 0] + (if flag (setq a 1 b 2) (setq a 3 b 4)) + (label again) + (when (neg? a) (goto again)) + (+ a b) + value)] + result)) diff --git a/test/com/tailrecursion/blacktide/aot_test.clj b/test/com/tailrecursion/blacktide/aot_test.clj new file mode 100644 index 0000000..445a334 --- /dev/null +++ b/test/com/tailrecursion/blacktide/aot_test.clj @@ -0,0 +1,42 @@ +(ns com.tailrecursion.blacktide.aot-test + "AOT in a fresh JVM must compile dependencies and run without source files or + the type checker. Preloading compiler namespaces would hide initializer limits." + (:require [clojure.test :refer [deftest is]] + [clojure.java.io :as io] + [clojure.string :as str])) + +(defn subprocess [classpath form] + (let [java (str (io/file (System/getProperty "java.home") "bin" "java")) + process (-> (ProcessBuilder. ^java.util.List [java "-Xmx768m" "-cp" classpath "clojure.main" "-e" (pr-str form)]) + (.redirectErrorStream true) (.start)) + output (slurp (.getInputStream process))] + {:exit (.waitFor process) :output output})) + +(deftest fresh-aot-and-source-free-use + (let [directory (.toFile (java.nio.file.Files/createTempDirectory "loophole-aot-" (make-array java.nio.file.attribute.FileAttribute 0))) + classpath (System/getProperty "java.class.path") + dependencies (filter #(str/ends-with? % ".jar") (str/split classpath (re-pattern java.io.File/pathSeparator))) + compiled (subprocess classpath `(binding [*compile-path* ~(.getPath directory)] + (compile 'com.tailrecursion.blacktide.aot-fixture)))] + (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))) + (assert (= 7 (com.tailrecursion.blacktide.aot-fixture/keyword-read {:k 7}))) + (let [value (Object.)] + (doseq [flag [true false]] + (assert (identical? value (com.tailrecursion.blacktide.aot-fixture/reference-site flag value))))) + (assert (= 9 (eval '(com.tailrecursion.blacktide/loophole [x 1] + (com.tailrecursion.blacktide/setq x 9) x)))) + (assert (= 11 (eval '(com.tailrecursion.blacktide/loophole [x nil] + (:k (com.tailrecursion.blacktide/setq x {:k 11})))))) + (println "AOT client and source-free macro expansion passed")))] + (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_test.clj b/test/com/tailrecursion/blacktide/assignment_test.clj new file mode 100644 index 0000000..b40ee92 --- /dev/null +++ b/test/com/tailrecursion/blacktide/assignment_test.clj @@ -0,0 +1,107 @@ +(ns com.tailrecursion.blacktide.assignment-test + "Assignment semantics, saved operands, and independent interpreter comparisons." + (:require [clojure.test :refer [deftest is]] + [com.tailrecursion.blacktide :as lh :refer [loophole setq psetq]] + [com.tailrecursion.blacktide.source-test :as source] + [com.tailrecursion.nanopass.assignment-test :as oracle])) + +(defn add [a b] (+ a b)) +(defn event! [events tag value] (swap! events conj [tag value]) value) + +(deftest batch-results-and-repeated-targets + (is (= [nil nil] (loophole [] [(setq) (psetq)]))) + (is (= [2 1] (loophole [x 1 y 2] (psetq x y y x) [x y]))) + (is (= [3 3 3] (loophole [x 1 y 2] [(setq x 3 y x) x y]))) + (is (= [nil 2 2] (loophole [x 1 y 2] [(psetq x 9 x y) x y]))) + (is (= [2 9] (loophole [x 1 y 2] (psetq x y y (setq x 9)) [x y]))) + (is (= [1 2] (loophole [x 1 y 2] (psetq x (do (psetq x y y x) y) y x) [x y]))) + (is (= "changed" (loophole [x 1] (setq x 2.5 x "changed") x))) + (is (= [1 10 10 10] (loophole [x 1] (let [a x b (setq x 10) c x] [a b c x]))))) + +(deftest operand-and-assignment-snapshots + (is (= 30 (loophole [x 0] (add (do (setq x 10) x) (do (setq x 20) x))))) + (is (= [1 10 10 20] (loophole [x 1] [x (setq x 10) x (setq x 20)]))) + (is (= [3 9] (loophole [x 1] [(add x (setq x 2)) (setq x 9)]))) + (is (= 7 (loophole [f add] (f (do (setq f vector) 3) 4)))) + (is (= [nil 1 8] (loophole [x 1 y 2] [(psetq x (setq y 8) y x) y x])))) + +(deftest writes-in-collections-and-metadata + (is (= #{1 2} (loophole [x 0] #{(setq x 1) (setq x 2)}))) + (is (= [{1 2} 2] (loophole [x 1] [{x (setq x 2)} x]))) + (is (= [[1] 9 {:note 9}] + (loophole [x 1] (let [v ^{:note (setq x 9)} [x]] [v x (meta v)]))))) + +(deftest initialization-and-abandonment + (is (= [1 9 9] (loophole [x 1 before x y (setq x 9)] [before x y]))) + (let [events (atom []) fail (fn [x] (event! events :before-throw x) (throw (ex-info "stop" {})))] + (is (thrown-with-msg? clojure.lang.ExceptionInfo #"stop" + (loophole [x 1 y 2] (psetq x (event! events :first 10) y (fail x))))) + (is (= [[:first 10] [:before-throw 1]] @events)) + (reset! events []) + (is (thrown-with-msg? clojure.lang.ExceptionInfo #"stop" + (loophole [x 1 y 2] + (psetq x (event! events :first 10) + y (do (setq x 9) (fail x)))))) + (is (= [[:first 10] [:before-throw 9]] @events))) + (let [events (atom [])] + (is (= [nil 2 1] + (loophole [x 1 y 2] [(psetq x (event! events :first y) y (event! events :second x)) x y]))) + (is (= [[:first 2] [:second 1]] @events)))) + +(deftest assignment-diagnostics-and-shadowing + (doseq [[body kind] [['(setq x) :invalid-assignment] + ['(psetq x 1 y) :invalid-assignment] + ['(setq ns/x 1) :invalid-assignment] + ['(setq [x] 1) :invalid-assignment] + ['(psetq missing 1) :unknown-assignment] + ['(let [x 3] (setq x 4)) :immutable-assignment]]] + (is (= kind (:kind (source/rejection (list 'loophole '[x 1] body)))))) + (is (= [1 2] (loophole [setq vector] (setq 1 2)))) + (is (= [3] (loophole [psetq vector] (psetq 3)))) + (is (= :immutable-assignment (:kind (source/rejection '(let [outside 1] (loophole [] (setq outside 2))))))) + (is (= 2 (loophole [x 1] (lh/setq x 2))))) + +(defn render [node] + (case (:op node) + :const (list 'quote (:value node)) + :read (:binding node) + :add (list '+ (render (:left node)) (render (:right node))) + :do (list* 'do (map render (:forms node))) + :if (list 'if (render (:test node)) (render (:then node)) (render (:else node))) + :event (list 'event! 'events (:tag node) (render (:value node))) + (:setq :psetq) (list* (symbol (name (:op node))) + (mapcat (fn [[name rhs]] [name (render rhs)]) (:writes node))))) + +(defn execute [node x y] + (binding [*ns* (the-ns 'com.tailrecursion.blacktide.assignment-test)] + (try + (eval (list 'let ['events '(atom [])] + (list 'loophole ['x x 'y y] + (list 'let ['result (render node)] + {:status :return :value 'result :trace '(deref events) + :vars {(list 'quote 'x) 'x (list 'quote 'y) 'y}})))) + (catch Throwable e {:compiler-error (class e) :message (ex-message e)})))) + +(deftest generated-assignment-programs + ;; Reuse the library client's independently interpreted source semantics, not + ;; its lowering or output evaluator. Both the compiler and its IR are separate. + (doseq [seed [712381 314159]] + (let [random (java.util.Random. seed)] + (dotimes [trial 100] + (let [node (oracle/expression random 3)] + (doseq [[x y] [[0 0] [1 2] [-7 5] [10 -3]]] + (let [expected (oracle/execute node {'x x 'y y}) actual (execute node x y)] + (is (= expected actual) + (when (not= expected actual) + (pr-str {:seed seed :trial trial :input [x y] + :source (oracle/minimize node + #(let [candidate (execute % x y)] + (and (= (boolean (:compiler-error actual)) (boolean (:compiler-error candidate))) + (not= (oracle/execute % {'x x 'y y}) candidate))))})))))))))) + +(deftest invocation-arity-boundaries-preserve-operator-and-argument-snapshots + (doseq [n [19 20 21 40]] + (let [args (concat (map #(list 'setq 'x %) (range 1 (inc n))) ['(do (setq f list) x) 'x]) + form (list 'loophole '[f vector x 0] [(cons 'f args) '(f :after) 'x]) + expected [(vec (concat (range 1 (inc n)) [n n])) '(:after) n]] + (is (= expected (source/run-form form)) (pr-str {:arity (+ n 2) :form form}))))) diff --git a/test/com/tailrecursion/blacktide/backend_test.clj b/test/com/tailrecursion/blacktide/backend_test.clj new file mode 100644 index 0000000..244271f --- /dev/null +++ b/test/com/tailrecursion/blacktide/backend_test.clj @@ -0,0 +1,311 @@ +(ns com.tailrecursion.blacktide.backend-test + "Backend decisions, primitive storage, reference identity and conversion order." + (:require [clojure.test :refer [deftest is]] + [com.tailrecursion.blacktide :refer [loophole label goto setq psetq]] + [com.tailrecursion.blacktide.compiler.backend :as backend] + [com.tailrecursion.blacktide.compiler.emit :as emit] + [com.tailrecursion.blacktide.compiler.structure :as structure] + [com.tailrecursion.blacktide.compiler.storage :as storage] + [com.tailrecursion.blacktide.compiler.compact :as compact] + [com.tailrecursion.blacktide.compiler.value-facts :as facts] + [com.tailrecursion.blacktide.compiler.box-origins :as origins] + [com.tailrecursion.blacktide.compiler.graph :as graph] + [com.tailrecursion.blacktide.compiler.jvm :as jvm] + [com.tailrecursion.blacktide.compiler.prepare :as prepare] + [com.tailrecursion.blacktide.compiler.lower :as lower] + [com.tailrecursion.blacktide.machine-test :as m])) + +(defn compile-program [form mode] + (binding [*ns* (the-ns 'com.tailrecursion.blacktide.backend-test)] + (with-redefs [backend/compile-program (if (= mode :dispatcher) + emit/program-form + backend/compile-program)] + (eval (list 'fn '[events] form))))) + +(defn selection [form] + (let [selected (atom nil) recover structure/recover] + (with-redefs [structure/recover (fn [analysis] (let [out (recover analysis)] (reset! selected out) out))] + (compile-program form :default)) + @selected)) + +(defn stored [form] + (let [selected (atom nil) choose storage/select-storage] + (with-redefs [storage/select-storage (fn [analysis expr] + (let [out (choose analysis expr)] (reset! selected out) out))] + (compile-program form :default)) + @selected)) + +(defn execution [form mode] + (let [f (compile-program form mode) events (atom [])] + (try {:value (f events) :trace @events} + (catch Throwable error {:throw (class error) :trace @events})))) + +(defn compacted [form] + (let [selected (atom nil) simplify compact/program] + (with-redefs [compact/program (fn [program] (let [out (simplify program)] (reset! selected out) out))] + (compile-program form :default)) + @selected)) + +(defmacro local-class [name] + (list 'quote (.getJavaClass ^clojure.lang.Compiler$LocalBinding (get &env name)))) + +(deftest result-type-boundary + (doseq [body '[1 2.5 nil false "text" (loop [] 1) (goto end)] + mode [:default :dispatcher]] + (let [form (list 'let ['result (list 'loophole [] body)] '(local-class result))] + (is (= Object ((compile-program form mode) nil)) (pr-str [body mode]))))) + +(deftest object-arguments-need-facts-and-snapshots-but-no-cast + (let [conversions (atom []) graphs (atom []) conversion prepare/conversion compile-graph lower/compile-graph] + (with-redefs [prepare/conversion + (fn [how expr] + (let [out (conversion how expr)] + (when (= Object (:type how)) (swap! conversions conj out)) out)) + lower/compile-graph (fn [supply expr] + (let [out (compile-graph supply expr)] (swap! graphs conj out) out))] + (doseq [mode [:default :dispatcher]] + (is (= {:value [false 30] :trace [10 20]} + (execution '(loophole [x 10] + [(java.util.Objects/equals (do (swap! events conj x) x) + (do (setq x 20) (swap! events conj x) (setq x 30))) x]) mode))) + (is (= :throw + (if (:throw (execution '(loophole [] + (^{:param-tags [Object Object]} clojure.lang.Numbers/minus + (long Long/MIN_VALUE) (long Long/MIN_VALUE))) mode)) :throw :returned))) + (is (.startsWith ^String (:value (execution '(loophole [] + (^{:param-tags [Object]} String/valueOf (char-array [\a \b]))) mode)) + "[C@")))) + (is (seq @conversions)) + (is (every? #(and (= Object (:type (:jvm %))) (false? (:primitive? (:jvm %)))) @conversions)) + (is (not-any? #(and (map? %) (= :convert (:op %)) (= Object (:type (:conversion %)))) + (mapcat #(tree-seq coll? seq %) @graphs))))) + +(deftest integer-conversion-overloads-are-not-interchangeable + (let [random (java.util.Random. 173205) + outcome (fn [f x] (try (let [v (f x)] [v (class v)]) + (catch Throwable e [(class e) (ex-message e)]))) + reference (fn [x] (int (identity x)))] + (doseq [[cast values] + [['long (concat [Long/MIN_VALUE (dec (long Integer/MIN_VALUE)) Integer/MIN_VALUE + -1 0 1 Integer/MAX_VALUE (inc (long Integer/MAX_VALUE)) Long/MAX_VALUE] + (repeatedly 64 #(.nextLong random)))] + ['double [Double/NaN Double/POSITIVE_INFINITY Double/NEGATIVE_INFINITY + -0.0 0.0 1.5 -1.5 2147483647.5 -2147483648.9 + (double Long/MIN_VALUE) (double Long/MAX_VALUE)]]] + mode [:default :dispatcher] + :let [form (list 'fn '[input] (list 'loophole ['x (list cast 'input)] '(int x))) + compiled ((compile-program form mode) (atom []))] + value values] + (let [value (if (= cast 'long) (long value) (double value))] + (is (= (outcome reference value) (outcome compiled value)) + (pr-str [173205 cast mode value])))))) + +(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] + '(while (< i 2) (setq i (inc i))) 'x)] + (is (= cached? (origins/reboxable? {:op :value :value (m/literal literal)}))) + (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. + (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] + (while (< i 2) (setq i (inc i))) x) mode)] + (is (identical? value (f value))))) + (doseq [mode [:default :dispatcher] + initializer '[(Long. 0) (Long. 128) 0 127 128]] + (let [form (list 'loophole ['i 0 'x initializer 'saved 'x] + '(while (< i 2) (setq x (inc x) i (inc i))) + '(do (reset! events saved) saved)) + f (compile-program form mode) events (atom nil) result (f events)] + (is (identical? result @events))))) + +(deftest primitive-proofs-follow-producers + (let [hint (jvm/fact false {:hint 'long :var-meta {}} Long/TYPE)] + (is (= :boxed (facts/rhs-type {} {:op :external :name 'x :jvm hint}))) + (is (= Long/TYPE (facts/rhs-type {} {:op :external :name 'x :jvm (assoc hint :primitive? true)}))) + (is (= :boxed (facts/atom-type {} (m/literal (Integer/valueOf 1000)))))) + (is (= {'x Long/TYPE 'y Long/TYPE} + (facts/solve {'x [{:op :value :value (m/literal 1)} {:op :value :value (m/local 'y)}] + 'y [{:op :value :value (m/local 'x)}]}))) + (is (= {'x :boxed 'y :boxed} + (facts/solve {'x [{:op :value :value (m/literal 1)} {:op :value :value (m/local 'y)}] + 'y [{:op :value :value (m/local 'x)} {:op :value :value (m/literal 2.0)}]})))) + +(deftest dead-blocks-are-retained-but-not-type-proofs + (let [machine (graph/analyze {:entry 0 :blocks {0 (m/block [(m/put-value 'x 1)] {:op :return :value (m/local 'x)}) + 1 (m/block [(m/put-value 'x nil)] {:op :return :value (m/local 'x)})}}) + analysis (facts/analyze machine)] + (is (= machine (:machine analysis))) + (is (= #{0} (:reachable analysis))) + (is (= {'x Long/TYPE} (:types analysis))))) + +(deftest feasible-paths-preserve-primitive-state + (let [form '(loophole [^long i 0 ^long total 0] + (while (< i 5) + (setq total (cond (= i 2) (unchecked-add total 10) + :else (unchecked-inc total)) + i (unchecked-inc i))) + (long total)) + program (stored form) + slots (storage/loop-slots (:body program))] + (is (= 2 (count slots))) + (is (= #{Long/TYPE} (set (vals (select-keys (:types program) slots))))) + (is (every? (:types program) slots)) + (doseq [mode [:default :dispatcher]] + (is (= {:value 14 :trace []} (execution form mode))))) + (doseq [[value expected] [[nil -3] [false -3] [true 3] [0 3] ["" 3] [:else 3] [[] 3]] + mode [:default :dispatcher]] + (let [form (list 'loophole '[i 0 x 0] + (list 'while '(< i 3) + (list 'if (list 'do '(swap! events conj :test) (list 'quote value)) + '(setq x (inc x)) '(setq x (dec x))) + '(setq i (inc i))) 'x)] + (is (= {:value expected :trace [:test :test :test]} (execution form mode)))))) + +(deftest structured-loops-and-fallbacks + (let [form '(loophole [i 0 a 0 b 1] + (label again) + (swap! events conj [i a b]) + (if (< i 6) + (do (psetq a b b (+ a b)) (setq i (inc i)) (goto again)) + [a b]))] + (is (= :structured (:op (selection form)))) + (doseq [mode [:default :dispatcher]] + (let [events (atom [])] + (is (= [8 13] ((compile-program form mode) events))) + (is (= [[0 0 1] [1 1 1] [2 1 2] [3 2 3] [4 3 5] [5 5 8] [6 8 13]] @events))))) + (is (= :cycles (:reason (selection '(loophole [i 0 j 0] + (while (< i 2) (setq i (inc i))) + (while (< j 3) (setq j (inc j))) [i j]))))) + (let [step '(if (odd? i) (setq x (inc x) y (inc y)) (setq x (+ x 2) y (+ y 2))) + form (list 'loophole '[i 0 x 0 y 0] + (list* 'while '(< i 3) (concat (repeat 8 step) ['(setq i (inc i))])) '[x y])] + (is (= :budget (:reason (selection form)))) + (is (= [40 40] ((compile-program form :default) (atom [])))))) + +(deftest structured-code-preserves-native-validation + (doseq [invalid '[(Math/abs 1 2 3) (String. 1 2 3 4 5)]] + (let [form (list 'loophole '[i 0] '(label again) '(setq i (inc i)) + '(when (< i 2) (goto again)) '(goto end) invalid)] + (doseq [mode [:default :dispatcher]] + (is (thrown? clojure.lang.Compiler$CompilerException (compile-program form mode)))))) + (let [form '(loophole [i 0] + (label again) (setq i (inc i)) (when (< i 2) (goto again)) + (goto end) (swap! events conj :dead)) + events (atom [])] + (is (= :validation (:reason (selection form)))) + (is (nil? ((compile-program form :default) events))) + (is (empty? @events)))) + +(deftest joins-share-one-continuation + (let [step '(if (odd? i) (setq hits (inc hits)) (setq hits (+ hits 2))) + form (list 'loophole '[i 0 hits 0] + (list* 'while '(< i 3) (concat (repeat 8 step) ['(setq i (inc i))])) 'hits) + decision (selection form)] + (is (= :structured (:op decision))) + (is (= 9 (count (filter #(and (map? %) (= :branch (:op %))) (tree-seq coll? seq (:body decision)))))) + (doseq [mode [:default :dispatcher]] (is (= 40 ((compile-program form mode) (atom [])))))) + (let [form (list 'loophole '[i 0 answer nil] + (list 'while '(< i 96) + (list 'setq 'answer (list* 'case 'i (concat (mapcat #(vector % %) (range 96)) [nil]))) + '(setq i (inc i))) 'answer)] + (is (= :structured (:op (selection form)))) + (doseq [mode [:default :dispatcher]] (is (= 95 ((compile-program form mode) (atom [])))))) + (doseq [mode [:default :dispatcher]] + (let [events (atom [])] + (is (= [false nil 7] + ((compile-program '(loophole [i 0 values []] + (while (< i 3) + (let [value (case i 0 (do (swap! events conj :false) false) + 1 (do (swap! events conj :nil) nil) + (do (swap! events conj :number) 7))] + (setq values (conj values value))) + (setq i (inc i))) values) mode) events))) + (is (= [:false :nil :number] @events))))) + +(defn primitive-target ^long [^long n] (+ n 1)) +(defn primitive-replacement ^long [^long n] (+ n 100)) + +(deftest primitive-conversions-effects-and-exceptions + (doseq [mode [:default :dispatcher]] + (is (= {:throw ArithmeticException :trace [:before]} + (execution '(loophole [^long i 1] + (while (< i 2) + (swap! events conj :before) + (+ Long/MAX_VALUE i) + (swap! events conj :after) + (setq i (inc i)))) mode))) + (is (= {:throw NullPointerException :trace []} + (execution '(loophole [^long i 0] + (while (< i 1) + (^{:param-tags [long long]} Math/max nil (do (swap! events conj :argument) 1)) + (setq i (inc i)))) mode))) + (let [f (compile-program '(loophole [^long i 0] + (while (< i 1) + (primitive-target (do (swap! events conj :argument) 5)) + (setq i (inc i)))) mode) + events (atom [])] + (with-redefs [primitive-target (fn [x] x)] + (is (thrown? ClassCastException (f events))) + (is (empty? @events)))) + (with-redefs [primitive-target primitive-target] + (is (= {:value 6 :trace []} + (execution '(loophole [^long i 0 result nil] + (while (< i 1) + (setq result (primitive-target + (do (alter-var-root #'primitive-target (constantly primitive-replacement)) 5)) + i (inc i))) result) mode)))))) + +(deftest object-conversions-preserve-references-dispatch-and-order + (doseq [mode [:default :dispatcher]] + (is (= {:throw ClassCastException :trace [:array]} + (execution '(loophole [^long i 0] + (while (< i 1) + (^{:param-tags [long/1 int long]} clojure.lang.RT/aset + (do (swap! events conj :array) (identity :wrong)) + (do (swap! events conj :index) 0) + (do (swap! events conj :value) 1)) + (setq i (inc i)))) mode))) + (is (= {:value true :trace []} + (execution '(loophole [i 0 x (inc (long 1000)) same true] + (while (< i 3) + (let [copy (java.util.Objects/requireNonNull x)] + (setq same (and same (identical? copy x)) i (inc i)))) same) mode))) + (is (= {:value [false false "null"] :trace [:left :right]} + (execution '(loophole [i 0 x nil result nil] + (while (< i 1) + (setq result (java.util.Objects/equals + (do (swap! events conj :left) x) + (do (setq x false) (swap! events conj :right) x)) + i (inc i))) + [result x (^{:param-tags [Object]} String/valueOf nil)]) mode))) + (is (= {:throw NullPointerException :trace [:message]} + (execution '(loophole [i 0] + (while (< i 1) + (^{:param-tags [Object String]} java.util.Objects/requireNonNull + nil (do (swap! events conj :message) "required")) + (setq i (inc i)))) mode))))) + +(deftest primitive-cast-cleanup-keeps-observable-boxes + (let [form '(loophole [x (long events)] + (< (^{:param-tags [long]} clojure.lang.RT/longCast x) 0)) + program (compacted form)] + (is (not-any? #(and (map? %) (= :host (:op %)) + (= clojure.lang.RT (:class (:operation %))) + (= 'longCast (:member (:operation %))) + (= [Long/TYPE] (:tags (:operation %)))) + (tree-seq coll? seq (:body program)))) + (doseq [mode [:default :dispatcher] input [Long/MIN_VALUE 1000 Long/MAX_VALUE]] + (is (= (neg? input) ((compile-program form mode) input))))) + (doseq [mode [:default :dispatcher]] + (is (false? ((compile-program '(loophole [] (let [x 1000] (identical? x x))) mode) nil))) + (is (true? ((compile-program '(loophole [x (long 1000) y nil] (psetq y x) (identical? x y)) mode) nil))))) diff --git a/test/com/tailrecursion/blacktide/case_test.clj b/test/com/tailrecursion/blacktide/case_test.clj new file mode 100644 index 0000000..bb77319 --- /dev/null +++ b/test/com/tailrecursion/blacktide/case_test.clj @@ -0,0 +1,225 @@ +(ns com.tailrecursion.blacktide.case-test + "Compare native case dispatch, then attack unique arms, lexical joins and + pending evaluation. Constants are deliberately not interpreted with ordinary =." + (:require [clojure.test :refer [deftest is]] + [clojure.walk :as walk] + [com.tailrecursion.blacktide :refer [loophole setq psetq label goto]] + [com.tailrecursion.blacktide.jvm-test :as jvm] + [com.tailrecursion.blacktide.source-test :as source])) + +(defn compile-function [form] + (binding [*ns* (the-ns 'com.tailrecursion.blacktide.case-test)] + (eval (list 'fn ['input] form)))) + +(defn result [f value] + (try {:value (f value)} (catch Throwable error {:error (class error) :message (ex-message error)}))) + +(deftest constants-match-native-dispatch + (let [tables ['[1 :integer] + '[:a :keyword nil :nil false :false true :true] + '["Aa" :aa "BB" :bb "AaAa" :aaaa "BBBB" :bbbb] + '[alpha :alpha beta :beta] + '[\a :character \newline :newline] + '[1N :bigint 1M :decimal 1/2 :ratio 4294967297 :large] + '[nil :nil [1 2] :vector {:a 1} :map #{1 2} :set] + '[1 :one :a :keyword "Aa" :string \a :character nil :nil false :false] + '[(1 2) :integers ("Aa" "BB") :strings (alpha beta) :symbols] + (vec (mapcat (fn [n] [n (* n n)]) (range 96)))] + values [nil false true -1 0 1 2 (int 1) (byte 1) 1N 1M 1.0 (float 1) 1/2 + 4294967297 95 96 :a :missing 'alpha 'beta 'String "Aa" "BB" "AaAa" "BBBB" "other" + \a \newline [1 2] '(1 2) {:a 1} #{1 2} String]] + (doseq [table tables default? [true false]] + (let [form (list* 'case 'input (cond-> table default? (conj :default))) + expected (compile-function form) actual (compile-function (list 'loophole [] form))] + (doseq [value values] + (is (= (result expected value) (result actual value)) (pr-str {:table table :default? default? :value value}))))))) + +(deftest dispatch-effects-and-branch-results + (is (= 10 (loophole [x 0] + (case (do (setq x (inc x)) x) 1 (setq x 10) 2 (setq x 20) (setq x 30)) x))) + (let [events (atom [])] + (is (= [1 30 40 40] + (loophole [x 0] + [(do (setq x 1) x) + (case (do (swap! events conj :dispatch) (setq x 2)) + 1 (do (swap! events conj :wrong) (setq x 10)) + 2 (do (swap! events conj :chosen) (+ (setq x 10) (setq x 20))) + (setq x 99)) + (setq x 40) x]))) + (is (= [:dispatch :chosen] @events))) + (is (= [9 nil 3] + (loophole [x 1 y 2] + [(case x 1 (setq x (case y 2 9 0)) 0) + (case x 9 (psetq x 3 y x) 0) x]))) + (is (= 0 (loophole [x 0] (case (setq x 1) :default) (setq x 0)))) + (is (= [:string :long :object :object] + (loophole [x 1] + [(.choose ^com.tailrecursion.blacktide.jvm_test.Choice jvm/chooser (case x 1 "x" "y")) + (.choose ^com.tailrecursion.blacktide.jvm_test.Choice jvm/chooser (case x 1 2 3)) + (.choose ^com.tailrecursion.blacktide.jvm_test.Choice jvm/chooser (case x 1 true false)) + (.choose ^com.tailrecursion.blacktide.jvm_test.Choice jvm/chooser (case x 1 2 nil))])))) + +(deftest case-jumps-do-not-fall-through + (is (= :done (loophole [] + (case :a (:a :b :c) (goto done) :wrong) + (throw (Exception. "fell through")) (label done) :done))) + (is (= [3 [0 1 2 3]] + (loophole [n 0 trace []] + (label again) + (case (do (setq trace (conj trace n)) n) + (0 1 2) (do (setq n (inc n)) (goto again)) + 3 :done) + [n trace])))) + +(deftest cases-and-original-loops + (is (= [3 6] (loophole [sum 0] + (loop [i 0] + (case i + (0 1 2) (do (setq sum (+ sum i)) (recur (inc i))) + 3 (do (setq sum (+ sum i)) [i sum])))))) + (doseq [form '[(loop [i 3] (case i 0 :done (recur (dec i)))) + (loop [i 1] (.choose ^com.tailrecursion.blacktide.jvm_test.Choice chooser (case i 1 i 0))) + (loop [i 1] (.choose ^com.tailrecursion.blacktide.jvm_test.Choice chooser (case i 1 "a" nil))) + (loop [i 0] (case i 0 (let [i 1] (case i 1 :yes :no)) :wrong))]] + (is (= (jvm/outcome form) (jvm/outcome (list 'loophole [] form))) (pr-str form))) + (is (= :done (loophole [] (loop [i 1] (case i 0 :done (do (setq) (psetq) (recur 0)))))))) + +(defmacro env-size [] (count &env)) +(defmacro dispatch-local [] (first (keys &env))) +(def expansions (atom [])) +(defmacro expanded [tag value] (swap! expansions conj tag) value) + +(deftest case-macro-environments-and-initialization + (reset! expansions []) + (is (= :group + (source/run-form + '(com.tailrecursion.blacktide/loophole [] + (case :a + (:a :b) (com.tailrecursion.blacktide.case-test/expanded :group :group) + "Aa" (com.tailrecursion.blacktide.case-test/expanded :aa :aa) + "BB" (com.tailrecursion.blacktide.case-test/expanded :bb :bb) + (com.tailrecursion.blacktide.case-test/expanded :default :default)))))) + (is (= [:group :aa :bb :default] @expansions)) + (let [body '(let [x 0] (case input 1 (env-size) :other))] + (is (= (result (compile-function body) 1) (result (compile-function (list 'loophole [] body)) 1)))) + (binding [*ns* (the-ns 'com.tailrecursion.blacktide.case-test)] + (is (= 1 (eval '(loophole [] (case 1 1 (dispatch-local)))))) + (let [form '(loophole [] (goto body) (case 1 1 (do (label body) (dispatch-local))))] + (is (= :marker-position + (try (eval form) nil + (catch Throwable error + (:kind (ex-data (loop [error error] (if-let [cause (ex-cause error)] (recur cause) error))))))))))) + +(deftest native-case-errors-and-source-validation + (doseq [form '[(case 1 1 :a 1 :b) + (case 1 (1 2) :a 2 :b) + (case 1 1 :a 1N :b) + (case 0 () :empty :other) + (case 0)]] + (is (= (:error (jvm/outcome form)) (:error (jvm/outcome (list 'loophole [] form)))) (pr-str form))) + ;; The public diagnostic contract distinguishes a missing dispatch expression + ;; from core case's constant/default errors. + (is (= :invalid-case (:kind (source/rejection '(loophole [] (case)))))) + (is (= :marker-position (:kind (source/rejection '(loophole [] (case 1 1 (label same) 2 (label same))))))) + (is (= :undefined-label (:kind (source/rejection '(loophole [] (case 1 1 :ok 2 (goto missing))))))) + (is (= :marker-position + (:kind (source/rejection '(loophole [] (goto inside) (+ 1 (case 0 0 (do (label inside) 2) 3))))))) + (is (= :invalid-recur (:kind (source/rejection '(loophole [] (loop [] (inc (case 0 0 (recur) 1))))))))) + +(deftest prepared-dispatch-and-expansion-equivalence + (doseq [body '[(case input 0 :zero 2 :two :other) + (case input "Aa" :aa "BB" :bb :other) + (case input (:a :b) :group nil :nil :other) + (case input [1 2] :sequential :other) + (case input 1N :one 1M :decimal :other)] + :let [expanded (binding [*ns* (the-ns 'com.tailrecursion.blacktide.case-test)] (macroexpand-1 body)) + expected (compile-function body) actual (compile-function (list 'loophole [] expanded))] + value [nil :a :b 0 1 1N 1M 2 "Aa" "BB" "missing" [1 2] '(1 2)]] + (is (= (result expected value) (result actual value)) (pr-str {:body body :input value}))) + (is (= [7 3] + (loophole [x 0] + [(+ (setq x 1) (case (setq x 2) 2 (setq x 3) 99) x) x]))) + (is (= [3 :text true] + (loophole [x nil out []] + (dotimes [i 3] + (case i 0 (setq x 3) 1 (setq x :text) (setq x true)) + (setq out (conj out x))) out))) + (doseq [body '[(case input 1 "x" (throw (Exception. "missing"))) + (case input 1 (long 2) (long 3)) + (case input 1 (boolean true) (boolean false)) + (case input 1 "x" nil) + (case input 1 nil "x")]] + (let [form (list 'let '[input 1] + (list '.choose (with-meta 'chooser {:tag 'com.tailrecursion.blacktide.jvm_test.Choice}) body))] + (is (= (jvm/outcome form) (jvm/outcome (list 'loophole [] form))))))) + +(defn command-model [{:keys [tokens a b weight width]}] + ;; Independent transitions on an input tape; no labels, assignment lowering, + ;; dispatch ordinals or production compiler representation participate. + (loop [i 0 m a n b out []] + (if (= i (count tokens)) [m n i out] + (let [token (nth tokens i) snapshot m i (inc i) + delta (cond (= 0 (mod m 3)) weight (= 1 (mod m 3)) (- weight) :else 0)] + (cond + (= :stop token) [m n i out] + (= :skip token) (recur i m n out) + :else + (let [[m n] (cond + (or (= :swap token) (= \s token)) [n m] + (= :add token) [(+ m delta) (+ n m delta)] + (= "Aa" token) [(+ m (reduce + 0 (map #(+ delta %) (range width)))) n] + (= "BB" token) [(if (even? n) (+ m delta) (- m delta)) m] + (nil? token) [(if (zero? snapshot) 7 snapshot) n] + :else [m (- n delta)])] + (recur i m n (conj out [i snapshot m n])))))))) + +(defn command-program [{:keys [tokens a b weight width]}] + `(loophole [~'m ~a ~'n ~b ~'i 0 ~'out []] + (label ~'scan) + (when (= ~'i ~(count tokens)) (goto ~'finished)) + (let [~'snapshot ~'m ~'token (nth ~tokens ~'i) + ~'delta (case (mod ~'snapshot 3) 0 ~weight 1 (- ~weight) 0)] + (setq ~'i (inc ~'i)) + (case ~'token + (:swap \s) (psetq ~'m ~'n ~'n ~'m) + :add (setq ~'m (+ ~'m ~'delta) ~'n (+ ~'n ~'m)) + "Aa" (loop [~'snapshot 0] + (when (< ~'snapshot ~width) + (setq ~'m (+ ~'m ~'delta ~'snapshot)) (recur (inc ~'snapshot)))) + "BB" (psetq ~'m (case (mod ~'n 2) 0 (+ ~'m ~'delta) (- ~'m ~'delta)) ~'n ~'m) + :stop (goto ~'finished) + :skip (goto ~'scan) + nil (setq ~'m (case ~'snapshot 0 7 ~'snapshot)) + (setq ~'n (- ~'n ~'delta))) + (setq ~'out (conj ~'out [~'i ~'snapshot ~'m ~'n]))) + (goto ~'scan) + (label ~'finished) [~'m ~'n ~'i ~'out])) + +(defn command-outcome [config wrap] + (try {:value (source/run-form (wrap (command-program config)))} + (catch Throwable error {:error (class error) :message (ex-message error)}))) + +(defn minimize-command [config wrap] + (let [failure (:error (command-outcome config wrap))] + (loop [config config] + (let [smaller (concat + (for [index (range (count (:tokens config)))] + (update config :tokens #(into (subvec % 0 index) (subvec % (inc index))))) + (for [key [:a :b :weight :width] value (distinct [0 (quot (get config key) 2)]) + :when (not= value (get config key))] (assoc config key value)))] + (if-let [candidate (first (filter #(let [actual (command-outcome % wrap)] + (and (= failure (:error actual)) (not= {:value (command-model %)} actual))) smaller))] + (recur candidate) config))))) + +(deftest generated-case-loop-jump-programs + (let [seed 173205 random (java.util.Random. seed) alphabet [:swap \s :add "Aa" "BB" :stop :skip nil :other]] + (dotimes [index 120] + (let [config {:tokens (vec (repeatedly (.nextInt random 25) #(nth alphabet (.nextInt random (count alphabet))))) + :a (- (.nextInt random 21) 10) :b (- (.nextInt random 21) 10) + :weight (- (.nextInt random 7) 3) :width (.nextInt random 6)} + expected {:value (command-model config)}] + (doseq [wrap [identity #(list 'do (walk/postwalk-replace '{m left n right scan again finished done snapshot saved} %))]] + (let [actual (command-outcome config wrap)] + (is (= expected actual) + (pr-str {:seed seed :index index :config config :expected expected :actual actual + :minimal (when-not (= expected actual) (command-program (minimize-command config wrap)))})))))))) diff --git a/test/com/tailrecursion/blacktide/check.clj b/test/com/tailrecursion/blacktide/check.clj new file mode 100644 index 0000000..9d3b0cf --- /dev/null +++ b/test/com/tailrecursion/blacktide/check.clj @@ -0,0 +1,86 @@ +(ns com.tailrecursion.blacktide.check + "Check production compiler pass bodies independently of the nanopass library." + (:require [clojure.test :refer [deftest is]] + [typed.clojure :as t] + [com.tailrecursion.nanopass.check :as probes] + [com.tailrecursion.blacktide.annotations])) + +(deftest compiler-passes + (is (t/check-ns-clj 'com.tailrecursion.blacktide.compiler.errors)) + (is (t/check-ns-clj 'com.tailrecursion.blacktide.compiler.feasible)) + (is (t/check-ns-clj 'com.tailrecursion.blacktide.compiler.value-facts)) + (is (t/check-ns-clj 'com.tailrecursion.blacktide.compiler.box-origins)) + (is (t/check-ns-clj 'com.tailrecursion.blacktide.compiler.invariants)) + (is (t/check-ns-clj 'com.tailrecursion.blacktide.compiler.structure)) + (is (t/check-ns-clj 'com.tailrecursion.blacktide.compiler.joins)) + (is (t/check-ns-clj 'com.tailrecursion.blacktide.compiler.storage)) + (is (t/check-ns-clj 'com.tailrecursion.blacktide.compiler.compact)) + (is (t/check-ns-clj 'com.tailrecursion.blacktide.compiler.structured-emit)) + (is (t/check-ns-clj 'com.tailrecursion.blacktide.compiler.backend)) + (is (t/check-ns-clj 'com.tailrecursion.blacktide.compiler.jvm)) + (is (t/check-ns-clj 'com.tailrecursion.blacktide.compiler.source-host)) + (is (t/check-ns-clj 'com.tailrecursion.blacktide.compiler.source-jvm)) + (is (t/check-ns-clj 'com.tailrecursion.blacktide.compiler.source-case)) + (is (t/check-ns-clj 'com.tailrecursion.blacktide.compiler.cases)) + (is (t/check-ns-clj 'com.tailrecursion.blacktide.compiler.case-emit)) + (is (t/check-ns-clj 'com.tailrecursion.blacktide.compiler.host-emit)) + (is (t/check-ns-clj 'com.tailrecursion.blacktide.compiler.emit)) + (is (t/check-ns-clj 'com.tailrecursion.blacktide.compiler.graph)) + (is (t/check-ns-clj 'com.tailrecursion.blacktide.compiler.source)) + (is (t/check-ns-clj 'com.tailrecursion.blacktide.compiler.loop-facts)) + (is (t/check-ns-clj 'com.tailrecursion.blacktide.compiler.effects)) + (is (t/check-ns-clj 'com.tailrecursion.blacktide.compiler.boundaries)) + (is (t/check-ns-clj 'com.tailrecursion.blacktide.compiler.native)) + (is (t/check-ns-clj 'com.tailrecursion.blacktide.compiler.assignment)) + (is (t/check-ns-clj 'com.tailrecursion.blacktide.compiler.source-values)) + (is (t/check-ns-clj 'com.tailrecursion.blacktide.compiler.collections)) + (is (t/check-ns-clj 'com.tailrecursion.blacktide.compiler.prepare)) + (is (t/check-ns-clj 'com.tailrecursion.blacktide.compiler.order)) + (is (t/check-ns-clj 'com.tailrecursion.blacktide.compiler.lower)) + (is (t/check-ns-clj 'com.tailrecursion.blacktide.compiler.transfers)) + (is (t/check-ns-clj 'com.tailrecursion.blacktide.compiler))) + +(deftest assignment-stage-exclusions + (let [expr (fn [node] {:site {:form nil :file nil :line nil :column nil} + :jvm {:hint nil :var-meta {} :type :unknown :primitive? false :hint-error nil} + :node node}) + batch {:op :psetq :pairs []} + assigned 'com.tailrecursion.blacktide.compiler.source-languages/Assigned-Expr] + (probes/accepts? 'com.tailrecursion.blacktide.compiler.source-languages/Bound-Node {:op :setq :pairs []} true) + (probes/accepts? 'com.tailrecursion.blacktide.compiler.source-languages/Sequential-Node {:op :setq :pairs []} false) + (probes/accepts? 'com.tailrecursion.blacktide.compiler.source-languages/Sequential-Node batch true) + (probes/accepts? assigned (expr batch) false) + (probes/accepts? assigned (expr {:op :do :forms [(expr batch)]}) false) + (probes/accepts? 'com.tailrecursion.blacktide.compiler.source-languages/Native-Expr + (expr {:op :do :forms [(expr batch)]}) false) + (probes/accepts? assigned (expr {:op :literal :value batch}) true))) + +(deftest native-and-recur-boundaries + (probes/accepts? 'com.tailrecursion.blacktide.compiler.source-languages/Native-Node {:op :setq :pairs []} false) + (probes/accepts? 'com.tailrecursion.blacktide.compiler.source-languages/Native-Node + {:op :goto :name 'end} false) + (probes/accepts? 'com.tailrecursion.blacktide.compiler.source-languages/Ordered-Node + {:op :recur :target {:id 'loop :bindings []} :args []} true) + (probes/accepts? 'com.tailrecursion.blacktide.compiler.source-languages/Ordered-Node + {:op :recur :target {:id 'loop :bindings []} :args [{:op :literal :value 1}]} false)) + +(deftest host-preparation-boundary + (let [operation {:op :method-value :name 'String/.length} + unprepared {:op :host :operation operation :args []} + prepared {:op :host-call :operation operation :args []}] + (probes/accepts? 'com.tailrecursion.blacktide.compiler.source-languages/Collections-Node unprepared true) + (probes/accepts? 'com.tailrecursion.blacktide.compiler.source-languages/Prepared-Node unprepared false) + (probes/accepts? 'com.tailrecursion.blacktide.compiler.source-languages/Prepared-Node prepared true) + (probes/accepts? 'com.tailrecursion.blacktide.compiler.source-languages/Ordered-Node + (assoc prepared :args [{:op :literal :value nil}]) false))) + +(deftest case-selection-boundary + (let [expr {:site {:form nil :file nil :line nil :column nil} + :jvm {:hint nil :var-meta {} :type Long/TYPE :primitive? true :hint-error nil} + :node {:op :literal :value 0}} + node {:op :case :selector expr :arms [expr]}] + (probes/accepts? 'com.tailrecursion.blacktide.compiler.source-languages/Assigned-Node node true) + (probes/accepts? 'com.tailrecursion.blacktide.compiler.source-languages/Cases-Node node false) + (probes/accepts? 'com.tailrecursion.blacktide.compiler.source-languages/Cases-Node (assoc node :op :select) true) + (probes/accepts? 'com.tailrecursion.blacktide.compiler.languages/CFG-Term + {:op :switch :test {:op :literal :value 0} :targets [1 2]} true))) diff --git a/test/com/tailrecursion/blacktide/collections_test.clj b/test/com/tailrecursion/blacktide/collections_test.clj new file mode 100644 index 0000000..67b4392 --- /dev/null +++ b/test/com/tailrecursion/blacktide/collections_test.clj @@ -0,0 +1,135 @@ +(ns com.tailrecursion.blacktide.collections-test + "Compare native collection construction, metadata, errors and observable order." + (:require [clojure.test :refer [deftest is]] + [com.tailrecursion.blacktide :refer [loophole]] + [com.tailrecursion.blacktide.compiler.backend :as backend] + [com.tailrecursion.blacktide.compiler.emit :as emit])) + +(defn record! [events tag value] (swap! events conj tag) value) +(defmacro key-a [] :a) +(defmacro key-b [] :a) + +(defn observe [form] + (binding [*ns* (the-ns 'com.tailrecursion.blacktide.collections-test)] + (eval (list 'let ['events '(atom []) + 'result (list 'try [:value form] + '(catch Throwable e [:error (class e) (ex-message e)]))] + ['result '(deref events)])))) + +(deftest collection-values-and-constant-identity + (doseq [body ['[] '{} '#{} '[1 2 :a] '{:a 1 :b 2} '#{1 :a} + '[1 {:a [2]} #{3}] '[nil false true 1N 2M 1/2]]] + (is (= (observe body) (observe (list 'loophole [] body))) (pr-str body))) + (let [plain (fn [] [1 {:a 2}]) compiled (fn [] (loophole [] [1 {:a 2}])) + plain-meta (fn [] ^{:note :yes} [1]) compiled-meta (fn [] (loophole [] ^{:note :yes} [1]))] + (is (identical? (plain) (plain))) + (is (identical? (compiled) (compiled))) + (is (not (identical? (plain-meta) (plain-meta)))) + (is (not (identical? (compiled-meta) (compiled-meta)))))) + +(deftest source-traversal-is-frozen + (let [entry (fn [i] [(list 'record! 'events (keyword (str "key" i)) i) + (list 'record! 'events (keyword (str "value" i)) (- i))]) + map-form (into {} (map entry (range 19))) + set-form (set (map first (map entry (range 19))))] + (doseq [body [map-form set-form (vec (map second (map entry (range 19))))]] + (is (= (observe body) (observe (list 'loophole [] body))))))) + +(deftest duplicate-errors-and-metadata-extent + (doseq [body [(array-map '(record! events :key1 :a) '(record! events :value1 1) + '(record! events :key2 :a) '(record! events :value2 2)) + #{'(record! events :first :a) '(record! events :second :a)}]] + (let [body (with-meta body {:note '(record! events :metadata :never)}) + expected (observe body) actual (observe (list 'loophole [] body))] + (is (= expected actual)) + (is (= :error (ffirst actual))) + (is (not (some #{:metadata} (second actual)))))) + (let [body (with-meta ['(record! events :value 1)] {:note '(record! events :metadata :done)}) + compiled (list 'loophole [] body)] + (is (= (observe body) (observe compiled))) + (is (= [:value :metadata] (second (observe compiled)))) + (is (= {:note :done} (meta (second (first (observe compiled))))))) + (let [quoted (list 'quote (with-meta [1] {:note '(unresolved-data)}))] + (is (= {:note '(unresolved-data)} (meta (second (first (observe (list 'loophole [] quoted))))))))) + +(deftest constant-duplicates-are-compile-errors + (doseq [body [(array-map '(key-a) 1 '(key-b) 2)] + form [body (list 'loophole [] body)]] + (let [error (try (observe form) nil (catch Throwable e (loop [e e] (if-let [cause (ex-cause e)] (recur cause) e))))] + (is (instance? IllegalArgumentException error)))) + ;; Native constant sets coalesce macro-expanded duplicates. Metadata prevents + ;; that constant folding, so the same duplicates then fail during construction. + (let [body #{'(key-a) '(key-b)}] + (is (= [[:value #{:a}] []] (observe body) (observe (list 'loophole [] body)))) + (let [body (with-meta body {})] + (is (= (observe body) (observe (list 'loophole [] body)))) + (is (= :error (ffirst (observe (list 'loophole [] body)))))))) + +(deftype TracedKey [id events] + Object + (hashCode [_] (swap! events conj [:hash id]) (int id)) + (equals [_ other] (and (instance? TracedKey other) (= id (.-id ^TracedKey other))))) + +(deftest construction-retains-key-hash-order + (doseq [body ['#{a b c} '{a 1 b 2 c 3}]] + (let [wrap (fn [body] (list 'let ['a '(TracedKey. 1 events) 'b '(TracedKey. 2 events) 'c '(TracedKey. 3 events)] body))] + (is (= (observe (wrap body)) (observe (wrap (list 'loophole [] body)))))))) + +(deftest construction-uses-native-operations + (let [f (fn [x] (loophole [] ^{:note :yes} [x {x x} #{x}])) + value (with-redefs [with-meta (constantly :wrong) vector (constantly :wrong) + hash-map (constantly :wrong) hash-set (constantly :wrong)] (f 3))] + (is (= [3 {3 3} #{3}] value)) + (is (= {:note :yes} (meta value))))) + +(deftest vector-construction-boundaries + (doseq [mode [:default :dispatcher]] + (binding [*ns* (the-ns 'com.tailrecursion.blacktide.collections-test)] + (with-redefs [backend/compile-program (if (= mode :dispatcher) + emit/program-form + backend/compile-program)] + (doseq [size [0 1 6 7 32 33 63] metadata [nil {} {:note :yes}]] + (let [body (with-meta (mapv #(list 'record! 'events % %) (range size)) metadata) + wrap identity + constant (with-meta (vec (range size)) metadata) + plain (eval (list 'fn [] (wrap constant))) + compiled (eval (list 'fn [] (list 'loophole [] (wrap constant)))) + observation (fn [f] [(class (f)) (meta (f))])] + (is (= [(observe (wrap body)) (observation plain)] + [(observe (list 'loophole [] (wrap body))) (observation compiled)]) + (pr-str [mode size metadata])))) + (doseq [size [1 6 7 32 33] + offset (distinct [0 (quot size 2) (dec size)])] + (let [body (assoc (mapv #(list 'record! 'events % %) (range size)) offset + '(throw (IllegalStateException. "stop")))] + (is (= (observe body) (observe (list 'loophole [] body))) (pr-str [mode size offset])))) + (doseq [size [1 6 7 32 33]] + (let [body (assoc (vec (range size)) 0 '(int 1000))] + (is (= (mapv class (second (first (observe body)))) + (mapv class (second (first (observe (list 'loophole [] body))))))))) + (doseq [size [1 6 7 33] value [1000 1.5 (Integer/valueOf 1000)]] + (let [body (with-meta (assoc (vec (range size)) 0 value) {}) + wrap identity + plain (eval (list 'fn [] (wrap body))) + compiled (eval (list 'fn [] (list 'loophole [] (wrap body)))) + observation (fn [f] [(class (first (f))) (first (f))])] + (is (= (observation plain) (observation compiled)) (pr-str [mode size value])))))))) + +(deftest deterministic-construction-differentials + (let [seed 314159 random (java.util.Random. seed) + value (fn [tag] (let [v (.nextInt random 8)] + (case (.nextInt random 3) + 0 v + 1 (list 'record! 'events tag v) + (list 'let ['x v] (list 'record! 'events tag 'x)))))] + (dotimes [trial 150] + (let [n (.nextInt random 13) + values (mapv #(value (keyword (str "item" %))) (range (* 2 n))) + body (case (.nextInt random 3) + 0 values + 1 (into {} (map vec (partition 2 values))) + (set values)) + body (if (.nextBoolean random) (with-meta body {:note '(record! events :metadata :ready)}) body) + wrap (fn [body] (list 'let ['out body] ['out '(meta out)]))] + (is (= (observe (wrap body)) (observe (wrap (list 'loophole [] body)))) + (str "seed " seed ", trial " trial ": " (pr-str body))))))) diff --git a/test/com/tailrecursion/blacktide/control_test.clj b/test/com/tailrecursion/blacktide/control_test.clj new file mode 100644 index 0000000..c6d0829 --- /dev/null +++ b/test/com/tailrecursion/blacktide/control_test.clj @@ -0,0 +1,80 @@ +(ns com.tailrecursion.blacktide.control-test + "Source control flow and live values across state-machine transitions." + (:require [clojure.test :refer [deftest is]] + [com.tailrecursion.blacktide :refer [loophole setq psetq]] + [com.tailrecursion.blacktide.source-test :as source] + [com.tailrecursion.blacktide.assignment-test :as assignment] + [com.tailrecursion.nanopass.assignment-test :as oracle])) + +(defn record! [events tag value] (swap! events conj tag) value) + +(deftest clojure-conditions-and-joins + (doseq [condition [nil false true 0 "" []]] + (is (= (if condition :yes :no) (loophole [] (if condition :yes :no))))) + (is (nil? (loophole [] (if false :unused)))) + (is (= 20 (loophole [x 0] (if (setq x false) (setq x 10) (setq x 20)) x))) + (doseq [condition [true false]] + (is (= (if condition [10 2] [1 "changed"]) + (loophole [x 1 y 2] (if condition (setq x 10) (setq y "changed")) [x y])))) + (is (= [3 4] (loophole [x 1] [(if true (if false 2 (setq x 3)) 9) (setq x 4)])))) + +(deftest operands-survive-partial-evaluation + (is (= [1 10 10 20] + (loophole [x 1] [x (if true (setq x 10) 0) x (if false 0 (setq x 20))]))) + (is (= [1 :changed 9] + (loophole [f vector x 1] + (f x (if true (do (setq f list x 9) :changed) :unused) x)))) + (is (= [nil 2 1] + (loophole [x 1 y 2] + [(psetq x (if true y 99) y (if false 99 x)) x y]))) + (is (= [1 3 3] + (loophole [x 1] + [(let [before x after (if true (setq x 3) 4)] before) x x])))) + +(deftest selected-effects-and-short-circuit-macros + (let [events (atom [])] + (is (= [:yes nil false 7] + (loophole [x 1] + [(if (record! events :test true) (record! events :yes :yes) (record! events :no :no)) + (and (setq x nil) (record! events :unused 4)) + (or (setq x false) (record! events :or false)) + (cond (setq x false) (record! events :unused 6) :else (record! events :cond 7))]))) + (is (= [:test :yes :or :cond] @events))) + (is (= [3 3] (loophole [x nil] (when-not x (setq x 3)) [(when x x) x]))) + (is (= [9 nil] (loophole [x 9] [(if-let [y x] y :absent) (when-let [y (setq x nil)] y)])))) + +(deftest throwing-branches-and-dead-source + (let [one (ex-info "one" {}) two (ex-info "two" {})] + (doseq [condition [true false]] + (is (identical? (if condition one two) + (try (loophole [] (if condition (throw one) (throw two))) (catch Throwable e e)))))) + (let [events (atom []) error (ex-info "stop" {})] + (is (identical? error + (try (loophole [x 1 y 2] + (psetq x (record! events :first 10) + y (if (record! events x true) (throw error) 9)) + (record! events :after :bad)) + (catch Throwable e e)))) + (is (= [:first 1] @events))) + (is (= :unresolved-symbol (:kind (source/rejection '(loophole [] (if true 1 missing)))))) + (is (= :unresolved-symbol (:kind (source/rejection '(loophole [] (throw nil) missing))))) + (doseq [form ['(if true) '(if true 1 2 3) '(throw) '(throw nil nil)]] + (is (= (if (= 'if (first form)) :invalid-if :invalid-throw) + (:kind (source/rejection (list 'loophole [] form))))))) + +(deftest generated-effectful-conditions + (let [seed 20261104 random (java.util.Random. seed)] + (dotimes [trial 100] + (let [test (case (.nextInt random 3) + 0 (oracle/batch :psetq 'x (oracle/read-var 'y) 'y (oracle/read-var 'x)) + 1 (oracle/batch :setq 'x (oracle/lit 0)) + (oracle/read-var 'x)) + node {:op :if :test (oracle/event :test test) + :then (oracle/expression random 2) :else (oracle/expression random 2)}] + (doseq [[x y] [[0 0] [1 2] [-7 5] [10 -3]]] + (let [expected (oracle/execute node {'x x 'y y}) actual (assignment/execute node x y)] + (is (= expected actual) + (when (not= expected actual) + (pr-str {:seed seed :trial trial :input [x y] + :source (oracle/minimize node + #(not= (oracle/execute % {'x x 'y y}) (assignment/execute % x y)))}))))))))) diff --git a/test/com/tailrecursion/blacktide/fixtures/HostFixture.java b/test/com/tailrecursion/blacktide/fixtures/HostFixture.java new file mode 100644 index 0000000..556ec2b --- /dev/null +++ b/test/com/tailrecursion/blacktide/fixtures/HostFixture.java @@ -0,0 +1,10 @@ +package com.tailrecursion.blacktide.fixtures; + +public final class HostFixture { + public static String clash = "field"; + public static String clash() { return "method"; } + public static String clash(String suffix) { return "method:" + suffix; } + public static String argumentOnly = "field"; + public static String argumentOnly(String suffix) { return suffix; } + public static String fieldOnly = "field"; +} diff --git a/test/com/tailrecursion/blacktide/generated_test.clj b/test/com/tailrecursion/blacktide/generated_test.clj new file mode 100644 index 0000000..a4a2756 --- /dev/null +++ b/test/com/tailrecursion/blacktide/generated_test.clj @@ -0,0 +1,145 @@ +(ns com.tailrecursion.blacktide.generated-test + "Bounded source generation with a separate state model, two backends, alpha + variants and validity-preserving shrinking. Coverage includes fallback code." + (:require [clojure.test :refer [deftest is]] + [clojure.walk :as walk] + [com.tailrecursion.blacktide :refer [loophole label goto setq psetq]] + [com.tailrecursion.blacktide.backend-test :as backend] + [com.tailrecursion.blacktide.compiler.structure :as structure] + [com.tailrecursion.blacktide.compiler.storage :as storage])) + +(defn three-steps [x k] (+ x (* 3 k))) + +(def operations [:parallel :if :case :snapshot :arguments :shadow :helper :loop :check :truth :cast]) + +(defn model-step [{:keys [x y] :as state} i index [op k]] + (if (and (= op :check) (= i k)) (reduced (assoc state :throw ArithmeticException)) + (let [[x y] (case op + :parallel [(+ y k) (- x k)] + :if [(if (< x y) (+ x k) (- x k)) y] + :case [(+ x (case (mod i 3) 0 k 1 (- k) (* 2 k))) y] + :snapshot [(+ x k) (+ y x)] + :arguments [x (+ (* 2 x) k)] + :shadow [x (+ y (* 2 k))] + :helper [(+ x (* 3 k)) y] + :loop [(+ x (* 2 k)) y] + :truth [(+ x (if (even? k) k (- k))) y] + :cast [(+ x k) y] + :check [x y])] + (assoc state :x x :y y :trace (conj (:trace state) [i index x y]))))) + +(defn model [{:keys [limit cut x y steps scalar?]}] + (loop [i 0 state {:x x :y y :trace []}] + (cond + (:throw state) (select-keys state [:throw :trace]) + (or (= i limit) (= i cut)) {:value (if scalar? (:x state) [(:x state) (:y state)]) :trace (:trace state)} + :else (recur (inc i) (reduce (fn [state [index step]] (model-step state i index step)) + state (map-indexed vector steps)))))) + +(defn observed [numeric? id] + ;; A numeric computation observes the number without observing the identity + ;; of its source box, so primitive storage can participate in this regime. + (if numeric? (list 'unchecked-add id 0) id)) + +(defn step-form [numeric? index [op k]] + `(do + ~(case op + :parallel `(psetq ~'x (+ ~'y ~k) ~'y (- ~'x ~k)) + :if `(if (< ~'x ~'y) (setq ~'x (+ ~'x ~k)) (setq ~'x (- ~'x ~k))) + :case `(setq ~'x (case (mod ~'i 3) 0 (+ ~'x ~k) 1 (- ~'x ~k) (+ ~'x (* 2 ~k)))) + :snapshot `(let [~'old ~'x] (setq ~'x (+ ~'x ~k) ~'y (+ ~'y ~'old))) + :arguments `(setq ~'y (+ (do (setq ~'x (+ ~'x ~k)) ~'x) + (do (setq ~'x (- ~'x ~k)) ~'x))) + :shadow `(let [~'x (+ ~'y ~k)] (setq ~'y (+ ~'x ~k))) + :helper `(setq ~'x (com.tailrecursion.blacktide.generated-test/three-steps ~'x ~k)) + :loop `(let [~'old ~'x] + (setq ~'x (loop [~'n 2 ~'v ~'old] + (if (zero? ~'n) ~'v (recur (dec ~'n) (+ ~'v ~k)))))) + :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)) + :check `(when (= ~'i ~k) (+ Long/MAX_VALUE 1))) + (swap! ~'events conj [~(observed numeric? 'i) ~index ~(observed numeric? 'x) ~(observed numeric? 'y)]))) + +(defn program [{:keys [limit cut x y steps numeric? scalar? hinted? native-result? literal-init?]} control renamed?] + (let [body (map-indexed (partial step-form numeric?) steps) + local #(if hinted? (with-meta % {:tag 'long}) %) + initial #(if (and numeric? (not literal-init?)) (list 'long %) %) + cut-form `(when (= ~'i ~cut) (goto ~'done)) + forms (case control + :linear (mapcat (fn [i] `((setq ~'i ~i) ~cut-form ~@body)) (range limit)) + :goto `((label ~'again) (when (>= ~'i ~limit) (goto ~'done)) ~cut-form + ~@body (setq ~'i (inc ~'i)) (goto ~'again)) + :recur `((loop [~'i 0] + (when (< ~'i ~limit) ~cut-form ~@body (recur (inc ~'i))))) + :nested `((while (< ~'i ~limit) ~cut-form + (dotimes [~'inner 1] ~@body) (setq ~'i (inc ~'i))))) + result (if scalar? 'x [(observed numeric? 'x) (observed numeric? 'y)]) + result (if native-result? `(loop [remaining# 1] + (if (zero? remaining#) ~result (recur (dec remaining#)))) result) + form `(loophole [~(local 'i) 0 ~(local 'x) ~(initial x) ~(local 'y) ~(initial y)] + ~@forms (label ~'done) ~result)] + (if renamed? + (let [renames '{i counter__17 x value__93 y value__94 again start__51 done finish__88}] + (list 'let ['unused :harmless] + (walk/postwalk #(if (symbol? %) (with-meta (get renames % %) (meta %)) %) form))) + form))) + +(defn outcome [config control renamed? mode] + (try (backend/execution (program config control renamed?) mode) + (catch Throwable error {:compile-error (class error) :message (ex-message error)}))) + +(defn minimize [config control renamed? mode] + (loop [config config] + (let [smaller (concat + (for [index (range (count (:steps config)))] + (update config :steps #(vec (concat (subvec % 0 index) (subvec % (inc index)))))) + (for [key [:limit :cut :x :y] value (distinct [0 (quot (get config key) 2)]) + :when (not= value (get config key))] (assoc config key value)) + (for [index (range (count (:steps config))) + :when (not (zero? (get-in config [:steps index 1])))] + (assoc-in config [:steps index 1] 0)))] + (if-let [next (first (filter #(not= (model %) (outcome % control renamed? mode)) smaller))] + (recur next) config)))) + +(deftest generated-backend-equivalence + (let [seed 618033 random (java.util.Random. seed) coverage (atom {}) recover structure/recover + choose storage/select-storage] + (with-redefs [structure/recover (fn [analysis] + (let [out (recover analysis)] + (when (and (= :structured (:op out)) + (empty? (second (structure/scan (:machine analysis) #{} [#{} #{}] + (:entry (:machine analysis)))))) + (swap! coverage update :acyclic (fnil inc 0))) + (swap! coverage update (or (:reason out) (:op out)) (fnil inc 0)) out)) + storage/select-storage (fn [analysis expr] + (let [out (choose analysis expr)] + (when (seq (select-keys (:types out) (storage/loop-slots (:body out)))) + (swap! coverage update :primitive-loops (fnil inc 0))) + (when (some #(and (map? %) (= :return (:op %)) + (contains? (:types out) (:id (:value %)))) + (tree-seq coll? seq (:body out))) + (swap! coverage update :primitive-returns (fnil inc 0))) + out))] + (dotimes [index 80] + (let [config {:numeric? (odd? index) :hinted? (= 1 (mod index 4)) :scalar? (zero? (mod index 3)) + :native-result? (zero? (mod index 5)) + :literal-init? (= 3 (mod index 4)) + :limit (.nextInt random 7) :cut (.nextInt random 9) + :x (- (.nextInt random 31) 15) :y (- (.nextInt random 31) 15) + :steps (vec (repeatedly (inc (.nextInt random 5)) + #(vector (nth operations (.nextInt random (count operations))) + (- (.nextInt random 9) 4))))} + expected (model config)] + (doseq [control [:linear :goto :recur :nested] renamed? [false true] mode [:default :dispatcher]] + (let [actual (outcome config control renamed? mode)] + (is (= expected actual) + (when-not (= expected actual) + (pr-str {:seed seed :index index :control control :mode mode :expected expected :actual actual + :minimal (program (minimize config control renamed? mode) control renamed?)})))))))) + (is (pos? (get @coverage :structured 0)) (str @coverage)) + (is (pos? (get @coverage :acyclic 0)) (str @coverage)) + (is (pos? (get @coverage :cycles 0)) (str @coverage)) + (is (pos? (get @coverage :primitive-loops 0)) (str @coverage)) + (is (pos? (get @coverage :primitive-returns 0)) (str @coverage)) + (println "Control generator: seed=" seed "source variants=640 executions=1280 coverage=" @coverage))) diff --git a/test/com/tailrecursion/blacktide/graph_test.clj b/test/com/tailrecursion/blacktide/graph_test.clj new file mode 100644 index 0000000..afcaf56 --- /dev/null +++ b/test/com/tailrecursion/blacktide/graph_test.clj @@ -0,0 +1,109 @@ +(ns com.tailrecursion.blacktide.graph-test + "Graph invariants and executable allocation, independent of source lowering." + (:require [clojure.test :refer [deftest is]] + [com.tailrecursion.blacktide.compiler.graph :as graph] + [com.tailrecursion.blacktide.machine-test :as m])) + +(defn jump [target] {:op :jump :target target}) +(defn result [value] {:op :return :value value}) +(defn failure [program] + (try (graph/analyze program) nil (catch clojure.lang.ExceptionInfo e (ex-data e)))) + +(deftest validate-destinations-before-reachability + (is (= {:kind :invalid-graph :block 0} (failure {:entry 0 :blocks {}}))) + (is (= {:kind :invalid-graph :block 9 :target 10} + (failure {:entry 0 :blocks {0 (m/block [] (result (m/literal :ok))) + 9 (m/block [] (jump 10))}})))) + +(deftest initialization-is-path-sensitive + (doseq [program [{:entry 0 :blocks {0 (m/block [] (result (m/local 'x)))}} + {:entry 0 :blocks {0 (m/block [(m/assign 'x {:op :value :value (m/local 'x)})] + (result (m/local 'x)))}} + {:entry 0 :blocks {0 (m/block [] {:op :branch :test (m/literal true) :yes 1 :no 2}) + 1 (m/block [(m/put-value 'x 1)] (jump 3)) + 2 (m/block [] (jump 3)) + 3 (m/block [] (result (m/local 'x)))}}]] + (is (= :uninitialized-slot (:kind (failure program)))) + (is (= '#{x} (:slots (failure program))))) + ;; A definition after a read cannot initialize that read, even with a backedge. + (is (= :uninitialized-slot + (:kind (failure {:entry 0 :blocks {0 (m/block [(m/assign 'y {:op :value :value (m/local 'x)}) + (m/put-value 'x 1)] (jump 0))}}))))) + +(deftest sequential-facts-and-opaque-payloads + (is (= {:reads '#{x} :defs '#{x y} :successors []} + (graph/block-facts + (m/block [(m/assign 'y {:op :value :value (m/local 'x)}) + (m/put-value 'x 2)] (result (m/local 'y)))))) + (let [data {:op :local :id 'ghost} + machine (graph/analyze {:entry 0 :blocks {0 (m/block [(m/put-value 'x data)] + (result (m/local 'x))) + 9 (m/block [] (result (m/local 'ghost)))}})] + (is (= #{0 9} (set (keys (:blocks machine))))) + (is (= '[ghost] (:slots machine))) + (is (= data (m/run-machine machine))))) + +(deftest multiway-joins + (is (= :invalid-graph (:kind (failure {:entry 0 :blocks {0 (m/block [] {:op :switch :test (m/literal 0) :targets []})}})))) + (doseq [index (range 3)] + (let [program {:entry 0 :blocks {0 (m/block [] {:op :switch :test (m/literal index) :targets [1 2 3]}) + 1 (m/block [(m/put-value 'x :first)] (jump 4)) + 2 (m/block [(m/put-value 'x :middle)] (jump 4)) + 3 (m/block [(m/put-value 'x :last)] (jump 4)) + 4 (m/block [] (result (m/local 'x)))}}] + (is (= (nth [:first :middle :last] index) (m/run-machine (graph/analyze program)))) + (is (= :uninitialized-slot (:kind (failure (assoc-in program [:blocks 2 :ops] [])))))))) + +(deftest joins-and-live-value-survival + (doseq [test [true false]] + (let [machine (graph/analyze + {:entry 0 :blocks {0 (m/block [(m/put-value 'x 10) + (m/assign 'saved {:op :value :value (m/local 'x)})] + {:op :branch :test (m/literal test) :yes 1 :no 2}) + 1 (m/block [(m/put-value 'x "new")] (jump 3)) + 2 (m/block [(m/put-value 'x 2.5)] (jump 3)) + 3 (m/block [(m/assign 'f {:op :global :name 'clojure.core/vector}) + (m/call 'out 'f (m/local 'saved) (m/local 'x))] + (result (m/local 'out)))}})] + (is (= '[saved x] (:slots machine))) + (is (= '#{saved} (get (:live machine) 1))) + (is (= '#{saved x} (get (:live machine) 3))) + (is (= [10 (if test "new" 2.5)] (m/run-machine machine)))))) + +(defn step-value [value n] (if (odd? n) (+ value n) (- value n))) + +(defn counting-graph [initial n] + {:entry 0 + :blocks {0 (m/block [(m/put-value 'x initial) (m/put-value 'n n) + (m/assign 'step {:op :global :name 'com.tailrecursion.blacktide.graph-test/step-value}) + (m/assign 'dec {:op :global :name 'clojure.core/dec}) + (m/assign 'zero {:op :global :name 'clojure.core/zero?})] + (jump 1)) + 1 (m/block [(m/call 'done 'zero (m/local 'n))] + {:op :branch :test (m/local 'done) :yes 3 :no 2}) + 2 (m/block [(m/call 'x 'step (m/local 'x) (m/local 'n)) + (m/call 'n 'dec (m/local 'n))] (jump 1)) + 3 (m/block [] (result (m/local 'x)))}}) + +(defn rename-blocks [program] + (let [rename #(+ 100 (* 7 %))] + {:entry (rename (:entry program)) + :blocks (into {} (map (fn [[id block]] + [(rename id) + (update block :term + (fn [term] + (case (:op term) + :jump (update term :target rename) + :branch (-> term (update :yes rename) (update :no rename)) + term)))])) (:blocks program))})) + +(deftest deterministic-backedges-and-metamorphisms + (let [seed 8675309 random (java.util.Random. seed)] + (dotimes [trial 75] + (let [initial (- (.nextInt random 200) 100) n (.nextInt random 30) + expected (+ initial (reduce + 0 (map #(if (odd? %) % (- %)) (range 1 (inc n))))) + program (counting-graph initial n)] + (doseq [variant [program (rename-blocks program) + (assoc-in program [:blocks 99] (m/block [] (result (m/local 'uninitialized))))]] + (is (= expected (m/run-machine (graph/analyze variant))) + (str "seed " seed ", trial " trial ", input " [initial n]))))))) diff --git a/test/com/tailrecursion/blacktide/invocation_test.clj b/test/com/tailrecursion/blacktide/invocation_test.clj new file mode 100644 index 0000000..968c9e1 --- /dev/null +++ b/test/com/tailrecursion/blacktide/invocation_test.clj @@ -0,0 +1,142 @@ +(ns com.tailrecursion.blacktide.invocation-test + "Direct protocol calls select interface dispatch before falling back to the + method Var. Compare values, failures and evaluation traces with Clojure." + (:require [clojure.test :refer [deftest is]] + [clojure.core.protocols :as p] + [com.tailrecursion.blacktide.backend-test :as backend])) + +(deftype DirectReduction [events] + p/CollReduce + (coll-reduce [_ f] (swap! events conj :interface) (f :direct)) + (coll-reduce [_ f init] (swap! events conj :interface) (f init :direct))) + +(defn observe [form mode direct? target-kind replacement] + (binding [*compiler-options* {:direct-linking direct?}] + (let [events (atom []) f ((backend/compile-program form mode) nil) + target (case target-kind :direct (DirectReduction. events) :nil nil :vector [1 2]) + replacement (case replacement :function (fn [& xs] (swap! events conj :fallback) (vec (rest xs))) + :nil nil :invalid 42)] + (with-redefs [p/coll-reduce replacement] + (let [out (try {:value (f target events)} (catch Throwable e {:throw (class e)}))] + (assoc out :trace @events)))))) + +(defn record-result [events tag & _] (swap! events conj tag) tag) + +(defn wrapped [body location] + (case location + :region (list 'fn '[target events] (list 'loophole [] body)) + :ordinary (list 'fn '[target events] body))) + +(deftest direct-protocol-and-first-class-calls-stay-distinct + (doseq [body '[(clojure.core.protocols/coll-reduce target vector) + (clojure.core.protocols/coll-reduce target vector 7) + ((do clojure.core.protocols/coll-reduce) target vector 7) + (let [f clojure.core.protocols/coll-reduce] (f target vector 7))] + target [:direct :nil :vector] direct? [false true] + mode [:default :dispatcher]] + (is (= (observe (wrapped body :ordinary) :default direct? target :function) + (observe (wrapped body :region) mode direct? target :function)) + (pr-str [body target direct? mode])))) + +(deftest fallback-root-is-read-after-the-target-before-later-arguments + (let [body '(clojure.core.protocols/coll-reduce + (do (swap! events conj :target) + (alter-var-root #'clojure.core.protocols/coll-reduce + (constantly (partial com.tailrecursion.blacktide.invocation-test/record-result events :early))) target) + (do (swap! events conj :argument) + (alter-var-root #'clojure.core.protocols/coll-reduce + (constantly (partial com.tailrecursion.blacktide.invocation-test/record-result events :late))) vector) + (do (swap! events conj :initial) 7))] + (doseq [target [:direct :nil :vector] mode [:default :dispatcher]] + (is (= (observe (wrapped body :ordinary) :default false target :function) + (observe (wrapped body :region) mode false target :function)) + (pr-str [target mode]))))) + +(deftest invalid-fallback-roots-fail-after-arguments + (doseq [body '[(clojure.core.protocols/coll-reduce + (do (swap! events conj :target) target) + (do (swap! events conj :argument) vector) + (do (swap! events conj :initial) 7)) + (clojure.core.protocols/coll-reduce target vector + (do (swap! events conj :throw) (throw (ex-info "argument" {}))))] + target [:direct :nil] replacement [:nil :invalid] + mode [:default :dispatcher]] + (is (= (observe (wrapped body :ordinary) :default false target replacement) + (observe (wrapped body :region) mode false target replacement)) + (pr-str [body target replacement mode])))) + +(deftest protocol-operands-may-change-region-state + (let [body '(clojure.core.protocols/coll-reduce + (do (swap! events conj :target) target) + (do (swap! events conj :argument) vector) + (do (swap! events conj :initial) 7)) + source '(fn [target events] + (loophole [x nil] + (clojure.core.protocols/coll-reduce + (do (swap! events conj :target) (setq x target)) + (do (swap! events conj :argument) (setq x vector)) + (do (swap! events conj :initial) (setq x 7)))))] + (doseq [target [:direct :nil :vector] mode [:default :dispatcher]] + (is (= (observe (wrapped body :ordinary) :default false target :function) + (observe source mode false target :function)))))) + +(defmacro remove-protocol [value] + (alter-meta! #'p/coll-reduce dissoc :protocol) + value) + +(deftest protocol-facts-are-captured-after-arguments-before-following-forms + (let [metadata (meta #'p/coll-reduce) + observe (fn [body mode location] + (try (reset-meta! #'p/coll-reduce metadata) + (observe (wrapped body location) mode false :direct :function) + (finally (reset-meta! #'p/coll-reduce metadata))))] + (doseq [body '[(do (clojure.core.protocols/coll-reduce target vector 7) + (com.tailrecursion.blacktide.invocation-test/remove-protocol :done)) + (clojure.core.protocols/coll-reduce + (com.tailrecursion.blacktide.invocation-test/remove-protocol target) vector 7)] + mode [:default :dispatcher]] + (is (= (observe body :default :ordinary) (observe body mode :region)) + (pr-str [body mode]))))) + +(deftest invalid-direct-protocol-arity-is-rejected-at-compilation + (doseq [body '[(clojure.core.protocols/coll-reduce) + (clojure.core.protocols/coll-reduce target) + (clojure.core.protocols/coll-reduce target vector 7 8)] + location [:ordinary :region] mode [:default :dispatcher]] + (is (thrown? clojure.lang.Compiler$CompilerException + (backend/compile-program (wrapped body location) mode)) + (pr-str [body location mode])))) + +;; Existing Java interfaces can also be protocol dispatch targets. They expose +;; conversions that the Object-only interfaces generated by defprotocol cannot. +(def java-list {:on 'java.util.List :method-map {:list-set :set}}) +(defn list-set [& _] :fallback) +(alter-meta! #'list-set assoc :protocol #'java-list) +(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) + +(deftest interface-conversions-are-conditional-and-ordered + (let [observe (fn [body mode location direct?] + (let [events (atom []) f ((backend/compile-program (wrapped body location) mode) nil) + target (when direct? (java.util.ArrayList. [1 2]))] + [(try [:value (f target events)] (catch Throwable e [:throw (class e)])) + (when target (vec target)) @events]))] + (doseq [index [0 2147483648 nil] direct? [false true] + mode [:default :dispatcher] + :let [body (list 'com.tailrecursion.blacktide.invocation-test/list-set 'target + (list 'do '(swap! events conj :index) index) + '(do (swap! events conj :value) :new))]] + (is (= (observe body :default :ordinary direct?) (observe body mode :region direct?)) + (pr-str [index direct? mode]))))) + +(deftest interface-functional-arguments + (let [body '(com.tailrecursion.blacktide.invocation-test/map-compute target :key + (do (swap! events conj :argument) vector)) + observe (fn [mode location direct?] + (let [events (atom []) f ((backend/compile-program (wrapped body location) mode) nil) + target (when direct? (java.util.HashMap. {:key :old}))] + [(f target events) (when target (into {} target)) @events]))] + (doseq [direct? [false true] mode [:default :dispatcher]] + (is (= (observe :default :ordinary direct?) (observe mode :region direct?)) + (pr-str [direct? mode]))))) diff --git a/test/com/tailrecursion/blacktide/jumps_test.clj b/test/com/tailrecursion/blacktide/jumps_test.clj new file mode 100644 index 0000000..60e04c8 --- /dev/null +++ b/test/com/tailrecursion/blacktide/jumps_test.clj @@ -0,0 +1,116 @@ +(ns com.tailrecursion.blacktide.jumps-test + "Statement transfers, outer labels, scope reentry and bounded control graphs." + (:require [clojure.test :refer [deftest is]] + [com.tailrecursion.blacktide :refer [loophole label goto setq psetq]] + [com.tailrecursion.blacktide.source-test :as source])) + +(defn step [n] (dec n)) +(defn more? [n] (pos? n)) +(defn add [a b] (+' a b)) +(defn odd-value? [n] (odd? n)) +(defn record! [events tag value] (swap! events conj tag) value) + +(deftest basic-and-conditional-transfers + (is (= 1 (loophole [x 1] (goto done) (setq x 9) (label done) x))) + (is (nil? (loophole [] (goto end) :unreachable))) + (is (nil? (loophole [] (label first) (label last)))) + (is (= 0 (loophole [n 3] (label again) (setq n (step n)) (when (more? n) (goto again)) n)))) + +(deftest scope-reentry + (let [events (atom [])] + (is (= 1 (loophole [n 3 result nil] + (label again) + (let [x (record! events n n)] + (setq n (step n)) + (if (more? n) (goto again) (setq result x))) result))) + (is (= [3 2 1] @events)))) + +(deftest invalid-transfers-even-in-dead-source + (doseq [[body kind] [['(goto missing) :undefined-label] + ['(do (label x) (label x)) :duplicate-label] + ['(label end) :reserved-label] + ['(label :keyword) :invalid-marker] + ['(goto qualified/name) :invalid-marker] + ['(vector (label x)) :marker-position] + ['(let [] (label x)) :marker-position] + ['(if true (label x) nil) :marker-position] + ['(case 0 0 (label x) nil) :marker-position] + ['(vector (do (goto end) 1)) :marker-position] + ['(let [x (do (goto end) 1)] x) :marker-position] + ['(setq x (do (goto end) 1)) :marker-position] + ['(loophole [] 1) :unsupported-form]] + prefix [[] ['(goto end)]]] + (is (= kind (:kind (source/rejection (list* 'loophole '[x 0] (concat prefix [body]))))) + (pr-str [prefix body])))) + +(defn sum-through [input] + (loophole [n input total 0] + (label again) + (when-not (more? n) (goto done)) + (setq total (add total n) n (step n)) + (goto again) + (label done) total)) + +(defn fibonacci [input] + (loophole [n input a 0 b 1] + (label again) + (when-not (more? n) (goto done)) + (psetq a b b (add a b)) + (setq n (step n)) + (goto again) + (label done) a)) + +(defn reference-fibonacci [n] + (loop [n n a 0 b 1] (if (zero? n) a (recur (dec n) b (+' a b))))) + +(deftest bounded-algorithm-differentials + (let [seed 424242 random (java.util.Random. seed)] + (dotimes [trial 300] + (let [n (.nextInt random 50)] + (is (= (reduce +' 0 (range 1 (inc n))) (sum-through n)) (str "sum seed " seed ", trial " trial)) + (is (= (reference-fibonacci n) (fibonacci n)) (str "fibonacci seed " seed ", trial " trial))))) + (is (= 5000050000 (sum-through 100000)))) + +(defn reference-machine [table budget value] + (loop [state 0 budget budget value value trace []] + (if (zero? budget) [value trace] + (let [{:keys [delta yes no]} (nth table state) + next-value (+ value delta)] + (recur (if (odd? next-value) yes no) (dec budget) next-value (conj trace [state value])))))) + +(defn machine-form [table budget value labels wrapped?] + (let [blocks + (mapcat (fn [state {:keys [delta yes no]}] + (let [body (list 'let ['x (list 'record! 'events [state 'value] 'value)] + (list 'setq 'value (list 'add 'x delta) 'budget '(step budget)) + (list 'if '(more? budget) + (list 'if '(odd-value? value) (list 'goto (nth labels yes)) (list 'goto (nth labels no))) + '(goto done)))] + [(list 'label (nth labels state)) (if wrapped? (list 'do (list 'do body)) body)])) + (range) table)] + (list 'let ['events '(atom [])] + (list* 'loophole ['budget budget 'value value] + (concat ['(when-not (more? budget) (goto done)) (list 'goto (first labels))] + blocks ['(label done) ['value '(deref events)]]))))) + +(defn execute-machine [table budget value labels wrapped?] + (binding [*ns* (the-ns 'com.tailrecursion.blacktide.jumps-test)] + (try (eval (machine-form table budget value labels wrapped?)) + (catch Throwable e {:compiler-error (ex-message e)})))) + +(deftest generated-bounded-control-graphs + (let [seed 8675309 random (java.util.Random. seed)] + (dotimes [trial 60] + (let [table (vec (repeatedly 3 #(hash-map :delta (- (.nextInt random 9) 4) + :yes (.nextInt random 3) :no (.nextInt random 3)))) + budget (.nextInt random 14) value (- (.nextInt random 41) 20) + expected (reference-machine table budget value)] + (doseq [[labels wrapped?] [['[a b c] false] ['[renamed-a renamed-b renamed-c] true]]] + (let [actual (execute-machine table budget value labels wrapped?)] + (is (= expected actual) + (when (not= expected actual) + ;; Shrink the execution budget while retaining a mismatch. + (let [small (first (filter #(not= (reference-machine table % value) + (execute-machine table % value labels wrapped?)) (range (inc budget))))] + (pr-str {:seed seed :trial trial :table table :budget small :value value + :program (machine-form table small value labels wrapped?)})))))))))) diff --git a/test/com/tailrecursion/blacktide/jvm_fixture.clj b/test/com/tailrecursion/blacktide/jvm_fixture.clj new file mode 100644 index 0000000..d96a53e --- /dev/null +++ b/test/com/tailrecursion/blacktide/jvm_fixture.clj @@ -0,0 +1,18 @@ +(ns com.tailrecursion.blacktide.jvm-fixture + "Compile the small Java-only syntax fixture once into a temporary directory." + (:require [clojure.java.io :as io])) + +(def fixture-class + (let [directory (.toFile (java.nio.file.Files/createTempDirectory "loophole-jvm-fixture-" + (make-array java.nio.file.attribute.FileAttribute 0))) + source (io/file directory "HostFixture.java") + compiler (javax.tools.ToolProvider/getSystemJavaCompiler)] + (with-open [input (io/input-stream (io/resource "com/tailrecursion/blacktide/fixtures/HostFixture.java"))] + (io/copy input source)) + (when-not (zero? (.run compiler nil nil nil + (into-array String ["--release" "17" "-d" (.getPath directory) (.getPath source)]))) + (throw (ex-info "Java fixture compilation failed" {:directory directory}))) + (.defineClass ^clojure.lang.DynamicClassLoader (clojure.lang.RT/baseLoader) + "com.tailrecursion.blacktide.fixtures.HostFixture" + (java.nio.file.Files/readAllBytes + (.toPath (io/file directory "com/tailrecursion/blacktide/fixtures/HostFixture.class"))) nil))) diff --git a/test/com/tailrecursion/blacktide/jvm_test.clj b/test/com/tailrecursion/blacktide/jvm_test.clj new file mode 100644 index 0000000..8c6fba0 --- /dev/null +++ b/test/com/tailrecursion/blacktide/jvm_test.clj @@ -0,0 +1,233 @@ +(ns com.tailrecursion.blacktide.jvm-test + "Native Clojure is the dispatch oracle; event traces test receivers, pending + operands and assignment effects that result-only comparisons cannot observe." + (:require [clojure.test :refer [deftest is]] + [com.tailrecursion.blacktide.source-test :as source] + [com.tailrecursion.blacktide.jvm-fixture] + [com.tailrecursion.blacktide :refer [loophole setq psetq label goto]])) + +(definterface Choice + (choose [^boolean x]) (choose [^long x]) (choose [^int x]) (choose [^double x]) + (choose [^String x]) (choose [^chars x]) (choose [^Object x]) (choose [^Number x])) +(def chooser + (reify Choice + (choose [_ ^boolean x] :boolean) (choose [_ ^long x] :long) (choose [_ ^int x] :int) (choose [_ ^double x] :double) + (choose [_ ^String x] :string) (choose [_ ^chars x] :chars) + (choose [_ ^Object x] :object) (choose [_ ^Number x] :number))) +(defn ^String text [] "x") +(defn primitive ^long [] 1) +(def ^:dynamic *value* :root) +(defmacro .answer [] 42) +(defmacro constructed. [] 43) +(defmacro inferred-tag [sym] (list 'quote (:tag (get &env sym)))) + +(defn outcome [form] + (try + {:value (binding [*ns* (the-ns 'com.tailrecursion.blacktide.jvm-test)] + (eval (list (list 'fn [] form))))} + (catch Throwable error + {:error (class (loop [e error] (if-let [cause (ex-cause e)] (recur cause) e)))}))) + +(deftest overloads-preserve-source-types + (doseq [expr '[1 1.5 1N 1M 1/2 "x" nil false \a + (int 1) (long 1) (double 1) (char-array [\a]) + (String. "x") (.substring "xyz" 1) (instance? String "x") + (identity "x") (identity nil) (inc 1) (+ 1 2) + (if true nil "x") (if true "x" nil) (if true 1 nil) + (if true nil 1) (if true 1 2.0) (if true (int 1) (int 2)) + (let [a "x"] a) (text) (primitive) Integer/MAX_VALUE + (.-x (java.awt.Point. 1 2)) (String/valueOf 1) (do nil "x") ^Object (if true 1 2) ^Object (let [x 1] x)] + wrap [identity #(list 'if true % %) + #(list 'let ['value %] '(if true nil nil) 'value)]] + (let [arg (wrap expr) form (list '.choose (with-meta 'chooser {:tag 'Choice}) arg)] + (is (= (outcome form) (outcome (list 'loophole [] form))) (pr-str arg))))) + +(deftest host-syntax-and-typing + (is (= 3 (loophole [] (. "abc" (length))))) + (is (= 4 (loophole [] (.length (StringBuilder. "abcd")))))) + +(deftest source-tags-follow-native-metadata-rules + (doseq [tag [Object String Long/TYPE :ignored 42 false] + form [(list 'let [(with-meta 'x {:tag tag}) 1] 'x) + (list 'let ['x 1] (with-meta 'x {:tag tag})) + (list '.choose (with-meta 'chooser {:tag 'Choice}) (with-meta '(identity "x") {:tag tag})) + (list '.choose (with-meta 'chooser {:tag 'Choice}) (with-meta '(.length "x") {:tag tag}))]] + (is (= (outcome form) (outcome (list 'loophole [] form))) (pr-str [tag form]))) + (is (= :string (loophole [] (.choose ^Choice chooser (^String identity "x"))))) + (is (= :string (loophole [] (loop [] (.choose ^Choice chooser (text)))))) + (doseq [tag ["String" "java.lang.String"]] + (let [form (list 'let [(with-meta 'x {:tag tag}) '(identity "word")] '(.length x))] + (is (= {:value 4} (outcome form) (outcome (list 'loophole [] form)))))) + (let [form (list (with-meta 'Math/abs {:param-tags [Integer/TYPE]}) -2)] + (is (= {:value 2} (outcome form) (outcome (list 'loophole [] form)))))) + +(deftest caller-facts-and-qualified-operations + (is (= 42 (loophole [] (.answer)))) + (is (= 43 (loophole [] (constructed.)))) + (is (= "java.lang.String" (loophole [] (.getName String)))) + (let [values (java.util.ArrayList. [10 20 30]) index (int 1)] + (is (= [20 [10 30]] (loophole [] [(.remove values index) (vec values)])))) + (is (= 2 (loophole [x 0] (String/.length (String/new (do (setq x 1) "ab")))))) + (is (= 7 (loophole [] (^{:param-tags [String]} Integer/parseInt "7")))) + (is (= "7" (loophole [] (^{:param-tags [Object]} String/valueOf 7)))) + (is (= "ab" (loophole [] (^{:param-tags [String]} String/new "ab")))) + (is (= 3 (loophole [] (let [length String/.length] (length "abc"))))) + (is (= "13" (loophole [] (let [convert ^{:param-tags [Object]} String/valueOf] (convert 13))))) + (is (= Integer/MAX_VALUE (loophole [] (Integer/MAX_VALUE)))) + (is (= "3" (loophole [String nil] (. String valueOf 3)))) + (is (= (outcome '(let [nil-value nil] (.choose ^Choice chooser nil-value))) + (outcome '(let [nil-value nil] (loophole [] (.choose ^Choice chooser nil-value))))))) + +(deftest binding-hints-and-macro-environments + (is (= 'java.lang.String (loophole [] (let [s (text)] (inferred-tag s))))) + (is (= [true false] + (loophole [] + (let [^java.util.function.Predicate predicate odd?] + (if true nil nil) + [(.test predicate 1) (.test predicate 2)])))) + (doseq [form '[(let [^Object value 1] value) + (let [value 1] ^double value) + (let [^Object value (primitive)] value) + (.choose ^Choice chooser ^double (.length "abc")) + (let [value ^double (.length "abc")] (.choose ^Choice chooser value))]] + (is (= (outcome form) (outcome (list 'loophole [] form))) (pr-str form)))) + +(deftest inline-expansion-and-arithmetic + (is (= 30 (loophole [x 0] (+ (do (setq x 10) x) (do (setq x 20) x))))) + (is (= [1 2] (loophole [+ vector] (+ 1 2)))) + (doseq [unchecked [false true :warn-on-boxed] + form '[(+ Long/MAX_VALUE 1) (inc Long/MAX_VALUE) + (* Long/MAX_VALUE 2) (int Long/MAX_VALUE) + (let [x 4] (+ x (if true 2 3))) + (let [x (identity Long/MAX_VALUE)] (+ x 1))]] + (binding [*unchecked-math* unchecked] + (is (= (outcome form) (outcome (list 'loophole [] form))) (pr-str [unchecked form]))))) + +(defn numeric-expression [^java.util.Random random depth locals] + (let [pick #(.nextInt random %) child #(numeric-expression random (dec depth) locals)] + (if (zero? depth) + (if (and (seq locals) (zero? (pick 2))) (nth locals (pick (count locals))) + (nth [-13 -1 0 1 7 2.5 1/2] (pick 7))) + (case (pick 8) + 0 (list '+ (child) (child)) + 1 (list 'if (list 'neg? (child)) (child) (child)) + 2 (let [name (nth '[x y] (pick 2))] + (list 'let [name (child)] (numeric-expression random (dec depth) (conj locals name)))) + 3 (list '.length (list 'StringBuilder. (list 'str (child)))) + 4 (list 'Math/max (list 'double (child)) (list 'double (child))) + 5 (list 'aget (list 'long-array [(child) (child)]) (list 'int 1)) + 6 (list 'alength (list 'char-array (list 'str (child)))) + (list 'do (child) (child)))))) + +(defn minimize-host [form] + (loop [form form] + (if-let [smaller (first (filter #(and (not= form %) + (contains? (outcome %) :value) + (not= (outcome %) (outcome (list 'loophole [] %)))) + (source/smaller form)))] + (recur smaller) form))) + +(deftest generated-host-and-inline-expressions + (let [seed 112358 random (java.util.Random. seed)] + (dotimes [trial 180] + (let [form (numeric-expression random 3 []) expected (outcome form)] + (doseq [variant [form (list 'do form) (list 'let ['unused nil] form)]] + (let [actual (outcome (list 'loophole [] variant))] + (is (= expected actual) + (when (not= expected actual) + (pr-str {:seed seed :trial trial :form (minimize-host variant) + :expected expected :actual actual}))))))))) + +(defn failure-trace [form] + (binding [*ns* (the-ns 'com.tailrecursion.blacktide.jvm-test)] + (eval (list 'let ['events '(atom [])] + (list 'try form + '(catch Throwable error [(class error) @events])))))) + +(deftest conversions-happen-before-later-operands + (doseq [form '[(0 (do (swap! events conj :argument) 1)) + (String/.substring (do (swap! events conj :receiver) 7) + (do (swap! events conj :argument) 1)) + (^{:param-tags [long long]} Math/max + (do (swap! events conj :first) nil) + (do (swap! events conj :second) 1)) + (String. ^bytes (do (swap! events conj :first) "wrong") + (do (swap! events conj :second) (java.nio.charset.Charset/defaultCharset))) +]] + (is (= (failure-trace form) (failure-trace (list 'loophole [] form))) (pr-str form)))) + +(defn direct-fixed [x] [:fixed x]) +(defn direct-rest [x & xs] [x (vec xs)]) +(defn direct-primitive ^long [^long x] (inc x)) +(defn ^:redef redefinable [x] [:original x]) +(def ^:dynamic *direct-dynamic* identity) + +(deftest direct-linking-and-redefinitions + (doseq [direct? [false true] + form '[(direct-fixed 1) (direct-rest 1) (direct-rest 1 2 3) + (direct-primitive 4) (redefinable 5) (*direct-dynamic* 6)]] + (let [[native rewritten] + (binding [*ns* (the-ns 'com.tailrecursion.blacktide.jvm-test) + *compiler-options* {:direct-linking direct?}] + [(eval (list 'fn [] form)) (eval (list 'fn [] (list 'loophole [] form)))])] + (with-redefs [direct-fixed (constantly :changed) + direct-rest (constantly :changed) + direct-primitive (fn ^long [^long _] 99) + redefinable (constantly :changed) + *direct-dynamic* (constantly :changed)] + (is (= (native) (rewritten)) (pr-str [direct? form])))))) + +(definterface BoxChoice + (boxChoice [^Integer x ^String y]) (boxChoice [^Long x ^String y])) +(def box-chooser + (reify BoxChoice + (boxChoice [_ ^Integer x ^String y] :integer) + (boxChoice [_ ^Long x ^String y] :long))) +(defmacro integer-constant [] (Integer/valueOf 1000)) + +(deftest reflective-calls-preserve-boxed-operands + (is (= :integer + (.boxChoice ^BoxChoice box-chooser (integer-constant) (identity "s")) + (loophole [] (.boxChoice ^BoxChoice box-chooser (integer-constant) (identity "s")))))) + +(def ^:const constant-number 7) +(defmacro var-inline [value] (list #'inc value)) +(defmacro var-macro [value] (list #'when true value)) +(defmacro var-marker [value] (list #'setq 'x value)) + +(deftest constant-vars-and-var-operators + (is (= :long (loophole [] (.choose ^Choice chooser constant-number)))) + (is (= 8 (loophole [] (var-inline constant-number)))) + (is (= :body (loophole [] (var-macro :body)))) + (is (= 9 (loophole [x 0] (var-marker 9) x))) + (is (= [2 [3 4] [1 2 3 4]] + (loophole [] (let [[_ a & more :as all] [1 2 3 4]] [a more all])))) + (is (= [1 9] + (loophole [x 0] (let [{:keys [a] :or {a (setq x 9)}} {:a 1}] [a x]))))) + +(deftest overlapping-field-and-method-names + (doseq [form '[com.tailrecursion.blacktide.fixtures.HostFixture/clash + (com.tailrecursion.blacktide.fixtures.HostFixture/clash) + (com.tailrecursion.blacktide.fixtures.HostFixture/clash "x") + (.choose ^Choice chooser com.tailrecursion.blacktide.fixtures.HostFixture/clash) + + (com.tailrecursion.blacktide.fixtures.HostFixture/argumentOnly "x") + (.choose ^Choice chooser com.tailrecursion.blacktide.fixtures.HostFixture/argumentOnly) + ^{:param-tags []} com.tailrecursion.blacktide.fixtures.HostFixture/fieldOnly]] + (is (= (outcome form) (outcome (list 'loophole [] form))) (pr-str form)))) + +(deftest unreachable-source-still-receives-native-validation + (doseq [form '[(Math/abs 1 2 3) Math/MISSING (String. 1 2 3 4 5)] + wrap [identity #(list 'do %)]] + (let [body (wrap form) + expected (outcome body) + actual (outcome (list 'loophole [] '(goto end) body))] + (is (:error expected) (pr-str {:form body :native expected})) + (is (= (:error expected) (:error actual)) (pr-str {:form body :native expected :actual actual})))) + ;; Instance reflection reports a missing overload only when executed. + (is (nil? (loophole [] (goto end) (.substring "abc" 1 2 3)))) + (let [events (atom [])] + (is (nil? (loophole [] (goto end) + (swap! events conj :expression) + (identity (swap! events conj :after))))) + (is (empty? @events)))) diff --git a/test/com/tailrecursion/blacktide/lookup_test.clj b/test/com/tailrecursion/blacktide/lookup_test.clj new file mode 100644 index 0000000..f936066 --- /dev/null +++ b/test/com/tailrecursion/blacktide/lookup_test.clj @@ -0,0 +1,199 @@ +(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." + (:require [clojure.test :refer [deftest is]] + [com.tailrecursion.blacktide.backend-test :as backend])) + +(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))))) + +(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)) + +(def ^:const constant-key :k) +(defmacro lookup [target] `(:k ~target)) +(defmacro lookup-key [] :k) + +(defn trace [function] + (let [events (atom []) a (ObservedLookup. events {:k 7}) b (ObservedLookup. events {:k nil}) + c (UncachedLookup. events {:k 9}) + 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 + (doseq [body '[(:k (do (swap! events conj :target) target)) + (:missing target) + (:k (do (swap! events conj :target) target) (do (swap! events conj :default) 17)) + ((quote :k) target) + (let [key :k] (key target)) + ((do :k) target) + ((if true :k :other) target) + (com.tailrecursion.blacktide.lookup-test/constant-key target) + ((com.tailrecursion.blacktide.lookup-test/lookup-key) target) + (com.tailrecursion.blacktide.lookup-test/lookup target)] + wrap [identity #(list 'loop [] %)] + :let [form (list 'fn '[target events] (wrap body)) + reference ((backend/compile-program form :default) nil)] + mode [:default :dispatcher] + :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 + (let [reference '(fn [target events] + (loop [i 0 out []] + (if (< i 3) (recur (inc i) (conj out (:k target))) out))) + source '(fn [target events] + (loophole [i 0 out []] + (label again) + (setq out (conj out (:k target)) i (inc i)) + (when (< i 3) (goto again)) out))] + (doseq [mode [:default :dispatcher]] + (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 assigned-keyword-targets + (doseq [operator '[:k :missing (quote :k) (do :k) + (com.tailrecursion.blacktide.lookup-test/lookup-key) + com.tailrecursion.blacktide.lookup-test/constant-key + com.tailrecursion.blacktide.lookup-test/lookup] + mode [:default :dispatcher] + :let [target '(do (swap! events conj :target) target) + assigned '(do (swap! events conj :target) (setq saved target)) + reference (list 'fn '[target events] (list operator target)) + source (list 'fn '[target events] (list 'loophole '[saved nil] (list operator assigned)))]] + (is (= (trace ((backend/compile-program reference :default) nil)) + (trace ((backend/compile-program source mode) nil))) (pr-str [operator mode]))) + (doseq [mode [:default :dispatcher] + :let [reference '(fn [target events] [(:k target) true]) + source '(fn [target events] + (loophole [saved nil] + (let [result (:k (case (count (swap! events identity)) + 0 (setq saved target) + (do (psetq saved target) saved)))] + [result (identical? saved target)])))]] + (is (= (trace ((backend/compile-program reference :default) nil)) + (trace ((backend/compile-program source mode) nil))) (str mode)))) + +(deftest assigned-target-keeps-reentrant-snapshot + (let [reference '(fn read-key [target events] + (:k (do (swap! events conj :target) + (if (vector? target) + (do (read-key (first target) events) (second target)) target)))) + source '(fn read-key [target events] + (loophole [saved nil] + (:k (do (swap! events conj :target) + (setq saved (if (vector? target) + (do (read-key (first target) events) (second target)) target)))))) + run (fn [form mode] + (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))))) + +(deftest skipped-lookup-and-reentry + (let [reference '(fn [target events] + (loop [i 0 out []] + (if (< i 3) + (if (= i 1) (recur (inc i) out) + (recur (inc i) (conj out (:k (do (swap! events conj :target) target))))) out))) + source '(fn [target events] + (loophole [i 0 out [] saved nil] + (label again) + (when (= i 1) (setq i (inc i)) (goto again)) + (setq out (conj out (:k (do (swap! events conj :target) (setq saved target))))) + (setq i (inc i)) (when (< i 3) (goto again)) out))] + (doseq [mode [:default :dispatcher]] + (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] + (loophole [a 0 b 0 saved nil] + (if flag (setq a 1 b 2) (setq a 3 b 4)) + (label again) + (when (neg? a) (goto again)) + [(+ a b) (:k (setq saved target))])) + run (fn [form mode] + (let [f ((backend/compile-program form mode) nil) + events (atom []) target (ObservedLookup. events {:k 42})] + [(mapv #(f % target events) [true false false true]) @events]))] + (doseq [mode [:default :dispatcher]] + (is (= (run reference :default) (run source mode)) (str mode))))) diff --git a/test/com/tailrecursion/blacktide/loops_test.clj b/test/com/tailrecursion/blacktide/loops_test.clj new file mode 100644 index 0000000..812187e --- /dev/null +++ b/test/com/tailrecursion/blacktide/loops_test.clj @@ -0,0 +1,220 @@ +(ns com.tailrecursion.blacktide.loops-test + "Original recur targets, native loop inference, and bounded independent loop + models. Iteration budgets are structural, so shrinking cannot create hangs." + (:require [clojure.test :refer [deftest is]] + [com.tailrecursion.blacktide :refer [loophole label goto setq psetq]] + [com.tailrecursion.blacktide.source-test :as source] + [com.tailrecursion.blacktide.jvm-test :as jvm])) + +(deftest source-recursion-and-macro-loops + (is (= [2 1] (loophole [] (loop [n 3 a 1 b 2] (if (zero? n) [a b] (recur (dec n) b a)))))) + (is (= [2 1 3] (loophole [calls 0] + (loop [n 3 a 1 b 2] + (if (zero? n) [a b calls] + (do (setq calls (inc calls)) (recur (dec n) b a))))))) + (is (= 18 (loophole [total 0] + (dotimes [i 4] (dotimes [i i] (setq total (+ total 3)))) total))) + (is (= [0 2 4] (loophole [out []] + (doseq [i (range 6) :when (even? i) :let [i (* i 2)] :while (< i 10)] + (setq out (conj out (quot i 2)))) out))) + (is (= 5 (loophole [n 0] (while (< n 5) (setq n (inc n))) n))) + (is (= :zero (loophole [] (dotimes [i 0] (throw (Exception. "unreachable"))) :zero))) + (is (= 6 (loophole [n 0] (loop [] (setq n (inc n)) (when (< n 6) (recur))) n)))) + +(deftest recur-operands-and-abandonment + (let [events (atom [])] + (is (= [1 10 20 20] + (loophole [x 0] + (loop [n 0 a nil b nil] + (if (zero? n) + (recur (do (swap! events conj :first) 1) + (do (setq x 10) (swap! events conj x) x) + (do (setq x 20) (swap! events conj x) x)) + [n a b x]))))) + (is (= [:first 10 20] @events))) + (is (= [0 1 0 1] (loophole [round 0 out []] + (label again) + (loop [i 0] + (setq out (conj out i)) + (when (zero? i) (recur 1))) + (setq round (inc round)) + (when (< round 2) (goto again)) out))) + (is (= 8 (loophole [x 0] + (loop [n 4] + (when (pos? n) + (loop [m 2] (when (pos? m) (setq x (inc x)) (recur (dec m)))) + (recur (dec n)))) x)))) + +(deftest native-loop-inference + (doseq [form '[(loop [i 1] (.choose ^com.tailrecursion.blacktide.jvm_test.Choice chooser i)) + (loop [i (int 1)] (.choose ^com.tailrecursion.blacktide.jvm_test.Choice chooser i)) + (loop [i (float 1)] (.choose ^com.tailrecursion.blacktide.jvm_test.Choice chooser i)) + (loop [n 1 value 0] (if (pos? n) (recur (dec n) "string") value)) + (loop [n 1 value 0] (if (pos? n) (recur (dec n) 1.5) (class value))) + (loop [n 1 value 0] (if (pos? n) (recur (dec n) 1.5) + (.choose ^com.tailrecursion.blacktide.jvm_test.Choice chooser value))) + (loop [a (java.util.ArrayList. [1 2 3]) i (int 1)] (.remove a i) (vec a)) + (loop [x "abc"] (let [snapshot x] (String/.substring snapshot 1))) + (loop [n 0] (if (zero? n) (recur 1) ^{:value n} [n])) + (loop [f ^{:param-tags [int]} Math/abs] (f -2)) +] + lowered? [false true] + :let [body (if lowered? (list* (first form) (second form) '(setq touched true) (nnext form)) form)]] + (is (= (jvm/outcome form) (jvm/outcome (list 'loophole '[touched nil] body))) (pr-str [lowered? form]))) + (is (= :long (loophole [x 1] + (loop [n 2] (setq x n) + (loop [n 1] (.choose ^com.tailrecursion.blacktide.jvm_test.Choice jvm/chooser n)))))) + ;; DSL writes retain the same initializer promotions as ordinary Clojure. + (is (= (loop [i (int 1)] (class i)) (loophole [x nil] (loop [i (int 1)] (setq x true) (class i))))) + (is (= (loop [i (float 1)] (class i)) (loophole [x nil] (loop [i (float 1)] (setq x true) (class i)))))) + +(def ^String tagged-value "word") +(defmacro erase-tag [] (alter-meta! #'tagged-value assoc :tag 'Object) :retagged) +(def changing-value :old) +(defmacro make-constant [] (alter-meta! #'changing-value assoc :const true) nil) + +(deftest native-types-survive-later-macro-expansion + (let [body '[(loop [] (.choose ^com.tailrecursion.blacktide.jvm_test.Choice jvm/chooser tagged-value)) (erase-tag)] + evaluate (fn [wrap] + (alter-meta! #'tagged-value assoc :tag 'String) + (binding [*ns* (the-ns 'com.tailrecursion.blacktide.loops-test)] + (eval (list (list 'fn [] (wrap body))))))] + (try + (is (= [:string :retagged] (evaluate identity) (evaluate #(list 'loophole [] %)))) + (finally (alter-meta! #'tagged-value assoc :tag 'String)))) + (doseq [body ['[changing-value (make-constant)] '[(loop [] changing-value) (make-constant)]]] + (try + (let [evaluate (fn [wrap] + (alter-meta! #'changing-value dissoc :const) + (alter-var-root #'changing-value (constantly :old)) + (let [compiled (binding [*ns* (the-ns 'com.tailrecursion.blacktide.loops-test)] + (eval (list 'fn [] (wrap body))))] + (alter-var-root #'changing-value (constantly :new)) + (compiled)))] + (is (= [:new nil] (evaluate identity) (evaluate #(list 'loophole [] %))))) + (finally (alter-meta! #'changing-value dissoc :const) + (alter-var-root #'changing-value (constantly :old)))))) + +(deftest lowered-loop-interop-boundary + (doseq [body '[(.choose ^com.tailrecursion.blacktide.jvm_test.Choice com.tailrecursion.blacktide.jvm-test/chooser i) + (let [copy i] (.choose ^com.tailrecursion.blacktide.jvm_test.Choice com.tailrecursion.blacktide.jvm-test/chooser copy)) + (.choose ^com.tailrecursion.blacktide.jvm_test.Choice com.tailrecursion.blacktide.jvm-test/chooser (inc i)) + (loop [] (.choose ^com.tailrecursion.blacktide.jvm_test.Choice com.tailrecursion.blacktide.jvm-test/chooser i)) + (loop [copy i] (.choose ^com.tailrecursion.blacktide.jvm_test.Choice com.tailrecursion.blacktide.jvm-test/chooser copy))]] + (let [form (list 'loophole '[x 0] (list 'loop '[i 1] '(setq x 1) body))] + (is (= (jvm/outcome (list 'loop '[i 1] body)) (jvm/outcome form)) (pr-str form)))) + (is (= :int (loophole [x 0] (loop [i 1] (setq x 1) + (.choose ^com.tailrecursion.blacktide.jvm_test.Choice jvm/chooser (int i)))))) + (is (= :object (loophole [x 0] (loop [i (identity 1)] (setq x 1) + (.choose ^com.tailrecursion.blacktide.jvm_test.Choice jvm/chooser i))))) + (is (= :long (loophole [x 0] (loop [i 1] (setq x 1) + (^{:param-tags [long]} com.tailrecursion.blacktide.jvm_test.Choice/.choose jvm/chooser i)))))) + +(deftest recur-legality-and-loop-scopes + (doseq [[form kind] + [['(loophole [] (recur)) :invalid-recur] + ['(loophole [] (loop [n 0] (recur))) :recur-arity] + ['(loophole [] (loop [] (recur) 1)) :invalid-recur] + ['(loophole [] (loop [n 0] (if (recur 1) n n))) :invalid-recur] + ['(loophole [] (loop [] [(recur)])) :invalid-recur] + ['(loophole [] (loop [n (recur 0)] n)) :invalid-recur] + ['(loophole [] (loop [] (loophole [] (recur)))) :unsupported-form] + ['(loophole [] (loop [n 1] (setq n 2))) :immutable-assignment] + ['(loophole [x nil] (loop [n (byte 1)] (setq x n) (recur nil))) :invalid-recur-type] + ['(loophole [x nil] (loop [n 1] (setq x n) (recur (short n)))) :invalid-recur-type] + ['(loophole [] (goto body) (loop [] (label body))) :marker-position] + ['(loophole [] (loop [] (goto other)) (loop [] (label other))) :marker-position] + ['(loophole [] (goto end) (loop [] (recur 1))) :recur-arity]]] + (is (= kind (:kind (source/rejection form))) (pr-str form)))) + +(deftest illegal-primitive-recur-conversions + (doseq [cast '[byte short char]] + (let [body (list 'loop '[n 0] (list 'recur (list cast 1))) + lowered (list 'loop '[n 0] '(setq x n) (list 'recur (list cast 1)))] + (is (= IllegalArgumentException (:error (jvm/outcome body)))) + (is (= :invalid-recur-type + (:kind (source/rejection (list 'loophole '[x nil] lowered)))))))) + +(defn loop-model [{:keys [limit cut width a b weight]}] + (loop [i 0 a a b b history []] + (if (or (= i limit) (= i cut)) [a b history i] + (let [delta (reduce + weight (range width)) + next (if (even? i) [(+ b delta) (- a delta)] [(- b delta) (+ a delta)])] + (recur (inc i) (first next) (second next) (conj history [a b (inc i)])))))) + +(defn loop-program [{:keys [limit cut width a b weight]}] + `(loophole [~'ticks 0 ~'result nil] + (setq ~'result + (loop [~'i 0 ~'a ~a ~'b ~b ~'history []] + (if (= ~'i ~cut) [~'a ~'b ~'history ~'ticks] + (if (= ~'i ~limit) [~'a ~'b ~'history ~'ticks] + (let [~'delta (loop [~'i 0 ~'total ~weight] + (if (= ~'i ~width) ~'total (recur (inc ~'i) (+ ~'total ~'i))))] + (if (even? ~'i) + (recur (do (setq ~'ticks (inc ~'ticks)) (inc ~'i)) (+ ~'b ~'delta) (- ~'a ~'delta) + (conj ~'history [~'a ~'b ~'ticks])) + (recur (do (setq ~'ticks (inc ~'ticks)) (inc ~'i)) (- ~'b ~'delta) (+ ~'a ~'delta) + (conj ~'history [~'a ~'b ~'ticks])))))))) + (label ~'done) ~'result)) + +(defn loop-outcome [config wrap] + (try {:value (source/run-form (wrap (loop-program config)))} + (catch Throwable error {:error (class error) :message (ex-message error)}))) + +(defn minimize-loop [config wrap] + (loop [config config] + (if-let [smaller (first (for [key [:limit :cut :width :a :b :weight] + value (distinct [0 (quot (get config key) 2)]) + :when (not= value (get config key)) + :let [candidate (assoc config key value)] + :when (not= {:value (loop-model candidate)} (loop-outcome candidate wrap))] + candidate))] + (recur smaller) config))) + +(deftest generated-nested-loops + (let [seed 161803 random (java.util.Random. seed)] + (dotimes [index 240] + (let [config {:limit (.nextInt random 12) :cut (.nextInt random 15) :width (.nextInt random 6) + :a (- (.nextInt random 21) 10) :b (- (.nextInt random 21) 10) :weight (- (.nextInt random 7) 3)} + expected {:value (loop-model config)}] + (doseq [wrap [identity #(list 'let ['unused 1] (list 'do %))]] + (let [actual (loop-outcome config wrap)] + (is (= expected actual) + (pr-str {:seed seed :index index :config config :expected expected :actual actual + :minimal (when-not (= expected actual) (loop-program (minimize-loop config wrap)))})))))))) + +(defn insertion-sort! [^longs values] + (loophole [writes 0] + (loop [i 1] + (when (< i (alength values)) + (let [key (aget values (int i))] + (loop [j i] + (if (and (pos? j) (> (aget values (int (dec j))) key)) + (do (aset-long values (int j) (aget values (int (dec j)))) + (setq writes (inc writes)) (recur (dec j))) + (aset-long values (int j) key)))) + (recur (inc i)))) + writes)) + +(defn first-negative [^longs values] + (loophole [found -1] + (loop [i 0] + (when (< i (alength values)) + (when (neg? (aget values (int i))) (setq found i) (goto done)) + (recur (inc i)))) + (label done) found)) + +(defn gcd [a b] + (loophole [steps 0] + (loop [a (abs a) b (abs b)] + (if (zero? b) a (do (setq steps (inc steps)) (recur b (rem a b))))))) + +(deftest algorithms-against-independent-references + (let [seed 577215 random (java.util.Random. seed)] + (dotimes [index 180] + (let [input (vec (repeatedly (.nextInt random 25) #(- (.nextInt random 15) 7))) + values (long-array input) a (- (.nextInt random 1001) 500) b (- (.nextInt random 1001) 500)] + (insertion-sort! values) + (is (= (vec (sort input)) (vec values)) (pr-str {:seed seed :index index :input input})) + (is (= (or (first (keep-indexed #(when (neg? %2) %1) input)) -1) (first-negative (long-array input)))) + (is (= (.longValue (.gcd (biginteger a) (biginteger b))) (gcd a b))))))) diff --git a/test/com/tailrecursion/blacktide/machine_test.clj b/test/com/tailrecursion/blacktide/machine_test.clj new file mode 100644 index 0000000..128393a --- /dev/null +++ b/test/com/tailrecursion/blacktide/machine_test.clj @@ -0,0 +1,98 @@ +(ns com.tailrecursion.blacktide.machine-test + "Compile allocated machines with the real Clojure compiler and execute them." + (:require [clojure.test :refer [deftest is]] + [com.tailrecursion.blacktide.compiler.jvm :as jvm] + [com.tailrecursion.blacktide.compiler.emit :as emit])) + +(defn literal [value] {:op :literal :value value}) +(defn local [id] {:op :local :id id}) +(defn assign [id rhs] {:target id :value rhs}) +(defn put-value [id value] (assign id {:op :value :value (literal value)})) +(defn call [id f & args] (assign id {:op :invoke :function (local f) :args (vec args)})) +(defn block [ops term] {:ops (vec ops) :term term}) +(defn run-machine [machine] + (binding [*ns* (the-ns 'com.tailrecursion.blacktide.machine-test)] + (eval (emit/program-form machine)))) + +(deftest values-and-clojure-truth + (doseq [value [nil false true 0 1N 1/2 2.5 2M "text" :keyword 'symbol [1 2] {:a 3} #{4}]] + (is (= value (run-machine {:entry 0 :slots [] :live {0 #{}} + :blocks {0 (block [] {:op :return :value (literal value)})}})))) + (doseq [condition [nil false true 0 "" []]] + (is (= (if condition :yes :no) + (run-machine {:entry 0 :slots [] :live {0 #{} 1 #{} 2 #{}} + :blocks {0 (block [] {:op :branch :test (literal condition) :yes 1 :no 2}) + 1 (block [] {:op :return :value (literal :yes)}) + 2 (block [] {:op :return :value (literal :no)})}}))))) + +(deftest label-values-and-exceptions + (doseq [label [0N 2147483648 Long/MAX_VALUE]] + (is (= :ok (run-machine {:entry label :slots [] :live {label #{}} + :blocks {label (block [] {:op :return :value (literal :ok)})}})))) + (let [machine {:entry 0 :slots [] :live {0 #{}} + :blocks {0 (block [(assign 'make-error {:op :global :name 'clojure.core/ex-info}) + (call 'error 'make-error (literal "machine failure") (literal {:tag :expected}))] + {:op :throw :value (local 'error)})}} + error (try (run-machine machine) nil (catch clojure.lang.ExceptionInfo e e))] + (is (= {:tag :expected} (ex-data error))) + (is (= "machine failure" (ex-message error))))) + +(deftest boxed-storage-and-native-calls + (doseq [value [nil false 3 2.5 1/2 "text"]] + (is (= value + (run-machine {:entry 0 :slots '[x] :live {0 #{} 1 '#{x}} + :blocks {0 (block [(put-value 'x value)] {:op :jump :target 1}) + 1 (block [(assign 'copy {:op :value :value (local 'x)})] + {:op :return :value (local 'copy)})}})))) + (let [machine {:entry 0 :slots ['x] :live {0 #{} 1 '#{x} 2 '#{x}} + :blocks {0 (block [(put-value 'x 1)] {:op :jump :target 1}) + 1 (block [(put-value 'x "changed")] {:op :jump :target 2}) + 2 (block [] {:op :return :value (local 'x)})}}] + (is (= "changed" (run-machine machine)))) + (is (= [10 20] + (run-machine {:entry 0 :slots [] :live {0 #{}} + :blocks {0 (block [(assign 'f {:op :global :name 'clojure.core/vector}) + (call 'value 'f (literal 10) (literal 20))] + {:op :return :value (local 'value)})}}))) + (is (= #'clojure.core/vector + (run-machine {:entry 0 :slots [] :live {0 #{}} + :blocks {0 (block [(assign 'v {:op :var :name 'clojure.core/vector})] + {:op :return :value (local 'v)})}})))) + +(deftest backedges-and-constant-stack + (let [machine {:entry 0 :slots '[case dec-fn zero-fn] + :live {0 #{} 1 '#{case dec-fn zero-fn} 2 '#{case dec-fn zero-fn} 3 '#{case}} + :blocks {0 (block [(put-value 'case 100000) + (assign 'dec-fn {:op :global :name 'clojure.core/dec}) + (assign 'zero-fn {:op :global :name 'clojure.core/zero?})] + {:op :jump :target 1}) + 1 (block [(call 'done 'zero-fn (local 'case))] + {:op :branch :test (local 'done) :yes 3 :no 2}) + 2 (block [(call 'case 'dec-fn (local 'case))] {:op :jump :target 1}) + 3 (block [] {:op :return :value (local 'case)})}}] + (is (zero? (run-machine machine))) + (is (zero? (run-machine machine))) + (let [form (emit/program-form machine) + nodes (tree-seq #(and (coll? %) (not (and (seq? %) (= 'quote (first %))))) seq form) + operators (set (keep #(when (seq? %) (first %)) nodes))] + (is (= 'loop* (first form))) + (is (operators 'clojure.core/case)) + (is (operators 'recur)) + (is (not-any? operators '[atom volatile! object-array ref reset! swap! aset]))))) + +(defn record! [events tag value] + (swap! events conj tag) + value) + +(deftest external-values-and-ordered-effects + (let [machine {:entry 0 :slots '[events f a] + :live {0 #{} 1 '#{events f a}} + :blocks {0 (block [(assign 'events {:op :external :name 'event-log :jvm (jvm/fact false {:hint nil :var-meta {}} :unknown)}) + (assign 'f {:op :global :name 'com.tailrecursion.blacktide.machine-test/record!}) + (call 'a 'f (local 'events) (literal :first) (literal 10))] + {:op :jump :target 1}) + 1 (block [(call 'b 'f (local 'events) (literal :second) (literal 20))] + {:op :return :value (local 'a)})}} + form (list 'let ['event-log '(atom []) 'value (emit/program-form machine)] + ['value '(deref event-log)])] + (is (= [10 [:first :second]] (eval form))))) diff --git a/test/com/tailrecursion/blacktide/native_sites_test.clj b/test/com/tailrecursion/blacktide/native_sites_test.clj new file mode 100644 index 0000000..8b98233 --- /dev/null +++ b/test/com/tailrecursion/blacktide/native_sites_test.clj @@ -0,0 +1,101 @@ +(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]] + [com.tailrecursion.blacktide.backend-test :as backend] + [com.tailrecursion.blacktide :refer [loophole setq label goto]] + [com.tailrecursion.blacktide.invocation-test] + [com.tailrecursion.blacktide.lookup-test :as lookup])) + +(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.invocation-test/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 (lookup/->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 [^java.util.Random random depth path] + (if (zero? depth) + (list 'do (list 'swap! 'events 'conj [:leaf path]) + (list 'quote (nth [nil false 1000 1.5 :selected "text"] (.nextInt random 6)))) + (let [test (list 'do (list 'swap! 'events 'conj [:test path]) + (list 'mod (list '+ 'input (.nextInt random 5)) 3)) + arm (fn [index] (selection-tree random (dec depth) (conj path index)))] + (if (.nextBoolean random) + (list 'if (list 'zero? test) (arm 0) (arm 1)) + (list 'let ['choice test] + (list 'case 'choice 0 (arm 0) 1 (arm 1) (arm 2))))))) + +(deftest nested-joins-preserve-one-continuation + ;; Java method values after a join retain one class construction site. + (let [seed 331662 random (java.util.Random. seed)] + (doseq [trial (range 32)] + (let [tree (selection-tree random (+ 1 (.nextInt random 3)) []) + 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)] + (is (= :structured (:op (backend/selection (source form)))) (pr-str [seed trial tree])) + (doseq [mode [:default :dispatcher]] + (is (= expected (observe form mode)) (pr-str [seed trial mode tree]))))))) diff --git a/test/com/tailrecursion/blacktide/package_test.clj b/test/com/tailrecursion/blacktide/package_test.clj new file mode 100644 index 0000000..720be9e --- /dev/null +++ b/test/com/tailrecursion/blacktide/package_test.clj @@ -0,0 +1,32 @@ +(ns com.tailrecursion.blacktide.package-test + "A release built from a working checkout must exclude nested editor caches." + (:require [clojure.test :refer [deftest is]] + [clojure.java.io :as io] + [clojure.java.shell :as shell])) + +(deftest jar-excludes-editor-state + (let [directory (.toFile (java.nio.file.Files/createTempDirectory "blacktide-package-" + (make-array java.nio.file.attribute.FileAttribute 0))) + 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"] + 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}") + (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))] + (is (zero? (:exit result)) (str (:out result) (:err result))) + (when (zero? (:exit result)) + (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 (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_fixture.clj b/test/com/tailrecursion/blacktide/source_fixture.clj new file mode 100644 index 0000000..306271f --- /dev/null +++ b/test/com/tailrecursion/blacktide/source_fixture.clj @@ -0,0 +1,7 @@ +(ns com.tailrecursion.blacktide.source-fixture + "Foreign macro identities and visibility for source-expansion tests." + (:require [com.tailrecursion.blacktide :as lh])) + +(def expansion-events (atom 0)) +(defmacro ^:private hidden [] (swap! expansion-events inc) :hidden) +(defmacro goto [value] value) diff --git a/test/com/tailrecursion/blacktide/source_loops_test.clj b/test/com/tailrecursion/blacktide/source_loops_test.clj new file mode 100644 index 0000000..d7e20a3 --- /dev/null +++ b/test/com/tailrecursion/blacktide/source_loops_test.clj @@ -0,0 +1,241 @@ +(ns com.tailrecursion.blacktide.source-loops-test + "Compare ordinary numeric loops with both backends, including source boxing, + recur inference, evaluation traces and bounded generated alpha variants." + (:require [com.tailrecursion.blacktide :refer [loophole]] + [com.tailrecursion.blacktide.compiler.backend :as backend] + [com.tailrecursion.blacktide.compiler.emit :as emit] + [com.tailrecursion.blacktide.jvm-test :as jvm] + [clojure.walk :as walk] + [clojure.test :as t])) +(defn root-error [e] (if-let [cause (ex-cause e)] (recur cause) e)) +(defn canonical [value] + (cond + (instance? Double value) [(class value) (Double/doubleToLongBits value)] + (instance? Float value) [(class value) (Float/floatToIntBits value)] + (number? value) [(class value) value] + (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)) + (list* (first body) (second body) (concat (butlast (nnext body)) [(enclose (last body) mode)])) + (if (= mode :ordinary) body + (list 'loophole '[loop-effect nil] + (list* (first body) (second body) '(com.tailrecursion.blacktide/setq loop-effect true) (nnext body)))))) + +(defn compile-body [body mode] + (try + (binding [*ns* (the-ns 'com.tailrecursion.blacktide.source-loops-test)] + (with-redefs [backend/compile-program (if (= mode :dispatcher) + emit/program-form + backend/compile-program)] + (eval (list 'fn '[events input n] (enclose body mode))))) + (catch Throwable e {:compile (class (root-error e))}))) +(defn outcome [f input n] + (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)}))))) +(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)]))) + (let [step (fn [x] x)] + (loop [i 0 x 1000 y x] + (if (< i n) (recur (inc i) (step x) x) [(identical? x y) (identical? x x)]))) + (let [step (fn [x] x)] + (loop [i 0 x Long/MIN_VALUE] + (if (< i n) (recur (inc i) (step x)) (- x x)))) + (let [step (fn [x] x)] + (loop [i 0 x Long/MIN_VALUE y 1000] + (if (< i n) (recur (inc i) x (step y)) (- x x)))) + (let [step (fn [x] x)] + (loop [i 0 x Long/MIN_VALUE y Long/MIN_VALUE] + (if (< i n) (recur (inc i) (step x) x) (- y y)))) + (let [step (fn ^long [^long x] (unchecked-inc x))] + (loop [i 1000 x (long input)] + (if (< i (+ 1000 n)) (recur (inc i) (step x)) [(identical? i i) x]))) + (let [step (fn ^long [^long x] (unchecked-inc x))] + (loop [i 0 x 0] + (if (< i n) (let [next (step x)] + (recur (do (swap! events conj [:index i]) (inc i)) + (do (swap! events conj [:value x next]) next))) + x))) + (let [step (fn ^double [^double x] (+ x 0.5))] + (loop [i 0 x (double input)] (if (< i n) (recur (inc i) (step x)) (Double/doubleToRawLongBits x)))) + (let [step (fn [x] input)] + (loop [i 0 x 0] (if (< i n) (recur (inc i) (step x)) [x (class x)]))) + (let [step (fn ^long [^long x] (inc x))] + (loop [i 0 x 1000] + (if (< i n) (recur (int (inc i)) (step x)) x))) + (let [step (fn ^double [^double x] (+ x 0.5))] + (loop [i 0.0 x 1.5] + (if (< i n) (recur (float (+ i 1.0)) (step x)) [i x (class i)]))) + (let [step (fn ^long [^long x] (inc x))] + (loop [i 0 x (do (swap! events conj :initial) 1000)] + (if (< i n) (recur (inc i) (step x)) x))) + (let [step (fn [x] (if x nil 1000))] + (loop [i 0 x 1000] (if (< i n) (recur (inc i) (step x)) x)))]) + +(t/deftest dependent-loop-initializers-retain-final-types + (doseq [init [1000 1.5] + :let [body `(let [~'step (fn [~'x] (swap! ~'events conj ~'x) ~'x)] + (loop [~'i 0 ~'x ~init ~'y (+ ~'x 1)] + (if (< ~'i ~'n) + (recur (inc ~'i) (~'step ~'x) (~'step ~'y)) + [(.choose ^com.tailrecursion.blacktide.jvm_test.Choice jvm/chooser ~'x) + (.choose ^com.tailrecursion.blacktide.jvm_test.Choice jvm/chooser ~'y) + ~'x ~'y]))) + ordinary (compile-body body :ordinary)]] + (doseq [mode [:default :dispatcher] + :let [compiled (compile-body body mode)] n [0 1 3]] + (t/is (= (outcome ordinary nil n) (outcome compiled nil n)) (pr-str [init mode n]))))) + +(t/deftest source-loop-regressions + (doseq [body bodies + :let [ordinary (compile-body body :ordinary)] + mode [:default :dispatcher] + :let [compiled (compile-body body mode)] + input [0 1000 -7 1.5 1/3 1000N 2M nil] + n [0 1 3]] + (t/is (= (outcome ordinary input n) (outcome compiled input n)) (pr-str [mode input n body]))) + (doseq [initializer '[1000 1.5 -0.0 Double/NaN] + :let [body (list 'let ['step '(fn [x] x)] + (list 'loop ['i 0 'x initializer] + '(if (< i n) (recur (inc i) (step x)) x))) + ordinary (compile-body body :ordinary)] + mode [:default :dispatcher] :let [compiled (compile-body body mode)] + n [0 1 3]] + (t/is (= (identical? (ordinary nil nil n) (ordinary nil nil n)) + (identical? (compiled nil nil n) (compiled nil nil n))) (pr-str [mode n body]))) + ;; A primitive backedge is insufficient to specialize an unknown initial box. + (let [body '(loop [i 0 x (identity input)] + (if (< i n) (recur (inc i) (Math/fma 1.5 (double x) 0.25)) x))] + (doseq [mode [:default :dispatcher] input [0 1.5 -0.0 1M nil] n [0 1 3]] + (t/is (= (outcome (compile-body body :ordinary) input n) + (outcome (compile-body body mode) input n)))))) + +(t/deftest source-loop-overloads-follow-fixed-point-types + (doseq [body '[(let [step (fn ^long [^long x] (inc x))] + (loop [i 0 x 1000] + (if (< i n) (recur (inc i) (step x)) + [(.choose ^com.tailrecursion.blacktide.jvm_test.Choice jvm/chooser x) + (.choose ^com.tailrecursion.blacktide.jvm_test.Choice jvm/chooser i)]))) + (let [step (fn ^long [^long x] (inc x))] + (loop [i 0 x 1000 y x] + (if (< i n) (recur (inc i) (step x) x) + (.choose ^com.tailrecursion.blacktide.jvm_test.Choice jvm/chooser y)))) + (let [step (fn ^double [^double x] (+ x 0.5))] + (loop [i 0 x 1000.0] + (if (< i n) (recur (inc i) (step x)) + (.choose ^com.tailrecursion.blacktide.jvm_test.Choice jvm/chooser x)))) + (let [step (fn [x] input)] + (loop [i 0 x 1000] + (if (< i n) (recur (inc i) (step x)) + (.choose ^com.tailrecursion.blacktide.jvm_test.Choice jvm/chooser x)))) + (let [step (fn ^long [^long x] (inc x))] + (loop [i 0 x 1000] + (if (< i n) (recur (inc i) (step x)) (Math/abs x)))) + (let [step (fn [x] input)] + (loop [i 0 x 1000] + (if (< i n) (recur (inc i) (step x)) (.substring x 1)))) + (let [step (fn [x] input)] + (loop [i 0 x 1000] + (if (< i n) (recur (inc i) (step x)) (.length x))))] + :let [reference (compile-body body :ordinary)]] + (doseq [mode [:default :dispatcher] :let [compiled (compile-body body mode)] + input [0 1.5 1/3 2M "word" nil] n [0 1 3]] + (t/is (= (outcome reference input n) (outcome compiled input n)) (pr-str [body mode input n]))))) + +(defn arithmetic [double? value [op k]] + (let [v (gensym "operand") k (if double? (double k) k)] + `(let [~v ~value] + ~(case op + :add `(+ ~v ~k) + :increment `(inc ~v) + :negate `(- ~v) + :scale `(* ~v ~k) + :branch `(if (< ~v 0) (+ ~v ~k) (- ~v ~k)) + :flip (if double? `(- 0.0 ~v) `(bit-xor ~v ~(long k))))))) + +(defn generated-body [{:keys [double? typed? narrow? alias? shadow? scalar? forward? literal? steps k]} renamed?] + (let [cast (if double? 'double 'long) + parameters (cond-> ['p] typed? (with-meta {:tag cast})) + parameters (if typed? (with-meta [(with-meta 'p {:tag cast})] (meta parameters)) parameters) + compute (reduce (partial arithmetic double?) (list cast 'p) steps) + compute `(do (swap! ~'events conj [:call ~'p ~'bias]) ~compute) + call (list 'step 'x) + call (if shadow? `(let [~'x (+ ~'x ~'bias)] ~call) call) + update (if alias? `(let [~'snapshot ~call] ~'snapshot) call) + next-index (if narrow? (list (if double? 'float 'int) '(inc i)) '(inc i)) + initial (if literal? (if double? 1.5 1000) (list cast 'input)) + result (if scalar? '(- y y) + '[x y (class x) (class y) (identical? x x) (identical? x y) (identical? i i) + (.choose ^com.tailrecursion.blacktide.jvm_test.Choice jvm/chooser x) + (.choose ^com.tailrecursion.blacktide.jvm_test.Choice jvm/chooser y)]) + body `(let [~'bias ~(if double? (double k) k) + ~'step (fn ~parameters ~compute)] + (loop [~'i ~(if double? 0.0 0) + ~'x (do (swap! ~'events conj :init) ~initial) + ~'y ~'x] + (if (< ~'i ~'n) + (recur (do (swap! ~'events conj [:index ~'i]) ~next-index) + (do (swap! ~'events conj [:left ~'x]) ~update) + (do (swap! ~'events conj [:right ~'y]) ~(if forward? 'x '(step y)))) + ~result)))] + (if renamed? + (list 'let ['unused :harmless] + (walk/postwalk #(if (symbol? %) (with-meta (get '{i counter x left y right p value + step when bias amount snapshot saved} % %) (meta %)) %) body)) + body))) + +(defn config-outcome [config renamed? mode n] + (let [form (generated-body config renamed?)] + (outcome (compile-body form mode) (:input config) n))) + +(defn minimize [config renamed? mode n] + (loop [config config] + (let [candidates (concat + (for [index (range (count (:steps config)))] + (update config :steps #(into (subvec % 0 index) (subvec % (inc index))))) + (for [key [:typed? :narrow? :alias? :shadow? :scalar? :forward? :literal?] + :when (get config key)] (assoc config key false)) + (when (not= 0 (:input config)) [(assoc config :input 0)]) + (when (not= 0 (:k config)) [(assoc config :k 0)]))] + (if-let [smaller (first (filter #(not= (config-outcome % false :ordinary n) + (config-outcome % renamed? mode n)) candidates))] + (recur smaller) config)))) + +(t/deftest generated-source-loop-equivalence + (let [seed 141421 random (java.util.Random. seed) + choose #(nth % (.nextInt random (count %)))] + (dotimes [index 128] + (let [double? (odd? index) + config {:double? double? :typed? (.nextBoolean random) :narrow? (.nextBoolean random) + :alias? (.nextBoolean random) :shadow? (.nextBoolean random) + :scalar? (.nextBoolean random) :forward? (.nextBoolean random) + :literal? (.nextBoolean random) :k (- (.nextInt random 7) 3) + :input (choose (if double? [-0.0 1.5 Double/NaN Double/POSITIVE_INFINITY Double/MIN_VALUE] + [0 1000 Long/MIN_VALUE Long/MAX_VALUE 2M 1/3 nil])) + :steps (vec (repeatedly (inc (.nextInt random 4)) + #(vector (choose [:add :increment :negate :scale :branch :flip]) + (- (.nextInt random 7) 3))))} + reference (compile-body (generated-body config false) :ordinary)] + (doseq [renamed? [false true] mode [:default :dispatcher] + :let [compiled (compile-body (generated-body config renamed?) mode)] n [0 1 4]] + (let [expected (outcome reference (:input config) n) actual (outcome compiled (:input config) n)] + (t/is (= expected actual) + (when (not= expected actual) + (let [small (minimize config renamed? mode n)] + (pr-str {:seed seed :index index :mode mode :renamed? renamed? :n n + :input (:input small) :form (generated-body small renamed?)})))))))) + (println "Source loop generator:" {:seed seed :configs 128 :comparisons 1536}))) diff --git a/test/com/tailrecursion/blacktide/source_test.clj b/test/com/tailrecursion/blacktide/source_test.clj new file mode 100644 index 0000000..2c64535 --- /dev/null +++ b/test/com/tailrecursion/blacktide/source_test.clj @@ -0,0 +1,179 @@ +(ns com.tailrecursion.blacktide.source-test + "Source binding and expansion compared with ordinary Clojure, including + generated lexical programs." + (:require [clojure.test :refer [deftest is]] + [com.tailrecursion.blacktide :as lh :refer [loophole label goto setq psetq]] + [com.tailrecursion.blacktide.source-fixture :as fixture])) + +(defn sum [a b] (+ a b)) +(defn record! [events tag value] (swap! events conj tag) value) +(defmacro sees-local? [name] (contains? &env name)) +(defmacro local-name [name] (list 'quote (:name (get &env name)))) +(defmacro nested [expr] `(do (do ~expr))) +(defmacro bind-x [value & body] `(let [~'x ~value] ~@body)) +(def expansion-events (atom [])) +(defmacro expanded [value] (swap! expansion-events conj value) value) +(def ^:dynamic *operator* identity) +(defn change-operator [x] (set! *operator* (constantly :new)) x) + +(defn run-form [form] + (binding [*ns* (the-ns 'com.tailrecursion.blacktide.source-test)] (eval form))) +(defn rejection [form] + (try (run-form form) nil + (catch Throwable e + (loop [e e] (if-let [cause (ex-cause e)] (recur cause) (ex-data e)))))) + +(deftest sequential-scopes-and-results + (is (nil? (loophole []))) + (is (nil? (loophole [x 1]))) + (is (= [1 1 3 4] + (loophole [x 1 y x] (let [x 3 z (sum x y)] (vector y y x z))))) + (is (= [1 2 1] + (loophole [x 1] (vector x (let [x 2] x) x)))) + (is (= :done (loophole [] 1 nil :done))) + (is (= 3 (loophole [x 1 x (sum x 2)] x))) + (is (= [3] (loophole [x 3] (-> x vector)))) + (is (= '(setq x 3) (loophole [] '(setq x 3)))) + (is (= {:op :local :id 'ghost} (loophole [] '{:op :local :id ghost}))) + (is (= #'sum (loophole [sum 2] (var sum)))) + (is (= String (loophole [] String))) + (let [x 9 f (fn [n] (+ n x))] + (is (= 13 (loophole [] (f 4)))))) + +(deftest expansion-knows-lexical-context + (is (loophole [x 1] (sees-local? x))) + (is (= 'x (loophole [x 1] (local-name x)))) + (is (false? (loophole [] (sees-local? absent)))) + (let [caller 1] (is (loophole [] (sees-local? caller)))) + (is (= 10 (loophole [x 3] (bind-x 10 (nested x))))) + (is (= [1 2] (loophole [goto vector] (goto 1 2)))) + (is (= [3] (loophole [when vector] (when 3)))) + (is (= [4] (loophole [loophole vector] (loophole 4)))) + (is (= 6 (loophole [] (fixture/goto 6)))) + (reset! expansion-events []) + (is (= 3 (run-form '(loophole [x (expanded 1) y (expanded 2)] (expanded 3))))) + (is (= [1 2 3] @expansion-events))) + +(deftest macro-visibility-before-expansion + (reset! fixture/expansion-events 0) + (is (thrown-with-msg? IllegalStateException #"not public" (run-form '(fixture/hidden)))) + (is (= :private-var (:kind (rejection '(loophole [] (fixture/hidden)))))) + (let [v (ns-resolve 'com.tailrecursion.blacktide.source-fixture 'hidden) original (meta v)] + (try + (alter-meta! v assoc :ns (the-ns 'com.tailrecursion.blacktide.source-test)) + (is (thrown-with-msg? IllegalStateException #"not public" (run-form '(fixture/hidden)))) + (is (= :private-var (:kind (rejection '(loophole [] (fixture/hidden)))))) + (finally (reset-meta! v original)))) + (is (zero? @fixture/expansion-events)) + (binding [*ns* (the-ns 'com.tailrecursion.blacktide.source-fixture)] + (is (= :hidden (eval '(hidden)) (eval '(lh/loophole [] (hidden)))))) + (is (= 2 @fixture/expansion-events))) + +(deftest source-evaluation-order + (is (= :old + (binding [*operator* (constantly :old)] (*operator* (change-operator 0))) + (binding [*operator* (constantly :old)] (loophole [] (*operator* (change-operator 0)))))) + (let [events (atom [])] + (is (= [10 20 30] + (loophole [x (record! events :init 10)] + ((do (record! events :operator vector)) + (record! events :first x) + (let [x (record! events :second 20)] x) + (record! events :third 30))))) + (is (= [:init :operator :first :second :third] @events))) + (let [events (atom []) error (ex-info "source failure" {}) + explode (fn [] (throw error))] + (is (identical? error + (try (loophole [x (record! events :first 1) y (explode)] + (record! events :unreachable x)) + (catch Throwable e e)))) + (is (= [:first] @events)))) + +(deftest storage-is-independent-of-redefined-vars + (with-redefs [identity (constantly :changed)] + (is (= 1 (loophole [x 1] x))) + (is (= :changed (loophole [] (identity 2))))) + (doseq [value [nil true false 1 2.5 1N 1M 1/2 \a "text" :key 'name + (byte 1) (short 2) (int 3) (float 4) (Object.)]] + (is (identical? value (loophole [x value] x))))) + +(deftest diagnostics-and-public-surface + (is (not (contains? (loaded-libs) 'typed.clojure))) + (is (= '#{loophole label goto setq psetq} (set (keys (ns-publics 'com.tailrecursion.blacktide))))) + (doseq [op '[label goto setq psetq]] + (is (= :outside-region (:kind (rejection (list op)))))) + (doseq [[form kind] [['(loophole [x] x) :invalid-bindings] + ['(loophole [(x) 1] x) :invalid-binding] + ['(loophole [] missing) :unresolved-symbol] + ['(loophole [] (quote 1 2)) :invalid-quote] + ['(loophole [] (var missing)) :invalid-var] + ['(loophole [] (def x 1)) :unsupported-form] + ['(loophole [] (reify Object (toString [_] "later"))) :unsupported-form] + ['(loophole [x 1] (fn [] x)) :unsupported-form]]] + (is (= kind (:kind (rejection form))) (pr-str form))) + (let [bad (with-meta '(quote 1 2) {:file "example.clj" :line 31 :column 9}) + data (rejection (list 'loophole [] bad))] + (is (= {:file "example.clj" :line 31 :column 9 :form bad :kind :invalid-quote} + data)))) + +(defn expression [^java.util.Random random depth locals] + (let [pick #(.nextInt random %)] + (if (zero? depth) + (if (and (seq locals) (zero? (pick 2))) (nth locals (pick (count locals))) (- (pick 31) 15)) + (case (pick 5) + 0 (list 'sum (expression random (dec depth) locals) (expression random (dec depth) locals)) + 1 (let [name (nth '[x y z] (pick 3))] + (list 'let [name (expression random (dec depth) locals)] + (expression random (dec depth) (conj locals name)))) + 2 (list 'do (expression random (dec depth) locals) (expression random (dec depth) locals)) + 3 (list 'identity (expression random (dec depth) locals)) + (expression random 0 locals))))) + +(defn smaller [form] + (when (seq? form) + (let [[op & args] form] + (concat [0] + (if (= op 'let) + [(last args) (list 'let [(first (first args)) 0] (last args))] + args) + (for [i (range (count args)) child (smaller (nth args i))] + (list* op (assoc (vec args) i child))))))) + +(defn difference [body] + ;; Invalid shrinks are discarded by the independent Clojure oracle. + (try + (let [expected (run-form (list 'let '[x 7] body))] + (try (let [actual (run-form (list 'loophole '[x 7] body))] + (when-not (= expected actual) :value)) + (catch Throwable _ :compilation-or-execution))) + (catch Throwable _ nil))) + +(defn minimize [body kind] + (loop [body body] + (if-let [small (first (filter #(and (not= body %) (= kind (difference %))) (smaller body)))] + (recur small) body))) + +(deftest deterministic-lexical-programs + (let [seed 20261103 random (java.util.Random. seed)] + (dotimes [trial 150] + (let [body (expression random 3 '[x]) + expected (run-form (list 'let '[x 7] body))] + (doseq [variant [body (list 'do body) (list 'let ['unused 99] body)]] + (let [actual (try [:value (run-form (list 'loophole '[x 7] variant))] + (catch Throwable e [:error (ex-message e)]))] + (is (= [:value expected] actual) + (when (not= [:value expected] actual) + (str "seed " seed ", trial " trial ": " + (pr-str (if-let [kind (difference variant)] (minimize variant kind) variant))))))))))) + +(deftest diagnostic-context-survives-expansion-and-native-boundaries + (doseq [body '[(case)] + prefix [[] ['(goto end)]]] + (let [form (with-meta (list* 'loophole [] (concat prefix [body])) {:line 123 :column 9}) + error (binding [*file* "diagnostic-example.clj"] (rejection form))] + (is (= :invalid-case (:kind error))) + (is (= '(case) (:form error))) + (is (string? (:file error))) + (is (integer? (:line error))) + (is (integer? (:column error))))) + (let [case (constantly 4)] (is (= 4 (loophole [] (case)))))) diff --git a/test/com/tailrecursion/blacktide/source_values_test.clj b/test/com/tailrecursion/blacktide/source_values_test.clj new file mode 100644 index 0000000..9a46f1a --- /dev/null +++ b/test/com/tailrecursion/blacktide/source_values_test.clj @@ -0,0 +1,42 @@ +(ns com.tailrecursion.blacktide.source-values-test + "Primitive source reads box at each Object use; mutable locations and saved + Object references retain their identity across normalization and transfers." + (:require [clojure.test :refer [deftest is]] + [com.tailrecursion.blacktide.backend-test :as backend])) + +(def initializers + '[1000 1.5 (int 1000) (short 1000) (byte 4) (float 1.5) (char 256) (boolean true) + (identity 1000) (identity 1.5) (Integer. 1000) (Double. 1.5)]) + +(deftest source-reads-preserve-width-and-identity + (doseq [init initializers + body '[(identical? x x) + (let [[a b] [x x]] (identical? a b)) + (let [snapshot (identity x)] (identical? snapshot snapshot)) + (let [x (identity x)] (identical? x x))] + :let [form (list 'let ['x init] body) reference (backend/compile-program form :default)] + mode [:default :dispatcher]] + (is (= (reference nil) ((backend/compile-program (list 'loophole [] form) mode) nil)) (pr-str [init body mode]))) + (doseq [init initializers :let [form (list 'let ['x init] 'x) reference (backend/compile-program form :default)] + mode [:default :dispatcher] :let [compiled (backend/compile-program (list 'loophole [] form) mode)]] + (is (= (reference nil) (compiled nil)) (pr-str [init mode])) + (is (= (class (reference nil)) (class (compiled nil))) (pr-str [init mode])) + (is (= (identical? (reference nil) (reference nil)) (identical? (compiled nil) (compiled nil))) + (pr-str [init mode])))) + +(deftest mutable-snapshots-remain-references + (doseq [mode [:default :dispatcher] init initializers + form [(list 'loophole ['x init 'y nil] '(psetq y x x x) '(identical? x y)) + (list 'loophole ['x nil 'y nil] (list 'psetq 'y (list 'setq 'x init)) '(identical? x y)) + (list 'loophole ['x init] + '(let [saved x] (setq x nil) (identical? saved saved)))]] + (is (true? ((backend/compile-program form mode) nil)) (pr-str [mode form]))) + (doseq [mode [:default :dispatcher]] + (is (= [1000 true false] + ((backend/compile-program + '(loophole [x 1000 y nil count 0 saved x] + (label again) + (psetq y x x (long 1001)) + (setq count (inc count)) + (when (< count 2) (setq x saved) (goto again)) + [saved (identical? saved y) (identical? saved x)]) mode) nil))))) diff --git a/test/com/tailrecursion/blacktide/subset_test.clj b/test/com/tailrecursion/blacktide/subset_test.clj new file mode 100644 index 0000000..12719e3 --- /dev/null +++ b/test/com/tailrecursion/blacktide/subset_test.clj @@ -0,0 +1,52 @@ +(ns com.tailrecursion.blacktide.subset-test + "The language boundary applies after macroexpansion, including dead code; + callers may freely use Clojure outside the region. Quoted data stays opaque." + (:require [clojure.test :refer [deftest is]] + [com.tailrecursion.blacktide :refer [loophole label goto setq]] + [com.tailrecursion.blacktide.source-test :as source])) + +(defmacro hidden-function [] `(fn [] :hidden)) +(defmacro hidden-label [] `(label ~'hidden)) + +(deftest excluded-forms-are-rejected-before-reachability + (doseq [body '[(fn [] 1) (fn* [] 1) (letfn [(f [] 1)] (f)) (letfn* [f (fn* [] 1)] (f)) + (try 1 (catch Exception e 2) (finally nil)) (monitor-enter nil) (monitor-exit nil) + (set! x 1) (def value 1) (declare value) (reify Object (toString [_] "x")) + (loophole [] 1) (delay 1) (lazy-seq [1]) (future 1) + (binding [*print-length* 1] 2) (locking (Object.) 1) + (with-open [r (java.io.StringReader. "")] 1) (with-redefs [identity vector] 1) + (com.tailrecursion.blacktide.subset-test/hidden-function)] + prefix [[] ['(goto end)]]] + (is (= :unsupported-form + (:kind (source/rejection (list* 'loophole '[x 0] (concat prefix [body]))))) + (pr-str [prefix body])))) + +(deftest operands-cannot-hide-transfers + (doseq [body '[(identity (do (goto end) 1)) + ((do (goto end) identity) 1) + (if (do (goto end) true) 1 2) + (case (do (goto end) 1) 1 2 3) + (let [a (do (goto end) 1)] a) + (loop [a (do (goto end) 1)] a) + (loop [a 0] (recur (do (goto end) 1))) + (setq x (case 1 1 (goto end) 2)) + [(let [] (goto end) 1)] + (identity (loop [] (goto end))) + ^{:note (do (goto end) 1)} [1] + (let [] (com.tailrecursion.blacktide.subset-test/hidden-label))]] + (is (= :marker-position (:kind (source/rejection (list 'loophole '[x 0] body)))) (pr-str body))) + (is (= :ok (loophole [] (do (label a) (do (goto b))) :wrong (label b) :ok))) + (is (= '(fn [] (goto end)) (loophole [] '(fn [] (goto end)))))) + +(deftest caller-functions-and-dynamic-extent + (let [events (atom []) helper (fn [x] (try (inc x) (finally (swap! events conj :helper))))] + (is (= 4 (try (loophole [x 0] (dotimes [_ 4] (setq x (helper x))) x) + (finally (swap! events conj :caller))))) + (is (= [:helper :helper :helper :helper :caller] @events))) + (letfn [(even-state [n] (if (zero? n) true (odd-state (dec n)))) + (odd-state [n] (if (zero? n) false (even-state (dec n))))] + (is (= [true false] (loophole [] [(even-state 4) (odd-state 4)])))) + (binding [*print-length* 1] + (is (= "[1 ...]" (loophole [] (pr-str [1 2]))))) + (let [error (Exception. "caller handles it")] + (is (identical? error (try (loophole [] (throw error)) (catch Exception e e)))))) diff --git a/test/com/tailrecursion/check_runner.clj b/test/com/tailrecursion/check_runner.clj new file mode 100644 index 0000000..aae620f --- /dev/null +++ b/test/com/tailrecursion/check_runner.clj @@ -0,0 +1,11 @@ +(ns com.tailrecursion.check-runner + "Run the library's static probes and the production compiler checks." + (:require [clojure.test :as test] + [com.tailrecursion.nanopass.check] + [com.tailrecursion.blacktide.check])) + +(defn -main [& _] + (let [{:keys [fail error]} (test/run-tests 'com.tailrecursion.nanopass.check + 'com.tailrecursion.blacktide.check)] + (shutdown-agents) + (when (pos? (+ fail error)) (System/exit 1)))) diff --git a/test/com/tailrecursion/nanopass/annotations.clj b/test/com/tailrecursion/nanopass/annotations.clj new file mode 100644 index 0000000..b7aa96e --- /dev/null +++ b/test/com/tailrecursion/nanopass/annotations.clj @@ -0,0 +1,17 @@ +(ns com.tailrecursion.nanopass.annotations + "Checking-only declarations, generated from the ordinary language values." + (:require [typed.clojure] + [com.tailrecursion.nanopass :as np] + [com.tailrecursion.nanopass.fixtures :as f] + [com.tailrecursion.nanopass.passes :as p] + [com.tailrecursion.nanopass.clients.assignment :as assignment] + [com.tailrecursion.nanopass.clients.flow :as flow] + [com.tailrecursion.nanopass.clients.flow-languages :as flow-languages])) + +(np/ann-languages f/Source f/Core f/Literals f/Mutual f/Stopped) +(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) +(np/ann-languages flow-languages/Syntax flow-languages/Bound flow-languages/Machine) +(np/ann-passes flow/bind-expr flow/bind-exprs flow/bind-local flow/fresh-block flow/fresh-value + flow/emit-op flow/seal flow/start flow/lower flow/lower-exprs flow/compile-program) diff --git a/test/com/tailrecursion/nanopass/assignment_test.clj b/test/com/tailrecursion/nanopass/assignment_test.clj new file mode 100644 index 0000000..6fa625c --- /dev/null +++ b/test/com/tailrecursion/nanopass/assignment_test.clj @@ -0,0 +1,173 @@ +(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]] + [com.tailrecursion.nanopass.clients.assignment :as lower])) + +(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])]) + :exit (throw (ex-info "Region exit" {:status :exit :state state})) + :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))))))) + +(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] + transfer [{:op :exit} {:op :throw :tag :failed}] + nested? [false true]] + (let [source (batch op 'x (event :first (lit 10)) + 'y (sequence-of + (if nested? (batch :setq 'x (lit 9)) (lit nil)) + transfer)) + expected (cond-> {:status (if (= :exit (:op transfer)) :exit :throw) + :vars {'x (cond nested? 9 (= op :setq) 10 :else 1) 'y 2} + :trace [[:first 10]]} + (= :throw (:op transfer)) (assoc :tag :failed))] + (testing (pr-str [op transfer nested?]) + (is (= expected (execute source {'x 1 'y 2}))) + (is (= expected (execute (lower/lower source) {'x 1 'y 2}))))))) + +(defn expression [^java.util.Random rng depth] + (let [pick #(.nextInt rng %) + child #(expression rng (dec depth)) + writes #(vec (repeatedly (pick 4) (fn [] [(nth '[x y] (pick 2)) (child)])))] + (if (zero? depth) + (if (zero? (pick 2)) (lit (- (pick 11) 5)) (read-var (nth '[x y] (pick 2)))) + (case (pick 7) + 0 (child) + 1 {:op :add :left (child) :right (child)} + 2 (sequence-of (child) (child)) + 3 {:op :if :test (lit (zero? (pick 2))) :then (child) :else (child)} + 4 (event (nth [:a :b :c] (pick 3)) (child)) + 5 (sequence-of {:op :setq :writes (writes)} (read-var 'x)) + 6 (sequence-of {:op :psetq :writes (writes)} (read-var 'y)))))) + +(defn child-paths [node] + (case (:op node) + :add [[:left] [:right]] + :if [[:test] [:then] [:else]] + :event [[:value]] + :do (mapv #(vector :forms %) (range (count (:forms node)))) + (:setq :psetq) (mapv #(vector :writes % 1) (range (count (:writes node)))) + [])) + +(defn paths [node] + (cons [] (mapcat (fn [path] (map #(into path %) (paths (get-in node path)))) (child-paths node)))) + +(defn minimize [source fails?] + (loop [source source] + (if-let [smaller (some (fn [path] + (let [candidate (if (seq path) (assoc-in source path (lit 0)) (lit 0))] + (when (and (not= candidate source) (fails? candidate)) candidate))) + (paths source))] + (recur smaller) source))) + +(deftest generated-assignment-combinations + (doseq [seed [712381 314159]] + (let [rng (java.util.Random. seed)] + (dotimes [index 200] + (let [source (expression rng 4) lowered (lower/lower source)] + (doseq [[x y] [[0 0] [1 2] [-7 5] [10 -3]]] + (let [vars {'x x 'y y} + expected (execute source vars) + actual (execute lowered vars) + fails? (fn [program] + (let [expected (execute program vars) + actual (execute (lower/lower program) vars)] + (and (= :return (:status expected) (:status actual)) (not= expected actual))))] + (is (= expected actual) + (pr-str {:seed seed :index index :input [x y] + :source (if (= expected actual) source (minimize source fails?))}))))))))) + +(deftest shrinking-preserves-semantic-failure + (let [source (batch :psetq 'x (sequence-of (lit 8) (read-var 'y)) 'y (read-var 'x)) + fails? (fn [node] + (when (= :psetq (:op node)) + (not= (execute node {'x 1 'y 2}) + (execute (lower/lower (sequence-of (assoc node :op :setq) (lit nil))) {'x 1 'y 2})))) + smaller (minimize source fails?)] + (is (fails? smaller)) + (is (< (count (paths smaller)) (count (paths source)))) + (is (= [[]] (vec (paths (lit {:op :setq :writes :opaque}))))))) diff --git a/test/com/tailrecursion/nanopass/check.clj b/test/com/tailrecursion/nanopass/check.clj new file mode 100644 index 0000000..7c3befa --- /dev/null +++ b/test/com/tailrecursion/nanopass/check.clj @@ -0,0 +1,123 @@ +(ns com.tailrecursion.nanopass.check + "Compile-time probes for generated recursive languages." + (:require [clojure.test :refer [deftest is run-tests]] + [clojure.core.typed :as checker] + [typed.clojure :as t] + [com.tailrecursion.nanopass :as np] + [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.nanopass.assignment-test :as semantics])) + +(defn check-form [form expected] + (binding [*ns* (the-ns 'com.tailrecursion.nanopass.check) + *err* (java.io.StringWriter.)] + (checker/check-form-info form :expected expected :type-provided? true + :check-config {:check-form-eval :never}))) + +(defn accepts? [expected form accepted] + (let [{:keys [ex delayed-errors]} (check-form (list 'quote form) expected) + errors (concat delayed-errors (:errors (ex-data ex)))] + (is (or (nil? ex) (= :top-level-error (:type-error (ex-data ex)))) (str ex)) + (is (= accepted (empty? errors)) (pr-str [form errors])))) + +(deftest recursive-languages + (doseq [[accepted form] + [[true {:op :literal :value '{:op :assign :value Expr}}] + [true {:op :write :name 'x :value {:op :literal :value 42}}] + [false {:op :assign :binding {:op :binding :name 'x :value {:op :literal :value 1}} + :value {:op :literal :value 2}}] + [false {:op :group :bindings [{:op :binding :name 'x + :value {:op :assign :binding {:op :binding :name 'y + :value {:op :literal :value 1}} + :value {:op :literal :value 2}}}] + :body {:op :literal :value nil}}] + [false {:op :write :name 42 :value {:op :literal :value nil}}] + [false {:op :literal :value 1 :extra true}]]] + (accepts? 'f/Core-Expr form accepted))) + +(deftest recursive-containers + (let [leaf {:op :literal :value 1} + bad {:op :assign :binding {:op :binding :name 'x :value leaf} :value leaf} + base {:op :many :items {} :pair [leaf nil] :unique #{}}] + (doseq [form [base (assoc base :note "ok") + (assoc base :items {:a [leaf]} :pair [leaf leaf] :unique #{leaf})]] + (accepts? 'f/Core-Expr form true)) + (doseq [form [(assoc base :items {:a [bad]}) (assoc base :pair [bad nil]) + (assoc base :pair [leaf bad]) (assoc base :unique #{bad}) + (assoc base :note 42)]] + (accepts? 'f/Core-Expr form false)))) + +(deftest indirect-recursion + (accepts? 'f/Mutual-A {:op :a :next {:op :b :next {:op :a :next nil}}} true) + (accepts? 'f/Stopped-A {:op :a :next {:op :stop}} true) + (accepts? 'f/Stopped-A {:op :a :next {:op :b :next nil}} false)) + +(deftest checked-passes + (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.clients.flow))) + +(defn generated-function [clause] + (let [[_ _ _ args body] + (binding [*ns* (the-ns 'com.tailrecursion.nanopass.passes)] + (macroexpand-1 + (list 'np/defpass 'lower + '{:from [f/Source Expr] :to [f/Core Expr] :processors {Binding lower-binding}} + '[node] :assign clause)))] + (list 'fn args body))) + +(deftest generated-negative-probes + (let [good (generated-function '{:op :write :name (:name (:binding node)) + :value (p/lower (:value node))}) + [f args [case-op test & arms]] good + missing (list f args (list* case-op test + (concat (mapcat identity (remove #(= :assign (first %)) + (partition 2 (butlast arms)))) + [(last arms)])))] + (doseq [[accepted form] + [[true good] + [false missing] + [false (generated-function 'node)] + [false (generated-function '{:op :write :name 42 :value (p/lower (:value node))})] + [false '(fn [node] (p/lower (p/lower node)))]]] + (let [{:keys [ex delayed-errors]} (check-form form '[f/Source-Expr :-> f/Core-Expr]) + errors (concat delayed-errors (:errors (ex-data ex)))] + (is (or (nil? ex) (= :top-level-error (:type-error (ex-data ex)))) (str ex)) + (is (= accepted (empty? errors)) (pr-str [form errors])))))) + +(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)))]] + (let [{:keys [ex delayed-errors]} (check-form form '[assignment/Source-Expr :-> assignment/Core-Expr]) + errors (concat delayed-errors (:errors (ex-data ex)))] + (is (or (nil? ex) (= :top-level-error (:type-error (ex-data ex)))) (str ex)) + (is (seq errors) (pr-str form))))) + +(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})))))) + +(defn -main [& _] + (let [{:keys [fail error]} (run-tests 'com.tailrecursion.nanopass.check)] + (shutdown-agents) + (when (pos? (+ fail error)) (System/exit 1)))) diff --git a/test/com/tailrecursion/nanopass/clients/assignment.clj b/test/com/tailrecursion/nanopass/clients/assignment.clj new file mode 100644 index 0000000..c6f13d1 --- /dev/null +++ b/test/com/tailrecursion/nanopass/clients/assignment.clj @@ -0,0 +1,48 @@ +(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 ':exit} :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)]}}) + +(declare lower) + +(np/defpass 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)}) + +(np/defpass 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))))) + +(np/defpass lower {:from [Source Expr] :to [Core Expr]} + [node] + :setq (sequential-writes (:writes node)) + :psetq (parallel-writes (:writes node))) diff --git a/test/com/tailrecursion/nanopass/clients/flow.clj b/test/com/tailrecursion/nanopass/clients/flow.clj new file mode 100644 index 0000000..6d5b683 --- /dev/null +++ b/test/com/tailrecursion/nanopass/clients/flow.clj @@ -0,0 +1,112 @@ +(ns ^:typed.clojure com.tailrecursion.nanopass.clients.flow + "Lexical binding followed by CFG construction, with immutable explicit state. + This independent toy language has integer conditions: zero is false." + (:require [com.tailrecursion.nanopass :as np] + [com.tailrecursion.nanopass.clients.flow-languages :as l] + [typed.clojure :as-alias t])) + +(declare bind-expr bind-exprs bind-local lower lower-exprs) + +(np/defpass bind-exprs + {:from [l/Syntax Exprs] :to (t/HVec [t/Int (t/Vec l/Bound-Expr)]) + :args [(t/Map t/Sym t/Sym) t/Int] :body true} + [env supply forms] + (reduce (fn [^{::t/- (t/HVec [t/Int (t/Vec l/Bound-Expr)])} acc ^{::t/- l/Syntax-Expr} form] + (let [[supply out] acc [supply form] (bind-expr env supply form)] + [supply (conj out form)])) [supply []] forms)) + +(np/defpass bind-local + {:from [l/Syntax Binding] :to (t/HVec [t/Int l/Bound-Binding]) + :args [(t/Map t/Sym t/Sym) t/Int] :body true} + [env supply node] + (let [[supply init] (bind-expr env supply (:value node))] + [(inc supply) {:op :binding :id (symbol (str "local__" supply)) :value init}])) + +(np/defpass bind-expr + {:from [l/Syntax Expr] :to (t/HVec [t/Int l/Bound-Expr]) :args [(t/Map t/Sym t/Sym) t/Int]} + [env supply node] + :int [supply node] + :read [supply {:op :ref :id (or (get env (:name node)) (throw (ex-info "Unbound name" {:name (:name node)})))}] + :set (let [id (or (get env (:name node)) (throw (ex-info "Unbound name" {:name (:name node)}))) + [supply value] (bind-expr env supply (:value node))] + [supply {:op :write :id id :value value}]) + :plus (let [[supply left] (bind-expr env supply (:left node)) + [supply right] (bind-expr env supply (:right node))] + [supply {:op :plus :left left :right right}]) + :let (let [[supply local] (bind-local env supply (:binding node)) + env (assoc env (:name (:binding node)) (:id local)) + [supply body] (bind-expr env supply (:body node))] + [supply {:op :let :binding local :body body}]) + :if (let [[supply test] (bind-expr env supply (:test node)) + [supply yes] (bind-expr env supply (:then node)) + [supply no] (bind-expr env supply (:else node))] + [supply {:op :if :test test :then yes :else no}]) + :while (let [[supply test] (bind-expr env supply (:test node)) + [supply body] (bind-expr env supply (:body node))] + [supply {:op :while :test test :body body}]) + :do (let [[supply forms] (bind-exprs env supply (:forms node))] + [supply {:op :do :forms forms}])) + +(np/defpass fresh-block {:from [l/Machine Builder] :to (t/HVec [l/Machine-Builder t/Int]) :body true} + [state] + [(assoc state :next (inc (:next state))) (:next state)]) + +(np/defpass fresh-value {:from [l/Machine Builder] :to (t/HVec [l/Machine-Builder t/Sym]) :body true} + [state] + [(assoc state :next-value (inc (:next-value state))) (symbol (str "value__" (:next-value state)))]) + +(np/defpass emit-op {:from [l/Machine Operation] :to [l/Machine Builder] :args [l/Machine-Builder] :body true} + [state op] + (assoc state :ops (conj (:ops state) op))) + +(np/defpass seal {:from [l/Machine Term] :to [l/Machine Builder] :args [l/Machine-Builder] :body true} + [state term] + (assoc state :blocks (assoc (:blocks state) (:current state) {:ops (:ops state) :term term}) :ops [])) + +(np/defpass start {:from [l/Machine Builder] :to [l/Machine Builder] :args [t/Int] :body true} + [id state] + (assoc state :current id :ops [])) + +(np/defpass lower-exprs + {:from [l/Bound Exprs] :to (t/HVec [l/Machine-Builder l/Machine-Operand]) :args [l/Machine-Builder] :body true} + [state forms] + (reduce (fn [^{::t/- (t/HVec [l/Machine-Builder l/Machine-Operand])} acc ^{::t/- l/Bound-Expr} form] + (lower (first acc) form)) [state {:op :literal :value 0}] forms)) + +(np/defpass lower + {:from [l/Bound Expr] :to (t/HVec [l/Machine-Builder l/Machine-Operand]) :args [l/Machine-Builder]} + [state node] + :int [state {:op :literal :value (:value node)}] + :ref (let [[state id] (fresh-value state)] + [(emit-op state {:op :put :id id :value {:op :register :id (:id node)}}) {:op :register :id id}]) + :write (let [[state value] (lower state (:value node))] + [(emit-op state {:op :put :id (:id node) :value value}) value]) + :plus (let [[state left] (lower state (:left node)) + [state right] (lower state (:right node)) + [state id] (fresh-value state)] + [(emit-op state {:op :add :id id :left left :right right}) {:op :register :id id}]) + :let (let [local (:binding node) [state init] (lower state (:value local))] + (lower (emit-op state {:op :put :id (:id local) :value init}) (:body node))) + :do (lower-exprs state (:forms node)) + :if (let [[state test] (lower state (:test node)) + [state yes] (fresh-block state) [state no] (fresh-block state) [state join] (fresh-block state) + [state result] (fresh-value state) + state (seal state {:op :branch :test test :yes yes :no no}) + [state a] (lower (start yes state) (:then node)) + state (seal (emit-op state {:op :put :id result :value a}) {:op :jump :target join}) + [state b] (lower (start no state) (:else node)) + state (seal (emit-op state {:op :put :id result :value b}) {:op :jump :target join})] + [(start join state) {:op :register :id result}]) + :while (let [[state header] (fresh-block state) [state body] (fresh-block state) [state exit] (fresh-block state) + state (seal state {:op :jump :target header}) + [state test] (lower (start header state) (:test node)) + state (seal state {:op :branch :test test :yes body :no exit}) + [state _] (lower (start body state) (:body node)) + state (seal state {:op :jump :target header})] + [(start exit state) {:op :literal :value 0}])) + +(np/defpass compile-program {:from [l/Syntax Expr] :to [l/Machine Graph] :body true} + [source] + (let [[_ source] (bind-expr {} 0 source) + [state value] (lower {:next 1 :current 0 :next-value 0 :ops [] :blocks {}} source)] + {:entry 0 :blocks (:blocks (seal state {:op :return :value value}))})) diff --git a/test/com/tailrecursion/nanopass/clients/flow_languages.clj b/test/com/tailrecursion/nanopass/clients/flow_languages.clj new file mode 100644 index 0000000..cf279db --- /dev/null +++ b/test/com/tailrecursion/nanopass/clients/flow_languages.clj @@ -0,0 +1,35 @@ +(ns com.tailrecursion.nanopass.clients.flow-languages + "Source, bound, and graph contracts for the independent flow compiler client." + (:require [com.tailrecursion.nanopass :as np] + [typed.clojure :as-alias t])) + +(np/deflanguage Syntax + {Expr (t/U (t/HMap :mandatory {:op ':int :value t/Int :note t/Any} :complete? true) + (t/HMap :mandatory {:op ':read :name t/Sym} :complete? true) + (t/HMap :mandatory {:op ':set :name t/Sym :value Expr} :complete? true) + (t/HMap :mandatory {:op ':plus :left Expr :right Expr} :complete? true) + (t/HMap :mandatory {:op ':let :binding Binding :body Expr} :complete? true) + (t/HMap :mandatory {:op ':if :test Expr :then Expr :else Expr} :complete? true) + (t/HMap :mandatory {:op ':while :test Expr :body Expr} :complete? true) + (t/HMap :mandatory {:op ':do :forms Exprs} :complete? true)) + Binding (t/HMap :mandatory {:op ':binding :name t/Sym :value Expr} :complete? true) + Exprs (t/Vec Expr)}) + +(np/derive-language Bound Syntax + {:remove {Expr #{:read :set}} + :add {Expr [(t/HMap :mandatory {:op ':ref :id t/Sym} :complete? true) + (t/HMap :mandatory {:op ':write :id t/Sym :value Expr} :complete? true)]} + :replace {Binding (t/HMap :mandatory {:op ':binding :id t/Sym :value Expr} :complete? true)}}) + +(np/deflanguage Machine + {Operand (t/U (t/HMap :mandatory {:op ':literal :value t/Int} :complete? true) + (t/HMap :mandatory {:op ':register :id t/Sym} :complete? true)) + Operation (t/U (t/HMap :mandatory {:op ':put :id t/Sym :value Operand} :complete? true) + (t/HMap :mandatory {:op ':add :id t/Sym :left Operand :right Operand} :complete? true)) + Term (t/U (t/HMap :mandatory {:op ':jump :target t/Int} :complete? true) + (t/HMap :mandatory {:op ':branch :test Operand :yes t/Int :no t/Int} :complete? true) + (t/HMap :mandatory {:op ':return :value Operand} :complete? true)) + Block (t/HMap :mandatory {:ops (t/Vec Operation) :term Term} :complete? true) + Graph (t/HMap :mandatory {:entry t/Int :blocks (t/Map t/Int Block)} :complete? true) + Builder (t/HMap :mandatory {:next t/Int :current t/Int :next-value t/Int + :ops (t/Vec Operation) :blocks (t/Map t/Int Block)} :complete? true)}) diff --git a/test/com/tailrecursion/nanopass/fixtures.clj b/test/com/tailrecursion/nanopass/fixtures.clj new file mode 100644 index 0000000..8050f2f --- /dev/null +++ b/test/com/tailrecursion/nanopass/fixtures.clj @@ -0,0 +1,29 @@ +(ns com.tailrecursion.nanopass.fixtures + "Small languages exercising recursive derivation without a compiler client." + (:require [com.tailrecursion.nanopass :as np] + [typed.clojure :as-alias t])) + +(np/deflanguage Source + {Expr (t/U (t/HMap :mandatory {:op ':literal :value t/Any} :complete? true) + (t/HMap :mandatory {:op ':read :name t/Sym} :complete? true) + (t/HMap :mandatory {:op ':assign :binding Binding :value Expr} :complete? true) + (t/HMap :mandatory {:op ':group :bindings (t/Vec Binding) :body Expr} :complete? true) + (t/HMap :mandatory {:op ':many :items (t/Map t/Keyword (t/Vec Expr)) + :pair (t/HVec [Expr (t/Option Expr)]) :unique (t/Set Expr)} + :optional {:note t/Str :child Expr :maybe (t/Option Expr)} :complete? true)) + Binding (t/HMap :mandatory {:op ':binding :name t/Sym :value Expr} :complete? true) + Envelope (t/HMap :optional {:child Expr :maybe (t/Option Expr)} :complete? true)}) + +(np/derive-language Core Source + {:remove {Expr #{:assign}} + :add {Expr [(t/HMap :mandatory {:op ':write :name t/Sym :value Expr} :complete? true)]}}) + +(np/derive-language Literals Core + {:replace {Expr (t/HMap :mandatory {:op ':literal :value t/Any} :complete? true)}}) + +(np/deflanguage Mutual + {A (t/HMap :mandatory {:op ':a :next (t/Option B)} :complete? true) + B (t/HMap :mandatory {:op ':b :next (t/Option A)} :complete? true)}) + +(np/derive-language Stopped Mutual + {:replace {B (t/HMap :mandatory {:op ':stop} :complete? true)}}) diff --git a/test/com/tailrecursion/nanopass/flow_test.clj b/test/com/tailrecursion/nanopass/flow_test.clj new file mode 100644 index 0000000..dc44246 --- /dev/null +++ b/test/com/tailrecursion/nanopass/flow_test.clj @@ -0,0 +1,101 @@ +(ns com.tailrecursion.nanopass.flow-test + "Execute a client's graphs against independent arithmetic and scope results. + This bounded VM is test instrumentation, not a library or Loophole backend." + (:require [clojure.test :refer [deftest is]] + [com.tailrecursion.nanopass.clients.flow :as flow])) + +(defn number-node [n] {:op :int :value n :note '{:op :read :name opaque}}) +(defn read-node [name] {:op :read :name name}) +(defn plus [a b] {:op :plus :left a :right b}) +(defn set-node [name value] {:op :set :name name :value value}) +(defn let-node [name value body] {:op :let :binding {:op :binding :name name :value value} :body body}) +(defn forms [& nodes] {:op :do :forms (vec nodes)}) + +(defn execute [{:keys [entry blocks]}] + (let [value (fn [registers operand] + (case (:op operand) + :literal (:value operand) + :register (let [id (:id operand)] + (when-not (contains? registers id) + (throw (ex-info "Uninitialized register" {:id id}))) + (get registers id))))] + (loop [pc entry registers {} steps 0] + (when (> steps 10000) (throw (ex-info "Nonterminating test graph" {:pc pc}))) + (let [{:keys [ops term] :as block} (get blocks pc) + _ (when-not block (throw (ex-info "Missing block" {:pc pc}))) + registers (reduce (fn [registers op] + (assoc registers (:id op) + (case (:op op) + :put (value registers (:value op)) + :add (+ (value registers (:left op)) (value registers (:right op)))))) + registers ops)] + (case (:op term) + :return (value registers (:value term)) + :jump (recur (:target term) registers (inc steps)) + :branch (recur (if (zero? (value registers (:test term))) (:no term) (:yes term)) + registers (inc steps))))))) + +(defn sum-program [n] + (let-node 'n (number-node n) + (let-node 'total (number-node 0) + (forms {:op :while :test (read-node 'n) + :body (forms (set-node 'total (plus (read-node 'total) (read-node 'n))) + (set-node 'n (plus (read-node 'n) (number-node -1))))} + (read-node 'total))))) + +(deftest contextual-bindings-and-snapshots + (let [source (let-node 'x (number-node 7) + (plus (let-node 'x (plus (read-node 'x) (number-node 1)) (read-node 'x)) + (read-node 'x))) + [supply bound] (flow/bind-expr {} 0 source)] + (is (= 2 supply)) + (is (= 15 (execute (flow/compile-program source)))) + (is (not= (get-in bound [:binding :id]) (get-in bound [:body :left :binding :id]))) + (is (= '{:op :read :name opaque} (get-in bound [:binding :value :note])))) + (is (= 17 (execute (flow/compile-program + (let-node 'x (number-node 7) + (plus (read-node 'x) (set-node 'x (number-node 10)))))))) + (is (thrown-with-msg? clojure.lang.ExceptionInfo #"Unbound name" + (flow/compile-program (read-node 'missing))))) + +(deftest joins-backedges-and-repeated-execution + (doseq [n (range 31)] + (let [program (sum-program n) graph (flow/compile-program program)] + (is (= (reduce + (range (inc n))) (execute graph) (execute graph))) + (is (= graph (flow/compile-program program))))) + (doseq [n (range 10)] + (let [source (let-node 'x (number-node n) + (forms {:op :if :test (read-node 'x) + :then (set-node 'x (plus (read-node 'x) (number-node 4))) + :else (set-node 'x (number-node 19))} + (read-node 'x)))] + (is (= (if (zero? n) 19 (+ n 4)) (execute (flow/compile-program source))))))) + +(defn inconvenient-program [n initial pivot bias offset] + (let-node 'n (number-node n) + (let-node 'a (number-node initial) + (forms + {:op :while :test (read-node 'n) + :body (forms + (let-node 'snapshot (read-node 'a) + {:op :if :test (plus (read-node 'n) (number-node (- pivot))) + :then (set-node 'a (plus (read-node 'a) + (set-node 'a (plus (read-node 'n) (number-node offset))))) + :else (set-node 'a (plus (read-node 'snapshot) + (let-node 'a (read-node 'snapshot) + (set-node 'a (plus (read-node 'a) (number-node bias))))))}) + (set-node 'n (plus (read-node 'n) (number-node -1))))} + (read-node 'a))))) + +(deftest generated-scope-and-control-combinations + (let [seed 271828 rng (java.util.Random. seed)] + (dotimes [index 250] + (let [n (.nextInt rng 13) initial (- (.nextInt rng 11) 5) + pivot (.nextInt rng 16) bias (- (.nextInt rng 7) 3) offset (- (.nextInt rng 7) 3) + source (inconvenient-program n initial pivot bias offset) + expected (loop [i n result initial] + (if (zero? i) result + (recur (dec i) (if (= i pivot) (+ result result bias) (+ result i offset)))))] + (doseq [variant [source (forms source) (let-node 'unused (number-node 999) source)]] + (is (= expected (execute (flow/compile-program variant))) + (pr-str {:seed seed :index index :input [n initial pivot bias offset]}))))))) diff --git a/test/com/tailrecursion/nanopass/language_test.clj b/test/com/tailrecursion/nanopass/language_test.clj new file mode 100644 index 0000000..4430265 --- /dev/null +++ b/test/com/tailrecursion/nanopass/language_test.clj @@ -0,0 +1,55 @@ +(ns com.tailrecursion.nanopass.language-test + "Derivation, syntax rejection, and terminal opacity independent of the checker." + (:require [clojure.test :refer [deftest is]] + [com.tailrecursion.nanopass.language :as l] + [com.tailrecursion.nanopass.fixtures :as f])) + +(defn failure [f] + (try (f) nil (catch clojure.lang.ExceptionInfo e (:kind (ex-data e))))) + +(deftest derivation + (is (not (contains? (loaded-libs) 'typed.clojure))) + (is (= #{:literal :read :assign :group :many} + (set (map l/tag (l/alternatives (get-in f/Source [:types 'Expr])))))) + (is (= #{:literal :read :write :group :many} + (set (map l/tag (l/alternatives (get-in f/Core [:types 'Expr])))))) + (is (= 'com.tailrecursion.nanopass.fixtures/Core-Expr (l/type-name f/Core 'Expr))) + (let [types (l/type-forms [f/Core]) + symbols (set (filter symbol? (tree-seq coll? seq types)))] + (is (symbols 'com.tailrecursion.nanopass.fixtures/Core-Binding)) + (is (not (symbols 'com.tailrecursion.nanopass.fixtures/Source-Expr)))) + (is (= '(quote Expr) (l/instantiate f/Core '(quote Expr)))) + (is (= '(typed.clojure/Val Expr) (l/instantiate f/Core '(typed.clojure/Val Expr)))) + (is (= '(typed.clojure/Val {:symbol Expr}) + (l/qualify *ns* '#{Expr} '(clojure.core.typed/Val {:symbol Expr})))) + (is (= '(typed.clojure/HMap :mandatory {:op (quote :binding) :name typed.clojure/Sym + :value com.tailrecursion.nanopass.fixtures/Literals-Expr} + :complete? true) + (l/instantiate f/Literals (get-in f/Literals [:types 'Binding]))))) + +(deftest invalid-grammars + (doseq [[expected form] + '[[:invalid-hmap (typed.clojure/HMap :mandatory {:op ':a})] + [:invalid-hmap (typed.clojure/HMap :mandatory {:x Expr} :optional {:x Expr} :complete? true)] + [:invalid-tag (typed.clojure/HMap :mandatory {:op typed.clojure/Keyword} :complete? true)] + [:duplicate-tag (typed.clojure/U (typed.clojure/HMap :mandatory {:op ':a} :complete? true) + (typed.clojure/HMap :mandatory {:op ':a} :complete? true))] + [:duplicate-tag (typed.clojure/U (typed.clojure/HMap :mandatory {:op ':a} :complete? true) + (typed.clojure/U (typed.clojure/HMap :mandatory {:op ':a} :complete? true)))] + [:invalid-tuple (typed.clojure/HVec [Expr] :rest Expr)] + [:invalid-type (typed.clojure/Map Expr)] + [:unsupported-recursion (typed.clojure/IFn [Expr :-> Expr])]]] + (is (= expected (failure #(l/language 'example/L {'Expr form}))) (pr-str form))) + (is (= :unknown-type (failure #(l/qualify *ns* '#{Expr} 'Missing)))) + (is (= :unsupported-type (failure #(l/qualify *ns* '#{Expr} '(quote [Expr]))))) + (is (= :unsupported-type (failure #(l/qualify *ns* '#{Expr} '(quote {:value Expr}))))) + (is (= :unknown-production + (failure #(l/derive 'example/L f/Source '{:remove {Expr #{:absent}}}))))) + +(deftest extension-validation + (doseq [[expected changes] + [[:invalid-extension '{:remove {Expr #{:read}} :replace {Expr typed.clojure/Any}}] + [:unknown-nonterminal '{:remove {Missing #{:read}}}] + [:invalid-extension '{:extra {}}] + [:duplicate-tag {:add {'Expr [(first (l/alternatives (get-in f/Source [:types 'Expr]))) ]}}]]] + (is (= expected (failure #(l/derive 'example/L f/Source changes)))))) diff --git a/test/com/tailrecursion/nanopass/pass_test.clj b/test/com/tailrecursion/nanopass/pass_test.clj new file mode 100644 index 0000000..56d9de2 --- /dev/null +++ b/test/com/tailrecursion/nanopass/pass_test.clj @@ -0,0 +1,64 @@ +(ns com.tailrecursion.nanopass.pass-test + "Generated traversal must preserve structure, terminal data, and context." + (:require [clojure.test :refer [deftest is]] + [com.tailrecursion.nanopass :as np] + [com.tailrecursion.nanopass.passes :as p])) + +(deftest recursive-traversal + (let [literal {:op :literal :value '{:op :assign :name x}} + input (with-meta {:op :many :items {:a [literal {:op :read :name 'x}]} + :pair [literal {:op :read :name 'x}] + :unique #{{:op :read :name 'x}} :note "preserved"} {:line 42}) + renamed (p/rename {'x 'y} (p/lower input))] + (is (= {:line 42} (meta renamed))) + (is (= "preserved" (:note renamed))) + (is (= literal (first (:pair renamed)))) + (is (= 'y (get-in renamed [:items :a 1 :name]))) + (is (= 'y (get-in renamed [:pair 1 :name]))) + (is (= #{{:op :read :name 'y}} (:unique renamed))) + (is (= [8 renamed] (p/count-step 7 renamed))) + (is (= {:op :group :bindings [{:op :binding :name 'x + :value {:op :write :name 'x :value literal}}] + :body literal} + (p/lower {:op :group :bindings [{:op :binding :name 'x + :value {:op :assign :binding {:op :binding :name 'x :value literal} + :value literal}}] + :body literal})))) + (let [value {:op :many :items {} :pair [{:op :literal :value nil} nil] :unique #{}}] + (is (= value (p/lower value))) + (is (not (contains? (p/lower value) :note))) + (let [leaf {:op :literal :value nil} + assignment {:op :assign :binding {:op :binding :name 'x :value leaf} :value leaf} + output (p/lower (assoc value :child assignment :maybe nil))] + (is (= {:op :write :name 'x :value leaf} (:child output))) + (is (contains? output :maybe)) + (is (nil? (:maybe output))) + (is (= :write (:op (:maybe (p/lower (assoc value :maybe assignment))))))))) + +(deftest invalid-pass-definitions + (doseq [[kind form] + '[[:missing-clause (np/defpass broken {:from [f/Source Expr] :to [f/Core Expr] + :processors {Binding lower-binding}} [node])] + [:missing-processor (np/defpass broken {:from [f/Core Binding] :to [f/Core Binding]} [node])] + [:unknown-production (np/defpass broken {:from [f/Core Binding] :to [f/Core Binding]} + [node] :absent node)] + [:invalid-pass (np/defpass broken {:from [f/Core Expr] :to [f/Core Expr]} + [node] :read node :read node)] + [:invalid-pass (np/defpass broken {:from [f/Core Expr] :to [f/Core Expr] :body true} + [node] node node)] + [:invalid-pass (np/defpass broken {:from [f/Core Expr] :to [f/Core Expr] :body :yes} + [node] node)] + [:invalid-pass (np/defpass broken {:from [f/Core Expr]} [node] node)]]] + (is (= kind (binding [*ns* (the-ns 'com.tailrecursion.nanopass.passes)] + (try (macroexpand-1 form) nil + (catch Exception e (-> (or (ex-cause e) e) ex-data :kind))))) + (pr-str form)))) + +(deftest optional-records + (is (= {} (p/lower-envelope {}))) + (is (= {:maybe nil} (p/lower-envelope {:maybe nil}))) + (let [leaf {:op :literal :value 1} + assignment {:op :assign :binding {:op :binding :name 'x :value leaf} :value leaf}] + (is (= {:child {:op :write :name 'x :value leaf}} + (p/lower-envelope {:child assignment})))) + (is (= {:line 17} (meta (p/lower-envelope (with-meta {} {:line 17})))))) diff --git a/test/com/tailrecursion/nanopass/passes.clj b/test/com/tailrecursion/nanopass/passes.clj new file mode 100644 index 0000000..e6eebf8 --- /dev/null +++ b/test/com/tailrecursion/nanopass/passes.clj @@ -0,0 +1,40 @@ +(ns ^:typed.clojure com.tailrecursion.nanopass.passes + "Real generated pass bodies: mutual recursion, contexts, and explicit results." + (:require [com.tailrecursion.nanopass :as np] + [com.tailrecursion.nanopass.fixtures :as f] + [typed.clojure :as-alias t])) + +(declare lower lower-binding) + +(np/defpass lower + {:from [f/Source Expr] :to [f/Core Expr] :processors {Binding lower-binding}} + [node] + :assign {:op :write :name (:name (:binding node)) :value (lower (:value node))}) + +(np/defpass lower-binding {:from [f/Source Binding] :to [f/Core Binding] + :processors {Expr lower}} + [node]) + +(declare rename rename-binding) + +(np/defpass rename + {:from [f/Core Expr] :to [f/Core Expr] :args [(t/Map t/Sym t/Sym)] + :processors {Binding rename-binding}} + [env node] + :read {:op :read :name (get env (:name node) (:name node))} + :write {:op :write :name (get env (:name node) (:name node)) :value (rename env (:value node))}) + +(np/defpass rename-binding + {:from [f/Core Binding] :to [f/Core Binding] :args [(t/Map t/Sym t/Sym)] + :processors {Expr rename}} + [env node] + :binding {:op :binding :name (get env (:name node) (:name node)) :value (rename env (:value node))}) + +(np/defpass count-step + {:from [f/Core Expr] :to (t/HVec [t/Int f/Core-Expr]) :args [t/Int] :body true} + [state node] + [(inc state) node]) + +(np/defpass lower-envelope + {:from [f/Source Envelope] :to [f/Core Envelope] :processors {Expr lower}} + [node]) diff --git a/test/com/tailrecursion/test_runner.clj b/test/com/tailrecursion/test_runner.clj new file mode 100644 index 0000000..879c48d --- /dev/null +++ b/test/com/tailrecursion/test_runner.clj @@ -0,0 +1,37 @@ +(ns com.tailrecursion.test-runner + "Run semantic and generative suites; fail the process on any failing check." + (:require [clojure.test :as test])) + +(def suites + '[com.tailrecursion.nanopass.language-test + com.tailrecursion.nanopass.pass-test + com.tailrecursion.nanopass.assignment-test + com.tailrecursion.nanopass.flow-test + com.tailrecursion.blacktide.machine-test + com.tailrecursion.blacktide.backend-test + com.tailrecursion.blacktide.generated-test + com.tailrecursion.blacktide.aot-test + com.tailrecursion.blacktide.package-test + com.tailrecursion.blacktide.graph-test + com.tailrecursion.blacktide.source-test + com.tailrecursion.blacktide.subset-test + com.tailrecursion.blacktide.collections-test + com.tailrecursion.blacktide.assignment-test + com.tailrecursion.blacktide.control-test + com.tailrecursion.blacktide.jumps-test + com.tailrecursion.blacktide.jvm-test + com.tailrecursion.blacktide.loops-test + com.tailrecursion.blacktide.source-loops-test + com.tailrecursion.blacktide.source-values-test + com.tailrecursion.blacktide.case-test + com.tailrecursion.blacktide.lookup-test + com.tailrecursion.blacktide.invocation-test + com.tailrecursion.blacktide.native-sites-test + com.tailrecursion.blacktide.algorithms-test]) + +(defn -main [& selected] + (let [suites (if (seq selected) (mapv symbol selected) suites)] + (doseq [suite suites] (require suite)) + (let [{:keys [fail error]} (apply test/run-tests suites)] + (shutdown-agents) + (when (pos? (+ fail error)) (System/exit 1)))))