Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: preserve named types in semantic types and when translating between Motoko and Candid #4943

Open
wants to merge 22 commits into
base: master
Choose a base branch
from

Conversation

crusso
Copy link
Contributor

@crusso crusso commented Mar 11, 2025

Inspired by discussion with @giorgiopiatti-dfinity

Both Motoko and Candid support syntactically named types for more informative type (especially function) signatures.

Eg.

(x: Int, y:Int) -> (z:int) 

vs.

(Int, Int) -> (Int)

These are purely syntactic and erased from the syntax before type checking. They are not preserved by type checking nor by translation to Candid.

This PR attempts to preserve name types throughout front-end type checking to produce better Candid and more informative types. Named types are erased, along with type fields, in early IR pass erase_typ_fields, so most ir passes are unaffected.

This requires fairly extensive surgery to ignore names in various operations on types, e.g. sub-typing and type equivalence.

Example Candid output:

names.mo:

actor {

  /// named
  public func named(x:Nat, y:Nat) : async (r : Nat) {
    x + y
  };

  /// anon
  public func anon(_ : Nat, _ : Nat) : async Nat {
    0
  };

  /// record
  public func record({x:Nat; y:Nat}) : async {r:Nat} {
    {r = x + y}
  };

  /// function arg/ret
  public func f(named : shared (x:Nat, y:Nat) -> async (r : Nat)) :
     async (unamed : shared (Nat, Nat) -> async Nat) {
    named
  };

  /// actor arg/ret
  public func g(A :
    actor {
      named : shared (x:Nat, y:Nat) -> async (r : Nat);
      anon : shared (Nat, Nat) -> async Nat
     }) :
     async (actor {
      named : shared (Nat, Nat) -> async Nat;
      anon : shared (x:Nat, y:Nat) -> async (r : Nat)
     }) {
    A
  };

  /// escape candid keywords
  public func escape(int : Int, bool : Bool, service : actor {}) :
   async (int : Int, bool : Bool, service : actor {}) {
    (int, bool, service)
  }
}

now produces Candid:

service : {
  /// anon
  anon: (nat, nat) -> (nat);
  /// escape candid keywords
  escape: ("int": int, "bool": bool, "service": service {}) -> 
    ("int": int, "bool":  bool, "service": service { });
  /// function arg/ret
  f: (named: func (x: nat, y: nat) -> (r: nat)) -> 
       (unamed: func (nat, nat) -> (nat));
  /// actor arg/ret
  g: (A:
   service {
     anon: (nat, nat) -> (nat);
     named: (x: nat, y: nat) -> (r: nat);
   }) ->
   (service {
      anon: (x: nat, y: nat) -> (r: nat);
      named: (nat, nat) -> (nat);
    });
  /// named
  named: (x: nat, y: nat) -> (r: nat);
  /// record
  "record": (record {
               x: nat;
               y: nat;
             }) -> (record {r: nat;});
}

In comparison, master branch produces:

service : {
  /// anon
  anon: (nat, nat) -> (nat);
  /// escape candid keywords
  escape: (int, bool, service {
                      }) -> (int, bool, service {
                                        });
  /// function arg/ret
  f: (func (nat, nat) -> (nat)) -> (func (nat, nat) -> (nat));
  /// actor arg/ret
  g: (service {
        anon: (nat, nat) -> (nat);
        named: (nat, nat) -> (nat);
      }) -> (service {
               anon: (nat, nat) -> (nat);
               named: (nat, nat) -> (nat);
             });
  /// named
  named: (nat, nat) -> (nat);
  /// record
  "record": (record {
               x: nat;
               y: nat;
             }) -> (record {r: nat;});
}

To avoid information overload, we suppress names under object fields, tags and options.

TODO:
[ ] perhaps drop names when reporting type errors, since they lead to spurious syntactic differences (either by erasing names before printing or instructing the printer to drop names in certain contexts (e.g. when displaying two types that should be related).
[x] add space before around colon (current is consistent with current formatting of candid fields/tags)
[x] suppress names under object fields, tags and options (too much information)




crusso added 12 commits March 11, 2025 12:28

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
…rough Named types
@crusso
Copy link
Contributor Author

crusso commented Mar 12, 2025

This is what base/OrderedMap renders as:

module {
    type Map<K, V> = {root : Tree<K, V>; size : Nat};
    type Operations<K> =
      {
        all : <V>(m : Map<K, V>, pred : (K, V) -> Bool) -> Bool;
        contains : <V>(m : Map<K, V>, key : K) -> Bool;
        delete : <V>(m : Map<K, V>, key : K) -> Map<K, V>;
        empty : <V>() -> Map<K, V>;
        entries : <V>(m : Map<K, V>) -> Iter__1<(K, V)>;
        entriesRev : <V>(m : Map<K, V>) -> Iter__1<(K, V)>;
        foldLeft :
          <Value, Accum>(map : Map<K, Value>, base : Accum,
                         combine : (Accum, K, Value) -> Accum) ->
            Accum;
        foldRight :
          <Value, Accum>(map : Map<K, Value>, base : Accum,
                         combine : (K, Value, Accum) -> Accum) ->
            Accum;
        fromIter : <V>(i : Iter__1<(K, V)>) -> Map<K, V>;
        get : <V>(m : Map<K, V>, key : K) -> ?V;
        keys : <V>(m : Map<K, V>) -> Iter__1<K>;
        map : <V1, V2>(m : Map<K, V1>, f : (K, V1) -> V2) -> Map<K, V2>;
        mapFilter :
          <V1, V2>(m : Map<K, V1>, f : (K, V1) -> ?V2) -> Map<K, V2>;
        maxEntry : <V>(m : Map<K, V>) -> ?(K, V);
        minEntry : <V>(m : Map<K, V>) -> ?(K, V);
        put : <V>(m : Map<K, V>, key : K, value : V) -> Map<K, V>;
        remove : <V>(m : Map<K, V>, key : K) -> (Map<K, V>, ?V);
        replace : <V>(m : Map<K, V>, key : K, value : V) -> (Map<K, V>, ?V);
        size : <V>(m : Map<K, V>) -> Nat;
        some : <V>(m : Map<K, V>, pred : (K, V) -> Bool) -> Bool;
        validate : <V>(m : Map<K, V>) -> ();
        vals : <V>(m : Map<K, V>) -> Iter__1<V>
      };
    Make : <K>(compare : (K, K) -> Order) -> Operations<K>;
    Operations : <K>(compare : (K, K) -> Order) -> Operations<K>
  }

Copy link

github-actions bot commented Mar 12, 2025

Comparing from 825d2a4 to 3853a61:
In terms of gas, no changes are observed in 5 tests.
In terms of size, no changes are observed in 5 tests.

@crusso crusso marked this pull request as ready for review March 13, 2025 14:58
@crusso crusso requested a review from a team as a code owner March 13, 2025 14:58
@crusso crusso changed the title experiment: preserve named types when translating between Motoko and Candid feat: preserve named types in semantics types and when translating between Motoko and Candid Mar 13, 2025
@crusso crusso changed the title feat: preserve named types in semantics types and when translating between Motoko and Candid feat: preserve named types in semantic types and when translating between Motoko and Candid Mar 13, 2025
@crusso
Copy link
Contributor Author

crusso commented Mar 19, 2025

Fixes #4344.

Copy link
Contributor

@luc-blaeser luc-blaeser left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for doing this PR. Let's merge.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants