Git Product home page Git Product logo

slicec's Introduction

The Slice compiler library (slicec)

CI License

To build slicec you must have Rust and Cargo installed. To install these, we recommend reading the following guide.

Building

Run the following command to build slicec and its dependencies:

cargo build

Running the tests

Run the following command to run the test suite:

cargo test

Generating documentation

To generate documentation for slicec, run the following command:

cargo doc --no-deps --document-private-items

This will generate documentation in the target/doc/slicec directory.

Additionally, you can easily view the documentation after generating it with the open flag:

cargo doc --no-deps --document-private-items --open

Generating a code coverage report

slicec uses llvm-cov to generate coverage reports. So, to generate reports you must install it:

cargo install cargo-llvm-cov

To generate a coverage report for slicec, run the following command:

cargo llvm-cov --html

This will generate an HTML report in the target/llvm-cov/html directory.

Additionally, you can easily view the report after generating it with the open flag:

cargo llvm-cov --open

slicec's People

Contributors

bernardnormier avatar externl avatar insertcreativityhere avatar pepone avatar reecehumphreys avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

slicec's Issues

Remove remaining semicolons from Slice

encoding = 1.1;

module IceRpc::Tests::Slice;

inteface Foo
{
   op1();
   op2(p: int) -> int;
   op3() -> (x: int, y: int);
}

Seems to me there would be no ambiguity if we remove those, encoding statement will end with the whitespace after 1.1, and the module too if there isn't an open curly bracket; same for operations it will end with the param declaration, the return tuple () or the return type.

Change range of tags with slice2

The range of tags with slice2 is currently the varint32 range.

With slice1, their range is 0..int32.Max

Proposal

  1. Change the range of slice2 tags to be the same as slice1 tags: 0..int32.Max

Rationale: it's big enough and it's simpler to use the same range for slice1 and slice2

  1. Encode slice2 tags using the varuint encoding, just like icerpc request/response field keys and icerpc settings keys.

Split 'typealias' into 'type' and 'alias'

Currently there are 2 main uses cases for the 'typealias' construct:

  • Convenience: Instead of having to type out complex types, or identifiers deep in other modules, you can use a typealias:
module My::Very::Nested::Module {
    interface MyInterface {}
}
typealias MyInterface = My::Very::Nested::Module::MyInterface;
// Now you can just use the unqualified 'MyInterface'.
typealias MySeq = sequence<dictionary<varulong, sequence<int>>>;
struct S
{
   data: MySeq, // Much easier than writing out the whole type.
}
  • Custom type-mapping at the language level
// Here we can map a 'ulong' to a C# 'DateTime' object for usability.
typealias Time = [cs:type("DateTime")] ulong;

I think these are unrelated use cases, and more importantly, calling this an 'alias' is objectively incorrect.
This functions more like a 'typedef' than a 'typealias' (See: #72 (comment))

So, I propose that we split the typealias construct in two: type and alias

Alias

alias is purely about providing convenience to the Slice file writer

  • Syntax: alias MyAlias = MyComplicatedType;
  • Attributes are NOT allowed on aliases, and they also cannot be marked optional
  • They are purely transparent; they don't survive past parsing. The parser effectively 'copy-pastes' aliases away.
    After parsing, anywhere where MyAlias is used, will literally be replaced with the right hand side: MyComplicatedType.
  • These are true aliases, MyAlias and MyComplicatedType are totally indistinguishable on all levels.
  • Can apply to ANY slice construct, currently TypeAlias is limited to types). This will be expanded to also allow for modules, exceptions, anything.
  • Declared inside a module, and are 'exported'. So it's visible to other modules, it's not local to that slice file.

Open Questions

  • Should we generate code for these? We could generate typealias MyAlias = MyComplicatedType in swift, and type MyAlias = MyComplicatedType in Rust, and so on. I lean towards yet, in languages that support aliases like this.
  • Should these support nesting (typealiases of typealiases)? I lean towards yes, but what's the point?

Type

type is purely about declaring a new type for the language mapping

  • Syntax: type NewType = OriginalType;
  • Attributes are allowed on custom types, and often these will be used with 'type' attributes:
type Time = [cs:type("System.DateTime")] [rust:type("std::time::SystemTime")] ulong;
  • Survives parsing, it is a true and distinct type at the Slice level (even if it happens to share an on-the-wire encoding)
  • Can only declare these on types; you can't have type MyType = MyModule
  • Declared inside a module, and are 'exported'. So it's visible to other modules, it's not local to that slice file.
  • Has a distinct TypeId (For example IceRpc::Time for the above example). So these can even be used with traits.

Open Questions

  • What is the default mapping here? If the user provides a type attribute, we map to that, but what if they don't? 3 options:
    1. We require a type attribute (seems lame)
    1. We map to the underlying type, as if there is no special type construct at all.
    1. We generate a new type with the corresponding identifier. For example, in Rust we'd use the newtype pattern (https://doc.rust-lang.org/rust-by-example/generics/new_types.html):
type MyNewType = int;        //Slice
// Would get mapped to:
struct MyNewType = (i32);     // Rust

It isn't as clear what to do in C# to me, @bernardnormier proposed by default we map an un-customized type as if it was a compact struct with 1 member, which I think is reasonable.

  • Should it support nesting? Can you have make a custom type from another custom type?
    If we go with 3 from above, I could see this being simple to implement, but again, what's a real use case here?
    If we don't go with 3, I think the complexity outweights it's benefits.
    (If we don't go with 3 and allow nesting, we'll have to traverse the entire stack of custom types for attributes, which complicates things)

Limitations

One limitation with custom type mappings in C#, is that they might not always be 'trait'-able.
For instance if you map a type to System.DateTime, there's no way to implement a trait on this, since you have no control over this system type.

I think this is okay. In Rust and I believe Swift, this isn't an issue, you can implement a trait/protocol on anything. Even if C#, if you really need to use a trait, you can always map to your own custom type that extends or contains a DateTime.
This is a limitation of C#, not of the slice construct.

Allow attributes on both member declarations and member types

(A member is a data member, parameter, or return member)

Currently, Slice only allows you to apply attributes to member declarations, instead of their types:

[deprecated] [cs::generic("Foo")] myParam: sequence<int8>,

In this case deprecate will be applied to the parameter declaration ('myParam' is deprecated),
and cs::generic will be applied to the parameter's type ('sequence will be mapped to "Foo")

I propose we allow attributes to be applied in both positions:

[deprecated] myParam: [cs::generic("Foo")] sequence<int8>,

