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

Spritely Goblins and Agoric CapTP interop discussion #1827

Closed
cwebber opened this issue Sep 30, 2020 · 24 comments
Closed

Spritely Goblins and Agoric CapTP interop discussion #1827

cwebber opened this issue Sep 30, 2020 · 24 comments
Labels
enhancement New feature or request

Comments

@cwebber
Copy link
Contributor

cwebber commented Sep 30, 2020

Spritely is getting to a more serious state, and the Spritely Goblins layer is now implementing CapTP (still a few things to be done, but I think it's maybe at feature-parity with Agoric's implementation... a little bit ahead actually because we now have distributed acyclic garbage collection). Of course, that couldn't have happened without the wonderful help of folks at Agoric and on the cap-talk list who answered many questions for me.

Now the question is, can we get our CapTP implementations to interop? Obviously I'm not the only one that thinks it's a good idea.

So let's talk about what's different so far, of substance and non-substance.

Non-substantial things to eventually address:

  • We don't use the same serialization format. Not a big deal, this is easy enough to converge on the same thing. I think we can both flesh out our implementations and find out our needs for the moment; neither of us is yet using the serialization format that we want long-term so it's easy enough to both switch later to something shared.
  • Agoric's CapTP sees Vats as the fundamental barrier between connections and Goblins' sees Machines as the barrier. However, this might also not matter because really either can be perceived of as "the name that holds many objects that you're talking to", and you can't really tell whether or not its one or several event loops at this point.
  • Making sure that all our operations look the same

Of medium substance:

  • Making sure we have the same scalars.
  • Goblins' CapTP provides an open world "extension type type" and I think that's a good idea for Agoric to have too.
  • Should method names be symbols or strings? It's symbols conventionally in Goblins. I think that's the right call, especially because...
  • Goblins is providing an optimization where symbols can be shortened and cached for a session. One side can say "I'm assigning this symbol to integer N", not unlike "I'm exporting this promise to integer N". This can reduce message sizes significantly.

Of major substance:

  • AFAICT, Agoric's CapTP has no notion of a "session" ever breaking because it's assumed that store and forward mostly solves this. However a) Goblins needs to support low-latency, non-store-and-forward connection for some game purposes (though we want to support store and forward as well) and those will of course break and b) I think even store and forward sessions can end up in an irreconcilable state; if Alice believes the last message she received from Bob was 3, but Bob's machine goes up in flames and is restored from a backup with 2, and there are no records of 3 sitting around to be shared back to Bob again because they weren't held onto or GC'ed or whatever, then there's no way for those two to talk any more and a new session must be established anyway.
  • For that matter, Goblins needs to correctly handle session disconnects too, heh
  • Both of us need to implement handoff tables
  • Both of us want to implement certificate chains, so we should examine a shared format

The biggest one:

  • In Goblins's world, lambda is the ultimate. Any actor is merely providing a method handler as a procedure. Method dispatch is one particular kind of method that dispatches to a sub-procedure based on the first argument, but it's not fundamental. Therefore in Goblins, there is only one kind of message send, <-, and Lambda is the Ultimate. In Agoric's world, there are multiple kinds afaict: one for invoking methods, one for invoking procedures, one for fetching attributes (or are they called properties? I forget what term is used). Additionally, Agoric's implementation has a very nice feature where some kinds of scalars can also be indexed against to retrieve by numerical or string keys in a promise pipelining system, and Goblins doesn't provide this yet.

I have a suggestion on how to solve this last one taking good ideas for both but it's worth making a separate post on this issue for that since this is already long. :)

Thankfully, our worlds are made easier by both of us keeping the CapTP layer separate from what's traditionally called the VatTP layer (confusing name imo, but not a big deal... I like MachineTP better for multiple reasons but we can talk about that later.)

Okay... so I think that lays out the scope of stuff to think about for interoperability. Let's plan on getting our stuff talking!

@cwebber cwebber added the enhancement New feature or request label Sep 30, 2020
@zenhack
Copy link

zenhack commented Sep 30, 2020

To pile onto this issue a bit, I'd like to also talk about possible compatibility with capnproto rpc. It seems likely that it may be hard to just shoehorn Goblins and Agoric's stuff into capnproto, and given that we already have a deployed protocol with several implementations that limits our ability to make big protocol changes on our end, but I suspect if we're careful we can design things so that it is at least possible to write a proxy that bridges the two protocols. There is a bit more flexibility on the capnproto end with features not-yet implemented.

