diff --git a/docs/kcl/std.json b/docs/kcl/std.json index d43fdbe01..1739e33af 100644 --- a/docs/kcl/std.json +++ b/docs/kcl/std.json @@ -82690,6 +82690,58 @@ }, "BodyItem": { "oneOf": [ + { + "type": "object", + "required": [ + "end", + "items", + "path", + "raw_path", + "start", + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "ImportStatement" + ] + }, + "start": { + "type": "integer", + "format": "uint", + "minimum": 0.0 + }, + "end": { + "type": "integer", + "format": "uint", + "minimum": 0.0 + }, + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ImportItem" + } + }, + "path": { + "type": "string" + }, + "raw_path": { + "type": "string" + }, + "digest": { + "type": "array", + "items": { + "type": "integer", + "format": "uint8", + "minimum": 0.0 + }, + "maxItems": 32, + "minItems": 32, + "nullable": true + } + } + }, { "type": "object", "required": [ @@ -82763,6 +82815,9 @@ "$ref": "#/components/schemas/VariableDeclarator" } }, + "visibility": { + "$ref": "#/components/schemas/ItemVisibility" + }, "kind": { "$ref": "#/components/schemas/VariableKind" }, @@ -82822,6 +82877,54 @@ } ] }, + "ImportItem": { + "type": "object", + "required": [ + "end", + "name", + "start" + ], + "properties": { + "name": { + "description": "Name of the item to import.", + "allOf": [ + { + "$ref": "#/components/schemas/Identifier" + } + ] + }, + "alias": { + "description": "Rename the item using an identifier after \"as\".", + "allOf": [ + { + "$ref": "#/components/schemas/Identifier" + } + ], + "nullable": true + }, + "start": { + "type": "integer", + "format": "uint", + "minimum": 0.0 + }, + "end": { + "type": "integer", + "format": "uint", + "minimum": 0.0 + }, + "digest": { + "type": "array", + "items": { + "type": "integer", + "format": "uint8", + "minimum": 0.0 + }, + "maxItems": 32, + "minItems": 32, + "nullable": true + } + } + }, "Expr": { "description": "An expression can be evaluated to yield a single KCL value.", "oneOf": [ @@ -84475,6 +84578,13 @@ } } }, + "ItemVisibility": { + "type": "string", + "enum": [ + "default", + "export" + ] + }, "VariableKind": { "oneOf": [ { @@ -86338,6 +86448,58 @@ }, "BodyItem": { "oneOf": [ + { + "type": "object", + "required": [ + "end", + "items", + "path", + "raw_path", + "start", + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "ImportStatement" + ] + }, + "start": { + "type": "integer", + "format": "uint", + "minimum": 0.0 + }, + "end": { + "type": "integer", + "format": "uint", + "minimum": 0.0 + }, + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ImportItem" + } + }, + "path": { + "type": "string" + }, + "raw_path": { + "type": "string" + }, + "digest": { + "type": "array", + "items": { + "type": "integer", + "format": "uint8", + "minimum": 0.0 + }, + "maxItems": 32, + "minItems": 32, + "nullable": true + } + } + }, { "type": "object", "required": [ @@ -86411,6 +86573,9 @@ "$ref": "#/components/schemas/VariableDeclarator" } }, + "visibility": { + "$ref": "#/components/schemas/ItemVisibility" + }, "kind": { "$ref": "#/components/schemas/VariableKind" }, @@ -86470,6 +86635,54 @@ } ] }, + "ImportItem": { + "type": "object", + "required": [ + "end", + "name", + "start" + ], + "properties": { + "name": { + "description": "Name of the item to import.", + "allOf": [ + { + "$ref": "#/components/schemas/Identifier" + } + ] + }, + "alias": { + "description": "Rename the item using an identifier after \"as\".", + "allOf": [ + { + "$ref": "#/components/schemas/Identifier" + } + ], + "nullable": true + }, + "start": { + "type": "integer", + "format": "uint", + "minimum": 0.0 + }, + "end": { + "type": "integer", + "format": "uint", + "minimum": 0.0 + }, + "digest": { + "type": "array", + "items": { + "type": "integer", + "format": "uint8", + "minimum": 0.0 + }, + "maxItems": 32, + "minItems": 32, + "nullable": true + } + } + }, "Expr": { "description": "An expression can be evaluated to yield a single KCL value.", "oneOf": [ @@ -88123,6 +88336,13 @@ } } }, + "ItemVisibility": { + "type": "string", + "enum": [ + "default", + "export" + ] + }, "VariableKind": { "oneOf": [ { @@ -89990,6 +90210,58 @@ }, "BodyItem": { "oneOf": [ + { + "type": "object", + "required": [ + "end", + "items", + "path", + "raw_path", + "start", + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "ImportStatement" + ] + }, + "start": { + "type": "integer", + "format": "uint", + "minimum": 0.0 + }, + "end": { + "type": "integer", + "format": "uint", + "minimum": 0.0 + }, + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ImportItem" + } + }, + "path": { + "type": "string" + }, + "raw_path": { + "type": "string" + }, + "digest": { + "type": "array", + "items": { + "type": "integer", + "format": "uint8", + "minimum": 0.0 + }, + "maxItems": 32, + "minItems": 32, + "nullable": true + } + } + }, { "type": "object", "required": [ @@ -90063,6 +90335,9 @@ "$ref": "#/components/schemas/VariableDeclarator" } }, + "visibility": { + "$ref": "#/components/schemas/ItemVisibility" + }, "kind": { "$ref": "#/components/schemas/VariableKind" }, @@ -90122,6 +90397,54 @@ } ] }, + "ImportItem": { + "type": "object", + "required": [ + "end", + "name", + "start" + ], + "properties": { + "name": { + "description": "Name of the item to import.", + "allOf": [ + { + "$ref": "#/components/schemas/Identifier" + } + ] + }, + "alias": { + "description": "Rename the item using an identifier after \"as\".", + "allOf": [ + { + "$ref": "#/components/schemas/Identifier" + } + ], + "nullable": true + }, + "start": { + "type": "integer", + "format": "uint", + "minimum": 0.0 + }, + "end": { + "type": "integer", + "format": "uint", + "minimum": 0.0 + }, + "digest": { + "type": "array", + "items": { + "type": "integer", + "format": "uint8", + "minimum": 0.0 + }, + "maxItems": 32, + "minItems": 32, + "nullable": true + } + } + }, "Expr": { "description": "An expression can be evaluated to yield a single KCL value.", "oneOf": [ @@ -91775,6 +92098,13 @@ } } }, + "ItemVisibility": { + "type": "string", + "enum": [ + "default", + "export" + ] + }, "VariableKind": { "oneOf": [ { @@ -114494,6 +114824,58 @@ }, "BodyItem": { "oneOf": [ + { + "type": "object", + "required": [ + "end", + "items", + "path", + "raw_path", + "start", + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "ImportStatement" + ] + }, + "start": { + "type": "integer", + "format": "uint", + "minimum": 0.0 + }, + "end": { + "type": "integer", + "format": "uint", + "minimum": 0.0 + }, + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ImportItem" + } + }, + "path": { + "type": "string" + }, + "raw_path": { + "type": "string" + }, + "digest": { + "type": "array", + "items": { + "type": "integer", + "format": "uint8", + "minimum": 0.0 + }, + "maxItems": 32, + "minItems": 32, + "nullable": true + } + } + }, { "type": "object", "required": [ @@ -114567,6 +114949,9 @@ "$ref": "#/components/schemas/VariableDeclarator" } }, + "visibility": { + "$ref": "#/components/schemas/ItemVisibility" + }, "kind": { "$ref": "#/components/schemas/VariableKind" }, @@ -114626,6 +115011,54 @@ } ] }, + "ImportItem": { + "type": "object", + "required": [ + "end", + "name", + "start" + ], + "properties": { + "name": { + "description": "Name of the item to import.", + "allOf": [ + { + "$ref": "#/components/schemas/Identifier" + } + ] + }, + "alias": { + "description": "Rename the item using an identifier after \"as\".", + "allOf": [ + { + "$ref": "#/components/schemas/Identifier" + } + ], + "nullable": true + }, + "start": { + "type": "integer", + "format": "uint", + "minimum": 0.0 + }, + "end": { + "type": "integer", + "format": "uint", + "minimum": 0.0 + }, + "digest": { + "type": "array", + "items": { + "type": "integer", + "format": "uint8", + "minimum": 0.0 + }, + "maxItems": 32, + "minItems": 32, + "nullable": true + } + } + }, "Expr": { "description": "An expression can be evaluated to yield a single KCL value.", "oneOf": [ @@ -116279,6 +116712,13 @@ } } }, + "ItemVisibility": { + "type": "string", + "enum": [ + "default", + "export" + ] + }, "VariableKind": { "oneOf": [ { @@ -118535,6 +118975,58 @@ }, "BodyItem": { "oneOf": [ + { + "type": "object", + "required": [ + "end", + "items", + "path", + "raw_path", + "start", + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "ImportStatement" + ] + }, + "start": { + "type": "integer", + "format": "uint", + "minimum": 0.0 + }, + "end": { + "type": "integer", + "format": "uint", + "minimum": 0.0 + }, + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ImportItem" + } + }, + "path": { + "type": "string" + }, + "raw_path": { + "type": "string" + }, + "digest": { + "type": "array", + "items": { + "type": "integer", + "format": "uint8", + "minimum": 0.0 + }, + "maxItems": 32, + "minItems": 32, + "nullable": true + } + } + }, { "type": "object", "required": [ @@ -118608,6 +119100,9 @@ "$ref": "#/components/schemas/VariableDeclarator" } }, + "visibility": { + "$ref": "#/components/schemas/ItemVisibility" + }, "kind": { "$ref": "#/components/schemas/VariableKind" }, @@ -118667,6 +119162,54 @@ } ] }, + "ImportItem": { + "type": "object", + "required": [ + "end", + "name", + "start" + ], + "properties": { + "name": { + "description": "Name of the item to import.", + "allOf": [ + { + "$ref": "#/components/schemas/Identifier" + } + ] + }, + "alias": { + "description": "Rename the item using an identifier after \"as\".", + "allOf": [ + { + "$ref": "#/components/schemas/Identifier" + } + ], + "nullable": true + }, + "start": { + "type": "integer", + "format": "uint", + "minimum": 0.0 + }, + "end": { + "type": "integer", + "format": "uint", + "minimum": 0.0 + }, + "digest": { + "type": "array", + "items": { + "type": "integer", + "format": "uint8", + "minimum": 0.0 + }, + "maxItems": 32, + "minItems": 32, + "nullable": true + } + } + }, "Expr": { "description": "An expression can be evaluated to yield a single KCL value.", "oneOf": [ @@ -120320,6 +120863,13 @@ } } }, + "ItemVisibility": { + "type": "string", + "enum": [ + "default", + "export" + ] + }, "VariableKind": { "oneOf": [ { @@ -122183,6 +122733,58 @@ }, "BodyItem": { "oneOf": [ + { + "type": "object", + "required": [ + "end", + "items", + "path", + "raw_path", + "start", + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "ImportStatement" + ] + }, + "start": { + "type": "integer", + "format": "uint", + "minimum": 0.0 + }, + "end": { + "type": "integer", + "format": "uint", + "minimum": 0.0 + }, + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ImportItem" + } + }, + "path": { + "type": "string" + }, + "raw_path": { + "type": "string" + }, + "digest": { + "type": "array", + "items": { + "type": "integer", + "format": "uint8", + "minimum": 0.0 + }, + "maxItems": 32, + "minItems": 32, + "nullable": true + } + } + }, { "type": "object", "required": [ @@ -122256,6 +122858,9 @@ "$ref": "#/components/schemas/VariableDeclarator" } }, + "visibility": { + "$ref": "#/components/schemas/ItemVisibility" + }, "kind": { "$ref": "#/components/schemas/VariableKind" }, @@ -122315,6 +122920,54 @@ } ] }, + "ImportItem": { + "type": "object", + "required": [ + "end", + "name", + "start" + ], + "properties": { + "name": { + "description": "Name of the item to import.", + "allOf": [ + { + "$ref": "#/components/schemas/Identifier" + } + ] + }, + "alias": { + "description": "Rename the item using an identifier after \"as\".", + "allOf": [ + { + "$ref": "#/components/schemas/Identifier" + } + ], + "nullable": true + }, + "start": { + "type": "integer", + "format": "uint", + "minimum": 0.0 + }, + "end": { + "type": "integer", + "format": "uint", + "minimum": 0.0 + }, + "digest": { + "type": "array", + "items": { + "type": "integer", + "format": "uint8", + "minimum": 0.0 + }, + "maxItems": 32, + "minItems": 32, + "nullable": true + } + } + }, "Expr": { "description": "An expression can be evaluated to yield a single KCL value.", "oneOf": [ @@ -123968,6 +124621,13 @@ } } }, + "ItemVisibility": { + "type": "string", + "enum": [ + "default", + "export" + ] + }, "VariableKind": { "oneOf": [ { @@ -125829,6 +126489,58 @@ }, "BodyItem": { "oneOf": [ + { + "type": "object", + "required": [ + "end", + "items", + "path", + "raw_path", + "start", + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "ImportStatement" + ] + }, + "start": { + "type": "integer", + "format": "uint", + "minimum": 0.0 + }, + "end": { + "type": "integer", + "format": "uint", + "minimum": 0.0 + }, + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ImportItem" + } + }, + "path": { + "type": "string" + }, + "raw_path": { + "type": "string" + }, + "digest": { + "type": "array", + "items": { + "type": "integer", + "format": "uint8", + "minimum": 0.0 + }, + "maxItems": 32, + "minItems": 32, + "nullable": true + } + } + }, { "type": "object", "required": [ @@ -125902,6 +126614,9 @@ "$ref": "#/components/schemas/VariableDeclarator" } }, + "visibility": { + "$ref": "#/components/schemas/ItemVisibility" + }, "kind": { "$ref": "#/components/schemas/VariableKind" }, @@ -125961,6 +126676,54 @@ } ] }, + "ImportItem": { + "type": "object", + "required": [ + "end", + "name", + "start" + ], + "properties": { + "name": { + "description": "Name of the item to import.", + "allOf": [ + { + "$ref": "#/components/schemas/Identifier" + } + ] + }, + "alias": { + "description": "Rename the item using an identifier after \"as\".", + "allOf": [ + { + "$ref": "#/components/schemas/Identifier" + } + ], + "nullable": true + }, + "start": { + "type": "integer", + "format": "uint", + "minimum": 0.0 + }, + "end": { + "type": "integer", + "format": "uint", + "minimum": 0.0 + }, + "digest": { + "type": "array", + "items": { + "type": "integer", + "format": "uint8", + "minimum": 0.0 + }, + "maxItems": 32, + "minItems": 32, + "nullable": true + } + } + }, "Expr": { "description": "An expression can be evaluated to yield a single KCL value.", "oneOf": [ @@ -127614,6 +128377,13 @@ } } }, + "ItemVisibility": { + "type": "string", + "enum": [ + "default", + "export" + ] + }, "VariableKind": { "oneOf": [ { diff --git a/docs/kcl/types/BodyItem.md b/docs/kcl/types/BodyItem.md index 5fab05509..38982b74a 100644 --- a/docs/kcl/types/BodyItem.md +++ b/docs/kcl/types/BodyItem.md @@ -18,6 +18,27 @@ layout: manual +## Properties + +| Property | Type | Description | Required | +|----------|------|-------------|----------| +| `type` |enum: `ImportStatement`| | No | +| `start` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No | +| `end` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No | +| `items` |`[` [`ImportItem`](/docs/kcl/types/ImportItem) `]`| | No | +| `path` |`string`| | No | +| `raw_path` |`string`| | No | +| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No | + + +---- + +**Type:** `object` + + + + + ## Properties | Property | Type | Description | Required | @@ -45,6 +66,7 @@ layout: manual | `start` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No | | `end` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No | | `declarations` |`[` [`VariableDeclarator`](/docs/kcl/types/VariableDeclarator) `]`| | No | +| `visibility` |[`ItemVisibility`](/docs/kcl/types/ItemVisibility)| | No | | `kind` |[`VariableKind`](/docs/kcl/types/VariableKind)| | No | | `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No | diff --git a/docs/kcl/types/ImportItem.md b/docs/kcl/types/ImportItem.md new file mode 100644 index 000000000..393fe9fd9 --- /dev/null +++ b/docs/kcl/types/ImportItem.md @@ -0,0 +1,24 @@ +--- +title: "ImportItem" +excerpt: "" +layout: manual +--- + + +**Type:** `object` + + + + + +## Properties + +| Property | Type | Description | Required | +|----------|------|-------------|----------| +| `name` |[`Identifier`](/docs/kcl/types/Identifier)| Name of the item to import. | No | +| `alias` |[`Identifier`](/docs/kcl/types/Identifier)| Rename the item using an identifier after "as". | No | +| `start` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No | +| `end` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No | +| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No | + + diff --git a/docs/kcl/types/ItemVisibility.md b/docs/kcl/types/ItemVisibility.md new file mode 100644 index 000000000..a24f9aa59 --- /dev/null +++ b/docs/kcl/types/ItemVisibility.md @@ -0,0 +1,16 @@ +--- +title: "ItemVisibility" +excerpt: "" +layout: manual +--- + + +**enum:** `default`, `export` + + + + + + + + diff --git a/src/editor/plugins/lsp/kcl/highlight.ts b/src/editor/plugins/lsp/kcl/highlight.ts index 54dab6966..5a3c2ee9f 100644 --- a/src/editor/plugins/lsp/kcl/highlight.ts +++ b/src/editor/plugins/lsp/kcl/highlight.ts @@ -1,6 +1,9 @@ import { styleTags, tags as t } from '@lezer/highlight' export const kclHighlight = styleTags({ + 'import export': t.moduleKeyword, + ImportItemAs: t.definitionKeyword, + ImportFrom: t.moduleKeyword, 'fn var let const': t.definitionKeyword, 'if else': t.controlKeyword, return: t.controlKeyword, diff --git a/src/editor/plugins/lsp/kcl/kcl.grammar b/src/editor/plugins/lsp/kcl/kcl.grammar index 3de93e787..2434eab24 100644 --- a/src/editor/plugins/lsp/kcl/kcl.grammar +++ b/src/editor/plugins/lsp/kcl/kcl.grammar @@ -15,8 +15,9 @@ } statement[@isGroup=Statement] { - FunctionDeclaration { kw<"fn"> VariableDefinition Equals ParamList Arrow Body } | - VariableDeclaration { (kw<"var"> | kw<"let"> | kw<"const">)? VariableDefinition Equals expression } | + ImportStatement { kw<"import"> ImportItems ImportFrom String } | + FunctionDeclaration { kw<"export">? kw<"fn"> VariableDefinition Equals ParamList Arrow Body } | + VariableDeclaration { kw<"export">? (kw<"var"> | kw<"let"> | kw<"const">)? VariableDefinition Equals expression } | ReturnStatement { kw<"return"> expression } | ExpressionStatement { expression } } @@ -25,6 +26,9 @@ ParamList { "(" commaSep ")" Body { "{" statement* "}" } +ImportItems { commaSep1NoTrailingComma } +ImportItem { identifier (ImportItemAs identifier)? } + expression[@isGroup=Expression] { String | Number | @@ -74,6 +78,8 @@ kw { @specialize[@name={term}] } commaSep { (term ("," term)*)? ","? } +commaSep1NoTrailingComma { term ("," term)* } + @tokens { String[isolate] { "'" ("\\" _ | !['\\])* "'" | '"' ("\\" _ | !["\\])* '"' } @@ -106,6 +112,9 @@ commaSep { (term ("," term)*)? ","? } Shebang { "#!" ![\n]* } + ImportItemAs { "as" } + ImportFrom { "from" } + "(" ")" "{" "}" "[" "]" diff --git a/src/lang/modifyAst.ts b/src/lang/modifyAst.ts index 9ad67056b..8c1218056 100644 --- a/src/lang/modifyAst.ts +++ b/src/lang/modifyAst.ts @@ -501,6 +501,7 @@ export function sketchOnExtrudedFace( createIdentifier(extrudeName ? extrudeName : oldSketchName), _tag, ]), + undefined, 'const' ) @@ -682,6 +683,7 @@ export function createPipeExpression( export function createVariableDeclaration( varName: string, init: VariableDeclarator['init'], + visibility: VariableDeclaration['visibility'] = 'default', kind: VariableDeclaration['kind'] = 'const' ): VariableDeclaration { return { @@ -699,6 +701,7 @@ export function createVariableDeclaration( init, }, ], + visibility, kind, } } diff --git a/src/lang/queryAst.ts b/src/lang/queryAst.ts index 6783e4c63..73c655fe4 100644 --- a/src/lang/queryAst.ts +++ b/src/lang/queryAst.ts @@ -28,6 +28,7 @@ import { getConstraintType, } from './std/sketchcombos' import { err } from 'lib/trap' +import { ImportStatement } from 'wasm-lib/kcl/bindings/ImportStatement' /** * Retrieves a node from a given path within a Program node structure, optionally stopping at a specified node type. @@ -120,7 +121,12 @@ export function getNodeFromPathCurry( } function moreNodePathFromSourceRange( - node: Expr | ExpressionStatement | VariableDeclaration | ReturnStatement, + node: + | Expr + | ImportStatement + | ExpressionStatement + | VariableDeclaration + | ReturnStatement, sourceRange: Selection['range'], previousPath: PathToNode = [['body', '']] ): PathToNode { diff --git a/src/lang/wasm.ts b/src/lang/wasm.ts index f3140cdf3..705c9c555 100644 --- a/src/lang/wasm.ts +++ b/src/lang/wasm.ts @@ -426,6 +426,7 @@ export const _executor = async ( baseUnit, engineCommandManager, fileSystemManager, + undefined, isMock ) return execStateFromRaw(execState) diff --git a/src/wasm-lib/derive-docs/src/lib.rs b/src/wasm-lib/derive-docs/src/lib.rs index 590fa5181..8281ed24b 100644 --- a/src/wasm-lib/derive-docs/src/lib.rs +++ b/src/wasm-lib/derive-docs/src/lib.rs @@ -762,7 +762,7 @@ fn generate_code_block_test(fn_name: &str, code_block: &str, index: usize) -> pr context_type: crate::executor::ContextType::Mock, }; - ctx.run(&program, None, id_generator).await.unwrap(); + ctx.run(&program, None, id_generator, None).await.unwrap(); } #[tokio::test(flavor = "multi_thread", worker_threads = 5)] diff --git a/src/wasm-lib/derive-docs/tests/args_with_lifetime.gen b/src/wasm-lib/derive-docs/tests/args_with_lifetime.gen index d3cdaa7d5..a826f845a 100644 --- a/src/wasm-lib/derive-docs/tests/args_with_lifetime.gen +++ b/src/wasm-lib/derive-docs/tests/args_with_lifetime.gen @@ -17,7 +17,7 @@ mod test_examples_someFn { settings: Default::default(), context_type: crate::executor::ContextType::Mock, }; - ctx.run(&program, None, id_generator).await.unwrap(); + ctx.run(&program, None, id_generator, None).await.unwrap(); } #[tokio::test(flavor = "multi_thread", worker_threads = 5)] diff --git a/src/wasm-lib/derive-docs/tests/args_with_refs.gen b/src/wasm-lib/derive-docs/tests/args_with_refs.gen index 7a5013323..f75f6dff1 100644 --- a/src/wasm-lib/derive-docs/tests/args_with_refs.gen +++ b/src/wasm-lib/derive-docs/tests/args_with_refs.gen @@ -17,7 +17,7 @@ mod test_examples_someFn { settings: Default::default(), context_type: crate::executor::ContextType::Mock, }; - ctx.run(&program, None, id_generator).await.unwrap(); + ctx.run(&program, None, id_generator, None).await.unwrap(); } #[tokio::test(flavor = "multi_thread", worker_threads = 5)] diff --git a/src/wasm-lib/derive-docs/tests/array.gen b/src/wasm-lib/derive-docs/tests/array.gen index 21c2e4b18..d9dec25e8 100644 --- a/src/wasm-lib/derive-docs/tests/array.gen +++ b/src/wasm-lib/derive-docs/tests/array.gen @@ -17,7 +17,7 @@ mod test_examples_show { settings: Default::default(), context_type: crate::executor::ContextType::Mock, }; - ctx.run(&program, None, id_generator).await.unwrap(); + ctx.run(&program, None, id_generator, None).await.unwrap(); } #[tokio::test(flavor = "multi_thread", worker_threads = 5)] @@ -51,7 +51,7 @@ mod test_examples_show { settings: Default::default(), context_type: crate::executor::ContextType::Mock, }; - ctx.run(&program, None, id_generator).await.unwrap(); + ctx.run(&program, None, id_generator, None).await.unwrap(); } #[tokio::test(flavor = "multi_thread", worker_threads = 5)] diff --git a/src/wasm-lib/derive-docs/tests/box.gen b/src/wasm-lib/derive-docs/tests/box.gen index 18a2ac2af..1ec101210 100644 --- a/src/wasm-lib/derive-docs/tests/box.gen +++ b/src/wasm-lib/derive-docs/tests/box.gen @@ -17,7 +17,7 @@ mod test_examples_show { settings: Default::default(), context_type: crate::executor::ContextType::Mock, }; - ctx.run(&program, None, id_generator).await.unwrap(); + ctx.run(&program, None, id_generator, None).await.unwrap(); } #[tokio::test(flavor = "multi_thread", worker_threads = 5)] diff --git a/src/wasm-lib/derive-docs/tests/doc_comment_with_code.gen b/src/wasm-lib/derive-docs/tests/doc_comment_with_code.gen index 7c98fdfb5..03bf0f224 100644 --- a/src/wasm-lib/derive-docs/tests/doc_comment_with_code.gen +++ b/src/wasm-lib/derive-docs/tests/doc_comment_with_code.gen @@ -18,7 +18,7 @@ mod test_examples_my_func { settings: Default::default(), context_type: crate::executor::ContextType::Mock, }; - ctx.run(&program, None, id_generator).await.unwrap(); + ctx.run(&program, None, id_generator, None).await.unwrap(); } #[tokio::test(flavor = "multi_thread", worker_threads = 5)] @@ -52,7 +52,7 @@ mod test_examples_my_func { settings: Default::default(), context_type: crate::executor::ContextType::Mock, }; - ctx.run(&program, None, id_generator).await.unwrap(); + ctx.run(&program, None, id_generator, None).await.unwrap(); } #[tokio::test(flavor = "multi_thread", worker_threads = 5)] diff --git a/src/wasm-lib/derive-docs/tests/lineTo.gen b/src/wasm-lib/derive-docs/tests/lineTo.gen index d46cd45c0..c699a8a49 100644 --- a/src/wasm-lib/derive-docs/tests/lineTo.gen +++ b/src/wasm-lib/derive-docs/tests/lineTo.gen @@ -18,7 +18,7 @@ mod test_examples_line_to { settings: Default::default(), context_type: crate::executor::ContextType::Mock, }; - ctx.run(&program, None, id_generator).await.unwrap(); + ctx.run(&program, None, id_generator, None).await.unwrap(); } #[tokio::test(flavor = "multi_thread", worker_threads = 5)] @@ -52,7 +52,7 @@ mod test_examples_line_to { settings: Default::default(), context_type: crate::executor::ContextType::Mock, }; - ctx.run(&program, None, id_generator).await.unwrap(); + ctx.run(&program, None, id_generator, None).await.unwrap(); } #[tokio::test(flavor = "multi_thread", worker_threads = 5)] diff --git a/src/wasm-lib/derive-docs/tests/min.gen b/src/wasm-lib/derive-docs/tests/min.gen index dfe9681be..63c776961 100644 --- a/src/wasm-lib/derive-docs/tests/min.gen +++ b/src/wasm-lib/derive-docs/tests/min.gen @@ -17,7 +17,7 @@ mod test_examples_min { settings: Default::default(), context_type: crate::executor::ContextType::Mock, }; - ctx.run(&program, None, id_generator).await.unwrap(); + ctx.run(&program, None, id_generator, None).await.unwrap(); } #[tokio::test(flavor = "multi_thread", worker_threads = 5)] @@ -51,7 +51,7 @@ mod test_examples_min { settings: Default::default(), context_type: crate::executor::ContextType::Mock, }; - ctx.run(&program, None, id_generator).await.unwrap(); + ctx.run(&program, None, id_generator, None).await.unwrap(); } #[tokio::test(flavor = "multi_thread", worker_threads = 5)] diff --git a/src/wasm-lib/derive-docs/tests/option.gen b/src/wasm-lib/derive-docs/tests/option.gen index a6d683cac..a3624d9ee 100644 --- a/src/wasm-lib/derive-docs/tests/option.gen +++ b/src/wasm-lib/derive-docs/tests/option.gen @@ -17,7 +17,7 @@ mod test_examples_show { settings: Default::default(), context_type: crate::executor::ContextType::Mock, }; - ctx.run(&program, None, id_generator).await.unwrap(); + ctx.run(&program, None, id_generator, None).await.unwrap(); } #[tokio::test(flavor = "multi_thread", worker_threads = 5)] diff --git a/src/wasm-lib/derive-docs/tests/option_input_format.gen b/src/wasm-lib/derive-docs/tests/option_input_format.gen index 8e0b43ef2..0473f1ebe 100644 --- a/src/wasm-lib/derive-docs/tests/option_input_format.gen +++ b/src/wasm-lib/derive-docs/tests/option_input_format.gen @@ -17,7 +17,7 @@ mod test_examples_import { settings: Default::default(), context_type: crate::executor::ContextType::Mock, }; - ctx.run(&program, None, id_generator).await.unwrap(); + ctx.run(&program, None, id_generator, None).await.unwrap(); } #[tokio::test(flavor = "multi_thread", worker_threads = 5)] diff --git a/src/wasm-lib/derive-docs/tests/return_vec_box_sketch.gen b/src/wasm-lib/derive-docs/tests/return_vec_box_sketch.gen index 948b7affb..83b21aa2a 100644 --- a/src/wasm-lib/derive-docs/tests/return_vec_box_sketch.gen +++ b/src/wasm-lib/derive-docs/tests/return_vec_box_sketch.gen @@ -17,7 +17,7 @@ mod test_examples_import { settings: Default::default(), context_type: crate::executor::ContextType::Mock, }; - ctx.run(&program, None, id_generator).await.unwrap(); + ctx.run(&program, None, id_generator, None).await.unwrap(); } #[tokio::test(flavor = "multi_thread", worker_threads = 5)] diff --git a/src/wasm-lib/derive-docs/tests/return_vec_sketch.gen b/src/wasm-lib/derive-docs/tests/return_vec_sketch.gen index 173c22ee7..1f0670cd5 100644 --- a/src/wasm-lib/derive-docs/tests/return_vec_sketch.gen +++ b/src/wasm-lib/derive-docs/tests/return_vec_sketch.gen @@ -17,7 +17,7 @@ mod test_examples_import { settings: Default::default(), context_type: crate::executor::ContextType::Mock, }; - ctx.run(&program, None, id_generator).await.unwrap(); + ctx.run(&program, None, id_generator, None).await.unwrap(); } #[tokio::test(flavor = "multi_thread", worker_threads = 5)] diff --git a/src/wasm-lib/derive-docs/tests/show.gen b/src/wasm-lib/derive-docs/tests/show.gen index c899b3105..822c93c2a 100644 --- a/src/wasm-lib/derive-docs/tests/show.gen +++ b/src/wasm-lib/derive-docs/tests/show.gen @@ -17,7 +17,7 @@ mod test_examples_show { settings: Default::default(), context_type: crate::executor::ContextType::Mock, }; - ctx.run(&program, None, id_generator).await.unwrap(); + ctx.run(&program, None, id_generator, None).await.unwrap(); } #[tokio::test(flavor = "multi_thread", worker_threads = 5)] diff --git a/src/wasm-lib/derive-docs/tests/test_args_with_exec_state.gen b/src/wasm-lib/derive-docs/tests/test_args_with_exec_state.gen index 7687cb39e..87d2c4b3c 100644 --- a/src/wasm-lib/derive-docs/tests/test_args_with_exec_state.gen +++ b/src/wasm-lib/derive-docs/tests/test_args_with_exec_state.gen @@ -17,7 +17,7 @@ mod test_examples_some_function { settings: Default::default(), context_type: crate::executor::ContextType::Mock, }; - ctx.run(&program, None, id_generator).await.unwrap(); + ctx.run(&program, None, id_generator, None).await.unwrap(); } #[tokio::test(flavor = "multi_thread", worker_threads = 5)] diff --git a/src/wasm-lib/kcl-macros/tests/macro_test.rs b/src/wasm-lib/kcl-macros/tests/macro_test.rs index 41d3f12e8..f4961b73e 100644 --- a/src/wasm-lib/kcl-macros/tests/macro_test.rs +++ b/src/wasm-lib/kcl-macros/tests/macro_test.rs @@ -1,7 +1,7 @@ extern crate alloc; use kcl_lib::ast::types::{ - BodyItem, Expr, Identifier, Literal, LiteralValue, NonCodeMeta, Program, VariableDeclaration, VariableDeclarator, - VariableKind, + BodyItem, Expr, Identifier, ItemVisibility, Literal, LiteralValue, NonCodeMeta, Program, VariableDeclaration, + VariableDeclarator, VariableKind, }; use kcl_macros::parse; use pretty_assertions::assert_eq; @@ -33,6 +33,7 @@ fn basic() { })), digest: None, }], + visibility: ItemVisibility::Default, kind: VariableKind::Const, digest: None, })], diff --git a/src/wasm-lib/kcl-test-server/src/lib.rs b/src/wasm-lib/kcl-test-server/src/lib.rs index c4e55a518..2f727aa38 100644 --- a/src/wasm-lib/kcl-test-server/src/lib.rs +++ b/src/wasm-lib/kcl-test-server/src/lib.rs @@ -178,7 +178,7 @@ async fn snapshot_endpoint(body: Bytes, state: ExecutorContext) -> Response(); let timer = time_until(done_rx); - let snapshot = match state.execute_and_prepare_snapshot(&program, id_generator).await { + let snapshot = match state.execute_and_prepare_snapshot(&program, id_generator, None).await { Ok(sn) => sn, Err(e) => return kcl_err(e), }; diff --git a/src/wasm-lib/kcl-to-core/src/conn_mock_core.rs b/src/wasm-lib/kcl-to-core/src/conn_mock_core.rs index 7ad3a88c6..4d578dd5f 100644 --- a/src/wasm-lib/kcl-to-core/src/conn_mock_core.rs +++ b/src/wasm-lib/kcl-to-core/src/conn_mock_core.rs @@ -1,6 +1,7 @@ use anyhow::Result; use indexmap::IndexMap; use kcl_lib::{ + engine::ExecutionKind, errors::KclError, executor::{DefaultPlanes, IdGenerator}, }; @@ -26,6 +27,7 @@ pub struct EngineConnection { batch_end: Arc>>, core_test: Arc>, default_planes: Arc>>, + execution_kind: Arc>, } impl EngineConnection { @@ -39,6 +41,7 @@ impl EngineConnection { batch_end: Arc::new(Mutex::new(IndexMap::new())), core_test: result, default_planes: Default::default(), + execution_kind: Default::default(), }) } @@ -360,6 +363,18 @@ impl kcl_lib::engine::EngineManager for EngineConnection { self.batch_end.clone() } + fn execution_kind(&self) -> ExecutionKind { + let guard = self.execution_kind.lock().unwrap(); + *guard + } + + fn replace_execution_kind(&self, execution_kind: ExecutionKind) -> ExecutionKind { + let mut guard = self.execution_kind.lock().unwrap(); + let original = *guard; + *guard = execution_kind; + original + } + async fn default_planes( &self, id_generator: &mut IdGenerator, diff --git a/src/wasm-lib/kcl-to-core/src/lib.rs b/src/wasm-lib/kcl-to-core/src/lib.rs index b2e6ed699..ce007d177 100644 --- a/src/wasm-lib/kcl-to-core/src/lib.rs +++ b/src/wasm-lib/kcl-to-core/src/lib.rs @@ -23,7 +23,7 @@ pub async fn kcl_to_engine_core(code: &str) -> Result { settings: Default::default(), context_type: kcl_lib::executor::ContextType::MockCustomForwarded, }; - let _memory = ctx.run(&program, None, IdGenerator::default()).await?; + let _memory = ctx.run(&program, None, IdGenerator::default(), None).await?; let result = result.lock().expect("mutex lock").clone(); Ok(result) diff --git a/src/wasm-lib/kcl/src/ast/modify.rs b/src/wasm-lib/kcl/src/ast/modify.rs index 7e4f4fa3b..195296639 100644 --- a/src/wasm-lib/kcl/src/ast/modify.rs +++ b/src/wasm-lib/kcl/src/ast/modify.rs @@ -48,7 +48,10 @@ pub async fn modify_ast_for_sketch( // Get the information about the sketch. if let Some(ast_sketch) = program.get_variable(sketch_name) { - let constraint_level = ast_sketch.get_constraint_level(); + let constraint_level = match ast_sketch { + super::types::Definition::Variable(var) => var.get_constraint_level(), + super::types::Definition::Import(import) => import.get_constraint_level(), + }; match &constraint_level { ConstraintLevel::None { source_ranges: _ } => {} ConstraintLevel::Ignore { source_ranges: _ } => {} diff --git a/src/wasm-lib/kcl/src/ast/types.rs b/src/wasm-lib/kcl/src/ast/types.rs index 90f04e5a9..31c116846 100644 --- a/src/wasm-lib/kcl/src/ast/types.rs +++ b/src/wasm-lib/kcl/src/ast/types.rs @@ -40,6 +40,11 @@ mod none; /// Position-independent digest of the AST node. pub type Digest = [u8; 32]; +pub enum Definition<'a> { + Variable(&'a VariableDeclarator), + Import(&'a ImportStatement), +} + /// A KCL program top level, or function body. #[derive(Debug, Default, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, Bake)] #[databake(path = kcl_lib::ast::types)] @@ -198,6 +203,7 @@ impl Program { // Recurse over the item. match item { + BodyItem::ImportStatement(_) => None, BodyItem::ExpressionStatement(expression_statement) => Some(&expression_statement.expression), BodyItem::VariableDeclaration(variable_declaration) => variable_declaration.get_expr_for_position(pos), BodyItem::ReturnStatement(return_statement) => Some(&return_statement.argument), @@ -214,6 +220,7 @@ impl Program { // Recurse over the item. let expr = match item { + BodyItem::ImportStatement(_) => None, BodyItem::ExpressionStatement(expression_statement) => Some(&expression_statement.expression), BodyItem::VariableDeclaration(variable_declaration) => variable_declaration.get_expr_for_position(pos), BodyItem::ReturnStatement(return_statement) => Some(&return_statement.argument), @@ -257,6 +264,7 @@ impl Program { // We only care about the top level things in the program. for item in &self.body { match item { + BodyItem::ImportStatement(_) => continue, BodyItem::ExpressionStatement(expression_statement) => { if let Some(folding_range) = expression_statement.expression.get_lsp_folding_range() { ranges.push(folding_range) @@ -280,6 +288,12 @@ impl Program { let mut old_name = None; for item in &mut self.body { match item { + BodyItem::ImportStatement(stmt) => { + if let Some(var_old_name) = stmt.rename_symbol(new_name, pos) { + old_name = Some(var_old_name); + break; + } + } BodyItem::ExpressionStatement(_expression_statement) => { continue; } @@ -306,6 +320,7 @@ impl Program { // Recurse over the item. let mut value = match item { + BodyItem::ImportStatement(_) => None, // TODO BodyItem::ExpressionStatement(ref mut expression_statement) => { Some(&mut expression_statement.expression) } @@ -337,6 +352,9 @@ impl Program { fn rename_identifiers(&mut self, old_name: &str, new_name: &str) { for item in &mut self.body { match item { + BodyItem::ImportStatement(ref mut stmt) => { + stmt.rename_identifiers(old_name, new_name); + } BodyItem::ExpressionStatement(ref mut expression_statement) => { expression_statement.expression.rename_identifiers(old_name, new_name); } @@ -354,6 +372,9 @@ impl Program { pub fn replace_variable(&mut self, name: &str, declarator: VariableDeclarator) { for item in &mut self.body { match item { + BodyItem::ImportStatement(_) => { + continue; + } BodyItem::ExpressionStatement(_expression_statement) => { continue; } @@ -374,6 +395,7 @@ impl Program { pub fn replace_value(&mut self, source_range: SourceRange, new_value: Expr) { for item in &mut self.body { match item { + BodyItem::ImportStatement(_) => {} // TODO BodyItem::ExpressionStatement(ref mut expression_statement) => expression_statement .expression .replace_value(source_range, new_value.clone()), @@ -388,16 +410,23 @@ impl Program { } /// Get the variable declaration with the given name. - pub fn get_variable(&self, name: &str) -> Option<&VariableDeclarator> { + pub fn get_variable(&self, name: &str) -> Option> { for item in &self.body { match item { + BodyItem::ImportStatement(stmt) => { + for import_item in &stmt.items { + if import_item.identifier() == name { + return Some(Definition::Import(stmt.as_ref())); + } + } + } BodyItem::ExpressionStatement(_expression_statement) => { continue; } BodyItem::VariableDeclaration(variable_declaration) => { for declaration in &variable_declaration.declarations { if declaration.id.name == name { - return Some(declaration); + return Some(Definition::Variable(declaration)); } } } @@ -454,6 +483,7 @@ pub(crate) use impl_value_meta; #[ts(export)] #[serde(tag = "type")] pub enum BodyItem { + ImportStatement(Box), ExpressionStatement(ExpressionStatement), VariableDeclaration(VariableDeclaration), ReturnStatement(ReturnStatement), @@ -462,6 +492,7 @@ pub enum BodyItem { impl BodyItem { pub fn compute_digest(&mut self) -> Digest { match self { + BodyItem::ImportStatement(s) => s.compute_digest(), BodyItem::ExpressionStatement(es) => es.compute_digest(), BodyItem::VariableDeclaration(vs) => vs.compute_digest(), BodyItem::ReturnStatement(rs) => rs.compute_digest(), @@ -470,6 +501,7 @@ impl BodyItem { pub fn start(&self) -> usize { match self { + BodyItem::ImportStatement(stmt) => stmt.start(), BodyItem::ExpressionStatement(expression_statement) => expression_statement.start(), BodyItem::VariableDeclaration(variable_declaration) => variable_declaration.start(), BodyItem::ReturnStatement(return_statement) => return_statement.start(), @@ -478,6 +510,7 @@ impl BodyItem { pub fn end(&self) -> usize { match self { + BodyItem::ImportStatement(stmt) => stmt.end(), BodyItem::ExpressionStatement(expression_statement) => expression_statement.end(), BodyItem::VariableDeclaration(variable_declaration) => variable_declaration.end(), BodyItem::ReturnStatement(return_statement) => return_statement.end(), @@ -1123,6 +1156,124 @@ impl NonCodeMeta { } } +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, Bake)] +#[databake(path = kcl_lib::ast::types)] +#[ts(export)] +#[serde(tag = "type")] +pub struct ImportItem { + /// Name of the item to import. + pub name: Identifier, + /// Rename the item using an identifier after "as". + pub alias: Option, + + pub start: usize, + pub end: usize, + + pub digest: Option, +} + +impl_value_meta!(ImportItem); + +impl ImportItem { + compute_digest!(|slf, hasher| { + let name = slf.name.name.as_bytes(); + hasher.update(name.len().to_ne_bytes()); + hasher.update(name); + if let Some(alias) = &mut slf.alias { + hasher.update([1]); + hasher.update(alias.compute_digest()); + } else { + hasher.update([0]); + } + }); + + pub fn identifier(&self) -> &str { + match &self.alias { + Some(alias) => &alias.name, + None => &self.name.name, + } + } + + pub fn rename_symbol(&mut self, new_name: &str, pos: usize) -> Option { + match &mut self.alias { + Some(alias) => { + let alias_source_range = SourceRange::from(&*alias); + if !alias_source_range.contains(pos) { + return None; + } + let old_name = std::mem::replace(&mut alias.name, new_name.to_owned()); + Some(old_name) + } + None => { + let use_source_range = SourceRange::from(&*self); + if use_source_range.contains(pos) { + self.alias = Some(Identifier::new(new_name)); + } + // Return implicit name. + return Some(self.identifier().to_owned()); + } + } + } + + pub fn rename_identifiers(&mut self, old_name: &str, new_name: &str) { + if let Some(alias) = &mut self.alias { + alias.rename(old_name, new_name); + } + } +} + +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, Bake)] +#[databake(path = kcl_lib::ast::types)] +#[ts(export)] +#[serde(tag = "type")] +pub struct ImportStatement { + pub start: usize, + pub end: usize, + pub items: Vec, + pub path: String, + pub raw_path: String, + + pub digest: Option, +} + +impl_value_meta!(ImportStatement); + +impl ImportStatement { + compute_digest!(|slf, hasher| { + for item in &mut slf.items { + hasher.update(item.compute_digest()); + } + let path = slf.path.as_bytes(); + hasher.update(path.len().to_ne_bytes()); + hasher.update(path); + }); + + pub fn get_constraint_level(&self) -> ConstraintLevel { + ConstraintLevel::Full { + source_ranges: vec![self.into()], + } + } + + pub fn rename_symbol(&mut self, new_name: &str, pos: usize) -> Option { + for item in &mut self.items { + let source_range = SourceRange::from(&*item); + if source_range.contains(pos) { + let old_name = item.rename_symbol(new_name, pos); + if old_name.is_some() { + return old_name; + } + } + } + None + } + + pub fn rename_identifiers(&mut self, old_name: &str, new_name: &str) { + for item in &mut self.items { + item.rename_identifiers(old_name, new_name); + } + } +} + #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, Bake)] #[databake(path = kcl_lib::ast::types)] #[ts(export)] @@ -1284,6 +1435,32 @@ impl PartialEq for Function { } } +#[derive( + Debug, Default, Clone, Copy, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, FromStr, Display, Bake, +)] +#[databake(path = kcl_lib::ast::types)] +#[ts(export)] +#[serde(rename_all = "snake_case")] +#[display(style = "snake_case")] +pub enum ItemVisibility { + #[default] + Default, + Export, +} + +impl ItemVisibility { + fn digestable_id(&self) -> [u8; 1] { + match self { + ItemVisibility::Default => [0], + ItemVisibility::Export => [1], + } + } + + fn is_default(&self) -> bool { + matches!(self, Self::Default) + } +} + #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, Bake)] #[databake(path = kcl_lib::ast::types)] #[ts(export)] @@ -1292,6 +1469,8 @@ pub struct VariableDeclaration { pub start: usize, pub end: usize, pub declarations: Vec, + #[serde(default, skip_serializing_if = "ItemVisibility::is_default")] + pub visibility: ItemVisibility, pub kind: VariableKind, // Change to enum if there are specific values pub digest: Option, @@ -1337,14 +1516,16 @@ impl VariableDeclaration { for declarator in &mut slf.declarations { hasher.update(declarator.compute_digest()); } + hasher.update(slf.visibility.digestable_id()); hasher.update(slf.kind.digestable_id()); }); - pub fn new(declarations: Vec, kind: VariableKind) -> Self { + pub fn new(declarations: Vec, visibility: ItemVisibility, kind: VariableKind) -> Self { Self { start: 0, end: 0, declarations, + visibility, kind, digest: None, } diff --git a/src/wasm-lib/kcl/src/engine/conn.rs b/src/wasm-lib/kcl/src/engine/conn.rs index 0873dc706..3df07bade 100644 --- a/src/wasm-lib/kcl/src/engine/conn.rs +++ b/src/wasm-lib/kcl/src/engine/conn.rs @@ -24,6 +24,8 @@ use crate::{ executor::{DefaultPlanes, IdGenerator}, }; +use super::ExecutionKind; + #[derive(Debug, PartialEq)] enum SocketHealth { Active, @@ -46,6 +48,8 @@ pub struct EngineConnection { default_planes: Arc>>, /// If the server sends session data, it'll be copied to here. session_data: Arc>>, + + execution_kind: Arc>, } pub struct TcpRead { @@ -300,6 +304,7 @@ impl EngineConnection { batch_end: Arc::new(Mutex::new(IndexMap::new())), default_planes: Default::default(), session_data, + execution_kind: Default::default(), }) } } @@ -314,6 +319,18 @@ impl EngineManager for EngineConnection { self.batch_end.clone() } + fn execution_kind(&self) -> ExecutionKind { + let guard = self.execution_kind.lock().unwrap(); + *guard + } + + fn replace_execution_kind(&self, execution_kind: ExecutionKind) -> ExecutionKind { + let mut guard = self.execution_kind.lock().unwrap(); + let original = *guard; + *guard = execution_kind; + original + } + async fn default_planes( &self, id_generator: &mut IdGenerator, diff --git a/src/wasm-lib/kcl/src/engine/conn_mock.rs b/src/wasm-lib/kcl/src/engine/conn_mock.rs index 97f7ecf4c..3f9de21be 100644 --- a/src/wasm-lib/kcl/src/engine/conn_mock.rs +++ b/src/wasm-lib/kcl/src/engine/conn_mock.rs @@ -22,10 +22,13 @@ use crate::{ executor::{DefaultPlanes, IdGenerator}, }; +use super::ExecutionKind; + #[derive(Debug, Clone)] pub struct EngineConnection { batch: Arc>>, batch_end: Arc>>, + execution_kind: Arc>, } impl EngineConnection { @@ -33,6 +36,7 @@ impl EngineConnection { Ok(EngineConnection { batch: Arc::new(Mutex::new(Vec::new())), batch_end: Arc::new(Mutex::new(IndexMap::new())), + execution_kind: Default::default(), }) } } @@ -47,6 +51,18 @@ impl crate::engine::EngineManager for EngineConnection { self.batch_end.clone() } + fn execution_kind(&self) -> ExecutionKind { + let guard = self.execution_kind.lock().unwrap(); + *guard + } + + fn replace_execution_kind(&self, execution_kind: ExecutionKind) -> ExecutionKind { + let mut guard = self.execution_kind.lock().unwrap(); + let original = *guard; + *guard = execution_kind; + original + } + async fn default_planes( &self, _id_generator: &mut IdGenerator, diff --git a/src/wasm-lib/kcl/src/engine/conn_wasm.rs b/src/wasm-lib/kcl/src/engine/conn_wasm.rs index 8d6f1edaf..3ed5b4931 100644 --- a/src/wasm-lib/kcl/src/engine/conn_wasm.rs +++ b/src/wasm-lib/kcl/src/engine/conn_wasm.rs @@ -9,6 +9,7 @@ use kittycad_modeling_cmds as kcmc; use wasm_bindgen::prelude::*; use crate::{ + engine::ExecutionKind, errors::{KclError, KclErrorDetails}, executor::{DefaultPlanes, IdGenerator}, }; @@ -42,6 +43,7 @@ pub struct EngineConnection { manager: Arc, batch: Arc>>, batch_end: Arc>>, + execution_kind: Arc>, } // Safety: WebAssembly will only ever run in a single-threaded context. @@ -54,6 +56,7 @@ impl EngineConnection { manager: Arc::new(manager), batch: Arc::new(Mutex::new(Vec::new())), batch_end: Arc::new(Mutex::new(IndexMap::new())), + execution_kind: Default::default(), }) } } @@ -68,6 +71,18 @@ impl crate::engine::EngineManager for EngineConnection { self.batch_end.clone() } + fn execution_kind(&self) -> ExecutionKind { + let guard = self.execution_kind.lock().unwrap(); + *guard + } + + fn replace_execution_kind(&self, execution_kind: ExecutionKind) -> ExecutionKind { + let mut guard = self.execution_kind.lock().unwrap(); + let original = *guard; + *guard = execution_kind; + original + } + async fn default_planes( &self, _id_generator: &mut IdGenerator, diff --git a/src/wasm-lib/kcl/src/engine/mod.rs b/src/wasm-lib/kcl/src/engine/mod.rs index 792c9699b..5bb1876b1 100644 --- a/src/wasm-lib/kcl/src/engine/mod.rs +++ b/src/wasm-lib/kcl/src/engine/mod.rs @@ -41,6 +41,23 @@ lazy_static::lazy_static! { pub static ref GRID_SCALE_TEXT_OBJECT_ID: uuid::Uuid = uuid::Uuid::parse_str("10782f33-f588-4668-8bcd-040502d26590").unwrap(); } +/// The mode of execution. When isolated, like during an import, attempting to +/// send a command results in an error. +#[derive(Debug, Default, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, ts_rs::TS, JsonSchema)] +#[ts(export)] +#[serde(rename_all = "camelCase")] +pub enum ExecutionKind { + #[default] + Normal, + Isolated, +} + +impl ExecutionKind { + pub fn is_isolated(&self) -> bool { + matches!(self, ExecutionKind::Isolated) + } +} + #[async_trait::async_trait] pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static { /// Get the batch of commands to be sent to the engine. @@ -49,6 +66,13 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static { /// Get the batch of end commands to be sent to the engine. fn batch_end(&self) -> Arc>>; + /// Get the current execution kind. + fn execution_kind(&self) -> ExecutionKind; + + /// Replace the current execution kind with a new value and return the + /// existing value. + fn replace_execution_kind(&self, execution_kind: ExecutionKind) -> ExecutionKind; + /// Get the default planes. async fn default_planes( &self, @@ -102,6 +126,10 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static { source_range: crate::executor::SourceRange, cmd: &ModelingCmd, ) -> Result<(), crate::errors::KclError> { + let execution_kind = self.execution_kind(); + if execution_kind.is_isolated() { + return Err(KclError::Semantic(KclErrorDetails { message: "Cannot send modeling commands while importing. Wrap your code in a function if you want to import the file.".to_owned(), source_ranges: vec![source_range] })); + } let req = WebSocketRequest::ModelingCmdReq(ModelingCmdReq { cmd: cmd.clone(), cmd_id: id.into(), diff --git a/src/wasm-lib/kcl/src/errors.rs b/src/wasm-lib/kcl/src/errors.rs index dfc894ab1..9f8dea691 100644 --- a/src/wasm-lib/kcl/src/errors.rs +++ b/src/wasm-lib/kcl/src/errors.rs @@ -14,6 +14,8 @@ pub enum KclError { Syntax(KclErrorDetails), #[error("semantic: {0:?}")] Semantic(KclErrorDetails), + #[error("import cycle: {0:?}")] + ImportCycle(KclErrorDetails), #[error("type: {0:?}")] Type(KclErrorDetails), #[error("unimplemented: {0:?}")] @@ -52,6 +54,7 @@ impl KclError { KclError::Lexical(_) => "lexical", KclError::Syntax(_) => "syntax", KclError::Semantic(_) => "semantic", + KclError::ImportCycle(_) => "import cycle", KclError::Type(_) => "type", KclError::Unimplemented(_) => "unimplemented", KclError::Unexpected(_) => "unexpected", @@ -68,6 +71,7 @@ impl KclError { KclError::Lexical(e) => e.source_ranges.clone(), KclError::Syntax(e) => e.source_ranges.clone(), KclError::Semantic(e) => e.source_ranges.clone(), + KclError::ImportCycle(e) => e.source_ranges.clone(), KclError::Type(e) => e.source_ranges.clone(), KclError::Unimplemented(e) => e.source_ranges.clone(), KclError::Unexpected(e) => e.source_ranges.clone(), @@ -85,6 +89,7 @@ impl KclError { KclError::Lexical(e) => &e.message, KclError::Syntax(e) => &e.message, KclError::Semantic(e) => &e.message, + KclError::ImportCycle(e) => &e.message, KclError::Type(e) => &e.message, KclError::Unimplemented(e) => &e.message, KclError::Unexpected(e) => &e.message, @@ -102,6 +107,7 @@ impl KclError { KclError::Lexical(e) => e.source_ranges = source_ranges, KclError::Syntax(e) => e.source_ranges = source_ranges, KclError::Semantic(e) => e.source_ranges = source_ranges, + KclError::ImportCycle(e) => e.source_ranges = source_ranges, KclError::Type(e) => e.source_ranges = source_ranges, KclError::Unimplemented(e) => e.source_ranges = source_ranges, KclError::Unexpected(e) => e.source_ranges = source_ranges, @@ -121,6 +127,7 @@ impl KclError { KclError::Lexical(e) => e.source_ranges.extend(source_ranges), KclError::Syntax(e) => e.source_ranges.extend(source_ranges), KclError::Semantic(e) => e.source_ranges.extend(source_ranges), + KclError::ImportCycle(e) => e.source_ranges.extend(source_ranges), KclError::Type(e) => e.source_ranges.extend(source_ranges), KclError::Unimplemented(e) => e.source_ranges.extend(source_ranges), KclError::Unexpected(e) => e.source_ranges.extend(source_ranges), diff --git a/src/wasm-lib/kcl/src/executor.rs b/src/wasm-lib/kcl/src/executor.rs index fe294030f..362dee937 100644 --- a/src/wasm-lib/kcl/src/executor.rs +++ b/src/wasm-lib/kcl/src/executor.rs @@ -1,6 +1,9 @@ //! The executor for the AST. -use std::{collections::HashMap, sync::Arc}; +use std::{ + collections::{HashMap, HashSet}, + sync::Arc, +}; use anyhow::Result; use async_recursion::async_recursion; @@ -23,12 +26,12 @@ type Point3D = kcmc::shared::Point3d; use crate::{ ast::types::{ - human_friendly_type, BodyItem, Expr, ExpressionStatement, FunctionExpression, KclNone, Program, - ReturnStatement, TagDeclarator, + human_friendly_type, BodyItem, Expr, ExpressionStatement, FunctionExpression, ImportStatement, ItemVisibility, + KclNone, Program, ReturnStatement, TagDeclarator, }, - engine::EngineManager, + engine::{EngineManager, ExecutionKind}, errors::{KclError, KclErrorDetails}, - fs::FileManager, + fs::{FileManager, FileSystem}, settings::types::UnitLength, std::{FnAsArg, StdLib}, }; @@ -47,6 +50,14 @@ pub struct ExecState { /// The current value of the pipe operator returned from the previous /// expression. If we're not currently in a pipeline, this will be None. pub pipe_value: Option, + /// Identifiers that have been exported from the current module. + pub module_exports: HashSet, + /// The stack of import statements for detecting circular module imports. + /// If this is empty, we're not currently executing an import statement. + pub import_stack: Vec, + /// The directory of the current project. This is used for resolving import + /// paths. If None is given, the current working directory is used. + pub project_directory: Option, } #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)] @@ -391,6 +402,20 @@ impl KclValue { KclValue::Face(_) => "Face", } } + + pub(crate) fn is_function(&self) -> bool { + match self { + KclValue::UserVal(..) + | KclValue::TagIdentifier(..) + | KclValue::TagDeclarator(..) + | KclValue::Plane(..) + | KclValue::Face(..) + | KclValue::Solid(..) + | KclValue::Solids { .. } + | KclValue::ImportedGeometry(..) => false, + KclValue::Function { .. } => true, + } + } } impl From for KclValue { @@ -1504,6 +1529,14 @@ impl From for Metadata { } } +impl From<&ImportStatement> for Metadata { + fn from(stmt: &ImportStatement) -> Self { + Self { + source_range: SourceRange::new(stmt.start, stmt.end), + } + } +} + impl From<&ExpressionStatement> for Metadata { fn from(exp_statement: &ExpressionStatement) -> Self { Self { @@ -1967,8 +2000,9 @@ impl ExecutorContext { program: &crate::ast::types::Program, memory: Option, id_generator: IdGenerator, + project_directory: Option, ) -> Result { - self.run_with_session_data(program, memory, id_generator) + self.run_with_session_data(program, memory, id_generator, project_directory) .await .map(|x| x.0) } @@ -1980,6 +2014,7 @@ impl ExecutorContext { program: &crate::ast::types::Program, memory: Option, id_generator: IdGenerator, + project_directory: Option, ) -> Result<(ExecState, Option), KclError> { let memory = if let Some(memory) = memory { memory.clone() @@ -1989,6 +2024,7 @@ impl ExecutorContext { let mut exec_state = ExecState { memory, id_generator, + project_directory, ..Default::default() }; // Before we even start executing the program, set the units. @@ -2027,6 +2063,91 @@ impl ExecutorContext { // Iterate over the body of the program. for statement in &program.body { match statement { + BodyItem::ImportStatement(import_stmt) => { + let source_range = SourceRange::from(import_stmt); + let path = import_stmt.path.clone(); + let resolved_path = if let Some(project_dir) = &exec_state.project_directory { + std::path::PathBuf::from(project_dir).join(&path) + } else { + std::path::PathBuf::from(&path) + }; + if exec_state.import_stack.contains(&resolved_path) { + return Err(KclError::ImportCycle(KclErrorDetails { + message: format!( + "circular import of modules is not allowed: {} -> {}", + exec_state + .import_stack + .iter() + .map(|p| p.as_path().to_string_lossy()) + .collect::>() + .join(" -> "), + resolved_path.to_string_lossy() + ), + source_ranges: vec![import_stmt.into()], + })); + } + let source = self.fs.read_to_string(&resolved_path, source_range).await?; + let program = crate::parser::parse(&source)?; + let (module_memory, module_exports) = { + exec_state.import_stack.push(resolved_path.clone()); + let original_execution = self.engine.replace_execution_kind(ExecutionKind::Isolated); + let original_memory = std::mem::take(&mut exec_state.memory); + let original_exports = std::mem::take(&mut exec_state.module_exports); + let result = self + .inner_execute(&program, exec_state, crate::executor::BodyType::Root) + .await; + let module_exports = std::mem::replace(&mut exec_state.module_exports, original_exports); + let module_memory = std::mem::replace(&mut exec_state.memory, original_memory); + self.engine.replace_execution_kind(original_execution); + exec_state.import_stack.pop(); + + result.map_err(|err| { + if let KclError::ImportCycle(_) = err { + // It was an import cycle. Keep the original message. + err.override_source_ranges(vec![source_range]) + } else { + KclError::Semantic(KclErrorDetails { + message: format!( + "Error loading imported file. Open it to view more details. {path}: {}", + err.message() + ), + source_ranges: vec![source_range], + }) + } + })?; + + (module_memory, module_exports) + }; + for import_item in &import_stmt.items { + // Extract the item from the module. + let item = module_memory + .get(&import_item.name.name, import_item.into()) + .map_err(|_err| { + KclError::UndefinedValue(KclErrorDetails { + message: format!("{} is not defined in module", import_item.name.name), + source_ranges: vec![SourceRange::from(&import_item.name)], + }) + })?; + // Check that the item is allowed to be imported. + if !module_exports.contains(&import_item.name.name) { + return Err(KclError::Semantic(KclErrorDetails { + message: format!( + "Cannot import \"{}\" from module because it is not exported. Add \"export\" before the definition to export it.", + import_item.name.name + ), + source_ranges: vec![SourceRange::from(&import_item.name)], + })); + } + + // Add the item to the current module. + exec_state.memory.add( + import_item.identifier(), + item.clone(), + SourceRange::from(&import_item.name), + )?; + } + last_expr = None; + } BodyItem::ExpressionStatement(expression_statement) => { let metadata = Metadata::from(expression_statement); last_expr = Some( @@ -2053,7 +2174,21 @@ impl ExecutorContext { StatementKind::Declaration { name: &var_name }, ) .await?; + let is_function = memory_item.is_function(); exec_state.memory.add(&var_name, memory_item, source_range)?; + // Track exports. + match variable_declaration.visibility { + ItemVisibility::Export => { + if !is_function { + return Err(KclError::Semantic(KclErrorDetails { + message: "Only functions can be exported".to_owned(), + source_ranges: vec![source_range], + })); + } + exec_state.module_exports.insert(var_name); + } + ItemVisibility::Default => {} + } } last_expr = None; } @@ -2158,8 +2293,9 @@ impl ExecutorContext { &self, program: &Program, id_generator: IdGenerator, + project_directory: Option, ) -> Result { - let _ = self.run(program, None, id_generator).await?; + let _ = self.run(program, None, id_generator, project_directory).await?; // Zoom to fit. self.engine @@ -2304,7 +2440,7 @@ mod tests { settings: Default::default(), context_type: ContextType::Mock, }; - let exec_state = ctx.run(&program, None, IdGenerator::default()).await?; + let exec_state = ctx.run(&program, None, IdGenerator::default(), None).await?; Ok(exec_state.memory) } diff --git a/src/wasm-lib/kcl/src/fs/local.rs b/src/wasm-lib/kcl/src/fs/local.rs index c447ff3b9..573c16c05 100644 --- a/src/wasm-lib/kcl/src/fs/local.rs +++ b/src/wasm-lib/kcl/src/fs/local.rs @@ -37,6 +37,19 @@ impl FileSystem for FileManager { }) } + async fn read_to_string + std::marker::Send + std::marker::Sync>( + &self, + path: P, + source_range: crate::executor::SourceRange, + ) -> Result { + tokio::fs::read_to_string(&path).await.map_err(|e| { + KclError::Engine(KclErrorDetails { + message: format!("Failed to read file `{}`: {}", path.as_ref().display(), e), + source_ranges: vec![source_range], + }) + }) + } + async fn exists + std::marker::Send + std::marker::Sync>( &self, path: P, diff --git a/src/wasm-lib/kcl/src/fs/mod.rs b/src/wasm-lib/kcl/src/fs/mod.rs index b6572663d..9c9cf3d0b 100644 --- a/src/wasm-lib/kcl/src/fs/mod.rs +++ b/src/wasm-lib/kcl/src/fs/mod.rs @@ -23,6 +23,13 @@ pub trait FileSystem: Clone { source_range: crate::executor::SourceRange, ) -> Result, crate::errors::KclError>; + /// Read a file from the local file system. + async fn read_to_string + std::marker::Send + std::marker::Sync>( + &self, + path: P, + source_range: crate::executor::SourceRange, + ) -> Result; + /// Check if a file exists on the local file system. async fn exists + std::marker::Send + std::marker::Sync>( &self, diff --git a/src/wasm-lib/kcl/src/fs/wasm.rs b/src/wasm-lib/kcl/src/fs/wasm.rs index b7b4e63a1..408f29433 100644 --- a/src/wasm-lib/kcl/src/fs/wasm.rs +++ b/src/wasm-lib/kcl/src/fs/wasm.rs @@ -78,6 +78,22 @@ impl FileSystem for FileManager { Ok(bytes) } + async fn read_to_string + std::marker::Send + std::marker::Sync>( + &self, + path: P, + source_range: crate::executor::SourceRange, + ) -> Result { + let bytes = self.read(path, source_range).await?; + let string = String::from_utf8(bytes).map_err(|e| { + KclError::Engine(KclErrorDetails { + message: format!("Failed to convert bytes to string: {:?}", e), + source_ranges: vec![source_range], + }) + })?; + + Ok(string) + } + async fn exists + std::marker::Send + std::marker::Sync>( &self, path: P, diff --git a/src/wasm-lib/kcl/src/lsp/kcl/mod.rs b/src/wasm-lib/kcl/src/lsp/kcl/mod.rs index b57712403..061766bfa 100644 --- a/src/wasm-lib/kcl/src/lsp/kcl/mod.rs +++ b/src/wasm-lib/kcl/src/lsp/kcl/mod.rs @@ -596,7 +596,7 @@ impl Backend { .clear_scene(&mut id_generator, SourceRange::default()) .await?; - let exec_state = match executor_ctx.run(ast, None, id_generator).await { + let exec_state = match executor_ctx.run(ast, None, id_generator, None).await { Ok(exec_state) => exec_state, Err(err) => { self.memory_map.remove(params.uri.as_str()); diff --git a/src/wasm-lib/kcl/src/parser.rs b/src/wasm-lib/kcl/src/parser.rs index 208b5e8d2..a871ace48 100644 --- a/src/wasm-lib/kcl/src/parser.rs +++ b/src/wasm-lib/kcl/src/parser.rs @@ -12,6 +12,13 @@ pub(crate) mod parser_impl; pub const PIPE_SUBSTITUTION_OPERATOR: &str = "%"; pub const PIPE_OPERATOR: &str = "|>"; +/// Parse the given KCL code into an AST. +pub fn parse(code: &str) -> Result { + let tokens = crate::token::lexer(code)?; + let parser = Parser::new(tokens); + parser.ast() +} + pub struct Parser { pub tokens: Vec, pub unknown_tokens: Vec, diff --git a/src/wasm-lib/kcl/src/parser/parser_impl.rs b/src/wasm-lib/kcl/src/parser/parser_impl.rs index 7c49dface..66a06e072 100644 --- a/src/wasm-lib/kcl/src/parser/parser_impl.rs +++ b/src/wasm-lib/kcl/src/parser/parser_impl.rs @@ -12,10 +12,10 @@ use crate::{ ast::types::{ ArrayExpression, ArrayRangeExpression, BinaryExpression, BinaryOperator, BinaryPart, BodyItem, CallExpression, CommentStyle, ElseIf, Expr, ExpressionStatement, FnArgPrimitive, FnArgType, FunctionExpression, Identifier, - IfExpression, Literal, LiteralIdentifier, LiteralValue, MemberExpression, MemberObject, NonCodeMeta, - NonCodeNode, NonCodeValue, ObjectExpression, ObjectProperty, Parameter, PipeExpression, PipeSubstitution, - Program, ReturnStatement, TagDeclarator, UnaryExpression, UnaryOperator, ValueMeta, VariableDeclaration, - VariableDeclarator, VariableKind, + IfExpression, ImportItem, ImportStatement, ItemVisibility, Literal, LiteralIdentifier, LiteralValue, + MemberExpression, MemberObject, NonCodeMeta, NonCodeNode, NonCodeValue, ObjectExpression, ObjectProperty, + Parameter, PipeExpression, PipeSubstitution, Program, ReturnStatement, TagDeclarator, UnaryExpression, + UnaryOperator, ValueMeta, VariableDeclaration, VariableDeclarator, VariableKind, }, errors::{KclError, KclErrorDetails}, executor::SourceRange, @@ -956,8 +956,10 @@ fn body_items_within_function(i: TokenSlice) -> PResult { // Any of the body item variants, each of which can optionally be followed by a comment. // If there is a comment, it may be preceded by whitespace. let item = dispatch! {peek(any); - token if token.declaration_keyword().is_some() => + token if token.declaration_keyword().is_some() || token.visibility_keyword().is_some() => (declaration.map(BodyItem::VariableDeclaration), opt(noncode_just_after_code)).map(WithinFunction::BodyItem), + token if token.value == "import" && matches!(token.token_type, TokenType::Keyword) => + (import_stmt.map(BodyItem::ImportStatement), opt(noncode_just_after_code)).map(WithinFunction::BodyItem), Token { ref value, .. } if value == "return" => (return_stmt.map(BodyItem::ReturnStatement), opt(noncode_just_after_code)).map(WithinFunction::BodyItem), token if !token.is_code_token() => { @@ -1122,6 +1124,111 @@ pub fn function_body(i: TokenSlice) -> PResult { }) } +fn import_stmt(i: TokenSlice) -> PResult> { + let import_token = any + .try_map(|token: Token| { + if matches!(token.token_type, TokenType::Keyword) && token.value == "import" { + Ok(token) + } else { + Err(KclError::Syntax(KclErrorDetails { + source_ranges: token.as_source_ranges(), + message: format!("{} is not the 'import' keyword", token.value.as_str()), + })) + } + }) + .context(expected("the 'import' keyword")) + .parse_next(i)?; + let start = import_token.start; + + require_whitespace(i)?; + + let items = separated(1.., import_item, comma_sep) + .parse_next(i) + .map_err(|e| e.cut())?; + + require_whitespace(i)?; + + any.try_map(|token: Token| { + if matches!(token.token_type, TokenType::Keyword | TokenType::Word) && token.value == "from" { + Ok(()) + } else { + Err(KclError::Syntax(KclErrorDetails { + source_ranges: token.as_source_ranges(), + message: format!("{} is not the 'from' keyword", token.value.as_str()), + })) + } + }) + .context(expected("the 'from' keyword")) + .parse_next(i) + .map_err(|e| e.cut())?; + + require_whitespace(i)?; + + let path = string_literal(i)?; + let end = path.end(); + let path_string = match path.value { + LiteralValue::String(s) => s, + _ => unreachable!(), + }; + if path_string + .chars() + .any(|c| !c.is_ascii_alphanumeric() && c != '_' && c != '-' && c != '.') + { + return Err(ErrMode::Cut( + KclError::Syntax(KclErrorDetails { + source_ranges: vec![SourceRange::new(path.start, path.end)], + message: "import path may only contain alphanumeric characters, underscore, hyphen, and period. Files in other directories are not yet supported.".to_owned(), + }) + .into(), + )); + } + Ok(Box::new(ImportStatement { + items, + path: path_string, + raw_path: path.raw, + start, + end, + digest: None, + })) +} + +fn import_item(i: TokenSlice) -> PResult { + let name = identifier.context(expected("an identifier to import")).parse_next(i)?; + let start = name.start; + let alias = opt(preceded( + (whitespace, import_as_keyword, whitespace), + identifier.context(expected("an identifier to alias the import")), + )) + .parse_next(i)?; + let end = if let Some(ref alias) = alias { + alias.end() + } else { + name.end() + }; + Ok(ImportItem { + name, + alias, + start, + end, + digest: None, + }) +} + +fn import_as_keyword(i: TokenSlice) -> PResult { + any.try_map(|token: Token| { + if matches!(token.token_type, TokenType::Keyword | TokenType::Word) && token.value == "as" { + Ok(token) + } else { + Err(KclError::Syntax(KclErrorDetails { + source_ranges: token.as_source_ranges(), + message: format!("{} is not the 'as' keyword", token.value.as_str()), + })) + } + }) + .context(expected("the 'as' keyword")) + .parse_next(i) +} + /// Parse a return statement of a user-defined function, e.g. `return x`. pub fn return_stmt(i: TokenSlice) -> PResult { let start = any @@ -1214,6 +1321,19 @@ fn possible_operands(i: TokenSlice) -> PResult { .parse_next(i) } +/// Parse an item visibility specifier, e.g. export. +fn item_visibility(i: TokenSlice) -> PResult<(ItemVisibility, Token)> { + any.verify_map(|token: Token| { + if token.token_type == TokenType::Keyword && token.value == "export" { + Some((ItemVisibility::Export, token)) + } else { + None + } + }) + .context(expected("item visibility, e.g. 'export'")) + .parse_next(i) +} + fn declaration_keyword(i: TokenSlice) -> PResult<(VariableKind, Token)> { let res = any .verify_map(|token: Token| token.declaration_keyword().map(|kw| (kw, token))) @@ -1223,6 +1343,9 @@ fn declaration_keyword(i: TokenSlice) -> PResult<(VariableKind, Token)> { /// Parse a variable/constant declaration. fn declaration(i: TokenSlice) -> PResult { + let (visibility, visibility_token) = opt(terminated(item_visibility, whitespace)) + .parse_next(i)? + .map_or((ItemVisibility::Default, None), |pair| (pair.0, Some(pair.1))); let decl_token = opt(declaration_keyword).parse_next(i)?; if decl_token.is_some() { // If there was a declaration keyword like `fn`, then it must be followed by some spaces. @@ -1235,11 +1358,14 @@ fn declaration(i: TokenSlice) -> PResult { "an identifier, which becomes name you're binding the value to", )) .parse_next(i)?; - let (kind, start, dec_end) = if let Some((kind, token)) = &decl_token { + let (kind, mut start, dec_end) = if let Some((kind, token)) = &decl_token { (*kind, token.start, token.end) } else { (VariableKind::Const, id.start(), id.end()) }; + if let Some(token) = visibility_token { + start = token.start; + } ignore_whitespace(i); equals(i)?; @@ -1288,6 +1414,7 @@ fn declaration(i: TokenSlice) -> PResult { init: val, digest: None, }], + visibility, kind, digest: None, }) diff --git a/src/wasm-lib/kcl/src/parser/snapshots/kcl_lib__parser__parser_impl__snapshot_tests__au.snap b/src/wasm-lib/kcl/src/parser/snapshots/kcl_lib__parser__parser_impl__snapshot_tests__au.snap index a0ac8e371..6252b0365 100644 --- a/src/wasm-lib/kcl/src/parser/snapshots/kcl_lib__parser__parser_impl__snapshot_tests__au.snap +++ b/src/wasm-lib/kcl/src/parser/snapshots/kcl_lib__parser__parser_impl__snapshot_tests__au.snap @@ -1,6 +1,5 @@ --- source: kcl/src/parser/parser_impl.rs -assertion_line: 3423 expression: actual --- { diff --git a/src/wasm-lib/kcl/src/parser/snapshots/kcl_lib__parser__parser_impl__snapshot_tests__bb.snap b/src/wasm-lib/kcl/src/parser/snapshots/kcl_lib__parser__parser_impl__snapshot_tests__bb.snap index 8f0ed715a..04c8555e3 100644 --- a/src/wasm-lib/kcl/src/parser/snapshots/kcl_lib__parser__parser_impl__snapshot_tests__bb.snap +++ b/src/wasm-lib/kcl/src/parser/snapshots/kcl_lib__parser__parser_impl__snapshot_tests__bb.snap @@ -1,6 +1,5 @@ --- source: kcl/src/parser/parser_impl.rs -assertion_line: 3470 expression: actual --- { diff --git a/src/wasm-lib/kcl/src/test_server.rs b/src/wasm-lib/kcl/src/test_server.rs index 927eddc4e..91b3eb911 100644 --- a/src/wasm-lib/kcl/src/test_server.rs +++ b/src/wasm-lib/kcl/src/test_server.rs @@ -30,7 +30,7 @@ async fn do_execute_and_snapshot(ctx: &ExecutorContext, code: &str) -> anyhow::R let program = parser.ast()?; let snapshot = ctx - .execute_and_prepare_snapshot(&program, IdGenerator::default()) + .execute_and_prepare_snapshot(&program, IdGenerator::default(), None) .await?; // Create a temporary file to write the output to. diff --git a/src/wasm-lib/kcl/src/token.rs b/src/wasm-lib/kcl/src/token.rs index 14c110c11..7533ffda3 100644 --- a/src/wasm-lib/kcl/src/token.rs +++ b/src/wasm-lib/kcl/src/token.rs @@ -7,7 +7,11 @@ use serde::{Deserialize, Serialize}; use tower_lsp::lsp_types::SemanticTokenType; use winnow::stream::ContainsToken; -use crate::{ast::types::VariableKind, errors::KclError, executor::SourceRange}; +use crate::{ + ast::types::{ItemVisibility, VariableKind}, + errors::KclError, + executor::SourceRange, +}; mod tokeniser; @@ -196,6 +200,16 @@ impl Token { vec![self.as_source_range()] } + pub fn visibility_keyword(&self) -> Option { + if !matches!(self.token_type, TokenType::Keyword) { + return None; + } + match self.value.as_str() { + "export" => Some(ItemVisibility::Export), + _ => None, + } + } + /// Is this token the beginning of a variable/function declaration? /// If so, what kind? /// If not, returns None. diff --git a/src/wasm-lib/kcl/src/token/tokeniser.rs b/src/wasm-lib/kcl/src/token/tokeniser.rs index 80ba3d416..589a8d478 100644 --- a/src/wasm-lib/kcl/src/token/tokeniser.rs +++ b/src/wasm-lib/kcl/src/token/tokeniser.rs @@ -169,11 +169,17 @@ fn string(i: &mut Located<&str>) -> PResult { Ok(Token::from_range(range, TokenType::String, value.to_string())) } -fn keyword(i: &mut Located<&str>) -> PResult { +fn import_keyword(i: &mut Located<&str>) -> PResult { + let (value, range) = "import".with_span().parse_next(i)?; + let token_type = peek(alt((' '.map(|_| TokenType::Keyword), '('.map(|_| TokenType::Word)))).parse_next(i)?; + Ok(Token::from_range(range, token_type, value.to_owned())) +} + +fn unambiguous_keywords(i: &mut Located<&str>) -> PResult { // These are the keywords themselves. let keyword_candidates = alt(( "if", "else", "for", "while", "return", "break", "continue", "fn", "let", "mut", "loop", "true", "false", - "nil", "and", "or", "not", "var", "const", + "nil", "and", "or", "not", "var", "const", "export", )); // Look ahead. If any of these characters follow the keyword, then it's not a keyword, it's just // the start of a normal word. @@ -185,6 +191,10 @@ fn keyword(i: &mut Located<&str>) -> PResult { Ok(Token::from_range(range, TokenType::Keyword, value.to_owned())) } +fn keyword(i: &mut Located<&str>) -> PResult { + alt((import_keyword, unambiguous_keywords)).parse_next(i) +} + fn type_(i: &mut Located<&str>) -> PResult { // These are the types themselves. let type_candidates = alt(("string", "number", "bool", "sketch", "sketch_surface", "solid")); @@ -1572,4 +1582,28 @@ const things = "things" assert_tokens(expected, actual); } + + #[test] + fn import_keyword() { + let actual = lexer("import foo").unwrap(); + let expected = Token { + token_type: TokenType::Keyword, + value: "import".to_owned(), + start: 0, + end: 6, + }; + assert_eq!(actual[0], expected); + } + + #[test] + fn import_function() { + let actual = lexer("import(3)").unwrap(); + let expected = Token { + token_type: TokenType::Word, + value: "import".to_owned(), + start: 0, + end: 6, + }; + assert_eq!(actual[0], expected); + } } diff --git a/src/wasm-lib/kcl/src/unparser.rs b/src/wasm-lib/kcl/src/unparser.rs index 9001f7dbe..5bc1e7d48 100644 --- a/src/wasm-lib/kcl/src/unparser.rs +++ b/src/wasm-lib/kcl/src/unparser.rs @@ -3,9 +3,9 @@ use std::fmt::Write; use crate::{ ast::types::{ ArrayExpression, ArrayRangeExpression, BinaryExpression, BinaryOperator, BinaryPart, BodyItem, CallExpression, - Expr, FormatOptions, FunctionExpression, IfExpression, Literal, LiteralIdentifier, LiteralValue, - MemberExpression, MemberObject, NonCodeValue, ObjectExpression, PipeExpression, Program, TagDeclarator, - UnaryExpression, VariableDeclaration, VariableKind, + Expr, FormatOptions, FunctionExpression, IfExpression, ImportStatement, ItemVisibility, Literal, + LiteralIdentifier, LiteralValue, MemberExpression, MemberObject, NonCodeValue, ObjectExpression, + PipeExpression, Program, TagDeclarator, UnaryExpression, VariableDeclaration, VariableKind, }, parser::PIPE_OPERATOR, }; @@ -17,6 +17,7 @@ impl Program { .body .iter() .map(|statement| match statement.clone() { + BodyItem::ImportStatement(stmt) => stmt.recast(options, indentation_level), BodyItem::ExpressionStatement(expression_statement) => { expression_statement .expression @@ -108,6 +109,27 @@ impl NonCodeValue { } } +impl ImportStatement { + pub fn recast(&self, options: &FormatOptions, indentation_level: usize) -> String { + let indentation = options.get_indentation(indentation_level); + let mut string = format!("{}import ", indentation); + for (i, item) in self.items.iter().enumerate() { + if i > 0 { + string.push_str(", "); + } + string.push_str(&item.name.name); + if let Some(alias) = &item.alias { + // If the alias is the same, don't output it. + if item.name.name != alias.name { + string.push_str(&format!(" as {}", alias.name)); + } + } + } + string.push_str(&format!(" from {}", self.raw_path)); + string + } +} + impl Expr { pub(crate) fn recast(&self, options: &FormatOptions, indentation_level: usize, is_in_pipe: bool) -> String { match &self { @@ -168,7 +190,11 @@ impl CallExpression { impl VariableDeclaration { pub fn recast(&self, options: &FormatOptions, indentation_level: usize) -> String { let indentation = options.get_indentation(indentation_level); - self.declarations.iter().fold(String::new(), |mut output, declaration| { + let output = match self.visibility { + ItemVisibility::Default => String::new(), + ItemVisibility::Export => "export ".to_owned(), + }; + self.declarations.iter().fold(output, |mut output, declaration| { let keyword = match self.kind { VariableKind::Fn => "fn ", VariableKind::Const => "", @@ -581,6 +607,46 @@ mod tests { assert_eq!(output, input); } + #[test] + fn test_recast_import() { + let input = r#"import a from "a.kcl" +import a as aaa from "a.kcl" +import a, b from "a.kcl" +import a as aaa, b from "a.kcl" +import a, b as bbb from "a.kcl" +import a as aaa, b as bbb from "a.kcl" +"#; + let tokens = crate::token::lexer(input).unwrap(); + let parser = crate::parser::Parser::new(tokens); + let program = parser.ast().unwrap(); + let output = program.recast(&Default::default(), 0); + assert_eq!(output, input); + } + + #[test] + fn test_recast_import_as_same_name() { + let input = r#"import a as a from "a.kcl" +"#; + let program = crate::parser::parse(input).unwrap(); + let output = program.recast(&Default::default(), 0); + let expected = r#"import a from "a.kcl" +"#; + assert_eq!(output, expected); + } + + #[test] + fn test_recast_export_fn() { + let input = r#"export fn a = () => { + return 0 +} +"#; + let tokens = crate::token::lexer(input).unwrap(); + let parser = crate::parser::Parser::new(tokens); + let program = parser.ast().unwrap(); + let output = program.recast(&Default::default(), 0); + assert_eq!(output, input); + } + #[test] fn test_recast_bug_fn_in_fn() { let some_program_string = r#"// Start point (top left) diff --git a/src/wasm-lib/kcl/src/walk/ast_node.rs b/src/wasm-lib/kcl/src/walk/ast_node.rs index 6f7dbdfb2..337ebf49c 100644 --- a/src/wasm-lib/kcl/src/walk/ast_node.rs +++ b/src/wasm-lib/kcl/src/walk/ast_node.rs @@ -9,6 +9,7 @@ use crate::{ pub enum Node<'a> { Program(&'a types::Program), + ImportStatement(&'a types::ImportStatement), ExpressionStatement(&'a types::ExpressionStatement), VariableDeclaration(&'a types::VariableDeclaration), ReturnStatement(&'a types::ReturnStatement), @@ -42,6 +43,7 @@ impl From<&Node<'_>> for SourceRange { fn from(node: &Node) -> Self { match node { Node::Program(p) => SourceRange([p.start, p.end]), + Node::ImportStatement(e) => SourceRange([e.start(), e.end()]), Node::ExpressionStatement(e) => SourceRange([e.start(), e.end()]), Node::VariableDeclaration(v) => SourceRange([v.start(), v.end()]), Node::ReturnStatement(r) => SourceRange([r.start(), r.end()]), @@ -79,6 +81,7 @@ macro_rules! impl_from { } impl_from!(Node, Program); +impl_from!(Node, ImportStatement); impl_from!(Node, ExpressionStatement); impl_from!(Node, VariableDeclaration); impl_from!(Node, ReturnStatement); diff --git a/src/wasm-lib/kcl/src/walk/ast_walk.rs b/src/wasm-lib/kcl/src/walk/ast_walk.rs index 1a61931da..a62e9620e 100644 --- a/src/wasm-lib/kcl/src/walk/ast_walk.rs +++ b/src/wasm-lib/kcl/src/walk/ast_walk.rs @@ -277,6 +277,12 @@ where // We don't walk a BodyItem since it's an enum itself. match node { + BodyItem::ImportStatement(xs) => { + if !f.walk(xs.as_ref().into())? { + return Ok(false); + } + Ok(true) + } BodyItem::ExpressionStatement(xs) => { if !f.walk(xs.into())? { return Ok(false); diff --git a/src/wasm-lib/src/wasm.rs b/src/wasm-lib/src/wasm.rs index b0d807b89..143f6fa7a 100644 --- a/src/wasm-lib/src/wasm.rs +++ b/src/wasm-lib/src/wasm.rs @@ -20,6 +20,7 @@ pub async fn execute_wasm( units: &str, engine_manager: kcl_lib::engine::conn_wasm::EngineCommandManager, fs_manager: kcl_lib::fs::wasm::FileSystemManager, + project_directory: Option, is_mock: bool, ) -> Result { console_error_panic_hook::set_once(); @@ -62,7 +63,7 @@ pub async fn execute_wasm( }; let exec_state = ctx - .run(&program, Some(memory), id_generator) + .run(&program, Some(memory), id_generator, project_directory) .await .map_err(String::from)?; diff --git a/src/wasm-lib/tests/executor/inputs/no_visuals/export_constant.kcl b/src/wasm-lib/tests/executor/inputs/no_visuals/export_constant.kcl new file mode 100644 index 000000000..471bcd1cc --- /dev/null +++ b/src/wasm-lib/tests/executor/inputs/no_visuals/export_constant.kcl @@ -0,0 +1 @@ +export three = 3 diff --git a/src/wasm-lib/tests/executor/inputs/no_visuals/export_side_effect.kcl b/src/wasm-lib/tests/executor/inputs/no_visuals/export_side_effect.kcl new file mode 100644 index 000000000..2a2d6b7d4 --- /dev/null +++ b/src/wasm-lib/tests/executor/inputs/no_visuals/export_side_effect.kcl @@ -0,0 +1,5 @@ +export fn foo = () => { return 0 } + +// This interacts with the engine. +part001 = startSketchOn('XY') + |> startProfileAt([0, 0], %) diff --git a/src/wasm-lib/tests/executor/inputs/no_visuals/identity.kcl b/src/wasm-lib/tests/executor/inputs/no_visuals/identity.kcl new file mode 100644 index 000000000..174ff882a --- /dev/null +++ b/src/wasm-lib/tests/executor/inputs/no_visuals/identity.kcl @@ -0,0 +1,3 @@ +export fn identity = (x) => { + return x +} diff --git a/src/wasm-lib/tests/executor/inputs/no_visuals/import_constant.kcl b/src/wasm-lib/tests/executor/inputs/no_visuals/import_constant.kcl new file mode 100644 index 000000000..f9411745e --- /dev/null +++ b/src/wasm-lib/tests/executor/inputs/no_visuals/import_constant.kcl @@ -0,0 +1 @@ +import three from "export_constant.kcl" diff --git a/src/wasm-lib/tests/executor/inputs/no_visuals/import_cycle1.kcl b/src/wasm-lib/tests/executor/inputs/no_visuals/import_cycle1.kcl new file mode 100644 index 000000000..101ba0dd8 --- /dev/null +++ b/src/wasm-lib/tests/executor/inputs/no_visuals/import_cycle1.kcl @@ -0,0 +1,3 @@ +import two from "import_cycle2.kcl" + +export fn one = () => { return two() - 1 } diff --git a/src/wasm-lib/tests/executor/inputs/no_visuals/import_cycle2.kcl b/src/wasm-lib/tests/executor/inputs/no_visuals/import_cycle2.kcl new file mode 100644 index 000000000..16eb70527 --- /dev/null +++ b/src/wasm-lib/tests/executor/inputs/no_visuals/import_cycle2.kcl @@ -0,0 +1,3 @@ +import three from "import_cycle3.kcl" + +export fn two = () => { return three() - 1 } diff --git a/src/wasm-lib/tests/executor/inputs/no_visuals/import_cycle3.kcl b/src/wasm-lib/tests/executor/inputs/no_visuals/import_cycle3.kcl new file mode 100644 index 000000000..6d7eb6240 --- /dev/null +++ b/src/wasm-lib/tests/executor/inputs/no_visuals/import_cycle3.kcl @@ -0,0 +1,3 @@ +import one from "import_cycle1.kcl" + +export fn three = () => { return one() + one() + one() } diff --git a/src/wasm-lib/tests/executor/inputs/no_visuals/import_from_other_directory.kcl b/src/wasm-lib/tests/executor/inputs/no_visuals/import_from_other_directory.kcl new file mode 100644 index 000000000..bd10324de --- /dev/null +++ b/src/wasm-lib/tests/executor/inputs/no_visuals/import_from_other_directory.kcl @@ -0,0 +1 @@ +import cube from "../cube.kcl" diff --git a/src/wasm-lib/tests/executor/inputs/no_visuals/import_in_function.kcl b/src/wasm-lib/tests/executor/inputs/no_visuals/import_in_function.kcl new file mode 100644 index 000000000..c1c727e2b --- /dev/null +++ b/src/wasm-lib/tests/executor/inputs/no_visuals/import_in_function.kcl @@ -0,0 +1,4 @@ +fn foo = () => { + import identity from "identity.kcl" + return 1 +} diff --git a/src/wasm-lib/tests/executor/inputs/no_visuals/import_in_if.kcl b/src/wasm-lib/tests/executor/inputs/no_visuals/import_in_if.kcl new file mode 100644 index 000000000..a443f1c37 --- /dev/null +++ b/src/wasm-lib/tests/executor/inputs/no_visuals/import_in_if.kcl @@ -0,0 +1,6 @@ +if true { + import identity from "identity.kcl" + 1 +} else { + 2 +} diff --git a/src/wasm-lib/tests/executor/inputs/no_visuals/import_side_effect.kcl b/src/wasm-lib/tests/executor/inputs/no_visuals/import_side_effect.kcl new file mode 100644 index 000000000..d4f6de6a0 --- /dev/null +++ b/src/wasm-lib/tests/executor/inputs/no_visuals/import_side_effect.kcl @@ -0,0 +1 @@ +import foo from "export_side_effect.kcl" diff --git a/src/wasm-lib/tests/executor/inputs/no_visuals/import_simple.kcl b/src/wasm-lib/tests/executor/inputs/no_visuals/import_simple.kcl new file mode 100644 index 000000000..2ee77568a --- /dev/null +++ b/src/wasm-lib/tests/executor/inputs/no_visuals/import_simple.kcl @@ -0,0 +1,25 @@ +import identity from "identity.kcl" + +answer = identity(42) +assertEqual(answer, 42, 0.0001, "identity") + +import identity as id from "identity.kcl" + +answer43 = id(43) +assertEqual(answer43, 43, 0.0001, "identity") + +import increment, decrement from "numbers.kcl" + +answer3 = increment(2) +assertEqual(answer3, 3, 0.0001, "increment") + +answer5 = decrement(6) +assertEqual(answer5, 5, 0.0001, "decrement") + +import increment as inc, decrement as dec from "numbers.kcl" + +answer4 = inc(3) +assertEqual(answer4, 4, 0.0001, "inc") + +answer6 = dec(7) +assertEqual(answer6, 6, 0.0001, "dec") diff --git a/src/wasm-lib/tests/executor/inputs/no_visuals/numbers.kcl b/src/wasm-lib/tests/executor/inputs/no_visuals/numbers.kcl new file mode 100644 index 000000000..d14d30b6f --- /dev/null +++ b/src/wasm-lib/tests/executor/inputs/no_visuals/numbers.kcl @@ -0,0 +1,7 @@ +export fn increment = (x) => { + return x + 1 +} + +export fn decrement = (x) => { + return x - 1 +} diff --git a/src/wasm-lib/tests/executor/no_visuals.rs b/src/wasm-lib/tests/executor/no_visuals.rs index f9fbbab8c..cc125393a 100644 --- a/src/wasm-lib/tests/executor/no_visuals.rs +++ b/src/wasm-lib/tests/executor/no_visuals.rs @@ -2,6 +2,7 @@ use kcl_lib::{ ast::types::Program, errors::KclError, executor::{ExecutorContext, IdGenerator}, + parser, }; macro_rules! gen_test { @@ -25,10 +26,28 @@ macro_rules! gen_test_fail { }; } +macro_rules! gen_test_parse_fail { + ($file:ident, $expected:literal) => { + #[tokio::test] + async fn $file() { + let code = include_str!(concat!("inputs/no_visuals/", stringify!($file), ".kcl")); + let actual = run_parse_fail(&code).await; + assert_eq!(actual.get_message(), $expected); + } + }; +} + async fn run(code: &str) { let (ctx, program, id_generator) = setup(code).await; - let res = ctx.run(&program, None, id_generator).await; + let res = ctx + .run( + &program, + None, + id_generator, + Some("tests/executor/inputs/no_visuals/".to_owned()), + ) + .await; match res { Ok(state) => { println!("{:#?}", state.memory); @@ -57,12 +76,27 @@ async fn setup(program: &str) -> (ExecutorContext, Program, IdGenerator) { async fn run_fail(code: &str) -> KclError { let (ctx, program, id_generator) = setup(code).await; - let Err(e) = ctx.run(&program, None, id_generator).await else { + let Err(e) = ctx + .run( + &program, + None, + id_generator, + Some("tests/executor/inputs/no_visuals/".to_owned()), + ) + .await + else { panic!("Expected this KCL program to fail, but it (incorrectly) never threw an error."); }; e } +async fn run_parse_fail(code: &str) -> KclError { + let Err(e) = parser::parse(code) else { + panic!("Expected this KCL program to fail to parse, but it (incorrectly) never threw an error."); + }; + e +} + gen_test!(property_of_object); gen_test!(index_of_array); gen_test!(comparisons); @@ -111,5 +145,31 @@ gen_test!(if_else); // "syntax: blocks inside an if/else expression must end in an expression" // ); gen_test_fail!(comparisons_multiple, "syntax: Invalid number: true"); +gen_test!(import_simple); +gen_test_fail!( + import_cycle1, + "import cycle: circular import of modules is not allowed: tests/executor/inputs/no_visuals/import_cycle2.kcl -> tests/executor/inputs/no_visuals/import_cycle3.kcl -> tests/executor/inputs/no_visuals/import_cycle1.kcl -> tests/executor/inputs/no_visuals/import_cycle2.kcl" +); +gen_test_fail!( + import_constant, + "semantic: Error loading imported file. Open it to view more details. export_constant.kcl: Only functions can be exported" +); +gen_test_fail!( + import_side_effect, + "semantic: Error loading imported file. Open it to view more details. export_side_effect.kcl: Cannot send modeling commands while importing. Wrap your code in a function if you want to import the file." +); +gen_test_parse_fail!( + import_from_other_directory, + "syntax: import path may only contain alphanumeric characters, underscore, hyphen, and period. Files in other directories are not yet supported." +); +// TODO: We'd like these tests. +// gen_test_fail!( +// import_in_if, +// "syntax: Can import only import at the top level" +// ); +// gen_test_fail!( +// import_in_function, +// "syntax: Can import only import at the top level" +// ); gen_test!(add_lots); gen_test!(double_map); diff --git a/src/wasm-lib/tests/modify/main.rs b/src/wasm-lib/tests/modify/main.rs index 862e4ebce..dbebfb83b 100644 --- a/src/wasm-lib/tests/modify/main.rs +++ b/src/wasm-lib/tests/modify/main.rs @@ -35,7 +35,7 @@ async fn setup(code: &str, name: &str) -> Result<(ExecutorContext, Program, uuid let parser = kcl_lib::parser::Parser::new(tokens); let program = parser.ast()?; let ctx = kcl_lib::executor::ExecutorContext::new(&client, Default::default()).await?; - let exec_state = ctx.run(&program, None, IdGenerator::default()).await?; + let exec_state = ctx.run(&program, None, IdGenerator::default(), None).await?; // We need to get the sketch ID. // Get the sketch ID from memory.