Skip to content

Commit fc762b1

Browse files
Dangercoderdmiller
authored andcommitted
Add .NET 11 runtime async/await interop
Adds first-class async/await support for ClojureCLR targeting .NET 11's runtime async feature. The compiler emits MethodImplAttributes.Async (0x2000) and calls AsyncHelpers.Await — the runtime handles suspension, resumption, and local hoisting. No state machine generation needed. Compiler changes: - New AwaitExpr AST node with await* special form - ^:async metadata on defn sets the async flag on generated methods - FnMethod emits invokeAsync() with 0x2000 flag, invoke() delegates to it - Compile-time validation: await outside ^:async fn, await in catch/finally - ^:async + primitive type hints rejected at compile time Async interface support for gen-class and proxy: - Methods returning Task/Task<T>/ValueTask/ValueTask<T> get 0x2000 flag - Stubs await the IFn result and cast to the interface return type - Enables implementing IAsyncDisposable, custom async interfaces, etc. New Clojure namespace clojure.async.task: - t/await — macro expanding to await* special form - t/async — immediately-invoked async lambda block - t/await-all — parallel await via Task.WhenAll - t/->task, t/task? — helpers All async code is gated behind #if NET11_0_OR_GREATER. Existing targets (net6.0 through net10.0, net462, net481) are unaffected. Includes 14 NUnit tests covering: basic async defn, args, closures across await, chaining, void Task, async blocks, Task<T>, compile errors (outside async, in catch, prim hints), C# interop, deftype async interface.
1 parent 758c5a6 commit fc762b1

File tree

13 files changed

+844
-29
lines changed

13 files changed

+844
-29
lines changed

Clojure/Clojure.Source/Clojure.Source.csproj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,9 @@
153153
<EmbeddedResource Include="clojure\core\server.clj">
154154
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
155155
</EmbeddedResource>
156+
<EmbeddedResource Include="clojure\async\task.clj">
157+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
158+
</EmbeddedResource>
156159
</ItemGroup>
157160

158161
</Project>
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
(ns clojure.async.task
2+
"Task-based async/await interop for .NET 11+ runtime async.
3+
Requires .NET 11 Preview 1 or later."
4+
(:refer-clojure :exclude [await]))
5+
6+
(defmacro await
7+
"Suspends the current ^:async function until the given Task completes.
8+
Returns the task's result value. Only valid inside a ^:async defn
9+
or an (async ...) block.
10+
11+
Usage:
12+
(t/await (.ReadAllTextAsync System.IO.File path))
13+
(t/await some-task)"
14+
[task-expr]
15+
`(await* ~task-expr))
16+
17+
(defmacro async
18+
"Executes body in an async context, returning a Task<object>.
19+
The body may use (t/await ...) to suspend on Tasks.
20+
21+
Usage:
22+
(t/async
23+
(let [data (t/await (fetch url))]
24+
(process data)))"
25+
[& body]
26+
`((^:async fn* [] ~@body)))
27+
28+
(defmacro await-all
29+
"Awaits multiple tasks in parallel. Returns a vector of results.
30+
31+
Usage:
32+
(let [[a b c] (t/await-all (task1) (task2) (task3))]
33+
...)"
34+
[& task-exprs]
35+
(let [tasks-sym (gensym "tasks")]
36+
`(let [~tasks-sym (into-array System.Threading.Tasks.Task [~@task-exprs])]
37+
(await* (System.Threading.Tasks.Task/WhenAll ~tasks-sym))
38+
(mapv (fn [t#] (.Result ^|System.Threading.Tasks.Task`1[System.Object]| t#)) ~tasks-sym))))
39+
40+
(defn ->task
41+
"Wraps a value in a completed Task<object>.
42+
Uses an immediately-invoked async fn to wrap the value."
43+
[value]
44+
((^:async fn* [] value)))
45+
46+
(defn task?
47+
"Returns true if x is a Task."
48+
[x]
49+
(instance? System.Threading.Tasks.Task x))

Clojure/Clojure.Source/clojure/core.clj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -333,7 +333,7 @@
333333
;;todo - restore propagation of fn name
334334
;;must figure out how to convey primitive hints to self calls first
335335
;;(cons `fn fdecl)
336-
(with-meta (cons `fn fdecl) {:rettag (:tag m)})))))
336+
(with-meta (cons `fn fdecl) (if (:async m) {:rettag (:tag m) :async true} {:rettag (:tag m)}))))))
337337

338338
(. (var defn) (setMacro))
339339

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
/**
2+
* Copyright (c) Rich Hickey. All rights reserved.
3+
* The use and distribution terms for this software are covered by the
4+
* Eclipse Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php)
5+
* which can be found in the file epl-v10.html at the root of this distribution.
6+
* By using this software in any fashion, you are agreeing to be bound by
7+
* the terms of this license.
8+
* You must not remove this notice, or any other, from this software.
9+
**/
10+
11+
#if NET11_0_OR_GREATER
12+
13+
using System;
14+
using System.Reflection;
15+
using System.Reflection.Emit;
16+
using System.Threading.Tasks;
17+
using clojure.lang.CljCompiler.Context;
18+
19+
namespace clojure.lang.CljCompiler.Ast
20+
{
21+
public class AwaitExpr : Expr
22+
{
23+
#region Data
24+
25+
readonly Expr _taskExpr;
26+
public Expr TaskExpr => _taskExpr;
27+
28+
readonly Type _resultType;
29+
readonly MethodInfo _awaitMethod;
30+
31+
#endregion
32+
33+
#region Ctors
34+
35+
public AwaitExpr(Expr taskExpr, Type resultType, MethodInfo awaitMethod)
36+
{
37+
_taskExpr = taskExpr;
38+
_resultType = resultType;
39+
_awaitMethod = awaitMethod;
40+
}
41+
42+
#endregion
43+
44+
#region Type mangling
45+
46+
public bool HasClrType => true;
47+
48+
public Type ClrType => _resultType == typeof(void) ? typeof(object) : _resultType;
49+
50+
#endregion
51+
52+
#region Parsing
53+
54+
public sealed class Parser : IParser
55+
{
56+
public Expr Parse(ParserContext pcon, object frm)
57+
{
58+
ISeq form = (ISeq)frm;
59+
60+
if (!Compiler.RuntimeAsyncAvailable)
61+
throw new ParseException(
62+
"(await* ...) requires .NET 11+ with runtime async support");
63+
64+
if (!Compiler.AsyncMethodCache.IsSupported)
65+
throw new ParseException(
66+
"(await* ...) requires System.Runtime.CompilerServices.AsyncHelpers");
67+
68+
if (RT.count(form) != 2)
69+
throw new ParseException(
70+
"Wrong number of arguments to await*, expected: (await* expr)");
71+
72+
ObjMethod method = (ObjMethod)Compiler.MethodVar.deref();
73+
74+
if (method == null)
75+
throw new ParseException(
76+
"(await* ...) must appear inside a function body");
77+
78+
if (Compiler.InCatchFinallyVar.deref() is not null)
79+
throw new ParseException(
80+
"(await* ...) cannot appear inside a catch, finally, or fault handler");
81+
82+
if (!method.IsAsync)
83+
throw new ParseException(
84+
"(await* ...) can only be used inside a ^:async function or (async ...) block");
85+
86+
if (pcon.Rhc == RHC.Eval)
87+
return Compiler.Analyze(pcon,
88+
RT.list(RT.list(Compiler.FnOnceSym, PersistentVector.EMPTY, form)),
89+
"await__" + RT.nextID());
90+
91+
Expr taskExpr = Compiler.Analyze(
92+
pcon.SetRhc(RHC.Expression).SetAssign(false),
93+
RT.second(form));
94+
95+
Type taskType;
96+
if (taskExpr.HasClrType)
97+
{
98+
taskType = taskExpr.ClrType;
99+
100+
bool isTaskType =
101+
taskType == typeof(Task)
102+
|| taskType == typeof(ValueTask)
103+
|| (taskType.IsGenericType &&
104+
(taskType.GetGenericTypeDefinition() == typeof(Task<>)
105+
|| taskType.GetGenericTypeDefinition() == typeof(ValueTask<>)));
106+
107+
if (!isTaskType)
108+
{
109+
if (taskType == typeof(object))
110+
taskType = typeof(Task<object>);
111+
else
112+
throw new ParseException(
113+
$"(await* ...) requires a Task, Task<T>, ValueTask, or ValueTask<T>, got: {taskType.FullName}");
114+
}
115+
}
116+
else
117+
{
118+
taskType = typeof(Task<object>);
119+
}
120+
121+
MethodInfo awaitMethod =
122+
Compiler.AsyncMethodCache.ResolveAwaitMethod(taskType, out Type resultType);
123+
124+
if (awaitMethod == null)
125+
throw new ParseException(
126+
"Failed to resolve AsyncHelpers.Await method for type: " + taskType.FullName);
127+
128+
method.HasAwait = true;
129+
130+
return new AwaitExpr(taskExpr, resultType, awaitMethod);
131+
}
132+
}
133+
134+
#endregion
135+
136+
#region eval
137+
138+
public object Eval()
139+
{
140+
throw new InvalidOperationException("Can't eval await*");
141+
}
142+
143+
#endregion
144+
145+
#region Code generation
146+
147+
public void Emit(RHC rhc, ObjExpr objx, CljILGen ilg)
148+
{
149+
_taskExpr.Emit(RHC.Expression, objx, ilg);
150+
151+
if (_taskExpr.HasClrType && _taskExpr.ClrType == typeof(object))
152+
{
153+
ilg.Emit(OpCodes.Castclass, typeof(Task<object>));
154+
}
155+
156+
ilg.Emit(OpCodes.Call, _awaitMethod);
157+
158+
if (_resultType != typeof(void))
159+
{
160+
if (rhc == RHC.Statement)
161+
ilg.Emit(OpCodes.Pop);
162+
else if (_resultType.IsValueType)
163+
ilg.Emit(OpCodes.Box, _resultType);
164+
}
165+
else
166+
{
167+
if (rhc != RHC.Statement)
168+
ilg.Emit(OpCodes.Ldnull);
169+
}
170+
}
171+
172+
public bool HasNormalExit() => true;
173+
174+
#endregion
175+
}
176+
}
177+
178+
#endif

Clojure/Clojure/CljCompiler/Ast/FnExpr.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ public class FnExpr(object tag) : ObjExpr(tag)
2323

2424
static readonly Keyword KW_ONCE = Keyword.intern(null, "once");
2525

26+
#if NET11_0_OR_GREATER
27+
static readonly Keyword KW_ASYNC = Keyword.intern(null, "async");
28+
public bool IsAsync { get; private set; }
29+
#endif
30+
2631
FnMethod _variadicMethod = null;
2732
public FnMethod VariadicMethod => _variadicMethod;
2833
bool IsVariadic => _variadicMethod is not null;
@@ -107,7 +112,18 @@ public static Expr Parse(ParserContext pcon, ISeq form, string name)
107112
if (((IMeta)form.first()).meta() is not null)
108113
{
109114
fn.OnceOnly = RT.booleanCast(RT.get(RT.meta(form.first()), KW_ONCE));
115+
#if NET11_0_OR_GREATER
116+
fn.IsAsync = RT.booleanCast(RT.get(RT.meta(form.first()), KW_ASYNC));
117+
#endif
118+
}
119+
120+
#if NET11_0_OR_GREATER
121+
// Also check metadata on the form itself (propagated by defn -> fn macro)
122+
if (!fn.IsAsync && RT.meta(form) is IPersistentMap formMeta)
123+
{
124+
fn.IsAsync = RT.booleanCast(RT.get(formMeta, KW_ASYNC));
110125
}
126+
#endif
111127

112128
fn.ComputeNames(form, name);
113129

0 commit comments

Comments
 (0)