With the semantics that only declaration attributes can be put on the parameter, and only type attributes can be put on the type. Under this proposal, [cs::generic("Foo") myParam: sequence<int8> would be an error, since a parameter can't be mapped to "Foo", only a type can.

This seems more logical to me, and more consistent with how we allow type attributes in other places (like type-aliases and return types where there is no parameter, just a type).

One downside is that for new-comers to Slice, it might be confusing to figure out which position an attribute belongs.
But I think this is a small price to pay.

Incorrect min wire size for structs and exceptions with optional types

The min wire size of a struct (including compact struct) or exception with one or more optional data members (not tagged) is not correct.

It apparently just adds up the min wire size of each element, and assigns "0" to a data member with an optional type. The correct method is to compute the number of bytes in the bit sequence from the number of optional data members (not including tagged members).

In C#, the formula is: (bitCount >> 3) + ((bitCount & 0x07) != 0 ? 1 : 0).

where "bitCount" represents the number of non-tagged data members with optional types.

Example:

// Slice
compact struct TestStruct
{
    x: int32?
}

interface CompactStructOperations
{
    opTestStruct(p: sequence<TestStruct>);
}

// generated C#
     await request.DecodeArgsAsync(
                    SliceEncoding.Slice2,
                    _defaultActivator,
                    (ref SliceDecoder decoder) =>
                        decoder.DecodeSequence(
                            minElementSize: 0, // should be 1
                            (ref SliceDecoder decoder) => new TestStruct(ref decoder)),
                    hasStream: false,
                    cancel).ConfigureAwait(false);
struct TestStruct
{
    x: int32?
}

interface StructOperations
{
    opTestStruct(p: sequence<TestStruct>);
}

// generated C#
                await request.DecodeArgsAsync(
                    SliceEncoding.Slice2,
                    _defaultActivator,
                    (ref SliceDecoder decoder) =>
                        decoder.DecodeSequence(
                            minElementSize: 1, // should be 2
                            (ref SliceDecoder decoder) => new TestStruct(ref decoder)),
                    hasStream: false,
                    cancel).ConfigureAwait(false);

I think more broadly we should never retrieve the min wire size of an "optional type" (such as string? or int?). If we need to provide such an API, it should panic, not return 0.

Add Support for 'Using' directives

Many languages provide a way of 'pulling in' other namespaces:

  • C++ using MyNamespace
  • C# using MyNamespace
  • Java import MyPackage
  • Rust use MyModule

Slice should also provide a feature like this, to avoid needing to repeat long namespaces.

Proposed Syntax

using <qualified_module>;

using IceRpc::Internal;
// Now anything in `IceRpc::Internal` can be accessed without qualification

It MUST be declared at the top of a slice file, before any module declarations (Same as file attributes).
Additionally, it's file-specific and NOT exported. Only the contents of the slice file will be affected by this.

Open Questions
What happens when there's a conflict? If a locally declared type conflicts with a type that is being pulled in.
A) We silently hide the pulled-in type, and the locally declared type is what gets used
B) We issue an error saying that the type is ambiguous.
I lean heavily towards B, otherwise it seems very unsafe to me. But apparently C# went with option A, so idk.

@return doc comment field warnings

We should implement warnings for when the user adds a doc comment for @return that is false
For example

/// @return bool
myOp() 

Should warn the user that this doc comment is not true.

Allow Enums to Implement Traits

With the 2.0 encoding, traits are the sole construct for polymorphism in Slice.
However, they are currently only usable with structs. This seems limited to me.
I think that traits should be supported with all data-carrying Slice constructs, and in Slice, there is one other: enums.
(Exceptions aren't currently usable as a data-type, so they don't count for this)

Proposal:
Allow traits to be used with enums.

Unfortunately, C# does not allow interfaces to be implemented on enums, but many other languages do:

  • In Swift, you can implement protocols on enums
  • In Rust, you can implement traits on enums

One of the beautys of traits, is that they're very local to an application.
This means one language not supporting this has no-impact on any other language.

However, since C# doesn't support this, and right now we only have C#, this proposal's implementation isn't viable yet.

In a future language that does support this, implementing it would be dead simple:

  • Generate a TypeId on enums
  • Generate an EncodeTrait method on enums

That's it.

compiler panics with exception inheritance

The compiler panics with the following Slice definition

module Demo
{
    exception A { int x; }
    exception B : A { int y; }
}

Seems that panics happen when trying to borrow the base type ref

d:\IceRPC\icerpc-dev\target\debug\slicec-cs.exe test.ice
thread 'main' panicked at 'called `Option::unwrap()` on a `None` value', slicec\src\ptr_util.rs:122:30
stack backtrace:
...
  16:     0x7ff66d615179 - enum$<core::option::Option<ptr_const$<slice::grammar::slice::Exception> > >::unwrap<ptr_const$<slice::grammar::slice::Exception> >
                               at /rustc/59eed8a2aac0230a8b53e89d4e99d55912ba6b35\library\core\src\option.rs:735
  17:     0x7ff66d60d57f - slice::ptr_util::WeakPtr<slice::grammar::slice::Exception>::borrow<slice::grammar::slice::Exception>
                               at D:\IceRPC\icerpc-dev\slicec\src\ptr_util.rs:122
  18:     0x7ff66d605572 - slice::grammar::slice::TypeRef<slice::grammar::slice::Exception>::definition<slice::grammar::slice::Exception>
                               at D:\IceRPC\icerpc-dev\slicec\src\grammar\slice.rs:769
  19:     0x7ff66d604e7e - slice::grammar::slice::impl$6::base_exception::closure$0
                               at D:\IceRPC\icerpc-dev\slicec\src\grammar\slice.rs:256
  20:     0x7ff66d613ce2 - enum$<core::option::Option<ref$<slice::grammar::slice::TypeRef<slice::grammar::slice::Exception> > >, 1, 18446744073709551615, Some>::map<ref$<slice::grammar::slice::TypeRef<slice::grammar::slice::Exception> >,ref$<slice::grammar::slice::Exception>,slice:
                               at /rustc/59eed8a2aac0230a8b53e89d4e99d55912ba6b35\library\core\src\option.rs:836
  21:     0x7ff66d618044 - slice::grammar::slice::Exception::base_exception
                               at D:\IceRPC\icerpc-dev\slicec\src\grammar\slice.rs:255
  22:     0x7ff66d617f99 - slice::grammar::slice::Exception::all_members
                               at D:\IceRPC\icerpc-dev\slicec\src\grammar\slice.rs:248
  23:     0x7ff66d4117d4 - slicec_cs::exception_visitor::impl$0::visit_exception_start
                               at D:\IceRPC\icerpc-dev\slicec-cs\src\exception_visitor.rs:31
...

Convert deadline into a Field

deadline is currently a data member of the icerpc RequestHeader:

https://github.com/zeroc-ice/icerpc-csharp/blob/main/slice/IceRpc/Internal/IceRpcDefinitions.slice#L44

I propose to convert it into a field. See also #82.

Currently, the deadline data member is only set by the TimeoutInterceptor, either directly (you install a timeout interceptor in your pipeline) or by the Slice engine (you set an Invocation.Timeout and the Slice engine adds a TimeoutInterceptor as the first invoker).

On the server side, the deadline is only provided "for information", in particular a server is not expected or or supposed to enforce it. A server can also, if it wants, forwards this "for info" deadline in remote calls it makes. This is done by setting Invocation.Deadline to the received Dispatch.Deadline.

As a result, it would make sense to keep Dispatch.Deadline. We would retrieve it from a field instead of retrieving it from a request property.

Allowed Types for Dictionary Keys

The allowed types for dictionary key with Slice1 are (should be) the same as in Ice:
https://doc.zeroc.com/ice/3.7/the-slice-language/user-defined-types/dictionaries#id-.Dictionariesv3.7-AllowableTypesforDictionaryKeysandValues

(structures in Ice = compact struct in IceRPC)

What about Slice2?

Definitely allowed types:

  • integral types (signed, unsigned and varint)
  • bool
  • string
  • compact struct of the above (recursively)

Definitely disallowed types:

  • float
  • sequence
  • dictionary

TBD types:

  • proxy (not allowed with Slice1)
  • optional types, e.g. int?, string?
  • non-compact struct
  • non-compact struct with tagged members
  • exceptions
  • custom
  • trait

Slice driven encoding (revised)

This is new revised & simplified version of #69.

1. Eliminate payload encoding

Proxies, icerpc requests and icerpc responses no longer carry a "payload encoding", because the caller always knows how to decode a response. This knowledge is derived from:

  • the result type of the response
  • possibly parameters including in the request ("please send me a response payload encoded in HTML")
  • possibly one or more fields attached to the response ("this response payload is encoded in HTML")
  • possibly information in the payload itself

When sending requests and receiving responses through Slice-generated code, this knowledge comes purely from the ResultType carried by the response:

  • ResultType = Success (0): the payload holds a return value encoded with the same Slice encoding as the request payload
  • ResultType = Failure (1): the payload holds a Slice exception encoded with the Slice encoding of the protocol
  • ResultType = 2: the payload holds a Slice exception encoded with the same Slice encoding as the request payload

2. Each Slice file can start with an encoding definition such as:

encoding = 1.1;
encoding = 2;
encoding = 3;

where encoding is a new Slice keyword.

encoding, when present, must be the first definition in a Slice file (only comments, whitespaces and attributes can appear before it).

No encoding definition in a Slice file is equivalent to:

encoding = 2;

This definition means that
(a) all definitions in the Slice file must be encodable with the specified encoding
(b) the args and return value of all operations in this Slice file are encoded with the specified encoding (and only that encoding)

For example:

encoding = 2;
module Demo;

interface Greeter
{
    sayHello(greeting: string) -> string;
}

Both the client generated code and server generated code expect and accept only encoding Slice 2 for operation sayHello. This information is not transmitted with icerpc requests and responses since it's part of the contract between the client and server.

Example 2:

encoding = 1.1;
module Demo;

enum Fruit { Apple, Orange } // ok, 1.1-compatible
encoding = 2;
module Demo;

interface Foo
{
    op(fruit: Fruit) -> int?; // ok, reuse Fruit enum and encodes it with Slice 2.
}

3. A type defined in a file with encoding = n:

  • is encodable/decodable with n
  • is not encodable/decodable with m when m < n
  • is encodable/decodable with p (with p > n) IF this type would be valid in an encoding = p file

A consequence of this rule is a type defined in an encoding = 1.1 file can only use and depend on types defined in other encoding = 1.1 files. For example:

encoding = 1.1
module Demo;

interface Foo
{
    op(bar: Bar);
}

and say Bar is an interface. Bar must be defined in an encoding = 1.1 file, even though the encoding of a Bar proxy defined in an encoding = 2 file is doable.

As a result, proxies passed in encoding = 1.1 operations are always encoding=1.1 (in the 1.1 ProxyData), since they necessarily refer to proxies for interfaces defined in encoding = 1.1 files.

Change Slice file extension to .slice

We should change the extension for Slice files using the new Slice IceRPC syntax to .slice because:

  • it's consistent with the language name
  • it's consistent with the tool names (slicec-...)
  • it's different from the name used by ZeroC Ice; this way a tool named ice2slice could convert file Foo.ice into file File.slice in the same directory

I assume this can also help with syntax highlighting as the .ice and .slice formats will be pretty different.

Rework TypeAlias

I think we currently don't distinguish typelias from other types

This is correct.
The idea behind type-aliases is that they're for convenience when writing your Slice definition.
After parsing is complete, everywhere where a type-alias was used is replaced with the actual underlying type.

For example:

typealias MyType = [cs:type("foo")] sequence<int>;
class C {
    my_data: MyType,
}

After parsing, this will become:

typealias MyType = sequence<int>;
class C {
    my_data: [cs:type("foo")] sequence<int>,
}

There is currently no way to tell whether a type was originally from an alias or not.
To implement this proposal we'd need to add either
A) Add an extra field to TypeRef which stores a Option<TypeAlias> that we could check to if an alias was used
B) Change the parser to no longer "parse out" aliases.

Originally posted by @InsertCreativityHere in icerpc/icerpc-csharp#674 (comment)

Change uses_classes implementation on class to search data members

Currently the uses_classes implementation for Class always returns true:

impl Type for Class {

    ...


    fn uses_classes(&self) -> bool {
        true
    }

    ...

}

I believe this API leads to confusion which was encountered in PR #148. As it seems that the intent is for uses_classes to return true if any members within the "Container" type are a class. Something along the lines of this seems more logical:

    fn uses_classes(&self) -> bool {
        self.all_members()
            .iter()
            .any(|member| member.data_type.is_class_type())
    }

Slice attribute syntax

In IceRPC, Slice metadata is called Slice attributes, and we use the following syntax:

[oneway] // a Slice attribute with no parameter

[compressArgs] // another Slice attribute with no parameter
[compressReturn]

The case is camelCase, like operation names in Slice.

The only allowable attribute parameters are strings, and they are double-quoted. For example:

[someName("someParam")]

Slice also provide tool specific attributes, with the tool-name prefix, for example cs::

[cs::readOnly] // a slicec-cs attribute with no parameter

[cs::namespace("IceRpc")] // a slicec-cs attribute with a parameter

[cs::generic("SortedDictionary")] // another slicec-cs attribute with a parameter

The scope separator is the usual Slice scope separator, ::.

Slice: switch from C-style to Pascal-style

The Slice language is currently a C-style language, with:

  • return values specified on the left hand-side of an operation (e.g. int op())
  • parameter and data member definitions use the Type name syntax

This is a proposal to switch to a Pascal / Rust / Swift -like syntax:

  • return values would move to the right handside after an arrow as in rpc op() -> int
  • parameters and data members use the name: Type syntax like in Rust (TBD use comma or semicolon for data member separators?)

For example:

// existing Slice
interface Greeter
{
    string greet(string message);
}

becomes:

interface Greeter
{
    rpc greet(message: string) -> string;
}

There are two advantages to switching:

  • the Pascal style allows to qualify an operation or a parameter / return without ambiguity, e.g.
   idempotent rpc op() -> int; // op is idempotent, not its return value

   rpc op() -> stream int; // op returns a stream of int; op itself is not "streamed"
  • the Pascal / Swift / Rust style is now the prevalent style, used by all modern languages, in particular those we want to support in the near future (Rust, TypeScript, Kotlin, Swift, Go).

