Skip to content

Project 5 — First-Class Functions: #2

Open
SapoSopa wants to merge 11 commits into
rbonifacio:mainfrom
SapoSopa:main
Open

Project 5 — First-Class Functions: #2
SapoSopa wants to merge 11 commits into
rbonifacio:mainfrom
SapoSopa:main

Conversation

@SapoSopa
Copy link
Copy Markdown

@SapoSopa SapoSopa commented Apr 19, 2026

Grupo:

  • Álvaro Cavalcante Negromonte
  • Gabriel Valença Mayerhofer
  • Henrique César Higino Holanda Cordeiro
  • João Victor Nascimento Lima
  • Vinicius de Souza Rodrigues

Segue a baixo a descrição pelo copilot.

This pull request adds support for first-class and anonymous functions (lambdas), function values, and function calls via expressions, including closures with lexical scoping. It also updates the parser, type checker, and interpreter to handle these new language features, and refines variable declaration and initialization rules.

Support for first-class functions and lambdas:

  • Added new AST nodes for lambda expressions (Expr::Lambda) and function calls via expressions (Expr::CallExpr), enabling anonymous functions and higher-order programming. [1] [2]
  • Extended the parser to recognize and parse lambda expressions, function types, and function calls via expressions. Reserved word list now includes fn. [1] [2] [3] [4] [5] [6]
  • Updated the type checker to type-check lambdas, closures, and function values, including argument and return type compatibility and closure environments. [1] [2] [3]

Interpreter and runtime changes:

  • Interpreter now supports evaluating lambda expressions and closures, including capturing lexical environments for closures and supporting both direct and expression-based function calls. [1] [2]
  • Added a new Closure variant to FnValue to represent closures with captured environments, and implemented appropriate equality and debug formatting. [1] [2] [3]

Variable declaration and initialization:

  • Variable declarations can now omit initializers, but both the type checker and interpreter enforce that variables must be initialized before use, improving error handling and language safety. [1] [2] [3] [4] [5]

Function type parsing and compatibility:

  • Added parsing for function types (fn(...) -> ...) as type annotations, with checks to avoid confusion with lambdas. Improved type compatibility checks for function types. [1] [2]

These changes together provide robust support for functional programming constructs in the language, including lambdas, closures, and first-class function values.

Copilot AI review requested due to automatic review settings April 19, 2026 16:19
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR extends the MiniC language implementation to support first-class functions, including function types (fn(...) -> ...), anonymous functions (lambdas), and calling function values via expressions (closures with lexical capture), with accompanying parser/type-checker/interpreter updates and new tests.

Changes:

  • Added AST and parser support for function types, lambdas (Expr::Lambda), and expression-based calls (Expr::CallExpr).
  • Updated the type checker and interpreter to handle function values/closures and calling via expressions.
  • Added/updated tests for function type parsing/integration plus lambda/closure behavior.

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
src/ir/ast.rs Adds AST nodes for lambdas and call-by-expression; makes declaration initializer optional.
src/parser/identifiers.rs Reserves fn keyword.
src/parser/functions.rs Adds parsing for function types (fn(...) -> ...).
src/parser/expressions.rs Adds lambda parsing and parses calls as postfix on any primary expression.
src/parser/statements.rs Allows Type ident [= expr]; declarations; exports block_statement for lambda parsing.
src/semantic/type_checker.rs Adds typing for lambdas/call-by-expression and function-type compatibility; changes decl-init handling.
src/interpreter/value.rs Adds FnValue::Closure and equality/debug behavior for closures.
src/interpreter/eval_expr.rs Evaluates lambdas into closures, supports expression-based calls, and introduces eval_call_value.
src/interpreter/exec_stmt.rs Changes declaration execution to require an initializer at runtime.
tests/type_function_tests.rs New unit tests for parsing function types via type_name.
tests/type_function_integration_tests.rs New parser integration tests for function types in declarations and function signatures.
tests/type_checker.rs Adds lambda type-checking tests and updates decl-init expectations.
tests/parser.rs Updates decl test for optional initializer and tweaks an index-pattern match.
tests/interpreter.rs Adds lambda/closure interpreter tests and a new helper run_full.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +50 to 56
let init = init.as_ref().ok_or_else(|| {
RuntimeError::new(format!(
"variable '{}' declared without initializer",
name
))
})?;
let val = eval_expr(init, env)?;
Copy link

Copilot AI Apr 19, 2026

Choose a reason for hiding this comment

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

Runtime now errors immediately on Decl without initializer. Since the parser/AST allow Type ident ;, this prevents the intended workflow of declaring a variable and assigning later (and also blocks fn(...) -> ... f; as documented in the AST). If uninitialized declarations are meant to be supported, declare the variable with a sentinel (e.g., Value::Uninitialized) and raise an error only when the variable is read/called before assignment.

