component-model icon indicating copy to clipboard operation
component-model copied to clipboard

Resource properties

Open badeend opened this issue 2 years ago • 18 comments

Most languages have either:

  • language-level support for properties
    • C#: string MyProperty { get; set; }
    • JavaScript: get myProperty()
    • JavaScript: set myProperty(value)
  • or conventions on how to name property-like methods.
    • Java: String getMyProperty()
    • Java: void setMyProperty(String value)
    • Rust: my_property(&self) -> String
    • Rust: set_my_property(&mut self, value: String) -> ()

If properties are not encoded natively in WIT, we'll probably end up with everybody choosing their own convention (based on their own language). Which is bound to end up looking foreign to the other languages. Or we can prescribe a convention for tooling to convert from&to, but at that point we might as well just encode it into Wit itself.

My suggestion:

interface abc {
    resource blob {
        content-type1: string { get } // Read-only property
        content-type2: string { set } // Write-only property
        content-type3: string { get, set } // Read/write property

        content-type4: string // TODO: Without annotation defaults to { get } ? Or { get, set } ?

        content-type5: string { // Customized signatures
            get -> result<string, my-get-error>,
            set -> result<_, my-set-error>,
        }
        content-type6: string { // Infallible getter, fallible setter
            get, // If no custom signature is provided, the getter/setter is assumed to be infallible.
            set -> result<_, my-set-error>,
        }

        /// General doc-comment applicable to both the getter & setter.
        content-type7: string {
            /// Documentation specific to the getter.
            get,

            /// Documentation specific to the setter.
            set,
        }
    }

    // Is equivalent to:



    resource blob
    %[get]blob.content-type1: func(self: borrow<blob>) -> string
    %[set]blob.content-type2: func(self: borrow<blob>, value: string)
    %[get]blob.content-type3: func(self: borrow<blob>) -> string
    %[set]blob.content-type3: func(self: borrow<blob>, value: string)

    // %[???]blob.content-type4

    %[get]blob.content-type5: func(self: borrow<blob>) -> result<string, my-get-error>
    %[set]blob.content-type5: func(self: borrow<blob>, value: string) -> result<_, my-set-error>
    %[get]blob.content-type6: func(self: borrow<blob>) -> string
    %[set]blob.content-type6: func(self: borrow<blob>, value: string) -> result<_, my-set-error>

}

Or more Javascript-esque:

interface abc {
    resource blob {
        // Together these two declarations represent one logical property.
        content-type: get func() -> string
        content-type: set func(value: string)
    }
}

Either way, in both variants getters and setters:

  • are never async
  • always borrow their self argument, regardless of the outcome of #226
  • must satisfy one of these signatures:
    • %[get]T.property-name: func(self: borrow<T>) -> T
    • %[get]T.property-name: func(self: borrow<T>) -> result<T, error-type>
    • %[set]T.property-name: func(self: borrow<T>, value: T)
    • %[set]T.property-name: func(self: borrow<T>, value: T) -> result<_, error-type>

badeend avatar Aug 18 '23 13:08 badeend

Good points and thanks for the great write up! Yes, this has been in the idea queue for a while (I think maybe @guybedford brought this up somewhere too?) and hasn't been added yet mostly due to not having yet had the time to analyze the option space. Thanks for the detailed examples and ideas, which look reasonable. I can't say I have any strong opinions yet on whether to be more like JS or C#; I'd be interested to see more examples of what other languages do beyond C# and JS.

lukewagner avatar Aug 18 '23 21:08 lukewagner

Here ya go:

Python

@property
def the_property_name(self):
   return # value

@the_property_name.setter
def the_property_name(self, value):
    # update value

Visual Basic

Public Property ThePropertyName As String
    Get
        Return name
    End Get

    Set(ByVal value As String)
        name = value
    End Set

End Property

Ruby

def name # get
    @name
end

def name=(name) # set
    @name = name
end

Swift

class Point {
    var x: Int {
        get { return /* value */ }
        set { /* update value */ }
    }
}

Kotlin

var thePropertyName: String
    get() {
        return /* value */
    }
    set(value) {
        /* update value */
    }

Objective-C

@property int someNumber;