If folks are open to using capnp at least at the serialization level, that could simplify things a bit.

I think probably the biggest divergence in design is that capnproto is all built around static types. But I think from an API perspective we could solve that by, when goblins/agoric objects are viewed from capnproto, they see an interface like:

interface Goblin {
  callFn @0 (args :List(Value)) -> (results :LIst(Value));
  callMethod @1 (name :Text, args :List(Value)) -> (results :List(Value));
  # ...
}

...with the exact set of methods signatures matching whatever you guys decide to support; see @cwebber's comment about lambdas only vs. separate method/property access. the Value type would contain whatever data types exist on the agoric/goblins protocol level; you might have:

struct Value {
  union {
    string @0 :Text;
    float @1 :Float64;
    list @2 :List(Value);
    # ...
  }
}

When viewing capnproto objects from the agoric/goblin side, we could have the bridge use introspection (which is not implemented, but has been brought up on the mailing list recently) to map the untyped method calls to whatever methods that the object actually supports.

@erights
Copy link
Member

erights commented Oct 1, 2020

a little bit ahead actually because we now have distributed acyclic garbage collection

Excellent! Distributed acyclic gc needs local WeakRefs, which JS now has! Our on-chain implementation also needs determinism, and it will be a long time until we have good support for deterministic WeakRefs for on-chain use (long story). Thus, we do not expect on-chain computation to emit gc info for now. However, our solo vats do not need determinism, and so should be able to do distributed acyclic GC. Any vat, on chain or off, should be able to react to gc messages it receives since there's nothing non-deterministic about that. If you're following the way E/CapTP of old did it, I don't think this will be a barrier to interoperation. Doing our side of the distributed acyclic gc protocol is not top of our priority list; but if you send us a PR I'll happily review it!

  • We don't use the same serialization format.

You're correct that we should both converge on something non-JSON-based. But in the meantime, JSON is rather universal. Of course, we have another level of encoding into JSON (marshal module) since JSON itself is not adequately expressive. But you already have plenty of experience with exactly that with json-ld and ocap-ld. How hard would it be to write bi-directional converters from your serialization format to our marshal encoded JSON? We should try this first, so we can learn enough to productively work towards converging on something else.

  • Agoric's CapTP sees Vats as the fundamental barrier between connections and Goblins' sees Machines as the barrier. However, this might also not matter because really either can be perceived of as "the name that holds many objects that you're talking to", and you can't really tell whether or not its one or several event loops at this point.

Exactly. We're already making use of this transparency when a vats in one SwingSet talks to vats in another SwingSet. To each normal vat, the SwingSet kernel seems to be the rest of the world. There's a special (normally) per-swingset vat we call the "comms vat" that speaks captp to the rest of the world. To the swingset kernel, the comms vat seems to be the rest of the world. To the comms vat, the swingset kernel seems to be all local vats. Each remote swingset appears the same as each remote solo vat. So there should not be an issue here.

  • Making sure we have the same scalars.

Scalars:

  • Distinct null and undefined. Sorry, we have no choice but to insist :(
  • boolean: true and false
  • string: All strings encodable in JSON. @FUDCo , what does JSON say about bare UTF-16 surrogate pairs? I no longer remember.
  • number: IEEE double precision floating point numbers, including one NaN, +Infinity, -Infinity, and one zero. There is only one NaN value. Our marshal layer canonicalizes -0 to 0.
  • We currently do not consider symbols to be passable. However, in the past we did admit registered symbols and we may again.
  • Goblins' CapTP provides an open world "extension type type" and I think that's a good idea for Agoric to have too.

I don't understand what this is but you have me curious. Set us a PR and I'll happily review it!

  • Should method names be symbols or strings? It's symbols conventionally in Goblins. I think that's the right call, especially because... [optimization]

We should be careful about whether we mean the same thing by the term "symbol". In any case, we can both provide the equivalent optimization for string method names. Let's not worry much about optimization until we consider transitioning from JSON.

  • AFAICT, Agoric's CapTP has no notion of a "session" ever breaking because it's assumed that store and forward mostly solves this. However a) Goblins needs to support low-latency, non-store-and-forward connection for some game purposes (though we want to support store and forward as well) and those will of course break and b) I think even store and forward sessions can end up in an irreconcilable state; if Alice believes the last message she received from Bob was 3, but Bob's machine goes up in flames and is restored from a backup with 2, and there are no records of 3 sitting around to be shared back to Bob again because they weren't held onto or GC'ed or whatever, then there's no way for those two to talk any more and a new session must be established anyway.

