Hypercosm / HIDL

Hypercosm Interface Description Language
https://hypercosm.dev/HIDL
0 stars 0 forks source link

[RFC] Object model #15

Closed aDotInTheVoid closed 2 years ago

aDotInTheVoid commented 2 years ago

Object model

HIDL is fundamentally object oriented. Their are 5 main components

Singletons

Singletons exist once, and dont inherit. They may be thaugh of as modules (TODO: Should we call them something else? modules?) to obtain one, call get_singleton or get_singletons on the Root singleton That is special, as Root always id=0, so you can use it to bootstrap the singletons

This method is special, as you need to downcast the vu64 to singleton ids, as this is not type safe, but this is the only place in the API where you do this, so we dont have a object type.

Each extension has an implicit singleton (but no longer an implicit interface, which was always a singleton anyway)

Classes

Classes can have several instances. To instantiate one, use a constuctor method on a singleton. eg

extension demo {
    methods {
        // TODO: Should classes have static methods that get hoisted to the extensions
        // inplicit singleton
        new_foo(initial: vi32) -> Foo
        default_foo() -> Foo
    }

    class Foo {
        get_val() -> vi32
        set_val(v: vi32)
    }
}

TODO: Should we rename objects/instances to avoid object refering both to the top level interface and instances of classes.

Interfaces

Classes and interfaces can all inherit 1 or more interfaces, and they all inherit the Object interface. The exception is the Object interface, which doesnt inherit anything.

TODO: Why do we have interfaces, as the way method resolution works, you cant call a method unless you know its type, so passing around interfaces is sort of useless. Should we just have Object be a special case.

Types

Objects can only be passed by ID, because they are opaque,

Types are data that can be passed, but cant have methods.

Types specify the data layout

Types can be struct (rust struct), enum (rust enum, but with no data in field), or flags (rust bitflags)

Extensions

Each extensions exists in its own file. Core is tecnicly modeled as an extension, even though it is mandatory.

All singletons/classes/interfaces/types/ must exist in an extension.

Each extension exists in a namespace, which may be nested a la java packages.

The offical hypercosm extensions exists in the hypercosm namespaces, eg hypercosm.core, hypercosm.asset_delivery

Each extesnions consists of a (potentialy empty) implicit singleton, and potentialy some named singletons, classes, interfaces?? and types

daeken commented 2 years ago

First, a question: What is the difference between an interface and a class? In the Core Protocol spec, we talk only about interfaces, but this seems to make a class more ... concrete? I guess I don't see a reason to separate the two.

Each extension has an implicit singleton

Extensions need not contain any interfaces, e.g. compression or scripting extensions.

TODO: Why do we have interfaces, as the way method resolution works, you cant call a method unless you know its type, so passing around interfaces is sort of useless. Should we just have Object be a special case.

The purpose of interface inheritance is to allow sharing code. It's very much an OO-y concept, but if I have some Shape interface and it specifies a bunch of common properties that the Triangle and Square child interfaces inherit, that can really simplify things. That said, I don't expect it'll get a ton of use.

All that being said, I agree with the thrust of the proposal.

aDotInTheVoid commented 2 years ago

First, a question: What is the difference between an interface and a class?

Each object/instance has 1 class and 1 or more interfaces. To make a call, you must know the class.

A class cannot be inherited from (ie is final). This means if we have a instance of a class we can get the id for each method. If you only know that a object with a certain ID implements a interface, then you still cant get a method number, as the underlying class may have another interface "above" it, which would push down the method

aDotInTheVoid commented 2 years ago

Extensions need not contain any interfaces, e.g. compression or scripting extensions.

I'm not sure how this is relevent this is . I don't think it makes sense to have multiple singletons for a extension, so I think saying theirs only one seems good

aDotInTheVoid commented 2 years ago

That said, I don't expect it'll get a ton of use.

Should we then say interfaces may give hints to the server/client about when they may do things with their code, but the IDL may not use them as args/return types for the IDL

daeken commented 2 years ago

If you only know that a object with a certain ID implements a interface, then you still cant get a method number, as the underlying class may have another interface "above" it, which would push down the method

Assuming that you're doing it at runtime (you're just handed an arbitrary object and need to do something with it), you can call list_interfaces and get the complete list of implemented interfaces. From that, you can do the standard depth-first method number assignment until you get to the function you want to call.

I'm not sure how this is relevent this is . I don't think it makes sense to have multiple singletons for a extension, so I think saying theirs only one seems good

I'm okay with saying there's either zero or one. That is, I think it should be 100% valid to have an extension that contains no interfaces, no classes, no structs, no enums, or anything else; just an empty shell should be fine.

I'm also not positive that we should limit extensions to a single singleton, but I can't see any immediate downside to doing so.

Should we then say interfaces may give hints to the server/client about when they may do things with their code, but the IDL may not use them as args/return types for the IDL

What's the advantage of that limitation? If that's in place, then (going back to my example) how could you have a method that takes both a Triangle and a Square, without it being able to accept a Shape argument?

aDotInTheVoid commented 2 years ago

I'm okay with saying there's either zero or one.