The only C-style language we want to support with IceRPC is C#.

Note that Protobuf is an unusual mix of both styles: message definitions use C-style while rpc definitions use Pascal style.

Compiler Crashes when Parsing an Enum with no Underlying type, if it hasn't seen an `int` Primitive.

Like the title says, this crashes the compiler:

module Foo
{
    enum MyEnum {}
}

All user defined types are stored in the AST vector, but primitives are stored in a special primitive_cache. Since they are used heavily, we do this to avoid creating a new int object everytime the user uses int. Instead we have one intinstance stored in the cache, which is used for allints. But we lazily fill the cache. We only add a longinstance into it the first time we parse along`.

The crash likely happens because an enum without an underlying type is implicitly backed by int. In this situation, we reference the int object, but it doesn't exist, since the parser never parsed an int, and hence created an int object in the primitive cache.

If this is the reason, we should create the entire cache upfront when parsing begins, instead of being fancy and filling it lazily.

Enum with associated values

This is a proposal to add associated values to Slice enum, similar to Swift and Rust.
See:
https://docs.swift.org/swift-book/LanguageGuide/Enumerations.html
https://doc.rust-lang.org/rust-by-example/custom_types/enum.html

The syntax of Slice enums with associated values is similar to Rust/Swift:

enum WebEvent
{
    PageLoad,
    PageUnload,
    KeyPress(byte key),
    Paste(string str),
    Click(long x, long y)
}

Syntactically, it's similar to an operation with parameters and no return value.

A Slice enum where any enumerator has one or more associated values:

  • cannot be unchecked
  • cannot have an explicit value for any of its enumerators (like = 5)
  • cannot have an underlying integer type (like enum WebEvent : byte)

In C#, WebEvent and its enumerators would map to a small generated record hierarchy:

public record WebEvent
{
    public sealed record PageLoad() : WebEvent;
    public sealed record PageUnload() : WebEvent;
    public sealed record KeyPress(byte Key) : WebEvent;
    public sealed record Paste(string Str) : WebEvent;
    public sealed record Click(long X, long Y) : WebEvent;
}

Usage: https://dotnetfiddle.net/LH5C6q

This also works well with switch case and switch expression, since we can match on the type (like WebEvent.KeyPress).

panic compiling ClassTests.ice

d:\IceRPC\icerpc-dev\target\debug\slicec-cs.exe -Rd:\IceRPC\icerpc-csharp-newcompiler\slice -R. --output-dir generated ClassTests.ice
thread 'main' panicked at 'called `Option::unwrap()` on a `None` value', slicec\src\ptr_util.rs:122:30
stack backtrace:
   0: std::panicking::begin_panic_handler
             at /rustc/59eed8a2aac0230a8b53e89d4e99d55912ba6b35\/library\std\src\panicking.rs:517
   1: core::panicking::panic_fmt
             at /rustc/59eed8a2aac0230a8b53e89d4e99d55912ba6b35\/library\core\src\panicking.rs:101
   2: core::panicking::panic
             at /rustc/59eed8a2aac0230a8b53e89d4e99d55912ba6b35\/library\core\src\panicking.rs:50
   3: enum$<core::option::Option<ptr_const$<dyn$<slice::grammar::traits::Type> > >, 1, 18446744073709551615, Some>::unwrap<ptr_const$<dyn$<slice::grammar::traits::Type> > >
             at /rustc/59eed8a2aac0230a8b53e89d4e99d55912ba6b35\library\core\src\option.rs:735
   4: slice::ptr_util::WeakPtr<dyn$<slice::grammar::traits::Type> >::borrow<dyn$<slice::grammar::traits::Type> >
             at D:\IceRPC\icerpc-dev\slicec\src\ptr_util.rs:122
   5: slice::grammar::slice::TypeRef<dyn$<slice::grammar::traits::Type> >::definition<dyn$<slice::grammar::traits::Type> >
             at D:\IceRPC\icerpc-dev\slicec\src\grammar\slice.rs:769
   6: slice::grammar::wrappers::impl$1::concrete_type<dyn$<slice::grammar::traits::Type> >
             at D:\IceRPC\icerpc-dev\slicec\src\grammar\wrappers.rs:141
   7: slicec_cs::slicec_ext::type_ref_ext::impl$0::to_non_optional_type_string<dyn$<slice::grammar::traits::Type> >
             at D:\IceRPC\icerpc-dev\slicec-cs\src\slicec_ext\type_ref_ext.rs:40
   8: slicec_cs::slicec_ext::type_ref_ext::impl$0::to_type_string<dyn$<slice::grammar::traits::Type> >
             at D:\IceRPC\icerpc-dev\slicec-cs\src\slicec_ext\type_ref_ext.rs:58
   9: slicec_cs::slicec_ext::member_ext::impl$1::to_type_string
             at D:\IceRPC\icerpc-dev\slicec-cs\src\slicec_ext\member_ext.rs:60
  10: slicec_cs::slicec_ext::member_ext::impl$2::to_tuple_type::closure$0
             at D:\IceRPC\icerpc-dev\slicec-cs\src\slicec_ext\member_ext.rs:92
  11: core::iter::adapters::map::map_fold::closure$0<ref$<ref$<slice::grammar::slice::Parameter> >,alloc::string::String,tuple$<>,slicec_cs::slicec_ext::member_ext::impl$2::to_tuple_type::closure$0,core::iter::traits::iterator::Iterator::for_each::call::closure
             at /rustc/59eed8a2aac0230a8b53e89d4e99d55912ba6b35\library\core\src\iter\adapters\map.rs:84
  12: core::iter::traits::iterator::Iterator::fold<core::slice::iter::Iter<ref$<slice::grammar::slice::Parameter> >,tuple$<>,core::iter::adapters::map::map_fold::closure$0>
             at /rustc/59eed8a2aac0230a8b53e89d4e99d55912ba6b35\library\core\src\iter\traits\iterator.rs:2170
  13: core::iter::adapters::map::impl$2::fold<alloc::string::String,core::slice::iter::Iter<ref$<slice::grammar::slice::Parameter> >,slicec_cs::slicec_ext::member_ext::impl$2::to_tuple_type::closure$0,tuple$<>,core::iter::traits::iterator::Iterator::for_each::c
             at /rustc/59eed8a2aac0230a8b53e89d4e99d55912ba6b35\library\core\src\iter\adapters\map.rs:124
  14: core::iter::traits::iterator::Iterator::for_each<core::iter::adapters::map::Map<core::slice::iter::Iter<ref$<slice::grammar::slice::Parameter> >,slicec_cs::slicec_ext::member_ext::impl$2::to_tuple_type::closure$0>,alloc::vec::spec_extend::impl$1::spec_ext
             at /rustc/59eed8a2aac0230a8b53e89d4e99d55912ba6b35\library\core\src\iter\traits\iterator.rs:733
  15: alloc::vec::spec_extend::impl$1::spec_extend<alloc::string::String,core::iter::adapters::map::Map<core::slice::iter::Iter<ref$<slice::grammar::slice::Parameter> >,slicec_cs::slicec_ext::member_ext::impl$2::to_tuple_type::closure$0>,alloc::alloc::Global>
             at /rustc/59eed8a2aac0230a8b53e89d4e99d55912ba6b35\library\alloc\src\vec\spec_extend.rs:40
  16: alloc::vec::spec_from_iter_nested::impl$1::from_iter<alloc::string::String,core::iter::adapters::map::Map<core::slice::iter::Iter<ref$<slice::grammar::slice::Parameter> >,slicec_cs::slicec_ext::member_ext::impl$2::to_tuple_type::closure$0> >
             at /rustc/59eed8a2aac0230a8b53e89d4e99d55912ba6b35\library\alloc\src\vec\spec_from_iter_nested.rs:56
  17: alloc::vec::spec_from_iter::impl$0::from_iter<alloc::string::String,core::iter::adapters::map::Map<core::slice::iter::Iter<ref$<slice::grammar::slice::Parameter> >,slicec_cs::slicec_ext::member_ext::impl$2::to_tuple_type::closure$0> >
             at /rustc/59eed8a2aac0230a8b53e89d4e99d55912ba6b35\library\alloc\src\vec\spec_from_iter.rs:33
  18: alloc::vec::impl$18::from_iter<alloc::string::String,core::iter::adapters::map::Map<core::slice::iter::Iter<ref$<slice::grammar::slice::Parameter> >,slicec_cs::slicec_ext::member_ext::impl$2::to_tuple_type::closure$0> >
             at /rustc/59eed8a2aac0230a8b53e89d4e99d55912ba6b35\library\alloc\src\vec\mod.rs:2486
  19: core::iter::traits::iterator::Iterator::collect<core::iter::adapters::map::Map<core::slice::iter::Iter<ref$<slice::grammar::slice::Parameter> >,slicec_cs::slicec_ext::member_ext::impl$2::to_tuple_type::closure$0>,alloc::vec::Vec<alloc::string::String,allo
             at /rustc/59eed8a2aac0230a8b53e89d4e99d55912ba6b35\library\core\src\iter\traits\iterator.rs:1745
  20: slicec_cs::slicec_ext::member_ext::impl$2::to_tuple_type
             at D:\IceRPC\icerpc-dev\slicec-cs\src\slicec_ext\member_ext.rs:91
  21: slicec_cs::slicec_ext::operation_ext::operation_return_type
             at D:\IceRPC\icerpc-dev\slicec-cs\src\slicec_ext\operation_ext.rs:87
  22: slicec_cs::slicec_ext::operation_ext::impl$0::return_task
             at D:\IceRPC\icerpc-dev\slicec-cs\src\slicec_ext\operation_ext.rs:54
  23: slicec_cs::proxy_visitor::proxy_interface_operations
             at D:\IceRPC\icerpc-dev\slicec-cs\src\proxy_visitor.rs:419
  24: slicec_cs::proxy_visitor::impl$0::visit_interface_start
             at D:\IceRPC\icerpc-dev\slicec-cs\src\proxy_visitor.rs:67
  25: slice::grammar::slice::Interface::visit_with<slicec_cs::proxy_visitor::ProxyVisitor>
             at D:\IceRPC\icerpc-dev\slicec\src\visitor.rs:253
  26: slice::grammar::slice::Module::visit_with<slicec_cs::proxy_visitor::ProxyVisitor>
             at D:\IceRPC\icerpc-dev\slicec\src\visitor.rs:184
  27: slice::grammar::slice::Module::visit_with<slicec_cs::proxy_visitor::ProxyVisitor>
             at D:\IceRPC\icerpc-dev\slicec\src\visitor.rs:180
  28: slice::grammar::slice::Module::visit_with<slicec_cs::proxy_visitor::ProxyVisitor>
             at D:\IceRPC\icerpc-dev\slicec\src\visitor.rs:180
  29: slice::slice_file::SliceFile::visit_with<slicec_cs::proxy_visitor::ProxyVisitor>
             at D:\IceRPC\icerpc-dev\slicec\src\visitor.rs:162
  30: slicec_cs::try_main
             at D:\IceRPC\icerpc-dev\slicec-cs\src\main.rs:71
  31: slicec_cs::main
             at D:\IceRPC\icerpc-dev\slicec-cs\src\main.rs:42
  32: core::ops::function::FnOnce::call_once<void (*)(),tuple$<> >
             at /rustc/59eed8a2aac0230a8b53e89d4e99d55912ba6b35\library\core\src\ops\function.rs:227
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.

Quotation marks aren't parsed correctly in unquoted metadata

The Slice preprocessor (handled by MCPP) does string-aware scanning of source code and expects quotation marks to be balanced and strings to be well-formed.

This clashes with the fact that unquoted metadata should be able to contain arbitrary text (barring '[' and ']') and treat it as plain-text. In the future when we revisit or replace the Slice preprocessor we should address this.

Doc comments are not correctly parsed

With the new compiler from #50 seeing this, I think this was already fixed in dev by @externl

/// <summary>/// Tests whether the target service implements the specified Slice interface.
        /// /// @param id The Ice type ID of the Slice interface to test against.
        /// /// @return True when the target service implements this interface; otherwise, false.</summary>
        /// <param name="invocation">The invocation properties.</param>
        /// <param name="cancel">A cancellation token that receives the cancellation requests.</param>

This results in https://docs.microsoft.com/en-us/dotnet/csharp/misc/cs1573?f1url=%3FappId%3Droslyn%26k%3Dk(CS1573)
and prevents the code to compile

Slice driven encoding

Currently, the Slice encoding of a request payload depends on:

  • the parameters of the operation: if there is any Slice parameter, the encoding is 1.1. Otherwise, keep going
  • the encoding of the proxy
    • if specified, the specified encoding is used
    • if not specified, the "default encoding" is the proxy's protocol encoding

And this works today since the proxy has always a protocol (ice1 or ice2).

This is a proposal to:

  • stop using the proxy to select the encoding, and as a result remove the encoding property of proxies
  • specify the Slice encoding to use when encoding a request payload in the Slice definitions, and nowhere else

With this proposal, it's also possible/easier to make the Ice protocol of a proxy truly optional.

Proposal

Encoding Attributes

Add [encoding11] and [encoding20] attributes to Slice operations and the containers of operations (interfaces, module openings).

This attribute applies only to request parameters and means: when encoding these parameters, the Slice engine uses the specified encoding. For example:

[encoding11]
interface Test
{
    op(str: string) -> string; // if I send an op request, the payload will be encoded with encoding11
}

This attribute is not used for response payloads: the server still uses the request's payload encoding to encode the response (like today).

Remove the "+" in encoding 1.1

Encoding 1.1 is about interop with ZeroC Ice. There is no point to support new Slice syntax such as optional types and streams with 1.1.

Enforce Encoding Compatibility During Slice Compilation

Unlike what we do today, the Slice compiler does not automatically infer the encoding to select from the parameters/return of your operation. If you specify [encoding11], all the parameters and return value of your operation must be 1.1-compatible (else, compilation failure). Likewise, if you specify [encoding20], all the parameters and return value of your operation must be 2.0-compatible (else, compilation failure).

If you don't specify anything, it's equivalent to specifying [encoding20] on the enclosing module opening*.

For example:

interface Foo
{
    op1(str: string) -> string; // ok, 2.0 encoding
    op2(c: MyClass);  // failure, can't encode MyClass with 2.0
    op3() -> MyClass; // failure, can't return MyClass from an encoding20 operation
    [encoding11] op4(str: string?) -> string?; // failure: can't send or return an optional string with 1.1
}

(*) module opening because the attribute applies to all the interfaces/operations in this particular module opening, not all opening of this module.

The net effect is most of the time the encoding of your request and even response is fully specified. This proposal uses attributes (not keywords) for encoding which allows for client / server Slice definition mismatches. For example:

For example:

interface Foo // client version
{
    [encoding11] op1(str: string) -> string;
}
interface Foo // server version
{
    op1(str: string) -> string;
}

Here the client will send a 1.1-encoded request and the server will return a 1.1-encoded response, even though the server Slice definitions make no mention of 1.1.

Tagged members not marked as optional do not emit errors

Currently, we are not verifying that tagged members must be optional. For example, this test fails because the error reporter contains no errors. This is a bug as we require tags to be used on optional types.

    #[test]
    #[ignore] // TODO: We do not verify that tagged data members are optional.
    fn tagged_data_members_must_be_optional() {
        // Arrange
        let slice = "
        encoding = 1;
        module Test;
        class C {
            i: int32,
            s: string,
            b: tag(10) bool,
        }
        ";

        let error_reporter = parse_for_errors(slice);

        // Assert
        assert_errors!(error_reporter, &["Tagged data members must be optional"]);
    }

exception enum

Our current remote exceptions are “exception struct/class”, which works reasonably well for language with exception support. The application can define a bunch of exceptions (without inheritance for the 2.0 encoding) and then catch them:

try
{
    await prx.OpAsync();
}
catch (FooException ex) // specific exception
{
}
catch (BarException ex) // specific exception
{
}
catch (RemoteException ex) // base exception 
{
}
catch
{
}

However:

  • defining several unrelated exceptions/errors for a component is not idiomatic in Rust
  • our base RemoteException is now more useful (with error message and origin included all the time): for most applications, it provides all the data needed. So it’s likely exceptions will be usually empty or the application won’t define its own exceptions and just throw a plain RemoteException

What is more idiomatic in Rust is for a component to define its own Error type (struct) with an associated ErrorKind enum, for example: https://doc.rust-lang.org/std/io/struct.Error.html

In Swift, using an enum directly as an error type seems common: https://docs.swift.org/swift-book/LanguageGuide/ErrorHandling.html

In Slice, we could add an exception enum construct that is closer to Rust / Swift:

exception enum SomeException { A, B, C }

This way, an application could easily use a more Rust-y/Swift-y approach and define a single exception enum per application component.

One candidate for this consolidation is actually IceRpc itself, that currently defines 4 exceptions (all empty): ServiceNotFoundException, OperationNotFoundException, UnhandledException and DispatchException. We could convert them to a single exception:

exception enum DispatchException
{
    ServiceNotFound,
    OperationNotFound,
    Unhandled,
    ImplementationLimit, // This corresponds to most or all our uses of DispatchException today
}

Encoding-independent Exceptions

This proposal for encoding-independent exceptions is expressed in C# but is submitted to icerpc because (if adopted) it would serve as a template for all language mappings. Some aspects are also programming language independent.

  1. All remote exceptions in C# derive from the base exception class IceRpc.RemoteException.

A "remote exception" is an exception that originates in a remote peer and gets transmitted via ice or icerpc to the local peer where it's usually thrown.

public abstract class RemoteException : System.Exception // not defined in Slice
{
    // nothing remarkable
}
  1. There are several kinds of remote exceptions:
  • invocation exceptions
    an invocation exception is an exception reported by a client to server during a request invocation. For example, if a client sends a request to a server and encounters an error while sending the request's payload to the server, it "throws" an invocation exception to the server.
enum InvocationErrorCode : ulong // actual range is 0..2^62-1
{
    NoError = 0,
    ... enumerators...
}

public sealed class InvocationException : RemoteException
{
    public InvocationErrorCode ErrorCode { get; }

    public InvocationException(InvocationErrorCode errorCode)
    {
        ... throws ArgumentException is errorCode is NoError
    }
}

An invocation exception is encoding-independent and does not provide any support for Slice encoding.

  • dispatch exceptions
    A dispatch exception is an exception reported by a server/service to a cllent during a request dispatch. For example, if a Server has no dispatcher set, it throws to the client a DispatchException(ServiceNotFound).

Another example is a service receiving a streamed payload from a client and a later portion of this streamed payload can't be decoded. The service can then throw to the client a DispatchException(PayloadDecodingFailed).

public readonly record struct RemoteExceptionOrigin // not defined in Slice
{
    public string Path { get; init; }
    public string Fragment { get; init; }
    public string Operation { get; init; }
}

enum DispatchErrorCode : ulong // actual range is 0..2^62-1
{
    NoError = 0,
    Unspecified = 1,
    ServiceNotFound = 2,
    OperationNotFound = 3,
    ... many more ...
}

public sealed class DispatchException : RemoteException // not defined in Slice
{
     public bool ConvertToUnspecified { get; set; }
     public DispatchErrorCode ErrorCode { get; }
     public RemoteExceptionOrigin Origin { get; init; }
}

Like an invocation exception, a dispatch exception is encoding-independent and doesn't provide any support for Slice encoding.

ConvertToUnspecified avoids accidental propagation of dispatch exceptions. When a dispatch exception is received from a remote peer, ConvertToUnspecified is set to true. And when a dispatch exception is about to be sent to a remote peer and ConvertToUnspecified is true, we actually send DispatchException(Unspecified).

  • Slice-defined exceptions

Exceptions defined in Slice files and generated by the Slice compiler all derive from AnyException, which itself derives from RemoteException:

public class AnyException : RemoteException // AnyException is the base class for Slice-defined exceptions in C#
{
     public bool ConvertToUnhandled { get; set; }
     public RemoteExceptionOrigin Origin { get; internal set; } // just like DispatchException
     public RetryPolicy RetryPolicy { get; }

     // Encode/Decode support with SliceEncoder/Decoder
}

Note: a Slice-defined exception IS NOT a dispatch exception because dispatch exceptions can be thrown at times when Slice-defined exceptions cannot be thrown.

  1. A middleware can throw dispatch exceptions; they are transmitted to the client by IceRPC. The fidelity of this transmission depends on the RPC protocol.

If a middleware throws another kind of exception, such as a Slice exception, the exception gets transmitted to the client as DispatchException(Unspecified).

  1. The Slice engine (and other future encoding engines) must not let its exceptions flow to the middleware chain. The Slice engine is responsible for catching its exceptions and creating an outgoing response with a Slice-encoded payload.

In C#, this means IceRpc.Service (part of the Slice engine) must catch exceptions - especially Slice-defined exceptions - and create an OutgoingResponse from those exceptions. Currently, it doesn't, see:
https://github.com/zeroc-ice/icerpc-csharp/blob/main/src/IceRpc/Service.cs#L122

It's also there that we insert the logic for converting a ConvertToUnhandled==true exception to a Slice-defined UnhandledException:

// Slice
exception UnhandledException // just like today
{
}

The Slice engine also converts other exceptions - such as ArgumentException - into UnhandledException and sends the full stack trace to the Slice client when built Debug.
When the Slice engine catches a DispatchException with ConvertToUnspecified set to true, it converts this exception into an UnhandledException. When ConvertToUnspecified is false, it lets this exception through to the middleware chain.

  1. Transmission of invocation exceptions and dispatch exceptions over ice

An invocation exception is not transmitted over ice and kills the connection.

A dispatch exception is transmitted as a system exception over ice (replyStatus > 1) with the following mapping:
ServiceNotFound = ObjectNotExist
OperationNotFound => OperationNotExist
other => Unknown, message includes error code value and name if available

Likewise, an ice system exception is "decoded" as a DispatchException with the following mapping:
ObjectNotExist || FacetNotExist => ServiceNotFound
OperationNotExist => OperationNotFound
Unknown, UnknownLocal, UnknownUserException => Unspecified, message includes Unknown's message

  1. Transmission of invocation exceptions and dispatch exceptions over icerpc

An invocation exception or dispatch exception is transmitted over icerpc's underlying multiplexed transport using a RESET_STREAM or similar frame with an application error code equal to the InvocationErrorCode or DispatchErrorCode. The exception is then recreated as-is on the other side.

Convert idempotent into a Field

idempotent is currently a data member of the icerpc RequestHeader:

https://github.com/zeroc-ice/icerpc-csharp/blob/main/slice/IceRpc/Internal/IceRpcDefinitions.slice#L44

I propose to convert it into a field.

The logic is as follows:

  • a request property is a property used by the IceRPC core
  • a field is for something not used by the IceRPC core, and typically used by interceptors and middleware

Today, idempotent is used for:

  1. retry by the local Retry interceptor (don't need a request property or field for this usage)
  2. retry by a Retry interceptor in a remote intermediary server (need a field for this usage)
  3. error checking by the Slice engine. The Slice generated code makes sure there is no mismatch between the Slice operation in the client and the Slice operation in the server: if the Slice client specified idempotent, the Slice server must also specify idempotent on the operation (but not vice-versa).

The Slice engine is not a core feature and as a result a field is also suitable for this error checking.

Naturally we should be able to do "2" and "3" without any interceptor in the Slice client. This means the Slice engine needs to set:

  • an "idempotent operation" field, and
  • an "idempotent operation" feature for the local Retry interceptor, if present

Add file scoped modules

Just like new C# file scoped namespaces, or java packages we should add file scoped modules.

[cs:namespace("ZeroC.Demo")]
module Demo;

struct Foo {}

or

module ZeroC::Demo;

struct Foo {}

And like C# if you use file scoped modules there cannot be additional nested modules in the file.

Compiler type lookup failure?

module M0::M2::M3
{
    struct  X { int y; }
    module M0
    {
    
        struct X { int J; }
        interface Operations
        {
            void op(X x1, M0::M2::M3::X x2);
        }
    }
}

The compiler fails to lookup X here and then panics, this is the case for dev and PR #50

The old C++ compiler correctly compiles this and the generated code has the expected parameters:

OpAsync(X x1, global::M0.M2.M3.X x2, ....

The new compiler correctly works if we use ::M0::M2::M3::X x2 FQN, but shouldn't it be able to handle both cases like the old compiler?

problem resolving primitive types typealias

d:\IceRPC\icerpc-csharp-newcompiler\tests\IceRpc.Tests.Slice>d:\IceRPC\icerpc-dev\target\debug\slicec-cs.exe -Rd:\IceRPC\icerpc-csharp-newcompiler\slice -R. AliasTests.ice --output-dir generated
error:   --> 36:9
   |
36 |         boolA opBoolA(boolA p1);␍␊
   |         ^---
   |
   = expected operation
error:   --> 36:9
   |
36 |         boolA opBoolA(boolA p1);␍␊
   |         ^---
   |
   = expected operation
error:  --> 7:19

Move IceRpc::Slice to Slice

Slice is a subcomponent of IceRPC, however, the Slice language is in theory not limited to one implementation. It could be implemented by:

  • ZeroC Ice
  • IceRPC
  • some future RPC framework, e.g. FireRPC

So I think it would make sense to move module IceRpc::Slice to a top-level Slice module. In IceRPC C#, we would map it IceRpc::Slice. For example:

encoding = 1.1;

[cs:namespace(IceRpc.Slice)]
module Slice;

interface Service { ... } // type ID is ::Slice::Service

FireRPC could ship fully compatible definitions with different attributes:

encoding = 1.1;

[cs:namespace(FireRpc.Slice)]
module Slice;

interface Service { ... }  // type ID is ::Slice::Service

Remove min wire size

We no longer use the min wire size in slicec-cs, so we should remove it from slicec.

Sequences as dictionary keys

module Demo
{
    struct MyStruct
    {
        int x;
        int y;
    }

    sequence<MyStruct> MyStructSeq;
    dictionary<MyStructSeq, string> MyDict;

    interface Operations
    {
         void opSeq(MyDict d);
    }
}

This Slice produces a warning, but the generated code doesn't compile

Hello.ice:14: warning: use of sequences in dictionary keys has been deprecated
1>D:\IceRPC\icerpc-csharp\examples\Hello\Client\generated\Hello.cs(184,17,186,222): error CS0029: Cannot implicitly convert type 'System.Collections.Generic.Dictionary<Demo.MyStruct[], string>' to 'System.Collections.Generic.Dictionary<System.Collections.Generic.IList<Demo.MyStruct>, string>'

Error code support

As mentioned by @pepone in PR #112, error code support could be a nice addition.
It would allow for the tests to avoid having to string match against the errors and would be useful for when we write documentation.

cs:namespace does not work

Slice

[cs:namespace(IceRpc)]
module Ice
{
    /// The identity of a service reachable with the ice1 protocol.
    [cs:readonly]
    struct Identity
    {

C# generated code

[assembly:IceRpc.Slice.Slice("Identity.ice")]

namespace Ice
{
    /// <summary>/// The identity of a service reachable with the ice1 protocol.</summary>
    public readonly partial record struct Identity
    {

The generated code should be in IceRpc namespace not in Ice namespace

Enum refactoring

In ZeroC Ice, an enum:

  • cannot have an explicit underlying type
  • is encoded as a size (range: 0..Int32Max)
  • checked vs unchecked semantics are language mapping dependent
  • is typically mapped to an enum with the default underlying type (int)

Proposal

See also #87.

  1. For encoding = 1.1, we keep checked vs unchecked enums but disallow underlying types. The implicit underlying type is varint but cannot be explicitly stated in a 1.1 enum definition.

We disallow enumerators outside range 0..Int32Max.

If you define:

encoding = 1.1

unchecked enum Foo
{
}

and attempt to encode a -1 Foo with the 1.1 encoding, you'll get a runtime error. The same works fine with the 2.0 encoding. (That's obviously a rare corner case)

  1. We support the following underlying types with 2.0:
  • byte, short, ushort, int, uint (like today, i.e. fixed size integers)
  • varint, varuint, varlong, varulong (not supported as of today)
  1. encoding = 2 is just like encoding = 1.1 except:
  • you can specify an underlying type
  • you can define enumerators with negative values (if within underlying type's range)
  • for unchecked enums, you can encode/decode any value within the underlying's range (for 1.1, the range is always 0..Int32Max).

Slice custom type

Slice allows users to define custom types with an attribute on compact struct.

The idea is the Slice definition is the fallback when there is no attribute (such as cs:type) that specifies the mapped type (and encoding/decoding) in the target language.

The custom keyword goes one step further: the user must specify the type and encoding/decoding through a language-mapping attribute; if he doesn't, the code generation fails.

The syntax is similar to traits, e.g.

custom DateTime; // attribute must specify mapped type, encoding and decoding.

Naturally it makes sense to use the same attributes as for compact struct.

Rename Ice encoding to Slice encoding

I propose to rename "Ice encoding" to "Slice encoding", so:

  • Ice 1.0 => Slice 1.0
  • Ice 1.1 => Slice 1.1
  • Ice 2.0 => Slice 2.0
  • Ice20Decoder => Slice20Decoder etc.

Slice encoding makes more sense than Ice encoding as it's about encoding Slice-defined types. The implementation of the Ice/Slice encoding is part of the Slice engine/sub-namespace. Using Slice to talk about the encoding (e.g. [encoding] Slice 2.0) also reduces confusion with the Ice protocol (e.g. [protocol] Ice 2).

The parser is Completely Whitespace Insensative

In places where whitespace should be required as a delimiter, the parser is completely insensitive to whitespace, treating module Foo and moduleFoo as identical.

This is because of Pest's special WHITESPACE rule, which can be implicitly matched in between any 2 other tokens when enabled. This needs to be disabled in most of our rules, and whitespace needs to be explicitly handled in these instead.

enhance pest errors

Before switching to the new compiler we need a workaround for this error reporting,

slicec-cs.exe --output-dir Client\generated Hello.ice
error:   --> 12:23
   |
12 |     [cs:generic(List)]sequence<MyEnum> MyEnumSeq;␍␊
   |                       ^---
   |
   = expected enum_start, line_doc_comment, block_doc_comment, module_kw, struct_kw, class_kw, exception_kw, interface_kw, or type_alias_kw
Compilation failed with 1 error(s) and 0 warning(s).

We discussed this in https://zeroc.slack.com/archives/C01QREU29D1/p1634148755045000

Otherwise, when compiling multiple files this error would be a night manner

Slice driven encoding and the ice protocol

My assumptions are:

  • we implement Slice driven encoding for the IceRPC framework as described in #87
  • we only support interop with ZeroC Ice applications (client and server) that use the 1.1 encoding
    • we don't care about ZeroC Ice applications that use the 1.0 encoding
    • and we don't care about ZeroC Ice applications (if any) that rely on the ice major.minor encoding in request/response to be something other than 1.1

Proposal

a) when IceRPC sends a request or response over ice, and this request or response contains an encapsulation, IceRPC always writes "1.1" as encapsulation's encoding. We do this blindly: when we encode the outgoing ice request/response header, we have no idea which encoding was used to encode the payload bytes.

b) when IceRPC receives a request or response over ice, and this request or response contains an encapsulation, IceRPC checks the encapsulation's encoding is 1.1. Any other value is an error. This 1.1 has no particular meaning and is not remembered. The Slice engine uses Slice-driven encoding (as usual) to decode the payload.

In practice this means a IceRPC app will interoperate with a ZeroC Ice app provided both use 1.1: the Ice app because 1.1 is its main/default encoding and the IceRPC app because of encoding = 1.1 in Slice definitions.

An IceRPC app can also talk to another IceRPC app using any encoding (such as the slice 2 encoding). Both ends read/write "1.1" as the encapsulation encoding and ignore it.

Now, if we decide to add support for encoding = 2 in ZeroC Ice 3.8 (maybe just a couple language mappings), we would switch to Slice-driven encoding in ZeroC Ice 3.8 as well and ignore proxy encoding, -e x.y, encoding in encapsulations etc. They would all remain 1.1.

Better Numeric Types

In most languages nowadays int, long are just aliases for the language's corresponding int32, int64 type depending on the architecture.

In Swift for instance Int is Int32 on 32-bit platforms and and Int64 on 64-bit platforms. C++ has a similar "feature".

I think it's often common these days to prefer use of explicitly sized types. int32/int64.

I propose renaming our integer types to reflect their on the wire bit size.

Current Proposed
bool bool
byte uint8 / int8
short int16
int int32
long int64
ushort uint16
uint uint32
ulong uint64
varuint varuint32
varlong varuint64
float float32
double float64

Allow anonymous sequence and dictionaries

As of Ice 3.7, Slice does not support anonymous sequences or dictionaries. You have to name them.

This works:

sequence<string> StringSeq;
struct St
{
     StringSeq seq;
}

interface Intf
{
    void op(StringSeq seq); 
}

This doesn't:

struct St
{
     sequence<string> seq;
}

interface Intf
{
    void op(sequence<string> seq); 
}

Since most programming languages support anonymous sequences (list/arrays) and dictionaries (map), this restriction is not strictly necessary. In some languages like Java, the alias in not used at all in the generated API.

In the rare situations where a named type is required - like some JavaScript maps - the Slice compiler could generate a name when mapping anonymous types. For example St_Seq and Intf_Op_Seq.

case conversion does not handle snake_case

We have this code for case conversion

match case {
        CaseStyle::Camel => s.to_owned(), // strings are in camel-case by default.
        CaseStyle::Pascal => {
            let mut chars = s.chars();
            // We already handled empty strings, so unwrap is safe; there must be at least 1 char.
            let first_letter = chars.next().unwrap();

            // We capitalize the first letter and convert it to an owned string, then append the
            // rest of the original string to it. The 'chars' iterator skipped over the first char
            // when we called 'next', and so only contains the rest of the string.
            //
            // We need to 'collect' here, since 'to_uppercase' returns an iterator. 1 lowercase
            // grapheme can produce multiple graphemes when capitalized in UTF8.
            first_letter.to_uppercase().collect::<String>() + chars.as_str()
        }
        CaseStyle::Snake => {
            s.to_owned() // TODOAUSTIN I need to actually write this logic.
        }
    }

This doesn't correctly handle ice_ping, we cannot assume strings are in camel-case, this produces Ice_PingAsync the expected result is IcePingAsync

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.