internal: KCL modules, part 1 (#4149)

Addresses #4080. (Not ready to close it yet.)

# Important

Requires a fix for #4147 before it can work in ZMA.

# Overview

```kcl
// numbers.kcl
export fn inc = (x) => {
  return x + 1
}
```

```kcl
import inc from "numbers.kcl"

answer = inc(41)
```

This also implements multiple imports with optional renaming.

```kcl
import inc, dec from "numbers.kcl"
import identity as id, length as len from "utils.kcl"
```

Note: Imported files _must_ be in the same directory.

Things for a follow-up PR:

- #4147. Currently, we cannot read files in WebAssembly, i.e. ZMA.
- Docs
- Should be an error to `import` anywhere besides the top level. Needs parser restructuring to track the context of a "function body".
- Should be an error to have `export` anywhere besides the top level. It has no effect, but we should tell people it's not valid instead of silently ignoring it.
- Error message for cycle detection is funky because the Rust side doesn't actually know the name of the first file. Message will say "b -> a -> b" instead of "a -> b -> a" when "a" is the top-level file.
- Cache imported files so that they don't need to be re-parsed and re-executed.
This commit is contained in:
Jonathan Tran
2024-10-17 00:48:33 -04:00
committed by GitHub
parent 7d44de0c12
commit 0577b6a984
65 changed files with 1746 additions and 58 deletions

View File

@ -82690,6 +82690,58 @@
}, },
"BodyItem": { "BodyItem": {
"oneOf": [ "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", "type": "object",
"required": [ "required": [
@ -82763,6 +82815,9 @@
"$ref": "#/components/schemas/VariableDeclarator" "$ref": "#/components/schemas/VariableDeclarator"
} }
}, },
"visibility": {
"$ref": "#/components/schemas/ItemVisibility"
},
"kind": { "kind": {
"$ref": "#/components/schemas/VariableKind" "$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": { "Expr": {
"description": "An expression can be evaluated to yield a single KCL value.", "description": "An expression can be evaluated to yield a single KCL value.",
"oneOf": [ "oneOf": [
@ -84475,6 +84578,13 @@
} }
} }
}, },
"ItemVisibility": {
"type": "string",
"enum": [
"default",
"export"
]
},
"VariableKind": { "VariableKind": {
"oneOf": [ "oneOf": [
{ {
@ -86338,6 +86448,58 @@
}, },
"BodyItem": { "BodyItem": {
"oneOf": [ "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", "type": "object",
"required": [ "required": [
@ -86411,6 +86573,9 @@
"$ref": "#/components/schemas/VariableDeclarator" "$ref": "#/components/schemas/VariableDeclarator"
} }
}, },
"visibility": {
"$ref": "#/components/schemas/ItemVisibility"
},
"kind": { "kind": {
"$ref": "#/components/schemas/VariableKind" "$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": { "Expr": {
"description": "An expression can be evaluated to yield a single KCL value.", "description": "An expression can be evaluated to yield a single KCL value.",
"oneOf": [ "oneOf": [
@ -88123,6 +88336,13 @@
} }
} }
}, },
"ItemVisibility": {
"type": "string",
"enum": [
"default",
"export"
]
},
"VariableKind": { "VariableKind": {
"oneOf": [ "oneOf": [
{ {
@ -89990,6 +90210,58 @@
}, },
"BodyItem": { "BodyItem": {
"oneOf": [ "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", "type": "object",
"required": [ "required": [
@ -90063,6 +90335,9 @@
"$ref": "#/components/schemas/VariableDeclarator" "$ref": "#/components/schemas/VariableDeclarator"
} }
}, },
"visibility": {
"$ref": "#/components/schemas/ItemVisibility"
},
"kind": { "kind": {
"$ref": "#/components/schemas/VariableKind" "$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": { "Expr": {
"description": "An expression can be evaluated to yield a single KCL value.", "description": "An expression can be evaluated to yield a single KCL value.",
"oneOf": [ "oneOf": [
@ -91775,6 +92098,13 @@
} }
} }
}, },
"ItemVisibility": {
"type": "string",
"enum": [
"default",
"export"
]
},
"VariableKind": { "VariableKind": {
"oneOf": [ "oneOf": [
{ {
@ -114494,6 +114824,58 @@
}, },
"BodyItem": { "BodyItem": {
"oneOf": [ "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", "type": "object",
"required": [ "required": [
@ -114567,6 +114949,9 @@
"$ref": "#/components/schemas/VariableDeclarator" "$ref": "#/components/schemas/VariableDeclarator"
} }
}, },
"visibility": {
"$ref": "#/components/schemas/ItemVisibility"
},
"kind": { "kind": {
"$ref": "#/components/schemas/VariableKind" "$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": { "Expr": {
"description": "An expression can be evaluated to yield a single KCL value.", "description": "An expression can be evaluated to yield a single KCL value.",
"oneOf": [ "oneOf": [
@ -116279,6 +116712,13 @@
} }
} }
}, },
"ItemVisibility": {
"type": "string",
"enum": [
"default",
"export"
]
},
"VariableKind": { "VariableKind": {
"oneOf": [ "oneOf": [
{ {
@ -118535,6 +118975,58 @@
}, },
"BodyItem": { "BodyItem": {
"oneOf": [ "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", "type": "object",
"required": [ "required": [
@ -118608,6 +119100,9 @@
"$ref": "#/components/schemas/VariableDeclarator" "$ref": "#/components/schemas/VariableDeclarator"
} }
}, },
"visibility": {
"$ref": "#/components/schemas/ItemVisibility"
},
"kind": { "kind": {
"$ref": "#/components/schemas/VariableKind" "$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": { "Expr": {
"description": "An expression can be evaluated to yield a single KCL value.", "description": "An expression can be evaluated to yield a single KCL value.",
"oneOf": [ "oneOf": [
@ -120320,6 +120863,13 @@
} }
} }
}, },
"ItemVisibility": {
"type": "string",
"enum": [
"default",
"export"
]
},
"VariableKind": { "VariableKind": {
"oneOf": [ "oneOf": [
{ {
@ -122183,6 +122733,58 @@
}, },
"BodyItem": { "BodyItem": {
"oneOf": [ "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", "type": "object",
"required": [ "required": [
@ -122256,6 +122858,9 @@
"$ref": "#/components/schemas/VariableDeclarator" "$ref": "#/components/schemas/VariableDeclarator"
} }
}, },
"visibility": {
"$ref": "#/components/schemas/ItemVisibility"
},
"kind": { "kind": {
"$ref": "#/components/schemas/VariableKind" "$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": { "Expr": {
"description": "An expression can be evaluated to yield a single KCL value.", "description": "An expression can be evaluated to yield a single KCL value.",
"oneOf": [ "oneOf": [
@ -123968,6 +124621,13 @@
} }
} }
}, },
"ItemVisibility": {
"type": "string",
"enum": [
"default",
"export"
]
},
"VariableKind": { "VariableKind": {
"oneOf": [ "oneOf": [
{ {
@ -125829,6 +126489,58 @@
}, },
"BodyItem": { "BodyItem": {
"oneOf": [ "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", "type": "object",
"required": [ "required": [
@ -125902,6 +126614,9 @@
"$ref": "#/components/schemas/VariableDeclarator" "$ref": "#/components/schemas/VariableDeclarator"
} }
}, },
"visibility": {
"$ref": "#/components/schemas/ItemVisibility"
},
"kind": { "kind": {
"$ref": "#/components/schemas/VariableKind" "$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": { "Expr": {
"description": "An expression can be evaluated to yield a single KCL value.", "description": "An expression can be evaluated to yield a single KCL value.",
"oneOf": [ "oneOf": [
@ -127614,6 +128377,13 @@
} }
} }
}, },
"ItemVisibility": {
"type": "string",
"enum": [
"default",
"export"
]
},
"VariableKind": { "VariableKind": {
"oneOf": [ "oneOf": [
{ {

View File

@ -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 ## Properties
| Property | Type | Description | Required | | Property | Type | Description | Required |
@ -45,6 +66,7 @@ layout: manual
| `start` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No | | `start` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `end` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No | | `end` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `declarations` |`[` [`VariableDeclarator`](/docs/kcl/types/VariableDeclarator) `]`| | No | | `declarations` |`[` [`VariableDeclarator`](/docs/kcl/types/VariableDeclarator) `]`| | No |
| `visibility` |[`ItemVisibility`](/docs/kcl/types/ItemVisibility)| | No |
| `kind` |[`VariableKind`](/docs/kcl/types/VariableKind)| | 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 | | `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 |

View File

@ -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 |

View File

@ -0,0 +1,16 @@
---
title: "ItemVisibility"
excerpt: ""
layout: manual
---
**enum:** `default`, `export`

View File

@ -1,6 +1,9 @@
import { styleTags, tags as t } from '@lezer/highlight' import { styleTags, tags as t } from '@lezer/highlight'
export const kclHighlight = styleTags({ export const kclHighlight = styleTags({
'import export': t.moduleKeyword,
ImportItemAs: t.definitionKeyword,
ImportFrom: t.moduleKeyword,
'fn var let const': t.definitionKeyword, 'fn var let const': t.definitionKeyword,
'if else': t.controlKeyword, 'if else': t.controlKeyword,
return: t.controlKeyword, return: t.controlKeyword,

View File

@ -15,8 +15,9 @@
} }
statement[@isGroup=Statement] { statement[@isGroup=Statement] {
FunctionDeclaration { kw<"fn"> VariableDefinition Equals ParamList Arrow Body } | ImportStatement { kw<"import"> ImportItems ImportFrom String } |
VariableDeclaration { (kw<"var"> | kw<"let"> | kw<"const">)? VariableDefinition Equals expression } | 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 } | ReturnStatement { kw<"return"> expression } |
ExpressionStatement { expression } ExpressionStatement { expression }
} }
@ -25,6 +26,9 @@ ParamList { "(" commaSep<Parameter { VariableDefinition "?"? (":" type)? }> ")"
Body { "{" statement* "}" } Body { "{" statement* "}" }
ImportItems { commaSep1NoTrailingComma<ImportItem> }
ImportItem { identifier (ImportItemAs identifier)? }
expression[@isGroup=Expression] { expression[@isGroup=Expression] {
String | String |
Number | Number |
@ -74,6 +78,8 @@ kw<term> { @specialize[@name={term}]<identifier, term> }
commaSep<term> { (term ("," term)*)? ","? } commaSep<term> { (term ("," term)*)? ","? }
commaSep1NoTrailingComma<term> { term ("," term)* }
@tokens { @tokens {
String[isolate] { "'" ("\\" _ | !['\\])* "'" | '"' ("\\" _ | !["\\])* '"' } String[isolate] { "'" ("\\" _ | !['\\])* "'" | '"' ("\\" _ | !["\\])* '"' }
@ -106,6 +112,9 @@ commaSep<term> { (term ("," term)*)? ","? }
Shebang { "#!" ![\n]* } Shebang { "#!" ![\n]* }
ImportItemAs { "as" }
ImportFrom { "from" }
"(" ")" "(" ")"
"{" "}" "{" "}"
"[" "]" "[" "]"

View File

@ -501,6 +501,7 @@ export function sketchOnExtrudedFace(
createIdentifier(extrudeName ? extrudeName : oldSketchName), createIdentifier(extrudeName ? extrudeName : oldSketchName),
_tag, _tag,
]), ]),
undefined,
'const' 'const'
) )
@ -682,6 +683,7 @@ export function createPipeExpression(
export function createVariableDeclaration( export function createVariableDeclaration(
varName: string, varName: string,
init: VariableDeclarator['init'], init: VariableDeclarator['init'],
visibility: VariableDeclaration['visibility'] = 'default',
kind: VariableDeclaration['kind'] = 'const' kind: VariableDeclaration['kind'] = 'const'
): VariableDeclaration { ): VariableDeclaration {
return { return {
@ -699,6 +701,7 @@ export function createVariableDeclaration(
init, init,
}, },
], ],
visibility,
kind, kind,
} }
} }

View File

@ -28,6 +28,7 @@ import {
getConstraintType, getConstraintType,
} from './std/sketchcombos' } from './std/sketchcombos'
import { err } from 'lib/trap' 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. * 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( function moreNodePathFromSourceRange(
node: Expr | ExpressionStatement | VariableDeclaration | ReturnStatement, node:
| Expr
| ImportStatement
| ExpressionStatement
| VariableDeclaration
| ReturnStatement,
sourceRange: Selection['range'], sourceRange: Selection['range'],
previousPath: PathToNode = [['body', '']] previousPath: PathToNode = [['body', '']]
): PathToNode { ): PathToNode {

View File

@ -426,6 +426,7 @@ export const _executor = async (
baseUnit, baseUnit,
engineCommandManager, engineCommandManager,
fileSystemManager, fileSystemManager,
undefined,
isMock isMock
) )
return execStateFromRaw(execState) return execStateFromRaw(execState)

View File

@ -762,7 +762,7 @@ fn generate_code_block_test(fn_name: &str, code_block: &str, index: usize) -> pr
context_type: crate::executor::ContextType::Mock, 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)] #[tokio::test(flavor = "multi_thread", worker_threads = 5)]

View File

@ -17,7 +17,7 @@ mod test_examples_someFn {
settings: Default::default(), settings: Default::default(),
context_type: crate::executor::ContextType::Mock, 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)] #[tokio::test(flavor = "multi_thread", worker_threads = 5)]

View File

@ -17,7 +17,7 @@ mod test_examples_someFn {
settings: Default::default(), settings: Default::default(),
context_type: crate::executor::ContextType::Mock, 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)] #[tokio::test(flavor = "multi_thread", worker_threads = 5)]

View File

@ -17,7 +17,7 @@ mod test_examples_show {
settings: Default::default(), settings: Default::default(),
context_type: crate::executor::ContextType::Mock, 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)] #[tokio::test(flavor = "multi_thread", worker_threads = 5)]
@ -51,7 +51,7 @@ mod test_examples_show {
settings: Default::default(), settings: Default::default(),
context_type: crate::executor::ContextType::Mock, 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)] #[tokio::test(flavor = "multi_thread", worker_threads = 5)]

View File

@ -17,7 +17,7 @@ mod test_examples_show {
settings: Default::default(), settings: Default::default(),
context_type: crate::executor::ContextType::Mock, 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)] #[tokio::test(flavor = "multi_thread", worker_threads = 5)]

View File

@ -18,7 +18,7 @@ mod test_examples_my_func {
settings: Default::default(), settings: Default::default(),
context_type: crate::executor::ContextType::Mock, 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)] #[tokio::test(flavor = "multi_thread", worker_threads = 5)]
@ -52,7 +52,7 @@ mod test_examples_my_func {
settings: Default::default(), settings: Default::default(),
context_type: crate::executor::ContextType::Mock, 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)] #[tokio::test(flavor = "multi_thread", worker_threads = 5)]

View File

@ -18,7 +18,7 @@ mod test_examples_line_to {
settings: Default::default(), settings: Default::default(),
context_type: crate::executor::ContextType::Mock, 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)] #[tokio::test(flavor = "multi_thread", worker_threads = 5)]
@ -52,7 +52,7 @@ mod test_examples_line_to {
settings: Default::default(), settings: Default::default(),
context_type: crate::executor::ContextType::Mock, 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)] #[tokio::test(flavor = "multi_thread", worker_threads = 5)]

View File

@ -17,7 +17,7 @@ mod test_examples_min {
settings: Default::default(), settings: Default::default(),
context_type: crate::executor::ContextType::Mock, 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)] #[tokio::test(flavor = "multi_thread", worker_threads = 5)]
@ -51,7 +51,7 @@ mod test_examples_min {
settings: Default::default(), settings: Default::default(),
context_type: crate::executor::ContextType::Mock, 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)] #[tokio::test(flavor = "multi_thread", worker_threads = 5)]

View File

@ -17,7 +17,7 @@ mod test_examples_show {
settings: Default::default(), settings: Default::default(),
context_type: crate::executor::ContextType::Mock, 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)] #[tokio::test(flavor = "multi_thread", worker_threads = 5)]

View File

@ -17,7 +17,7 @@ mod test_examples_import {
settings: Default::default(), settings: Default::default(),
context_type: crate::executor::ContextType::Mock, 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)] #[tokio::test(flavor = "multi_thread", worker_threads = 5)]

View File

@ -17,7 +17,7 @@ mod test_examples_import {
settings: Default::default(), settings: Default::default(),
context_type: crate::executor::ContextType::Mock, 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)] #[tokio::test(flavor = "multi_thread", worker_threads = 5)]

View File

@ -17,7 +17,7 @@ mod test_examples_import {
settings: Default::default(), settings: Default::default(),
context_type: crate::executor::ContextType::Mock, 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)] #[tokio::test(flavor = "multi_thread", worker_threads = 5)]

View File

@ -17,7 +17,7 @@ mod test_examples_show {
settings: Default::default(), settings: Default::default(),
context_type: crate::executor::ContextType::Mock, 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)] #[tokio::test(flavor = "multi_thread", worker_threads = 5)]

View File

@ -17,7 +17,7 @@ mod test_examples_some_function {
settings: Default::default(), settings: Default::default(),
context_type: crate::executor::ContextType::Mock, 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)] #[tokio::test(flavor = "multi_thread", worker_threads = 5)]

View File

@ -1,7 +1,7 @@
extern crate alloc; extern crate alloc;
use kcl_lib::ast::types::{ use kcl_lib::ast::types::{
BodyItem, Expr, Identifier, Literal, LiteralValue, NonCodeMeta, Program, VariableDeclaration, VariableDeclarator, BodyItem, Expr, Identifier, ItemVisibility, Literal, LiteralValue, NonCodeMeta, Program, VariableDeclaration,
VariableKind, VariableDeclarator, VariableKind,
}; };
use kcl_macros::parse; use kcl_macros::parse;
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
@ -33,6 +33,7 @@ fn basic() {
})), })),
digest: None, digest: None,
}], }],
visibility: ItemVisibility::Default,
kind: VariableKind::Const, kind: VariableKind::Const,
digest: None, digest: None,
})], })],

View File

@ -178,7 +178,7 @@ async fn snapshot_endpoint(body: Bytes, state: ExecutorContext) -> Response<Body
// Let users know if the test is taking a long time. // Let users know if the test is taking a long time.
let (done_tx, done_rx) = oneshot::channel::<()>(); let (done_tx, done_rx) = oneshot::channel::<()>();
let timer = time_until(done_rx); 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, Ok(sn) => sn,
Err(e) => return kcl_err(e), Err(e) => return kcl_err(e),
}; };

View File

@ -1,6 +1,7 @@
use anyhow::Result; use anyhow::Result;
use indexmap::IndexMap; use indexmap::IndexMap;
use kcl_lib::{ use kcl_lib::{
engine::ExecutionKind,
errors::KclError, errors::KclError,
executor::{DefaultPlanes, IdGenerator}, executor::{DefaultPlanes, IdGenerator},
}; };
@ -26,6 +27,7 @@ pub struct EngineConnection {
batch_end: Arc<Mutex<IndexMap<uuid::Uuid, (WebSocketRequest, kcl_lib::executor::SourceRange)>>>, batch_end: Arc<Mutex<IndexMap<uuid::Uuid, (WebSocketRequest, kcl_lib::executor::SourceRange)>>>,
core_test: Arc<Mutex<String>>, core_test: Arc<Mutex<String>>,
default_planes: Arc<RwLock<Option<DefaultPlanes>>>, default_planes: Arc<RwLock<Option<DefaultPlanes>>>,
execution_kind: Arc<Mutex<ExecutionKind>>,
} }
impl EngineConnection { impl EngineConnection {
@ -39,6 +41,7 @@ impl EngineConnection {
batch_end: Arc::new(Mutex::new(IndexMap::new())), batch_end: Arc::new(Mutex::new(IndexMap::new())),
core_test: result, core_test: result,
default_planes: Default::default(), default_planes: Default::default(),
execution_kind: Default::default(),
}) })
} }
@ -360,6 +363,18 @@ impl kcl_lib::engine::EngineManager for EngineConnection {
self.batch_end.clone() 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( async fn default_planes(
&self, &self,
id_generator: &mut IdGenerator, id_generator: &mut IdGenerator,

View File

@ -23,7 +23,7 @@ pub async fn kcl_to_engine_core(code: &str) -> Result<String> {
settings: Default::default(), settings: Default::default(),
context_type: kcl_lib::executor::ContextType::MockCustomForwarded, 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(); let result = result.lock().expect("mutex lock").clone();
Ok(result) Ok(result)

View File

@ -48,7 +48,10 @@ pub async fn modify_ast_for_sketch(
// Get the information about the sketch. // Get the information about the sketch.
if let Some(ast_sketch) = program.get_variable(sketch_name) { 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 { match &constraint_level {
ConstraintLevel::None { source_ranges: _ } => {} ConstraintLevel::None { source_ranges: _ } => {}
ConstraintLevel::Ignore { source_ranges: _ } => {} ConstraintLevel::Ignore { source_ranges: _ } => {}

View File

@ -40,6 +40,11 @@ mod none;
/// Position-independent digest of the AST node. /// Position-independent digest of the AST node.
pub type Digest = [u8; 32]; pub type Digest = [u8; 32];
pub enum Definition<'a> {
Variable(&'a VariableDeclarator),
Import(&'a ImportStatement),
}
/// A KCL program top level, or function body. /// A KCL program top level, or function body.
#[derive(Debug, Default, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, Bake)] #[derive(Debug, Default, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, Bake)]
#[databake(path = kcl_lib::ast::types)] #[databake(path = kcl_lib::ast::types)]
@ -198,6 +203,7 @@ impl Program {
// Recurse over the item. // Recurse over the item.
match item { match item {
BodyItem::ImportStatement(_) => None,
BodyItem::ExpressionStatement(expression_statement) => Some(&expression_statement.expression), BodyItem::ExpressionStatement(expression_statement) => Some(&expression_statement.expression),
BodyItem::VariableDeclaration(variable_declaration) => variable_declaration.get_expr_for_position(pos), BodyItem::VariableDeclaration(variable_declaration) => variable_declaration.get_expr_for_position(pos),
BodyItem::ReturnStatement(return_statement) => Some(&return_statement.argument), BodyItem::ReturnStatement(return_statement) => Some(&return_statement.argument),
@ -214,6 +220,7 @@ impl Program {
// Recurse over the item. // Recurse over the item.
let expr = match item { let expr = match item {
BodyItem::ImportStatement(_) => None,
BodyItem::ExpressionStatement(expression_statement) => Some(&expression_statement.expression), BodyItem::ExpressionStatement(expression_statement) => Some(&expression_statement.expression),
BodyItem::VariableDeclaration(variable_declaration) => variable_declaration.get_expr_for_position(pos), BodyItem::VariableDeclaration(variable_declaration) => variable_declaration.get_expr_for_position(pos),
BodyItem::ReturnStatement(return_statement) => Some(&return_statement.argument), 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. // We only care about the top level things in the program.
for item in &self.body { for item in &self.body {
match item { match item {
BodyItem::ImportStatement(_) => continue,
BodyItem::ExpressionStatement(expression_statement) => { BodyItem::ExpressionStatement(expression_statement) => {
if let Some(folding_range) = expression_statement.expression.get_lsp_folding_range() { if let Some(folding_range) = expression_statement.expression.get_lsp_folding_range() {
ranges.push(folding_range) ranges.push(folding_range)
@ -280,6 +288,12 @@ impl Program {
let mut old_name = None; let mut old_name = None;
for item in &mut self.body { for item in &mut self.body {
match item { 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) => { BodyItem::ExpressionStatement(_expression_statement) => {
continue; continue;
} }
@ -306,6 +320,7 @@ impl Program {
// Recurse over the item. // Recurse over the item.
let mut value = match item { let mut value = match item {
BodyItem::ImportStatement(_) => None, // TODO
BodyItem::ExpressionStatement(ref mut expression_statement) => { BodyItem::ExpressionStatement(ref mut expression_statement) => {
Some(&mut expression_statement.expression) Some(&mut expression_statement.expression)
} }
@ -337,6 +352,9 @@ impl Program {
fn rename_identifiers(&mut self, old_name: &str, new_name: &str) { fn rename_identifiers(&mut self, old_name: &str, new_name: &str) {
for item in &mut self.body { for item in &mut self.body {
match item { match item {
BodyItem::ImportStatement(ref mut stmt) => {
stmt.rename_identifiers(old_name, new_name);
}
BodyItem::ExpressionStatement(ref mut expression_statement) => { BodyItem::ExpressionStatement(ref mut expression_statement) => {
expression_statement.expression.rename_identifiers(old_name, new_name); 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) { pub fn replace_variable(&mut self, name: &str, declarator: VariableDeclarator) {
for item in &mut self.body { for item in &mut self.body {
match item { match item {
BodyItem::ImportStatement(_) => {
continue;
}
BodyItem::ExpressionStatement(_expression_statement) => { BodyItem::ExpressionStatement(_expression_statement) => {
continue; continue;
} }
@ -374,6 +395,7 @@ impl Program {
pub fn replace_value(&mut self, source_range: SourceRange, new_value: Expr) { pub fn replace_value(&mut self, source_range: SourceRange, new_value: Expr) {
for item in &mut self.body { for item in &mut self.body {
match item { match item {
BodyItem::ImportStatement(_) => {} // TODO
BodyItem::ExpressionStatement(ref mut expression_statement) => expression_statement BodyItem::ExpressionStatement(ref mut expression_statement) => expression_statement
.expression .expression
.replace_value(source_range, new_value.clone()), .replace_value(source_range, new_value.clone()),
@ -388,16 +410,23 @@ impl Program {
} }
/// Get the variable declaration with the given name. /// 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<Definition<'_>> {
for item in &self.body { for item in &self.body {
match item { 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) => { BodyItem::ExpressionStatement(_expression_statement) => {
continue; continue;
} }
BodyItem::VariableDeclaration(variable_declaration) => { BodyItem::VariableDeclaration(variable_declaration) => {
for declaration in &variable_declaration.declarations { for declaration in &variable_declaration.declarations {
if declaration.id.name == name { 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)] #[ts(export)]
#[serde(tag = "type")] #[serde(tag = "type")]
pub enum BodyItem { pub enum BodyItem {
ImportStatement(Box<ImportStatement>),
ExpressionStatement(ExpressionStatement), ExpressionStatement(ExpressionStatement),
VariableDeclaration(VariableDeclaration), VariableDeclaration(VariableDeclaration),
ReturnStatement(ReturnStatement), ReturnStatement(ReturnStatement),
@ -462,6 +492,7 @@ pub enum BodyItem {
impl BodyItem { impl BodyItem {
pub fn compute_digest(&mut self) -> Digest { pub fn compute_digest(&mut self) -> Digest {
match self { match self {
BodyItem::ImportStatement(s) => s.compute_digest(),
BodyItem::ExpressionStatement(es) => es.compute_digest(), BodyItem::ExpressionStatement(es) => es.compute_digest(),
BodyItem::VariableDeclaration(vs) => vs.compute_digest(), BodyItem::VariableDeclaration(vs) => vs.compute_digest(),
BodyItem::ReturnStatement(rs) => rs.compute_digest(), BodyItem::ReturnStatement(rs) => rs.compute_digest(),
@ -470,6 +501,7 @@ impl BodyItem {
pub fn start(&self) -> usize { pub fn start(&self) -> usize {
match self { match self {
BodyItem::ImportStatement(stmt) => stmt.start(),
BodyItem::ExpressionStatement(expression_statement) => expression_statement.start(), BodyItem::ExpressionStatement(expression_statement) => expression_statement.start(),
BodyItem::VariableDeclaration(variable_declaration) => variable_declaration.start(), BodyItem::VariableDeclaration(variable_declaration) => variable_declaration.start(),
BodyItem::ReturnStatement(return_statement) => return_statement.start(), BodyItem::ReturnStatement(return_statement) => return_statement.start(),
@ -478,6 +510,7 @@ impl BodyItem {
pub fn end(&self) -> usize { pub fn end(&self) -> usize {
match self { match self {
BodyItem::ImportStatement(stmt) => stmt.end(),
BodyItem::ExpressionStatement(expression_statement) => expression_statement.end(), BodyItem::ExpressionStatement(expression_statement) => expression_statement.end(),
BodyItem::VariableDeclaration(variable_declaration) => variable_declaration.end(), BodyItem::VariableDeclaration(variable_declaration) => variable_declaration.end(),
BodyItem::ReturnStatement(return_statement) => return_statement.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<Identifier>,
pub start: usize,
pub end: usize,
pub digest: Option<Digest>,
}
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<String> {
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<ImportItem>,
pub path: String,
pub raw_path: String,
pub digest: Option<Digest>,
}
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<String> {
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)] #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, Bake)]
#[databake(path = kcl_lib::ast::types)] #[databake(path = kcl_lib::ast::types)]
#[ts(export)] #[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)] #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, Bake)]
#[databake(path = kcl_lib::ast::types)] #[databake(path = kcl_lib::ast::types)]
#[ts(export)] #[ts(export)]
@ -1292,6 +1469,8 @@ pub struct VariableDeclaration {
pub start: usize, pub start: usize,
pub end: usize, pub end: usize,
pub declarations: Vec<VariableDeclarator>, pub declarations: Vec<VariableDeclarator>,
#[serde(default, skip_serializing_if = "ItemVisibility::is_default")]
pub visibility: ItemVisibility,
pub kind: VariableKind, // Change to enum if there are specific values pub kind: VariableKind, // Change to enum if there are specific values
pub digest: Option<Digest>, pub digest: Option<Digest>,
@ -1337,14 +1516,16 @@ impl VariableDeclaration {
for declarator in &mut slf.declarations { for declarator in &mut slf.declarations {
hasher.update(declarator.compute_digest()); hasher.update(declarator.compute_digest());
} }
hasher.update(slf.visibility.digestable_id());
hasher.update(slf.kind.digestable_id()); hasher.update(slf.kind.digestable_id());
}); });
pub fn new(declarations: Vec<VariableDeclarator>, kind: VariableKind) -> Self { pub fn new(declarations: Vec<VariableDeclarator>, visibility: ItemVisibility, kind: VariableKind) -> Self {
Self { Self {
start: 0, start: 0,
end: 0, end: 0,
declarations, declarations,
visibility,
kind, kind,
digest: None, digest: None,
} }

View File

@ -24,6 +24,8 @@ use crate::{
executor::{DefaultPlanes, IdGenerator}, executor::{DefaultPlanes, IdGenerator},
}; };
use super::ExecutionKind;
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq)]
enum SocketHealth { enum SocketHealth {
Active, Active,
@ -46,6 +48,8 @@ pub struct EngineConnection {
default_planes: Arc<RwLock<Option<DefaultPlanes>>>, default_planes: Arc<RwLock<Option<DefaultPlanes>>>,
/// If the server sends session data, it'll be copied to here. /// If the server sends session data, it'll be copied to here.
session_data: Arc<Mutex<Option<ModelingSessionData>>>, session_data: Arc<Mutex<Option<ModelingSessionData>>>,
execution_kind: Arc<Mutex<ExecutionKind>>,
} }
pub struct TcpRead { pub struct TcpRead {
@ -300,6 +304,7 @@ impl EngineConnection {
batch_end: Arc::new(Mutex::new(IndexMap::new())), batch_end: Arc::new(Mutex::new(IndexMap::new())),
default_planes: Default::default(), default_planes: Default::default(),
session_data, session_data,
execution_kind: Default::default(),
}) })
} }
} }
@ -314,6 +319,18 @@ impl EngineManager for EngineConnection {
self.batch_end.clone() 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( async fn default_planes(
&self, &self,
id_generator: &mut IdGenerator, id_generator: &mut IdGenerator,

View File

@ -22,10 +22,13 @@ use crate::{
executor::{DefaultPlanes, IdGenerator}, executor::{DefaultPlanes, IdGenerator},
}; };
use super::ExecutionKind;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct EngineConnection { pub struct EngineConnection {
batch: Arc<Mutex<Vec<(WebSocketRequest, crate::executor::SourceRange)>>>, batch: Arc<Mutex<Vec<(WebSocketRequest, crate::executor::SourceRange)>>>,
batch_end: Arc<Mutex<IndexMap<uuid::Uuid, (WebSocketRequest, crate::executor::SourceRange)>>>, batch_end: Arc<Mutex<IndexMap<uuid::Uuid, (WebSocketRequest, crate::executor::SourceRange)>>>,
execution_kind: Arc<Mutex<ExecutionKind>>,
} }
impl EngineConnection { impl EngineConnection {
@ -33,6 +36,7 @@ impl EngineConnection {
Ok(EngineConnection { Ok(EngineConnection {
batch: Arc::new(Mutex::new(Vec::new())), batch: Arc::new(Mutex::new(Vec::new())),
batch_end: Arc::new(Mutex::new(IndexMap::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() 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( async fn default_planes(
&self, &self,
_id_generator: &mut IdGenerator, _id_generator: &mut IdGenerator,

View File

@ -9,6 +9,7 @@ use kittycad_modeling_cmds as kcmc;
use wasm_bindgen::prelude::*; use wasm_bindgen::prelude::*;
use crate::{ use crate::{
engine::ExecutionKind,
errors::{KclError, KclErrorDetails}, errors::{KclError, KclErrorDetails},
executor::{DefaultPlanes, IdGenerator}, executor::{DefaultPlanes, IdGenerator},
}; };
@ -42,6 +43,7 @@ pub struct EngineConnection {
manager: Arc<EngineCommandManager>, manager: Arc<EngineCommandManager>,
batch: Arc<Mutex<Vec<(WebSocketRequest, crate::executor::SourceRange)>>>, batch: Arc<Mutex<Vec<(WebSocketRequest, crate::executor::SourceRange)>>>,
batch_end: Arc<Mutex<IndexMap<uuid::Uuid, (WebSocketRequest, crate::executor::SourceRange)>>>, batch_end: Arc<Mutex<IndexMap<uuid::Uuid, (WebSocketRequest, crate::executor::SourceRange)>>>,
execution_kind: Arc<Mutex<ExecutionKind>>,
} }
// Safety: WebAssembly will only ever run in a single-threaded context. // Safety: WebAssembly will only ever run in a single-threaded context.
@ -54,6 +56,7 @@ impl EngineConnection {
manager: Arc::new(manager), manager: Arc::new(manager),
batch: Arc::new(Mutex::new(Vec::new())), batch: Arc::new(Mutex::new(Vec::new())),
batch_end: Arc::new(Mutex::new(IndexMap::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() 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( async fn default_planes(
&self, &self,
_id_generator: &mut IdGenerator, _id_generator: &mut IdGenerator,

View File

@ -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(); 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] #[async_trait::async_trait]
pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static { pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
/// Get the batch of commands to be sent to the engine. /// 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. /// Get the batch of end commands to be sent to the engine.
fn batch_end(&self) -> Arc<Mutex<IndexMap<uuid::Uuid, (WebSocketRequest, crate::executor::SourceRange)>>>; fn batch_end(&self) -> Arc<Mutex<IndexMap<uuid::Uuid, (WebSocketRequest, crate::executor::SourceRange)>>>;
/// 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. /// Get the default planes.
async fn default_planes( async fn default_planes(
&self, &self,
@ -102,6 +126,10 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
source_range: crate::executor::SourceRange, source_range: crate::executor::SourceRange,
cmd: &ModelingCmd, cmd: &ModelingCmd,
) -> Result<(), crate::errors::KclError> { ) -> 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 { let req = WebSocketRequest::ModelingCmdReq(ModelingCmdReq {
cmd: cmd.clone(), cmd: cmd.clone(),
cmd_id: id.into(), cmd_id: id.into(),

View File

@ -14,6 +14,8 @@ pub enum KclError {
Syntax(KclErrorDetails), Syntax(KclErrorDetails),
#[error("semantic: {0:?}")] #[error("semantic: {0:?}")]
Semantic(KclErrorDetails), Semantic(KclErrorDetails),
#[error("import cycle: {0:?}")]
ImportCycle(KclErrorDetails),
#[error("type: {0:?}")] #[error("type: {0:?}")]
Type(KclErrorDetails), Type(KclErrorDetails),
#[error("unimplemented: {0:?}")] #[error("unimplemented: {0:?}")]
@ -52,6 +54,7 @@ impl KclError {
KclError::Lexical(_) => "lexical", KclError::Lexical(_) => "lexical",
KclError::Syntax(_) => "syntax", KclError::Syntax(_) => "syntax",
KclError::Semantic(_) => "semantic", KclError::Semantic(_) => "semantic",
KclError::ImportCycle(_) => "import cycle",
KclError::Type(_) => "type", KclError::Type(_) => "type",
KclError::Unimplemented(_) => "unimplemented", KclError::Unimplemented(_) => "unimplemented",
KclError::Unexpected(_) => "unexpected", KclError::Unexpected(_) => "unexpected",
@ -68,6 +71,7 @@ impl KclError {
KclError::Lexical(e) => e.source_ranges.clone(), KclError::Lexical(e) => e.source_ranges.clone(),
KclError::Syntax(e) => e.source_ranges.clone(), KclError::Syntax(e) => e.source_ranges.clone(),
KclError::Semantic(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::Type(e) => e.source_ranges.clone(),
KclError::Unimplemented(e) => e.source_ranges.clone(), KclError::Unimplemented(e) => e.source_ranges.clone(),
KclError::Unexpected(e) => e.source_ranges.clone(), KclError::Unexpected(e) => e.source_ranges.clone(),
@ -85,6 +89,7 @@ impl KclError {
KclError::Lexical(e) => &e.message, KclError::Lexical(e) => &e.message,
KclError::Syntax(e) => &e.message, KclError::Syntax(e) => &e.message,
KclError::Semantic(e) => &e.message, KclError::Semantic(e) => &e.message,
KclError::ImportCycle(e) => &e.message,
KclError::Type(e) => &e.message, KclError::Type(e) => &e.message,
KclError::Unimplemented(e) => &e.message, KclError::Unimplemented(e) => &e.message,
KclError::Unexpected(e) => &e.message, KclError::Unexpected(e) => &e.message,
@ -102,6 +107,7 @@ impl KclError {
KclError::Lexical(e) => e.source_ranges = source_ranges, KclError::Lexical(e) => e.source_ranges = source_ranges,
KclError::Syntax(e) => e.source_ranges = source_ranges, KclError::Syntax(e) => e.source_ranges = source_ranges,
KclError::Semantic(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::Type(e) => e.source_ranges = source_ranges,
KclError::Unimplemented(e) => e.source_ranges = source_ranges, KclError::Unimplemented(e) => e.source_ranges = source_ranges,
KclError::Unexpected(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::Lexical(e) => e.source_ranges.extend(source_ranges),
KclError::Syntax(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::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::Type(e) => e.source_ranges.extend(source_ranges),
KclError::Unimplemented(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), KclError::Unexpected(e) => e.source_ranges.extend(source_ranges),

View File

@ -1,6 +1,9 @@
//! The executor for the AST. //! The executor for the AST.
use std::{collections::HashMap, sync::Arc}; use std::{
collections::{HashMap, HashSet},
sync::Arc,
};
use anyhow::Result; use anyhow::Result;
use async_recursion::async_recursion; use async_recursion::async_recursion;
@ -23,12 +26,12 @@ type Point3D = kcmc::shared::Point3d<f64>;
use crate::{ use crate::{
ast::types::{ ast::types::{
human_friendly_type, BodyItem, Expr, ExpressionStatement, FunctionExpression, KclNone, Program, human_friendly_type, BodyItem, Expr, ExpressionStatement, FunctionExpression, ImportStatement, ItemVisibility,
ReturnStatement, TagDeclarator, KclNone, Program, ReturnStatement, TagDeclarator,
}, },
engine::EngineManager, engine::{EngineManager, ExecutionKind},
errors::{KclError, KclErrorDetails}, errors::{KclError, KclErrorDetails},
fs::FileManager, fs::{FileManager, FileSystem},
settings::types::UnitLength, settings::types::UnitLength,
std::{FnAsArg, StdLib}, std::{FnAsArg, StdLib},
}; };
@ -47,6 +50,14 @@ pub struct ExecState {
/// The current value of the pipe operator returned from the previous /// The current value of the pipe operator returned from the previous
/// expression. If we're not currently in a pipeline, this will be None. /// expression. If we're not currently in a pipeline, this will be None.
pub pipe_value: Option<KclValue>, pub pipe_value: Option<KclValue>,
/// Identifiers that have been exported from the current module.
pub module_exports: HashSet<String>,
/// 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<std::path::PathBuf>,
/// 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<String>,
} }
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)] #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
@ -391,6 +402,20 @@ impl KclValue {
KclValue::Face(_) => "Face", 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<SketchSet> for KclValue { impl From<SketchSet> for KclValue {
@ -1504,6 +1529,14 @@ impl From<SourceRange> 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 { impl From<&ExpressionStatement> for Metadata {
fn from(exp_statement: &ExpressionStatement) -> Self { fn from(exp_statement: &ExpressionStatement) -> Self {
Self { Self {
@ -1967,8 +2000,9 @@ impl ExecutorContext {
program: &crate::ast::types::Program, program: &crate::ast::types::Program,
memory: Option<ProgramMemory>, memory: Option<ProgramMemory>,
id_generator: IdGenerator, id_generator: IdGenerator,
project_directory: Option<String>,
) -> Result<ExecState, KclError> { ) -> Result<ExecState, KclError> {
self.run_with_session_data(program, memory, id_generator) self.run_with_session_data(program, memory, id_generator, project_directory)
.await .await
.map(|x| x.0) .map(|x| x.0)
} }
@ -1980,6 +2014,7 @@ impl ExecutorContext {
program: &crate::ast::types::Program, program: &crate::ast::types::Program,
memory: Option<ProgramMemory>, memory: Option<ProgramMemory>,
id_generator: IdGenerator, id_generator: IdGenerator,
project_directory: Option<String>,
) -> Result<(ExecState, Option<ModelingSessionData>), KclError> { ) -> Result<(ExecState, Option<ModelingSessionData>), KclError> {
let memory = if let Some(memory) = memory { let memory = if let Some(memory) = memory {
memory.clone() memory.clone()
@ -1989,6 +2024,7 @@ impl ExecutorContext {
let mut exec_state = ExecState { let mut exec_state = ExecState {
memory, memory,
id_generator, id_generator,
project_directory,
..Default::default() ..Default::default()
}; };
// Before we even start executing the program, set the units. // Before we even start executing the program, set the units.
@ -2027,6 +2063,91 @@ impl ExecutorContext {
// Iterate over the body of the program. // Iterate over the body of the program.
for statement in &program.body { for statement in &program.body {
match statement { 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::<Vec<_>>()
.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) => { BodyItem::ExpressionStatement(expression_statement) => {
let metadata = Metadata::from(expression_statement); let metadata = Metadata::from(expression_statement);
last_expr = Some( last_expr = Some(
@ -2053,7 +2174,21 @@ impl ExecutorContext {
StatementKind::Declaration { name: &var_name }, StatementKind::Declaration { name: &var_name },
) )
.await?; .await?;
let is_function = memory_item.is_function();
exec_state.memory.add(&var_name, memory_item, source_range)?; 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; last_expr = None;
} }
@ -2158,8 +2293,9 @@ impl ExecutorContext {
&self, &self,
program: &Program, program: &Program,
id_generator: IdGenerator, id_generator: IdGenerator,
project_directory: Option<String>,
) -> Result<TakeSnapshot> { ) -> Result<TakeSnapshot> {
let _ = self.run(program, None, id_generator).await?; let _ = self.run(program, None, id_generator, project_directory).await?;
// Zoom to fit. // Zoom to fit.
self.engine self.engine
@ -2304,7 +2440,7 @@ mod tests {
settings: Default::default(), settings: Default::default(),
context_type: ContextType::Mock, 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) Ok(exec_state.memory)
} }

View File

@ -37,6 +37,19 @@ impl FileSystem for FileManager {
}) })
} }
async fn read_to_string<P: AsRef<std::path::Path> + std::marker::Send + std::marker::Sync>(
&self,
path: P,
source_range: crate::executor::SourceRange,
) -> Result<String, KclError> {
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<P: AsRef<std::path::Path> + std::marker::Send + std::marker::Sync>( async fn exists<P: AsRef<std::path::Path> + std::marker::Send + std::marker::Sync>(
&self, &self,
path: P, path: P,

View File

@ -23,6 +23,13 @@ pub trait FileSystem: Clone {
source_range: crate::executor::SourceRange, source_range: crate::executor::SourceRange,
) -> Result<Vec<u8>, crate::errors::KclError>; ) -> Result<Vec<u8>, crate::errors::KclError>;
/// Read a file from the local file system.
async fn read_to_string<P: AsRef<std::path::Path> + std::marker::Send + std::marker::Sync>(
&self,
path: P,
source_range: crate::executor::SourceRange,
) -> Result<String, crate::errors::KclError>;
/// Check if a file exists on the local file system. /// Check if a file exists on the local file system.
async fn exists<P: AsRef<std::path::Path> + std::marker::Send + std::marker::Sync>( async fn exists<P: AsRef<std::path::Path> + std::marker::Send + std::marker::Sync>(
&self, &self,

View File

@ -78,6 +78,22 @@ impl FileSystem for FileManager {
Ok(bytes) Ok(bytes)
} }
async fn read_to_string<P: AsRef<std::path::Path> + std::marker::Send + std::marker::Sync>(
&self,
path: P,
source_range: crate::executor::SourceRange,
) -> Result<String, KclError> {
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<P: AsRef<std::path::Path> + std::marker::Send + std::marker::Sync>( async fn exists<P: AsRef<std::path::Path> + std::marker::Send + std::marker::Sync>(
&self, &self,
path: P, path: P,

View File

@ -596,7 +596,7 @@ impl Backend {
.clear_scene(&mut id_generator, SourceRange::default()) .clear_scene(&mut id_generator, SourceRange::default())
.await?; .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, Ok(exec_state) => exec_state,
Err(err) => { Err(err) => {
self.memory_map.remove(params.uri.as_str()); self.memory_map.remove(params.uri.as_str());

View File

@ -12,6 +12,13 @@ pub(crate) mod parser_impl;
pub const PIPE_SUBSTITUTION_OPERATOR: &str = "%"; pub const PIPE_SUBSTITUTION_OPERATOR: &str = "%";
pub const PIPE_OPERATOR: &str = "|>"; pub const PIPE_OPERATOR: &str = "|>";
/// Parse the given KCL code into an AST.
pub fn parse(code: &str) -> Result<Program, KclError> {
let tokens = crate::token::lexer(code)?;
let parser = Parser::new(tokens);
parser.ast()
}
pub struct Parser { pub struct Parser {
pub tokens: Vec<Token>, pub tokens: Vec<Token>,
pub unknown_tokens: Vec<Token>, pub unknown_tokens: Vec<Token>,

View File

@ -12,10 +12,10 @@ use crate::{
ast::types::{ ast::types::{
ArrayExpression, ArrayRangeExpression, BinaryExpression, BinaryOperator, BinaryPart, BodyItem, CallExpression, ArrayExpression, ArrayRangeExpression, BinaryExpression, BinaryOperator, BinaryPart, BodyItem, CallExpression,
CommentStyle, ElseIf, Expr, ExpressionStatement, FnArgPrimitive, FnArgType, FunctionExpression, Identifier, CommentStyle, ElseIf, Expr, ExpressionStatement, FnArgPrimitive, FnArgType, FunctionExpression, Identifier,
IfExpression, Literal, LiteralIdentifier, LiteralValue, MemberExpression, MemberObject, NonCodeMeta, IfExpression, ImportItem, ImportStatement, ItemVisibility, Literal, LiteralIdentifier, LiteralValue,
NonCodeNode, NonCodeValue, ObjectExpression, ObjectProperty, Parameter, PipeExpression, PipeSubstitution, MemberExpression, MemberObject, NonCodeMeta, NonCodeNode, NonCodeValue, ObjectExpression, ObjectProperty,
Program, ReturnStatement, TagDeclarator, UnaryExpression, UnaryOperator, ValueMeta, VariableDeclaration, Parameter, PipeExpression, PipeSubstitution, Program, ReturnStatement, TagDeclarator, UnaryExpression,
VariableDeclarator, VariableKind, UnaryOperator, ValueMeta, VariableDeclaration, VariableDeclarator, VariableKind,
}, },
errors::{KclError, KclErrorDetails}, errors::{KclError, KclErrorDetails},
executor::SourceRange, executor::SourceRange,
@ -956,8 +956,10 @@ fn body_items_within_function(i: TokenSlice) -> PResult<WithinFunction> {
// Any of the body item variants, each of which can optionally be followed by a comment. // 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. // If there is a comment, it may be preceded by whitespace.
let item = dispatch! {peek(any); 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), (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" => Token { ref value, .. } if value == "return" =>
(return_stmt.map(BodyItem::ReturnStatement), opt(noncode_just_after_code)).map(WithinFunction::BodyItem), (return_stmt.map(BodyItem::ReturnStatement), opt(noncode_just_after_code)).map(WithinFunction::BodyItem),
token if !token.is_code_token() => { token if !token.is_code_token() => {
@ -1122,6 +1124,111 @@ pub fn function_body(i: TokenSlice) -> PResult<Program> {
}) })
} }
fn import_stmt(i: TokenSlice) -> PResult<Box<ImportStatement>> {
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<ImportItem> {
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<Token> {
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`. /// Parse a return statement of a user-defined function, e.g. `return x`.
pub fn return_stmt(i: TokenSlice) -> PResult<ReturnStatement> { pub fn return_stmt(i: TokenSlice) -> PResult<ReturnStatement> {
let start = any let start = any
@ -1214,6 +1321,19 @@ fn possible_operands(i: TokenSlice) -> PResult<Expr> {
.parse_next(i) .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)> { fn declaration_keyword(i: TokenSlice) -> PResult<(VariableKind, Token)> {
let res = any let res = any
.verify_map(|token: Token| token.declaration_keyword().map(|kw| (kw, token))) .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. /// Parse a variable/constant declaration.
fn declaration(i: TokenSlice) -> PResult<VariableDeclaration> { fn declaration(i: TokenSlice) -> PResult<VariableDeclaration> {
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)?; let decl_token = opt(declaration_keyword).parse_next(i)?;
if decl_token.is_some() { if decl_token.is_some() {
// If there was a declaration keyword like `fn`, then it must be followed by some spaces. // 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<VariableDeclaration> {
"an identifier, which becomes name you're binding the value to", "an identifier, which becomes name you're binding the value to",
)) ))
.parse_next(i)?; .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) (*kind, token.start, token.end)
} else { } else {
(VariableKind::Const, id.start(), id.end()) (VariableKind::Const, id.start(), id.end())
}; };
if let Some(token) = visibility_token {
start = token.start;
}
ignore_whitespace(i); ignore_whitespace(i);
equals(i)?; equals(i)?;
@ -1288,6 +1414,7 @@ fn declaration(i: TokenSlice) -> PResult<VariableDeclaration> {
init: val, init: val,
digest: None, digest: None,
}], }],
visibility,
kind, kind,
digest: None, digest: None,
}) })

View File

@ -1,6 +1,5 @@
--- ---
source: kcl/src/parser/parser_impl.rs source: kcl/src/parser/parser_impl.rs
assertion_line: 3423
expression: actual expression: actual
--- ---
{ {

View File

@ -1,6 +1,5 @@
--- ---
source: kcl/src/parser/parser_impl.rs source: kcl/src/parser/parser_impl.rs
assertion_line: 3470
expression: actual expression: actual
--- ---
{ {

View File

@ -30,7 +30,7 @@ async fn do_execute_and_snapshot(ctx: &ExecutorContext, code: &str) -> anyhow::R
let program = parser.ast()?; let program = parser.ast()?;
let snapshot = ctx let snapshot = ctx
.execute_and_prepare_snapshot(&program, IdGenerator::default()) .execute_and_prepare_snapshot(&program, IdGenerator::default(), None)
.await?; .await?;
// Create a temporary file to write the output to. // Create a temporary file to write the output to.

View File

@ -7,7 +7,11 @@ use serde::{Deserialize, Serialize};
use tower_lsp::lsp_types::SemanticTokenType; use tower_lsp::lsp_types::SemanticTokenType;
use winnow::stream::ContainsToken; 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; mod tokeniser;
@ -196,6 +200,16 @@ impl Token {
vec![self.as_source_range()] vec![self.as_source_range()]
} }
pub fn visibility_keyword(&self) -> Option<ItemVisibility> {
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? /// Is this token the beginning of a variable/function declaration?
/// If so, what kind? /// If so, what kind?
/// If not, returns None. /// If not, returns None.

View File

@ -169,11 +169,17 @@ fn string(i: &mut Located<&str>) -> PResult<Token> {
Ok(Token::from_range(range, TokenType::String, value.to_string())) Ok(Token::from_range(range, TokenType::String, value.to_string()))
} }
fn keyword(i: &mut Located<&str>) -> PResult<Token> { fn import_keyword(i: &mut Located<&str>) -> PResult<Token> {
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<Token> {
// These are the keywords themselves. // These are the keywords themselves.
let keyword_candidates = alt(( let keyword_candidates = alt((
"if", "else", "for", "while", "return", "break", "continue", "fn", "let", "mut", "loop", "true", "false", "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 // 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. // the start of a normal word.
@ -185,6 +191,10 @@ fn keyword(i: &mut Located<&str>) -> PResult<Token> {
Ok(Token::from_range(range, TokenType::Keyword, value.to_owned())) Ok(Token::from_range(range, TokenType::Keyword, value.to_owned()))
} }
fn keyword(i: &mut Located<&str>) -> PResult<Token> {
alt((import_keyword, unambiguous_keywords)).parse_next(i)
}
fn type_(i: &mut Located<&str>) -> PResult<Token> { fn type_(i: &mut Located<&str>) -> PResult<Token> {
// These are the types themselves. // These are the types themselves.
let type_candidates = alt(("string", "number", "bool", "sketch", "sketch_surface", "solid")); let type_candidates = alt(("string", "number", "bool", "sketch", "sketch_surface", "solid"));
@ -1572,4 +1582,28 @@ const things = "things"
assert_tokens(expected, actual); 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);
}
} }

View File

@ -3,9 +3,9 @@ use std::fmt::Write;
use crate::{ use crate::{
ast::types::{ ast::types::{
ArrayExpression, ArrayRangeExpression, BinaryExpression, BinaryOperator, BinaryPart, BodyItem, CallExpression, ArrayExpression, ArrayRangeExpression, BinaryExpression, BinaryOperator, BinaryPart, BodyItem, CallExpression,
Expr, FormatOptions, FunctionExpression, IfExpression, Literal, LiteralIdentifier, LiteralValue, Expr, FormatOptions, FunctionExpression, IfExpression, ImportStatement, ItemVisibility, Literal,
MemberExpression, MemberObject, NonCodeValue, ObjectExpression, PipeExpression, Program, TagDeclarator, LiteralIdentifier, LiteralValue, MemberExpression, MemberObject, NonCodeValue, ObjectExpression,
UnaryExpression, VariableDeclaration, VariableKind, PipeExpression, Program, TagDeclarator, UnaryExpression, VariableDeclaration, VariableKind,
}, },
parser::PIPE_OPERATOR, parser::PIPE_OPERATOR,
}; };
@ -17,6 +17,7 @@ impl Program {
.body .body
.iter() .iter()
.map(|statement| match statement.clone() { .map(|statement| match statement.clone() {
BodyItem::ImportStatement(stmt) => stmt.recast(options, indentation_level),
BodyItem::ExpressionStatement(expression_statement) => { BodyItem::ExpressionStatement(expression_statement) => {
expression_statement expression_statement
.expression .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 { impl Expr {
pub(crate) fn recast(&self, options: &FormatOptions, indentation_level: usize, is_in_pipe: bool) -> String { pub(crate) fn recast(&self, options: &FormatOptions, indentation_level: usize, is_in_pipe: bool) -> String {
match &self { match &self {
@ -168,7 +190,11 @@ impl CallExpression {
impl VariableDeclaration { impl VariableDeclaration {
pub fn recast(&self, options: &FormatOptions, indentation_level: usize) -> String { pub fn recast(&self, options: &FormatOptions, indentation_level: usize) -> String {
let indentation = options.get_indentation(indentation_level); 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 { let keyword = match self.kind {
VariableKind::Fn => "fn ", VariableKind::Fn => "fn ",
VariableKind::Const => "", VariableKind::Const => "",
@ -581,6 +607,46 @@ mod tests {
assert_eq!(output, input); 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] #[test]
fn test_recast_bug_fn_in_fn() { fn test_recast_bug_fn_in_fn() {
let some_program_string = r#"// Start point (top left) let some_program_string = r#"// Start point (top left)

View File

@ -9,6 +9,7 @@ use crate::{
pub enum Node<'a> { pub enum Node<'a> {
Program(&'a types::Program), Program(&'a types::Program),
ImportStatement(&'a types::ImportStatement),
ExpressionStatement(&'a types::ExpressionStatement), ExpressionStatement(&'a types::ExpressionStatement),
VariableDeclaration(&'a types::VariableDeclaration), VariableDeclaration(&'a types::VariableDeclaration),
ReturnStatement(&'a types::ReturnStatement), ReturnStatement(&'a types::ReturnStatement),
@ -42,6 +43,7 @@ impl From<&Node<'_>> for SourceRange {
fn from(node: &Node) -> Self { fn from(node: &Node) -> Self {
match node { match node {
Node::Program(p) => SourceRange([p.start, p.end]), 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::ExpressionStatement(e) => SourceRange([e.start(), e.end()]),
Node::VariableDeclaration(v) => SourceRange([v.start(), v.end()]), Node::VariableDeclaration(v) => SourceRange([v.start(), v.end()]),
Node::ReturnStatement(r) => SourceRange([r.start(), r.end()]), Node::ReturnStatement(r) => SourceRange([r.start(), r.end()]),
@ -79,6 +81,7 @@ macro_rules! impl_from {
} }
impl_from!(Node, Program); impl_from!(Node, Program);
impl_from!(Node, ImportStatement);
impl_from!(Node, ExpressionStatement); impl_from!(Node, ExpressionStatement);
impl_from!(Node, VariableDeclaration); impl_from!(Node, VariableDeclaration);
impl_from!(Node, ReturnStatement); impl_from!(Node, ReturnStatement);

View File

@ -277,6 +277,12 @@ where
// We don't walk a BodyItem since it's an enum itself. // We don't walk a BodyItem since it's an enum itself.
match node { match node {
BodyItem::ImportStatement(xs) => {
if !f.walk(xs.as_ref().into())? {
return Ok(false);
}
Ok(true)
}
BodyItem::ExpressionStatement(xs) => { BodyItem::ExpressionStatement(xs) => {
if !f.walk(xs.into())? { if !f.walk(xs.into())? {
return Ok(false); return Ok(false);

View File

@ -20,6 +20,7 @@ pub async fn execute_wasm(
units: &str, units: &str,
engine_manager: kcl_lib::engine::conn_wasm::EngineCommandManager, engine_manager: kcl_lib::engine::conn_wasm::EngineCommandManager,
fs_manager: kcl_lib::fs::wasm::FileSystemManager, fs_manager: kcl_lib::fs::wasm::FileSystemManager,
project_directory: Option<String>,
is_mock: bool, is_mock: bool,
) -> Result<JsValue, String> { ) -> Result<JsValue, String> {
console_error_panic_hook::set_once(); console_error_panic_hook::set_once();
@ -62,7 +63,7 @@ pub async fn execute_wasm(
}; };
let exec_state = ctx let exec_state = ctx
.run(&program, Some(memory), id_generator) .run(&program, Some(memory), id_generator, project_directory)
.await .await
.map_err(String::from)?; .map_err(String::from)?;

View File

@ -0,0 +1 @@
export three = 3

View File

@ -0,0 +1,5 @@
export fn foo = () => { return 0 }
// This interacts with the engine.
part001 = startSketchOn('XY')
|> startProfileAt([0, 0], %)

View File

@ -0,0 +1,3 @@
export fn identity = (x) => {
return x
}

View File

@ -0,0 +1 @@
import three from "export_constant.kcl"

View File

@ -0,0 +1,3 @@
import two from "import_cycle2.kcl"
export fn one = () => { return two() - 1 }

View File

@ -0,0 +1,3 @@
import three from "import_cycle3.kcl"
export fn two = () => { return three() - 1 }

View File

@ -0,0 +1,3 @@
import one from "import_cycle1.kcl"
export fn three = () => { return one() + one() + one() }

View File

@ -0,0 +1 @@
import cube from "../cube.kcl"

View File

@ -0,0 +1,4 @@
fn foo = () => {
import identity from "identity.kcl"
return 1
}

View File

@ -0,0 +1,6 @@
if true {
import identity from "identity.kcl"
1
} else {
2
}

View File

@ -0,0 +1 @@
import foo from "export_side_effect.kcl"

View File

@ -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")

View File

@ -0,0 +1,7 @@
export fn increment = (x) => {
return x + 1
}
export fn decrement = (x) => {
return x - 1
}

View File

@ -2,6 +2,7 @@ use kcl_lib::{
ast::types::Program, ast::types::Program,
errors::KclError, errors::KclError,
executor::{ExecutorContext, IdGenerator}, executor::{ExecutorContext, IdGenerator},
parser,
}; };
macro_rules! gen_test { 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) { async fn run(code: &str) {
let (ctx, program, id_generator) = setup(code).await; 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 { match res {
Ok(state) => { Ok(state) => {
println!("{:#?}", state.memory); println!("{:#?}", state.memory);
@ -57,12 +76,27 @@ async fn setup(program: &str) -> (ExecutorContext, Program, IdGenerator) {
async fn run_fail(code: &str) -> KclError { async fn run_fail(code: &str) -> KclError {
let (ctx, program, id_generator) = setup(code).await; 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."); panic!("Expected this KCL program to fail, but it (incorrectly) never threw an error.");
}; };
e 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!(property_of_object);
gen_test!(index_of_array); gen_test!(index_of_array);
gen_test!(comparisons); gen_test!(comparisons);
@ -111,5 +145,31 @@ gen_test!(if_else);
// "syntax: blocks inside an if/else expression must end in an expression" // "syntax: blocks inside an if/else expression must end in an expression"
// ); // );
gen_test_fail!(comparisons_multiple, "syntax: Invalid number: true"); 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!(add_lots);
gen_test!(double_map); gen_test!(double_map);

View File

@ -35,7 +35,7 @@ async fn setup(code: &str, name: &str) -> Result<(ExecutorContext, Program, uuid
let parser = kcl_lib::parser::Parser::new(tokens); let parser = kcl_lib::parser::Parser::new(tokens);
let program = parser.ast()?; let program = parser.ast()?;
let ctx = kcl_lib::executor::ExecutorContext::new(&client, Default::default()).await?; 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. // We need to get the sketch ID.
// Get the sketch ID from memory. // Get the sketch ID from memory.