Skip to content

Commit 2df8488

Browse files
committed
CLJCLR-195: Add support for .NET 11 runtime async/await interop
1 parent 302d748 commit 2df8488

File tree

18 files changed

+804
-48
lines changed

18 files changed

+804
-48
lines changed

Clojure/Clojure.Compile/Clojure.Compile.csproj

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
<StartupObject>BootstrapCompile.Compile</StartupObject>
88
<EnableUnsafeBinaryFormatterSerialization>true</EnableUnsafeBinaryFormatterSerialization>
99
<SuppressTfmSupportBuildWarnings>true</SuppressTfmSupportBuildWarnings>
10+
<EnablePreviewFeatures Condition=" '$(TargetFramework)' == 'net11.0'">True</EnablePreviewFeatures>
1011
</PropertyGroup>
1112

1213
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
@@ -37,7 +38,9 @@
3738
<Message Importance="high" Text="TargetCmdLine = '$(TargetCmdLine)'"/>
3839
<Exec Condition=" '$(TargetFramework)' == 'net462' OR '$(TargetFramework)' == 'net481'" Command="$(TargetCmdline) clojure.core clojure.core.protocols clojure.core.server clojure.core.reducers clojure.main clojure.set clojure.zip clojure.walk clojure.stacktrace clojure.template clojure.test clojure.test.tap clojure.test.junit clojure.pprint clojure.clr.io clojure.repl clojure.clr.shell clojure.string clojure.data clojure.reflect clojure.edn clojure.datafy clojure.instant clojure.uuid clojure.math clojure.clr.basis.impl clojure.clr.basis clojure.clr.process clojure.tools.deps.interop clojure.repl.deps" WorkingDirectory="$(OutDir)" />
3940
<Exec Condition=" '$(TargetFramework)' == 'net9.0' OR '$(TargetFramework)' == 'net10.0' OR '$(TargetFramework)' == 'net11.0'" Command="$(TargetCmdline) clojure.core clojure.core.protocols clojure.core.server clojure.core.reducers clojure.main clojure.set clojure.zip clojure.walk clojure.stacktrace clojure.template clojure.test clojure.test.tap clojure.test.junit clojure.pprint clojure.clr.io clojure.repl clojure.clr.shell clojure.string clojure.data clojure.reflect clojure.edn clojure.datafy clojure.instant clojure.uuid clojure.math clojure.clr.basis.impl clojure.clr.basis clojure.clr.process clojure.tools.deps.interop clojure.repl.deps" WorkingDirectory="$(OutDir)" />
40-
<ItemGroup>
41+
<Exec Condition=" '$(TargetFramework)' == 'net11.0'" Command="$(TargetCmdline) clojure.core clojure.core.protocols clojure.core.server clojure.core.reducers clojure.main clojure.set clojure.zip clojure.walk clojure.stacktrace clojure.template clojure.test clojure.test.tap clojure.test.junit clojure.pprint clojure.clr.io clojure.repl clojure.clr.shell clojure.string clojure.data clojure.reflect clojure.edn clojure.datafy clojure.instant clojure.uuid clojure.math clojure.clr.basis.impl clojure.clr.basis clojure.clr.process clojure.tools.deps.interop clojure.repl.deps clojure.clr.async.task" WorkingDirectory="$(OutDir)" />
42+
43+
<ItemGroup>
4144
<CljCoreBin Include="$(ProjectDir)$(OutDir)clojure*.dll" />
4245
</ItemGroup>
4346
<Copy SourceFiles="@(CljCoreBin)" DestinationFolder="$(SolutionDir)Clojure.Main461\$(OutDir)" Condition=" '$(TargetFramework)' == 'net462' "/>

Clojure/Clojure.Main/Clojure.Main.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
<PackAsTool>true</PackAsTool>
99
<EnableUnsafeBinaryFormatterSerialization>true</EnableUnsafeBinaryFormatterSerialization>
1010
<SuppressTfmSupportBuildWarnings>true</SuppressTfmSupportBuildWarnings>
11+
<EnablePreviewFeatures Condition=" '$(TargetFramework)' == 'net11.0'">True</EnablePreviewFeatures>
1112
</PropertyGroup>
1213

1314
<PropertyGroup>

Clojure/Clojure.Source/Clojure.Source.csproj

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
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" />
1918
<None Remove="clojure\clr\process.clj" />
2019
<None Remove="clojure\repl\deps.cljc" />
2120
<None Remove="clojure\tools\deps\interop.cljc" />
@@ -154,6 +153,15 @@
154153
<EmbeddedResource Include="clojure\core\server.clj">
155154
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
156155
</EmbeddedResource>
156+
<EmbeddedResource Update="clojure\clr\async\task.clj">
157+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
158+
</EmbeddedResource>
159+
</ItemGroup>
160+
161+
<ItemGroup>
162+
<None Update="clojure\clr\async\task.clj">
163+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
164+
</None>
157165
</ItemGroup>
158166

159167
</Project>

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

Clojure/Clojure.Tests.Support/Clojure.Tests.Support.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
<TargetFrameworks>net6.0;net8.0;net9.0;net10.0;net11.0;net462;net481</TargetFrameworks>
55
<LangVersion>14.0</LangVersion>
66
<SuppressTfmSupportBuildWarnings>true</SuppressTfmSupportBuildWarnings>
7+
<EnablePreviewFeatures Condition=" '$(TargetFramework)' == 'net11.0'">True</EnablePreviewFeatures>
78
</PropertyGroup>
89

910
<ItemGroup>

Clojure/Clojure.Tests/Clojure.Tests.csproj

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@
77
</PropertyGroup>
88

99
<ItemGroup>
10-
<PackageReference Include="clojure.data.generators"/>
11-
<PackageReference Include="clojure.test.check"/>
12-
<PackageReference Include="clojure.test.generative"/>
13-
<PackageReference Include="clojure.tools.namespace"/>
10+
<PackageReference Include="clojure.data.generators" />
11+
<PackageReference Include="clojure.test.check" />
12+
<PackageReference Include="clojure.test.generative" />
13+
<PackageReference Include="clojure.tools.namespace" />
1414
</ItemGroup>
1515

1616
<ItemGroup>
@@ -57,12 +57,12 @@
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>
6360
<None Update="clojure\test_clojure\clr\added.clj">
6461
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
6562
</None>
63+
<None Update="clojure\test_clojure\clr\async\task.clj">
64+
<CopyToOutputDirectory Condition="'$(TargetFramework)' == 'net11.0'">PreserveNewest</CopyToOutputDirectory>
65+
</None>
6666
<None Update="clojure\test_clojure\clr\interop.clj">
6767
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
6868
</None>
@@ -282,7 +282,7 @@
282282
</ItemGroup>
283283

284284
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0' OR '$(TargetFramework)' == 'net9.0' OR '$(TargetFramework)' == 'net10.0' OR '$(TargetFramework)' == 'net11.0'">
285-
<PackageReference Include="System.Runtime.Serialization.Formatters"/>
285+
<PackageReference Include="System.Runtime.Serialization.Formatters" />
286286
</ItemGroup>
287287

288288

Clojure/Clojure.Tests/clojure/test_clojure/clr_async_task.clj renamed to Clojure/Clojure.Tests/clojure/test_clojure/clr/async/task.clj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
(ns clojure.test-clojure.clr-async-task
1+
(ns clojure.test-clojure.clr.async.task
22
(:require [clojure.test :refer [deftest is testing]]
33
[clojure.clr.async.task :as t])
44
(:import [System.Threading.Tasks Task]))
@@ -120,4 +120,4 @@
120120
(t/result
121121
(t/async
122122
(t/await (t/delay-task 50))
123-
"done")))))))
123+
"done")))))))
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+

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

Clojure/Clojure/CljCompiler/Ast/FnExpr.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ public class FnExpr(object tag) : ObjExpr(tag)
2222
#region Data
2323

2424
static readonly Keyword KW_ONCE = Keyword.intern(null, "once");
25+
static readonly Keyword KW_ASYNC = Keyword.intern(null, "async");
2526

2627
FnMethod _variadicMethod = null;
2728
public FnMethod VariadicMethod => _variadicMethod;
@@ -107,6 +108,13 @@ public static Expr Parse(ParserContext pcon, ISeq form, string name)
107108
if (((IMeta)form.first()).meta() is not null)
108109
{
109110
fn.OnceOnly = RT.booleanCast(RT.get(RT.meta(form.first()), KW_ONCE));
111+
fn.IsAsync = RT.booleanCast(RT.get(RT.meta(form.first()), KW_ASYNC));
112+
}
113+
114+
// Also check metadata on the form itself (propagated by defn -> fn macro)
115+
if (!fn.IsAsync && RT.meta(form) is IPersistentMap formMeta)
116+
{
117+
fn.IsAsync = RT.booleanCast(RT.get(formMeta, KW_ASYNC));
110118
}
111119

112120
fn.ComputeNames(form, name);

0 commit comments

Comments
 (0)