Skip to content

Commit cb220da

Browse files
hyperpolymathclaude
andcommitted
feat: add field access (expr.field) and index access (expr.[i])
Both are high-precedence postfix operations at ExprAtom level. Index uses dot-bracket syntax to avoid LALRPOP conflict with list literals. 164 tests pass including 7 new tests. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent d00adb9 commit cb220da

2 files changed

Lines changed: 95 additions & 0 deletions

File tree

compiler/bet-parse/src/grammar.lalrpop

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -479,6 +479,24 @@ ExprAtom: Expr = {
479479
// Holes
480480
"_" => Expr::Hole(None),
481481
"?" <name:IDENT> => Expr::Hole(Some(Symbol::intern(&name))),
482+
483+
// Field access: e.field (left-recursive via SpannedExprAtom).
484+
// Chainable: a.b.c parses as (a.b).c.
485+
<obj:SpannedExprAtom> "." <fl:@L> <field:IDENT> <fr:@R> =>
486+
Expr::Field(
487+
Box::new(obj),
488+
Spanned::new(Symbol::intern(&field), Span::new(fl as u32, fr as u32)),
489+
),
490+
491+
// Index access: e.[i] (left-recursive via SpannedExprAtom).
492+
// Uses dot-bracket syntax to avoid ambiguity with list literals/patterns.
493+
// Chainable: a.[i].[j] parses as (a.[i]).[j].
494+
// Also chains with field access: a.b.[i].c parses as ((a.b).[i]).c.
495+
<obj:SpannedExprAtom> "." "[" <idx:SpannedExpr> "]" =>
496+
Expr::Index(
497+
Box::new(obj),
498+
Box::new(idx),
499+
),
482500
};
483501

484502
RecordField: (Spanned<Symbol>, Spanned<Expr>) = {

compiler/bet-parse/src/lib.rs

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,4 +228,81 @@ mod lib_tests {
228228
let result = parse_expr("parallel 4 do x + 1 end");
229229
assert!(result.is_ok(), "Failed to parse parallel end: {:?}", result.err());
230230
}
231+
232+
#[test]
233+
fn test_parse_field_access() {
234+
let result = parse_expr("record.field");
235+
assert!(result.is_ok(), "Failed to parse field access: {:?}", result.err());
236+
match result.unwrap() {
237+
bet_syntax::ast::Expr::Field(_, field) => {
238+
assert_eq!(field.node.as_str(), "field");
239+
}
240+
other => panic!("Expected Field, got {:?}", other),
241+
}
242+
}
243+
244+
#[test]
245+
fn test_parse_chained_field_access() {
246+
let result = parse_expr("a.b.c");
247+
assert!(result.is_ok(), "Failed to parse chained field access: {:?}", result.err());
248+
// Should parse as (a.b).c
249+
match result.unwrap() {
250+
bet_syntax::ast::Expr::Field(base, field_c) => {
251+
assert_eq!(field_c.node.as_str(), "c");
252+
match &base.node {
253+
bet_syntax::ast::Expr::Field(_, field_b) => {
254+
assert_eq!(field_b.node.as_str(), "b");
255+
}
256+
other => panic!("Expected inner Field, got {:?}", other),
257+
}
258+
}
259+
other => panic!("Expected Field, got {:?}", other),
260+
}
261+
}
262+
263+
#[test]
264+
fn test_parse_index_access() {
265+
let result = parse_expr("arr.[0]");
266+
assert!(result.is_ok(), "Failed to parse index access: {:?}", result.err());
267+
match result.unwrap() {
268+
bet_syntax::ast::Expr::Index(_, _) => {}
269+
other => panic!("Expected Index, got {:?}", other),
270+
}
271+
}
272+
273+
#[test]
274+
fn test_parse_field_then_index() {
275+
let result = parse_expr("record.items.[0]");
276+
assert!(result.is_ok(), "Failed to parse field then index: {:?}", result.err());
277+
// Should parse as (record.items).[0]
278+
match result.unwrap() {
279+
bet_syntax::ast::Expr::Index(base, _) => {
280+
match &base.node {
281+
bet_syntax::ast::Expr::Field(_, field) => {
282+
assert_eq!(field.node.as_str(), "items");
283+
}
284+
other => panic!("Expected inner Field, got {:?}", other),
285+
}
286+
}
287+
other => panic!("Expected Index, got {:?}", other),
288+
}
289+
}
290+
291+
#[test]
292+
fn test_parse_field_access_in_binop() {
293+
// Field access should have higher precedence than binary operators
294+
let result = parse_expr("a.x + b.y");
295+
assert!(result.is_ok(), "Failed to parse field in binop: {:?}", result.err());
296+
match result.unwrap() {
297+
bet_syntax::ast::Expr::BinOp(bet_syntax::ast::BinOp::Add, _, _) => {}
298+
other => panic!("Expected BinOp(Add, ...), got {:?}", other),
299+
}
300+
}
301+
302+
#[test]
303+
fn test_parse_field_access_in_match() {
304+
// Field access on match scrutinee (keyword form)
305+
let result = parse_expr("match x.y a -> 1; b -> 2 end");
306+
assert!(result.is_ok(), "Failed to parse field in match: {:?}", result.err());
307+
}
231308
}

0 commit comments

Comments
 (0)