I like that point. In any case, we need to support partitioned connections anyway, as that is fundamental to the meaning of some transports, such as postMessage between workers with the browser. This requires us to have a clean semantics for severing ephemeral references, and a clean relationship between ephemeral references that get severed and non-ephemeral references that can be reconnected. Unfortunately, some of the elements of E semantics that helped are no longer available to us. This will be an important design issue that is not yet settled and will need to be.

  • For that matter, Goblins needs to correctly handle session disconnects too, heh

;)

  • Both of us need to implement handoff tables

Yes. Although we've reluctantly come to the conclusion that we cannot always shorten across arbitrary mixtures of transports. Nevertheless, we should shorten across enough of the cases that matter, and currently do not. When we cannot shorted, we need a better understanding of how to cope with the resulting identity confusions.

  • Both of us want to implement certificate chains, so we should examine a shared format

We should converge on the wormhole-op-like chain of unacknowledged A-to-C messages as extra payload in the A-to-B message where Alice introduces Bob to Carol. Unlike all other certificate chains, this is perpetually self shortening, and has zero length chains in quiescence.

The biggest one:

  • In Goblins's world, lambda is the ultimate. Any actor is merely providing a method handler as a procedure. Method dispatch is one particular kind of method that dispatches to a sub-procedure based on the first argument, but it's not fundamental. Therefore in Goblins, there is only one kind of message send, <-, and Lambda is the Ultimate. In Agoric's world, there are multiple kinds afaict: one for invoking methods, one for invoking procedures, one for fetching attributes (or are they called properties? I forget what term is used).

yes, "properties". Currently, we are not transmitting remote property gets. Only apply and applyMethod. You can without loss of generality consider both to be applyMethod, where apply is just applyMethod with undefined as the unique non-string method name for the function's call behavior.

Additionally, Agoric's implementation has a very nice feature where some kinds of scalars can also be indexed against to retrieve by numerical or string keys in a promise pipelining system, and Goblins doesn't provide this yet.

This is a form of property get, which we do not yet support remotely. But we will, so be prepared.

I have a suggestion on how to solve this last one taking good ideas for both but it's worth making a separate post on this issue for that since this is already long. :)

Looking forward!

Thankfully, our worlds are made easier by both of us keeping the CapTP layer separate from what's traditionally called the VatTP layer (confusing name imo, but not a big deal... I like MachineTP better for multiple reasons but we can talk about that later.)

Since it seems we're both agreed that neither side can tell the other is not a vat, and since the least the other side could be is a vat, I think we're already agreed on the semantics and "VatTP" is already a good name.

Okay... so I think that lays out the scope of stuff to think about for interoperability. Let's plan on getting our stuff talking!

YES!!!!

@erights
Copy link
Member

erights commented Oct 1, 2020

To pile onto this issue a bit, I'd like to also talk about possible compatibility with capnproto rpc. It seems likely that it may be hard to just shoehorn Goblins and Agoric's stuff into capnproto, and given that we already have a deployed protocol with several implementations that limits our ability to make big protocol changes on our end, but I suspect if we're careful we can design things so that it is at least possible to write a proxy that bridges the two protocols. There is a bit more flexibility on the capnproto end with features not-yet implemented.

Good! I very much want to see this work. I think that building a bridge / gateway that converts between the two protocols is a good approach, at least at first, as that let's the protocols remain different but practically interoperable. If more convergence is possible, a working bridge is a good place to stand to gradually learn how.

If folks are open to using capnp at least at the serialization level, that could simplify things a bit.

Unfortunately we have a political-ism closest-binary-attractor problem. @kentonv did capnp after he did proto2. Unsurprisingly, I prefer capnp to proto2. However, Cosmos / ICF / IBC, the foundation we layer VatTP and CapTP on top of, has converged on proto2. Thus, when we move from JSON to something reasonable, if it is proto-like, there are strong reasons pulling us towards proto2. I wish it were not so. This probably means we cannot actually get rid of the bridge / gateway between the protocols.

