rhai/tests/custom_syntax.rs

467 lines
14 KiB
Rust
Raw Permalink Normal View History

2022-07-05 16:59:03 +02:00
#![cfg(not(feature = "no_custom_syntax"))]
2021-07-10 09:50:31 +02:00
use rhai::{
Dynamic, Engine, EvalAltResult, ImmutableString, LexError, ParseErrorType, Position, Scope, INT,
2021-07-10 09:50:31 +02:00
};
2020-07-09 13:54:28 +02:00
#[test]
fn test_custom_syntax() -> Result<(), Box<EvalAltResult>> {
let mut engine = Engine::new();
engine.run("while false {}")?;
2020-07-10 16:01:47 +02:00
// Disable 'while' and make sure it still works with custom syntax
engine.disable_symbol("while");
assert!(matches!(
2023-04-10 17:23:59 +02:00
engine.compile("while false {}").unwrap_err().err_type(),
ParseErrorType::Reserved(err) if err == "while"
));
assert!(matches!(
2023-04-10 17:23:59 +02:00
engine.compile("let while = 0").unwrap_err().err_type(),
ParseErrorType::Reserved(err) if err == "while"
));
2020-07-10 16:01:47 +02:00
2022-08-20 16:10:15 +02:00
// Implement ternary operator
2020-08-05 11:02:11 +02:00
engine.register_custom_syntax(
2022-08-20 16:10:15 +02:00
["iff", "$expr$", "?", "$expr$", ":", "$expr$"],
false,
|context, inputs| match context.eval_expression_tree(&inputs[0])?.as_bool() {
Ok(true) => context.eval_expression_tree(&inputs[1]),
Ok(false) => context.eval_expression_tree(&inputs[2]),
Err(typ) => Err(Box::new(EvalAltResult::ErrorMismatchDataType(
"bool".to_string(),
typ.to_string(),
inputs[0].position(),
))),
},
)?;
assert_eq!(
engine.eval::<INT>(
"
let x = 42;
let y = iff x > 40 ? 0 : 123;
y
"
)?,
0
);
assert_eq!(
engine.eval::<INT>(
"
let x = 42;
let y = iff x == 0 ? 0 : 123;
y
"
)?,
123
);
// Custom syntax
engine.register_custom_syntax(
[
2021-07-10 09:50:31 +02:00
"exec", "[", "$ident$", "$symbol$", "$int$", "]", "->", "$block$", "while", "$expr$",
2020-08-05 11:02:11 +02:00
],
true,
2020-10-25 14:57:18 +01:00
|context, inputs| {
let var_name = inputs[0].get_string_value().unwrap();
2021-07-10 09:50:31 +02:00
let op = inputs[1].get_literal_value::<ImmutableString>().unwrap();
let max = inputs[2].get_literal_value::<INT>().unwrap();
let stmt = &inputs[3];
let condition = &inputs[4];
2020-07-09 13:54:28 +02:00
context.scope_mut().push(var_name.to_string(), 0 as INT);
2020-07-09 13:54:28 +02:00
let mut count: INT = 0;
2020-08-05 11:02:11 +02:00
loop {
2021-07-10 09:50:31 +02:00
let done = match op.as_str() {
"<" => count >= max,
"<=" => count > max,
">" => count <= max,
">=" => count < max,
"==" => count != max,
"!=" => count == max,
2022-08-11 13:01:23 +02:00
_ => return Err(format!("Unsupported operator: {op}").into()),
2021-07-10 09:50:31 +02:00
};
if done {
2021-06-10 04:16:39 +02:00
break;
}
2022-07-06 06:56:15 +02:00
// Do not rewind if the variable is upper-case
2022-12-30 18:07:39 +01:00
let _: Dynamic = if var_name.to_uppercase() == var_name {
#[allow(deprecated)] // not deprecated but unstable
context.eval_expression_tree_raw(stmt, false)
2022-07-06 06:56:15 +02:00
} else {
2022-12-30 18:07:39 +01:00
context.eval_expression_tree(stmt)
}?;
2022-07-06 06:56:15 +02:00
count += 1;
2020-07-09 13:54:28 +02:00
context
.scope_mut()
2022-08-11 13:01:23 +02:00
.push(format!("{var_name}{count}"), count);
2020-10-11 15:58:11 +02:00
let stop = !context
.eval_expression_tree(condition)?
2020-08-05 11:02:11 +02:00
.as_bool()
2020-10-11 15:58:11 +02:00
.map_err(|err| {
Box::new(EvalAltResult::ErrorMismatchDataType(
"bool".to_string(),
err.to_string(),
condition.position(),
))
})?;
if stop {
2020-08-05 11:02:11 +02:00
break;
2020-07-09 13:54:28 +02:00
}
2020-08-05 11:02:11 +02:00
}
2020-07-09 13:54:28 +02:00
Ok(count.into())
2020-08-05 11:02:11 +02:00
},
)?;
2020-07-09 13:54:28 +02:00
2021-07-10 09:50:31 +02:00
assert!(matches!(
*engine
.run("let foo = (exec [x<<15] -> { x += 2 } while x < 42) * 10;")
2023-04-10 17:23:59 +02:00
.unwrap_err(),
2022-02-08 02:02:15 +01:00
EvalAltResult::ErrorRuntime(..)
2021-07-10 09:50:31 +02:00
));
assert_eq!(
engine.eval::<INT>(
2021-04-20 06:01:35 +02:00
"
let x = 0;
2021-07-10 09:50:31 +02:00
let foo = (exec [x<15] -> { x += 2 } while x < 42) * 10;
foo
"
)?,
2021-06-10 04:16:39 +02:00
150
);
assert_eq!(
engine.eval::<INT>(
2021-04-20 06:01:35 +02:00
"
let x = 0;
2021-07-10 09:50:31 +02:00
exec [x<100] -> { x += 1 } while x < 42;
x
"
)?,
42
);
2020-07-09 13:54:28 +02:00
assert_eq!(
engine.eval::<INT>(
2021-04-20 06:01:35 +02:00
"
2021-07-10 09:50:31 +02:00
exec [x<100] -> { x += 1 } while x < 42;
2020-07-09 13:54:28 +02:00
x
"
)?,
42
);
assert_eq!(
engine.eval::<INT>(
"
let foo = 123;
2021-07-10 09:50:31 +02:00
exec [x<15] -> { x += 1 } while x < 42;
foo + x + x1 + x2 + x3
"
)?,
2021-06-10 04:16:39 +02:00
144
);
2022-07-06 06:56:15 +02:00
assert_eq!(
engine.eval::<INT>(
"
let foo = 123;
exec [x<15] -> { let foo = x; x += 1; } while x < 42;
foo
"
)?,
123
);
assert_eq!(
engine.eval::<INT>(
"
let foo = 123;
exec [ABC<15] -> { let foo = ABC; ABC += 1; } while ABC < 42;
foo
"
)?,
14
);
2020-07-09 13:54:28 +02:00
2020-07-10 16:01:47 +02:00
// The first symbol must be an identifier
2020-08-05 11:02:11 +02:00
assert_eq!(
*engine
2022-08-20 16:19:29 +02:00
.register_custom_syntax(["!"], false, |_, _| Ok(Dynamic::UNIT))
2023-04-10 17:23:59 +02:00
.unwrap_err()
.err_type(),
2020-11-02 05:50:27 +01:00
ParseErrorType::BadInput(LexError::ImproperSymbol(
2020-11-21 08:15:14 +01:00
"!".to_string(),
2020-10-25 14:57:18 +01:00
"Improper symbol for custom syntax at position #1: '!'".to_string()
2020-11-02 05:50:27 +01:00
))
2020-10-25 14:57:18 +01:00
);
2021-08-02 04:24:03 +02:00
// Check self-termination
engine
2022-08-20 16:19:29 +02:00
.register_custom_syntax(["test1", "$block$"], true, |_, _| Ok(Dynamic::UNIT))?
.register_custom_syntax(["test2", "}"], true, |_, _| Ok(Dynamic::UNIT))?
.register_custom_syntax(["test3", ";"], true, |_, _| Ok(Dynamic::UNIT))?;
2021-08-02 04:24:03 +02:00
assert_eq!(engine.eval::<INT>("test1 { x = y + z; } 42")?, 42);
assert_eq!(engine.eval::<INT>("test2 } 42")?, 42);
assert_eq!(engine.eval::<INT>("test3; 42")?, 42);
// Register the custom syntax: var x = ???
engine.register_custom_syntax(
2022-08-20 16:19:29 +02:00
["var", "$ident$", "=", "$expr$"],
true,
|context, inputs| {
let var_name = inputs[0].get_string_value().unwrap();
let expr = &inputs[1];
// Evaluate the expression
let value = context.eval_expression_tree(expr)?;
if !context.scope().is_constant(var_name).unwrap_or(false) {
context.scope_mut().set_value(var_name.to_string(), value);
2021-08-13 16:47:03 +02:00
Ok(Dynamic::UNIT)
} else {
2022-08-11 13:01:23 +02:00
Err(format!("variable {var_name} is constant").into())
2021-08-13 16:47:03 +02:00
}
},
)?;
let mut scope = Scope::new();
assert_eq!(
engine.eval_with_scope::<INT>(&mut scope, "var foo = 42; foo")?,
42
);
assert_eq!(scope.get_value::<INT>("foo"), Some(42));
assert_eq!(scope.len(), 1);
assert_eq!(
engine.eval_with_scope::<INT>(&mut scope, "var foo = 123; foo")?,
123
);
assert_eq!(scope.get_value::<INT>("foo"), Some(123));
assert_eq!(scope.len(), 1);
2020-10-25 14:57:18 +01:00
Ok(())
}
2023-04-25 17:14:08 +02:00
#[test]
fn test_custom_syntax_scope() -> Result<(), Box<EvalAltResult>> {
let mut engine = Engine::new();
engine.register_custom_syntax(
[
"with", "offset", "(", "$expr$", ",", "$expr$", ")", "$block$",
],
true,
|context, inputs| {
let x = context
.eval_expression_tree(&inputs[0])?
.as_int()
.map_err(|typ| {
Box::new(EvalAltResult::ErrorMismatchDataType(
"integer".to_string(),
typ.to_string(),
inputs[0].position(),
))
})?;
let y = context
.eval_expression_tree(&inputs[1])?
.as_int()
.map_err(|typ| {
Box::new(EvalAltResult::ErrorMismatchDataType(
"integer".to_string(),
typ.to_string(),
inputs[1].position(),
))
})?;
let orig_len = context.scope().len();
context.scope_mut().push_constant("x", x);
context.scope_mut().push_constant("y", y);
let result = context.eval_expression_tree(&inputs[2]);
context.scope_mut().rewind(orig_len);
result
},
)?;
assert_eq!(
engine.eval::<INT>(
"
let y = 1;
let x = 0;
with offset(44, 2) { x - y }
"
)?,
42
);
Ok(())
}
2023-02-20 06:28:17 +01:00
#[test]
fn test_custom_syntax_matrix() -> Result<(), Box<EvalAltResult>> {
let mut engine = Engine::new();
engine.disable_symbol("|");
engine.register_custom_syntax(
[
"@", //
"|", "$expr$", "$expr$", "$expr$", "|", //
"|", "$expr$", "$expr$", "$expr$", "|", //
"|", "$expr$", "$expr$", "$expr$", "|",
],
false,
|context, inputs| {
let mut values = [[0; 3]; 3];
for y in 0..3 {
for x in 0..3 {
let offset = y * 3 + x;
match context.eval_expression_tree(&inputs[offset])?.as_int() {
Ok(v) => values[y][x] = v,
Err(typ) => {
return Err(Box::new(EvalAltResult::ErrorMismatchDataType(
"integer".to_string(),
typ.to_string(),
inputs[offset].position(),
)))
}
}
}
}
Ok(Dynamic::from(values))
},
)?;
let r = engine.eval::<[[INT; 3]; 3]>(
"
let a = 42;
let b = 123;
let c = 1;
let d = 99;
@| a b 0 |
| -b a 0 |
| 0 0 c*d |
",
)?;
assert_eq!(r, [[42, 123, 0], [-123, 42, 0], [0, 0, 99]]);
Ok(())
}
2020-10-25 14:57:18 +01:00
#[test]
fn test_custom_syntax_raw() -> Result<(), Box<EvalAltResult>> {
let mut engine = Engine::new();
2022-09-12 06:03:32 +02:00
engine.register_custom_syntax_with_state_raw(
2020-10-25 14:57:18 +01:00
"hello",
|stream, look_ahead, state| match stream.len() {
2020-10-25 14:57:18 +01:00
0 => unreachable!(),
1 if look_ahead == "\"world\"" => {
*state = Dynamic::TRUE;
Ok(Some("$string$".into()))
}
1 => {
*state = Dynamic::FALSE;
Ok(Some("$ident$".into()))
}
2022-11-23 10:23:54 +01:00
2 => {
match stream[1].as_str() {
"world" if state.as_bool().unwrap_or(false) => Ok(Some("$$world".into())),
"world" => Ok(Some("$$hello".into())),
"kitty" => {
*state = (42 as INT).into();
Ok(None)
}
s => Err(LexError::ImproperSymbol(s.to_string(), String::new())
.into_err(Position::NONE)),
2022-09-12 06:03:32 +02:00
}
2022-11-23 10:23:54 +01:00
}
2020-10-25 14:57:18 +01:00
_ => unreachable!(),
},
true,
2022-09-12 06:03:32 +02:00
|context, inputs, state| {
2020-12-14 16:05:13 +01:00
context.scope_mut().push("foo", 999 as INT);
Ok(match inputs[0].get_string_value().unwrap() {
"world" => match inputs.last().unwrap().get_string_value().unwrap_or("") {
"$$hello" => 0 as INT,
"$$world" => 123456 as INT,
_ => 123 as INT,
},
2021-10-25 16:41:42 +02:00
"kitty" if inputs.len() > 1 => 999 as INT,
2022-09-12 06:03:32 +02:00
"kitty" => state.as_int().unwrap(),
2020-10-25 14:57:18 +01:00
_ => unreachable!(),
}
.into())
},
);
assert_eq!(engine.eval::<INT>(r#"hello "world""#)?, 123456);
2021-10-25 16:41:42 +02:00
assert_eq!(engine.eval::<INT>("hello world")?, 0);
2020-10-25 14:57:18 +01:00
assert_eq!(engine.eval::<INT>("hello kitty")?, 42);
assert_eq!(
engine.eval::<INT>("let foo = 0; (hello kitty) + foo")?,
1041
);
assert_eq!(engine.eval::<INT>("(hello kitty) + foo")?, 1041);
2020-10-25 14:57:18 +01:00
assert_eq!(
2023-04-10 17:23:59 +02:00
*engine.compile("hello hey").unwrap_err().err_type(),
2022-08-21 11:35:44 +02:00
ParseErrorType::BadInput(LexError::ImproperSymbol("hey".to_string(), String::new()))
2020-08-05 11:02:11 +02:00
);
2020-07-10 16:01:47 +02:00
2020-07-09 13:54:28 +02:00
Ok(())
}
#[test]
fn test_custom_syntax_raw2() -> Result<(), Box<EvalAltResult>> {
let mut engine = Engine::new();
2022-09-12 06:03:32 +02:00
engine.register_custom_syntax_with_state_raw(
"#",
2022-09-12 06:03:32 +02:00
|symbols, lookahead, _| match symbols.len() {
1 if lookahead == "-" => Ok(Some("$symbol$".into())),
1 => Ok(Some("$int$".into())),
2 if symbols[1] == "-" => Ok(Some("$int$".into())),
2 => Ok(None),
3 => Ok(None),
_ => unreachable!(),
},
false,
2022-09-12 06:03:32 +02:00
move |_, inputs, _| {
let id = if inputs.len() == 2 {
-inputs[1].get_literal_value::<INT>().unwrap()
} else {
inputs[0].get_literal_value::<INT>().unwrap()
};
Ok(id.into())
},
);
assert_eq!(engine.eval::<INT>("#-1")?, -1);
assert_eq!(engine.eval::<INT>("let x = 41; x + #1")?, 42);
2021-12-17 09:32:34 +01:00
#[cfg(not(feature = "no_object"))]
2021-12-16 15:40:10 +01:00
assert_eq!(engine.eval::<INT>("#-42.abs()")?, 42);
assert_eq!(engine.eval::<INT>("#42/2")?, 21);
assert_eq!(engine.eval::<INT>("sign(#1)")?, 1);
Ok(())
}