Suggested change
let init = init.as_ref().ok_or_else(|| {
RuntimeError::new(format!(
"variable '{}' declared without initializer",
name
))
})?;
let val = eval_expr(init, env)?;
let val = match init.as_ref() {
Some(init) => eval_expr(init, env)?,
None => Value::Uninitialized,
};

Copilot uses AI. Check for mistakes.
Comment thread src/parser/functions.rs
Comment on lines +80 to +86
// avoid confusing function types with lambdas
if rest.trim_start().starts_with("{") {
return Err(nom::Err::Error(nom::error::Error::new(
rest,
nom::error::ErrorKind::Tag,
)));
}
Copy link

Copilot AI Apr 19, 2026

Choose a reason for hiding this comment

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

fun_type rejects a function type if the remaining input starts with { to avoid confusing types with lambdas. This also rejects valid nested function types in contexts where a { legitimately follows a type, e.g. parsing a lambda that returns a function type: fn() -> fn(int)->int { ... } (the return type parse will see { and error). A safer disambiguation is to allow fun_type unconditionally and resolve the fn(...) -> ... { ... } vs fn(...) -> ... ambiguity in the expression parser (by trying lambda_expr before other atoms) and/or in the surrounding statement grammar, rather than inside type_name.

Copilot uses AI. Check for mistakes.
Comment on lines +206 to +230
fn eval_call_value(
callee: Value,
args: Vec<Value>,
env: &mut Environment<Value>,
) -> Result<Value, RuntimeError> {
match callee {
Value::Fn(FnValue::Native(f)) => (f)(args),

Value::Fn(FnValue::UserDefined(decl)) => {
if args.len() != decl.params.len() {
return Err(RuntimeError::new(format!(
"function '{}' expects {} arguments, got {}",
decl.name,
decl.params.len(),
args.len()
)));
}
let snapshot = env.snapshot();
for ((param_name, _), val) in decl.params.iter().zip(args.into_iter()) {
env.declare(param_name.clone(), val);
}
let result = exec_stmt(&decl.body, env)?;
env.restore(snapshot);
Ok(result.unwrap_or(Value::Void))
}
Copy link

Copilot AI Apr 19, 2026

Choose a reason for hiding this comment

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

eval_call_value supports FnValue::Closure, but eval_call (used by call-statements and as the interpreter entrypoint for main) does not. As a result, a closure stored in a variable will work in expression position (print(f(1)) via Expr::Call), but will fail at runtime as a statement (f(1); becomes Statement::Call and routes through eval_call). Consider implementing the Closure branch in eval_call as well, or refactoring eval_call to just look up the callee in the environment and delegate to eval_call_value.

Copilot uses AI. Check for mistakes.
Comment thread src/interpreter/value.rs
match (self, other) {
(FnValue::UserDefined(a), FnValue::UserDefined(b)) => a == b,
(FnValue::Native(a), FnValue::Native(b)) => (*a as usize) == (*b as usize),
(FnValue::Closure { decl: da, .. }, FnValue::Closure { decl: db, .. }) => da == db,
Copy link

Copilot AI Apr 19, 2026

Choose a reason for hiding this comment

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

FnValue::Closure equality compares only decl and ignores captured. This means two closures created from the same lambda body but with different captured values will compare equal (and Value derives PartialEq, so == uses this). With Type::Fun now considered compatible, function equality can be type-checked, so this can produce incorrect runtime results. Consider either (1) disallowing Eq/Ne on function values at the type-checker level, or (2) defining closure equality as identity (e.g., unique id) / include captured environment in the comparison.

Suggested change
(FnValue::Closure { decl: da, .. }, FnValue::Closure { decl: db, .. }) => da == db,
(
FnValue::Closure {
decl: da,
captured: ca,
},
FnValue::Closure {
decl: db,
captured: cb,
},
) => da == db && ca == cb,

Copilot uses AI. Check for mistakes.
Comment thread src/ir/ast.rs
Comment on lines +107 to +122
/// Chamada de função por expressão: chmd(args)
/// apenas para não mexer em Call, mas depois podemos mesclar os dois (Call pode ser um caso especial de CallExpr onde callee é um Ident).
/// Ex.: 'f(42)', '(funçãolambda)(42)', etc.)
CallExpr {
chmd: Box<ExprD<Ty>>,
args: Vec<ExprD<Ty>>,
},

/// Função Lambda: `fn(params) -> return_tipo { crp }`
/// regra pra não ficar ambiguo:
/// 'fn(...) -> ...' é tipo, ou seja 'Type::Fun'
/// 'fn(...) -> ... { ... }' é expressão, ou seja 'Expr::Lambda'
Lambda {
params: Vec<Param>,
return_tipo: Type,
crp: Box<StatementD<Ty>>,
Copy link

Copilot AI Apr 19, 2026

Choose a reason for hiding this comment

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

New AST fields use abbreviated/Portuguese names (chmd, crp, return_tipo) which are hard to understand and inconsistent with the rest of the AST (name, args, return_type, etc.). Renaming to clearer identifiers like callee, body/block, and return_type will make the parser/type-checker/interpreter code easier to follow and reduce future mistakes.

Suggested change
/// Chamada de função por expressão: chmd(args)
/// apenas para não mexer em Call, mas depois podemos mesclar os dois (Call pode ser um caso especial de CallExpr onde callee é um Ident).
/// Ex.: 'f(42)', '(funçãolambda)(42)', etc.)
CallExpr {
chmd: Box<ExprD<Ty>>,
args: Vec<ExprD<Ty>>,
},
/// Função Lambda: `fn(params) -> return_tipo { crp }`
/// regra pra não ficar ambiguo:
/// 'fn(...) -> ...' é tipo, ou seja 'Type::Fun'
/// 'fn(...) -> ... { ... }' é expressão, ou seja 'Expr::Lambda'
Lambda {
params: Vec<Param>,
return_tipo: Type,
crp: Box<StatementD<Ty>>,
/// Function call through an expression: callee(args)
/// Kept separate from `Call` for now, though the two could be merged later
/// (`Call` can be treated as a special case of `CallExpr` where `callee`
/// is an `Ident`). Examples: `f(42)`, `(lambda_fn)(42)`, etc.
CallExpr {
callee: Box<ExprD<Ty>>,
args: Vec<ExprD<Ty>>,
},
/// Lambda function: `fn(params) -> return_type { body }`
/// To avoid ambiguity:
/// `fn(...) -> ...` is a type, i.e. `Type::Fun`
/// `fn(...) -> ... { ... }` is an expression, i.e. `Expr::Lambda`
Lambda {
params: Vec<Param>,
return_type: Type,
body: Box<StatementD<Ty>>,

Copilot uses AI. Check for mistakes.
Comment thread tests/interpreter.rs
Comment on lines +262 to +272
/// Executa o pipeline completo: Parser -> Type Check -> Interpret
fn run_full(src: &str) -> Result<(), String> {
let (_, unchecked) = program(src).map_err(|e| format!("Parser Error: {:?}", e))?;

// Passando &unchecked (Referência)
let checked = type_check(&unchecked).map_err(|e| format!("Type Error: {}", e.message))?;

// Execução
interpret(&checked).map_err(|e| format!("Runtime Error: {}", e.message))?;

Ok(())
Copy link

Copilot AI Apr 19, 2026

Choose a reason for hiding this comment

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

run_full duplicates the existing run helper at the top of this file (same pipeline: parse → type_check → interpret) and adds a second set of slightly different error strings. This duplication makes future test maintenance harder; consider reusing run directly or extending it to cover the desired error formatting.

Suggested change
/// Executa o pipeline completo: Parser -> Type Check -> Interpret
fn run_full(src: &str) -> Result<(), String> {
let (_, unchecked) = program(src).map_err(|e| format!("Parser Error: {:?}", e))?;
// Passando &unchecked (Referência)
let checked = type_check(&unchecked).map_err(|e| format!("Type Error: {}", e.message))?;
// Execução
interpret(&checked).map_err(|e| format!("Runtime Error: {}", e.message))?;
Ok(())
/// Executa o pipeline completo reutilizando o helper padrão de testes.
fn run_full(src: &str) -> Result<(), String> {
run(src)

Copilot uses AI. Check for mistakes.
Comment thread tests/interpreter.rs
);
}

#[test]
Copy link

Copilot AI Apr 19, 2026

Choose a reason for hiding this comment

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

The added interpreter tests cover calling closures in expression position (print(somar(50))), but they don't cover calling a closure as a statement (somar(50);). Given Statement::Call currently routes through eval_call, this is an important case to test to prevent regressions once closure dispatch is unified for statements and expressions.

Suggested change
#[test]
#[test]
fn test_exec_closure_call_as_statement() {
// Garante cobertura para chamada de closure em posição de statement: somar(50);
let src = r#"
void main() {
int base = 100;
fn(int) -> void somar = fn(int n) -> void { print(n + base); };
somar(50);
}
"#;
let result = run_full(src);
assert!(
result.is_ok(),
"A chamada de closure em posição de statement falhou. Erro: {:?}",
result.err()
);
}
#[test]

Copilot uses AI. Check for mistakes.
Comment on lines +155 to +160
let init = init.as_ref().ok_or_else(|| {
TypeError::new(format!(
"variable '{}' must be initialized",
name
))
})?;
Copy link

Copilot AI Apr 19, 2026

Choose a reason for hiding this comment

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

Statement::Decl now rejects missing initializers (init: None) at type-check time, but the parser/AST were changed to allow Type ident ; (and even documented in ast.rs for fn(...) -> ... f;). This makes programs with declarations without init untypecheckable and contradicts the intended “declare first, initialize before use” behavior. Consider allowing init: None here (at least for function-typed vars), declaring the name in env, and tracking initialization state separately so reads/calls can error if used before assignment.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants