Add 'again' keyword for recursive calls
The `again` keyword is introduced to facilitate explicit recursive function calls. It is restricted to tail-call positions to prevent dead code and ensure TCO optimization. Type checking is enhanced to validate argument types against function parameters.
This commit is contained in:
@@ -123,6 +123,15 @@ impl<'a> Analyzer<'a> {
|
|||||||
(BoundKind::Call { callee: Box::new(callee_m), args: Box::new(args_m) }, p)
|
(BoundKind::Call { callee: Box::new(callee_m), args: Box::new(args_m) }, p)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
BoundKind::Again { args } => {
|
||||||
|
let args_m = self.visit(Rc::new((**args).clone()));
|
||||||
|
if let Some(lambda_id) = self.lambda_stack.last() {
|
||||||
|
self.recursive_identities.insert(lambda_id.clone());
|
||||||
|
is_recursive = true;
|
||||||
|
}
|
||||||
|
(BoundKind::Again { args: Box::new(args_m) }, 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());
|
||||||
let mut p = Purity::Pure;
|
let mut p = Purity::Pure;
|
||||||
|
|||||||
@@ -289,6 +289,19 @@ impl Binder {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
UntypedKind::Again { args } => {
|
||||||
|
if self.functions.len() <= 1 {
|
||||||
|
return Err("'again' is only allowed inside a function or lambda.".to_string());
|
||||||
|
}
|
||||||
|
let args = self.bind(args)?;
|
||||||
|
Ok(self.make_node(
|
||||||
|
node.identity.clone(),
|
||||||
|
BoundKind::Again {
|
||||||
|
args: Box::new(args),
|
||||||
|
},
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
UntypedKind::Block { exprs } => {
|
UntypedKind::Block { exprs } => {
|
||||||
let mut bound_exprs = Vec::new();
|
let mut bound_exprs = Vec::new();
|
||||||
for expr in exprs {
|
for expr in exprs {
|
||||||
|
|||||||
@@ -96,6 +96,10 @@ pub enum BoundKind<T = ()> {
|
|||||||
args: Box<BoundNode<T>>,
|
args: Box<BoundNode<T>>,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
Again {
|
||||||
|
args: Box<BoundNode<T>>,
|
||||||
|
},
|
||||||
|
|
||||||
Block {
|
Block {
|
||||||
exprs: Vec<BoundNode<T>>,
|
exprs: Vec<BoundNode<T>>,
|
||||||
},
|
},
|
||||||
@@ -207,6 +211,7 @@ where
|
|||||||
args: ab,
|
args: ab,
|
||||||
},
|
},
|
||||||
) => ca == cb && aa == ab,
|
) => ca == cb && aa == ab,
|
||||||
|
(BoundKind::Again { args: aa }, BoundKind::Again { args: ab }) => aa == ab,
|
||||||
(BoundKind::Block { exprs: ea }, BoundKind::Block { exprs: eb }) => ea == eb,
|
(BoundKind::Block { exprs: ea }, BoundKind::Block { exprs: eb }) => ea == eb,
|
||||||
(BoundKind::Tuple { elements: ea }, BoundKind::Tuple { elements: eb }) => ea == eb,
|
(BoundKind::Tuple { elements: ea }, BoundKind::Tuple { elements: eb }) => ea == eb,
|
||||||
(BoundKind::Record { fields: fa }, BoundKind::Record { fields: fb }) => fa == fb,
|
(BoundKind::Record { fields: fa }, BoundKind::Record { fields: fb }) => fa == fb,
|
||||||
@@ -254,6 +259,7 @@ impl<T> BoundKind<T> {
|
|||||||
format!("LAMBDA({}, Captures:{})", p_str, upvalues.len())
|
format!("LAMBDA({}, Captures:{})", p_str, upvalues.len())
|
||||||
}
|
}
|
||||||
BoundKind::Call { .. } => "CALL".to_string(),
|
BoundKind::Call { .. } => "CALL".to_string(),
|
||||||
|
BoundKind::Again { .. } => "AGAIN".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 { fields } => format!("RECORD({})", fields.len()),
|
BoundKind::Record { fields } => format!("RECORD({})", fields.len()),
|
||||||
|
|||||||
@@ -187,6 +187,13 @@ impl Dumper {
|
|||||||
self.indent -= 1;
|
self.indent -= 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
BoundKind::Again { args } => {
|
||||||
|
self.log("Again", node);
|
||||||
|
self.indent += 1;
|
||||||
|
self.visit(args);
|
||||||
|
self.indent -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
BoundKind::Block { exprs } => {
|
BoundKind::Block { exprs } => {
|
||||||
self.log("Block", node);
|
self.log("Block", node);
|
||||||
self.indent += 1;
|
self.indent += 1;
|
||||||
|
|||||||
@@ -313,6 +313,16 @@ impl Optimizer {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
BoundKind::Again { args } => {
|
||||||
|
let args = self.visit_node(*args, sub, path);
|
||||||
|
(
|
||||||
|
BoundKind::Again {
|
||||||
|
args: Box::new(args),
|
||||||
|
},
|
||||||
|
node.ty.clone(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
BoundKind::If {
|
BoundKind::If {
|
||||||
ref cond,
|
ref cond,
|
||||||
ref then_br,
|
ref then_br,
|
||||||
|
|||||||
@@ -40,6 +40,15 @@ impl TCO {
|
|||||||
args: Box::new(Self::transform(Rc::new((**args).clone()), false)),
|
args: Box::new(Self::transform(Rc::new((**args).clone()), false)),
|
||||||
},
|
},
|
||||||
|
|
||||||
|
BoundKind::Again { args } => {
|
||||||
|
if !is_tail_position {
|
||||||
|
panic!("'again' is only allowed in tail position to avoid dead code.");
|
||||||
|
}
|
||||||
|
BoundKind::Again {
|
||||||
|
args: Box::new(Self::transform(Rc::new((**args).clone()), false)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
BoundKind::If {
|
BoundKind::If {
|
||||||
cond,
|
cond,
|
||||||
then_br,
|
then_br,
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ struct TypeContext<'a> {
|
|||||||
slots: Vec<StaticType>,
|
slots: Vec<StaticType>,
|
||||||
/// Types of captured variables (passed from outer scope)
|
/// Types of captured variables (passed from outer scope)
|
||||||
upvalue_types: Vec<StaticType>,
|
upvalue_types: Vec<StaticType>,
|
||||||
|
/// The expected parameters of the current function (for 'again' validation)
|
||||||
|
current_params_ty: Option<StaticType>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> TypeContext<'a> {
|
impl<'a> TypeContext<'a> {
|
||||||
@@ -23,6 +25,7 @@ impl<'a> TypeContext<'a> {
|
|||||||
_parent: parent,
|
_parent: parent,
|
||||||
slots: vec![StaticType::Any; slot_count as usize],
|
slots: vec![StaticType::Any; slot_count as usize],
|
||||||
upvalue_types,
|
upvalue_types,
|
||||||
|
current_params_ty: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -324,6 +327,10 @@ impl TypeChecker {
|
|||||||
// 3. Check parameters and body
|
// 3. Check parameters and body
|
||||||
let params_typed =
|
let params_typed =
|
||||||
self.check_params(params.as_ref().clone(), &StaticType::Any, &mut lambda_ctx)?;
|
self.check_params(params.as_ref().clone(), &StaticType::Any, &mut lambda_ctx)?;
|
||||||
|
|
||||||
|
// Set current params type for 'again' validation
|
||||||
|
lambda_ctx.current_params_ty = Some(params_typed.ty.clone());
|
||||||
|
|
||||||
let body_typed = self.check_node((*body).clone(), &mut lambda_ctx)?;
|
let body_typed = self.check_node((*body).clone(), &mut lambda_ctx)?;
|
||||||
let ret_ty = body_typed.ty.clone();
|
let ret_ty = body_typed.ty.clone();
|
||||||
|
|
||||||
@@ -362,6 +369,26 @@ impl TypeChecker {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
BoundKind::Again { args } => {
|
||||||
|
let args_typed = self.check_node(*args, ctx)?;
|
||||||
|
|
||||||
|
if let Some(expected_ty) = &ctx.current_params_ty
|
||||||
|
&& !expected_ty.is_assignable_from(&args_typed.ty)
|
||||||
|
{
|
||||||
|
return Err(format!(
|
||||||
|
"Type mismatch in 'again' call: expected {}, but got {}",
|
||||||
|
expected_ty, args_typed.ty
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
(
|
||||||
|
BoundKind::Again {
|
||||||
|
args: Box::new(args_typed),
|
||||||
|
},
|
||||||
|
StaticType::Any,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
BoundKind::Tuple { elements } => {
|
BoundKind::Tuple { elements } => {
|
||||||
let mut typed_elements = Vec::new();
|
let mut typed_elements = Vec::new();
|
||||||
for e in elements {
|
for e in elements {
|
||||||
|
|||||||
@@ -99,6 +99,9 @@ impl UpvalueAnalyzer {
|
|||||||
Self::visit(callee, scopes, capture_map, current_lambda.clone());
|
Self::visit(callee, scopes, capture_map, current_lambda.clone());
|
||||||
Self::visit(args, scopes, capture_map, current_lambda.clone());
|
Self::visit(args, scopes, capture_map, current_lambda.clone());
|
||||||
}
|
}
|
||||||
|
UntypedKind::Again { args } => {
|
||||||
|
Self::visit(args, scopes, capture_map, current_lambda.clone());
|
||||||
|
}
|
||||||
UntypedKind::Block { exprs } => {
|
UntypedKind::Block { exprs } => {
|
||||||
for expr in exprs {
|
for expr in exprs {
|
||||||
Self::visit(expr, scopes, capture_map, current_lambda.clone());
|
Self::visit(expr, scopes, capture_map, current_lambda.clone());
|
||||||
|
|||||||
@@ -86,6 +86,9 @@ pub enum UntypedKind {
|
|||||||
callee: Box<Node<UntypedKind>>,
|
callee: Box<Node<UntypedKind>>,
|
||||||
args: Box<Node<UntypedKind>>,
|
args: Box<Node<UntypedKind>>,
|
||||||
},
|
},
|
||||||
|
Again {
|
||||||
|
args: Box<Node<UntypedKind>>,
|
||||||
|
},
|
||||||
Block {
|
Block {
|
||||||
exprs: Vec<Node<UntypedKind>>,
|
exprs: Vec<Node<UntypedKind>>,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -140,6 +140,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),
|
||||||
|
"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),
|
||||||
"do" => self.parse_do(identity),
|
"do" => self.parse_do(identity),
|
||||||
@@ -154,6 +155,15 @@ impl<'a> Parser<'a> {
|
|||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn parse_again(&mut self, identity: Identity) -> Result<Node<UntypedKind>, String> {
|
||||||
|
let args = Box::new(self.parse_expression()?);
|
||||||
|
Ok(Node {
|
||||||
|
identity,
|
||||||
|
kind: UntypedKind::Again { args },
|
||||||
|
ty: (),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn parse_if(&mut self, identity: Identity) -> Result<Node<UntypedKind>, String> {
|
fn parse_if(&mut self, identity: Identity) -> Result<Node<UntypedKind>, String> {
|
||||||
let cond = Box::new(self.parse_expression()?);
|
let cond = Box::new(self.parse_expression()?);
|
||||||
let then_br = Box::new(self.parse_expression()?);
|
let then_br = Box::new(self.parse_expression()?);
|
||||||
|
|||||||
@@ -249,6 +249,16 @@ impl StaticType {
|
|||||||
}
|
}
|
||||||
elements.iter().all(|e| e.is_assignable_from(inner))
|
elements.iter().all(|e| e.is_assignable_from(inner))
|
||||||
}
|
}
|
||||||
|
// Tuple to Tuple (Element-wise)
|
||||||
|
(StaticType::Tuple(elements), StaticType::Tuple(other_elements)) => {
|
||||||
|
if elements.len() != other_elements.len() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
elements
|
||||||
|
.iter()
|
||||||
|
.zip(other_elements.iter())
|
||||||
|
.all(|(e, o)| e.is_assignable_from(o))
|
||||||
|
}
|
||||||
// A Matrix is a Vector (of Vectors/Matrices)
|
// A Matrix is a Vector (of Vectors/Matrices)
|
||||||
(StaticType::Vector(inner, len), StaticType::Matrix(m_inner, shape)) => {
|
(StaticType::Vector(inner, len), StaticType::Matrix(m_inner, shape)) => {
|
||||||
if shape.is_empty() || shape[0] != *len {
|
if shape.is_empty() || shape[0] != *len {
|
||||||
|
|||||||
@@ -433,6 +433,37 @@ impl VM {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
BoundKind::Again { args } => {
|
||||||
|
let arg_vals = match &args.kind {
|
||||||
|
BoundKind::Tuple { elements } => {
|
||||||
|
let mut vals = Vec::with_capacity(elements.len());
|
||||||
|
let mut is_complex = false;
|
||||||
|
for e in elements {
|
||||||
|
if matches!(e.kind, BoundKind::Tuple { .. }) {
|
||||||
|
is_complex = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
vals.push(self.eval_internal(obs, e)?);
|
||||||
|
}
|
||||||
|
if is_complex {
|
||||||
|
self.prepare_args_internal(obs, args)?
|
||||||
|
} else {
|
||||||
|
vals
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => self.prepare_args_internal(obs, args)?,
|
||||||
|
};
|
||||||
|
|
||||||
|
let frame = self.frames.last().ok_or("No call frame for 'again'")?;
|
||||||
|
if let Some(closure) = &frame.closure {
|
||||||
|
Ok(Value::TailCallRequest(Box::new((
|
||||||
|
Rc::new(closure.as_ref().clone()) as Rc<dyn Object>,
|
||||||
|
arg_vals,
|
||||||
|
))))
|
||||||
|
} else {
|
||||||
|
Err("'again' called outside of a closure".to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
BoundKind::Tuple { elements } => {
|
BoundKind::Tuple { elements } => {
|
||||||
let mut vals = Vec::with_capacity(elements.len());
|
let mut vals = Vec::with_capacity(elements.len());
|
||||||
for e in elements {
|
for e in elements {
|
||||||
|
|||||||
@@ -263,6 +263,15 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[should_panic(expected = "'again' is only allowed in tail position to avoid dead code.")]
|
||||||
|
fn test_again_non_tail_panic() {
|
||||||
|
let env = Environment::new();
|
||||||
|
let source = "(do (def f (fn [x] (do (again [(- x 1)]) x))) (f 5))";
|
||||||
|
// This will trigger the TCO pass which contains the validation logic
|
||||||
|
let _ = env.run_script(source);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_dynamic_call_destructuring_underflow() {
|
fn test_dynamic_call_destructuring_underflow() {
|
||||||
let env = Environment::new();
|
let env = Environment::new();
|
||||||
|
|||||||
Reference in New Issue
Block a user