Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 81 additions & 1 deletion src/interpreter/eval_expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,30 @@ pub fn eval_expr(expr: &CheckedExpr, env: &mut Environment<Value>) -> Result<Val
Expr::Call { name, args } => {
let arg_vals: Result<Vec<Value>, RuntimeError> =
args.iter().map(|a| eval_expr(a, env)).collect();
eval_call(name, arg_vals?, env)

let callee = env.get(name)
.cloned()
.ok_or_else(|| RuntimeError::new(format!("undefined function '{}'", name)))?;

eval_call_value(callee, arg_vals?, env)
}

Expr::CallExpr { chmd, args } => {
let callee_val = eval_expr(chmd, env)?;
let arg_vals: Result<Vec<Value>, RuntimeError> =
args.iter().map(|a| eval_expr(a, env)).collect();
eval_call_value(callee_val, arg_vals?, env)
}

Expr::Lambda { params, return_tipo, crp } => {
let captured = env.snapshot();
let decl = crate::ir::ast::FunDecl {
name: "<lambda>".to_string(),
params: params.clone(),
return_type: return_tipo.clone(),
body: crp.clone(),
};
Ok(Value::Fn(FnValue::Closure { decl, captured }))
}
}
}
Expand Down Expand Up @@ -180,6 +203,63 @@ pub fn eval_call(
}
}

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))
}
Comment on lines +206 to +230
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.

Value::Fn(FnValue::Closure { decl, captured }) => {
if args.len() != decl.params.len() {
return Err(RuntimeError::new(format!(
"function expects {} arguments, got {}",
decl.params.len(),
args.len()
)));
}

let caller_snapshot = env.snapshot();

// entra no ambiente capturado (lexical scoping)
env.restore(captured);

// bind dos params “por cima” do capturado
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)?;

// volta para o caller
env.restore(caller_snapshot);

Ok(result.unwrap_or(Value::Void))
}

other => Err(RuntimeError::new(format!("'{}' is not a function", other))),
}
}

// --- Helpers ---

