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 .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"githubPullRequests.ignoredPullRequestBranches": [
"main"
]
}
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
10 changes: 10 additions & 0 deletions src/interpreter/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,21 @@ use crate::stdlib::NativeRegistry;

use eval_expr::eval_call;
use value::{FnValue, RuntimeError, Value};
use crate::ir::ast::Statement;
use crate::interpreter::eval_expr::eval_expr;

/// Interpret a type-checked MiniC program, starting execution at `main`.
pub fn interpret(program: &CheckedProgram) -> Result<(), RuntimeError> {
let mut env = Environment::<Value>::new();

for constant in &program.constants {
if let Statement::ConstDecl { name, init, .. } = &constant.stmt {
// Evaluate the initializer expression to a value
let value = eval_expr(init, &mut env)?;
env.declare(name.clone(), value);
}
}

// Register native stdlib functions as Value::Fn(FnValue::Native) bindings.
let registry = NativeRegistry::default();
for (name, entry) in registry.iter() {
Expand Down
7 changes: 7 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 Expand Up @@ -169,6 +175,7 @@ pub struct FunDecl<Ty> {
#[derive(Debug, Clone, PartialEq)]
pub struct Program<Ty> {
pub functions: Vec<FunDecl<Ty>>,
pub constants: Vec<StatementD<Ty>>,
}

// Type synonyms for checked and unchecked phases.
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};
106 changes: 101 additions & 5 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 All @@ -26,12 +31,103 @@
//! `main` is a semantic constraint checked in the next pipeline stage, not
//! a syntactic one enforced here.

use crate::ir::ast::{Program, UncheckedProgram};
// use crate::ir::ast::{Program, UncheckedProgram};
// use crate::parser::functions::fun_decl;
// use nom::{combinator::map, multi::many0, IResult};
//
// /// Parse a complete MiniC program: zero or more function declarations.
// /// Execution starts at the `main` function (validated by the type checker).
// pub fn program(input: &str) -> IResult<&str, UncheckedProgram> {
// map(many0(fun_decl), |functions| Program {
// functions,
// constants: vec![]
// })(input)
// }
// use crate::ir::ast::{Program, UncheckedProgram, FunDecl, StatementD};
// use crate::parser::functions::fun_decl;
// // Import the constant statement parser mentioned in your docs
// use crate::parser::statements::const_statement;
//
// use nom::{
// branch::alt,
// combinator::map,
// multi::many0,
// IResult,
// };
//
// /// An internal enum used to differentiate top-level elements during parsing.
// enum GlobalItem {
// Function(FunDecl<()>),
// Constant(StatementD<()>),
// }
//
// /// Parse a complete MiniC program: zero or more global constant declarations
// /// and function declarations in any order.
// pub fn program(input: &str) -> IResult<&str, UncheckedProgram> {
// // 1. Repeatedly parse either a function declaration OR a global constant
// map(
// many0(alt((
// map(fun_decl, GlobalItem::Function),
// map(const_statement, GlobalItem::Constant),
// ))),
// |items| {
// let mut functions = Vec::new();
// let mut constants = Vec::new();
//
// // 2. Separate them into their respective vectors for the Program AST
// for item in items {
// match item {
// GlobalItem::Function(f) => functions.push(f),
// GlobalItem::Constant(c) => constants.push(c),
// }
// }
//
// Program { functions, constants }
// },
// )(input)
// }
use crate::ir::ast::{Program, UncheckedProgram, FunDecl, StatementD};
use crate::parser::functions::fun_decl;
use nom::{combinator::map, multi::many0, IResult};
use crate::parser::statements::const_statement;

use nom::{
branch::alt,
combinator::map,
multi::many0,
character::complete::multispace0,
sequence::delimited,
IResult,
};

enum GlobalItem {
Function(FunDecl<()>),
Constant(StatementD<()>),
}

/// Parse a complete MiniC program: zero or more function declarations.
/// Execution starts at the `main` function (validated by the type checker).
pub fn program(input: &str) -> IResult<&str, UncheckedProgram> {
map(many0(fun_decl), |functions| Program { functions })(input)
// Wrap the top level elements so that trailing or leading whitespace/newlines
// do not prematurely break `many0`
map(
many0(delimited(
multispace0,
alt((
map(fun_decl, GlobalItem::Function),
map(const_statement, GlobalItem::Constant),
)),
multispace0,
)),
|items| {
let mut functions = Vec::new();
let mut constants = Vec::new();

for item in items {
match item {
GlobalItem::Function(f) => functions.push(f),
GlobalItem::Constant(c) => constants.push(c),
}
}

Program { functions, constants }
},
)(input)
}
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"
);
}
}
Loading