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
5 changes: 5 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,8 @@ nom = "7"

[dev-dependencies]
nom = "7"
proptest = "1.10.0"

[[test]]
name = "property"
path = "tests/property/mod.rs"
1,080 changes: 1,080 additions & 0 deletions nom.tags

Large diffs are not rendered by default.

1,659 changes: 1,659 additions & 0 deletions proptest.tags

Large diffs are not rendered by default.

44 changes: 44 additions & 0 deletions setupctags.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
#!/bin/bash
## Setup ctags for nom, rust-std crates and project
set -e # Exit on error

## check existence of stdlib
if [ ! -d rust ]; then
echo "Cloning Rust source..."
git clone --depth 1 https://github.com/rust-lang/rust.git
fi

## create stdlib tags
if [ -d rust ]; then
echo "Generating stdlib tags..."
(cd rust && ctags -R --languages=Rust -f ~/rust-stdlib.tags library/)
echo "✅ Created ~/rust-stdlib.tags"
fi

## create project tags
echo "Generating project tags..."
ctags -R --languages=Rust --exclude=target -f tags .
echo "✅ Created ./tags"

## find and tag nom (if exists)
NOM_PATH=$(find ~/.cargo/registry/src -type d -name "nom-*" 2>/dev/null | head -1)
if [ -n "$NOM_PATH" ]; then
echo "Generating nom tags..."
ctags -R --languages=Rust -f nom.tags "$NOM_PATH/src"
echo "✅ Created nom.tags"
else
echo "⚠️ nom not found in cargo cache (build your project first?)"
fi

## find and tag proptest (if exists)
PROPTEST_PATH=$(find ~/.cargo/registry/src/index.crates.io-* -type d -name "proptest-*" 2>/dev/null | head -1)
if [ -n "$PROPTEST_PATH" ]; then
echo "Generating proptest tags..."
ctags -R --languages=Rust -f proptest.tags "$PROPTEST_PATH/src"
echo "✅ Created proptest.tags"
else
echo "⚠️ proptest not found in cargo cache (build your project first?)"
fi

echo ""
echo "🎉 Done! Add to Vim: :set tags=./tags,~/rust-stdlib.tags"
7 changes: 7 additions & 0 deletions src/interpreter/exec_stmt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,13 @@ pub fn exec_stmt(stmt: &CheckedStmt, env: &mut Environment<Value>) -> ExecResult
Ok(None)
}

// --- Constant declaration ---
Statement::ConstDecl { name, init, .. } => {
let val = eval_expr(init, env)?;
env.declare(name.clone(), val);
Ok(None)
}

