Skip to content

Commit c832f00

Browse files
Dangercoderdmiller
authored andcommitted
Move clojure.async.task to clojure.clr.async.task with expanded API
Replace the macro-heavy clojure.async.task (4 macros) with clojure.clr.async.task — a function-first design following CLR namespace conventions. Only await and async remain as macros. New functions: await-all, await-any, delay-task, run, ->task, completed-task, task?, result. - await-all accepts any Task<T> (not just Task<Object>) with a fast path for Task<Object> via generic WhenAll and a reflection fallback for mixed types like Task<string> - No .Result usage anywhere — uses GetAwaiter().GetResult() for clean exception unwrapping (no AggregateException) - Generic methods resolved via (type-args Object) at compile time 13 tests, 21 assertions. Utility tests run on any .NET version; async tests gated on .NET 11+ runtime async availability.
1 parent 157f84e commit c832f00

File tree

5 files changed

+263
-50
lines changed

5 files changed

+263
-50
lines changed

Clojure/Clojure.Source/Clojure.Source.csproj

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
<None Remove="clojure\clr\basis.cljc" />
1616
<None Remove="clojure\clr\basis\impl.cljc" />
1717
<None Remove="clojure\clr\math.clj" />
18+
<None Remove="clojure\clr\async\task.clj" />
1819
<None Remove="clojure\clr\process.clj" />
1920
<None Remove="clojure\repl\deps.cljc" />
2021
<None Remove="clojure\tools\deps\interop.cljc" />
@@ -153,7 +154,7 @@
153154
<EmbeddedResource Include="clojure\core\server.clj">
154155
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
155156
</EmbeddedResource>
156-
<EmbeddedResource Include="clojure\async\task.clj">
157+
<EmbeddedResource Include="clojure\clr\async\task.clj">
157158
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
158159
</EmbeddedResource>
159160
</ItemGroup>

Clojure/Clojure.Source/clojure/async/task.clj

