Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 49 additions & 1 deletion crates/bindings-csharp/BSATN.Codegen/Type.cs
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,12 @@ public static TypeUse Parse(ISymbol member, ITypeSymbol typeSymbol, DiagReporter
),
INamedTypeSymbol named => named.OriginalDefinition.ToString() switch
{
"SpacetimeDB.Result<T, E>" or "SpacetimeDB.Result<T,E>" => new ResultUse(
type,
typeInfo,
Parse(member, named.TypeArguments[0], diag),
Parse(member, named.TypeArguments[1], diag)
),
"System.Collections.Generic.List<T>" => new ListUse(
type,
typeInfo,
Expand All @@ -107,6 +113,14 @@ public static TypeUse Parse(ISymbol member, ITypeSymbol typeSymbol, DiagReporter
};
}

/// <summary>
/// Get the name of the BSATN struct for this type.
/// </summary>
public virtual string ToBSATNString()
{
return this.BSATNName;
}

/// <summary>
/// Get a statement that declares outVar and assigns (inVar1 logically-equals inVar2) to it.
/// logically-equals:
Expand Down Expand Up @@ -139,6 +153,40 @@ public abstract string EqualsStatement(
public abstract string GetHashCodeStatement(string inVar, string outVar, int level = 0);
}

/// <summary>
/// A use of a Result&lt;T, E&gt; type.
/// </summary>
public sealed record ResultUse : TypeUse
{
public TypeUse Ok { get; }
public TypeUse Err { get; }

public string TypeName { get; }

public ResultUse(string typeName, string typeInfo, TypeUse ok, TypeUse err)
: base(typeName, typeInfo)
{
Ok = ok;
Err = err;
TypeName = typeName;
}

public override string ToBSATNString()
{
return $"{TypeName}.BSATN<{Ok.BSATNName}, {Err.BSATNName}>";
}

public override string EqualsStatement(
string inVar1,
string inVar2,
string outVar,
int level = 0
) => $"var {outVar} = {inVar1} == {inVar2};";

public override string GetHashCodeStatement(string inVar, string outVar, int level = 0) =>
$"var {outVar} = {inVar}.GetHashCode();";
}

/// <summary>
/// A use of an enum type.
/// (This is a C# enum, not one of our tagged enums.)
Expand Down Expand Up @@ -354,7 +402,7 @@ IEnumerable<MemberDeclaration> members
return string.Join(
"\n ",
members.Select(m =>
$"{visStr} static readonly {m.Type.BSATNName} {m.Name}{TypeUse.BsatnFieldSuffix} = new();"
$"{visStr} static readonly {m.Type.ToBSATNString()} {m.Name}{TypeUse.BsatnFieldSuffix} = new();"
)
);
}
Expand Down
4 changes: 4 additions & 0 deletions crates/bindings-csharp/BSATN.Runtime/BSATN/AlgebraicType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,8 @@ Unit F64
// Special AlgebraicType that can be recognised by the SpacetimeDB `generate` CLI as an Option<T>.
internal static AlgebraicType MakeOption(AlgebraicType someType) =>
new Sum([new("some", someType), new("none", Unit)]);

// Special AlgebraicType that can be recognised by the SpacetimeDB `generate` CLI as a Result<T, E>.
internal static AlgebraicType MakeResult(AlgebraicType okType, AlgebraicType errType) =>
new Sum([new("ok", okType), new("err", errType)]);
}
106 changes: 106 additions & 0 deletions crates/bindings-csharp/BSATN.Runtime/Builtins.cs
Original file line number Diff line number Diff line change
Expand Up @@ -605,3 +605,109 @@ public AlgebraicType GetAlgebraicType(ITypeRegistrar registrar) =>
// --- / customized ---
}
}

