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

Is there a way to add a new unary operator? #962

Open
hardliner66 opened this issue Feb 21, 2025 · 9 comments
Open

Is there a way to add a new unary operator? #962

hardliner66 opened this issue Feb 21, 2025 · 9 comments

Comments

@hardliner66
Copy link

I have written a DSL to format objects with rhai and recently, when trying to fix some bugs, I found out that those bugs stem from me abusing the unary operator - for printing. The problem is that if I were (able) to fix this for the minus operator, it would break code that uses the minus operator in the intended way. So instead I tried to use a different symbol as a unary operator (with really low precedence), but the parser always returns an error, saying the symbol wasn't expected there.

So, is there a way to add a new unary operator?

@schungx
Copy link
Collaborator

schungx commented Feb 22, 2025

Not at the moment. It was deemed before that unary operators are rare and not justified the complexity...

@schungx
Copy link
Collaborator

schungx commented Feb 22, 2025

Maybe you can overload the negative operator on your own data types?

Just overload the function - with one parameter. You won't affect normal operations with numbers.

@hardliner66
Copy link
Author

Thats what I did. The DSL was meant to be like this:

let s = "Test";
- "some string: " ++ s ++ " and some number: " ++ 1234;

where ++ concatenates values into a list and - takes and prints the list.

The problem is, that the - operator has the highest precedence in that expression, making it similar to something like this:

let s = "Test";
(- "some string: ") ++ s ++ " and some number: " ++ 1234;

which completely breaks the order of operation that I want.

So my only option was possibly breaking negative values, by lowering the precedence (if even possible, I never tried tbh), which is a no-go for me or by making the syntax of the DSL uglier by only using functions (e.g.: emit(a ++ b ++ c);), keeping it as is and relying on users to properly add parenthesis around expressions to disambiguate.

All of this I didn't really like, so I wanted to go with the better option of adding a new operator (~) with the lowest possible precedence. But when I tried using the operator, it returned the error that it wasn't expecting the symbol "~".

In the meantime I figured out that I can just add custom syntax:

{
    let messages = engine.clone_messages();
    engine
        .register_custom_syntax(
            ["~", "$expr$"],
            true,
            move |context: &mut rhai::EvalContext,
                  inputs: &[rhai::Expression]|
                  -> ScriptResult<Dynamic> {
                for e in inputs {
                    let result = context.eval_expression_tree(&e)?;
                    let mut m = flatten_dynamic(result)?;
                    messages.borrow_mut().append(&mut m);
                }
                Ok(Dynamic::UNIT)
            },
        )
        .unwrap();
}

Which works fine, but feels like it should be possible with just operators alone.

@schungx
Copy link
Collaborator

schungx commented Feb 23, 2025

Yes a custom syntax would be useful for this purpose.

However, beware that you seem to require the print symbol to start a new line?

Custom syntax are expressions so they don't necessarily start new lines. This may not be what you want.

@schungx
Copy link
Collaborator

schungx commented Feb 23, 2025

BTW what is the purpose of introducing a DSL? Since you can easily use the standard array literal syntax to create an array.

@hardliner66
Copy link
Author

BTW what is the purpose of introducing a DSL? Since you can easily use the standard array literal syntax to create an array.

Okay, so I'm building a code generation framework (SSD) in order to simplify the act of writing your own code generation tools. The framework has a defined data description format, which gets parsed and then handed off to one of the generators.

In order to provide a simple way to generate text, I built a DSL on top of rhai where instead of having to write print("something"); you could just do -("Something"); and instead of manually stringifying and concatenating your values and then passing it on to a print function or the - operator, you could just use ++, which would automatically print the left and then the right element or just one of them, if the other was of type ().

I thought this made for a nice DSL to write your code generation code in. And later found out that, because - was an unary operator, I could drop the parentheses and make look even cleaner.

Some time ago, I needed to format something again and thought, why not rip the whole rhai part out of the framework and put it into its own crate (https://github.com/hardliner66/script-format/). After I was done I wrote some simple tests and documentation and thought its time to bump it to 1.0. Right after the release I asked a few friends for review and one of them experimented around with it, hitting a few bugs, but also a few inconsistencies. The tool basically started printing things it shouldn't or get the order slightly wrong, because most operators I introduced had side effects. I also somehow never thought about using - in combination with ++ and the other operators in the way that he tried using them. So I could have just said that its used wrong and that parentheses are needed and that the operators can't be mixed, but I'd rather fix it so that it can't accidentally be used wrong.

So in order to mitigate the issues, I removed all the implicit printing and opted for a slightly less nice, but more straight forward syntax where ++ and other operators returned a vec of what needs to be printed and the - operator took the vec and printed it (actually, printing is done later, but thats an implementation detail).

Which is where I hit the problem of - binding too strong breaking operator combinations.

So its not that I need a DSL for concatenating values, but concatenating values like this is just what remained of the original when removing the implicit printing. Handling it this way also ensures that no matter the order of operations, it will always get concatenated correctly before the whole result gets committed for printing.

However, beware that you seem to require the print symbol to start a new line?

Not necessarily. I just need it to be treated like a print call, that takes all the values to its right, which it currently does. Using it inside an expression would be the same as using print in the middle of an expression, which is why I don't mind that thats a possibility.

If unary operators are a possibility in the future, I'll probably still switch to them, because I feel they are already close to what I need and I don't like to use the big guns, so to speak, unless necessary.

Hope that clarifies the use-case a bit.

@schungx
Copy link
Collaborator

schungx commented Feb 24, 2025

That's very clear. One thing that threw me off is that you may be able to use Rhai's standard + operator to do the strings concat instead of using ++.

You just define to_string for all your types and Rhai would know how to print it using + to concat the pieces.

- "some string: " + s + " and some number: " + 1234;

You can even use strings interpolation which is essentially the same thing.

- `some string: ${s} and some number: ${1234}`

I wonder what functionality that your ++ operator gives that standard Rhai doesn't...

Just trying to understand your use case to see why it isn't adequately covered.

@hardliner66
Copy link
Author

hardliner66 commented Feb 24, 2025

When dealing with constructing strings dynamically I tend to push all parts in a vec first and then join it into the final string, because it's easier to deal with the individual parts. The ++ operator takes two elements and puts them into a vec. It also flattens the elements if necessary.

It's probably possible to just use + instead, but as I said, the ++ operator used to not only concat, but also print, so having it be different from + was a good idea.

And, from a backwards compatibility standpoint, I didn't want to change too much syntax at once. I even left the unary - in and just show a depreciation notice to make the transition easier.

So in short: it might not be strictly necessary anymore to have a custom operator for that purpose, but there's historical reason for it and I also like the differentiation of ++ being to concat output strings versus + being for other coding.

Edit:
I think having a proper way to add unary operators with proper precedence would be a good thing either way and give users a more controlled way of adding stuff like this.

@schungx
Copy link
Collaborator

schungx commented Feb 25, 2025

Thanks for your detailed replies. Very clear now.

I'll look into adding custom unary operators. It may not be as straightforward as it sounds currently unary operators are always bound before binary ones are considered. So it may not be possible to change it to a low precedence.

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

No branches or pull requests

2 participants