// --- Assignment ---
Statement::Assign { target, value } => {
let val = eval_expr(value, env)?;
Expand Down
6 changes: 6 additions & 0 deletions src/ir/ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,12 @@ pub enum Statement<Ty> {
ty: Type,
init: Box<ExprD<Ty>>,
},
/// Constant declaration with initialization: `const int x = expr`.
ConstDecl {
name: String,
ty: Type,
init: Box<ExprD<Ty>>,
},
Assign {
target: Box<ExprD<Ty>>,
value: Box<ExprD<Ty>>,
Expand Down
2 changes: 1 addition & 1 deletion src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,4 +66,4 @@ pub use functions::fun_decl;
pub use identifiers::identifier;
pub use literals::{literal, Literal};
pub use program::program;
pub use statements::{assignment, statement};
pub use statements::{assignment, const_statement, statement};
5 changes: 5 additions & 0 deletions src/parser/program.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@
//! function declaration causes the parse to stop. The type checker then
//! verifies that a `main` function exists.
//!
//! Local `const T name = expr ;` is recognised inside function bodies via
//! [`crate::parser::statements::const_statement`] (wired from [`crate::parser::statements::statement`]).
//! Extending this module with `alt((const_statement, fun_decl))` plus a rich enough
//! [`crate::ir::ast::Program`] is the usual next step for top-level constants.
//!
//! # Design Decisions
//!
//! ## `many0` as the top-level combinator
Expand Down
88 changes: 84 additions & 4 deletions src/parser/statements.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@
//! Exposes two public functions:
//!
//! * [`statement`] — the top-level entry point; tries each statement form in
//! order: `return`, `if`, `while`, call-statement, block, declaration,
//! assignment.
//! order: `return`, `if`, `while`, call-statement, block, `const` declaration,
//! declaration, assignment.
//! * [`const_statement`] — `const T name = expr ;`, producing
//! [`Statement::ConstDecl`].
//! * [`assignment`] — parses `lvalue = expression ;`; exported separately
//! because the test suite uses it directly.
//!
Expand Down Expand Up @@ -47,7 +49,7 @@ use crate::parser::identifiers::identifier;
use nom::{
branch::alt,
bytes::complete::tag,
character::complete::{char, multispace0},
character::complete::{char, multispace0, multispace1},
combinator::{map, opt},
multi::many0,
sequence::{delimited, preceded, tuple},
Expand All @@ -58,7 +60,7 @@ fn wrap(s: Statement<()>) -> UncheckedStmt {
StatementD { stmt: s, ty: () }
}

/// Parse any statement: block | if | while | return | decl | call | assignment.
/// Parse any statement: block | if | while | return | const | decl | call | assignment.
pub fn statement(input: &str) -> IResult<&str, UncheckedStmt> {
preceded(
multispace0,
Expand All @@ -67,6 +69,7 @@ pub fn statement(input: &str) -> IResult<&str, UncheckedStmt> {
if_statement,
while_statement,
return_statement,
const_statement,
decl_statement,
call_statement,
assignment,
Expand All @@ -82,6 +85,30 @@ fn return_statement(input: &str) -> IResult<&str, UncheckedStmt> {
Ok((rest, wrap(Statement::Return(expr.map(Box::new)))))
}

/// Parse `const T name = expr ;`.
///
/// Produces [`Statement::ConstDecl`] so later phases can preserve immutability
/// information.
pub fn const_statement(input: &str) -> IResult<&str, UncheckedStmt> {
map(
tuple((
preceded(multispace0, tag("const")),
preceded(multispace1, type_name),
preceded(multispace1, identifier),
preceded(multispace0, tag("=")),
preceded(multispace0, expression),
preceded(multispace0, char(';')),
)),
|(_, ty, name, _, init, _)| {
wrap(Statement::ConstDecl {
name: name.to_string(),
ty,
init: Box::new(init),
})
},
)(input)
}

/// Parse a variable declaration: `Type ident = expr ;`. Must come before assignment.
fn decl_statement(input: &str) -> IResult<&str, UncheckedStmt> {
map(
Expand Down Expand Up @@ -207,3 +234,56 @@ pub fn assignment(input: &str) -> IResult<&str, UncheckedStmt> {
},
)(input)
}

#[cfg(test)]
mod tests {
use super::statement;
use crate::ir::ast::{Expr, Literal, Statement, Type};
use nom::combinator::all_consuming;

#[test]
fn parses_const_declaration_as_const_decl() {
let (_, stmt) = all_consuming(statement)("const int MAX_SIZE = 100;").expect("should parse");

match stmt.stmt {
Statement::ConstDecl { name, ty, init } => {
assert_eq!(name, "MAX_SIZE");
assert_eq!(ty, Type::Int);
assert_eq!(init.exp, Expr::Literal(Literal::Int(100)));
}
other => panic!("expected ConstDecl, got {:?}", other),
}
}

#[test]
fn parses_const_expression_initializer() {
let (_, stmt) = all_consuming(statement)("const int DOUBLE = 2 * 21;").expect("should parse");

match stmt.stmt {
Statement::ConstDecl { name, ty, .. } => {
assert_eq!(name, "DOUBLE");
assert_eq!(ty, Type::Int);
}
other => panic!("expected ConstDecl, got {:?}", other),
}
}

#[test]
fn keeps_mutable_declaration_as_decl() {
let (_, stmt) = all_consuming(statement)("int x = 5;").expect("should parse");

match stmt.stmt {
Statement::Decl { .. } => {}
Statement::ConstDecl { .. } => panic!("plain Decl must not produce ConstDecl"),
other => panic!("unexpected statement: {:?}", other),
}
}

#[test]
fn rejects_const_without_initializer() {
assert!(
all_consuming(statement)("const int X;").is_err(),
"const without initializer should fail to parse"
);
}
}
21 changes: 21 additions & 0 deletions src/semantic/type_checker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,27 @@ fn type_check_stmt(
init: Box::new(init_checked),
}
}
Statement::ConstDecl { name, ty, init } => {
if ty == &Type::Unit {
return Err(TypeError::new("cannot declare variable of type void"));
}
if env.get(name).is_some() {
return Err(TypeError::new(format!("redeclaration of variable: {}", name)));
}
let init_checked = type_check_expr_to_typed(init, env)?;
if !types_compatible(&init_checked.ty, ty) {
return Err(TypeError::new(format!(
"declaration of {}: expected {:?}, got {:?}",
name, ty, init_checked.ty
)));
}
env.declare(name.clone(), ty.clone());
Statement::ConstDecl {
name: name.clone(),
ty: ty.clone(),
init: Box::new(init_checked),
}
}
Statement::Assign { target, value } => {
let value_checked = type_check_expr_to_typed(value, env)?;
type_check_assign_target(&target.exp, &value_checked.ty, env)?;
Expand Down
Loading