Resource properties
Most languages have either:
- language-level support for properties
- C#:
string MyProperty { get; set; } - JavaScript:
get myProperty() - JavaScript:
set myProperty(value)
- C#:
- 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) -> ()
- Java:
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
selfargument, 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>
-
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.
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 */
}
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, orreentrant), it's easy enough to allow those to be used along withgetorsetif we want.
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).
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
TSetterParamitself orResult<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.
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?
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
-
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
- Pro: Keeps the parser & validation rules simpler: doesn't require making
get/setreserved 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.
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?
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.
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)?
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?
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:
-
monotonic-clock.resolution -
wall-clock.resolution -
insecure-seed.insecure-seed -
preopens.get-directories -
stdin.get-stdin -
stdout.get-stdout -
stderr.get-stderr -
terminal-stdin.get-terminal-stdin -
terminal-stdout.get-terminal-stdout -
terminal-stderr.get-terminal-stderr
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.
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
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, whereconstructortakes the place of thefunctoken (so that you don't writeconstructor func(...), but justconstructor(...)), could we haveget(...) ...andset(...) ...instead ofget 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.fooand[method]R.foomay not co-exist, but[get|set]R.fooand[method]R.(get|set)-fooMAY co-exist, but everyone can assume that[get]R.fooand[method]R.get-fooare 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.
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).
are you saying that a WIT-level getter/setter
get foo() -> stringin a resourceRwould 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?
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.
I've added it to the agenda 👍