feat: add deep path record update syntax
Adds parser desugaring for `{ ...base, pos.x: val, pos.y: val2 }` which
expands to `{ ...base, pos: { ...base.pos, x: val, y: val2 } }`.
Supports arbitrary nesting depth (e.g. world.physics.gravity.y).
Detects conflicts between flat and deep path fields.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
152
src/parser.rs
152
src/parser.rs
@@ -2296,12 +2296,34 @@ impl Parser {
|
||||
return self.parse_record_expr_rest(start);
|
||||
}
|
||||
|
||||
// Check if it's a record (ident: expr) or block
|
||||
// Check if it's a record (ident: expr or ident.path: expr) or block
|
||||
if matches!(self.peek_kind(), TokenKind::Ident(_)) {
|
||||
let lookahead = self.tokens.get(self.pos + 1).map(|t| &t.kind);
|
||||
if matches!(lookahead, Some(TokenKind::Colon)) {
|
||||
return self.parse_record_expr_rest(start);
|
||||
}
|
||||
// Check for deep path record: { ident.ident...: expr }
|
||||
if matches!(lookahead, Some(TokenKind::Dot)) {
|
||||
let mut look = self.pos + 2;
|
||||
loop {
|
||||
match self.tokens.get(look).map(|t| &t.kind) {
|
||||
Some(TokenKind::Ident(_)) => {
|
||||
look += 1;
|
||||
match self.tokens.get(look).map(|t| &t.kind) {
|
||||
Some(TokenKind::Colon) => {
|
||||
return self.parse_record_expr_rest(start);
|
||||
}
|
||||
Some(TokenKind::Dot) => {
|
||||
look += 1;
|
||||
continue;
|
||||
}
|
||||
_ => break,
|
||||
}
|
||||
}
|
||||
_ => break,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// It's a block
|
||||
@@ -2309,8 +2331,9 @@ impl Parser {
|
||||
}
|
||||
|
||||
fn parse_record_expr_rest(&mut self, start: Span) -> Result<Expr, ParseError> {
|
||||
let mut fields = Vec::new();
|
||||
let mut raw_fields: Vec<(Vec<Ident>, Expr)> = Vec::new();
|
||||
let mut spread = None;
|
||||
let mut has_deep_paths = false;
|
||||
|
||||
// Check for spread: { ...expr, ... }
|
||||
if self.check(TokenKind::DotDotDot) {
|
||||
@@ -2327,9 +2350,21 @@ impl Parser {
|
||||
|
||||
while !self.check(TokenKind::RBrace) {
|
||||
let name = self.parse_ident()?;
|
||||
|
||||
// Check for dotted path: pos.x, pos.x.y, etc.
|
||||
let mut path = vec![name];
|
||||
while self.check(TokenKind::Dot) {
|
||||
self.advance(); // consume .
|
||||
let segment = self.parse_ident()?;
|
||||
path.push(segment);
|
||||
}
|
||||
if path.len() > 1 {
|
||||
has_deep_paths = true;
|
||||
}
|
||||
|
||||
self.expect(TokenKind::Colon)?;
|
||||
let value = self.parse_expr()?;
|
||||
fields.push((name, value));
|
||||
raw_fields.push((path, value));
|
||||
|
||||
self.skip_newlines();
|
||||
if self.check(TokenKind::Comma) {
|
||||
@@ -2340,10 +2375,119 @@ impl Parser {
|
||||
|
||||
self.expect(TokenKind::RBrace)?;
|
||||
let span = start.merge(self.previous_span());
|
||||
|
||||
if has_deep_paths {
|
||||
Self::desugar_deep_fields(spread, raw_fields, span)
|
||||
} else {
|
||||
// No deep paths — use flat fields directly (common case, no allocation overhead)
|
||||
let fields = raw_fields
|
||||
.into_iter()
|
||||
.map(|(mut path, value)| (path.remove(0), value))
|
||||
.collect();
|
||||
Ok(Expr::Record {
|
||||
spread,
|
||||
fields,
|
||||
span,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Desugar deep path record fields into nested record spread expressions.
|
||||
/// `{ ...base, pos.x: vx, pos.y: vy }` becomes `{ ...base, pos: { ...base.pos, x: vx, y: vy } }`
|
||||
fn desugar_deep_fields(
|
||||
spread: Option<Box<Expr>>,
|
||||
raw_fields: Vec<(Vec<Ident>, Expr)>,
|
||||
outer_span: Span,
|
||||
) -> Result<Expr, ParseError> {
|
||||
use std::collections::HashMap;
|
||||
|
||||
// Group fields by first path segment, preserving order
|
||||
let mut groups: Vec<(String, Vec<(Vec<Ident>, Expr)>)> = Vec::new();
|
||||
let mut group_map: HashMap<String, usize> = HashMap::new();
|
||||
|
||||
for (path, value) in raw_fields {
|
||||
let key = path[0].name.clone();
|
||||
if let Some(&idx) = group_map.get(&key) {
|
||||
groups[idx].1.push((path, value));
|
||||
} else {
|
||||
group_map.insert(key.clone(), groups.len());
|
||||
groups.push((key, vec![(path, value)]));
|
||||
}
|
||||
}
|
||||
|
||||
let mut fields = Vec::new();
|
||||
for (_, group) in groups {
|
||||
let first_ident = group[0].0[0].clone();
|
||||
|
||||
let has_flat = group.iter().any(|(p, _)| p.len() == 1);
|
||||
let has_deep = group.iter().any(|(p, _)| p.len() > 1);
|
||||
|
||||
if has_flat && has_deep {
|
||||
return Err(ParseError {
|
||||
message: format!(
|
||||
"Field '{}' appears as both a direct field and a deep path prefix",
|
||||
first_ident.name
|
||||
),
|
||||
span: first_ident.span,
|
||||
});
|
||||
}
|
||||
|
||||
if has_flat {
|
||||
if group.len() > 1 {
|
||||
return Err(ParseError {
|
||||
message: format!("Duplicate field '{}'", first_ident.name),
|
||||
span: group[1].0[0].span,
|
||||
});
|
||||
}
|
||||
let (_, value) = group.into_iter().next().unwrap();
|
||||
fields.push((first_ident, value));
|
||||
} else {
|
||||
// Deep paths — create nested record with spread from parent
|
||||
let sub_spread = spread.as_ref().map(|s| {
|
||||
Box::new(Expr::Field {
|
||||
object: s.clone(),
|
||||
field: first_ident.clone(),
|
||||
span: first_ident.span,
|
||||
})
|
||||
});
|
||||
|
||||
// Strip first segment from all paths
|
||||
let sub_fields: Vec<(Vec<Ident>, Expr)> = group
|
||||
.into_iter()
|
||||
.map(|(mut path, value)| {
|
||||
path.remove(0);
|
||||
(path, value)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let has_nested_deep = sub_fields.iter().any(|(p, _)| p.len() > 1);
|
||||
if has_nested_deep {
|
||||
// Recursively desugar deeper paths
|
||||
let nested =
|
||||
Self::desugar_deep_fields(sub_spread, sub_fields, first_ident.span)?;
|
||||
fields.push((first_ident, nested));
|
||||
} else {
|
||||
// All sub-paths are single-segment — build Record directly
|
||||
let flat_fields: Vec<(Ident, Expr)> = sub_fields
|
||||
.into_iter()
|
||||
.map(|(mut path, value)| (path.remove(0), value))
|
||||
.collect();
|
||||
fields.push((
|
||||
first_ident.clone(),
|
||||
Expr::Record {
|
||||
spread: sub_spread,
|
||||
fields: flat_fields,
|
||||
span: first_ident.span,
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Expr::Record {
|
||||
spread,
|
||||
fields,
|
||||
span,
|
||||
span: outer_span,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user