rhaiscript / rhai

Rhai - An embedded scripting language for Rust.
https://crates.io/crates/rhai
Apache License 2.0
3.79k stars 177 forks source link

objects in rhai #73

Closed brianwp3000 closed 4 years ago

brianwp3000 commented 6 years ago

I see documentation on how to define structs in Rust and then make them available for use in Rhai. These structs can have fields that are accessible with dot notation, e.g:

let foo = new_ts(); // new_ts() is a Rust-defined fn that returns a Rust-defined struct
foo.bar = 5;

Is there any way to create an object in Rhai without first having to define it in Rust? I'm looking for something like Javascript's objects which allow you do something like this:

var foo = { bar: 0 };
foo.bar = 5;

Is this on the roadmap for Rhai?

luciusmagn commented 6 years ago

It's on the roadmap yes, but not possible yet, sorry

čt 21. 6. 2018 v 2:41 odesílatel Brian Porter notifications@github.com napsal:

I see documentation on how to define structs in Rust and then make them available for use in Rhai. These structs can have fields that are accessible with dot notation, e.g:

let foo = new_ts(); // new_ts() is a Rust-defined fn that returns a Rust-defined struct foo.bar = 5;

Is there any way to create an object in Rhai without first having to define it in Rust? I'm looking for something like Javascript's objects which allow you do something like this:

var foo = { bar: 0 }; foo.bar = 5;

Is this on the roadmap for Rhai?

— You are receiving this because you are subscribed to this thread. Reply to this email directly, view it on GitHub https://github.com/jonathandturner/rhai/issues/73, or mute the thread https://github.com/notifications/unsubscribe-auth/AIAzh1kOX4f7M7LFAvHbWCds_uD8Flo5ks5t-uuhgaJpZM4Uw-pI .

zutils commented 6 years ago

I would also like objects in Rhai. I REALLY would like to create structure types in Rhai and analyze them in Rust (viewing the properties/name/types etc...)

stevedonovan commented 6 years ago

There is a fairly straightforward route here - first, provide support for maps as well as vectors. This seems to be easy enough, if the maps (HashMap) go from String to Any - then the indexing is unambiguous. Second, allow 'object literals' like {a = 1, c = "hello"}. The value of this is just HashMap<String,Any> which is easy enough to work with on the Rust side.

stevedonovan commented 6 years ago

(Although that's syntactically ambiguous - I'm thinking too much in Lua here. But {a: 1} could work)

stevedonovan commented 6 years ago

(Although that's syntactically ambiguous - I'm thinking too much in Lua here. But {a: 1} could work)

stevedonovan commented 6 years ago

Sorry, comment lost: it is syntactically possible to have Javascript-style object literals like {one: 1, two: "hello"} which map to a HashMap<String,Box<Any>>. Can unambiguously extend [] to do lookup on such maps.

jhwgh1968 commented 4 years ago

I've been playing around with Rhai for a project I'm doing, and found this as a roadblock. Just to try and understand the code, I started on a slightly different implementation:

let myarray = [0, 1, 2];
let mymap = [red: "value", orange: 4.0, yellow: 20];

Would this syntax be acceptable?

schungx commented 4 years ago

Would this syntax be acceptable?

I'm quite sure an object syntax is possible, even not hard to implement. But is it really necessary to burden a scripting language with objects?

In Rhai, you can already register your own object types. That means all objects you want to create can be created first in Rust, then registered for use in Rhai.

So in your example:

struct MyMap {
    pub red: String,
    pub orange: f64,
    pub yellow: i64
}

engine.register_type::<MyMap>();
engine.register_fn("new_map", || MyMap { red: "value", orange: 4.0, yellow: 20 });
engine.register_get_set("red", |x: &mut MyMap| x.red, |x: &mut MyMap, val: String| x.red = val);
engine.register_get_set("orange", |x: &mut MyMap| x.orange, |x: &mut MyMap, val: f64| x.orange = val);
engine.register_get_set("yellow", |x: &mut MyMap| x.yellow, |x: &mut MyMap, val: i64| x.yellow = val);

In Rhai, you can then do things like:

let mymap = new_map();
let r = mymap.red;
mymap.yellow = mymap.orange + r.len().to_float();
print(mymap.yellow);

What you currently cannot do is to define and create objects with dynamic properties in Rhai. But is this so critical?

schungx commented 4 years ago

let mymap = [red: "value", orange: 4.0, yellow: 20];

Since Rhai is a recursive-descent parser with one look-ahead, it will be a bit tricky to decide whether the expression is an array or an object. Same with { ... } syntax, which cannot distinguish between a block statement or an object.

That's because the colon is two tokens after the wrapping token... We'll need to back-track. It would be much simpler to pick an unused symbol for object literals, for example:

let mymap = | red:"value", orange:4.0, yellow:20 |;

schungx commented 4 years ago

A simple implementation is probably to add support for HashMap<String, Box<dyn Any>> as a primitive type just like Array (which is Vec<Box<dyn Any>>).

Object literals can be parsed similar to array literal, and accessed via the dot or bracket notation.

Rust code accessing the object will be returned a HashMap<String, Box<dyn Any>>.

But my question remains: what potential use case will this solve that cannot be solved better by defining a Rust struct and registering it? Other than being able to return a whole structure at once (which you can already do by returning an Array).

profan commented 4 years ago

Isn't the whole point of having a script language to be able to do work in the script language without having to recompile the source of what you're using it in?

In the case of not having any ability to create your own intermediate structures in script you'll end up having to recompile the whole system as soon as you realise you want even one struct more, that might just be used in a single place in the script, seems to defeat the purpose of script a little no?

Plus it's very likely in the use cases many have for this (game scripting etc) the structures really have no reason to exist outside of script-land which is where most of your domain logic lives, so it's not like you can't do it by just defining them in rust-land, but it's nice if you don't have to clutter the rust source with structures that really aren't actually used in rust, but only in script.

At least personally I'm very interested in using rhai, but preferably not absent some kind of structures defined in rhai (either explicitly, or anonymously, either is fine with me at least) :)

schungx commented 4 years ago

That sounds reasonable. I guess I'm not writing a game so I won't know the needs there! I just need a scripting engine to write business rules...

The immediate problem is with the syntax. The normal object literal syntax:

{ field1: value1, field2: value2, field3: value3 }

is LR(2), which may require a bit of work to splice into the current parser. If I pick a different syntax, like using a symbol that's not used anywhere, I can make it 1-look-ahead, but then it'll be different from anything anybody is used to...

profan commented 4 years ago

Alternatively, maybe something like #{} for a hash literal? The "common" syntax would definitely be the most optimal, but I trust you'll come up with something reasonable if you don't want to turn the parser into a LR(2) one (which I reckon could actually be constructed as a LR(1) one still, but grammar probably being way more convoluted in that form).

Regardless, good luck :eyes:

jhwgh1968 commented 4 years ago

The normal object literal syntax... is LR(2), which may require a bit of work to splice into the current parser.

This is the reason I picked overloading the array syntax, @schungx.

My current prototype parses quite easily, since it is only LL(2). The language grammar becomes (to simplify some edge cases):

expr =>   <rules for integers, floats, etc> |
          leftbracket (expr comma)* rightbracket  |
          leftbracket ident colon expr (comma ident colon expr)* rightbracket | 
          leftbracket colon rightbracket

The top rule handles arrays, just like today. The second rule handles the non-empty map case. The last rule handles empty maps -- the colon is needed to distinguish them from empty arrays. (Even if array slicing is added, I presume we will not use the Python-like [:] to mean "copy the entire array".)

The key is that the look-ahead only needs to distinguish between three things after the bracket: ident colon (an illegal expression), colon (an illegal expression), or expr (anything else). I can do that just with a little extra logic in the expr parser, right before the [ token calls parse_array_literal, and there is no back-tracking.

The main limitation of my version is that it currently only accepts string keys via identifiers. That fits my use case, which is to have user code be able to return JSON-like objects to the calling Rust code. I think it could be expanded, but I'm interested in getting something working first.

@profan, I also chose the [] syntax, because there are languages that also use it for maps. The two I can think of are Perl (in certain circumstances) and PHP. The latter, in particular, uses an internal "map-like" type which becomes an array when indexed with numbers, and a map when indexed with items of other types.

EDIT: clarity

jhwgh1968 commented 4 years ago

A second general note, @schungx.

It's not a game engine, but my use case boils down to the same motivation as many game engines have: user-defined callback handlers.

That is, the Rust code defines an API for some operation, and the scripting language allows users to write code to match it. In games, that often involves custom data structures going both ways.

While passing in "output objects" or defining "output types" from Rust may work well enough, it denies the API designer a lot of flexibility, and makes the code harder to read.

Since you can't inspect the code to tell what type something is, only do your own personal type inference analysis in your head and guess, that becomes trouble when a script is expected to set a bunch of parameters on a passed-in output type.

It is also quite unusual, if you are used to other scripting languages with object types. I imagine it would take some people a while to understand the different types and what was going on if they didn't want to read the Rust code.

It becomes even more tricky if you want an API, for example, that uses one of three different types, which come in under different circumstances, or want different outputs of different types. Rhai does not seem to support option types natively -- you can't set None to Some or vice versa. This makes having optional parameters subpar on both the Rhai side and the Rust side. You can make it work by adding a ton of Rust boilerplate to import dozens methods, but that makes the API way more complex.

In short, anonymous compound types (i.e. "objects" in the JSON and Javascript sense) are in most scripting languages for a reason. They are explicit when explicitness is needed, they are flexible in their definition when that flexibility is useful, and people are familiar with them as a concept.

This feature will let people familiar with scripting languages write better, simpler, cleaner code, and better understand code if they have never touched Rhai before. (An important thing for me, since my project will be open source some day, I hope.)

EDIT: clarity

jhwgh1968 commented 4 years ago

I've poked at this for a while, and found that making the TokenIter LL(n) -- where n = 1 most of the time, but n = 2 for a small piece of code -- is trickier than I thought.

I expected it to be easy by using itertools::multipeek, but finding all the points I would need to reset_peek in the existing parser code turned out to be quite a challenge.

As a result, I have tweaked the syntax a little more to make it truly LL(1):

expr =>   <rules for integers, floats, arrays, etc> |
          leftbracket colon (ident colon expr comma)* rightbracket | 

Which means it now looks like this:

let myarray = [1, 2, 3];
let mymap = [: red: 1, orange: 2.0, yellow: "3"];

Any opinion on that compared to your suggestion of #{}, @profan?

schungx commented 4 years ago

I am thinking [: a:1, b:2 ] doesn't look exactly pretty, and it is easy to miss a colon somewhere and something gets turned into an array.

How about using the unused symbols # or $ instead?

Map: let x = #[ a: 1, b: 2 ]#; or let x = #[ a: 1, b: 2 ]; let x = #( a: 1, b: 2 )#; or let x = #( a: 1, b: 2 ); let x = #{ a: 1, b: 2 }; or let x = #{ a: 1, b: 2 }#; let x = ${ a: 1, b: 2 }; or let x = $[ a: 1, b: 2 ];

Empty map: #[]# #()# #[] #() #{} #{}# ${}

I personally like #( ... )# best because of the symmetry for the empty map: #()# which looks like a Unit wrapped in #'s. Also # can be pronounced "hash" which is what JS calls it...

${... } also does have a good ring to it...

let x = #[ a => 1, b => 2 ]; looks almost PHP...

schungx commented 4 years ago

Technically speaking, we can pick any symbol (used or not) that can never start a sub-expression (such as <). Syntax like:

<a=1, b=2>

looks almost XML. However, the matching ending token > does form part of an expression, so

< a=1, b=2 > y

will be ambiguous, if we ever take away the requirement of separating statements with ; (which it was originally optional). I added the rule just because the semicolon is so easy to forget and it sometimes generate obscure bugs that are very hard to track down. But if we want to relax this requirement in the future, this will generate a problem.

Still, < a=1, b=2 > +y will always be ambiguous.

So I still think sticking with an unused symbol is probably best.

schungx commented 4 years ago

Rhai does not seem to support option types natively

We don't need objects for this. All we need is a special API engine.eval_to_optional::<T> -> Option<T> that maps a return value in T to Some<T> and () to None. In your script, you can then throw (); to exit with None.

The only thing is the proliferation of API method calls:

eval_to_optional, eval_ast_to_optional, eval_file_to_optional, eval_to_optional_with_scope, eval_ast_to_optional_with_scope, eval_file_to_optional_with_scope

My Rust-fu is weak so I can't really figure out how to detect a type parameter is Option<> yet... doing it with a trait is a no-go because an Option<T> will conflict with T as both are Any. That's why in the code there are three traits to register functions: RegisterFn, RegisterDynamicFn, RegisterResultFn and they are almost identical except for functions returning different types, with RegisterFn being more general.

If somebody knows how to distinguish a type parameter, it would be great!

schungx commented 4 years ago

I've put in a full implementation for object maps in my fork under the object_maps branch:

https://github.com/schungx/rhai/tree/object_maps

This branch:

jhwgh1968 commented 4 years ago

I'll try your branch with my use case, @schungx! I bet it will work just fine. The addition of has is also nice!

schungx commented 4 years ago

The addition of has is also nice!

Lifted it almost straight from JS's hasOwnProperty LOL!

schungx commented 4 years ago

This PR https://github.com/jonathandturner/rhai/pull/120 adds object maps.

Poll: Would you prefer:

1) ${ prop: value } 2) ${ prop = value } 3) #{ prop: value } 4) #{ prop => value } 5) #[ prop => value ]