public partial record Result<T, E> : TaggedEnum<(T Ok, E Err)>
{
public static implicit operator Result<T, E>(T value) => new OkR(value);

public static implicit operator Result<T, E>(E error) => new ErrR(error);

public TResult Match<TResult>(Func<T, TResult> onOk, Func<E, TResult> onErr) =>
this switch
{
OkR(var v) => onOk(v),
ErrR(var e) => onErr(e),
_ => throw new InvalidOperationException("Unknown Result variant."),
};

public static Result<T, E> Ok(T value) => new OkR(value);

public static Result<T, E> Err(E error) => new ErrR(error);

public T UnwrapOrThrow()
{
return this switch
{
OkR(var v) => v,
ErrR(var e) when e is not null => throw new Exception(e.ToString()),
ErrR(_) => throw new InvalidOperationException(
"Result failed without an error object."
),
_ => throw new InvalidOperationException("Unknown Result variant."),
};
}

public T UnwrapOr(T defaultValue) =>
this switch
{
OkR(var v) => v,
_ => defaultValue,
};

public T UnwrapOrElse(Func<E, T> f) =>
this switch
{
OkR(var v) => v,
ErrR(var e) => f(e),
_ => throw new InvalidOperationException("Unknown Result variant."),
};

// ----- auto-generated -----

private Result() { }

internal enum @enum : byte
{
Ok,
Err,
}

public sealed record OkR(T Value) : Result<T, E>;

public sealed record ErrR(E Error) : Result<T, E>;

private enum Variant : byte
{
Ok = 0,
Err = 1,
}

public readonly struct BSATN<OkRW, ErrRW> : IReadWrite<Result<T, E>>
where OkRW : struct, IReadWrite<T>
where ErrRW : struct, IReadWrite<E>
{
private static readonly SpacetimeDB.BSATN.Enum<@enum> __enumTag = new();
private static readonly OkRW okRW = new();
private static readonly ErrRW errRW = new();

public Result<T, E> Read(BinaryReader reader) =>
__enumTag.Read(reader) switch
{
@enum.Ok => new OkR(okRW.Read(reader)),
@enum.Err => new ErrR(errRW.Read(reader)),
_ => throw new InvalidOperationException(),
};

public void Write(BinaryWriter writer, Result<T, E> value)
{
switch (value)
{
case OkR(var v):
__enumTag.Write(writer, @enum.Ok);
okRW.Write(writer, v);
break;

case ErrR(var e):
__enumTag.Write(writer, @enum.Err);
errRW.Write(writer, e);
break;
}
}

public AlgebraicType GetAlgebraicType(ITypeRegistrar registrar) =>
AlgebraicType.MakeResult(
okRW.GetAlgebraicType(registrar),
errRW.GetAlgebraicType(registrar)
);
}
}
6 changes: 3 additions & 3 deletions crates/bindings-csharp/Codegen/Module.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1178,7 +1178,7 @@ public Scope.Extensions GenerateSchedule()
using var writer = new BinaryWriter(stream);
{{string.Join(
"\n",
Args.Select(a => $"new {a.Type.BSATNName}().Write(writer, {a.Name});")
Args.Select(a => $"new {a.Type.ToBSATNString()}().Write(writer, {a.Name});")
)}}
SpacetimeDB.Internal.IReducer.VolatileNonatomicScheduleImmediate(nameof({{Name}}), stream);
}
Expand Down Expand Up @@ -1295,7 +1295,7 @@ public string GenerateClass()
}
else
{
var serializer = $"new {ReturnType.BSATNName}()";
var serializer = $"new {ReturnType.ToBSATNString()}()";
bodyLines = new[]
{
$"var result = {invocation};",
Expand Down Expand Up @@ -1372,7 +1372,7 @@ public Scope.Extensions GenerateSchedule()
using var writer = new BinaryWriter(stream);
{{string.Join(
"\n",
Args.Select(a => $"new {a.Type.BSATNName}().Write(writer, {a.Name});")
Args.Select(a => $"new {a.Type.ToBSATNString()}().Write(writer, {a.Name});")
)}}
SpacetimeDB.Internal.ProcedureExtensions.VolatileNonatomicScheduleImmediate(nameof({{Name}}), stream);
}
Expand Down
50 changes: 10 additions & 40 deletions crates/bindings-csharp/Runtime/ProcedureContext.cs
Original file line number Diff line number Diff line change
@@ -1,42 +1,8 @@
namespace SpacetimeDB;

using System.Diagnostics.CodeAnalysis;
using Internal;

Check warning on line 4 in crates/bindings-csharp/Runtime/ProcedureContext.cs

View workflow job for this annotation

GitHub Actions / unity-testsuite

Using directive is unnecessary. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0005)

Check warning on line 4 in crates/bindings-csharp/Runtime/ProcedureContext.cs

View workflow job for this annotation

GitHub Actions / csharp-testsuite

Using directive is unnecessary. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0005)

Check warning on line 4 in crates/bindings-csharp/Runtime/ProcedureContext.cs

View workflow job for this annotation

GitHub Actions / Test Suite

Using directive is unnecessary. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0005)

public readonly struct Result<T, E>(bool isSuccess, T? value, E? error)
where E : Exception
{
public bool IsSuccess { get; } = isSuccess;
public T? Value { get; } = value;
public E? Error { get; } = error;

public static Result<T, E> Ok(T value) => new(true, value, null);

public static Result<T, E> Err(E error) => new(false, default, error);

public T UnwrapOrThrow()
{
if (IsSuccess)
{
return Value!;
}

if (Error is not null)
{
throw Error;
}

throw new InvalidOperationException("Result failed without an error object.");
}

public T UnwrapOr(T defaultValue) => IsSuccess ? Value! : defaultValue;

public T UnwrapOrElse(Func<E, T> f) => IsSuccess ? Value! : f(Error!);

public TResult Match<TResult>(Func<T, TResult> onOk, Func<E, TResult> onErr) =>
IsSuccess ? onOk(Value!) : onErr(Error!);
}

