Closed chmielot closed 1 year ago
You can do this and this would be thread safe, but you have to be very careful when modifying the scope.
If you rerun an AST, the Engine may be accessing the wrong variable... I have not tried it so I'm not sure if this would be the case, but it is usually very dangerous to try to modify the scope.
In your case you're compiling the statements one by one so this problem may not occur. Except that it may define new variables even for something on the right, when it should throw an error. For example, x = y + z
should throw an error if z
is undefined, but your method would define z
to be false
instead.
There is an example in the Book on how to implement JavaScript's var
, which defines a new variable if one doesn't exist: https://rhai.rs/book/engine/custom-syntax.html#practical-example--recreating-javascripts-var-statement
If all your scripts are simple assignment statements, you can just attach var
in front of every line and be done with it.
Thank you for your comments. You are right, I can't use on_var
, because then I won't be able to throw an error if there is actually an undefined variable that was not supposed to be undefined. I need a more sophisticated approach.
What's your opinion on the following strategy? I compile all of the statements once when starting the application. The statements are one-liners that are NOT ending with a semicolon. I want to keep the syntax of the statements easy and leaving out the semicolon seems to be fine. After that I loop over all compiled statements (ASTs) and merge them. The missing semicolon is not an issue, because the one-line statements result in the same AST, independent from the semicolon. So merging the ASTs works as expected.
Having a final big AST, I populate a scope with incoming data records. Some of the variables will now be defined and have values, let's call them "inputs", others will still be undefined. The only undefined variables that I want to accept are the "outputs", so the ones on the left hand side of an assignment. At the end of processing, I want to extract all outputs, rewind the scope and continue with the next data record.
To identify and extract the outputs, it seems that I need to walk
the AST anyway. If I walk the AST anyway, I can define variables used on the left hand side of an assignment before running the AST and I can verify if there are any uninitialised variables on the right hand side of an assignment.
Does that sound like a viable approach?
This is how I'm now defining "output" variables and it seems to work fine. I'm searching for an Assignment
immediately followed by Variable
(which means I'm looking at the left hand side) where I extract the name of the variable from. If the name doesn't exist in the scope, I'm defining it using Dynamic
and the value false
due to the lack of knowledge what the final type will be. Ideally, the scope API would offer to simply define a variable without assigning a value. This at least seems to be possible in Rhai script. What are your thoughts on that?
let mut scope = Scope::new();
// ast1 is "A = 1"
// ast2 is "B = 1"
let final_ast = ast1.merge(&ast2);
final_ast.walk(&mut |nodes| -> bool {
let name = match nodes {
[.., ASTNode::Stmt(rhai::Stmt::Assignment(..)), ASTNode::Expr(Expr::Variable(info, ..))] => {
Some(&*info.3)
},
_ => None
};
if let Some(name) = name {
if scope.get_value::<Dynamic>(name).is_none() {
scope.push_dynamic(name.to_string().to_owned(), Dynamic::from(false));
}
}
true
});
match engine.run_ast_with_scope(&mut scope, &final_ast);
Ok(_) => println!("All ok"),
Err(e) => println!("{:#?}", e),
}
Thank you in advance.
Edit: Instead of modifying the scope programmatically, I can also generate rhai statement strings like
let A;
let B;
compile this and merge the actual user defined ASTs into that.
While experimenting a bit more, I have noticed that the scope is not updated when there are let
statements within an if
-statement.
#[test]
fn test_rhai() {
let engine = Engine::new();
let mut scope = Scope::new();
scope.set_value("Amount", 11000i64);
let ast = engine.compile( "if Amount > 10000 { print(\"Condition was true\"); let Intercept=true; let Score = 500; let X=2; } let Y = 3;").unwrap();
match engine.run_ast_with_scope(&mut scope, &ast) {
Ok(_) => println!("All ok"),
Err(e) => println!("{:#?}", e),
}
println!("{:#?}", scope);
assert_eq!(true, false);
}
The output is:
---- evaluation::tests::test_rhai stdout ----
Condition was true
All ok
Scope {
values: [
11000,
3,
],
names: [
"Amount",
"Y",
],
aliases: [
[],
[],
],
dummy: PhantomData,
}
Am I doing something wrong here and should I open a new issue?
While experimenting a bit more, I have noticed that the scope is not updated when there are
let
statements within anif
-statement.
That's the correct behaviour. Variables defined inside a block scope goes away at the end of the scope.
I think the AST walk method is the cleanest.
You can create a variable in a scope simply by setting its value to Dynamic::UNIT
which is the default value for variables.
Then you are certain all the outputs are defined.
Also, you can simply merge all the lines together into one large script (remember to add semicolons). Then you only need to compile once and no need to deal with merging ASTs.
While experimenting a bit more, I have noticed that the scope is not updated when there are
let
statements within anif
-statement.That's the correct behaviour. Variables defined inside a block scope goes away at the end of the scope.
Of course! That makes totally sense for how Rhai works in general. And because I don't want to use the "let" keyword anyway the AST walking code will define them in the scope and it's going to work exactly as I need it to be.
Thank you very much for the support!
I have a list of user defined statements which I compile into ASTs and which I then evaluate one by one passing the same scope that is modified by the statements:
For the right hand side of the statements I have data which I can use to populate the variables with. So I am able to define
y
. Ify
is actually not defined during evaluation, it means that there's something wrong and throwing an error is the right thing to do. However, a user writing these statements doesn't know and care if a variable on the left hand side has been defined earlier. If it wasn't it should automatically be defined.Right now I am doing the following, and I'm wondering if this is the right thing to do or if I'm missing something:
And would this be thread safe?