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

Introduce Attributes #962

Merged
merged 7 commits into from
Apr 9, 2021
Merged

Introduce Attributes #962

merged 7 commits into from
Apr 9, 2021

Conversation

ruby0x1
Copy link
Member

@ruby0x1 ruby0x1 commented Apr 6, 2021

Attributes

This introduces attributes to classes and methods in Wren.

Attributes are user-defined metadata associated with a class or method that
can be used at runtime, by external tools, and potentially by Wren itself.

Read the details in the draft documentation here.

Goals

  • Negligible cost overall
  • No impact on classes without meta (like no allocations unless needed)
  • Simple elegant syntax that matches Wren and it's style
  • Just enough flexibility to be minimal but still extremely useful

I feel the goals were achieved.

References

Attributes or the concept aren't new or unfamiliar for developers familiar with other languages.
Here's just a few:

Access at runtime

Attributes are optionally stored on the class at runtime for access. You can access them via
YourClass.attributes. The attributes field on a class will
be null if a class has no attributes.

If the class contains class or method attributes, it will be an object with
two getters:

  • YourClass.attributes.self for the class attributes
  • YourClass.attributes.methods for the method attributes

Details about that are in the documentation in this PR.

Example uses

Below I'll show only a tiny subset of possible uses, as they're user defined
it's fairly open ended. There's potential uses for Wren itself like conditional compilation
and such but mostly focused on the immediate term value to the user, as I'm using them already.

In these examples you'll see the syntax as well.

Note that I've been using and testing these attributes in luxe.

Documentation attributes.
You can see this live below in the context of code completion.
The other usecase is API documentation generation, which I also use this for.

firefox_qEXCosIRvM

A modified Wrenalyzer parses the attributes, and a work in progress completion server feeds them to vscode.
Note that the class, the method and the individual args have their own docs displayed.
Also note that the video shows out of date runtime access for the attributes.
Also note that you'll see optional type annotations, ignore those for now, I'm exploring them properly.

I7CVyURXuq.mp4

The code for the above example looks like this:

#doc = """
  A sprite is an image attached to an entity.   
  The `Sprite` modifier provides flipping, sizing, sub images and more.
  To attach a sprite to an entity, call `Sprite.create`:

      var entity = Entity.create(world)
      var material = Assets.material("luxe: material/logo")
      Sprite.create(entity, material, 128, 128)
"""
class Sprite {

    #doc = """
      Attach a `Sprite` modifier to `entity`, drawn using `material`,
      with a size of `width`x`height`.

          var entity = Entity.create(world)
          var material = Assets.material("luxe: material/logo")
          Sprite.create(entity, material, 128, 128)
    """
    #args(
      entity = "The entity to attach the `Sprite` to",
      material = "The material asset which controls how the sprite is drawn",
      width = "The width in world units for the sprite",
      height = "The height in world units for the sprite"
    )
    foreign static create(entity, material, width, height)

    #hidden //not shown on code completion or docs etc.
    static internal_create(entity)

  //...
}

Testing
It can be convenient to simply mark a method or class to execute for tests. Note that at the moment, it's not as easy (but possible) to call a method by name.

#test(skip = false)
#test(iterations = 32)
class Test {
  #test(iterations=100)
  test_num() { ... }
}

Profiling
Previously I explored a vm wide profiler,
but it can be tricky to profile specifics when there is a cost to every function.
Instead, a class or method can be annotated, and the profiling markers emitted just for those.

class Slow {
  #profile
  suspiciousMethod() {
    //...
  }
}

Optional typing
While proper optional type annotations are better, it's still possible to use attributes for this.

class Example {
  #return = Bool
  #types(
    arg = String,
    other = Num
  )
  method(arg, other) {
    //...
  }
}

ECS annotations
One of the examples recently discussed is annotating simple empty 'data classes'
for use in an ECS like context.

Details about the datatypes for a component could
be annotated using attributes, and used for code generation,
automatic field definitions on the native side, and so on.

Systems can be annotated to control when they execute, which components it wants, etc.

class Position {
  #type = number
  x {}
  #type = number
  y {}
}

class Velocity {
  #type = vec2
  velocity {}
}

#executeDuring = init
#component = Velocity
#component = Position
class MoveSystem {
  ...
}

... and so on

Editor metadata, customizable behaviours etc. The possibilities are pretty wide.

Conclusion

Given the amount of benefit we can get from tooling right away for end users, I'm happy with where these landed.

@ruby0x1 ruby0x1 changed the title introduce Attributes Introduce Attributes Apr 6, 2021
@ruby0x1 ruby0x1 marked this pull request as draft April 6, 2021 04:58
@ruby0x1 ruby0x1 mentioned this pull request Apr 6, 2021
@clsource
Copy link

clsource commented Apr 6, 2021

This is wonderful. I think another use case for attributes would be for binding when using Wren inside a wrapper for another language like WrenGo.

For example, a class can be created using these attributes, and then a tool could automatically generate the corresponding binding code.

@ruby0x1
Copy link
Member Author

ruby0x1 commented Apr 6, 2021

Oh yea definitely, code generation and bindings was in my notes and I forgot to add it, thanks for mentioning.

@mhermier
Copy link
Contributor

mhermier commented Apr 6, 2021 via email

@ruby0x1
Copy link
Member Author

ruby0x1 commented Apr 6, 2021

@mhermier decorator is a bit ambiguous for me, can you elaborate what you mean by that?

currently they are user defined meta data with no built in or predefined use by the vm. there are options for that but one step at a time.

You can view the draft documentation here

@mhermier
Copy link
Contributor

mhermier commented Apr 6, 2021 via email

@ruby0x1
Copy link
Member Author

ruby0x1 commented Apr 6, 2021

Ah ok, like python decorators. This is quite different in both intent and usage to me and evaluation is not a goal here. It's metadata given meaning by use case.

@ChayimFriedman2
Copy link
Contributor

ChayimFriedman2 commented Apr 6, 2021

I think it may be nice to allow list literals in attributes.

@mhermier
Copy link
Contributor

mhermier commented Apr 6, 2021 via email

@ruby0x1
Copy link
Member Author

ruby0x1 commented Apr 6, 2021

Unfortunately lists within are counter to this goal:

Just enough flexibility to be minimal but still extremely useful

Being simple and minimal is intentional, like the rest of Wren.

Values are stored in a list already because duplicate keys will be stored as one, what use case are you thinking of where you imagine a list to be required?

@ChayimFriedman2
Copy link
Contributor

In case multiple values are required, duplicate keys may be used, but I think it's more comfortable to just use a list.

@ruby0x1 ruby0x1 marked this pull request as ready for review April 8, 2021 19:43
@cxw42
Copy link
Contributor

cxw42 commented Apr 9, 2021

Looks good --- thanks for your hard work!

Maybe not for this PR, but have you given any thought to inlining the attributes for parameters? E.g., in addition to

class Example {
  #types(n=Num, s=String)
  meth(n,s) {...}
}

supporting

class Example {
  meth(#types = Num n, #types = String s) {...}
}

or postfix (not as consistent but maybe easier to read)

class Example {
  meth(n #types = Num, s #types = String) {...}
}

?

I am a fan of keeping parameters and descriptions close together. This is one reason I prefer Doxygen to gtk-doc for functions: Doxygen permits spreading the docs through a signature, whereas gtkdoc requires all the documentation in one place before the function.

@ruby0x1
Copy link
Member Author

ruby0x1 commented Apr 9, 2021

I definitely thought about it, but for me it escapes the minimal/simple goal.

In general, a key metric I use is "it is easy to do, but not to undo". Sure we can add anything we want... but Wren doesn't need or want everything turned up to 11, it's not designed that way and it's one of it's greatest strengths.

Thanks for the thoughts!

@ruby0x1 ruby0x1 merged commit a4ae905 into main Apr 9, 2021
@PureFox48
Copy link
Contributor

Sorry to be a bit late to the party, but a couple of quick points.

The combination #! is already used for shebangs which I believe Wren is intended to support (there are tests for them) though I've never been able to get them to work properly.

It seems to me that you'll generally want attributes to be available at runtime - otherwise you could just use a comment if you want it to be compiled out. That being so would it be better to use a simple # for runtime attributes and perhaps come up with some other notation for attributes which are to be compiled out but you nevertheless want to be readable by a tool?

@clsource
Copy link

clsource commented Apr 9, 2021

Sorry to be a bit late to the party, but a couple of quick points.

The combination #! is already used for shebangs which I believe Wren is intended to support (there are tests for them) though I've never been able to get them to work properly.

Yes that use case was considered

91a56b8

It seems to me that you'll generally want attributes to be available at runtime - otherwise you could just use a comment if you want it to be compiled out. That being so would it be better to use a simple # for runtime attributes and perhaps come up with some other notation for attributes which are to be compiled out but you nevertheless want to be readable by a tool?

No, simple comments won't do. They won't have the benefits of attributes. Maybe using // # and // #! can be used, but that was probably considered by Ruby and probably this syntax was the best relation cost benefit :)

@PureFox48
Copy link
Contributor

Ah, I hadn't actually looked at the source but was reading the Attributes section from the wren.io site.

So, given that shebangs have been considered, I'm happy now with the current implementation :)

@ruby0x1
Copy link
Member Author

ruby0x1 commented Apr 9, 2021

Consider that accessing an attribute at runtime is an intentional explicit action, there is much higher clarity in marking an attribute for runtime rather than having all attributes accidentally costing runtime memory you didn't know about.

For example documentation attributes would be on every. single. class. and you might not realize it's using up loads of memory, adding gc pressure.

Plus I like that you can easily see which ones have a cost via the !.

Overall, the majority of immediate and short term use is all compile time, and I prefer the intentional act of using them at runtime either way because it's obvious, which is why it defaults to being ignored.

I originally had it the other way around but realized this is more ideal. Thanks for the thoughts!

@PureFox48
Copy link
Contributor

Yep, it's all making perfect sense to me now.

Thanks for the explanation and a great new feature!

@IngwiePhoenix
Copy link

I only just checked Wrens commit log and found this and came here to tell you that this is an amazing feature!

Here is something that sprung to my mind, inspired by V's vweb module:

#!base = "/foo"
class Foo is Controller {
  #!path = "/bar"
  #!post
  bar() {
    // ...
  }
}

This could easily be used in a Wren based web framework to denote HTTP methods, routes and actions and other cool stuff and by doc generators to pick up doc hints. Really, REALLY cool feature!

Great work =)

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

Successfully merging this pull request may close these issues.

7 participants