#pragma warning disable STDB_UNSTABLE
public abstract class ProcedureContextBase(
Identity sender,
Expand Down Expand Up @@ -122,9 +88,13 @@
try
{
var result = RunWithRetry(body);
return result.IsSuccess
? TxOutcome<TResult>.Success(result.Value!)
: TxOutcome<TResult>.Failure(result.Error!);

return result switch
{
Result<TResult, TError>.OkR(var value) => TxOutcome<TResult>.Success(value),
Result<TResult, TError>.ErrR(var error) => TxOutcome<TResult>.Failure(error),
_ => throw new InvalidOperationException("Unknown Result variant."),
};
}
catch (Exception ex)
{
Expand All @@ -133,14 +103,14 @@
}

// Private transaction management methods (Rust-like encapsulation)
private long StartMutTx()

Check warning on line 106 in crates/bindings-csharp/Runtime/ProcedureContext.cs

View workflow job for this annotation

GitHub Actions / unity-testsuite

Member 'StartMutTx' does not access instance data and can be marked as static (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1822)

Check warning on line 106 in crates/bindings-csharp/Runtime/ProcedureContext.cs

View workflow job for this annotation

GitHub Actions / csharp-testsuite

Member 'StartMutTx' does not access instance data and can be marked as static (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1822)

Check warning on line 106 in crates/bindings-csharp/Runtime/ProcedureContext.cs

View workflow job for this annotation

GitHub Actions / Test Suite

Member 'StartMutTx' does not access instance data and can be marked as static (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1822)
{
var status = Internal.FFI.procedure_start_mut_tx(out var micros);
Internal.FFI.ErrnoHelpers.ThrowIfError(status);
return micros;
}

private void CommitMutTx()

Check warning on line 113 in crates/bindings-csharp/Runtime/ProcedureContext.cs

View workflow job for this annotation

GitHub Actions / unity-testsuite

Member 'CommitMutTx' does not access instance data and can be marked as static (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1822)

Check warning on line 113 in crates/bindings-csharp/Runtime/ProcedureContext.cs

View workflow job for this annotation

GitHub Actions / csharp-testsuite

Member 'CommitMutTx' does not access instance data and can be marked as static (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1822)

Check warning on line 113 in crates/bindings-csharp/Runtime/ProcedureContext.cs

View workflow job for this annotation

GitHub Actions / Test Suite

Member 'CommitMutTx' does not access instance data and can be marked as static (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1822)
{
var status = Internal.FFI.procedure_commit_mut_tx();
Internal.FFI.ErrnoHelpers.ThrowIfError(status);
Expand Down Expand Up @@ -181,15 +151,15 @@
where TError : Exception
{
var result = RunOnce(body);
if (!result.IsSuccess)
if (result is Result<TResult, TError>.ErrR)
{
return result;
}

bool Retry()
{
result = RunOnce(body);
return result.IsSuccess;
return result is Result<TResult, TError>.OkR;
}

if (!CommitMutTxWithRetry(Retry))
Expand Down Expand Up @@ -220,7 +190,7 @@
throw;
}

if (result.IsSuccess)
if (result is Result<TResult, TError>.OkR)
{
guard.Disarm();
return result;
Expand Down
1 change: 1 addition & 0 deletions crates/bindings-typescript/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ export * from './lib/timestamp';
export * from './lib/util';
export * from './lib/identity';
export * from './lib/option';
export * from './lib/result';
export * from './sdk';
52 changes: 52 additions & 0 deletions crates/bindings-typescript/src/lib/algebraic_type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,36 @@ export const SumType = {
} else {
writer.writeByte(1);
}
} else if (
ty.variants.length == 2 &&
ty.variants[0].name === 'ok' &&
ty.variants[1].name === 'err'
) {
let variantName: 'ok' | 'err';
let innerValue: any;
let index: number;
if ('ok' in value) {
variantName = 'ok';
innerValue = value.ok;
index = 0;
} else {
variantName = 'err';
innerValue = value.err;
index = 1;
}

if (index < 0) {
throw `Result serialization error: variant '${variantName}' not found in ${JSON.stringify(ty)}`;
}

writer.writeU8(index);

AlgebraicType.serializeValue(
writer,
ty.variants[index].algebraicType,
innerValue,
typespace
);
} else {
const variant = value['tag'];
const index = ty.variants.findIndex(v => v.name === variant);
Expand Down Expand Up @@ -479,6 +509,28 @@ export const SumType = {
} else {
throw `Can't deserialize an option type, couldn't find ${tag} tag`;
}
} else if (
ty.variants.length == 2 &&
ty.variants[0].name === 'ok' &&
ty.variants[1].name === 'err'
) {
if (tag === 0) {
const value = AlgebraicType.deserializeValue(
reader,
ty.variants[0].algebraicType,
typespace
);
return { ok: value };
} else if (tag === 1) {
const value = AlgebraicType.deserializeValue(
reader,
ty.variants[1].algebraicType,
typespace
);
return { err: value };
} else {
throw `Can't deserialize a result type, couldn't find ${tag} tag`;
}
} else {
const variant = ty.variants[tag];
const value = AlgebraicType.deserializeValue(
Expand Down
Loading
Loading