I think probably the biggest divergence in design is that capnproto is all built around static types. But I think from an API perspective we could solve that by, when goblins/agoric objects are viewed from capnproto, they see an interface like:

interface Goblin {
  callFn @0 (args :List(Value)) -> (results :LIst(Value));
  callMethod @1 (name :Text, args :List(Value)) -> (results :List(Value));
  # ...
}

...with the exact set of methods signatures matching whatever you guys decide to support; see @cwebber's comment about lambdas only vs. separate method/property access. the Value type would contain whatever data types exist on the agoric/goblins protocol level; you might have:

struct Value {
  union {
    string @0 :Text;
    float @1 :Float64;
    list @2 :List(Value);
    # ...
  }
}

Yes. This is the technique used for implementing dynamic languages in static languages --- all types of the dynamic language fit into branches of one type of the static language. This encoding of the dynamic language's types prevent it from talking to any pre-existing statically typed objects, except again via a bridge / gateway.

When viewing capnproto objects from the agoric/goblin side, we could have the bridge use introspection (which is not implemented, but has been brought up on the mailing list recently) to map the untyped method calls to whatever methods that the object actually supports.

Yes, perfect! Any idea when to expect it in capnp?

A possibly bigger issue separating capnp from CapTP is the issue of storage management. CapTP starts with the dynamic language perspective of a dynamic graph of objects that get gc'ed when unreachable. IIUC, capnp starts with a much more C++-inspired RAII perspective, probably even better suited to Rust, where the programmer must be aware of the transfer of the obligation to delete. The obligation to delete also implies to power to delete, which leads to different ocap design patterns. I'm not sure to what degree even a bridge / gateway can paper of the rift between these approaches. But I am hopeful!

In any case, I think Goblins and CapTP are much closer than either are to capnp, and so we should try reconciling Goblins and CapTP first, and without a gateway, before we proceed to capnp.

@FUDCo
Copy link
Contributor

FUDCo commented Oct 1, 2020

  • string: All strings encodable in JSON. @FUDCo , what does JSON say about bare UTF-16 surrogate pairs? I no longer remember.

Quoth ECMA-404: "whether a processor of JSON texts interprets such a surrogate pair as a single code point or as an
explicit surrogate pair is a semantic decision that is determined by the specific processor"
I.e., as far as JSON is concerned, they're just 16-bit blobs and what you make of them is your business.

@erights
Copy link
Member

erights commented Oct 1, 2020

Ok, what does EcmaScript standard JSON.parse and JSON.stringify do with them?

@FUDCo
Copy link
Contributor

FUDCo commented Oct 1, 2020

I believe the relevant link is https://www.ecma-international.org/ecma-262/#sec-utf16encoding

@zenhack
Copy link

zenhack commented Oct 2, 2020

Yes, perfect! Any idea when to expect it in capnp?

Hard to say; per the conversation it's been on @kentonv's TODO list for a while, and I don't know how likely Ryan is to plow forward and build this thing -- the perils of open source. It's also something where each implemenation would have to support it; it would not be sufficient for this to exist merely in the bridge.

A possibly bigger issue separating capnp from CapTP is the issue of storage management. CapTP starts with the dynamic language perspective of a dynamic graph of objects that get gc'ed when unreachable. IIUC, capnp starts with a much more C++-inspired RAII perspective, probably even better suited to Rust, where the programmer must be aware of the transfer of the obligation to delete. The obligation to delete also implies to power to delete, which leads to different ocap design patterns. I'm not sure to what degree even a bridge / gateway can paper of the rift between these approaches. But I am hopeful!

This is ultimately an issue for implementations of capnp in less RAII-oriented languages too. Note that at the protocol level I don't think anything is dfferent, and it's easy enough to wire things up the GC and making things work (as I do with in the Haskell implementation), but there do indeed exist APIs that treat dropping a capability as a semantically meaningful event.

iirc the node implementation allows you to call close() on a capability to explicitly finalize it. I've been meaning to add something similar to the Haskell implementation. But for capbility languages that's even maybe a bit dicey as a solution, since it means if you pass the capability to another local object that you don't trust, naively, that other object can revoke your access, so more thought is perhaps needed.

