Skip to content

Commit 2f5c842

Browse files
committed
feat: Add the pipeline operator |> for chained method calls.
1 parent 6b132e5 commit 2f5c842

File tree

6 files changed

+197
-4
lines changed

6 files changed

+197
-4
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,4 @@ doc/rhai.json
1313
.idea
1414
.idea/*
1515
src/eval/chaining.rs
16+
*.core

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ Rhai - Embedded Scripting for Rust
1616

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

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

src/packages/iter_basic.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ where
4040
}
4141

4242
// Range iterator with step
43-
#[derive(Clone, Hash, Eq, PartialEq)]
43+
#[derive(Clone)]
4444
pub struct StepRange<T> {
4545
/// Start of the range.
4646
pub from: T,

src/parser.rs

Lines changed: 124 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2329,7 +2329,7 @@ impl Engine {
23292329
namespace: crate::ast::Namespace::NONE,
23302330
name: self.get_interned_string(&op),
23312331
hashes: FnCallHashes::from_native_only(hash),
2332-
args: IntoIterator::into_iter([root, rhs]).collect(),
2332+
args: IntoIterator::into_iter([root.clone(), rhs.clone()]).collect(),
23332333
op_token: native_only.then(|| op_token.clone()),
23342334
capture_parent_scope: false,
23352335
};
@@ -2430,6 +2430,129 @@ impl Engine {
24302430
op_base.into_fn_call_expr(pos)
24312431
}
24322432

2433+
Token::PipeArrow => {
2434+
// Pipeline: lhs |> fn(args...) => fn(lhs, args...)
2435+
match rhs {
2436+
Expr::FnCall(f, func_pos) => {
2437+
// take inner FnCallExpr
2438+
let mut f = *f;
2439+
2440+
let args_len = f.args.len() + 1;
2441+
f.args.insert(0, root);
2442+
2443+
// Recalculate hash for the new argument count, preserving namespace if any
2444+
#[cfg(not(feature = "no_module"))]
2445+
{
2446+
let hash = if f.namespace.is_empty() {
2447+
calc_fn_hash(None, &f.name, args_len)
2448+
} else {
2449+
calc_fn_hash(
2450+
f.namespace.path.iter().map(Ident::as_str),
2451+
&f.name,
2452+
args_len,
2453+
)
2454+
};
2455+
f.hashes = if is_valid_function_name(&f.name) {
2456+
FnCallHashes::from_hash(hash)
2457+
} else {
2458+
FnCallHashes::from_native_only(hash)
2459+
};
2460+
}
2461+
#[cfg(feature = "no_module")]
2462+
{
2463+
f.hashes = if is_valid_function_name(&f.name) {
2464+
FnCallHashes::from_hash(calc_fn_hash(None, &f.name, args_len))
2465+
} else {
2466+
FnCallHashes::from_native_only(calc_fn_hash(
2467+
None, &f.name, args_len,
2468+
))
2469+
};
2470+
}
2471+
2472+
Expr::FnCall(f.into(), func_pos)
2473+
}
2474+
Expr::MethodCall(f, func_pos) => {
2475+
let mut f = *f;
2476+
2477+
let args_len = f.args.len() + 1;
2478+
f.args.insert(0, root);
2479+
2480+
// Recalculate hash for the new argument count
2481+
f.hashes = if is_valid_function_name(&f.name) {
2482+
#[cfg(not(feature = "no_function"))]
2483+
{
2484+
FnCallHashes::from_hash(calc_fn_hash(None, &f.name, args_len))
2485+
}
2486+
#[cfg(feature = "no_function")]
2487+
{
2488+
FnCallHashes::from_native_only(calc_fn_hash(
2489+
None, &f.name, args_len,
2490+
))
2491+
}
2492+
} else {
2493+
FnCallHashes::from_native_only(calc_fn_hash(
2494+
None, &f.name, args_len,
2495+
))
2496+
};
2497+
2498+
Expr::FnCall(f.into(), func_pos)
2499+
}
2500+
Expr::Variable(x, ..) => {
2501+
// Pipeline into a bare function name: lhs |> func => func(lhs)
2502+
let x = *x; // move out
2503+
2504+
#[cfg(not(feature = "no_module"))]
2505+
let (_index, name, namespace, _hash) = x;
2506+
#[cfg(feature = "no_module")]
2507+
let (index, name) = x;
2508+
2509+
let args_len = 1usize;
2510+
2511+
#[cfg(not(feature = "no_module"))]
2512+
let hashes = if is_valid_function_name(&name) {
2513+
FnCallHashes::from_hash(calc_fn_hash(
2514+
namespace.path.iter().map(Ident::as_str),
2515+
&name,
2516+
args_len,
2517+
))
2518+
} else {
2519+
FnCallHashes::from_native_only(calc_fn_hash(
2520+
namespace.path.iter().map(Ident::as_str),
2521+
&name,
2522+
args_len,
2523+
))
2524+
};
2525+
2526+
#[cfg(feature = "no_module")]
2527+
let hashes = if is_valid_function_name(&name) {
2528+
FnCallHashes::from_hash(calc_fn_hash(None, &name, args_len))
2529+
} else {
2530+
FnCallHashes::from_native_only(calc_fn_hash(None, &name, args_len))
2531+
};
2532+
2533+
let fn_call = FnCallExpr {
2534+
#[cfg(not(feature = "no_module"))]
2535+
namespace: {
2536+
#[cfg(not(feature = "no_module"))]
2537+
{
2538+
let mut ns = crate::ast::Namespace::NONE;
2539+
ns.path = namespace.path;
2540+
ns.index = namespace.index;
2541+
ns
2542+
}
2543+
},
2544+
name: name.clone(),
2545+
hashes,
2546+
args: IntoIterator::into_iter([root]).collect(),
2547+
capture_parent_scope: false,
2548+
op_token: None,
2549+
};
2550+
2551+
Expr::FnCall(fn_call.into(), pos)
2552+
}
2553+
_ => op_base.into_fn_call_expr(pos),
2554+
}
2555+
}
24332556
_ => op_base.into_fn_call_expr(pos),
24342557
};
24352558
}

src/tokenizer.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,8 @@ pub enum Token {
199199
Bang,
200200
/// `|`
201201
Pipe,
202+
/// `|>`
203+
PipeArrow,
202204
/// `||`
203205
Or,
204206
/// `^`
@@ -786,6 +788,7 @@ impl Token {
786788
EqualsTo => "==",
787789
NotEqualsTo => "!=",
788790
Pipe => "|",
791+
PipeArrow => "|>",
789792
Or => "||",
790793
Ampersand => "&",
791794
And => "&&",
@@ -1034,7 +1037,7 @@ impl Token {
10341037
use Token::*;
10351038

10361039
Precedence::new(match self {
1037-
Or | XOr | Pipe => 30,
1040+
Or | XOr | Pipe | PipeArrow => 30,
10381041

10391042
And | Ampersand => 60,
10401043

@@ -2350,7 +2353,7 @@ fn get_next_token_inner(
23502353
}
23512354
('|', '>') => {
23522355
stream.eat_next_and_advance(pos);
2353-
return (Token::Reserved(Box::new("|>".into())), start_pos);
2356+
return (Token::PipeArrow, start_pos);
23542357
}
23552358
('|', ..) => return (Token::Pipe, start_pos),
23562359

tests/pipeline_operator.rs

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
use rhai::{Engine, INT};
2+
3+
#[test]
4+
fn test_pipeline_basic() {
5+
let engine = Engine::new();
6+
7+
// Simple function chaining: value is passed as first argument to the function on the right
8+
let result = engine.eval::<INT>("fn inc(x) { x + 1 }; 1 |> inc |> inc").unwrap();
9+
10+
assert_eq!(result, 3);
11+
}
12+
13+
#[test]
14+
fn test_pipeline_with_extra_arg() {
15+
let engine = Engine::new();
16+
17+
// Pipeline into a function that takes additional arguments
18+
let result = engine.eval::<INT>("fn add(a, b) { a + b }; 1 |> add(2)").unwrap();
19+
20+
assert_eq!(result, 3);
21+
}
22+
23+
#[test]
24+
fn test_pipeline_into_method_call_style() {
25+
let engine = Engine::new();
26+
27+
// Pipeline into a method-call-style builtin (abs).
28+
// The pipeline passes the left-hand value as the first argument of the call on the right.
29+
let result = engine.eval::<INT>("let x = -123; x |> abs(); x").unwrap();
30+
31+
// abs should not have mutated `x` here, so `x` remains -123
32+
assert_eq!(result, -123);
33+
}
34+
35+
#[cfg(not(feature = "no_object"))]
36+
mod pipeline_method_tests {
37+
use rhai::{Engine, INT};
38+
39+
#[derive(Debug, Clone, Eq, PartialEq)]
40+
struct TestStruct {
41+
x: INT,
42+
}
43+
44+
impl TestStruct {
45+
fn update(&mut self, n: INT) {
46+
self.x += n;
47+
}
48+
49+
fn new() -> Self {
50+
Self { x: 1 }
51+
}
52+
}
53+
54+
#[test]
55+
fn test_pipeline_into_registered_method() {
56+
let mut engine = Engine::new();
57+
58+
engine.register_type::<TestStruct>().register_fn("update", TestStruct::update).register_fn("new_ts", TestStruct::new);
59+
60+
// Pipeline into a registered method should forward the left-hand value as the first argument
61+
let result = engine.eval::<TestStruct>("let x = new_ts(); x |> update(1000); x").unwrap();
62+
63+
assert_eq!(result, TestStruct { x: 1001 });
64+
}
65+
}

0 commit comments

Comments
 (0)