AST: Allow unlabeled kw args (#4686)

When declaring a function, its first parameter is allowed to be prefixed with `@`. This means that when users call this function, they don't have to label this argument.

Only the first parameter is allowed this prefix, no others.

Part of https://github.com/KittyCAD/modeling-app/issues/4600
This commit is contained in:
Adam Chalmers
2024-12-06 15:44:39 -06:00
committed by GitHub
parent 2274d6459c
commit dd370a9365
10 changed files with 527 additions and 7 deletions

View File

@ -15,7 +15,6 @@ redo-kcl-stdlib-docs:
TWENTY_TWENTY=overwrite {{cnr}} -p kcl-lib kcl_test_example
EXPECTORATE=overwrite {{cnr}} -p kcl-lib docs::gen_std_tests::test_generate_stdlib
# Copy a test KCL file from executor tests into a new simulation test.
copy-exec-test-into-sim-test test_name:
mkdir -p kcl/tests/{{test_name}}

View File

@ -2834,6 +2834,18 @@ impl Parameter {
}
}
impl From<&Parameter> for SourceRange {
fn from(p: &Parameter) -> Self {
let sr = Self::from(&p.identifier);
// If it's unlabelled, the span should start 1 char earlier than the identifier,
// to include the '@' symbol.
if !p.labeled {
return Self::new(sr.start() - 1, sr.end(), sr.module_id());
}
sr
}
}
fn is_true(b: &bool) -> bool {
*b
}

View File

