diff --git a/.env b/.env index 5da78ac3..2d04e71b 100644 --- a/.env +++ b/.env @@ -1,4 +1,4 @@ -VERSION="2.5.1" +VERSION="2.5.2" MAJOR=2 MINOR=5 -PATCH=1 +PATCH=2 diff --git a/Compiler/Program.cs b/Compiler/Program.cs index c515e8ce..95743ad5 100644 --- a/Compiler/Program.cs +++ b/Compiler/Program.cs @@ -195,7 +195,7 @@ private static async Task CompileSchema(Func ma if (result == Err) return Err; var generator = makeGenerator(schema); generator.WriteAuxiliaryFiles(outputFile.DirectoryName ?? string.Empty); - var compiled = generator.Compile(langVersion, !(_flags?.SkipGeneratedNotice ?? false)); + var compiled = generator.Compile(langVersion, writeGeneratedNotice: !(_flags?.SkipGeneratedNotice ?? false)); await File.WriteAllTextAsync(outputFile.FullName, compiled); return Ok; } diff --git a/Core/Exceptions/Exceptions.cs b/Core/Exceptions/Exceptions.cs index b3501324..c43b2d92 100644 --- a/Core/Exceptions/Exceptions.cs +++ b/Core/Exceptions/Exceptions.cs @@ -275,21 +275,46 @@ public DuplicateServiceDiscriminatorException(Token discriminator, string servic } [Serializable] - class DuplicateServiceFunctionNameException : SpanException + class DuplicateServiceMethodNameException : SpanException { - public DuplicateServiceFunctionNameException(ushort discriminator, string serviceName, string functionName, Span span) + public DuplicateServiceMethodNameException(uint discriminator, string serviceName, string functionName, Span span) : base($"Index {discriminator} duplicates the function name '{functionName}' which can only be used once in service '{serviceName}'.", span, 130) { } } [Serializable] - class DuplicateArgumentName : SpanException + class DuplicateServiceMethodIdException : SpanException { - public DuplicateArgumentName(Span span, string serviceName, string serviceIndex, string argumentName) - : base($"Index {serviceIndex} in service '{serviceName}' has duplicated argument name {argumentName}.", span, 131) + public DuplicateServiceMethodIdException(uint id, string serviceName, string methodName, Span span) + : base($"Index {id} duplicates the function name '{methodName}' which can only be used once in service '{serviceName}'.", span, 131) { } } - + + [Serializable] + class InvalidServiceRequestTypeException : SpanException + { + public InvalidServiceRequestTypeException(string serviceName, string methodName, TypeBase type, Span span) + : base($"The request type of method '{methodName}' in service '{serviceName}' is '{type.AsString}' must be a message, struct, or union.", span, 132) + { } + } + [Serializable] + class InvalidServiceReturnTypeException : SpanException + { + public InvalidServiceReturnTypeException(string serviceName, string methodName, TypeBase type, Span span) + : base($"The return type of method '{methodName}' in service '{serviceName}' is '{type.AsString}' but must be a message, struct, or union.", span, 133) + { } + } + + [Serializable] + class ServiceMethodIdCollisionException : SpanException + { + public ServiceMethodIdCollisionException(string serviceOneName, string methodOneName, string serviceTwoName, string methodTwoName, uint id, Span span) + : base($"The hashed ID of service '{serviceOneName}' and method '{methodOneName}' collides with the hashed ID of service '{serviceTwoName}' and '{methodTwoName}' (id: {id}).", span, 134) + { } + } + + + [Serializable] public class EnumZeroWarning : SpanException { diff --git a/Core/Generators/BaseGenerator.cs b/Core/Generators/BaseGenerator.cs index 06fb0e2b..ed7bfe6e 100644 --- a/Core/Generators/BaseGenerator.cs +++ b/Core/Generators/BaseGenerator.cs @@ -19,9 +19,10 @@ protected BaseGenerator(BebopSchema schema) /// Generate code for a Bebop schema. /// /// Determines a default language version the generated code will target. + /// Determines which components of a service will be generated. default to both client and server. /// Whether a generation notice should be written at the top of files. This is true by default. /// The generated code. - public abstract string Compile(Version? languageVersion, bool writeGeneratedNotice = true); + public abstract string Compile(Version? languageVersion, XrpcServices services = XrpcServices.Both, bool writeGeneratedNotice = true); /// /// Write auxiliary files to an output directory path. diff --git a/Core/Generators/CPlusPlus/CPlusPlusGenerator.cs b/Core/Generators/CPlusPlus/CPlusPlusGenerator.cs index 74d692ad..c3e08322 100644 --- a/Core/Generators/CPlusPlus/CPlusPlusGenerator.cs +++ b/Core/Generators/CPlusPlus/CPlusPlusGenerator.cs @@ -354,7 +354,7 @@ private string EmitLiteral(Literal literal) { /// Generate code for a Bebop schema. /// /// The generated code. - public override string Compile(Version? languageVersion, bool writeGeneratedNotice = true) + public override string Compile(Version? languageVersion, XrpcServices services = XrpcServices.Both, bool writeGeneratedNotice = true) { var builder = new StringBuilder(); if (writeGeneratedNotice) @@ -500,6 +500,8 @@ public override string Compile(Version? languageVersion, bool writeGeneratedNoti builder.AppendLine($"const {TypeName(cd.Value.Type)} {cd.Name} = {EmitLiteral(cd.Value)};"); builder.AppendLine(""); break; + case ServiceDefinition: + break; default: throw new InvalidOperationException($"unsupported definition {definition}"); } diff --git a/Core/Generators/CSharp/CSharpGenerator.cs b/Core/Generators/CSharp/CSharpGenerator.cs index 9e471c1f..12dce6ba 100644 --- a/Core/Generators/CSharp/CSharpGenerator.cs +++ b/Core/Generators/CSharp/CSharpGenerator.cs @@ -18,7 +18,7 @@ public CSharpGenerator(BebopSchema schema) : base(schema) } - public override string Compile(Version? languageVersion, bool writeGeneratedNotice = true) + public override string Compile(Version? languageVersion, XrpcServices services = XrpcServices.Both, bool writeGeneratedNotice = true) { if (languageVersion is not null) { @@ -69,6 +69,10 @@ public override string Compile(Version? languageVersion, bool writeGeneratedNoti { continue; } + if (definition is ServiceDefinition) + { + continue; + } if (!string.IsNullOrWhiteSpace(definition.Documentation)) { builder.AppendLine(FormatDocumentation(definition.Documentation, 0)); diff --git a/Core/Generators/Dart/DartGenerator.cs b/Core/Generators/Dart/DartGenerator.cs index 91ba94b4..efb1d1b4 100644 --- a/Core/Generators/Dart/DartGenerator.cs +++ b/Core/Generators/Dart/DartGenerator.cs @@ -296,7 +296,7 @@ private string EmitLiteral(Literal literal) { /// Generate code for a Bebop schema. /// /// The generated code. - public override string Compile(Version? languageVersion, bool writeGeneratedNotice = true) + public override string Compile(Version? languageVersion, XrpcServices services = XrpcServices.Both, bool writeGeneratedNotice = true) { var builder = new StringBuilder(); builder.AppendLine("import 'dart:typed_data';"); @@ -394,6 +394,8 @@ public override string Compile(Version? languageVersion, bool writeGeneratedNoti builder.AppendLine($"final {TypeName(cd.Value.Type)} {cd.Name} = {EmitLiteral(cd.Value)};"); builder.AppendLine(""); break; + case ServiceDefinition: + break; default: throw new InvalidOperationException($"unsupported definition {definition}"); } diff --git a/Core/Generators/Rust/RustGenerator.cs b/Core/Generators/Rust/RustGenerator.cs index c223eca7..d6d65d36 100644 --- a/Core/Generators/Rust/RustGenerator.cs +++ b/Core/Generators/Rust/RustGenerator.cs @@ -54,7 +54,7 @@ public class RustGenerator : BaseGenerator public RustGenerator(BebopSchema schema) : base(schema) { } - public override string Compile(Version? languageVersion, bool writeGeneratedNotice = true) + public override string Compile(Version? languageVersion, XrpcServices services = XrpcServices.Both, bool writeGeneratedNotice = true) { // the main scope which is where we write the const definitions and the borrowed types (as these are the // primary way to use bebop in Rust) @@ -107,6 +107,8 @@ public override string Compile(Version? languageVersion, bool writeGeneratedNoti WriteUnionDefinition(mainBuilder, ud, CodeRegion.Main); WriteUnionDefinition(ownedBuilder, ud, CodeRegion.Owned); break; + case ServiceDefinition: + break; default: throw new InvalidOperationException($"unsupported definition {definition.GetType()}"); } diff --git a/Core/Generators/ServiceGeneratorFlags.cs b/Core/Generators/ServiceGeneratorFlags.cs new file mode 100644 index 00000000..fc063bf4 --- /dev/null +++ b/Core/Generators/ServiceGeneratorFlags.cs @@ -0,0 +1,28 @@ +using System; +namespace Core.Generators +{ + /// + /// An enum that defines which parts of a service are generated + /// + public enum XrpcServices + { + /// + /// Indicates no service code should be generated + /// + None = 0, + /// + /// Indicates only client service code should be generated + /// + Client = 1, + /// + /// Indicates only server service code should be generated + /// + Server = 2, + /// + /// Indicates both client and server service code should be generated + /// + Both = 3 + + } +} + diff --git a/Core/Generators/TypeScript/TypeScriptGenerator.cs b/Core/Generators/TypeScript/TypeScriptGenerator.cs index dd7c7370..009c691e 100644 --- a/Core/Generators/TypeScript/TypeScriptGenerator.cs +++ b/Core/Generators/TypeScript/TypeScriptGenerator.cs @@ -369,7 +369,7 @@ private string EmitLiteral(Literal literal) { /// Generate code for a Bebop schema. /// /// The generated code. - public override string Compile(Version? languageVersion, bool writeGeneratedNotice = true) + public override string Compile(Version? languageVersion, XrpcServices services = XrpcServices.Both, bool writeGeneratedNotice = true) { var builder = new IndentedStringBuilder(); if (writeGeneratedNotice) @@ -377,6 +377,19 @@ public override string Compile(Version? languageVersion, bool writeGeneratedNoti builder.AppendLine(GeneratorUtils.GetXmlAutoGeneratedNotice()); } builder.AppendLine("import { BebopView, BebopRuntimeError, BebopRecord } from \"bebop\";"); + if (Schema.Definitions.Values.OfType().Any()) + { + if (services is XrpcServices.Client or XrpcServices.Both) + { + builder.AppendLine("import { Metadata } from \"@xrpc/common\";"); + builder.AppendLine("import { BaseClient, MethodInfo, CallOptions } from \"@xrpc/client\";"); + } + if (services is XrpcServices.Server or XrpcServices.Both) + { + builder.AppendLine("import { ServiceRegistry, BaseService, ServerContext, BebopMethodAny, BebopMethod } from \"@xrpc/server\";"); + } + } + builder.AppendLine(""); if (!string.IsNullOrWhiteSpace(Schema.Namespace)) { @@ -571,17 +584,158 @@ public override string Compile(Version? languageVersion, bool writeGeneratedNoti builder.AppendLine($"export const {cd.Name}: {TypeName(cd.Value.Type)} = {EmitLiteral(cd.Value)};"); builder.AppendLine(""); } + else if (definition is ServiceDefinition) + { + // noop + + } else { throw new InvalidOperationException($"Unsupported definition {definition}"); } } + var serviceDefinitions = Schema.Definitions.Values.OfType(); + if (serviceDefinitions is not null && serviceDefinitions.Any() && services is not XrpcServices.None) + { + if (services is XrpcServices.Server or XrpcServices.Both) + { + foreach(var service in serviceDefinitions) + { + builder.CodeBlock($"export abstract class {service.BaseClassName()} extends BaseService", indentStep, () => + { + builder.AppendLine($"public static readonly serviceName = '{service.ClassName()}';"); + foreach(var method in service.Methods) + { + builder.AppendLine($"public abstract {method.Definition.Name.ToCamelCase()}(record: I{method.Definition.ArgumentDefinition}, context: ServerContext): Promise;"); + } + }); + builder.AppendLine(); + } + + builder.CodeBlock("export class XrpcServiceRegistry extends ServiceRegistry", indentStep, () => + { + builder.AppendLine("private static readonly staticServiceInstances: Map = new Map();"); + builder.CodeBlock("public static register(serviceName: string)", indentStep, () => + { + builder.CodeBlock("return (constructor: Function) =>", indentStep, () => + { + builder.AppendLine("const service = Reflect.construct(constructor, [undefined]);"); + builder.CodeBlock("if (XrpcServiceRegistry.staticServiceInstances.has(serviceName))", indentStep, () => + { + builder.AppendLine("throw new Error(`Duplicate service registered: ${name}`);"); + }); + }); + + }); + builder.CodeBlock("public static tryGetService(serviceName: string): BaseService", indentStep, () => + { + builder.AppendLine("const service = XrpcServiceRegistry.staticServiceInstances.get(serviceName);"); + builder.CodeBlock("if (service === undefined)", indentStep, () => + { + builder.AppendLine("throw new Error(`Unable to retreive service '${serviceName}' - it is not registered.`);"); + }); + builder.AppendLine("return service;"); + }); + + builder.AppendLine(); + + builder.CodeBlock("public init(): void", indentStep, () => + { + builder.AppendLine("let service: BaseService;"); + builder.AppendLine("let serviceName: string;"); + foreach (var service in serviceDefinitions) + + { + + builder.AppendLine($"serviceName = '{service.ClassName()}';"); + builder.AppendLine($"service = XrpcServiceRegistry.tryGetService(serviceName);"); + builder.CodeBlock($"if (!(service instanceof {service.BaseClassName()}))", indentStep, () => + { + builder.AppendLine("throw new Error('todo');"); + }); + builder.AppendLine($"service.setLogger(this.logger.clone(serviceName));"); + builder.AppendLine("XrpcServiceRegistry.staticServiceInstances.delete(serviceName);"); + builder.AppendLine("this.serviceInstances.push(service);"); + foreach (var method in service.Methods) + { + var methodName = method.Definition.Name.ToCamelCase(); + builder.CodeBlock($"if (this.methods.has({method.Id}))", indentStep, () => + { + builder.AppendLine($"const conflictService = this.methods.get({method.Id})!;"); + builder.AppendLine($"throw new Error(`{service.ClassName()}.{methodName} collides with ${{conflictService.service}}.${{conflictService.name}}`)"); + }); + builder.CodeBlock($"this.methods.set({method.Id},", indentStep, () => + { + builder.AppendLine($"name: '{methodName}',"); + builder.AppendLine($"service: serviceName,"); + builder.AppendLine($"invoke: service.{methodName},"); + builder.AppendLine($"serialize: {method.Definition.ReturnDefintion}.encode,"); + builder.AppendLine($"deserialize: {method.Definition.ArgumentDefinition}.decode,"); + }, close: $"}} as BebopMethod);"); + } + } + + }); + + builder.AppendLine(); + builder.CodeBlock("getMethod(id: number): BebopMethodAny | undefined", indentStep, () => + { + builder.AppendLine("return this.methods.get(id);"); + }); + }); + + + } + + if (services is XrpcServices.Client or XrpcServices.Both) + { + foreach (var service in serviceDefinitions) + { + var clientName = service.ClassName().ReplaceLastOccurrence("Service", "Client"); + builder.CodeBlock($"export interface I{clientName}", indentStep, () => + { + foreach(var method in service.Methods) + { + builder.AppendLine($"{method.Definition.Name.ToCamelCase()}(request: I{method.Definition.ArgumentDefinition}): Promise;"); + builder.AppendLine($"{method.Definition.Name.ToCamelCase()}(request: I{method.Definition.ArgumentDefinition}, metadata: Metadata): Promise;"); + } + }); + builder.AppendLine(); + builder.CodeBlock($"export class {clientName} extends BaseClient implements I{clientName}", indentStep, () => + { + foreach(var method in service.Methods) + { + var methodInfoName = $"{method.Definition.Name.ToCamelCase()}MethodInfo"; + var methodName = method.Definition.Name.ToCamelCase(); + builder.CodeBlock($"private static readonly {methodInfoName}: MethodInfo =", indentStep, () => + { + builder.AppendLine($"name: '{methodName}',"); + builder.AppendLine($"service: '{service.ClassName()}',"); + builder.AppendLine($"id: {method.Id},"); + builder.AppendLine($"serialize: {method.Definition.ArgumentDefinition}.encode,"); + builder.AppendLine($"deserialize: {method.Definition.ReturnDefintion}.decode"); + }); + + builder.AppendLine($"async {methodName}(request: I{method.Definition.ArgumentDefinition}): Promise;"); + builder.AppendLine($"async {methodName}(request: I{method.Definition.ArgumentDefinition}, options: CallOptions): Promise;"); + + builder.CodeBlock($"async {methodName}(request: I{method.Definition.ArgumentDefinition}, options?: CallOptions): Promise", indentStep, () => + { + builder.AppendLine($"return await this.channel.send(request, this.getContext(), {clientName}.{methodInfoName}, options);"); + }); + } + + }); + } + } + } + + if (!string.IsNullOrWhiteSpace(Schema.Namespace)) { builder.Dedent(2); builder.AppendLine("}"); } - return builder.ToString(); } diff --git a/Core/Meta/BebopSchema.cs b/Core/Meta/BebopSchema.cs index 3b3473a6..0076e7fd 100644 --- a/Core/Meta/BebopSchema.cs +++ b/Core/Meta/BebopSchema.cs @@ -5,6 +5,7 @@ using Core.Exceptions; using Core.Lexer.Tokenization.Models; using Core.Meta.Extensions; +using Core.Parser; using Core.Parser.Extensions; namespace Core.Meta @@ -210,15 +211,28 @@ public List Validate() } if (definition is ServiceDefinition sd) { - var usedFunctionNames = new HashSet(); - - foreach (var b in sd.Branches) + var usedMethodNames = new HashSet(); + var usedMethodIds = new HashSet(); + foreach (var b in sd.Methods) { var fnd = b.Definition; - if (!usedFunctionNames.Add(fnd.Name.ToSnakeCase())) + if (!usedMethodNames.Add(fnd.Name.ToSnakeCase())) { - errors.Add(new DuplicateServiceFunctionNameException(b.Discriminator, sd.Name, fnd.Name, fnd.Span)); + errors.Add(new DuplicateServiceMethodNameException(b.Id, sd.Name, fnd.Name, fnd.Span)); } + if (!usedMethodIds.Add(b.Id)) + { + errors.Add(new DuplicateServiceMethodIdException(b.Id, sd.Name, fnd.Name, fnd.Span)); + } + if (!fnd.ArgumentDefinition.IsAggregate(this)) + { + errors.Add(new InvalidServiceRequestTypeException(sd.Name, fnd.Name, fnd.ArgumentDefinition, fnd.Span)); + } + if (!fnd.ReturnDefintion.IsAggregate(this)) + { + errors.Add(new InvalidServiceReturnTypeException(sd.Name, fnd.Name, fnd.ReturnDefintion, fnd.Span)); + } + if (fnd.Parent != sd) { throw new Exception("A function was registered to multiple services, this is an error in bebop core."); @@ -257,6 +271,19 @@ public List Validate() } } } + var methodIds = new Dictionary(); + + foreach (var service in Definitions.Values.OfType()) + { + foreach (var method in service.Methods) + { + if (methodIds.ContainsKey(method.Id)) + { + var (firstServiceName, firstMethodName) = methodIds[method.Id]; + errors.Add(new ServiceMethodIdCollisionException(firstServiceName, firstMethodName, service.Name, method.Definition.Name, method.Id, service.Span)); + } + } + } _validationErrors = errors; return errors; } diff --git a/Core/Meta/Definition.cs b/Core/Meta/Definition.cs index 84c38438..4d048997 100644 --- a/Core/Meta/Definition.cs +++ b/Core/Meta/Definition.cs @@ -214,14 +214,14 @@ public UnionBranch(byte discriminator, RecordDefinition definition) } } - public readonly struct ServiceBranch + public readonly struct ServiceMethod { - public readonly ushort Discriminator; + public readonly uint Id; public readonly FunctionDefinition Definition; - public ServiceBranch(ushort discriminator, FunctionDefinition definition) + public ServiceMethod(uint id, FunctionDefinition definition) { - Discriminator = discriminator; + Id = id; Definition = definition; } } @@ -246,19 +246,31 @@ override public int MinimalEncodedSize(BebopSchema schema) public class ServiceDefinition : Definition { - public ServiceDefinition(string name, Span span, string documentation, ICollection branches) : base(name, span, documentation) + public ServiceDefinition(string name, Span span, string documentation, ICollection methods) : base(name, span, documentation) { - foreach (var b in branches) + foreach (var m in methods) { - b.Definition.Parent = this; + m.Definition.Parent = this; } - Branches = branches; + Methods = methods; } - public ICollection Branches { get; } + public ICollection Methods { get; } - public override IEnumerable Dependencies() => Branches.SelectMany(f => f.Definition.Dependencies()); + public override IEnumerable Dependencies() => Methods.SelectMany(f => f.Definition.Dependencies()); + + public override string ToString() + { + var builder = new StringBuilder(); + builder.AppendLine($"Service: {Name}"); + builder.AppendLine("Methods ->"); + foreach(var method in Methods) + { + builder.AppendLine($" {method.Definition.Name}({method.Definition.ArgumentDefinition}): {method.Definition.ReturnDefintion} ({method.Id})"); + } + return builder.ToString(); + } } /// @@ -266,20 +278,18 @@ public ServiceDefinition(string name, Span span, string documentation, ICollecti /// public class FunctionDefinition : Definition { - public FunctionDefinition(string name, Span span, string documentation, ConstDefinition signature, StructDefinition argumentStruct, StructDefinition returnStruct, Definition? parent = null) + public FunctionDefinition(string name, Span span, string documentation, TypeBase argumentDefinition, TypeBase returnDefintion, Definition? parent = null) : base(name, span, documentation, parent) { - Signature = signature; - ArgumentStruct = argumentStruct; - ReturnStruct = returnStruct; + ArgumentDefinition = argumentDefinition; + ReturnDefintion = returnDefintion; } - public ConstDefinition Signature { get; } - public StructDefinition ArgumentStruct { get; } - public StructDefinition ReturnStruct { get; } + public TypeBase ArgumentDefinition { get; } + public TypeBase ReturnDefintion { get; } public override IEnumerable Dependencies() => - ArgumentStruct.Dependencies().Concat(ReturnStruct.Dependencies()); + ArgumentDefinition.Dependencies().Concat(ReturnDefintion.Dependencies()); } public class ConstDefinition : Definition diff --git a/Core/Meta/Extensions/StringExtensions.cs b/Core/Meta/Extensions/StringExtensions.cs index 0b24d837..0496a792 100644 --- a/Core/Meta/Extensions/StringExtensions.cs +++ b/Core/Meta/Extensions/StringExtensions.cs @@ -13,7 +13,17 @@ public static class StringExtensions private static readonly string[] NewLines = { "\r\n", "\r", "\n" }; - + public static string ReplaceLastOccurrence(this string source, string find, string replace) + { + int place = source.LastIndexOf(find); + + if (place == -1) + return source; + + return source.Remove(place, find.Length).Insert(place, replace); + } + + /// /// Splits the specified based on line ending. /// diff --git a/Core/Meta/Type.cs b/Core/Meta/Type.cs index 1677d011..1f909ef1 100644 --- a/Core/Meta/Type.cs +++ b/Core/Meta/Type.cs @@ -146,6 +146,11 @@ public static bool IsUnion(this TypeBase type, BebopSchema schema) return type is DefinedType dt && schema.Definitions[dt.Name] is UnionDefinition; } + public static bool IsAggregate(this TypeBase type, BebopSchema schema) + { + return IsUnion(type, schema) || IsMessage(type, schema) || IsStruct(type, schema); + } + public static bool IsEnum(this TypeBase type, BebopSchema schema) { return type is DefinedType dt && schema.Definitions[dt.Name] is EnumDefinition; diff --git a/Core/Meta/WellKnownTypes.cs b/Core/Meta/WellKnownTypes.cs index 9399da70..ccf2daa5 100644 --- a/Core/Meta/WellKnownTypes.cs +++ b/Core/Meta/WellKnownTypes.cs @@ -20,7 +20,8 @@ public static class ReservedWords "BebopRecord", "BebopMirror", "BebopConstants", - "BopConstants" + "BopConstants", + "Service" }; } diff --git a/Core/Parser/RpcSchema.cs b/Core/Parser/RpcSchema.cs index b3e73b5e..7de35422 100644 --- a/Core/Parser/RpcSchema.cs +++ b/Core/Parser/RpcSchema.cs @@ -2,105 +2,95 @@ { static class RpcSchema { - public const string RpcRequestHeader = @" -/* Static RPC request header used for all request datagrams. */ -readonly struct RpcRequestHeader { - /* - Identification for the caller to identify responses to this request. - - The caller should ensure ids are always unique at the time of calling. Two active - calls with the same id is undefined behavior. Re-using an id that is not currently - in-flight is acceptable. - These are unique per connection. - */ - uint16 id; + // Constants used in the hash algorithm for good distribution properties. + private const uint Seed = 0x5AFE5EED; + private const uint C1 = 0xcc9e2d51; + private const uint C2 = 0x1b873593; + private const uint N = 0xe6546b64; - /* - Function signature includes information about the args to ensure the caller and - callee are referencing precisely the same thing. There is a non-zero risk of - accidental signature collisions, but 32-bits is probably sufficient for peace of - mind. + /// + /// Gets the unique ID of a method + /// + /// The name of the service the method is on. + /// The name of the method + /// A unique unsigned 32-bit integer. + public static uint GetMethodId(string serviceName, string methodName) { + var path = $"/${serviceName}/${methodName}"; + return HashString(path); + } - I did some math, about a 26% chance of collision using 16-bits assuming 200 unique - RPC calls which is pretty high, or <0.0005% chance with 32-bits. - */ - uint32 signature; -} -"; - public const string RpcResponseHeader = @" -/* Static RPC response header used for all response datagrams. */ -readonly struct RpcResponseHeader { - /* The caller-assigned identifier */ - uint16 id; -} -"; + /// + /// Computes a 32-bit hash of the given string. + /// + /// The string to hash. + /// A 32-bit hash value. + /// The hash is based on MurmurHash3 with some new finalization constants used to reduce collisions + private static uint HashString(string input) + { + // The length of the input string. + int length = input.Length; + // The current index in the input string. + int currentIndex = 0; + // Initialize the hash value using the seed constant. + uint hash = Seed; - public const string RpcDatagram = @" -/* - All data sent over the transport MUST be represented by this union. + // Process 4 characters at a time + while (currentIndex + 4 <= length) + { + // Combine the 4 characters into a 32-bit unsigned integer (block). + uint block = ((uint)input[currentIndex] & 0xFF) | + (((uint)input[currentIndex + 1] & 0xFF) << 8) | + (((uint)input[currentIndex + 2] & 0xFF) << 16) | + (((uint)input[currentIndex + 3] & 0xFF) << 24); - Note that data is sent as binary arrays to avoid depending on the generated structure - definitions that we cannot know in this context. Ultimately the service will be - responsible for determining how to interpret the data. -*/ -union RpcDatagram { - 1 -> struct RpcRequestDatagram { - RpcRequestHeader header; - /* The function that is to be called. */ - uint16 opcode; - /* Callee can decode this given the opcode in the header. */ - byte[] request; - } - - 2 -> struct RpcResponseOk { - RpcResponseHeader header; - /* Caller can decode this given the id in the header. */ - byte[] data; - } + // Update the block with the constants C1 and C2. + block *= C1; + block = (block << 15) | (block >> 17); // Rotate left by 15 + block *= C2; - 3 -> struct RpcResponseErr { - RpcResponseHeader header; - /* - User is responsible for defining what code values mean. These codes denote - errors that happen only once user code is being executed and are specific - to each domain. - */ - uint32 code; - /* An empty string is acceptable */ - string info; - } + // Update the hash value with the calculated block. + hash ^= block; + hash = (hash << 13) | (hash >> 19); // Rotate left by 13 + hash = hash * 5 + N; - /* Default response if no handler was registered. */ - 0xfc -> struct CallNotSupported { - RpcResponseHeader header; - } + // Move to the next group of 4 characters. + currentIndex += 4; + } - /* Function id was unknown. */ - 0xfd -> struct RpcResponseUnknownCall { - RpcResponseHeader header; - } + // Process the remaining characters + uint tail = 0; + int remaining = length - currentIndex; - /* The remote function signature did not agree with the expected signature. */ - 0xfe -> struct RpcResponseInvalidSignature { - RpcResponseHeader header; - /* The remote function signature */ - uint32 signature; - } + // Process the remaining characters based on the number left. + switch (remaining) + { + case 3: + tail |= (uint)(input[currentIndex + 2] & 0xFF) << 16; + goto case 2; + case 2: + tail |= (uint)(input[currentIndex + 1] & 0xFF) << 8; + goto case 1; + case 1: + tail |= (uint)(input[currentIndex] & 0xFF); + tail *= C1; + tail = (tail << 15) | (tail >> 17); // Rotate left by 15 + tail *= C2; + hash ^= tail; + break; + } - /* - A message received by the other end was unintelligible. This indicates a - fundamental flaw with our encoding and possible bebop version mismatch. + // Finalization step to mix the hash value. + // This is where we deviate from MurmurHash3 + hash ^= hash >> 16; + hash *= 0x7feb352d; + hash ^= hash >> 15; + hash *= 0x846ca68b; + hash ^= hash >> 16; - This should never occur between proper implementations of the same version. - */ - 0xff -> struct DecodeError { - /* Information provided on a best-effort basis. */ - RpcResponseHeader header; - string info; - } -} -"; + // Return the final 32-bit hash value. + return hash; + } } } \ No newline at end of file diff --git a/Core/Parser/SchemaParser.cs b/Core/Parser/SchemaParser.cs index d10260c7..3298798d 100644 --- a/Core/Parser/SchemaParser.cs +++ b/Core/Parser/SchemaParser.cs @@ -55,7 +55,13 @@ public class SchemaParser /// private void AddDefinition(Definition definition) { - if (_definitions.ContainsKey(definition.Name)) return; + + if (_definitions.ContainsKey(definition.Name)) + { + _errors.Add(new MultipleDefinitionsException(definition)); + return; + } + _definitions.Add(definition.Name, definition); if (_scopes.Count > 0) { @@ -369,10 +375,6 @@ public async Task Parse() { throw new UnexpectedTokenException(TokenKind.Service, CurrentToken, "Did not expect service definition after opcode. (Services are not allowed opcodes)."); } - - _tokenizer.AddString("rpc_request_header", RpcSchema.RpcRequestHeader); - _tokenizer.AddString("rpc_response_header", RpcSchema.RpcResponseHeader); - _tokenizer.AddString("rpc_datagram", RpcSchema.RpcDatagram); return ParseServiceDefinition(CurrentToken, definitionDocumentation); } if (Eat(TokenKind.Union)) @@ -730,10 +732,13 @@ private Literal ParseLiteral(TypeBase type) return null; } StartScope(); - var name = definitionToken.Lexeme; - var branches = new List(); - var usedDiscriminators = new HashSet(){0}; - + var serviceName = $"{definitionToken.Lexeme.ToPascalCase()}Service"; + + + var methods = new List(); + var usedMethodIds = new HashSet(){0}; + var usedMethodNames = new HashSet() { string.Empty}; + var definitionEnd = CurrentToken.Span; var errored = false; var serviceFieldFollowKinds = new HashSet() { TokenKind.BlockComment, TokenKind.Number, TokenKind.CloseBrace }; @@ -752,29 +757,40 @@ private Literal ParseLiteral(TypeBase type) break; } - const string indexHint = "Branches in a service must be explicitly indexed: service U { 1 -> void doThing(int32 myarg); 2 -> bool foo(float32 a, float32 b); }"; - var indexToken = CurrentToken; - var indexLexeme = indexToken.Lexeme; - uint discriminator; + // The start of the function is the definition token of the function try { - Expect(TokenKind.Number, indexHint); - if (!indexLexeme.TryParseUInt(out discriminator)) + var functionStart = CurrentToken.Span; + const string hint = "A function must be defined with name, request type, and return type, such as 'myFunction(MyRequest): MyResponse;'"; + var indexToken = CurrentToken; + var methodName = ExpectLexeme(TokenKind.Identifier, hint).ToCamelCase(); + if (usedMethodNames.Contains(methodName)) { - throw new UnexpectedTokenException(TokenKind.Number, indexToken, "A function id must be an unsigned integer."); + _errors.Add(new DuplicateServiceDiscriminatorException(indexToken, serviceName)); } - if (discriminator < 1 || discriminator > 0xffff) + usedMethodNames.Add(methodName); + var methodId = RpcSchema.GetMethodId(serviceName, methodName); + if (usedMethodIds.Contains(methodId)) { - _errors.Add(new UnexpectedTokenException(TokenKind.Number, indexToken, "A function id must be between 1 and 65535.")); + _errors.Add(new DuplicateServiceDiscriminatorException(indexToken, serviceName)); } - if (usedDiscriminators.Contains(discriminator)) + Expect(TokenKind.OpenParenthesis, hint); + var paramType = ParseType(CurrentToken); + Expect(TokenKind.CloseParenthesis, hint); + Expect(TokenKind.Colon, hint); + var returnType = ParseType(CurrentToken); + var returnTypeSpan = functionStart.Combine(CurrentToken.Span); + Expect(TokenKind.Semicolon, "Function definition must end with a ';' semicolon"); + var functionSpan = functionStart.Combine(CurrentToken.Span); + var function = new FunctionDefinition(methodName, functionSpan, documentation, paramType, returnType); + if (function is null) { - _errors.Add(new DuplicateServiceDiscriminatorException(indexToken, name)); + // Just escape out of there if there's a parsing error in one of the definitions. + CancelScope(); + return null; } - usedDiscriminators.Add(discriminator); - // Parse an arrow ("->"). - Expect(TokenKind.Hyphen, indexHint); - Expect(TokenKind.CloseCaret, indexHint); + definitionEnd = CurrentToken.Span; + methods.Add(new(methodId, function)); } catch (SpanException e) { @@ -783,120 +799,16 @@ private Literal ParseLiteral(TypeBase type) SkipAndSkipUntil(new HashSet(serviceFieldFollowKinds.Concat(_universalFollowKinds))); continue; } - var definition = ParseFunctionDefinition(name, indexLexeme); - if (definition is null) - { - // Just escape out of there if there's a parsing error in one of the definitions. - CancelScope(); - return null; - } - Eat(TokenKind.Semicolon); - definitionEnd = CurrentToken.Span; - branches.Add(new((ushort)discriminator, definition)); } var definitionSpan = definitionToken.Span.Combine(definitionEnd); - - // add the implicit "ServiceName" function - var serviceNameReturnStruct = new StructDefinition($"_{name}NameReturn", definitionSpan, "", null, new List(){new("serviceName", new ScalarType(BaseType.String, definitionSpan, "name"), definitionSpan, null, 0, "")}, true); - AddDefinition(serviceNameReturnStruct); - var serviceNameArgsStruct = new StructDefinition($"_{name}NameArgs", definitionSpan, "", - null, new List() { }, true); - AddDefinition(serviceNameArgsStruct); - var serviceNameSignature = - MakeFunctionSignature(name, serviceNameReturnStruct, serviceNameArgsStruct, "name", definitionSpan); - AddDefinition(serviceNameSignature); - var serviceNameDefinition = new FunctionDefinition("name", definitionSpan, "", serviceNameSignature, serviceNameArgsStruct, serviceNameReturnStruct); - branches.Add(new(0, serviceNameDefinition)); - + // make the service itself - var serviceDefinition = new ServiceDefinition(name, definitionSpan, definitionDocumentation, branches); + var serviceDefinition = new ServiceDefinition(serviceName, definitionSpan, definitionDocumentation, methods); CloseScope(serviceDefinition); return serviceDefinition; } - /// - /// Parses an rpc function definition and adds its types to the collection. - /// - /// Name of the service this function is part of. - /// Index of this function within the service. - /// The parsed rpc function definition. - private FunctionDefinition? ParseFunctionDefinition(string serviceName, string serviceIndex) - { - var definitionDocumentation = ConsumeBlockComments(); - - // The start of the function is the definition token of the function - var definitionToken = CurrentToken; - var functionStart = CurrentToken.Span; - - var isReadonly = Eat(TokenKind.ReadOnly); - var returnType = EatPseudoKeyword("void") ? null : ParseType(definitionToken); - var returnTypeSpan = functionStart.Combine(CurrentToken.Span); - - const string hint = "A function must be defined with a return type, name, and arguments such as 'RetType myFunction(ArgType arg1, OtherArg arg2);' or 'void emptyFn();'"; - var name = ExpectLexeme(TokenKind.Identifier, hint); - - Expect(TokenKind.OpenParenthesis, hint); - - var argList = new List(); - var argsStart = CurrentToken.Span; - - // read parameter list - while (!Eat(TokenKind.CloseParenthesis)) - { - if (argList.Count > 0) - { - Expect(TokenKind.Comma, "Function arguments must be separated by commas"); - } - var paramStart = CurrentToken.Span; - var paramType = ParseType(definitionToken); - var paramName = ExpectLexeme(TokenKind.Identifier, hint); - var paramSpan = paramStart.Combine(CurrentToken.Span); - - if (argList.Any(t => t.Name.Equals(paramName))) - { - _errors.Add(new DuplicateArgumentName(definitionToken.Span.Combine(CurrentToken.Span), serviceName, serviceIndex, paramName)); - } - else - { - argList.Add(new Field(paramName, paramType, paramSpan, null, 0, "")); - } - } - - var argsSpan = argsStart.Combine(CurrentToken.Span); - - Expect(TokenKind.Semicolon, "Function definition must end with a ';' semicolon"); - - var functionSpan = functionStart.Combine(CurrentToken.Span); - - var returnStruct = new StructDefinition( - $"_{serviceName.ToPascalCase()}{name.ToPascalCase()}Return", - returnTypeSpan, - $"Wrapped return type of '{name}' in rpc service '{serviceName}'.", - null, - returnType is null - ? new List {} - : new List {new("value", returnType, returnTypeSpan, null, 0, "")}, - isReadonly - ); - AddDefinition(returnStruct); - - var argumentStruct = new StructDefinition( - $"_{serviceName.ToPascalCase()}{name.ToPascalCase()}Args", - argsSpan, - $"Wrapped arguments type of '{name}' in rpc service '{serviceName}'.", - null, - argList, - isReadonly - ); - AddDefinition(argumentStruct); - - var signature = MakeFunctionSignature(serviceName, returnStruct, argumentStruct, name, functionSpan); - AddDefinition(signature); - - var function = new FunctionDefinition(name, functionSpan, definitionDocumentation, signature, argumentStruct, returnStruct); - return function; - } /// /// Parses a union definition and adds it to the collection. @@ -1221,24 +1133,6 @@ private static bool TryParseBigInteger(string lexeme, out BigInteger value) return success; } - private ConstDefinition MakeFunctionSignature(string serviceName, StructDefinition returnStruct, - StructDefinition argumentStruct, string functionName, Span functionSpan) - { - var builder = new StringBuilder(); - TypeSignature(builder, returnStruct); - TypeSignature(builder, argumentStruct); - var textSignature = builder.ToString(); - - var binarySignature = ShortMD5(textSignature); - var signature = new ConstDefinition( - $"_{serviceName.ToPascalCase()}{functionName.ToPascalCase()}Signature", - functionSpan, - $"hash(\"{textSignature}\")", - new IntegerLiteral(new ScalarType(BaseType.Int32, functionSpan, "signature"), functionSpan, - $"0x{binarySignature:x8}") - ); - return signature; - } /// /// Create a text signature of this type. It should include all details which pertain to the binary diff --git a/Laboratory/Integration/package.json b/Laboratory/Integration/package.json index 987d34a4..ba9c754c 100644 --- a/Laboratory/Integration/package.json +++ b/Laboratory/Integration/package.json @@ -2,6 +2,8 @@ "dependencies": { "@types/node": "^14.14.6", "bebop": "file:../../Runtime/TypeScript", + "@xrpc/client": "0.0.1-alpha.1", + "@xrpc/server": "0.0.1-alpha.1", "ts-node": "^9.0.0", "typescript": "^4.1.2", "deep-equal": "^2.0.5", diff --git a/Laboratory/TypeScript/package.json b/Laboratory/TypeScript/package.json index adfe5dea..1c0c2138 100644 --- a/Laboratory/TypeScript/package.json +++ b/Laboratory/TypeScript/package.json @@ -4,13 +4,15 @@ }, "devDependencies": { "@types/benchmark": "^1.0.33", + "@types/node": "^18.16.2", "jest": "^26.6.3", "jest-cli": "^26.6.3" }, "dependencies": { "@msgpack/msgpack": "^2.3.0", "@types/jest": "^26.0.15", - "@types/node": "^14.14.6", + "@xrpc/client": "0.0.1-alpha.1", + "@xrpc/server": "0.0.1-alpha.1", "bebop": "file:../../Runtime/TypeScript", "benchmark": "^2.1.4", "pbf": "^3.2.1",