In any case, I think Goblins and CapTP are much closer than either are to capnp, and so we should try reconciling Goblins and CapTP first, and without a gateway, before we proceed to capnp.

This sounds reasonable; I will follow along and perhaps note any significant implications that occur to me.

@kentonv
Copy link

kentonv commented Oct 2, 2020

A possibly bigger issue separating capnp from CapTP is the issue of storage management. CapTP starts with the dynamic language perspective of a dynamic graph of objects that get gc'ed when unreachable. IIUC, capnp starts with a much more C++-inspired RAII perspective, probably even better suited to Rust, where the programmer must be aware of the transfer of the obligation to delete. The obligation to delete also implies to power to delete, which leads to different ocap design patterns. I'm not sure to what degree even a bridge / gateway can paper of the rift between these approaches. But I am hopeful!

That's not quite right. Cap'n Proto uses reference counting, much like CapTP. You can very well drop references based on GC finalization (if your language supports that), and some implementations do.

That said, in my experience, I have found that deterministic destruction and RAII is powerful for making many kinds of capability interactions feel more reliable and solid. Take the revoker pattern, for instance. In a RAII model, I can reasonably expect "fail-closed" behavior -- if anything goes wrong, the capability gets revoked, rather than being left exposed. CapTP has reactToLostClient() for this purpose, but it seems much more awkward to use. What if the capability has multiple clients, and only one has been lost? Having reactToLostClient() perform auto revocation seems too harsh. On the other hand, what if the capability has one client, but due to a bug in that client's code, the client "forgets" about the revoker? The power of RAII is that it makes it hard to "forget" to clean up.

So Cap'n-Proto-based protocols -- at least the ones I design -- are more likely to rely on deterministic destruction.

On another note, realistically, I do not believe distributed garbage collection is a tractable problem. In order to perform well, GC algorithms are deeply dependent on heuristic amortization. Most GCs are dependent on "memory pressure" signals to activate them, but what is memory pressure in a distributed system with many different vats that each have their own memory? If one vat in the network signals it is under memory pressure, are all other vats obliged to spin up their GCs to try to release capabilities that might point into the pressure'd vat? That seems problematic for a huge number of reasons.

My understanding is that CapTP never really did solve distributed GC, but made the assumption that it could be solved later. My take is that it can't be solved and we should instead embrace explicit reference counting.

@zenhack
Copy link

zenhack commented Oct 2, 2020

I think the right approach at an API level probably depends on:

  • The characteristics of your particular language, both its idioms and the details of how its GC works for your particular runtime
  • The details of the particular protocol

The Go implementation has shifted to a more explicit refcounting approach as @kentonv suggests, since in Go a well tuned program can go a very long time between GC cycles, and the runtime does not have good visibility into the true cost of remote capabilities.

For something like Haskell on the other hand, GC cycles run very frequently anyway; programs tend to allocate like crazy, and the major GC is not incremental, so it's not obvious to me that this is going to be a problem in practice, for most protocols.

For particular APIs it may make sense to handle resources in a way that is more manual however, if the underlying objects are likely to be significantly more expensive than the memory footprint of a client would expect. This mirrors the reality of managing lifetimes of e.g. files in languages that are otherwise GC oriented, and I think it probably makes sense to have a similar dual manual/automatic design for APIs like this. But I've found it's common for the memory usage of a server object to not be that dramatically different from the client stub. It seems like a shame to impose the burden of manual resource management on all APIs just for the benefit of what I suspect are a minority of cases.

@zarutian
Copy link
Contributor

zarutian commented Oct 4, 2020

One non JSON, non capnproto binary format you might want to look at. It is inspired by and was meant as a replacement of JOSS in Captp of E.

@erights
Copy link
Member

erights commented Oct 4, 2020

Any macro space benchmarks on what the size ratio is compared to JSON? Also, given that both this binary format or JSON would then be compressed by some generic fast compression algorithm, say zip, what's the size ratio post compression?

@zarutian
Copy link
Contributor

zarutian commented Oct 5, 2020

With msgpack without use of the extension types listed in that EBNF file the size is slightly less than JSON. And due to higher entropy per bit, it does not compress well with deflate.

