Skip to content

Commit 4336237

Browse files
authored
C#: use nullable types for non-nested options (#977)
We now generate bindings which use nullable types (e.g. `uint?`) to represent non-nested `option` types. Nested `option`s, such as `option<option<T>>` still use the `Option` class except for the innermost `option` in order to avoid ambiguity. Fixes #964 Signed-off-by: Joel Dice <joel.dice@fermyon.com>
1 parent 86d568a commit 4336237

File tree

6 files changed

+135
-25
lines changed

6 files changed

+135
-25
lines changed

crates/csharp/src/lib.rs

Lines changed: 46 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1298,16 +1298,16 @@ impl InterfaceGenerator<'_> {
12981298
}
12991299
TypeDefKind::Option(base_ty) => {
13001300
self.gen.needs_option = true;
1301-
if let Some(_name) = &ty.name {
1302-
format!(
1303-
"Option<{}>",
1304-
self.type_name_with_qualifier(base_ty, qualifier)
1305-
)
1301+
let nesting = if let Type::Id(id) = base_ty {
1302+
matches!(&self.resolve.types[*id].kind, TypeDefKind::Option(_))
13061303
} else {
1307-
format!(
1308-
"Option<{}>",
1309-
self.type_name_with_qualifier(base_ty, qualifier)
1310-
)
1304+
false
1305+
};
1306+
let base_ty = self.type_name_with_qualifier(base_ty, qualifier);
1307+
if nesting {
1308+
format!("Option<{base_ty}>")
1309+
} else {
1310+
format!("{base_ty}?")
13111311
}
13121312
}
13131313
TypeDefKind::Result(result) => {
@@ -2300,9 +2300,20 @@ impl Bindgen for FunctionBindgen<'_, '_> {
23002300

23012301
let op = &operands[0];
23022302

2303-
let block = |ty: Option<&Type>, Block { body, results, .. }, payload| {
2304-
let payload = if let Some(_ty) = self.gen.non_empty_type(ty) {
2305-
format!("var {payload} = {op}.Value;")
2303+
let nesting = if let Type::Id(id) = payload {
2304+
matches!(&self.gen.resolve.types[*id].kind, TypeDefKind::Option(_))
2305+
} else {
2306+
false
2307+
};
2308+
2309+
let mut block = |ty: Option<&Type>, Block { body, results, .. }, payload, nesting| {
2310+
let payload = if let Some(ty) = self.gen.non_empty_type(ty) {
2311+
let ty = self.gen.type_name_with_qualifier(ty, true);
2312+
if nesting {
2313+
format!("var {payload} = {op}.Value;")
2314+
} else {
2315+
format!("var {payload} = ({ty}) {op};")
2316+
}
23062317
} else {
23072318
String::new()
23082319
};
@@ -2321,15 +2332,21 @@ impl Bindgen for FunctionBindgen<'_, '_> {
23212332
)
23222333
};
23232334

2324-
let none = block(None, none, none_payload);
2325-
let some = block(Some(payload), some, some_payload);
2335+
let none = block(None, none, none_payload, nesting);
2336+
let some = block(Some(payload), some, some_payload, nesting);
2337+
2338+
let test = if nesting {
2339+
".HasValue"
2340+
} else {
2341+
" != null"
2342+
};
23262343

23272344
uwrite!(
23282345
self.src,
23292346
r#"
23302347
{declarations}
23312348
2332-
if ({op}.HasValue) {{
2349+
if ({op}{test}) {{
23332350
{some}
23342351
}} else {{
23352352
{none}
@@ -2346,6 +2363,12 @@ impl Bindgen for FunctionBindgen<'_, '_> {
23462363
let lifted = self.locals.tmp("lifted");
23472364
let op = &operands[0];
23482365

2366+
let nesting = if let Type::Id(id) = payload {
2367+
matches!(&self.gen.resolve.types[*id].kind, TypeDefKind::Option(_))
2368+
} else {
2369+
false
2370+
};
2371+
23492372
let payload = if self.gen.non_empty_type(Some(*payload)).is_some() {
23502373
some.results.into_iter().next().unwrap()
23512374
} else {
@@ -2354,20 +2377,26 @@ impl Bindgen for FunctionBindgen<'_, '_> {
23542377

23552378
let some = some.body;
23562379

2380+
let (none_value, some_value) = if nesting {
2381+
(format!("{ty}.None"), format!("new ({payload})"))
2382+
} else {
2383+
("null".into(), payload)
2384+
};
2385+
23572386
uwrite!(
23582387
self.src,
23592388
r#"
23602389
{ty} {lifted};
23612390
23622391
switch ({op}) {{
23632392
case 0: {{
2364-
{lifted} = {ty}.None;
2393+
{lifted} = {none_value};
23652394
break;
23662395
}}
23672396
23682397
case 1: {{
23692398
{some}
2370-
{lifted} = new ({payload});
2399+
{lifted} = {some_value};
23712400
break;
23722401
}}
23732402

tests/runtime/options.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ impl test::options::test::Host for MyImports {
2828
fn option_roundtrip(&mut self, a: Option<String>) -> Result<Option<String>> {
2929
Ok(a)
3030
}
31+
32+
fn double_option_roundtrip(&mut self, a: Option<Option<u32>>) -> Result<Option<Option<u32>>> {
33+
Ok(a)
34+
}
3135
}
3236

3337
#[test]
@@ -54,5 +58,17 @@ fn run_test(exports: Options, store: &mut Store<crate::Wasi<MyImports>>) -> Resu
5458
exports.call_option_roundtrip(&mut *store, Some("foo"))?,
5559
Some("foo".to_string())
5660
);
61+
assert_eq!(
62+
exports.call_double_option_roundtrip(&mut *store, Some(Some(42)))?,
63+
Some(Some(42))
64+
);
65+
assert_eq!(
66+
exports.call_double_option_roundtrip(&mut *store, Some(None))?,
67+
Some(None)
68+
);
69+
assert_eq!(
70+
exports.call_double_option_roundtrip(&mut *store, None)?,
71+
None
72+
);
5773
Ok(())
5874
}

tests/runtime/options/wasm.cs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
using System.Diagnostics;
2+
using OptionsWorld.wit.imports.test.options;
3+
4+
namespace OptionsWorld.wit.exports.test.options
5+
{
6+
public class TestImpl : ITest
7+
{
8+
public static void OptionNoneParam(string? a)
9+
{
10+
Debug.Assert(a == null);
11+
}
12+
13+
public static string? OptionNoneResult()
14+
{
15+
return null;
16+
}
17+
18+
public static void OptionSomeParam(string? a)
19+
{
20+
Debug.Assert(a == "foo");
21+
}
22+
23+
public static string? OptionSomeResult()
24+
{
25+
return "foo";
26+
}
27+
28+
public static string? OptionRoundtrip(string? a)
29+
{
30+
return a;
31+
}
32+
33+
public static Option<uint?> DoubleOptionRoundtrip(Option<uint?> a)
34+
{
35+
return a;
36+
}
37+
}
38+
}
39+
40+
namespace OptionsWorld
41+
{
42+
public class OptionsWorldImpl : IOptionsWorld
43+
{
44+
public static void TestImports()
45+
{
46+
TestInterop.OptionNoneParam(null);
47+
TestInterop.OptionSomeParam("foo");
48+
Debug.Assert(TestInterop.OptionNoneResult() == null);
49+
Debug.Assert(TestInterop.OptionSomeResult() == "foo");
50+
Debug.Assert(TestInterop.OptionRoundtrip("foo") == "foo");
51+
Debug.Assert(TestInterop.DoubleOptionRoundtrip(new Option<uint?>(42)).Value == 42);
52+
Debug.Assert(TestInterop.DoubleOptionRoundtrip(new Option<uint?>(null)).Value == null);
53+
Debug.Assert(!TestInterop.DoubleOptionRoundtrip(Option<uint?>.None).HasValue);
54+
}
55+
}
56+
}

tests/runtime/options/wasm.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ impl Guest for Component {
1515
assert!(option_none_result().is_none());
1616
assert_eq!(option_some_result(), Some("foo".to_string()));
1717
assert_eq!(option_roundtrip(Some("foo")), Some("foo".to_string()));
18+
assert_eq!(double_option_roundtrip(Some(Some(42))), Some(Some(42)));
19+
assert_eq!(double_option_roundtrip(Some(None)), Some(None));
20+
assert_eq!(double_option_roundtrip(None), None);
1821
}
1922
}
2023

@@ -38,4 +41,8 @@ impl exports::test::options::test::Guest for Component {
3841
fn option_roundtrip(a: Option<String>) -> Option<String> {
3942
a
4043
}
44+
45+
fn double_option_roundtrip(a: Option<Option<u32>>) -> Option<Option<u32>> {
46+
a
47+
}
4148
}

tests/runtime/options/world.wit

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ interface test {
77
option-some-result: func() -> option<string>;
88

99
option-roundtrip: func(a: option<string>) -> option<string>;
10+
11+
double-option-roundtrip: func(a: option<option<u32>>) -> option<option<u32>>;
1012
}
1113

1214
world options {

tests/runtime/resource_aggregates/wasm.cs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ public static uint Foo(
2222
ITest.V2 v2,
2323
List<ITest.Thing> l1,
2424
List<ITest.Thing> l2,
25-
Option<ITest.Thing> o1,
26-
Option<ITest.Thing> o2,
25+
ITest.Thing? o1,
26+
ITest.Thing? o2,
2727
Result<ITest.Thing, None> result1,
2828
Result<ITest.Thing, None> result2
2929
)
@@ -45,12 +45,12 @@ public static uint Foo(
4545
{
4646
il2.Add(((Thing) thing).val);
4747
}
48-
var io1 = o1.HasValue
49-
? new Option<Import.Thing>(((Thing) o1.Value).val)
50-
: Option<Import.Thing>.None;
51-
var io2 = o2.HasValue
52-
? new Option<Import.Thing>(((Thing) o2.Value).val)
53-
: Option<Import.Thing>.None;
48+
var io1 = o1 != null
49+
? ((Thing) o1).val
50+
: null;
51+
var io2 = o2 != null
52+
? ((Thing) o2).val
53+
: null;
5454
var iresult1 = result1.IsOk
5555
? Result<Import.Thing, None>.ok(((Thing) result1.AsOk).val)
5656
: Result<Import.Thing, None>.err(new None());

0 commit comments

Comments
 (0)