Skip to content

Commit fea0ec8

Browse files
author
Joel Abrahams
committed
fix: parsing while statement
1 parent d9bb321 commit fea0ec8

30 files changed

Lines changed: 113 additions & 46 deletions

‎CLAUDE.md‎

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,11 +55,10 @@ The pipeline for a single file is: `String` → **parse** → `AST` → **check*
5555
has its own README worth reading before making non-trivial changes there (`src/parse/README.md`,
5656
`src/check/README.md`, `src/generate/README.md`).
5757

58-
- **`src/parse`**: `lex/` tokenizes source into a `Vec<Token>`, handling indentation by emitting synthetic
59-
`Indent`/`Dedent` tokens that the parser uses to delimit blocks. The parser (`expression.rs`, `statement.rs`,
58+
- **`src/parse`**: `lex/` tokenizes source into a `Vec<Token>`. The parser (`expression.rs`, `statement.rs`,
6059
`class.rs`, `definition.rs`, `control_flow_*.rs`, `collection.rs`, `call.rs`, `operation.rs`, `ty.rs`, etc.)
6160
walks tokens via a `TokenIterator` and builds an `AST` (`parse/ast`), where each node carries a `Position` for
62-
error reporting. Errors here mean the token stream doesn't conform to the grammar.
61+
error reporting.
6362

6463
- **`src/check`**: the type checker, and where most language semantics live. Three phases:
6564
1. **Context building** (`check/context`): scans all ASTs (including implicit/explicit imports and built-in
@@ -102,6 +101,54 @@ has its own README worth reading before making non-trivial changes there (`src/p
102101
fixture path resolution, randomized temp output dirs, and the Python-AST-diff assertion logic used by
103102
`test_directory`/`test_directory_args`.
104103

104+
## Block syntax (post indent/dedent removal)
105+
106+
`for`/`while`/`with` bodies always require an explicit `do ... end` block — there is no single-statement
107+
shorthand for these three (`for a in b do c` is a parse error; it must be `for a in b do c end`). `if`/`then`/
108+
`else` branches are the exception: each branch is parsed as one `parse_expr_or_stmt`, which accepts either a
109+
bare single statement/expression or an explicit `do ... end` block (`if a then do ... end else c` is valid).
110+
A leading newline before a statement/expression is insignificant whitespace and is skipped (see
111+
`parse_expression`'s and `parse_expr_or_stmt`'s `eat_while(&Token::NL)`) — but a *trailing* newline is still
112+
usually required as a statement separator inside a block, so constructs that need to look past it for an
113+
optional following keyword (e.g. `parse_if` scanning past newlines for a possible `else`) must use a
114+
lookahead-with-rollback helper (`LexIterator::peek_if_skipping`) rather than unconditionally consuming the
115+
newline, or they'll break "no `else`, followed by more statements in the same block".
116+
117+
The call-site "handle" construct for a call that may raise is `<expr> ! where <case> ... end`, e.g.
118+
`f(10) ! where err: MyErr => do ... end end` — the `!` marks the call as fallible and must be consumed before
119+
looking for `where` (`parse_expr_or_stmt` in `expr_or_stmt.rs`).
120+
121+
`type X: Parent when <cond>` (single-line) / `type X: Parent when\n <cond>\n...\nend` (multi-line, terminated
122+
by `end`) is the *conditional type alias* form (produces `Node::TypeAlias`, binds `self` to `Parent` while
123+
checking the conditions). `type X where <defs> end` is a different form — an interface/type body of field and
124+
function *signatures* (produces `Node::TypeDef`, does **not** bind `self`). These two are easy to conflate
125+
(`when` vs `where`) since both start with `type X: Parent`; picking the wrong one either fails to parse or fails
126+
type-checking with a confusing "Undefined variable: self".
127+
128+
## Known incomplete work (branch `feat-remove-indent-dedent`, as of 2026-08-24)
129+
130+
This branch is mid-refactor from indentation-based blocks to the `do`/`end` scheme above, and several
131+
`tests/resource/valid/**` fixtures were rewritten ahead of the features they exercise:
132+
133+
- **`trait` is unimplemented.** It's a real, documented keyword (see the README's "traits" section and
134+
`docs/spec/trait-def` in `docs/spec/grammar.md`) but the lexer/parser has zero support for it today. A few
135+
fixtures (`tests/resource/valid/class/parent.mamba`, `multiple_parent.mamba`,
136+
`fun_with_body_in_interface.mamba`, `class_super_one_line_init.mamba`, and transitively `types.mamba` via a
137+
dropped parent class) were rewritten to use `trait` and no longer parse. Fixing these needs either
138+
implementing `trait` as a real parser+checker+codegen feature, or reverting them to the `class`/`type`-based
139+
syntax their paired `.py` reference files still expect.
140+
- **Class-body statements/field-initializers that depend on constructor state are never moved into a generated
141+
`__init__`.** E.g. `class X(a: Float) where\n def y: Y := Y(a)\nend` (a bare, non-`def` constructor arg used
142+
in a field initializer) or a bare executable statement in a class body (e.g. a `print(...)` call) — Python
143+
reference fixtures expect these to be hoisted into `__init__` (with a `None` placeholder left at class level
144+
for fields), but `src/generate/convert/class.rs`'s `extract_class`/`init` only handles parent-`__init__`
145+
calls and auto-generated `self.field = arg` assignments for constructor args, not general relocation. This
146+
needs a free-variable analysis over `Core` expressions to detect which class-body statements reference
147+
constructor-only names. The type checker itself does correctly resolve these now (see `constrain_class_body`
148+
in `src/check/constrain/generate/class.rs`, which binds non-`def` constructor args into the class body's
149+
environment) — it's specifically the codegen relocation that's missing, so affected programs type-check but
150+
transpile to Python that references undefined names.
151+
105152
## Documentation
106153

107154
`docs/` contains the (partially outdated, per its own README) language specification and philosophy docs,

‎src/parse/README.md‎

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,8 @@ Implementation wise, an `AST` is where a `Node` are stored.
1313

1414
## Lex
1515

16-
The lexer converts a string to a vector of `Token`s. This is in part to simplify the parsing stage.
17-
18-
The lexer also deals with the logic necessary to detect indentations. It produces special tokens `Indent` and `Dedent`,
19-
which the next stage uses to identify the start and end of blocks.
20-
16+
The lexer converts a string to a vector of `Token`s.
17+
This is in part to simplify the parsing stage.
2118
We get an error if we encounter an illegal character
2219

2320
## Parse

‎src/parse/class.rs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ pub fn parse_class(it: &mut LexIterator) -> ParseResult {
3232

3333
let mut parents = vec![];
3434
if it.eat_if(&Token::DoublePoint).is_some() {
35-
it.peek_while_not_token(&Token::Where, &mut |it, lex| match lex.token {
35+
it.peek_while_not_tokens(&[Token::NL, Token::Where], &mut |it, lex| match lex.token {
3636
Token::Id(_) | Token::LRBrack => {
3737
parents.push(*it.parse(&parse_parent, "parents", start)?);
3838
it.eat_if(&Token::Comma);

‎src/parse/control_flow_stmt.rs‎

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
use crate::parse::ast::{Node, AST};
22
use crate::parse::block::parse_block;
3-
use crate::parse::expr_or_stmt::parse_expr_or_stmt;
43
use crate::parse::iterator::LexIterator;
54
use crate::parse::lex::token::Token;
65
use crate::parse::operation::parse_expression;
@@ -34,9 +33,8 @@ pub fn parse_cntrl_flow_stmt(it: &mut LexIterator) -> ParseResult {
3433
fn parse_while(it: &mut LexIterator) -> ParseResult {
3534
let start = it.start_pos("while statement")?;
3635
it.eat(&Token::While, "while statement")?;
37-
let cond = it.parse(&parse_expression, "while statement", start)?;
38-
it.eat(&Token::Do, "while")?;
39-
let body = it.parse(&parse_expr_or_stmt, "while statement", start)?;
36+
let cond = it.parse(&parse_expression, "while statement condition", start)?;
37+
let body = it.parse(&parse_block, "while statement block", start)?;
4038

4139
let node = Node::While {
4240
cond,
@@ -282,7 +280,7 @@ mod test {
282280

283281
#[test]
284282
fn while_verify() {
285-
let source = String::from("while a do d");
283+
let source = String::from("while a do d end");
286284
let statements = parse_direct(&source).unwrap();
287285

288286
let (cond, body) = match &statements.first().expect("script empty.").node {
@@ -296,8 +294,14 @@ mod test {
296294
lit: String::from("a")
297295
}
298296
);
297+
298+
let Node::Block { statements } = body.node else {
299+
panic!("expected block: {body:#?}");
300+
};
301+
302+
assert_eq!(statements.len(), 1);
299303
assert_eq!(
300-
body.node,
304+
statements[0].node,
301305
Node::Id {
302306
lit: String::from("d")
303307
}

‎src/parse/expr_or_stmt.rs‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ use crate::parse::statement::{is_start_statement, parse_reassignment, parse_stat
99
use crate::parse::Lex;
1010

1111
pub fn parse_expr_or_stmt(it: &mut LexIterator) -> ParseResult {
12+
it.eat_while(&Token::NL);
13+
1214
let expr_or_stmt = it.peek_or_err(
1315
&|it, lex| match &lex.token {
1416
Token::Do => it.parse(&parse_block, "expression or statement", lex.pos),
@@ -19,9 +21,11 @@ pub fn parse_expr_or_stmt(it: &mut LexIterator) -> ParseResult {
1921
"expression or statement",
2022
)?;
2123

22-
// if expression/statement followed by newline and indent, we are dealing with a handle block
24+
// a call that may raise is marked with '!'; if followed by 'where' we are dealing with a handle block
25+
it.eat_if(&Token::Raise);
26+
2327
if it.peek_if(&|lex: &Lex| lex.token == Token::Where) {
24-
it.eat(&Token::NL, "internal error in parsing call")?; // peek covers this
28+
it.eat(&Token::Where, "handle expression")?;
2529

2630
// parse handle cases if indentation block after
2731
let cases = it.parse_vec(&parse_match_cases, "handle cases", expr_or_stmt.pos)?;

‎src/parse/result.rs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ pub fn expected_one_of(tokens: &[Token], actual: &Lex, parsing: &str) -> ParseEr
9191

9292
pub fn expected(expected: &Token, actual: &Lex, parsing: &str) -> ParseErr {
9393
let msg = format!(
94-
"Expected {}{expected:?} token while parsing {}{parsing}, but found '{}' ({})",
94+
"Expected {} token while parsing {}{parsing}, but found '{}' ({})",
9595
an_or_a(expected),
9696
an_or_a(parsing),
9797
actual.token,

‎src/parse/statement.rs‎

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
use crate::parse::ast::node_op::NodeOp;
22
use crate::parse::ast::Node;
33
use crate::parse::ast::AST;
4+
use crate::parse::block::parse_block;
45
use crate::parse::control_flow_stmt::parse_cntrl_flow_stmt;
56
use crate::parse::definition::parse_definition;
6-
use crate::parse::expr_or_stmt::parse_expr_or_stmt;
77
use crate::parse::iterator::LexIterator;
88
use crate::parse::lex::token::{Lex, Token};
99
use crate::parse::operation::parse_expression;
@@ -174,8 +174,7 @@ pub fn parse_with(it: &mut LexIterator) -> ParseResult {
174174
None
175175
};
176176

177-
it.eat(&Token::Do, "with")?;
178-
let expr = it.parse(&parse_expr_or_stmt, "with", start)?;
177+
let expr = it.parse(&parse_block, "with", start)?;
179178

180179
let node = Node::With {
181180
resource,

‎tests/resource/invalid/type/collection/set_conflicting_collection_types.mamba‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ def x := {"one", 51}
22

33
for item in x do
44
print(item + 2)
5+
end
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
for i in 10 do
22
print(i)
3+
end

‎tests/resource/invalid/type/control_flow/for_over_union_one_not_minus.mamba‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ def x: {Range, String} := "MyString"
22

33
for i in x do
44
print(i - 1)
5+
end

0 commit comments

Comments
 (0)