However if the serializer uses the letrec and ibid extension type agressively then quite a lot of repeated items in a captp message flow will only require, 2, 3, 4, or 6 bytes per ibid.

Main focus of mine when I composed this format was more on ease of implementation on various platforms such as mid range MCUs, self-descriptivity, and some support for streaming encoding and decoding.

@MostAwesomeDude
Copy link

I figure that I should chime in; in the Thanksgiving spirit, one big issue is better than two little issues, and after this, I'll go and have a Taco Tuesday that cannot be beat and not get up until the next morning.

The short version is that Monte has now a very basic, bumbling CapTP based on JSON encoding and AMP framing. The protocol is one-directional, can't build cycles, doesn't do GC, and has serious speed issues. I have a raytracer which I use as a speed test; the distributed version is much slower than the single-core version.

Part of this speed loss is due to encoding overhead, and we'll probably use Capn Proto RPC eventually simply because it promises to be less slow; pure-Monte decoding is often dominated by copy overhead, but we can do zero-copy slicing of buffers just like the Capn intended. (Our JSON decoding is also pure-Monte and not terribly fast. Many things could be optimized.)

From an interoperability standpoint, Monte is very much like Goblins. Objects have only one single invokable behavior, the call, which has only one signature: a triple of a string, a list of refs, and a map of strings to refs. Incidentally, this signature encapsulates multimethods, E-style calls, Capn-style calls, and even verbless function calls, but we probably can't put this to use other than via Postel's Law.

I should also point out that, once everything snaps together and into place, we expect to start doing fun high-level things. Already, the distributed raytracer works by compiling Monte code for raytracing maths into a so-called "muffin" module which has no dependencies, packing the muffin into a compact bytecode, and shipping the bytecode across AMP. This suggests that we could start to host Monte objects as una.

Right now we have to deal with some details in our Capn implementation, and I need to implement weakmaps, but we should continue plodding along.

@cwebber
Copy link
Contributor Author

cwebber commented Dec 2, 2020

From an interoperability standpoint, Monte is very much like Goblins. Objects have only one single invokable behavior, the call, which has only one signature: a triple of a string, a list of refs, and a map of strings to refs. Incidentally, this signature encapsulates multimethods, E-style calls, Capn-style calls, and even verbless function calls, but we probably can't put this to use other than via Postel's Law.

I think this is The Right Thing and it could be great if Agoric would also adopt the "one true invocation path" approach... just make method dispatch on objects a special case. When handling an incoming message, if a function, pass in all arguments. When passing a message to an object, take the first argument as a string (or symbol or whatever, sounds like string here though) and apply it to that method. As for the property-access for arrays/hashmaps which @erights told me is also used for promise pipelining, this too can be done. Lambda the Ultimate.

There is one potential problem with the above for Agoric, I'd suspect, which is when you want to do method invocation or property access on an object... I anticipate (but am not sure) that doing property access on an object effectively constructs a facet with only that property on it. Is this correct @erights? If so there are two workarounds: 1) make a magic method for property access, like .__getProp("prop") and any remoteObject.prop is translated as such or 2) simply construct facets manually.

I'm arguing this very strongly for a good reason, and not just that it makes my life (and @MostAwesomeDude's) easier: I tried adding an approach where function invocation, property access, and method invocation were all handled as three separate thing. This made a mess of become, and my programs started breaking and I lost all the lovely easy-to-program-with-time-travel stuff I made. I can expand on why this is but maybe it doesn't matter here.

Anyway, we need to decide on a unified thing: I'd argue the simplest is best. The fact that @MostAwesomeDude has found that a lambda-the-ultimate approach worked with something that's probably much closer to python/javascript than scheme indicates that it could probably work for Agoric as well.

But @erights, I suspect you have opinions. Care to share them?

@cwebber
Copy link
Contributor Author

cwebber commented Dec 2, 2020

BTW another thing to work out: keywords!

