Skip to content
Merged
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
4 changes: 2 additions & 2 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@ Snapshot: 2026-07-28.
- `cargo fmt --all -- --check` passes and is enforced for pull requests.
- `cargo check --all-targets` passes.
- `cargo test` passes and is enforced for pull requests, including all 48 integration specifications and the library doctest.
- `cargo clippy --all-targets --all-features -- -D warnings` passes locally.
- The crate exposes checked file/source execution APIs and typed I/O, parse, and runtime error categories.
- Lexer, parser, interpreter, type-inference, examples, mdBook documentation, and cargo-dist release assets exist.

### Quality gaps

- Strict Clippy reports 16 errors across the lexer, parser, type inference, and symbol table.
- Panic recovery wraps rather than removes many panic, `unwrap`, and `expect` paths; the interpreter alone contains roughly 90.
- `parser.rs` and `interpreter.rs` are approximately 975 and 1,217 lines and mix several responsibilities.
- Checked and unchecked parsing/execution paths duplicate logic.
Expand Down Expand Up @@ -72,7 +72,7 @@ Snapshot: 2026-07-28.
- [x] Open and complete an issue that applies rustfmt, adds `cargo fmt --all -- --check` to CI, and changes no behavior.
- [x] Fix the library doctest against the v0.1 API, make `cargo test` green, and require it in CI.
- [x] Resolve strict Clippy findings in the interpreter without lint suppressions or behavior changes.
- [ ] Resolve the remaining strict Clippy findings in the lexer, parser, type inference, and symbol table.
- [x] Resolve the remaining strict Clippy findings in the lexer, parser, type inference, and symbol table.
- [ ] Require `cargo clippy --all-targets --all-features -- -D warnings` in pull-request CI.
- [ ] Separate generated mdBook output from sources and define one reproducible documentation build command.
- [ ] Reconcile README commands, branch names, CI claims, supported features, and examples with executable behavior.
Expand Down
4 changes: 2 additions & 2 deletions src/lexer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ impl Lexer {
'0'..='9' => {
let mut num_str: String = self.input[self.pos..]
.chars()
.take_while(|c| c.is_digit(10))
.take_while(|c| c.is_ascii_digit())
.collect();
self.pos += num_str.len();

Expand All @@ -157,7 +157,7 @@ impl Lexer {
// It's a float
let num_str2: String = self.input[self.pos..]
.chars()
.take_while(|c| c.is_digit(10))
.take_while(|c| c.is_ascii_digit())
.collect();
self.pos += num_str2.len();
num_str.push('.');
Expand Down
58 changes: 23 additions & 35 deletions src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ impl Parser {
}
}
Token::Char(value) => {
let expr = Expr::Char(value.clone());
let expr = Expr::Char(*value);
self.advance();
Ok(expr)
}
Expand Down Expand Up @@ -225,7 +225,7 @@ impl Parser {
fn parse_struct_compound_checked(&mut self, struct_name: String) -> Result<Expr, LangError> {
self.eat_checked(Token::LeftCurly)?;
let mut props_exprs = HashMap::new();
while &self.current_token != &Token::RightCurly {
while self.current_token != Token::RightCurly {
if let Token::Identifier(key) = self.current_token.clone() {
self.advance();
self.eat_checked(Token::Colon)?;
Expand Down Expand Up @@ -505,10 +505,7 @@ impl Parser {
while self.current_token != Token::RightCurly {
body.push(self.parse_statement_checked()?);
}
let is_returning = &body.iter().any(|s| match s {
Stmt::Return(_) => true,
_ => false,
});
let is_returning = body.iter().any(|s| matches!(s, Stmt::Return(_)));
if !is_returning {
body.push(Stmt::Return(None))
}
Expand Down Expand Up @@ -553,13 +550,11 @@ impl Parser {
)));
}
}
Stmt::Return(None) => {
if *expected_type != Type::Void {
return Err(LangError::parse(format!(
"Expected return type {:?}, but function returned nothing",
expected_type
)));
}
Stmt::Return(None) if *expected_type != Type::Void => {
return Err(LangError::parse(format!(
"Expected return type {:?}, but function returned nothing",
expected_type
)));
}
_ => {}
}
Expand Down Expand Up @@ -700,8 +695,7 @@ impl Parser {

let step = if self.current_token == Token::Step {
self.advance();
let step_size = self.parse_expr_checked()?;
step_size
self.parse_expr_checked()?
} else {
Expr::Int(1)
};
Expand Down Expand Up @@ -755,7 +749,7 @@ impl Parser {
} else if let Token::Identifier(_) = &self.current_token {
let mut final_module_part = String::new();
while let Token::Identifier(sub_part) = &self.current_token {
final_module_part.push_str(&sub_part);
final_module_part.push_str(sub_part);
self.advance();
if let Token::DoubleColon = &self.current_token {
final_module_part.push_str("::");
Expand Down Expand Up @@ -854,7 +848,7 @@ impl Parser {
};
self.eat_checked(Token::LeftCurly)?;
let mut impl_methods = Vec::new();
while &self.current_token != &Token::RightCurly {
while self.current_token != Token::RightCurly {
let impl_method = self.parse_function_declaration_checked()?;
impl_methods.push(impl_method);
}
Expand Down Expand Up @@ -897,6 +891,8 @@ impl Parser {

// Parse any statement
fn parse_statement(&mut self) -> Stmt {
let identifier_is_reassignment = matches!(self.current_token, Token::Identifier(_))
&& matches!(self.peek_token(), Token::Assignment | Token::LeftBracket);
match &self.current_token {
Token::Use => self.parse_use(),
Token::Struct => self.parse_struct(),
Expand All @@ -906,16 +902,11 @@ impl Parser {
Token::For => self.parse_for_in_range(),
Token::Return => self.parse_return(),
Token::Poo | Token::Mut => self.parse_assignment(),
Token::Identifier(_) if identifier_is_reassignment => self.parse_reassignment(),
Token::Identifier(_) => {
if self.peek_token() == Token::Assignment || self.peek_token() == Token::LeftBracket
// vector index reassingment
{
self.parse_reassignment()
} else {
let expr = self.parse_expr();
self.eat(Token::SemiColon);
Stmt::Expression(expr)
}
let expr = self.parse_expr();
self.eat(Token::SemiColon);
Stmt::Expression(expr)
}
_ => {
let expr = self.parse_expr();
Expand All @@ -927,6 +918,8 @@ impl Parser {

// First checked parsing slice: expression statements use checked token consumption.
fn parse_statement_checked(&mut self) -> Result<Stmt, LangError> {
let identifier_is_reassignment = matches!(self.current_token, Token::Identifier(_))
&& matches!(self.peek_token(), Token::Assignment | Token::LeftBracket);
match &self.current_token {
Token::Use => self.parse_use_checked(),
Token::Struct => self.parse_struct_checked(),
Expand All @@ -936,16 +929,11 @@ impl Parser {
Token::For => self.parse_for_in_range_checked(),
Token::Return => self.parse_return_checked(),
Token::Poo | Token::Mut => self.parse_assignment_checked(),
Token::Identifier(_) if identifier_is_reassignment => self.parse_reassignment_checked(),
Token::Identifier(_) => {
if self.peek_token() == Token::Assignment || self.peek_token() == Token::LeftBracket
// vector index reassignment
{
self.parse_reassignment_checked()
} else {
let expr = self.parse_expr_checked()?;
self.eat_checked(Token::SemiColon)?;
Ok(Stmt::Expression(expr))
}
let expr = self.parse_expr_checked()?;
self.eat_checked(Token::SemiColon)?;
Ok(Stmt::Expression(expr))
}
_ => {
let expr = self.parse_expr_checked()?;
Expand Down
8 changes: 4 additions & 4 deletions src/type_inference.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ pub fn infer_expr_type(expr: &Expr, symbol_table: &ScopedSymbolTable) -> Type {
Expr::String(_) => Type::String,
Expr::Boolean(_) => Type::Bool,
Expr::Vector(v, _) => {
if let Some(a) = v.get(0) {
if let Some(a) = v.first() {
infer_expr_type(a, symbol_table)
} else {
Type::Vector(Box::new(Type::Void))
Expand All @@ -31,7 +31,7 @@ pub fn infer_expr_type(expr: &Expr, symbol_table: &ScopedSymbolTable) -> Type {
Expr::Identifier(name) => symbol_table
.get(name)
.cloned()
.expect(&format!("Undefined variable: {}", name)),
.unwrap_or_else(|| panic!("Undefined variable: {}", name)),
Expr::UnaryOp(_, _) => Type::Bool,
Expr::BinaryOp(lhs, op, rhs) => {
let lhs_type = infer_expr_type(lhs, symbol_table);
Expand Down Expand Up @@ -114,7 +114,7 @@ pub fn infer_expr_type(expr: &Expr, symbol_table: &ScopedSymbolTable) -> Type {
let func_type = symbol_table
.get(name)
.cloned()
.expect(&format!("Undefined function: {}", name));
.unwrap_or_else(|| panic!("Undefined function: {}", name));

match func_type {
Type::Function(params, return_type) => {
Expand Down Expand Up @@ -164,7 +164,7 @@ pub fn infer_stmt_types(stmt: &Stmt, symbol_table: &mut ScopedSymbolTable) {
let expr_type = infer_expr_type(expr, symbol_table);
let var_type = symbol_table
.get(name)
.expect(&format!("Variable '{}' used before declaration", name));
.unwrap_or_else(|| panic!("Variable '{}' used before declaration", name));

if var_type != &expr_type {
panic!(
Expand Down
6 changes: 6 additions & 0 deletions src/visitor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ pub struct ScopedSymbolTable {
scopes: Vec<HashMap<String, Type>>,
}

impl Default for ScopedSymbolTable {
fn default() -> Self {
Self::new()
}
}

impl ScopedSymbolTable {
pub fn new() -> Self {
Self {
Expand Down
Loading