Lines changed: 0 additions & 49 deletions
This file was deleted.
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
(ns clojure.clr.async.task
2+
"Task-based async/await interop for .NET 11+ runtime async.
3+
Provides idiomatic Clojure wrappers around System.Threading.Tasks.Task."
4+
(:refer-clojure :exclude [await])
5+
(:import [System.Threading.Tasks Task]))
6+
7+
(set! *warn-on-reflection* true)
8+
9+
;; ── Internal helpers ──────────────────────────────────────────────────
10+
11+
(defn- task-result
12+
"Extracts the result from a completed Task<T> via its typed awaiter.
13+
Uses GetAwaiter().GetResult() which unwraps exceptions cleanly
14+
(no AggregateException wrapping unlike .Result).
15+
Returns nil for non-generic Task (void)."
16+
[^Task task]
17+
(let [t (.GetType task)]
18+
(when (.IsGenericType t)
19+
(let [^Type type-arg (aget (.GetGenericArguments t) 0)]
20+
(when-not (= "VoidTaskResult" (.Name type-arg))
21+
(let [awaiter (.Invoke (.GetMethod t "GetAwaiter" Type/EmptyTypes) task nil)]
22+
(.Invoke (.GetMethod (.GetType ^Object awaiter) "GetResult" Type/EmptyTypes)
23+
awaiter nil)))))))
24+
25+
;; ── Macros (only these two require macro status) ──────────────────────
26+
27+
(defmacro await
28+
"Suspends the current ^:async function until the given Task completes.
29+
Returns the task's result value. Only valid inside a ^:async defn
30+
or an (async ...) block.
31+
32+
Usage:
33+
(require '[clojure.clr.async.task :as t])
34+
(t/await (.ReadAllTextAsync System.IO.File path))"
35+
[task-expr]
36+
`(await* ~task-expr))
37+
38+
(defmacro async
39+
"Executes body in an async context, returning Task<object>.
40+
The body may use (await ...) to suspend on Tasks.
41+
42+
Usage:
43+
(t/async
44+
(let [data (t/await (fetch url))]
45+
(process data)))"
46+
[& body]
47+
`((^:async fn* [] ~@body)))
48+
49+
;; ── Functions ─────────────────────────────────────────────────────────
50+
51+
(def ^:private task-object-type |System.Threading.Tasks.Task`1[System.Object]|)
52+
53+
(defn ^:async await-all
54+
"Awaits all tasks in parallel. Takes a collection of any Task types
55+
(Task<string>, Task<int>, Task<Object>, etc.).
56+
Returns an Object[] of results. Must be called in an async context.
57+
58+
Usage:
59+
(let [results (t/await (t/await-all [task-a task-b task-c]))]
60+
(vec results))"
61+
[tasks]
62+
(let [^|System.Threading.Tasks.Task[]| task-array (into-array Task tasks)]
63+
(if (every? #(instance? task-object-type %) task-array)
64+
;; Fast path: all Task<Object> — use generic WhenAll, no per-element reflection
65+
(let [^|System.Threading.Tasks.Task`1[System.Object][]| obj-array
66+
(into-array task-object-type tasks)]
67+
(await* (Task/WhenAll (type-args Object) obj-array)))
68+
;; Slow path: mixed types — await non-generic, extract results via reflection
69+
(do
70+
(await* (Task/WhenAll task-array))
71+
(into-array Object (map task-result task-array))))))
72+
73+
(defn ^:async await-any
74+
"Awaits the first task to complete from a collection of any Task types.
75+
Returns the first completed Task (not its result).
76+
Must be called in an async context.
77+
78+
Usage:
79+
(let [winner (t/await (t/await-any [fast-task slow-task]))]
80+
(t/result winner))"
81+
[tasks]
82+
(let [^|System.Threading.Tasks.Task[]| task-array (into-array Task tasks)]
83+
(await* (Task/WhenAny task-array))))
84+
85+
(defn delay-task
86+
"Returns a Task that completes after the specified duration in milliseconds.
87+
For TimeSpan, use (Task/Delay timespan) directly.
88+
89+
Usage:
90+
(t/await (t/delay-task 1000))"
91+
[milliseconds]
92+
(Task/Delay (int milliseconds)))
93+
94+
(defn run
95+
"Runs f (zero-arg fn) on the thread pool. Returns Task<object>.
96+
97+
Usage:
98+
(t/await (t/run (fn [] (+ 1 2 3))))"
99+
[f]
100+
(let [func (gen-delegate |System.Func`1[System.Object]| [] (f))]
101+
(Task/Run func)))
102+
103+
(defn ->task
104+
"Wraps a value in a completed Task<object>.
105+
106+
Usage:
107+
(t/->task 42) ;=> completed Task whose result is 42"
108+
[value]
109+
(Task/FromResult (type-args Object) value))
110+
111+
(defn completed-task
112+
"Returns a cached, already-completed void Task."
113+
[]
114+
Task/CompletedTask)
115+
116+
(defn task?
117+
"Returns true if x is a Task."
118+
[x]
119+
(instance? Task x))
120+
121+
(defn result
122+
"Blocks the calling thread until the task completes and returns its result.
123+
For Task<T>, returns T. For void Task, returns nil.
124+
Unwraps AggregateException to throw the inner exception directly.
125+
126+
Usage:
127+
(t/result (t/->task 42)) ;=> 42
128+
(t/result (t/completed-task)) ;=> nil
129+
(t/result (t/async (t/await (t/delay-task 100)) \"done\")) ;=> \"done\""
130+
[^Task task]
131+
;; Block until complete. Non-generic GetResult() handles void tasks
132+
;; and throws inner exception (not AggregateException) on fault.
133+
(-> task .GetAwaiter .GetResult)
134+
;; For Task<T>, extract the typed result via the generic awaiter.
135+
(task-result task))

Clojure/Clojure.Tests/Clojure.Tests.csproj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,9 @@
5757
<None Update="clojure\test_clojure\clojure_zip.clj">
5858
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
5959
</None>
60+
<None Update="clojure\test_clojure\clr_async_task.clj">
61+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
62+
</None>
6063
<None Update="clojure\test_clojure\clr\added.clj">
6164
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
6265
</None>
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
(ns clojure.test-clojure.clr-async-task
2+
(:require [clojure.test :refer [deftest is testing]]
3+
[clojure.clr.async.task :as t])
4+
(:import [System.Threading.Tasks Task]))
5+
6+
(set! *warn-on-reflection* true)
7+
8+
(def ^:private async-supported?
9+
(some? (Type/GetType "System.Runtime.CompilerServices.AsyncHelpers, System.Runtime")))
10+
11+
;; ── Utility function tests (work on any .NET version) ─────────────────
12+
13+
(deftest to-task-wraps-value
14+
(testing "->task returns an already-completed Task<object>"
15+
(let [^Task task (t/->task 42)]
16+
(is (t/task? task))
17+
(is (.IsCompleted task))
18+
(is (= 42 (t/result task))))))
19+
20+
(deftest completed-task-is-completed
21+
(testing "completed-task returns a completed Task"
22+
(let [^Task task (t/completed-task)]
23+
(is (t/task? task))
24+
(is (.IsCompleted task)))))
25+
26+
(deftest task-predicate
27+
(testing "task? returns true for tasks, false otherwise"
28+
(is (t/task? (t/->task 1)))
29+
(is (t/task? (t/completed-task)))
30+
(is (not (t/task? 42)))
31+
(is (not (t/task? "hello")))))
32+
33+
(deftest result-on-completed-task
34+
(testing "result returns value from completed Task<object>"
35+
(is (= 42 (t/result (t/->task 42)))))
36+
(testing "result returns nil from void completed-task"
37+
(is (nil? (t/result (t/completed-task))))))
38+
39+
(deftest delay-task-returns-task
40+
(testing "delay-task returns a Task"
41+
(is (t/task? (t/delay-task 1)))))
42+
43+
(deftest run-returns-task
44+
(testing "run returns a Task with the fn result"
45+
(let [task (t/run (fn [] (+ 1 2 3)))]
46+
(is (t/task? task))
47+
(is (= 6 (t/result task))))))
48+
49+
;; ── Async tests (require .NET 11+ runtime async) ─────────────────────
50+
51+
(deftest await-all-returns-results
52+
(when async-supported?
53+
(testing "await-all returns array of results from multiple tasks"
54+
(let [result (t/result
55+
(t/async
56+
(vec (t/await (t/await-all
57+
[(t/->task "a")
58+
(t/->task "b")
59+
(t/->task "c")])))))]
60+
(is (= ["a" "b" "c"] result))))))
61+
62+
(deftest await-all-with-async-work
63+
(when async-supported?
64+
(testing "await-all with tasks that actually do work"
65+
(let [result (t/result
66+
(t/async
67+
(let [tasks [(t/run (fn [] (System.Threading.Thread/Sleep 50) "a"))
68+
(t/run (fn [] (System.Threading.Thread/Sleep 50) "b"))
69+
(t/run (fn [] (System.Threading.Thread/Sleep 50) "c"))]
70+
results (t/await (t/await-all tasks))]
71+
(vec results))))]
72+
(is (= ["a" "b" "c"] result))))))
73+
74+
(deftest await-any-returns-first
75+
(when async-supported?
76+
(testing "await-any returns the first completed task"
77+
(let [result (t/result
78+
(t/async
79+
(let [fast (t/->task "fast")
80+
slow (t/delay-task 5000)
81+
^Task winner (t/await (t/await-any [fast slow]))]
82+
(t/result winner))))]
83+
(is (= "fast" result))))))
84+
85+
(deftest delay-task-waits
86+
(when async-supported?
87+
(testing "delay-task creates a delay"
88+
(let [result (t/result
89+
(t/async
90+
(let [start (System.DateTime/UtcNow)]
91+
(t/await (t/delay-task 100))
92+
(let [elapsed (.TotalMilliseconds
93+
(.Subtract (System.DateTime/UtcNow) start))]
94+
(>= elapsed 90)))))]
95+
(is (true? result))))))
96+
97+
(deftest run-offloads-to-threadpool
98+
(when async-supported?
99+
(testing "run executes fn on thread pool and returns result"
100+
(let [result (t/result
101+
(t/async
102+
(t/await (t/run (fn [] (+ 1 2 3))))))]
103+
(is (= 6 result))))))
104+
105+
(deftest await-all-mixed-task-types
106+
(when async-supported?
107+
(testing "await-all works with Task<string> and other Task<T> types"
108+
(let [result (t/result
109+
(t/async
110+
(vec (t/await (t/await-all
111+
[(Task/FromResult (type-args String) "a")
112+
(t/->task "b")
113+
(Task/FromResult (type-args String) "c")])))))]
114+
(is (= ["a" "b" "c"] result))))))
115+
116+
(deftest result-blocks-until-complete
117+
(when async-supported?
118+
(testing "result blocks until async work completes"
119+
(is (= "done"
120+
(t/result
121+
(t/async
122+
(t/await (t/delay-task 50))
123+
"done")))))))

0 commit comments

Comments
 (0)