Given that I just argued for lambda-the-ultimate, the "easiest" approach would be to say "there is only one argument list, and that is the positional argument list". Which means that you just layer keywords on top of that. Various languages do this, in different ways, but translating between them is probably no problem from an ocap perspective. Just to list them (and this is off the top of my head, I might get something wrong):

  • Javascript: one of your arguments might be the "keywords argument", really just a hashmap.
  • RnRS (standard vanilla) Scheme: Keywords? What keywords?!?!
  • Common Lisp and Guile: We have this special type which is the "keyword" type, and some functions do pair-grouping of (func :key1 argument1 :key2 argument2 ...) (in Guile, this is not even built in, but an abstraction, define*, which provides this behavior.

Aside from the question of "do we need to add keywords as a type in CapTP" (let's pretend we didn't hear anyone ask that) the nice thing here is that Lambda is the Ultimate and all of these can call each other just fine.

However, some languages do things "Differently" (and Differently is the name of a dragon that eats standardization efforts): Python and Racket actually have a separate syntax/argument list for applying keyword arguments. Thus Goblins, being first built on Racket, follows this convention: messages have a slot for both positional arguments (a list) and keyword arguments (a hashmap) when encoded across the wire. Racket has made very good CS hygiene arguments as to why this is a good idea but that's kind of irrelevant because we're living in a world where other languages have no idea what this is. But I'm planning a port of Goblins to Guile soon anyway, aiming for my first CapTP across implementations interop test (since it should be mostly easy, aside from this), so this is a problem I'll need to consider anyway.

Here's an observation though: all of these languages support passing arguments via an argument list, even if they also support passing arguments via a separate keyword map. Probably the easiest thing to do (even if it may feel less nice when doing captp across eg two python or two racket implementations) is to say "Sorry, you only get positional arguments."

I could look it up, but @MostAwesomeDude, what does AMP do btw?

@MostAwesomeDude
Copy link

@washort and I had a series of discussions about this, culminating in a realization years ago in a Portland bar: The correct way to do it is what Racket and Python (and Monte) do, with both a list of arguments and also a (string-indexed) map of named arguments. The main reasoning in favor is indeed some abstract CS principles; it's about how to upgrade objects in the presence of optional behaviors and arguments, and it's akin to using row polymorphism for the indices of the calling convention.

TBH if we had had a faster implementation plan and better data structures (some sort of hybrid ordered HAMT?), then Monte probably would have stopped having anonymous positional arguments. The readability argument alone is incredibly compelling, and when mixed with the upgrade argument, it's almost sublime. Indeed, our Capn Proto libraries use keyword arguments to build message buffers, precisely because it's going to be less brittle overall and matches Capn's own preferred semantics for upgrade.

AMP only supports string-indexed maps of strings. This makes decoding conversations easy. Paraphrasing our AMP implementation, we send the JSON-armored encoding of a map like this:

def map := [=> target, => verb, => args, => namedArgs]
# FAIL is the ejector which ends the turn
def payload := JSON.encode(map, FAIL)
# Third argument indicates whether we want a reply for this question
def rv := amp.send("call", [=> payload], true)

It would be possible to push the one map into the other map, flattening them by one level. We'll probably do that for Capn Proto RPC, turning the argument list into a special nameless keyword argument. (Capn Proto RPC has a position for selectors, which could be used to store verbs.)

@zarutian
Copy link
Contributor

zarutian commented Feb 1, 2021

From an interoperability standpoint, Monte is very much like Goblins. Objects have only one single invokable behavior, the call, which has only one signature: a triple of a string, a list of refs, and a map of strings to refs. Incidentally, this signature encapsulates multimethods, E-style calls, Capn-style calls, and even verbless function calls, but we probably can't put this to use other than via Postel's Law.

I think this is The Right Thing and it could be great if Agoric would also adopt the "one true invocation path" approach... just make method dispatch on objects a special case. When handling an incoming message, if a function, pass in all arguments. When passing a message to an object, take the first argument as a string (or symbol or whatever, sounds like string here though) and apply it to that method. As for the property-access for arrays/hashmaps which @erights told me is also used for promise pipelining, this too can be done. Lambda the Ultimate.

There is one potential problem with the above for Agoric, I'd suspect, which is when you want to do method invocation or property access on an object... I anticipate (but am not sure) that doing property access on an object effectively constructs a facet with only that property on it. Is this correct @erights? If so there are two workarounds: 1) make a magic method for property access, like .__getProp("prop") and any remoteObject.prop is translated as such or 2) simply construct facets manually.

I'm arguing this very strongly for a good reason, and not just that it makes my life (and @MostAwesomeDude's) easier: I tried adding an approach where function invocation, property access, and method invocation were all handled as three separate thing. This made a mess of become, and my programs started breaking and I lost all the lovely easy-to-program-with-time-travel stuff I made. I can expand on why this is but maybe it doesn't matter here.