- (int)someNumber {
    return // value
}
- (void)setSomeNumber: (int)newValue {
    // update value
}

Scala

def age = // value
def age_=(newValue: Int) {
    // update value
}

D

@property bar() { return 10; }
@property bar(int x) { writeln(x); }

Dart

int get thePropertyName {
    return /* value */
}

void set thePropertyName(int newValue) {
    /* update value */
}

F#

member MyProperty
    with get() = // value
    and set(value) = // update value

C#

public int MyProperty
{
    get
    {
        return /* value */;
    }
    set
    {
        /* update value */
    }
}

JavaScript

get thePropertyName() {
    return /* value */;
}

set thePropertyName(newValue) {
    /* update value */
}

badeend avatar Aug 19 '23 07:08 badeend

Thanks a bunch, that's super-helpful to see the spectrum. I guess the take-away for me is that it doesn't seem like there's a clear "everyone does it this way" precedent. Also, we're in a slightly different boat than all these languages since we only care about defining the signature, not implementations, which makes our constraints slightly different.

My impression is that the "JavaScript-esque" option you presented in your original comment is the better option:

  • syntactically, it seems simpler (just an optional token before func)
  • if and when we add other function attributes (e.g., shared, async, or reentrant), it's easy enough to allow those to be used along with get or set if we want.

lukewagner avatar Aug 21 '23 15:08 lukewagner

I have recently encountered the need for this as well. For syntax I would prefer something a little less verbose than the "Javascript-esque" option since having to define two functions each time seems like overkill. Some kind of default for the non-result getter/setter option would be nice, only requiring explicitly typing the functions if they differ from the simple case.

//This has the default getter/setter signatures
foo: string

//This type can fail on get or set, so must define the signature for both
bar: get func() -> result<string, error-type>
bar: set func(value: string) -> result<_, error-type>

A readonly keyword would be nice as well to indicate that a value would only have a getter (assuming that the default is gettable and settable).

landonxjames avatar Mar 23 '24 04:03 landonxjames

I've got this working in:

  • https://github.com/bytecodealliance/wasmtime/compare/main...badeend:wasmtime:get-set
  • https://github.com/bytecodealliance/wit-bindgen/compare/main...badeend:wit-bindgen:get-set
  • https://github.com/bytecodealliance/wasm-tools/compare/main...badeend:wasm-tools:get-set

I used the get func() / set func(..) WIT syntax and the [get]... / [set]... ABI encoding as mentioned above. An example:

https://github.com/bytecodealliance/wasmtime/blob/dff1071cfe1ec4f925085f0d7369c662c84b1c8a/crates/wasi/wit/deps/sockets/tcp.wit#L261-L264

I implemented this the most straightforward way as possible; by making get & set globally reserved keywords. This is incompatible with some existing WASI interfaces. See the wit changes in this commit: https://github.com/badeend/wasmtime/commit/e93f867759528807b183abf4279a41261c5a965d

What I think the validation rules should be:

  • Getters must have 0 parameters.
  • Getters must declare a single, non-unit, return type. E.g. they may not use the multiple return values syntax.
  • Setters must declare exactly 1 parameter.
  • Setters must either: return nothing (unit/void), or return a single result in the form of result<_, TError>, where the Ok type must be unit and the error type can be anything.
  • If a property has both a getter and a setter, the parameter type of the setter restricts the allowed return type of the getter. Specifically: the return type of the getter must either be TSetterParam itself or Result<TSetterParam, TError>. Where TError is again any error type.

Currently, member names must be unique within a resource. This should be relaxed to allow duplicates if-and-only-if the name declares a getter + setter. Names may not overload other kind of members (e.g. you may not declare a regular method and a getter with the same name). Per name there may of course be at most 1 getter and at most 1 setter. In wit-parser I've encoded this as follows: https://github.com/badeend/wasm-tools/blob/3dc78b29efea20320262d467dbdd9cdcdff0b3e2/crates/wit-parser/src/ast/resolve.rs#L1172-L1222

@lukewagner Let me know what you think and if/how you think I should proceed.

badeend avatar Jan 27 '25 19:01 badeend

That's awesome, thanks for working on this! This definitely seems like a valuable addition to include as soon as we can.

Just to reply first to your final question regarding next steps: given the plan presented at the last WASI SG meeting to get to a 0.3.0 release ASAP and given that, practically speaking, getters/setters will take a bit of effort to fully flesh out while a bunch other big stuff is happening, I think maybe good next steps are:

  • keep using today's WIT methods for the 0.3.0 release of WASI interfaces
  • work on the design/spec/impl of getters/setters concurrently, aimed at a 0.3.x release after 0.3.0
  • once getters/setters have shipped, add them to WASI interfaces in a subsequent 0.3.y release (using non-conflicting names)
  • in our next major (probably 1.0-rc) release, remove the methods and do any final renaming of the getters/setters, now that the methods are gone

How does that sound?

lukewagner avatar Jan 28 '25 20:01 lukewagner

Sounds reasonable to me! 👍


once getters/setters have shipped, add them to WASI interfaces in a subsequent 0.3.y release (using non-conflicting names). in our next major (probably 1.0-rc) release, remove the methods and do any final renaming of the getters/setters, now that the methods are gone

On that note, an alternative syntax could be to rely solely on annotations & naming conventions. E.g.:

@get
keep-alive-enabled: func() -> result<bool, error-code>;

@set
set-keep-alive-enabled: func(value: bool) -> result<_, error-code>;
  • Pro: Adding these annotations is fully backwards compatible. ABI stays the same; they remain [method]...s
  • Pro: Languages that don't have getters/setters can simply ignore them.
    • Edit: Actually, even languages without native getter/setter syntax still need to parse them to provide an idiomatic experience. E.g. Java likely wants to prefix getter methods with get, as in: getKeepAliveEnabled + setKeepAliveEnabled
  • Pro: Keeps the parser & validation rules simpler: doesn't require making get/set reserved keywords. And the resource member names remain unique.
  • Con: Depends on the naming convention that the setter is called set- + the getter's name. Might be a bit too implicit for some people's liking.

All the other validation rules from my previous comment would still apply.

badeend avatar Jan 29 '25 19:01 badeend

Getters must declare a single, non-unit, return type. E.g. they may not use the multiple return values syntax.

Is it allowed to return tuple<...> at wit level? What is the lower form of tuple in general?


in our next major (probably 1.0-rc) release, remove the methods and do any final renaming of the getters/setters, now that the methods are gone

For ABI compatibility reasons, is it possible to generate both @deprecated %[method] and %[get] / %[set] for old functions (@since 0.2.x) at the same time?

oovm avatar Jan 29 '25 23:01 oovm

Is it allowed to return tuple<...> at wit level?

Yes. If it has an setter too, the parameter type has to be that tuple as well.

What is the lower form of tuple in general?

See the despecialization section in the ABI. They lower into records with numeric field names.

For ABI compatibility reasons, is it possible to generate both @deprecated %[method] and %[get] / %[set] for old functions (@since 0.2.x) at the same time?

Yes, at the ABI level, it is possible for [method]s and [get]/[set] to coexist with the same name. It is unclear to me how this would translate to WIT syntax or generated source bindings.

badeend avatar Jan 30 '25 08:01 badeend

Image

I noticed that in addition to get and set, there is also static available in this position.

Is get static func() a parse error(failed to parse) or a lint error(failed to verify)?

oovm avatar Jan 30 '25 10:01 oovm

In my branch, only instance methods are allowed to be getters/setters. Freestanding functions & static resource methods are not. Maybe another reason to go for the annotation syntax?

badeend avatar Jan 30 '25 10:01 badeend

A real world example of this is https://github.com/WebAssembly/wasi-cli/blob/main/wit/environment.wit. The current interface is (abbreviated):

interface environment {
  get-environment: func() -> list<tuple<string, string>>;
  get-arguments: func() -> list<string>;
  initial-cwd: func() -> option<string>;
}

As anticipated in my initial post: this highlights the naming inconsistencies that are bound to happen without guidance from the CM/WIT; some use the get- prefix, some don't. (in the same interface :upside_down_face: )

In lieu of value imports, making them getters would be the next best thing:

interface environment {
  @get
  environment: func() -> list<tuple<string, string>>;

  @get
  arguments: func() -> list<string>;

