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
52 changes: 50 additions & 2 deletions docs/LOOPS.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,54 @@ jasi (foo small pass 101) start
end
```

## Limitations
## Loop Control Flow

The current loop construct `jasi` lacks support for common loop control flows such as `break` and `continue`, as well as higher-level loop constructs common in most programming languages, like `for` or `foreach`. These features are planned for a future release to make looping more expressive and concise.
### Break Statement (`comot`)

Exit the innermost loop immediately:

```naijascript
make i get 0
jasi (i small pass 10) start
if to say (i na 5) start
comot # Exit loop when i reaches 5
end
shout(i)
i get i add 1
end
# Prints: 0, 1, 2, 3, 4
```

### Continue Statement (`next`)

Skip to the next iteration of the innermost loop:

```naijascript
make i get 0
jasi (i small pass 5) start
i get i add 1
if to say (i mod 2 na 0) start
next # Skip even numbers
end
shout(i)
end
# Prints: 1, 3, 5
```

### Nested Loops

`comot` and `next` affect only the innermost loop:

```naijascript
make outer get 0
jasi (outer small pass 3) start
make inner get 0
jasi (inner small pass 3) start
if to say (inner na 1) start
comot # Exits inner loop only
end
inner get inner add 1
end
outer get outer add 1
end
```
5 changes: 5 additions & 0 deletions docs/grammar.ebnf
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ statement = assignment
| expression_statement
| if_statement
| loop_statement
| break_statement
| continue_statement
| block
| function_def
| return_statement
Expand All @@ -30,6 +32,9 @@ else_clause = "if not so" block
loop_statement = "jasi" "(" expression ")" block ;
break_statement = "comot" ;
continue_statement = "next" ;
block = "start" statement_list "end" ;
comment = "#" { comment_char } ;
Expand Down
32 changes: 32 additions & 0 deletions src/resolver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,9 @@ pub struct Resolver<'ast> {
// Track current function context for return statement validation
current_function: Option<&'ast str>,

// Track loop nesting depth (0 = not in loop)
in_loop: usize,

/// Collection of semantic errors found during analysis
pub errors: Diagnostics<'ast>,

Expand All @@ -111,6 +114,7 @@ impl<'ast> Resolver<'ast> {
variable_scopes: Vec::new_in(arena),
function_scopes: Vec::new_in(arena),
current_function: None,
in_loop: 0,
errors: Diagnostics::new(arena),
arena,
}
Expand Down Expand Up @@ -154,6 +158,8 @@ impl<'ast> Resolver<'ast> {
Stmt::Block { span, .. } => span,
Stmt::FunctionDef { span, .. } => span,
Stmt::Return { span, .. } => span,
Stmt::Break { span } => span,
Stmt::Continue { span } => span,
Stmt::Expression { span, .. } => span,
};

Expand Down Expand Up @@ -238,7 +244,9 @@ impl<'ast> Resolver<'ast> {
Stmt::Loop { cond, body, .. } => {
self.check_expr(cond);
self.check_boolean_expr(cond);
self.in_loop += 1;
self.check_block(body);
self.in_loop -= 1;
}
// Handle nested blocks
Stmt::Block { block, .. } => {
Expand All @@ -263,6 +271,30 @@ impl<'ast> Resolver<'ast> {
Stmt::Return { expr, span } => {
self.check_return_stmt(*expr, span);
}
Stmt::Break { span } => {
if self.in_loop == 0 {
self.emit_error(
*span,
SemanticError::UnreachableCode,
vec![Label {
span: *span,
message: ArenaCow::Borrowed("`comot` statement outside loop body"),
}],
);
}
}
Stmt::Continue { span } => {
if self.in_loop == 0 {
self.emit_error(
*span,
SemanticError::UnreachableCode,
vec![Label {
span: *span,
message: ArenaCow::Borrowed("`next` statement outside loop body"),
}],
);
}
}
Stmt::Expression { expr, .. } => {
self.check_expr(expr);
}
Expand Down
15 changes: 13 additions & 2 deletions src/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,8 +106,10 @@ struct ActivationRecord<'arena, 'src> {
// Controls how function execution should proceed after statements
#[derive(Debug, Clone, PartialEq)]
enum ExecFlow<'arena, 'src> {
Return(Value<'arena, 'src>),
Continue,
Return(Value<'arena, 'src>),
Break,
LoopContinue,
}

/// The runtime interface for NaijaScript using arena-allocated AST.
Expand Down Expand Up @@ -229,6 +231,8 @@ impl<'arena, 'src> Runtime<'arena, 'src> {
}
match self.exec_block_with_flow(body)? {
ExecFlow::Continue => continue,
ExecFlow::Break => break,
ExecFlow::LoopContinue => continue,
flow @ ExecFlow::Return(..) => return Ok(flow),
}
}
Expand All @@ -245,6 +249,8 @@ impl<'arena, 'src> Runtime<'arena, 'src> {
if let Some(expr_ref) = expr { self.eval_expr(expr_ref)? } else { Value::Null };
Ok(ExecFlow::Return(val))
}
Stmt::Break { .. } => Ok(ExecFlow::Break),
Stmt::Continue { .. } => Ok(ExecFlow::LoopContinue),
Stmt::Expression { expr, .. } => {
self.eval_expr(expr)?;
Ok(ExecFlow::Continue)
Expand All @@ -261,7 +267,7 @@ impl<'arena, 'src> Runtime<'arena, 'src> {
for stmt in block.stmts {
match self.exec_stmt(stmt)? {
ExecFlow::Continue => continue,
flow @ ExecFlow::Return(..) => {
flow @ (ExecFlow::Return(..) | ExecFlow::Break | ExecFlow::LoopContinue) => {
self.env.pop(); // exit block scope before returning
return Ok(flow);
}
Expand Down Expand Up @@ -496,6 +502,11 @@ impl<'arena, 'src> Runtime<'arena, 'src> {
let val = match self.exec_block_with_flow(func_def.body)? {
ExecFlow::Continue => Value::Null,
ExecFlow::Return(val) => val,
ExecFlow::Break | ExecFlow::LoopContinue => {
unreachable!(
"Break/Continue should be caught by loop, not escape to function boundary"
)
}
};

// We remove the activation record from the call stack
Expand Down
21 changes: 21 additions & 0 deletions src/syntax/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,13 @@ pub enum Stmt<'ast> {
expr: Option<ExprRef<'ast>>,
span: Span,
},
// Loop control flow statements
Break {
span: Span,
},
Continue {
span: Span,
},
// Expression statement (e.g., function calls)
Expression {
expr: ExprRef<'ast>,
Expand Down Expand Up @@ -266,6 +273,8 @@ impl<'src: 'ast, 'ast, I: Iterator<Item = SpannedToken<'ast, 'src>>> Parser<'src
| Token::Return
| Token::Identifier(..)
| Token::Start
| Token::Comot
| Token::Next
) {
match self.parse_statement() {
Some(stmt) => stmts.push(stmt),
Expand Down Expand Up @@ -309,6 +318,8 @@ impl<'src: 'ast, 'ast, I: Iterator<Item = SpannedToken<'ast, 'src>>> Parser<'src
| Token::Jasi
| Token::Do
| Token::Return
| Token::Comot
| Token::Next
) {
self.bump();
}
Expand All @@ -323,6 +334,16 @@ impl<'src: 'ast, 'ast, I: Iterator<Item = SpannedToken<'ast, 'src>>> Parser<'src
Token::Make => self.parse_assignment(start),
Token::IfToSay => self.parse_if(start),
Token::Jasi => self.parse_loop(start),
Token::Comot => {
self.bump(); // consume `comot`
let end = self.cur.span.end;
Some(self.alloc(Stmt::Break { span: Range::from(start..end) }))
}
Token::Next => {
self.bump(); // consume `next`
let end = self.cur.span.end;
Some(self.alloc(Stmt::Continue { span: Range::from(start..end) }))
}
// Parse a standalone or nested block as a statement
Token::Start => {
self.bump(); // consume `start` block
Expand Down
2 changes: 2 additions & 0 deletions src/syntax/scanner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -518,6 +518,8 @@ impl<'arena, 'input> Lexer<'arena, 'input> {
"jasi" => Token::Jasi,
"start" => Token::Start,
"end" => Token::End,
"comot" => Token::Comot,
"next" => Token::Next,
"na" => Token::Na,
"pass" => Token::Pass,
"true" => Token::True,
Expand Down
14 changes: 10 additions & 4 deletions src/syntax/token.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,16 @@ pub enum Token<'arena, 'input> {
Not, // "not" - logical not

// Control flow
Jasi, // "jasi" - while loop construct (Nigerian slang for "keep going")
Jasi, // "jasi" - while loop construct
Start, // "start" - block beginning
End, // "end" - block ending
Comot, // "comot" - break
Next, // "next" - continue

// Comparison operators
Na, // "na" - equality (Nigerian slang for "is")
Pass, // "pass" - greater than (Nigerian slang for "exceeds")
SmallPass, // "small pass" - less than (Nigerian slang for "smaller than")
Na, // "na" - equality
Pass, // "pass" - greater than
SmallPass, // "small pass" - less than

// Conditional constructs
IfToSay, // "if to say" - if statement
Expand Down Expand Up @@ -86,6 +88,8 @@ impl<'arena, 'input> std::fmt::Display for Token<'arena, 'input> {
Token::Jasi => write!(f, "jasi"),
Token::Start => write!(f, "start"),
Token::End => write!(f, "end"),
Token::Comot => write!(f, "comot"),
Token::Next => write!(f, "next"),
Token::Na => write!(f, "na"),
Token::Pass => write!(f, "pass"),
Token::SmallPass => write!(f, "small pass"),
Expand Down Expand Up @@ -120,6 +124,8 @@ impl<'arena, 'input> Token<'arena, 'input> {
| Token::Jasi
| Token::Start
| Token::End
| Token::Comot
| Token::Next
| Token::Na
| Token::Pass
| Token::SmallPass
Expand Down
10 changes: 10 additions & 0 deletions tests/resolver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,16 @@ fn test_string_number_comparison_in_loop() {
assert_resolve!(r#"jasi ("foo" pass 1) start end"#, SemanticError::TypeMismatch);
}

#[test]
fn test_break_outside_loop() {
assert_resolve!("comot", SemanticError::UnreachableCode);
}

#[test]
fn test_continue_outside_loop() {
assert_resolve!("next", SemanticError::UnreachableCode);
}

#[test]
fn test_string_modulus() {
assert_resolve!(r#"make x get "foo" mod "bar""#, SemanticError::TypeMismatch);
Expand Down
76 changes: 76 additions & 0 deletions tests/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,82 @@ fn loop_execution() {
assert_runtime!("make x get 1 jasi (x small pass 3) start shout(x) x get x add 1 end", output: vec![Value::Number(1.0), Value::Number(2.0)]);
}

#[test]
fn test_break_exits_loop() {
assert_runtime!(
r#"
make i get 0
jasi (i small pass 10) start
if to say (i na 5) start
comot
end
i get i add 1
end
shout(i)
"#,
output: vec![Value::Number(5.0)]
);
}

#[test]
fn test_continue_skips_iteration() {
assert_runtime!(
r#"
make sum get 0
make i get 0
jasi (i small pass 10) start
i get i add 1
if to say (i mod 2 na 0) start
next
end
sum get sum add i
end
shout(sum)
"#,
output: vec![Value::Number(25.0)] // 1+3+5+7+9 = 25
);
}

#[test]
fn test_break_in_nested_block() {
assert_runtime!(
r#"
make i get 0
jasi (i small pass 10) start
start
if to say (i na 3) start
comot
end
end
i get i add 1
end
shout(i)
"#,
output: vec![Value::Number(3.0)]
);
}

#[test]
fn test_continue_in_nested_block() {
assert_runtime!(
r#"
make count get 0
make i get 0
jasi (i small pass 5) start
i get i add 1
start
if to say (i na 2) start
next
end
end
count get count add 1
end
shout(count)
"#,
output: vec![Value::Number(4.0)]
);
}

#[test]
fn division_by_zero() {
assert_runtime!("shout(1 divide 0)", error: RuntimeErrorKind::DivisionByZero);
Expand Down
Loading