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:
Michael Schimmel
2026-02-23 20:33:50 +01:00
parent be7ce31408
commit 4905b08548
13 changed files with 147 additions and 0 deletions
+9
View File
@@ -123,6 +123,15 @@ impl<'a> Analyzer<'a> {
(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 } => {
let mut new_exprs = Vec::with_capacity(exprs.len());
let mut p = Purity::Pure;
+13
View File
@@ -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 } => {
let mut bound_exprs = Vec::new();
for expr in exprs {
+6
View File
@@ -96,6 +96,10 @@ pub enum BoundKind<T = ()> {
args: Box<BoundNode<T>>,
},
Again {
args: Box<BoundNode<T>>,
},
Block {
exprs: Vec<BoundNode<T>>,
},
@@ -207,6 +211,7 @@ where
args: 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::Tuple { elements: ea }, BoundKind::Tuple { elements: eb }) => ea == eb,
(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())
}
BoundKind::Call { .. } => "CALL".to_string(),
BoundKind::Again { .. } => "AGAIN".to_string(),
BoundKind::Block { .. } => "BLOCK".to_string(),
BoundKind::Tuple { elements } => format!("TUPLE({})", elements.len()),
BoundKind::Record { fields } => format!("RECORD({})", fields.len()),
+7
View File
@@ -187,6 +187,13 @@ impl Dumper {
self.indent -= 1;
}
BoundKind::Again { args } => {
self.log("Again", node);
self.indent += 1;
self.visit(args);
self.indent -= 1;
}
BoundKind::Block { exprs } => {
self.log("Block", node);
self.indent += 1;
+10
View File
@@ -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 {
ref cond,
ref then_br,
+9
View File
@@ -40,6 +40,15 @@ impl TCO {
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 {
cond,
then_br,
+27
View File
@@ -11,6 +11,8 @@ struct TypeContext<'a> {
slots: Vec<StaticType>,
/// Types of captured variables (passed from outer scope)
upvalue_types: Vec<StaticType>,
/// The expected parameters of the current function (for 'again' validation)
current_params_ty: Option<StaticType>,
}
impl<'a> TypeContext<'a> {
@@ -23,6 +25,7 @@ impl<'a> TypeContext<'a> {
_parent: parent,
slots: vec![StaticType::Any; slot_count as usize],
upvalue_types,
current_params_ty: None,
}
}
@@ -324,6 +327,10 @@ impl TypeChecker {
// 3. Check parameters and body
let params_typed =
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 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 } => {
let mut typed_elements = Vec::new();
for e in elements {
+3
View File
@@ -99,6 +99,9 @@ impl UpvalueAnalyzer {
Self::visit(callee, 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 } => {
for expr in exprs {
Self::visit(expr, scopes, capture_map, current_lambda.clone());