Anyway, we need to decide on a unified thing: I'd argue the simplest is best. The fact that @MostAwesomeDude has found that a lambda-the-ultimate approach worked with something that's probably much closer to python/javascript than scheme indicates that it could probably work for Agoric as well.

But @erights, I suspect you have opinions. Care to share them?

Here is why the Lambda Ultimation on the protocol level is immaterial in my opinion:

If the protocol has three diffrent kinds of deliverOps & deliverOnlyOps then the protocol decoder can just funnel them together into one kind of invocation, following a convention, on the vat interrior side. The local remote ref proxy presences can defunnel invocations, that follow the same convention, into the apropos protocol deliverOps & deliverOnlyOps.

If there is only one kind of deliverOps & deliverOnlyOps then, if the convention is known, the invocations, property accesses, and function/closure can be defunneled on the vat interrior side. Conversely the local remote proxy presence can do the funneling for those into the one deliverOps & deliverOnlyOps protocol messages.

@cwebber
Copy link
Contributor Author

cwebber commented Apr 9, 2021

Looks like @zarutian is surprising us all by already exploring implementing Spritely's version of CapTP on Javascript / Agoric's stuff...!

  • JS Syrup implementation (Syrup is the current binary encoding I'm using for Goblins' CapTP; not intended to be a permanent choice, but it's a simple encoding of the abstract Preserves data types, which I think are very smart, but more bencode/csexps like to keep things simple... not claiming this is the encoding we'll land on, just the one we currently use for ease-of-thinking/porting... definitely better than Agoric's JSON stuff encoding, imo)
  • Spritely-style CapTP start! Earliest stages at time of writing but it's starting to put the pieces in place.

Whoa, I did not expect we'd be starting to have an interop conversation quite this fast...! Amazing work @zarutian, please keep it up!

@cwebber
Copy link
Contributor Author

cwebber commented Apr 9, 2021

Oh, also I started this OcapN writeup which might be the right place to start coordinating on "this generation" of CapTP?

@zarutian
Copy link
Contributor

A bit of a warning though, the code is somewhat messy and quite a few of the comments and commit messages are in Icelandic, at the time of this post.

@erights
Copy link
Member

erights commented Apr 19, 2021

See https://github.com/Agoric/agoric-sdk/pull/2909/files for a draft more detailed semantics of the Agoric CapTP's Passable. The parts above the divider (currently the entire PR) are the abstract syntax and semantics. Only the part below the divider are about our concrete encoding into JSON. Switching to a different concrete seriailzation (e.g., syrup) should not affect the part above the divider.

Although somewhat biased towards JS, the abstract semantics above the divider is intended to be language independent enough to serve as a basis of inter-language interoperability, e.g., with Goblins and perhaps with Cap'n Proto. We'll see.

@ghost
Copy link

ghost commented Jun 1, 2022

What's the status of the interop effort? Silvermint is likely to want CapTP in Go, and it would be nice if it worked with at least one of the implementations.

Also, it looks like maybe endojs/endo now hosts the canonical CapTP implementation rather than Agoric. Should I open an interop issue there referring to this one?

@zenhack
Copy link

zenhack commented Jun 1, 2022

This has mostly been coordinated on the repo @cwebber linked above: https://github.com/ocapn/ocapn

...though as you can see there's been a bit of a lull; iiuc @cwebber has been very busy getting the spritely institute up and running, so that's sapped some momentum, but hopefully this will get picked back up again.

If you're looking for CapTP in Go, you could use the capnproto implementation: https://github.com/capnproto/go-capnproto2. Part of the interop effort is making sure that whatever we settle on is at least bridgeable to capnproto. (Note that I recommend using the latest alpha rather than the v2 branch; despite not being finalized it fixes some systemic problems with v2 and is therefore going to be more reliable).

@dckc
Copy link
Member

dckc commented Dec 13, 2022

let's call this subsumed by the 19 Oct Spritely funding announcement and the work that's now underway:

@dckc dckc closed this as completed Dec 13, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

9 participants