Feat: Add Pipe Node

Introduces a new `Pipe` node to the Abstract Syntax Tree (AST).
This node represents a functional composition pattern, allowing
for chaining of operations.

The changes include:
- Defining the `Pipe` variant in `BoundKind` and `UntypedKind`.
- Implementing parsing logic for the `pipe` keyword.
- Adding handling for the `Pipe` node in various compiler passes
  (analyzer, binder, captures, dumper, macros, optimizer, TCO,
  type checker).
- Including a placeholder implementation for `Pipe` execution in the
  VM.
- Updating integration tests to use the new `pipe` syntax.
This commit is contained in:
Michael Schimmel
2026-03-01 21:52:10 +01:00
parent a98e51c762
commit 50f33b2c30
14 changed files with 169 additions and 4 deletions
+14
View File
@@ -237,6 +237,20 @@ impl<'a> Analyzer<'a> {
Purity::Impure, Purity::Impure,
) )
} }
BoundKind::Pipe { inputs, lambda } => {
let mut analyzed_inputs = Vec::with_capacity(inputs.len());
for input in inputs {
analyzed_inputs.push(self.visit(Rc::new(input.clone())));
}
let a_lambda = Box::new(self.visit(Rc::new((**lambda).clone())));
(
BoundKind::Pipe {
inputs: analyzed_inputs,
lambda: a_lambda,
},
Purity::Impure,
)
}
BoundKind::Block { exprs } => { BoundKind::Block { exprs } => {
let mut new_exprs = Vec::with_capacity(exprs.len()); let mut new_exprs = Vec::with_capacity(exprs.len());
+15
View File
@@ -276,6 +276,21 @@ impl Binder {
} }
} }
UntypedKind::Pipe { inputs, lambda } => {
let mut bound_inputs = Vec::with_capacity(inputs.len());
for input in inputs {
bound_inputs.push(self.bind(&input)?);
}
let bound_lambda = Box::new(self.bind(lambda.as_ref())?);
Ok(self.make_node(
node.identity.clone(),
BoundKind::Pipe {
inputs: bound_inputs,
lambda: bound_lambda,
},
))
}
UntypedKind::Lambda { params, body } => { UntypedKind::Lambda { params, body } => {
let identity = node.identity.clone(); let identity = node.identity.clone();
self.functions.push(FunctionCompiler::new( self.functions.push(FunctionCompiler::new(
+5 -1
View File
@@ -135,7 +135,10 @@ pub enum BoundKind<T = ()> {
Again { Again {
args: Box<BoundNode<T>>, args: Box<BoundNode<T>>,
}, },
Pipe {
inputs: Vec<BoundNode<T>>,
lambda: Box<BoundNode<T>>,
},
Block { Block {
exprs: Vec<BoundNode<T>>, exprs: Vec<BoundNode<T>>,
}, },
@@ -304,6 +307,7 @@ impl<T> BoundKind<T> {
} }
BoundKind::Call { .. } => "CALL".to_string(), BoundKind::Call { .. } => "CALL".to_string(),
BoundKind::Again { .. } => "AGAIN".to_string(), BoundKind::Again { .. } => "AGAIN".to_string(),
BoundKind::Pipe { .. } => "PIPE".to_string(),
BoundKind::Block { .. } => "BLOCK".to_string(), BoundKind::Block { .. } => "BLOCK".to_string(),
BoundKind::Tuple { elements } => format!("TUPLE({})", elements.len()), BoundKind::Tuple { elements } => format!("TUPLE({})", elements.len()),
BoundKind::Record { values, .. } => format!("RECORD({})", values.len()), BoundKind::Record { values, .. } => format!("RECORD({})", values.len()),
+10 -1
View File
@@ -89,7 +89,16 @@ impl CapturePass {
args: Box::new(Self::transform(*args, capture_map)), args: Box::new(Self::transform(*args, capture_map)),
}; };
} }
BoundKind::Pipe { inputs, lambda } => {
let mut t_inputs = Vec::with_capacity(inputs.len());
for input in inputs {
t_inputs.push(Self::transform(input, capture_map));
}
node.kind = BoundKind::Pipe {
inputs: t_inputs,
lambda: Box::new(Self::transform(*lambda, capture_map)),
};
}
BoundKind::Block { exprs } => { BoundKind::Block { exprs } => {
node.kind = BoundKind::Block { node.kind = BoundKind::Block {
exprs: exprs exprs: exprs
+9
View File
@@ -203,6 +203,15 @@ impl Dumper {
self.visit(args); self.visit(args);
self.indent -= 1; self.indent -= 1;
} }
BoundKind::Pipe { inputs, lambda } => {
self.log("Pipe", node);
self.indent += 1;
for input in inputs {
self.visit(input);
}
self.visit(lambda);
self.indent -= 1;
}
BoundKind::Block { exprs } => { BoundKind::Block { exprs } => {
self.log("Block", node); self.log("Block", node);
+32
View File
@@ -152,6 +152,22 @@ impl<E: MacroEvaluator> MacroExpander<E> {
}) })
} }
UntypedKind::Pipe { inputs, lambda } => {
let mut expanded_inputs = Vec::with_capacity(inputs.len());
for input in inputs {
expanded_inputs.push(self.expand_recursive(input)?);
}
let expanded_lambda = Box::new(self.expand_recursive(*lambda)?);
Ok(Node {
identity: node.identity,
kind: UntypedKind::Pipe {
inputs: expanded_inputs,
lambda: expanded_lambda,
},
ty: (),
})
}
UntypedKind::Lambda { params, body } => { UntypedKind::Lambda { params, body } => {
self.registry.push(); self.registry.push();
let expanded_params = self.expand_recursive(*params)?; let expanded_params = self.expand_recursive(*params)?;
@@ -498,6 +514,22 @@ impl<E: MacroEvaluator> MacroExpander<E> {
}) })
} }
UntypedKind::Pipe { inputs, lambda } => {
let mut expanded_inputs = Vec::with_capacity(inputs.len());
for input in inputs {
expanded_inputs.push(self.expand_template(input, state)?);
}
let expanded_lambda = Box::new(self.expand_template(*lambda, state)?);
Ok(Node {
identity: node.identity,
kind: UntypedKind::Pipe {
inputs: expanded_inputs,
lambda: expanded_lambda,
},
ty: (),
})
}
UntypedKind::Lambda { params, body } => { UntypedKind::Lambda { params, body } => {
let expanded_params = self.expand_template(*params, state)?; let expanded_params = self.expand_template(*params, state)?;
let body = self.expand_template(Node::clone(&body), state)?; let body = self.expand_template(Node::clone(&body), state)?;
+15
View File
@@ -386,6 +386,21 @@ impl Optimizer {
) )
} }
BoundKind::Pipe { inputs, lambda } => {
let mut o_inputs = Vec::with_capacity(inputs.len());
for input in inputs {
o_inputs.push(self.visit_node(input, sub, path));
}
let o_lambda = Box::new(self.visit_node(*lambda, sub, path));
(
BoundKind::Pipe {
inputs: o_inputs,
lambda: o_lambda,
},
node.ty.clone(),
)
}
BoundKind::Block { ref exprs } => { BoundKind::Block { ref exprs } => {
let mut info = UsageInfo::default(); let mut info = UsageInfo::default();
if !exprs.is_empty() { if !exprs.is_empty() {
+6
View File
@@ -152,6 +152,12 @@ impl UsageInfo {
BoundKind::Again { args } => { BoundKind::Again { args } => {
self.collect(args); self.collect(args);
} }
BoundKind::Pipe { inputs, lambda } => {
for input in inputs {
self.collect(input);
}
self.collect(lambda);
}
BoundKind::Nop | BoundKind::FieldAccessor(_) | BoundKind::Extension(_) => {} BoundKind::Nop | BoundKind::FieldAccessor(_) | BoundKind::Extension(_) => {}
} }
} }
+10
View File
@@ -48,6 +48,16 @@ impl TCO {
args: Box::new(Self::transform(Rc::new((**args).clone()), false)), args: Box::new(Self::transform(Rc::new((**args).clone()), false)),
} }
} }
BoundKind::Pipe { inputs, lambda } => {
let mut t_inputs = Vec::with_capacity(inputs.len());
for input in inputs {
t_inputs.push(Self::transform(Rc::new(input.clone()), false));
}
BoundKind::Pipe {
inputs: t_inputs,
lambda: Box::new(Self::transform(Rc::new((**lambda).clone()), false)),
}
}
BoundKind::If { BoundKind::If {
cond, cond,
+20
View File
@@ -387,6 +387,26 @@ impl TypeChecker {
) )
} }
BoundKind::Pipe { inputs, lambda } => {
let mut typed_inputs = Vec::with_capacity(inputs.len());
for input in inputs {
typed_inputs.push(self.check_node(input, ctx)?);
}
let typed_lambda = self.check_node(*lambda, ctx)?;
let ret_ty = if let StaticType::Function(sig) = &typed_lambda.ty {
sig.ret.clone()
} else {
StaticType::Any
};
(
BoundKind::Pipe {
inputs: typed_inputs,
lambda: Box::new(typed_lambda),
},
StaticType::Series(Box::new(ret_ty)),
)
}
BoundKind::Block { exprs } => { BoundKind::Block { exprs } => {
let mut typed_exprs = Vec::new(); let mut typed_exprs = Vec::new();
let mut last_ty = StaticType::Void; let mut last_ty = StaticType::Void;
+4
View File
@@ -91,6 +91,10 @@ pub enum UntypedKind {
Again { Again {
args: Box<Node<UntypedKind>>, args: Box<Node<UntypedKind>>,
}, },
Pipe {
inputs: Vec<Node<UntypedKind>>,
lambda: Box<Node<UntypedKind>>,
},
Block { Block {
exprs: Vec<Node<UntypedKind>>, exprs: Vec<Node<UntypedKind>>,
}, },
+16
View File
@@ -137,6 +137,7 @@ impl<'a> Parser<'a> {
match sym.name.as_ref() { match sym.name.as_ref() {
"if" => self.parse_if(identity), "if" => self.parse_if(identity),
"fn" => self.parse_fn(identity), "fn" => self.parse_fn(identity),
"pipe" => self.parse_pipe(identity),
"again" => self.parse_again(identity), "again" => self.parse_again(identity),
"def" => self.parse_def(identity), "def" => self.parse_def(identity),
"assign" => self.parse_assign(identity), "assign" => self.parse_assign(identity),
@@ -229,6 +230,21 @@ impl<'a> Parser<'a> {
}) })
} }
fn parse_pipe(&mut self, identity: Identity) -> Result<Node<UntypedKind>, String> {
let inputs_node = self.parse_expression()?;
let inputs = match inputs_node.kind {
UntypedKind::Tuple { elements } => elements,
_ => vec![inputs_node],
};
let lambda = Box::new(self.parse_expression()?);
Ok(Node {
identity,
kind: UntypedKind::Pipe { inputs, lambda },
ty: (),
})
}
fn parse_fn(&mut self, identity: Identity) -> Result<Node<UntypedKind>, String> { fn parse_fn(&mut self, identity: Identity) -> Result<Node<UntypedKind>, String> {
let params = Box::new(self.parse_param_vector()?); let params = Box::new(self.parse_param_vector()?);
let body = self.parse_expression()?; let body = self.parse_expression()?;
+11
View File
@@ -368,6 +368,17 @@ impl VM {
Ok(Value::Void) Ok(Value::Void)
} }
} }
BoundKind::Pipe { inputs, lambda } => {
// To be implemented in next step:
// 1. Create PipeStream
// 2. Link with RootStream and Inputs
// 3. Create and return SharedSeries
for input in inputs {
self.eval_internal(obs, input)?;
}
self.eval_internal(obs, lambda)?;
Ok(Value::Void)
}
BoundKind::Block { exprs } => { BoundKind::Block { exprs } => {
let mut last = Value::Void; let mut last = Value::Void;
for e in exprs { for e in exprs {
+2 -2
View File
@@ -364,7 +364,7 @@ mod tests {
fn test_multi_level_destructuring() { fn test_multi_level_destructuring() {
let env = Environment::new(); let env = Environment::new();
let source = "(do let source = "(do
(def pipe (fn [conf] (def process_data (fn [conf]
(do (do
(def [str s] conf) (def [str s] conf)
(def [f ss] s) (def [f ss] s)
@@ -372,7 +372,7 @@ mod tests {
) )
) )
) )
(pipe [\"btc\" [:close \"cls\"]]))"; (process_data [\"btc\" [:close \"cls\"]]))";
let res = env.run_script(source).unwrap(); let res = env.run_script(source).unwrap();
assert_eq!( assert_eq!(