Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ doc/rhai.json
.idea
.idea/*
src/eval/chaining.rs
*.core
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ Rhai - Embedded Scripting for Rust

[![Rhai logo](https://rhai.rs/book/images/logo/rhai-banner-transparent-colour.svg)](https://rhai.rs)


Rhai is an embedded scripting language and evaluation engine for Rust that gives a safe and easy way
to add scripting to any application.

Expand Down
2 changes: 1 addition & 1 deletion src/packages/iter_basic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ where
}

// Range iterator with step
#[derive(Clone, Hash, Eq, PartialEq)]
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why remove the derive attributes?

#[derive(Clone)]
pub struct StepRange<T> {
/// Start of the range.
pub from: T,
Expand Down
125 changes: 124 additions & 1 deletion src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2329,7 +2329,7 @@
namespace: crate::ast::Namespace::NONE,
name: self.get_interned_string(&op),
hashes: FnCallHashes::from_native_only(hash),
args: IntoIterator::into_iter([root, rhs]).collect(),
args: IntoIterator::into_iter([root.clone(), rhs.clone()]).collect(),
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also is it necessary to clone the arguments?

op_token: native_only.then(|| op_token.clone()),
capture_parent_scope: false,
};
Expand Down Expand Up @@ -2430,6 +2430,129 @@
op_base.into_fn_call_expr(pos)
}

Token::PipeArrow => {
// Pipeline: lhs |> fn(args...) => fn(lhs, args...)
match rhs {
Expr::FnCall(f, func_pos) => {
// take inner FnCallExpr
let mut f = *f;

let args_len = f.args.len() + 1;
f.args.insert(0, root);

// Recalculate hash for the new argument count, preserving namespace if any
#[cfg(not(feature = "no_module"))]
{
let hash = if f.namespace.is_empty() {
calc_fn_hash(None, &f.name, args_len)
} else {
calc_fn_hash(
f.namespace.path.iter().map(Ident::as_str),
&f.name,
args_len,
)
};
f.hashes = if is_valid_function_name(&f.name) {
FnCallHashes::from_hash(hash)
} else {
FnCallHashes::from_native_only(hash)
};
}
#[cfg(feature = "no_module")]
{
f.hashes = if is_valid_function_name(&f.name) {
FnCallHashes::from_hash(calc_fn_hash(None, &f.name, args_len))
} else {
FnCallHashes::from_native_only(calc_fn_hash(
None, &f.name, args_len,
))
};
}

Expr::FnCall(f.into(), func_pos)
}
Expr::MethodCall(f, func_pos) => {
let mut f = *f;

let args_len = f.args.len() + 1;
f.args.insert(0, root);

// Recalculate hash for the new argument count
f.hashes = if is_valid_function_name(&f.name) {
#[cfg(not(feature = "no_function"))]
{
FnCallHashes::from_hash(calc_fn_hash(None, &f.name, args_len))
}
#[cfg(feature = "no_function")]
{
FnCallHashes::from_native_only(calc_fn_hash(
None, &f.name, args_len,
))
}
} else {
FnCallHashes::from_native_only(calc_fn_hash(
None, &f.name, args_len,
))
};

Expr::FnCall(f.into(), func_pos)
}
Expr::Variable(x, ..) => {
// Pipeline into a bare function name: lhs |> func => func(lhs)
let x = *x; // move out

#[cfg(not(feature = "no_module"))]
let (_index, name, namespace, _hash) = x;
#[cfg(feature = "no_module")]
let (index, name) = x;

Check warning on line 2507 in src/parser.rs

View workflow job for this annotation

GitHub Actions / Build (ubuntu-latest, --features testing-environ,no_module,serde,metadata,internals,debugging, st...

unused variable: `index`

Check warning on line 2507 in src/parser.rs

View workflow job for this annotation

GitHub Actions / Build (ubuntu-latest, --features testing-environ,no_time,no_function,no_float,no_position,no_inde...

unused variable: `index`

Check warning on line 2507 in src/parser.rs

View workflow job for this annotation

GitHub Actions / Build (ubuntu-latest, --features testing-environ,sync,no_time,no_function,no_float,no_position,no...

unused variable: `index`
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CI seems to complain about index being unused here...


let args_len = 1usize;

#[cfg(not(feature = "no_module"))]
let hashes = if is_valid_function_name(&name) {
FnCallHashes::from_hash(calc_fn_hash(
namespace.path.iter().map(Ident::as_str),
&name,
args_len,
))
} else {
FnCallHashes::from_native_only(calc_fn_hash(
namespace.path.iter().map(Ident::as_str),
&name,
args_len,
))
};

#[cfg(feature = "no_module")]
let hashes = if is_valid_function_name(&name) {
FnCallHashes::from_hash(calc_fn_hash(None, &name, args_len))
} else {
FnCallHashes::from_native_only(calc_fn_hash(None, &name, args_len))
};

let fn_call = FnCallExpr {
#[cfg(not(feature = "no_module"))]
namespace: {
#[cfg(not(feature = "no_module"))]
{
let mut ns = crate::ast::Namespace::NONE;
ns.path = namespace.path;
ns.index = namespace.index;
ns
}
},
name: name.clone(),
hashes,
args: IntoIterator::into_iter([root]).collect(),
capture_parent_scope: false,
op_token: None,
};

Expr::FnCall(fn_call.into(), pos)
}
_ => op_base.into_fn_call_expr(pos),
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'd need parsing errors if the stuff following the operator is not a proper function call, instead of allowing it to go through.

}
}
_ => op_base.into_fn_call_expr(pos),
};
}
Expand Down
7 changes: 5 additions & 2 deletions src/tokenizer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,8 @@ pub enum Token {
Bang,
/// `|`
Pipe,
/// `|>`
PipeArrow,
/// `||`
Or,
/// `^`
Expand Down Expand Up @@ -786,6 +788,7 @@ impl Token {
EqualsTo => "==",
NotEqualsTo => "!=",
Pipe => "|",
PipeArrow => "|>",
Or => "||",
Ampersand => "&",
And => "&&",
Expand Down Expand Up @@ -1034,7 +1037,7 @@ impl Token {
use Token::*;

Precedence::new(match self {
Or | XOr | Pipe => 30,
Or | XOr | Pipe | PipeArrow => 30,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need a lower precedence for |>?


And | Ampersand => 60,

Expand Down Expand Up @@ -2350,7 +2353,7 @@ fn get_next_token_inner(
}
('|', '>') => {
stream.eat_next_and_advance(pos);
return (Token::Reserved(Box::new("|>".into())), start_pos);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have to be careful here... some users may depend on the fact that |> is a reserved symbol and define it as a custom operator. This may break code, but I see that you're checking this after checking for custom operators...

return (Token::PipeArrow, start_pos);
}
('|', ..) => return (Token::Pipe, start_pos),

Expand Down
66 changes: 66 additions & 0 deletions tests/pipeline_operator.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
#![cfg(not(feature = "no_function"))]
use rhai::{Engine, INT};

#[test]
fn test_pipeline_basic() {
let engine = Engine::new();

// Simple function chaining: value is passed as first argument to the function on the right
let result = engine.eval::<INT>("fn inc(x) { x + 1 }; 1 |> inc |> inc").unwrap();

assert_eq!(result, 3);
}

#[test]
fn test_pipeline_with_extra_arg() {
let engine = Engine::new();

// Pipeline into a function that takes additional arguments
let result = engine.eval::<INT>("fn add(a, b) { a + b }; 1 |> add(2)").unwrap();

assert_eq!(result, 3);
}

#[test]
fn test_pipeline_into_method_call_style() {
let engine = Engine::new();

// Pipeline into a method-call-style builtin (abs).
// The pipeline passes the left-hand value as the first argument of the call on the right.
let result = engine.eval::<INT>("let x = -123; x |> abs(); x").unwrap();

// abs should not have mutated `x` here, so `x` remains -123
assert_eq!(result, -123);
}

#[cfg(not(feature = "no_object"))]
mod pipeline_method_tests {
use rhai::{Engine, INT};

#[derive(Debug, Clone, Eq, PartialEq)]
struct TestStruct {
x: INT,
}

impl TestStruct {
fn update(&mut self, n: INT) {
self.x += n;
}

fn new() -> Self {
Self { x: 1 }
}
}

#[test]
fn test_pipeline_into_registered_method() {
let mut engine = Engine::new();

engine.register_type::<TestStruct>().register_fn("update", TestStruct::update).register_fn("new_ts", TestStruct::new);

// Pipeline into a registered method should forward the left-hand value as the first argument
let result = engine.eval::<TestStruct>("let x = new_ts(); x |> update(1000); x").unwrap();

assert_eq!(result, TestStruct { x: 1001 });
}
}
Loading