  @get
  initial-cwd: func() -> option<string>;
}

Other candidates:


I just checked and according to Google all the languages mentioned earlier on in this thread that support instance getters&setters, also support static getters&setters: Python, JavaScript, Dart, C#, F#, VB.NET, Swift, Objective-C, Kotlin, Scala, Ruby, D. So I think WIT should have this ability as well at some point. Looking at the current WASI interfaces, the most bang-for-the-buck will come from resource instance-level getters&setters. So let's focus on that first, and if static getters&setters can come along "for free", that would be nice too of course.

badeend avatar Jan 30 '25 11:01 badeend

On that note, an alternative syntax could be to rely solely on annotations & naming conventions.

My impression is that if we want to (sometimes) have custom getter/setter syntax in generated bindings, then they're sufficiently special that we should give them custom syntax in WIT to make them more readable.

Speaking of syntax, one bikeshed: for symmetry with constructor, where constructor takes the place of the func token (so that you don't write constructor func(...), but just constructor(...)), could we have get(...) ... and set(...) ... instead of get func(...) .../set func(...)?

Yes, at the ABI level, it is possible for [method]s and [get]/[set] to coexist with the same name. It is unclear to me how this would translate to WIT syntax or generated source bindings.

Yes, great question. One related astute observation made by @alexcrichton earlier was that, to make the lives of bindings-generators easier, we should have the C-M validation predicate reject a single scope containing both [get]R.foo and [method]R.get-foo or [method]R.foo (in the same spirit as how validation currently rejects two names that differ only in case). Given those rules, then the following WIT would fail validation:

interface i {
  resource r {
    @since(version = 1.0.0)
    @deprecated(version = 1.1.0)
    get-foo: func() -> u32;

    @since(version = 1.1.0)
    foo: get() -> u32;
  }
}

This does raise the question of whether we should take "evasive action" in WASI 0.3.0 interfaces so that in whatever 0.3.x release that includes getter/setter syntax, we can use the obvious getter/setter property names without conflict. This seems a bit risky, given the difficulty of predicting the future. OTOH, forcing all the 0.3.x getter/setter property names to be a bit worse than they otherwise would be sounds unfortunate too.

I wonder if a workable compromise is to say that, in the 0.3.x timeframe, we relax the above rule and say that [get|set]R.foo and [method]R.foo may not co-exist, but [get|set]R.foo and [method]R.(get|set)-foo MAY co-exist, but everyone can assume that [get]R.foo and [method]R.get-foo are equivalent and thus if a bindings generator sees both, it is justified in just picking one if there is a name conflict. Then in 1.0-rc, we remove this wart and require the stronger validation rules.

The short-term implication is that we should rename all our would-be getters to have a get- prefix (resolving that inconsistency and providing the guidance that @badeend suggested above).

WDYT?


A few more-minor comments:

Getters must declare a single, non-unit, return type. E.g. they may not use the multiple return values syntax.

Just to clarify: it looks like #368 forgot to remove that example when it removed multi-return from the grammar (of WIT/C-M) entirely. I think that example shouldn't validate and so I'll remove it from WIT.md.

Looking at the current WASI interfaces, the most bang-for-the-buck will come from resource instance-level getters&setters. So let's focus on that first, and if static getters&setters can come along "for free", that would be nice too of course.

+1

lukewagner avatar Jan 31 '25 16:01 lukewagner

My impression is that if we want to (sometimes) have custom getter/setter syntax in generated bindings, then they're sufficiently special that we should give them custom syntax in WIT to make them more readable.

Speaking of syntax, one bikeshed: for symmetry with constructor, where constructor takes the place of the func token (so that you don't write constructor func(...), but just constructor(...)), could we have get(...) ... and set(...) ... instead of get func(...) .../set func(...)?

This - and the rest of your comment - makes sense and seems fine to me 👌

I wonder if a workable compromise is to say that, in the 0.3.x timeframe, we relax the above rule and say that [get|set]R.foo and [method]R.foo may not co-exist, but [get|set]R.foo and [method]R.(get|set)-foo MAY co-exist, but everyone can assume that [get]R.foo and [method]R.get-foo are equivalent and thus if a bindings generator sees both, it is justified in just picking one if there is a name conflict. Then in 1.0-rc, we remove this wart and require the stronger validation rules.

The short-term implication is that we should rename all our would-be getters to have a get- prefix (resolving that inconsistency and providing the guidance that @badeend suggested above).

A slight variation of this could be to do introduce specialized WIT syntax, like you suggested, which then serves as the cue for binding generators to do their magic. And at the same time also keep the lowered ABI form the same as regular get/set-* methods, indefinitely. This indirectly also resolves Alex' concern.

Assuming we preemptively rename all existing candidate getters to get-* before the 0.3 release, we can upgrade them to syntactically prettier (but semantically equivalent) getters/setters after the fact, without affecting backwards-compatibility.

badeend avatar Feb 02 '25 14:02 badeend

Thanks! Just to check my understanding of the variation you're suggesting, are you saying that a WIT-level getter/setter get foo() -> string in a resource R would produce a component-level import named [method]R.get-foo (as opposed to [get]R.foo)? If I haven't misunderstood, the trouble with that approach is that, as a general rule, the input to bindings generation is the raw component-model names/types (so [method]R.get-foo), not the original WIT syntax (which, in turn, ensures that we can generate equivalent bindings directly from random components' .wasm files when there are no WITs around).

lukewagner avatar Feb 04 '25 00:02 lukewagner

are you saying that a WIT-level getter/setter get foo() -> string in a resource R would produce a component-level import named [method]R.get-foo (as opposed to [get]R.foo)?

Correct.

the input to bindings generation is the raw component-model names/types (so [method]R.get-foo), not the original WIT syntax (which, in turn, ensures that we can generate equivalent bindings directly from random components' .wasm files when there are no WITs around).

Ah, okay. Too bad.

The short-term implication is that we should rename all our would-be getters to have a get- prefix (resolving that inconsistency and providing the guidance that @badeend suggested above).

For the phase 3 proposals, that concretely that boils down to prefixing the following members:

  • wasi:clocks/monotonic-clock: resolution
  • wasi:clocks/wall-clock: resolution
  • wasi:random/insecure-seed: insecure-seed
  • wasi:cli/environment: initial-cwd
  • wasi:sockets/types: tcp-socket::local-address
  • wasi:sockets/types: tcp-socket::remote-address
  • wasi:sockets/types: tcp-socket::is-listening
  • wasi:sockets/types: tcp-socket::address-family
  • wasi:sockets/types: tcp-socket::keep-alive-enabled
  • wasi:sockets/types: tcp-socket::keep-alive-idle-time
  • wasi:sockets/types: tcp-socket::keep-alive-interval
  • wasi:sockets/types: tcp-socket::keep-alive-count
  • wasi:sockets/types: tcp-socket::hop-limit
  • wasi:sockets/types: tcp-socket::receive-buffer-size
  • wasi:sockets/types: tcp-socket::send-buffer-size
  • wasi:sockets/types: udp-socket::local-address
  • wasi:sockets/types: udp-socket::remote-address
  • wasi:sockets/types: udp-socket::address-family
  • wasi:sockets/types: udp-socket::unicast-hop-limit
  • wasi:sockets/types: udp-socket::receive-buffer-size
  • wasi:sockets/types: udp-socket::send-buffer-size
  • wasi:http/types: fields::entries
  • wasi:http/types: request::method
  • wasi:http/types: request::path-with-query
  • wasi:http/types: request::scheme
  • wasi:http/types: request::authority
  • wasi:http/types: request-options::connect-timeout
  • wasi:http/types: request-options::first-byte-timeout
  • wasi:http/types: request-options::between-bytes-timeout
  • wasi:http/types: response::status-code

If you want, I can open PRs to perform these renames within the wit-0.3.0-draft folders. Or do you think this should be presented in a WASI meeting first?

badeend avatar Jun 18 '25 12:06 badeend

That sounds like a good plan to me and thank you! But also yes, it's probably a good idea to give a short WASI SG presentation first to get everyone on the same page; looks like there's one next week with an open agenda.

lukewagner avatar Jun 18 '25 15:06 lukewagner

I've added it to the agenda 👍

badeend avatar Jun 23 '25 17:06 badeend