KCL: End-exclusive ranges like [0..<10] (#7179)

Closes https://github.com/KittyCAD/modeling-app/issues/6843

To clarify:
`[1..10]` is 1, 2, ..., 8, 9, 10
`[1..<10]` is 1, 2, ... 8, 9
This commit is contained in:
Adam Chalmers
2025-05-22 22:13:27 -05:00
committed by GitHub
parent fa4b3cfd1b
commit d0958220fe
5 changed files with 155 additions and 5 deletions

View File

@ -1189,6 +1189,7 @@ impl LanguageServer for Backend {
}
async fn completion(&self, params: CompletionParams) -> RpcResult<Option<CompletionResponse>> {
// ADAM: This is the entrypoint.
let mut completions = vec![CompletionItem {
label: PIPE_OPERATOR.to_string(),
label_details: None,

View File

@ -886,7 +886,7 @@ fn array_end_start(i: &mut TokenSlice) -> PResult<Node<ArrayRangeExpression>> {
ignore_whitespace(i);
let start_element = expression.parse_next(i)?;
ignore_whitespace(i);
double_period.parse_next(i)?;
let end_inclusive = alt((end_inclusive_range.map(|_| true), end_exclusive_range.map(|_| false))).parse_next(i)?;
ignore_whitespace(i);
let end_element = expression.parse_next(i)?;
ignore_whitespace(i);
@ -895,7 +895,7 @@ fn array_end_start(i: &mut TokenSlice) -> PResult<Node<ArrayRangeExpression>> {
ArrayRangeExpression {
start_element,
end_element,
end_inclusive: true,
end_inclusive,
digest: None,
},
start,
@ -2705,7 +2705,7 @@ fn period(i: &mut TokenSlice) -> PResult<()> {
Ok(())
}
fn double_period(i: &mut TokenSlice) -> PResult<Token> {
fn end_inclusive_range(i: &mut TokenSlice) -> PResult<Token> {
any.try_map(|token: Token| {
if matches!(token.token_type, TokenType::DoublePeriod) {
Ok(token)
@ -2724,6 +2724,21 @@ fn double_period(i: &mut TokenSlice) -> PResult<Token> {
.parse_next(i)
}
fn end_exclusive_range(i: &mut TokenSlice) -> PResult<Token> {
any.try_map(|token: Token| {
if matches!(token.token_type, TokenType::DoublePeriodLessThan) {
Ok(token)
} else {
Err(CompilationError::fatal(
token.as_source_range(),
format!("expected a '..<' but found {}", token.value.as_str()),
))
}
})
.context(expected("the ..< operator, used for array ranges like [0..<10]"))
.parse_next(i)
}
fn colon(i: &mut TokenSlice) -> PResult<Token> {
TokenType::Colon.parse_from(i)
}
@ -5344,7 +5359,6 @@ mod snapshot_tests {
);
snapshot_test!(aa, r#"sg = -scale"#);
snapshot_test!(ab, "line(endAbsolute = [0, -1])");
snapshot_test!(ac, "myArray = [0..10]");
snapshot_test!(
ad,
r#"
@ -5485,6 +5499,11 @@ my14 = 4 ^ 2 - 3 ^ 2 * 2
)"#
);
snapshot_test!(kw_function_in_binary_op, r#"val = f(x = 1) + 1"#);
snapshot_test!(
array_ranges,
r#"incl = [1..10]
excl = [0..<10]"#
);
}
#[allow(unused)]

View File

@ -0,0 +1,117 @@
---
source: kcl-lib/src/parsing/parser.rs
expression: actual
---
{
"body": [
{
"commentStart": 0,
"declaration": {
"commentStart": 0,
"end": 14,
"id": {
"commentStart": 0,
"end": 4,
"name": "incl",
"start": 0,
"type": "Identifier"
},
"init": {
"commentStart": 7,
"end": 14,
"endElement": {
"commentStart": 11,
"end": 13,
"raw": "10",
"start": 11,
"type": "Literal",
"type": "Literal",
"value": {
"value": 10.0,
"suffix": "None"
}
},
"endInclusive": true,
"start": 7,
"startElement": {
"commentStart": 8,
"end": 9,
"raw": "1",
"start": 8,
"type": "Literal",
"type": "Literal",
"value": {
"value": 1.0,
"suffix": "None"
}
},
"type": "ArrayRangeExpression",
"type": "ArrayRangeExpression"
},
"start": 0,
"type": "VariableDeclarator"
},
"end": 14,
"kind": "const",
"start": 0,
"type": "VariableDeclaration",
"type": "VariableDeclaration"
},
{
"commentStart": 23,
"declaration": {
"commentStart": 23,
"end": 38,
"id": {
"commentStart": 23,
"end": 27,
"name": "excl",
"start": 23,
"type": "Identifier"
},
"init": {
"commentStart": 30,
"end": 38,
"endElement": {
"commentStart": 35,
"end": 37,
"raw": "10",
"start": 35,
"type": "Literal",
"type": "Literal",
"value": {
"value": 10.0,
"suffix": "None"
}
},
"endInclusive": false,
"start": 30,
"startElement": {
"commentStart": 31,
"end": 32,
"raw": "0",
"start": 31,
"type": "Literal",
"type": "Literal",
"value": {
"value": 0.0,
"suffix": "None"
}
},
"type": "ArrayRangeExpression",
"type": "ArrayRangeExpression"
},
"start": 23,
"type": "VariableDeclarator"
},
"end": 38,
"kind": "const",
"start": 23,
"type": "VariableDeclaration",
"type": "VariableDeclaration"
}
],
"commentStart": 0,
"end": 38,
"start": 0
}

View File

@ -369,6 +369,8 @@ pub enum TokenType {
Period,
/// A double period: `..`.
DoublePeriod,
/// A double period and a less than: `..<`.
DoublePeriodLessThan,
/// A line comment.
LineComment,
/// A block comment.
@ -410,6 +412,7 @@ impl TryFrom<TokenType> for SemanticTokenType {
| TokenType::DoubleColon
| TokenType::Period
| TokenType::DoublePeriod
| TokenType::DoublePeriodLessThan
| TokenType::Hash
| TokenType::Dollar
| TokenType::At

View File

@ -87,7 +87,7 @@ pub(super) fn token(i: &mut Input<'_>) -> PResult<Token> {
'0'..='9' => number,
';' => semi_colon,
':' => alt((double_colon, colon)),
'.' => alt((number, double_period, period)),
'.' => alt((number, double_period_less_than, double_period, period)),
'#' => hash,
'$' => dollar,
'!' => alt((operator, bang)),
@ -320,6 +320,16 @@ fn double_period(i: &mut Input<'_>) -> PResult<Token> {
))
}
fn double_period_less_than(i: &mut Input<'_>) -> PResult<Token> {
let (value, range) = "..<".with_span().parse_next(i)?;
Ok(Token::from_range(
range,
i.state.module_id,
TokenType::DoublePeriodLessThan,
value.to_string(),
))
}
/// Zero or more of either:
/// 1. Any character except " or \
/// 2. Any character preceded by \