Skip to content

Commit d9192f6

Browse files
authored
KCL: Array index can be any expression (#7755)
Resolves #6651. Before, `arr[f(i)]` and `arr[i + 1]` were syntax errors. Now they work as you would expect. ## Implementation Before, the properties in a MemberExpression were an enum with either Literal or Identifier, named (imaginatively) LiteralIdentifier. Now that enum is gone. The property of a MemberExpression is now just any Expr (an enum of all KCL expressions).
1 parent fe59171 commit d9192f6

File tree

79 files changed

+2801
-892
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

79 files changed

+2801
-892
lines changed
Loading
Loading

rust/kcl-lib/src/execution/exec_ast.rs

Lines changed: 63 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,10 @@ use crate::{
1717
},
1818
fmt,
1919
modules::{ModuleId, ModulePath, ModuleRepr},
20-
parsing::{
21-
ast::types::{
22-
Annotation, ArrayExpression, ArrayRangeExpression, AscribedExpression, BinaryExpression, BinaryOperator,
23-
BinaryPart, BodyItem, Expr, IfExpression, ImportPath, ImportSelector, ItemVisibility, LiteralIdentifier,
24-
LiteralValue, MemberExpression, Name, Node, NodeRef, ObjectExpression, PipeExpression, Program,
25-
TagDeclarator, Type, UnaryExpression, UnaryOperator,
26-
},
27-
token::NumericSuffix,
20+
parsing::ast::types::{
21+
Annotation, ArrayExpression, ArrayRangeExpression, AscribedExpression, BinaryExpression, BinaryOperator,
22+
BinaryPart, BodyItem, Expr, IfExpression, ImportPath, ImportSelector, ItemVisibility, MemberExpression, Name,
23+
Node, NodeRef, ObjectExpression, PipeExpression, Program, TagDeclarator, Type, UnaryExpression, UnaryOperator,
2824
},
2925
source_range::SourceRange,
3026
std::args::TyF64,
@@ -984,10 +980,20 @@ impl Node<Name> {
984980

985981
impl Node<MemberExpression> {
986982
async fn get_result(&self, exec_state: &mut ExecState, ctx: &ExecutorContext) -> Result<KclValue, KclError> {
987-
let property = Property::try_from(self.computed, self.property.clone(), exec_state, self.into())?;
988983
let meta = Metadata {
989984
source_range: SourceRange::from(self),
990985
};
986+
let property = Property::try_from(
987+
self.computed,
988+
self.property.clone(),
989+
exec_state,
990+
self.into(),
991+
ctx,
992+
&meta,
993+
&[],
994+
StatementKind::Expression,
995+
)
996+
.await?;
991997
let object = ctx
992998
.execute_expr(&self.object, exec_state, &meta, &[], StatementKind::Expression)
993999
.await?;
@@ -1726,87 +1732,62 @@ enum Property {
17261732
}
17271733

17281734
impl Property {
1729-
fn try_from(
1735+
#[allow(clippy::too_many_arguments)]
1736+
async fn try_from<'a>(
17301737
computed: bool,
1731-
value: LiteralIdentifier,
1732-
exec_state: &ExecState,
1738+
value: Expr,
1739+
exec_state: &mut ExecState,
17331740
sr: SourceRange,
1741+
ctx: &ExecutorContext,
1742+
metadata: &Metadata,
1743+
annotations: &[Node<Annotation>],
1744+
statement_kind: StatementKind<'a>,
17341745
) -> Result<Self, KclError> {
17351746
let property_sr = vec![sr];
1736-
let property_src: SourceRange = value.clone().into();
1737-
match value {
1738-
LiteralIdentifier::Identifier(identifier) => {
1739-
let name = &identifier.name;
1740-
if !computed {
1741-
// This is dot syntax. Treat the property as a literal.
1742-
Ok(Property::String(name.to_string()))
1743-
} else {
1744-
// This is bracket syntax. Actually evaluate memory to
1745-
// compute the property.
1746-
let prop = exec_state.stack().get(name, property_src)?;
1747-
jvalue_to_prop(prop, property_sr, name)
1748-
}
1749-
}
1750-
LiteralIdentifier::Literal(literal) => {
1751-
let value = literal.value.clone();
1752-
match value {
1753-
n @ LiteralValue::Number { value, suffix } => {
1754-
if !matches!(suffix, NumericSuffix::None | NumericSuffix::Count) {
1755-
return Err(KclError::new_semantic(KclErrorDetails::new(
1756-
format!("{n} is not a valid index, indices must be non-dimensional numbers"),
1757-
property_sr,
1758-
)));
1759-
}
1760-
if let Some(x) = crate::try_f64_to_usize(value) {
1761-
Ok(Property::UInt(x))
1762-
} else {
1763-
Err(KclError::new_semantic(KclErrorDetails::new(
1764-
format!("{n} is not a valid index, indices must be whole numbers >= 0"),
1765-
property_sr,
1766-
)))
1767-
}
1768-
}
1769-
_ => Err(KclError::new_semantic(KclErrorDetails::new(
1770-
"Only numbers (>= 0) can be indexes".to_owned(),
1771-
vec![sr],
1772-
))),
1773-
}
1774-
}
1747+
if !computed {
1748+
let Expr::Name(identifier) = value else {
1749+
// Should actually be impossible because the parser would reject it.
1750+
return Err(KclError::new_semantic(KclErrorDetails::new(
1751+
"Object expressions like `obj.property` must use simple identifier names, not complex expressions"
1752+
.to_owned(),
1753+
property_sr,
1754+
)));
1755+
};
1756+
return Ok(Property::String(identifier.to_string()));
17751757
}
1776-
}
1777-
}
17781758

1779-
fn jvalue_to_prop(value: &KclValue, property_sr: Vec<SourceRange>, name: &str) -> Result<Property, KclError> {
1780-
let make_err =
1781-
|message: String| Err::<Property, _>(KclError::new_semantic(KclErrorDetails::new(message, property_sr)));
1782-
match value {
1783-
n @ KclValue::Number { value: num, ty, .. } => {
1784-
if !matches!(
1785-
ty,
1786-
NumericType::Known(crate::exec::UnitType::Count) | NumericType::Default { .. } | NumericType::Any
1787-
) {
1788-
return make_err(format!(
1789-
"arrays can only be indexed by non-dimensioned numbers, found {}",
1790-
n.human_friendly_type()
1791-
));
1792-
}
1793-
let num = *num;
1794-
if num < 0.0 {
1795-
return make_err(format!("'{num}' is negative, so you can't index an array with it"));
1796-
}
1797-
let nearest_int = crate::try_f64_to_usize(num);
1798-
if let Some(nearest_int) = nearest_int {
1799-
Ok(Property::UInt(nearest_int))
1800-
} else {
1801-
make_err(format!(
1802-
"'{num}' is not an integer, so you can't index an array with it"
1803-
))
1759+
let prop_value = ctx
1760+
.execute_expr(&value, exec_state, metadata, annotations, statement_kind)
1761+
.await?;
1762+
match prop_value {
1763+
KclValue::Number { value, ty, meta: _ } => {
1764+
if !matches!(
1765+
ty,
1766+
NumericType::Unknown
1767+
| NumericType::Default { .. }
1768+
| NumericType::Known(crate::exec::UnitType::Count)
1769+
) {
1770+
return Err(KclError::new_semantic(KclErrorDetails::new(
1771+
format!(
1772+
"{value} is not a valid index, indices must be non-dimensional numbers. If you're sure this is correct, you can add `: number(Count)` to tell KCL this number is an index"
1773+
),
1774+
property_sr,
1775+
)));
1776+
}
1777+
if let Some(x) = crate::try_f64_to_usize(value) {
1778+
Ok(Property::UInt(x))
1779+
} else {
1780+
Err(KclError::new_semantic(KclErrorDetails::new(
1781+
format!("{value} is not a valid index, indices must be whole numbers >= 0"),
1782+
property_sr,
1783+
)))
1784+
}
18041785
}
1786+
_ => Err(KclError::new_semantic(KclErrorDetails::new(
1787+
"Only numbers (>= 0) can be indexes".to_owned(),
1788+
vec![sr],
1789+
))),
18051790
}
1806-
KclValue::String { value: x, meta: _ } => Ok(Property::String(x.to_owned())),
1807-
_ => make_err(format!(
1808-
"{name} is not a valid property/index, you can only use a string to get the property of an object, or an int (>= 0) to get an item in an array"
1809-
)),
18101791
}
18111792
}
18121793

rust/kcl-lib/src/parsing/ast/digest.rs

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ use crate::parsing::ast::types::{
44
Annotation, ArrayExpression, ArrayRangeExpression, AscribedExpression, BinaryExpression, BinaryPart, BodyItem,
55
CallExpressionKw, DefaultParamVal, ElseIf, Expr, ExpressionStatement, FunctionExpression, FunctionType, Identifier,
66
IfExpression, ImportItem, ImportSelector, ImportStatement, ItemVisibility, KclNone, LabelledExpression, Literal,
7-
LiteralIdentifier, LiteralValue, MemberExpression, Name, ObjectExpression, ObjectProperty, Parameter,
8-
PipeExpression, PipeSubstitution, PrimitiveType, Program, ReturnStatement, TagDeclarator, Type, TypeDeclaration,
9-
UnaryExpression, VariableDeclaration, VariableDeclarator, VariableKind,
7+
LiteralValue, MemberExpression, Name, ObjectExpression, ObjectProperty, Parameter, PipeExpression,
8+
PipeSubstitution, PrimitiveType, Program, ReturnStatement, TagDeclarator, Type, TypeDeclaration, UnaryExpression,
9+
VariableDeclaration, VariableDeclarator, VariableKind,
1010
};
1111

1212
/// Position-independent digest of the AST node.
@@ -170,14 +170,6 @@ impl BinaryPart {
170170
}
171171
}
172172

173-
impl LiteralIdentifier {
174-
pub fn compute_digest(&mut self) -> Digest {
175-
match self {
176-
LiteralIdentifier::Identifier(id) => id.compute_digest(),
177-
LiteralIdentifier::Literal(lit) => lit.compute_digest(),
178-
}
179-
}
180-
}
181173
impl Type {
182174
pub fn compute_digest(&mut self) -> Digest {
183175
let mut hasher = Sha256::new();

rust/kcl-lib/src/parsing/ast/mod.rs

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ pub mod types;
33

44
use crate::{
55
ModuleId,
6-
parsing::ast::types::{BinaryPart, BodyItem, Expr, LiteralIdentifier},
6+
parsing::ast::types::{BinaryPart, BodyItem, Expr},
77
};
88

99
impl BodyItem {
@@ -59,12 +59,3 @@ impl BinaryPart {
5959
}
6060
}
6161
}
62-
63-
impl LiteralIdentifier {
64-
pub fn module_id(&self) -> ModuleId {
65-
match self {
66-
LiteralIdentifier::Identifier(identifier) => identifier.module_id,
67-
LiteralIdentifier::Literal(literal) => literal.module_id,
68-
}
69-
}
70-
}

rust/kcl-lib/src/parsing/ast/types/mod.rs

Lines changed: 3 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -2758,53 +2758,13 @@ impl ObjectProperty {
27582758
}
27592759
}
27602760

2761-
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
2762-
#[ts(export)]
2763-
#[serde(tag = "type")]
2764-
pub enum LiteralIdentifier {
2765-
Identifier(BoxNode<Identifier>),
2766-
Literal(BoxNode<Literal>),
2767-
}
2768-
2769-
impl LiteralIdentifier {
2770-
pub fn start(&self) -> usize {
2771-
match self {
2772-
LiteralIdentifier::Identifier(identifier) => identifier.start,
2773-
LiteralIdentifier::Literal(literal) => literal.start,
2774-
}
2775-
}
2776-
2777-
pub fn end(&self) -> usize {
2778-
match self {
2779-
LiteralIdentifier::Identifier(identifier) => identifier.end,
2780-
LiteralIdentifier::Literal(literal) => literal.end,
2781-
}
2782-
}
2783-
2784-
pub(crate) fn contains_range(&self, range: &SourceRange) -> bool {
2785-
let sr = SourceRange::from(self);
2786-
sr.contains_range(range)
2787-
}
2788-
}
2789-
2790-
impl From<LiteralIdentifier> for SourceRange {
2791-
fn from(id: LiteralIdentifier) -> Self {
2792-
Self::new(id.start(), id.end(), id.module_id())
2793-
}
2794-
}
2795-
2796-
impl From<&LiteralIdentifier> for SourceRange {
2797-
fn from(id: &LiteralIdentifier) -> Self {
2798-
Self::new(id.start(), id.end(), id.module_id())
2799-
}
2800-
}
2801-
28022761
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
28032762
#[ts(export)]
28042763
#[serde(tag = "type")]
28052764
pub struct MemberExpression {
28062765
pub object: Expr,
2807-
pub property: LiteralIdentifier,
2766+
pub property: Expr,
2767+
/// True if `obj[prop]`, false if obj.prop
28082768
pub computed: bool,
28092769

28102770
#[serde(default, skip_serializing_if = "Option::is_none")]
@@ -2826,22 +2786,10 @@ impl MemberExpression {
28262786
/// Rename all identifiers that have the old name to the new given name.
28272787
fn rename_identifiers(&mut self, old_name: &str, new_name: &str) {
28282788
self.object.rename_identifiers(old_name, new_name);
2829-
2830-
match &mut self.property {
2831-
LiteralIdentifier::Identifier(identifier) => identifier.rename(old_name, new_name),
2832-
LiteralIdentifier::Literal(_) => {}
2833-
}
2789+
self.property.rename_identifiers(old_name, new_name);
28342790
}
28352791
}
28362792

2837-
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
2838-
#[ts(export)]
2839-
pub struct ObjectKeyInfo {
2840-
pub key: LiteralIdentifier,
2841-
pub index: usize,
2842-
pub computed: bool,
2843-
}
2844-
28452793
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
28462794
#[ts(export)]
28472795
#[serde(tag = "type")]

rust/kcl-lib/src/parsing/ast/types/path.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -266,7 +266,7 @@ impl NodePath {
266266
}
267267
if node.property.contains_range(&range) {
268268
path.push(Step::MemberExpressionProperty);
269-
return Some(path);
269+
return NodePath::from_expr(&node.property, range, path);
270270
}
271271
}
272272
Expr::UnaryExpression(node) => {

rust/kcl-lib/src/parsing/parser.rs

Lines changed: 29 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,10 @@ use crate::{
2828
Annotation, ArrayExpression, ArrayRangeExpression, BinaryExpression, BinaryOperator, BinaryPart, BodyItem,
2929
BoxNode, CallExpressionKw, CommentStyle, DefaultParamVal, ElseIf, Expr, ExpressionStatement,
3030
FunctionExpression, FunctionType, Identifier, IfExpression, ImportItem, ImportSelector, ImportStatement,
31-
ItemVisibility, LabeledArg, Literal, LiteralIdentifier, LiteralValue, MemberExpression, Name, Node,
32-
NodeList, NonCodeMeta, NonCodeNode, NonCodeValue, ObjectExpression, ObjectProperty, Parameter,
33-
PipeExpression, PipeSubstitution, PrimitiveType, Program, ReturnStatement, Shebang, TagDeclarator, Type,
34-
TypeDeclaration, UnaryExpression, UnaryOperator, VariableDeclaration, VariableDeclarator, VariableKind,
31+
ItemVisibility, LabeledArg, Literal, LiteralValue, MemberExpression, Name, Node, NodeList, NonCodeMeta,
32+
NonCodeNode, NonCodeValue, ObjectExpression, ObjectProperty, Parameter, PipeExpression, PipeSubstitution,
33+
PrimitiveType, Program, ReturnStatement, Shebang, TagDeclarator, Type, TypeDeclaration, UnaryExpression,
34+
UnaryOperator, VariableDeclaration, VariableDeclarator, VariableKind,
3535
},
3636
math::BinaryExpressionToken,
3737
token::{Token, TokenSlice, TokenType},
@@ -1299,36 +1299,34 @@ fn function_decl(i: &mut TokenSlice) -> ModalResult<Node<FunctionExpression>> {
12991299
}
13001300

13011301
/// E.g. `person.name`
1302-
fn member_expression_dot(i: &mut TokenSlice) -> ModalResult<(LiteralIdentifier, usize, bool)> {
1302+
fn member_expression_dot(i: &mut TokenSlice) -> ModalResult<(Expr, usize, bool)> {
13031303
period.parse_next(i)?;
13041304
let property = nameable_identifier
13051305
.map(Box::new)
1306-
.map(LiteralIdentifier::Identifier)
1306+
.map(|p| {
1307+
let ni: Node<Identifier> = *p;
1308+
let nn: Node<Name> = ni.into();
1309+
Expr::Name(Box::new(nn))
1310+
})
13071311
.parse_next(i)?;
13081312
let end = property.end();
1309-
Ok((property, end, false))
1313+
let computed = false;
1314+
Ok((property, end, computed))
13101315
}
13111316

1312-
/// E.g. `people[0]` or `people[i]` or `people['adam']`
1313-
fn member_expression_subscript(i: &mut TokenSlice) -> ModalResult<(LiteralIdentifier, usize, bool)> {
1317+
fn member_expression_subscript_complex_expr(i: &mut TokenSlice) -> ModalResult<(Expr, usize, bool)> {
13141318
let _ = open_bracket.parse_next(i)?;
1315-
// TODO: This should be an expression, not just a literal or identifier.
1316-
let property = alt((
1317-
literal.map(LiteralIdentifier::Literal),
1318-
nameable_identifier.map(Box::new).map(LiteralIdentifier::Identifier),
1319-
))
1320-
.parse_next(i)?;
1321-
1319+
let property = expression.parse_next(i)?;
13221320
let end = close_bracket.parse_next(i)?.end;
1323-
let computed = matches!(property, LiteralIdentifier::Identifier(_));
1321+
let computed = true;
13241322
Ok((property, end, computed))
13251323
}
13261324

13271325
/// Get a property of an object, or an index of an array, or a member of a collection.
13281326
/// Can be arbitrarily nested, e.g. `people[i]['adam'].age`.
13291327
fn build_member_expression(object: Expr, i: &mut TokenSlice) -> ModalResult<Node<MemberExpression>> {
13301328
// Now a sequence of members.
1331-
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'"));
1329+
let member = alt((member_expression_dot, member_expression_subscript_complex_expr)).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'"));
13321330
let mut members: Vec<_> = repeat(1.., member)
13331331
.context(expected("a sequence of at least one members/properties"))
13341332
.parse_next(i)?;
@@ -3407,6 +3405,19 @@ mod tests {
34073405
binary_expression.parse(tokens).unwrap();
34083406
}
34093407

3408+
#[test]
3409+
fn expression_in_array_index() {
3410+
let tokens = crate::parsing::token::lex("arr[x + 1]", ModuleId::default()).unwrap();
3411+
let tokens = tokens.as_slice();
3412+
let Expr::MemberExpression(expr) = expression.parse(tokens).unwrap() else {
3413+
panic!();
3414+
};
3415+
let Expr::BinaryExpression(be) = expr.inner.property else {
3416+
panic!();
3417+
};
3418+
assert_eq!(be.inner.operator, BinaryOperator::Add);
3419+
}
3420+
34103421
#[test]
34113422
fn parse_binary_operator_on_object() {
34123423
let tokens = crate::parsing::token::lex("{ a = 1 } + 2", ModuleId::default()).unwrap();

0 commit comments

Comments
 (0)