I think for the zero case, its neater to say their is a singleton that has no methods or events. And in the way im thinking for the syntax, this will have no effect anyway.

What's the advantage of that limitation?

I guess it can be relaxed, if you can do the interface lookup, and then get the ID from that. Althought we should probablly discourage anyway, as now a call takes 2 round trips and some computation (at least the first time), so its not accemptable for short lived objects, but maybe for long running ones

daeken commented 2 years ago

I think for the zero case, its neater to say their is a singleton that has no methods or events. And in the way im thinking for the syntax, this will have no effect anyway.

That's totally fair, and it can be understood that attempting to get an empty singleton is not guaranteed to give you an object back and may throw an error instead. Basically UB.

I guess it can be relaxed, if you can do the interface lookup, and then get the ID from that.

This actually raises an issue with my conception of the interface system: if Triangle implements interface Foo and then Shape, then the only way you can safely call Shape methods on it is by working out the command number on the fly...

Maybe we should just ditch inheritance entirely (aside from Object). It's complicated and I can't come up with a real-world usecase for it that isn't equally clear or simple without it.

aDotInTheVoid commented 2 years ago

can be understood that attempting to get an empty singleton is not guaranteed to give you an object back and may throw an error instead. Basically UB

The way I think of it is getting an empty singleton will return an ID, but you cannot call any methods on that ID, as none exists.

Maybe we should just ditch inheritance entirely (aside from Object). It's complicated and I can't come up with a real-world usecase for it that isn't equally clear or simple without it.

Lets do this for now, and then if someone comes up with a compelling use case we can add it.


If object is the only interface, then it doesnt realy make much sense to have a list_interfaces

And for release we can make it a method on Root, that takes the object ID.

And this way, we can eliminate inheritence all together.

LMK if this sounds good, and I'll write it up

daeken commented 2 years ago

The way I think of it is getting an empty singleton will return an ID, but you cannot call any methods on that ID, as none exists.

That still requires developers to handle allocating and returning an object. I think it should be specified that it's implementation-defined whether or not you receive an object whatsoever.

If object is the only interface, then it doesnt realy make much sense to have a list_interfaces

We don't need that exact method, but we do need some way to know what an arbitrary object is. Because of that, I think we ought to keep it, but maybe just change the name to something like get_interface_name (or something generic that works with classes too).

Now that we're not doing inheritance, maybe we should rethink some more of the naming. But it feels a bit like bikeshedding to do it here.

daeken commented 2 years ago

So I think the plan for action right now is:

Sound good?

aDotInTheVoid commented 2 years ago

If we want reflection, can we stick it into the root object / core singleton / core static methods / core functions (naming TBD)

daeken commented 2 years ago

I'd really rather keep it on the object itself, personally. It feels like something that the root shouldn't have to do, if only because code generators for this stuff (e.g. the one in my TestCosm) can automatically handle implementing the Object methods that exist on all interfaces. That's not something I could (easily, or cleanly) do with it being detached from the object itself.

aDotInTheVoid commented 2 years ago

Yeah, if you are building in an OO system it makes sense to think about it that way. But even if you put a GetClass method on all objects, when you get the call to root, you can look up that object in the map (which you need to do anyway if its an object call to determine which obj is being called on), and then call the get_class method on the object thats been looked up

aDotInTheVoid commented 2 years ago

That still requires developers to handle allocating and returning an object.

But thats not that expensive, given that the call is in batch, and you can just allocte a singleton that always returns an error for any call, and then return that one ID for all empty singletons.

The reason I want this is so that on startup I can get_singletons for all the extensions, and then not worry about having to filter based on which ones are empty.

daeken commented 2 years ago

Hmmm. Okay, that sounds good to me. Go for it!

aDotInTheVoid commented 2 years ago

Also, it might be worth list_extensions on root returning a [](string, objId) (assuming a tuple syntax), so you dont need to make a get_singletons call, as you pritty much always need to do that.

Furthermore, it might be worth each node pushing the list of extensions and their singleton ID on connection (assuming not resume) so we dont need to special case root=0, although I'm not sure if this is genuinly better, or just moving one specail case to another special case.

daeken commented 2 years ago

Also, it might be worth list_extensions on root returning a [](string, objId) (assuming a tuple syntax), so you dont need to make a get_singletons call, as you pritty much always need to do that.

This would be needlessly expensive if there's minimal overlap in extensions between the Browser and Cosm, or in cases where a connection is only going to use a single interface or whatnot. I think we should keep them separate, especially since we have the bulk get_singletons call.

Furthermore, it might be worth each node pushing the list of extensions and their singleton ID on connection

That adds another special-case to the handshake process, which currently is super minimal. I think we should leave that as-is, especially since I don't think sending the singletons themselves in the list call is a good idea in the first place.

However, I wouldn't be 100% opposed to a single call that the Browser or Cosm sends to the other side, where the extension list is passed by one party, and the opposite is returned by the other party. It'd cut out two round-trips. But tbh, I think it's unnecessary right now; we can worry about that much later on.

aDotInTheVoid commented 2 years ago

Superceeded by #16