KCL parser: Allow .prop or [index] to follow any expression (#7371)

Previously in a member expression like `foo.x` or `foo[3]`, `foo` had to be an identifier. You could not do something like `f().x` (and if you tried, you got a cryptic error). Rather than make the error better, we should just accept any expression to be the LHS of a member expression (aka its 'object').

This does knock our "parse lots of function calls" from 58 to 55 calls before it stack overflows. But I think it's fine, we'll address this in https://github.com/KittyCAD/modeling-app/pull/6226 when I get back to it.

Closes https://github.com/KittyCAD/modeling-app/issues/7273
This commit is contained in:
Adam Chalmers
2025-06-05 08:23:48 -05:00
committed by GitHub
parent 9136fb0d1b
commit 4575b32dbc
77 changed files with 3397 additions and 1987 deletions

View File

@ -4,7 +4,7 @@ use crate::parsing::ast::types::{
Annotation, ArrayExpression, ArrayRangeExpression, AscribedExpression, BinaryExpression, BinaryPart, BodyItem,
CallExpressionKw, DefaultParamVal, ElseIf, Expr, ExpressionStatement, FunctionExpression, FunctionType, Identifier,
IfExpression, ImportItem, ImportSelector, ImportStatement, ItemVisibility, KclNone, LabelledExpression, Literal,
LiteralIdentifier, LiteralValue, MemberExpression, MemberObject, Name, ObjectExpression, ObjectProperty, Parameter,
LiteralIdentifier, LiteralValue, MemberExpression, Name, ObjectExpression, ObjectProperty, Parameter,
PipeExpression, PipeSubstitution, PrimitiveType, Program, ReturnStatement, TagDeclarator, Type, TypeDeclaration,
UnaryExpression, VariableDeclaration, VariableDeclarator, VariableKind,
};
@ -167,15 +167,6 @@ impl BinaryPart {
}
}
impl MemberObject {
pub fn compute_digest(&mut self) -> Digest {
match self {
MemberObject::MemberExpression(me) => me.compute_digest(),
MemberObject::Identifier(id) => id.compute_digest(),
}
}
}
impl LiteralIdentifier {
pub fn compute_digest(&mut self) -> Digest {
match self {

View File

@ -2,7 +2,7 @@ pub(crate) mod digest;
pub mod types;
use crate::{
parsing::ast::types::{BinaryPart, BodyItem, Expr, LiteralIdentifier, MemberObject},
parsing::ast::types::{BinaryPart, BodyItem, Expr, LiteralIdentifier},
ModuleId,
};
@ -57,15 +57,6 @@ impl BinaryPart {
}
}
impl MemberObject {
pub fn module_id(&self) -> ModuleId {
match self {
MemberObject::MemberExpression(member_expression) => member_expression.module_id,
MemberObject::Identifier(identifier) => identifier.module_id,
}
}
}
impl LiteralIdentifier {
pub fn module_id(&self) -> ModuleId {
match self {

View File

@ -2756,47 +2756,6 @@ impl ObjectProperty {
}
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(tag = "type")]
pub enum MemberObject {
MemberExpression(BoxNode<MemberExpression>),
Identifier(BoxNode<Identifier>),
}
impl MemberObject {
pub fn start(&self) -> usize {
match self {
MemberObject::MemberExpression(member_expression) => member_expression.start,
MemberObject::Identifier(identifier) => identifier.start,
}
}
pub fn end(&self) -> usize {
match self {
MemberObject::MemberExpression(member_expression) => member_expression.end,
MemberObject::Identifier(identifier) => identifier.end,
}
}
pub(crate) fn contains_range(&self, range: &SourceRange) -> bool {
let sr = SourceRange::from(self);
sr.contains_range(range)
}
}
impl From<MemberObject> for SourceRange {
fn from(obj: MemberObject) -> Self {
Self::new(obj.start(), obj.end(), obj.module_id())
}
}
impl From<&MemberObject> for SourceRange {
fn from(obj: &MemberObject) -> Self {
Self::new(obj.start(), obj.end(), obj.module_id())
}
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(tag = "type")]
@ -2842,7 +2801,7 @@ impl From<&LiteralIdentifier> for SourceRange {
#[ts(export)]
#[serde(tag = "type")]
pub struct MemberExpression {
pub object: MemberObject,
pub object: Expr,
pub property: LiteralIdentifier,
pub computed: bool,
@ -2864,12 +2823,7 @@ impl Node<MemberExpression> {
impl MemberExpression {
/// Rename all identifiers that have the old name to the new given name.
fn rename_identifiers(&mut self, old_name: &str, new_name: &str) {
match &mut self.object {
MemberObject::MemberExpression(ref mut member_expression) => {
member_expression.rename_identifiers(old_name, new_name)
}
MemberObject::Identifier(ref mut identifier) => identifier.rename(old_name, new_name),
}
self.object.rename_identifiers(old_name, new_name);
match &mut self.property {
LiteralIdentifier::Identifier(ref mut identifier) => identifier.rename(old_name, new_name),

View File

@ -1,6 +1,6 @@
use serde::Serialize;
use super::{BodyItem, Expr, MemberObject, Node, Program};
use super::{BodyItem, Expr, Node, Program};
use crate::SourceRange;
/// A traversal path through the AST to a node.
@ -248,7 +248,7 @@ impl NodePath {
Expr::MemberExpression(node) => {
if node.object.contains_range(&range) {
path.push(Step::MemberExpressionObject);
return Self::from_member_expr_object(&node.object, range, path);
return NodePath::from_expr(&node.object, range, path);
}
if node.property.contains_range(&range) {
path.push(Step::MemberExpressionProperty);
@ -313,18 +313,6 @@ impl NodePath {
Some(path)
}
fn from_member_expr_object(mut expr: &MemberObject, range: SourceRange, mut path: NodePath) -> Option<NodePath> {
while let MemberObject::MemberExpression(node) = expr {
if !node.object.contains_range(&range) {
break;
}
path.push(Step::MemberExpressionObject);
expr = &node.object;
}
Some(path)
}
pub fn is_empty(&self) -> bool {
self.steps.is_empty()
}

View File

@ -26,8 +26,8 @@ use crate::{
Annotation, ArrayExpression, ArrayRangeExpression, BinaryExpression, BinaryOperator, BinaryPart, BodyItem,
BoxNode, CallExpressionKw, CommentStyle, DefaultParamVal, ElseIf, Expr, ExpressionStatement,
FunctionExpression, FunctionType, Identifier, IfExpression, ImportItem, ImportSelector, ImportStatement,
ItemVisibility, LabeledArg, Literal, LiteralIdentifier, LiteralValue, MemberExpression, MemberObject, Name,
Node, NodeList, NonCodeMeta, NonCodeNode, NonCodeValue, ObjectExpression, ObjectProperty, Parameter,
ItemVisibility, LabeledArg, Literal, LiteralIdentifier, LiteralValue, MemberExpression, Name, Node,
NodeList, NonCodeMeta, NonCodeNode, NonCodeValue, ObjectExpression, ObjectProperty, Parameter,
PipeExpression, PipeSubstitution, PrimitiveType, Program, ReturnStatement, Shebang, TagDeclarator, Type,
TypeDeclaration, UnaryExpression, UnaryOperator, VariableDeclaration, VariableDeclarator, VariableKind,
},
@ -1326,10 +1326,7 @@ fn member_expression_subscript(i: &mut TokenSlice) -> ModalResult<(LiteralIdenti
/// Get a property of an object, or an index of an array, or a member of a collection.
/// Can be arbitrarily nested, e.g. `people[i]['adam'].age`.
fn member_expression(i: &mut TokenSlice) -> ModalResult<Node<MemberExpression>> {
// This is an identifier, followed by a sequence of members (aka properties)
// First, the identifier.
let id = nameable_identifier.context(expected("the identifier of the object whose property you're trying to access, e.g. in 'shape.size.width', 'shape' is the identifier")).parse_next(i)?;
fn build_member_expression(object: Expr, i: &mut TokenSlice) -> ModalResult<Node<MemberExpression>> {
// Now a sequence of members.
let member = alt((member_expression_dot, member_expression_subscript)).context(expected("a member/property, e.g. size.x and size['height'] and size[0] are all different ways to access a member/property of 'size'"));
let mut members: Vec<_> = repeat(1.., member)
@ -1340,11 +1337,11 @@ fn member_expression(i: &mut TokenSlice) -> ModalResult<Node<MemberExpression>>
// It's safe to call remove(0), because the vec is created from repeat(1..),
// which is guaranteed to have >=1 elements.
let (property, end, computed) = members.remove(0);
let start = id.start;
let module_id = id.module_id;
let start = object.start();
let module_id = object.module_id();
let initial_member_expression = Node::new(
MemberExpression {
object: MemberObject::Identifier(Box::new(id)),
object,
computed,
property,
digest: None,
@ -1362,7 +1359,7 @@ fn member_expression(i: &mut TokenSlice) -> ModalResult<Node<MemberExpression>>
.fold(initial_member_expression, |accumulated, (property, end, computed)| {
Node::new(
MemberExpression {
object: MemberObject::MemberExpression(Box::new(accumulated)),
object: Expr::MemberExpression(Box::new(accumulated)),
computed,
property,
digest: None,
@ -2085,8 +2082,7 @@ fn unnecessarily_bracketed(i: &mut TokenSlice) -> ModalResult<Expr> {
}
fn expr_allowed_in_pipe_expr(i: &mut TokenSlice) -> ModalResult<Expr> {
alt((
member_expression.map(Box::new).map(Expr::MemberExpression),
let parsed_expr = alt((
bool_value.map(Box::new).map(Expr::Literal),
tag.map(Box::new).map(Expr::TagDeclarator),
literal.map(Expr::Literal),
@ -2100,14 +2096,19 @@ fn expr_allowed_in_pipe_expr(i: &mut TokenSlice) -> ModalResult<Expr> {
unnecessarily_bracketed,
))
.context(expected("a KCL expression (but not a pipe expression)"))
.parse_next(i)
.parse_next(i)?;
let maybe_member = build_member_expression(parsed_expr.clone(), i);
if let Ok(mem) = maybe_member {
return Ok(Expr::MemberExpression(Box::new(mem)));
}
Ok(parsed_expr)
}
fn possible_operands(i: &mut TokenSlice) -> ModalResult<Expr> {
let mut expr = alt((
unary_expression.map(Box::new).map(Expr::UnaryExpression),
bool_value.map(Box::new).map(Expr::Literal),
member_expression.map(Box::new).map(Expr::MemberExpression),
literal.map(Expr::Literal),
fn_call_kw.map(Box::new).map(Expr::CallExpressionKw),
name.map(Box::new).map(Expr::Name),
@ -2118,6 +2119,10 @@ fn possible_operands(i: &mut TokenSlice) -> ModalResult<Expr> {
"a KCL value which can be used as an argument/operand to an operator",
))
.parse_next(i)?;
let maybe_member = build_member_expression(expr.clone(), i);
if let Ok(mem) = maybe_member {
expr = Expr::MemberExpression(Box::new(mem));
}
let ty = opt((colon, opt(whitespace), type_)).parse_next(i)?;
if let Some((_, _, ty)) = ty {
@ -3258,7 +3263,7 @@ fn fn_call_kw(i: &mut TokenSlice) -> ModalResult<Node<CallExpressionKw>> {
return Err(ErrMode::Cut(
CompilationError::fatal(
SourceRange::from(&tok),
format!("There was an unexpected {}. Try removing it.", tok.value),
format!("There was an unexpected `{}`. Try removing it.", tok.value),
)
.into(),
));
@ -4427,7 +4432,7 @@ z(-[["#,
assert_err(
r#"z
(--#"#,
"There was an unexpected -. Try removing it.",
"There was an unexpected `-`. Try removing it.",
[3, 4],
);
}
@ -4997,6 +5002,17 @@ type foo = fn(fn, f: fn(number(_))): [fn([any]): string]
assert_no_err(some_program_string);
}
#[test]
fn test_parse_fn_call_then_field() {
let some_program_string = "myFunction().field";
let module_id = ModuleId::default();
let tokens = crate::parsing::token::lex(some_program_string, module_id).unwrap(); // Updated import path
let actual = expression.parse(tokens.as_slice()).unwrap();
let Expr::MemberExpression(_expr) = actual else {
panic!("expected member expression")
};
}
#[test]
fn test_parse_array_missing_closing_bracket() {
let some_program_string = r#"
@ -5185,7 +5201,7 @@ bar = 1
.cause
.as_ref()
.expect("Found an error, but there was no cause. Add a cause.");
assert_eq!(cause.message, "There was an unexpected }. Try removing it.",);
assert_eq!(cause.message, "There was an unexpected `}`. Try removing it.",);
assert_eq!(cause.source_range.start(), expected_src_start);
}

View File

@ -117,12 +117,20 @@ expression: actual
"computed": false,
"end": 45,
"object": {
"abs_path": false,
"commentStart": 40,
"end": 43,
"name": "obj",
"name": {
"commentStart": 40,
"end": 43,
"name": "obj",
"start": 40,
"type": "Identifier"
},
"path": [],
"start": 40,
"type": "Identifier",
"type": "Identifier"
"type": "Name",
"type": "Name"
},
"property": {
"commentStart": 44,

View File

@ -117,12 +117,20 @@ expression: actual
"computed": false,
"end": 49,
"object": {
"abs_path": false,
"commentStart": 41,
"end": 44,
"name": "obj",
"name": {
"commentStart": 41,
"end": 44,
"name": "obj",
"start": 41,
"type": "Identifier"
},
"path": [],
"start": 41,
"type": "Identifier",
"type": "Identifier"
"type": "Name",
"type": "Name"
},
"property": {
"commentStart": 45,

View File

@ -104,12 +104,20 @@ expression: actual
"computed": false,
"end": 44,
"object": {
"abs_path": false,
"commentStart": 36,
"end": 39,
"name": "obj",
"name": {
"commentStart": 36,
"end": 39,
"name": "obj",
"start": 36,
"type": "Identifier"
},
"path": [],
"start": 36,
"type": "Identifier",
"type": "Identifier"
"type": "Name",
"type": "Name"
},
"property": {
"commentStart": 40,

View File

@ -120,12 +120,20 @@ expression: actual
"computed": false,
"end": 49,
"object": {
"abs_path": false,
"commentStart": 41,
"end": 44,
"name": "obj",
"name": {
"commentStart": 41,
"end": 44,
"name": "obj",
"start": 41,
"type": "Identifier"
},
"path": [],
"start": 41,
"type": "Identifier",
"type": "Identifier"
"type": "Name",
"type": "Name"
},
"property": {
"commentStart": 45,

View File

@ -107,12 +107,20 @@ expression: actual
"computed": false,
"end": 45,
"object": {
"abs_path": false,
"commentStart": 37,
"end": 40,
"name": "obj",
"name": {
"commentStart": 37,
"end": 40,
"name": "obj",
"start": 37,
"type": "Identifier"
},
"path": [],
"start": 37,
"type": "Identifier",
"type": "Identifier"
"type": "Name",
"type": "Name"
},
"property": {
"commentStart": 41,

View File

@ -107,12 +107,20 @@ expression: actual
"computed": false,
"end": 44,
"object": {
"abs_path": false,
"commentStart": 36,
"end": 39,
"name": "obj",
"name": {
"commentStart": 36,
"end": 39,
"name": "obj",
"start": 36,
"type": "Identifier"
},
"path": [],
"start": 36,
"type": "Identifier",
"type": "Identifier"
"type": "Name",
"type": "Name"
},
"property": {
"commentStart": 40,

View File

@ -37,12 +37,20 @@ expression: actual
"computed": false,
"end": 18,
"object": {
"abs_path": false,
"commentStart": 13,
"end": 16,
"name": "obj",
"name": {
"commentStart": 13,
"end": 16,
"name": "obj",
"start": 13,
"type": "Identifier"
},
"path": [],
"start": 13,
"type": "Identifier",
"type": "Identifier"
"type": "Name",
"type": "Name"
},
"property": {
"commentStart": 17,

View File

@ -24,12 +24,20 @@ expression: actual
"computed": false,
"end": 19,
"object": {
"abs_path": false,
"commentStart": 11,
"end": 14,
"name": "obj",
"name": {
"commentStart": 11,
"end": 14,
"name": "obj",
"start": 11,
"type": "Identifier"
},
"path": [],
"start": 11,
"type": "Identifier",
"type": "Identifier"
"type": "Name",
"type": "Name"
},
"property": {
"commentStart": 15,

View File

@ -101,12 +101,20 @@ expression: actual
"computed": false,
"end": 44,
"object": {
"abs_path": false,
"commentStart": 36,
"end": 39,
"name": "obj",
"name": {
"commentStart": 36,
"end": 39,
"name": "obj",
"start": 36,
"type": "Identifier"
},
"path": [],
"start": 36,
"type": "Identifier",
"type": "Identifier"
"type": "Name",
"type": "Name"
},
"property": {
"commentStart": 40,

View File

@ -25,12 +25,20 @@ expression: actual
"computed": false,
"end": 16,
"object": {
"abs_path": false,
"commentStart": 7,
"end": 9,
"name": "yo",
"name": {
"commentStart": 7,
"end": 9,
"name": "yo",
"start": 7,
"type": "Identifier"
},
"path": [],
"start": 7,
"type": "Identifier",
"type": "Identifier"
"type": "Name",
"type": "Name"
},
"property": {
"commentStart": 10,

View File

@ -21,12 +21,20 @@ expression: actual
"computed": true,
"end": 11,
"object": {
"abs_path": false,
"commentStart": 6,
"end": 8,
"name": "b1",
"name": {
"commentStart": 6,
"end": 8,
"name": "b1",
"start": 6,
"type": "Identifier"
},
"path": [],
"start": 6,
"type": "Identifier",
"type": "Identifier"
"type": "Name",
"type": "Name"
},
"property": {
"commentStart": 9,

View File

@ -33,12 +33,20 @@ expression: actual
"computed": false,
"end": 13,
"object": {
"abs_path": false,
"commentStart": 7,
"end": 9,
"name": "yo",
"name": {
"commentStart": 7,
"end": 9,
"name": "yo",
"start": 7,
"type": "Identifier"
},
"path": [],
"start": 7,
"type": "Identifier",
"type": "Identifier"
"type": "Name",
"type": "Name"
},
"property": {
"commentStart": 10,

View File

@ -21,12 +21,20 @@ expression: actual
"computed": false,
"end": 11,
"object": {
"abs_path": false,
"commentStart": 6,
"end": 8,
"name": "b1",
"name": {
"commentStart": 6,
"end": 8,
"name": "b1",
"start": 6,
"type": "Identifier"
},
"path": [],
"start": 6,
"type": "Identifier",
"type": "Identifier"
"type": "Name",
"type": "Name"
},
"property": {
"commentStart": 9,

View File

@ -21,12 +21,20 @@ expression: actual
"computed": false,
"end": 16,
"object": {
"abs_path": false,
"commentStart": 6,
"end": 8,
"name": "b1",
"name": {
"commentStart": 6,
"end": 8,
"name": "b1",
"start": 6,
"type": "Identifier"
},
"path": [],
"start": 6,
"type": "Identifier",
"type": "Identifier"
"type": "Name",
"type": "Name"
},
"property": {
"commentStart": 9,

View File

@ -21,12 +21,20 @@ expression: actual
"computed": false,
"end": 13,
"object": {
"abs_path": false,
"commentStart": 6,
"end": 8,
"name": "b1",
"name": {
"commentStart": 6,
"end": 8,
"name": "b1",
"start": 6,
"type": "Identifier"
},
"path": [],
"start": 6,
"type": "Identifier",
"type": "Identifier"
"type": "Name",
"type": "Name"
},
"property": {
"commentStart": 9,