@ -2180,6 +2180,11 @@ fn question_mark(i: TokenSlice) -> PResult<()> {
Ok(())
}
fn at_sign(i: TokenSlice) -> PResult<()> {
TokenType::At.parse_from(i)?;
Ok(())
}
fn fun(i: TokenSlice) -> PResult<Token> {
any.try_map(|token: Token| match token.token_type {
TokenType::Keyword if token.value == "fn" => Ok(token),
@ -2252,13 +2257,15 @@ fn argument_type(i: TokenSlice) -> PResult<FnArgType> {
}
struct ParamDescription {
labeled: bool,
arg_name: Token,
type_: std::option::Option<FnArgType>,
is_optional: bool,
}
fn parameter(i: TokenSlice) -> PResult<ParamDescription> {
let (arg_name, optional, _, type_) = (
let (found_at_sign, arg_name, optional, _, type_) = (
opt(at_sign),
any.verify(|token: &Token| !matches!(token.token_type, TokenType::Brace) || token.value != ")"),
opt(question_mark),
opt(whitespace),
@ -2266,6 +2273,7 @@ fn parameter(i: TokenSlice) -> PResult<ParamDescription> {
)
.parse_next(i)?;
Ok(ParamDescription {
labeled: found_at_sign.is_none(),
arg_name,
type_,
is_optional: optional.is_some(),
@ -2284,6 +2292,7 @@ fn parameters(i: TokenSlice) -> PResult<Vec<Parameter>> {
.into_iter()
.map(
|ParamDescription {
labeled,
arg_name,
type_,
is_optional,
@ -2299,7 +2308,7 @@ fn parameters(i: TokenSlice) -> PResult<Vec<Parameter>> {
} else {
None
},
labeled: true,
labeled,
digest: None,
})
},
@ -2307,6 +2316,15 @@ fn parameters(i: TokenSlice) -> PResult<Vec<Parameter>> {
.collect::<Result<_, _>>()
.map_err(|e: CompilationError| ErrMode::Backtrack(ContextError::from(e)))?;
// Make sure the only unlabeled parameter is the first one.
if let Some(param) = params.iter().skip(1).find(|param| !param.labeled) {
let source_range = SourceRange::from(param);
return Err(ErrMode::Cut(ContextError::from(CompilationError::fatal(
source_range,
"Only the first parameter can be declared unlabeled",
))));
}
// Make sure optional parameters are last.
if let Err(e) = optional_after_required(&params) {
return Err(ErrMode::Cut(ContextError::from(e)));
@ -3475,12 +3493,18 @@ const mySk1 = startSketchAt([0, 0])"#;
}
#[track_caller]
fn assert_err(p: &str, msg: &str, src: [usize; 2]) {
fn assert_err(p: &str, msg: &str, src_expected: [usize; 2]) {
let result = crate::parsing::top_level_parse(p);
let err = result.unwrap_errs().next().unwrap();
assert_eq!(err.message, msg);
assert_eq!(err.source_range.start(), src[0]);
assert_eq!(err.source_range.end(), src[1]);
let src_actual = [err.source_range.start(), err.source_range.end()];
assert_eq!(
src_expected,
src_actual,
"expected error would highlight {} but it actually highlighted {}",
&p[src_expected[0]..src_expected[1]],
&p[src_actual[0]..src_actual[1]],
);
}
#[track_caller]
@ -3589,6 +3613,20 @@ const secondExtrude = startSketchOn('XY')
assert_err(">!", "Unexpected token: >", [0, 1]);
}
#[test]
fn test_parse_unlabeled_param_not_allowed() {
assert_err(
"fn f(@x, @y) { return 1 }",
"Only the first parameter can be declared unlabeled",
[9, 11],
);
assert_err(
"fn f(x, @y) { return 1 }",
"Only the first parameter can be declared unlabeled",
[8, 10],
);
}
#[test]
fn test_parse_z_percent_parens() {
assert_err("z%)", "Unexpected token: %", [1, 2]);
@ -4478,6 +4516,8 @@ const my14 = 4 ^ 2 - 3 ^ 2 * 2
);
snapshot_test!(kw_function_unnamed_first, r#"val = foo(x, y: z)"#);
snapshot_test!(kw_function_all_named, r#"val = foo(x: a, y: b)"#);
snapshot_test!(kw_function_decl_all_labeled, r#"fn foo(x, y) { return 1 }"#);
snapshot_test!(kw_function_decl_first_unlabeled, r#"fn foo(@x, y) { return 1 }"#);
}
#[allow(unused)]

View File

@ -0,0 +1,75 @@
---
source: kcl/src/parsing/parser.rs
expression: actual
snapshot_kind: text
---
{
"body": [
{
"declaration": {
"end": 25,
"id": {
"end": 6,
"name": "foo",
"start": 3,
"type": "Identifier"
},
"init": {
"body": {
"body": [
{
"argument": {
"end": 23,
"raw": "1",
"start": 22,
"type": "Literal",
"type": "Literal",
"value": 1.0
},
"end": 23,
"start": 15,
"type": "ReturnStatement",
"type": "ReturnStatement"
}
],
"end": 25,
"start": 13
},
"end": 25,
"params": [
{
"type": "Parameter",
"identifier": {
"end": 8,
"name": "x",
"start": 7,
"type": "Identifier"
}
},
{
"type": "Parameter",
"identifier": {
"end": 11,
"name": "y",
"start": 10,
"type": "Identifier"
}
}
],
"start": 6,
"type": "FunctionExpression",
"type": "FunctionExpression"
},
"start": 3,
"type": "VariableDeclarator"
},
"end": 25,
"kind": "fn",
"start": 0,
"type": "VariableDeclaration",
"type": "VariableDeclaration"
}
],
"end": 25,
"start": 0
}

View File

@ -0,0 +1,76 @@
---
source: kcl/src/parsing/parser.rs
expression: actual
snapshot_kind: text
---
{
"body": [
{
"declaration": {
"end": 26,
"id": {
"end": 6,
"name": "foo",
"start": 3,
"type": "Identifier"
},
"init": {
"body": {
"body": [
{
"argument": {
"end": 24,
"raw": "1",
"start": 23,
"type": "Literal",
"type": "Literal",
"value": 1.0
},
"end": 24,
"start": 16,
"type": "ReturnStatement",
"type": "ReturnStatement"
}
],
"end": 26,
"start": 14
},
"end": 26,
"params": [
{
"type": "Parameter",
"identifier": {
"end": 9,
"name": "x",
"start": 8,
"type": "Identifier"
},
"labeled": false
},
{
"type": "Parameter",
"identifier": {
"end": 12,
"name": "y",
"start": 11,
"type": "Identifier"
}
}
],
"start": 6,
"type": "FunctionExpression",
"type": "FunctionExpression"
},
"start": 3,
"type": "VariableDeclarator"
},
"end": 26,
"kind": "fn",
"start": 0,
"type": "VariableDeclaration",
"type": "VariableDeclaration"
}
],
"end": 26,
"start": 0
}

View File

@ -1481,3 +1481,24 @@ mod kittycad_svg {
super::execute(TEST_NAME, true).await
}
}
mod kw_fn {
const TEST_NAME: &str = "kw_fn";
/// Test parsing KCL.
#[test]
fn parse() {
super::parse(TEST_NAME)
}
/// Test that parsing and unparsing KCL produces the original KCL input.
#[test]
fn unparse() {
super::unparse(TEST_NAME)
}
/// Test that KCL is executed correctly.
#[tokio::test(flavor = "multi_thread")]
async fn kcl_test_execute() {
super::execute(TEST_NAME, true).await
}
}

View File

@ -659,7 +659,11 @@ impl FunctionExpression {
impl Parameter {
pub fn recast(&self, options: &FormatOptions, indentation_level: usize) -> String {
let mut result = self.identifier.name.clone();
let mut result = format!(
"{}{}",
if self.labeled { "" } else { "@" },
self.identifier.name.clone()
);
if let Some(ty) = &self.type_ {
result += ": ";
result += &ty.recast(options, indentation_level);

View File

@ -0,0 +1,138 @@
---
source: kcl/src/simulation_tests.rs
description: Result of parsing kw_fn.kcl
snapshot_kind: text
---
{
"Ok": {
"body": [
{
"declaration": {
"end": 35,
"id": {
"end": 12,
"name": "increment",
"start": 3,
"type": "Identifier"
},
"init": {
"body": {
"body": [
{
"argument": {
"end": 33,
"left": {
"end": 29,
"name": "x",
"start": 28,
"type": "Identifier",
"type": "Identifier"
},
"operator": "+",
"right": {
"end": 33,
"raw": "1",
"start": 32,
"type": "Literal",
"type": "Literal",
"value": 1.0
},
"start": 28,
"type": "BinaryExpression",
"type": "BinaryExpression"
},
"end": 33,
"start": 21,
"type": "ReturnStatement",
"type": "ReturnStatement"
}
],
"end": 35,
"start": 17
},
"end": 35,
"params": [
{
"type": "Parameter",
"identifier": {
"end": 15,
"name": "x",
"start": 14,
"type": "Identifier"
},
"labeled": false
}
],
"start": 12,
"type": "FunctionExpression",
"type": "FunctionExpression"
},
"start": 3,
"type": "VariableDeclarator"
},
"end": 35,
"kind": "fn",
"start": 0,
"type": "VariableDeclaration",
"type": "VariableDeclaration"
},
{
"declaration": {
"end": 55,
"id": {
"end": 40,
"name": "two",
"start": 37,
"type": "Identifier"
},
"init": {
"arguments": [
{
"end": 54,
"raw": "1",
"start": 53,
"type": "Literal",
"type": "Literal",
"value": 1.0
}
],
"callee": {
"end": 52,
"name": "increment",
"start": 43,
"type": "Identifier"
},
"end": 55,
"start": 43,
"type": "CallExpression",
"type": "CallExpression"
},
"start": 37,
"type": "VariableDeclarator"
},
"end": 55,
"kind": "const",
"start": 37,
"type": "VariableDeclaration",
"type": "VariableDeclaration"
}
],
"end": 56,
"nonCodeMeta": {
"nonCodeNodes": {
"0": [
{
"end": 37,
"start": 35,
"type": "NonCodeNode",
"value": {
"type": "newLine"
}
}
]
},
"startNodes": []
},
"start": 0
}
}

View File

@ -0,0 +1,5 @@
fn increment(@x) {
return x + 1
}
two = increment(1)

View File

@ -0,0 +1,150 @@
---
source: kcl/src/simulation_tests.rs
description: Program memory after executing kw_fn.kcl
snapshot_kind: text
---
{
"environments": [
{
"bindings": {
"HALF_TURN": {
"type": "Number",
"value": 180.0,
"__meta": []
},
"QUARTER_TURN": {
"type": "Number",
"value": 90.0,
"__meta": []
},
"THREE_QUARTER_TURN": {
"type": "Number",
"value": 270.0,
"__meta": []
},
"ZERO": {
"type": "Number",
"value": 0.0,
"__meta": []
},
"increment": {
"type": "Function",
"expression": {
"body": {
"body": [
{
"argument": {
"end": 33,
"left": {
"end": 29,
"name": "x",
"start": 28,
"type": "Identifier",
"type": "Identifier"
},
"operator": "+",
"right": {
"end": 33,
"raw": "1",
"start": 32,
"type": "Literal",
"type": "Literal",
"value": 1.0
},
"start": 28,
"type": "BinaryExpression",
"type": "BinaryExpression"
},
"end": 33,
"start": 21,
"type": "ReturnStatement",
"type": "ReturnStatement"
}
],
"end": 35,
"start": 17
},
"end": 35,
"params": [
{
"type": "Parameter",
"identifier": {
"end": 15,
"name": "x",
"start": 14,
"type": "Identifier"
},
"labeled": false
}
],
"start": 12,
"type": "FunctionExpression"
},
"memory": {
"environments": [
{
"bindings": {
"HALF_TURN": {
"type": "Number",
"value": 180.0,
"__meta": []
},
"QUARTER_TURN": {
"type": "Number",
"value": 90.0,
"__meta": []
},
"THREE_QUARTER_TURN": {
"type": "Number",
"value": 270.0,
"__meta": []
},
"ZERO": {
"type": "Number",
"value": 0.0,
"__meta": []
}
},
"parent": null
}
],
"currentEnv": 0,
"return": null
},
"__meta": [
{
"sourceRange": [
12,
35,
0
]
}
]
},
"two": {
"type": "Number",
"value": 2.0,
"__meta": [
{
"sourceRange": [
53,
54,
0
]
},
{
"sourceRange": [
32,
33,
0
]
}
]
}
},
"parent": null
}
],
"currentEnv": 0,
"return": null
}