fn eval_literal(lit: &Literal) -> Value {
Expand Down
6 changes: 6 additions & 0 deletions src/interpreter/exec_stmt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@ pub fn exec_stmt(stmt: &CheckedStmt, env: &mut Environment<Value>) -> ExecResult
match &stmt.stmt {
// --- Variable declaration ---
Statement::Decl { name, init, .. } => {
let init = init.as_ref().ok_or_else(|| {
RuntimeError::new(format!(
"variable '{}' declared without initializer",
name
))
})?;
let val = eval_expr(init, env)?;
Comment on lines +50 to 56
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.
env.declare(name.clone(), val);
Ok(None)
Expand Down
8 changes: 8 additions & 0 deletions src/interpreter/value.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
//! needs `Value`, and `Value` needs to reference the callable type.

use std::fmt;
use std::collections::HashMap;

use crate::ir::ast::CheckedFunDecl;

Expand All @@ -72,13 +73,19 @@ pub type NativeFn = fn(Vec<Value>) -> Result<Value, RuntimeError>;
pub enum FnValue {
UserDefined(CheckedFunDecl),
Native(NativeFn),

Closure {
decl: CheckedFunDecl,
captured: HashMap<String, Value>,
},
}

impl PartialEq for FnValue {
fn eq(&self, other: &Self) -> bool {
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.
_ => false,
}
}
Expand All @@ -89,6 +96,7 @@ impl fmt::Debug for FnValue {
match self {
FnValue::UserDefined(decl) => write!(f, "UserDefined({})", decl.name),
FnValue::Native(_) => write!(f, "Native(<fn ptr>)"),
FnValue::Closure { decl, .. } => write!(f, "Closure({})", decl.name),
}
}
}
Expand Down
22 changes: 21 additions & 1 deletion src/ir/ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,25 @@ pub enum Expr<Ty> {
name: String,
args: Vec<ExprD<Ty>>,
},

/// 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>>,
Comment on lines +107 to +122
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.
},

/// Array literal: [ expr, expr, ... ]
ArrayLit(Vec<ExprD<Ty>>),
/// Index expression: `base[index]`
Expand All @@ -126,7 +145,8 @@ pub enum Statement<Ty> {
Decl {
name: String,
ty: Type,
init: Box<ExprD<Ty>>,
/// Adicionado para suportar algo como 'fn(int) -> int f;'
init: Option<Box<ExprD<Ty>>>,
},
Assign {
target: Box<ExprD<Ty>>,
Expand Down
61 changes: 59 additions & 2 deletions src/parser/expressions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,11 @@
//! recursing on the right-hand side, which would accidentally produce
//! right-associative trees.

use crate::ir::ast::{Expr, ExprD, UncheckedExpr};
use crate::ir::ast::{Expr, ExprD, Param, UncheckedExpr};
use crate::parser::identifiers::identifier;
use crate::parser::functions::type_name;
use crate::parser::literals::literal;
use crate::parser::statements::block_statement;
use nom::{
branch::alt,
bytes::complete::tag,
Expand Down Expand Up @@ -65,11 +67,51 @@ pub fn parse_call(input: &str) -> IResult<&str, (String, Vec<UncheckedExpr>)> {
Ok((rest, (name.to_string(), args)))
}

/// Parser de Lambda
fn lambda_expr(input: &str) -> IResult<&str, UncheckedExpr> {
// fn ( Type name, ... ) -> Type { ... }
let (rest, _) = preceded(multispace0, tag("fn"))(input)?;
let (rest, params) = delimited(
preceded(multispace0, tag("(")),
separated_list0(
preceded(multispace0, tag(",")),
map(
tuple((
preceded(multispace0, type_name),
preceded(nom::character::complete::multispace1, identifier),
)),
|(ty, name)| -> Param { (name.to_string(), ty) },
),
),
preceded(multispace0, tag(")")),
)(rest)?;
let (rest, _) = preceded(multispace0, tag("->"))(rest)?;
let (rest, return_tipo) = preceded(multispace0, type_name)(rest)?;
let (rest, crp) = preceded(multispace0, block_statement)(rest)?;
Ok((
rest,
wrap(Expr::Lambda {
params,
return_tipo,
crp: Box::new(crp),
}),
))
}

/// Parser de lista de args
fn arg_list(input: &str) -> IResult<&str, Vec<UncheckedExpr>> {
delimited(
preceded(multispace0, tag("(")),
separated_list0(preceded(multispace0, tag(",")), preceded(multispace0, expression)),
preceded(multispace0, tag(")")),
)(input)
}

/// Atom: literal, call, array literal, identifier, or parenthesized expression.
fn atom(input: &str) -> IResult<&str, UncheckedExpr> {
alt((
lambda_expr,
map(literal, |l| wrap(Expr::Literal(l.into()))),
map(parse_call, |(name, args)| wrap(Expr::Call { name, args })),
map(
delimited(
preceded(multispace0, char('[')),
Expand All @@ -94,6 +136,21 @@ fn atom(input: &str) -> IResult<&str, UncheckedExpr> {
fn primary(input: &str) -> IResult<&str, UncheckedExpr> {
let (mut rest, mut acc) = atom(input)?;
loop {
if let Ok((r, args)) = arg_list(rest) {
acc = if let Expr::Ident(name) = &acc.exp {
wrap(Expr::Call {
name: name.clone(),
args,
})
} else {
wrap(Expr::CallExpr {
chmd: Box::new(acc),
args,
})
};
rest = r;
continue;
}
let index_parse = delimited(
preceded(multispace0, char('[')),
preceded(multispace0, expression),
Expand Down
34 changes: 33 additions & 1 deletion src/parser/functions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ pub fn type_name(input: &str) -> IResult<&str, Type> {
preceded(
multispace0,
alt((
fun_type,
// 2D arrays must be tried before 1D (longer prefix first)
map(tag("int[][]"), |_| Type::Array(Box::new(Type::Array(Box::new(Type::Int))))),
map(tag("float[][]"), |_| Type::Array(Box::new(Type::Array(Box::new(Type::Float))))),
Expand All @@ -57,6 +58,36 @@ pub fn type_name(input: &str) -> IResult<&str, Type> {
)(input)
}

/// Parse a function type: `fn(T1, T2, ...) -> Ret`.
///
/// This parser must reject lambdas like:
/// `fn(int x) -> int { return x; }`
fn fun_type(input: &str) -> IResult<&str, Type> {
let (rest, _) = preceded(multispace0, tag("fn"))(input)?;

let (rest, params) = delimited(
preceded(multispace0, tag("(")),
separated_list0(
preceded(multispace0, tag(",")),
preceded(multispace0, type_name),
),
preceded(multispace0, tag(")")),
)(rest)?;

let (rest, _) = preceded(multispace0, tag("->"))(rest)?;
let (rest, ret) = preceded(multispace0, type_name)(rest)?;

// 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,
)));
}
Comment on lines +80 to +86
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.

Ok((rest, Type::Fun(params, Box::new(ret))))
}

/// Parse a typed parameter (C-style): `Type name`.
fn param(input: &str) -> IResult<&str, (String, Type)> {
map(
Expand All @@ -79,6 +110,7 @@ pub fn fun_decl(input: &str) -> IResult<&str, UncheckedFunDecl> {
preceded(multispace0, tag(")")),
)(rest)?;
let (rest, body) = preceded(multispace0, statement)(rest)?;

Ok((
rest,
FunDecl {
Expand All @@ -88,4 +120,4 @@ pub fn fun_decl(input: &str) -> IResult<&str, UncheckedFunDecl> {
body: Box::new(body),
},
))
}
}
2 changes: 1 addition & 1 deletion src/parser/identifiers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ use nom::{
};

/// Reserved words: boolean literals and type names.
const RESERVED: &[&str] = &["true", "false", "int", "float", "bool", "str", "void", "return"];
const RESERVED: &[&str] = &["true", "false", "int", "float", "bool", "str", "void", "return", "fn"];

/// Parse an identifier (variable name).
/// Must start with letter or underscore; subsequent chars may be letter, digit, or underscore.
Expand Down
20 changes: 13 additions & 7 deletions src/parser/statements.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,29 +82,35 @@ fn return_statement(input: &str) -> IResult<&str, UncheckedStmt> {
Ok((rest, wrap(Statement::Return(expr.map(Box::new)))))
}

/// Parse a variable declaration: `Type ident = expr ;`. Must come before assignment.
/// Parse a variable declaration: `Type ident [= expr] ;`.
/// Must come before assignment.
fn decl_statement(input: &str) -> IResult<&str, UncheckedStmt> {
map(
tuple((
type_name,
preceded(nom::character::complete::multispace1, identifier),
preceded(multispace0, nom::bytes::complete::tag("=")),
preceded(multispace0, expression),
opt(preceded(
multispace0,
preceded(
nom::bytes::complete::tag("="),
preceded(multispace0, expression),
),
)),
preceded(multispace0, char(';')),
)),
|(ty, name, _, init, _)| {
|(ty, name, init, _)| {
wrap(Statement::Decl {
name: name.to_string(),
ty,
init: Box::new(init),
init: init.map(Box::new),
})
},
)(input)
}

/// Parse a block statement: `{ stmt* }`.
/// Each statement inside the block carries its own terminator (`;` or `}`).
fn block_statement(input: &str) -> IResult<&str, UncheckedStmt> {
pub(crate) fn block_statement(input: &str) -> IResult<&str, UncheckedStmt> {
map(
delimited(
preceded(multispace0, char('{')),
Expand Down Expand Up @@ -206,4 +212,4 @@ pub fn assignment(input: &str) -> IResult<&str, UncheckedStmt> {
})
},
)(input)
}
}
Loading
Loading