profan commented 4 years ago

I'd go for 3. personally, since you said $ is (relatively) commonly used in string interpolation type contexts, plus prop: value would fit in nicely with if one might want named arguments later and that could likely look similar ie. func(arg1: v, arg2: v) and would fit right in with it.

Then again you could make the same argument about = assuming you used the same thing in both places at least, but .. :thinking:

schungx commented 4 years ago

@profan you have a good point about code reuse, but the relevant code sections are so short (it really isn't very hard to parse a literal) that it might not save much.

@jhwgh1968 any objections to this?

So why don't I change it to #{ prop: value } and update the PR. Let's see how it goes (to paraphrase POTUS)...

jhwgh1968 commented 4 years ago

Option 3 sounds okay to me!

I like the colon syntax better than the others, and you could say it's a "hash map" -- since "#" is sometimes called "hash" in UNIX land. :grin:

schungx commented 4 years ago

Since this is really not an object as per OOP style (i.e. no encapsulation, inheritance and polymorphism), maybe I should change it to "hash map" instead of "object map" in the docs?

schungx commented 4 years ago

Off-topic: I have created a new issue to track a new feature: String interpolation

https://github.com/jonathandturner/rhai/issues/121

This is a follow-on our previous discussions on the ${} syntax.

schungx commented 4 years ago

I have a minor commit https://github.com/schungx/rhai/commit/c4a51b139001cf301ee6dd1755a99a8168578fb7 to my fork that adds a very useful function: mixin to object maps.

Now you can do:

let x = #{ ... };
let y = #{ ... };
x.mixin(y);
x += y;
let z = x + y;

Lifted this directly from JS.