Foreign imports go thru parallelization too (#6488)

* start of parallelizing forign imports

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* clippy

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* remove_printlns

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* remove shit that doesnt work

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* remove shit that doesnt work

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* put back

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* put back

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* put back

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* multiple

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* remove println

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* clone docs

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* add whole module import docs

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* add whole module import docs

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
This commit is contained in:
Jess Frazelle
2025-04-24 20:25:02 -07:00
committed by GitHub
parent bff13f6bfe
commit 5c0ca52291
35 changed files with 17153 additions and 185 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -10,7 +10,7 @@ isolated from other files as a separate module.
When you define a function, you can use `export` before it to make it available
to other modules.
```
```kcl
// util.kcl
export fn increment(x) {
return x + 1
@ -31,11 +31,11 @@ Imported files _must_ be in the same project so that units are uniform across
modules. This means that it must be in the same directory.
Import statements must be at the top-level of a file. It is not allowed to have
an `import` statement inside a function or in the body of an if-else.
an `import` statement inside a function or in the body of an ifelse.
Multiple functions can be exported in a file.
```
```kcl
// util.kcl
export fn increment(x) {
return x + 1
@ -58,6 +58,211 @@ Imported symbols can be renamed for convenience or to avoid name collisions.
import increment as inc, decrement as dec from "util.kcl"
```
---
## Functions vs `clone`
There are two common patterns for reusing geometry:
1. **Wrap the construction in a function** flexible and fully parametric.
2. **Duplicate an existing object with `clone`** lightningfast, but an exact
duplicate.
### Parametric function example
```kcl
fn cube(center) {
return startSketchOn(XY)
|> startProfileAt([center[0] - 10, center[1] - 10], %)
|> line(endAbsolute = [center[0] + 10, center[1] - 10])
|> line(endAbsolute = [center[0] + 10, center[1] + 10])
|> line(endAbsolute = [center[0] - 10, center[1] + 10])
|> close()
|> extrude(length = 10)
}
myCube = cube([0, 0])
```
*Pros*
- Any argument can be a parameter size, position, appearance, etc.
- Works great inside loops, arrays, or optimisation sweeps.
*Cons*
- Every invocation rebuilds the entire feature tree.
- **Slower** than a straight duplicate each call is its own render job.
### `clone` example
```kcl
sketch001 = startSketchOn(-XZ)
|> circle(center = [0, 0], radius = 10)
|> extrude(length = 5)
|> appearance(color = "#ff0000", metalness = 90, roughness = 90)
sketch002 = clone(sketch001) // ✓ instant copy
```
*Pros*
- Roughly an O(1) operation we just duplicate the underlying engine handle.
- Perfect when you need ten identical bolts or two copies of the same imported STEP file.
*Cons*
- **Not parametric** the clone is exactly the same shape as the source.
- If you need to tweak dimensions perinstance, youre back to a function.
> **Rule of thumb** Reach for `clone` when the geometry is already what you want. Reach for a function when you need customisation.
---
## Modulelevel parallelism
Under the hood, the Design Studio runs **every module in parallel** where it can. This means:
- The toplevel code of `foo.kcl`, `bar.kcl`, and `baz.kcl` all start executing immediately and concurrently.
- Imports that read foreign files (STEP/OBJ/…) overlap their I/O and background render.
- CPUbound calculations in separate modules get their own worker threads.
### Why modules beat onebigfile
If you shoehorn everything into `main.kcl`, each statement runs sequentially:
```norun
import "big.step" as gizmo // blocks main while reading
gizmo |> translate(x=50) // blocks again while waiting for render
```
Split `gizmo` into its own file and the read/render can overlap whatever else `main.kcl` is doing.
```norun
// gizmo.kcl (worker A)
import "big.step"
// main.kcl (worker B)
import "gizmo.kcl" as gizmo // nonblocking
// ... other setup ...
gizmo |> translate(x=50) // only blocks here
```
### Gotcha: defining but **not** calling functions
Defining a function inside a module is instantaneous we just record the bytecode. The heavy lifting happens when the function is **called**. So:
```norun
// util.kcl
export fn makeBolt(size) { /* … expensive CAD … */ }
```
If `main.kcl` waits until the very end to call `makeBolt`, *none* of that work was parallelised youve pushed the cost back onto the serial tail of your script.
**Better:** call it early or move the invocation into another module.
```norun
// bolt_instance.kcl
import makeBolt from "util.kcl"
bolt = makeBolt(5) // executed in parallel
bolt
```
Now `main.kcl` can `import "bolt_instance.kcl" as bolt` and get the result that was rendered while it was busy doing other things.
---
## Whole module import
You can also import the whole module. This is useful if you want to use the
result of a module as a variable, like a part.
```norun
import "tests/inputs/cube.kcl" as cube
cube
|> translate(x=10)
```
This imports the whole module and makes it available as `cube`. You can then
use it like any other object. The `cube` variable is now a reference to the
result of the module. This means that if you change the module, the `cube`
variable will change as well.
In `cube.kcl`, you cannot have multiple objects. It has to be a single part. If
you have multiple objects, you will get an error. This is because the module is
expected to return a single object that can be used as a variable.
You also cannot assign that object to a variable. This is because the module is
expected to return a single object that can be used as a variable.
So for example, this is not allowed:
```norun
... a bunch of code to create cube and cube2 ...
myUnion = union([cube, cube2])
```
What you need to do instead is:
```norun
... a bunch of code to create cube and cube2 ...
union([cube, cube2])
```
That way the last line will return the union of the two objects.
Or what you could do instead is:
```norun
... a bunch of code to create cube and cube2 ...
myUnion = union([cube, cube2])
myUnion
```
This will return the union of the two objects, but it will not be assigned to a
variable. This is because the module is expected to return a single object that
can be used as a variable.
---
## Multiple instances of the same import
Whether you are importing a file from another CAD system or a KCL file, that
file represents object(s) in memory. If you import the same file multiple times,
it will only be rendered once.
If you want to have multiple instances of the same object, you can use the
[`clone`](/docs/kcl/clone) function. This will render a new instance of the object in memory.
```norun
import cube from "tests/inputs/cube.kcl"
cube
|> translate(x=10)
clone(cube)
|> translate(x=20)
```
In the sample above, the `cube` object is imported from a KCL file. The first
instance is translated 10 units in the x direction. The second instance is
cloned and translated 20 units in the x direction. The two instances are now
separate objects in memory, and can be manipulated independently.
Here is an example with a file from another CAD system:
```kcl
import "tests/inputs/cube.step" as cube
cube
|> translate(x=10)
clone(cube)
|> translate(x=20)
```
---
## Importing files from other CAD systems
`import` can also be used to import files from other CAD systems. The format of the statement is the
@ -69,25 +274,17 @@ import "tests/inputs/cube.obj"
// Use `cube` just like a KCL object.
```
```norun
import "tests/inputs/cube-2.sldprt" as cube
```kcl
import "tests/inputs/cube.sldprt" as cube
// Use `cube` just like a KCL object.
```
You can make the file format explicit using a format attribute (useful if using a different
extension), e.g.,
```norun
@(format = obj)
import "tests/inputs/cube"
```
For formats lacking unit data (such as STL, OBJ, or PLY files), the default
unit of measurement is millimeters. Alternatively you may specify the unit
by using an attirbute. Likewise, you can also specify a coordinate system. E.g.,
by using an attribute. Likewise, you can also specify a coordinate system. E.g.,
```norun
```kcl
@(unitLength = ft, coords = opengl)
import "tests/inputs/cube.obj"
```
@ -110,97 +307,55 @@ Coordinate systems:
- `opengl`, forward: +Z, up: +Y, handedness: right
- `vulkan`, forward: +Z, up: -Y, handedness: left
### Performance
---
Parallelized foreign-file imports now let you overlap file reads, initialization,
## Performance deepdive for foreignfile imports
Parallelized foreignfile imports now let you overlap file reads, initialization,
and rendering. To maximize throughput, you need to understand the three distinct
stages—reading, initializing (background render start), and invocation (blocking)
—and structure your code to defer blocking operations until the end.
#### Foreign Import Execution Stages
### Foreign import execution stages
1. **Import (Read) Stage**
```norun
1. **Import (Read / Initialization) Stage**
```kcl
import "tests/inputs/cube.step" as cube
```
- Reads the file from disk and makes its API available.
- **Does _not_** start Engine rendering or block your script.
- Starts engine rendering but **does not block** your script.
- This kickstarts the render pipeline while you keep executing other code.
2. **Initialization (Background Render) Stage**
```norun
2. **Invocation (Blocking) Stage**
```kcl
import "tests/inputs/cube.step" as cube
myCube = cube // <- This line starts background rendering
```
- Invoking the imported symbol (assignment or plain call) triggers Engine rendering _in the background_.
- This kickstarts the render pipeline but doesnt block—you can continue other work while the Engine processes the model.
3. **Invocation (Blocking) Stage**
```norun
import "tests/inputs/cube.step" as cube
myCube = cube
myCube
|> translate(z=10) // <- This line blocks
cube
|> translate(z=10) // ← blocks here only
```
- Any method call (e.g., `translate`, `scale`, `rotate`) waits for the background render to finish before applying transformations.
- This is the only point where your script will block.
> **Nuance:** Foreign imports differ from pure KCL modules—calling the same import symbol multiple times (e.g., `screw` twice) starts background rendering twice.
### Best practices
#### Best Practices
#### 1. Defer blocking calls
##### 1. Defer Blocking Calls
Initialize early but delay all transformations until after your heavy computation:
```norun
import "tests/inputs/cube.step" as cube // 1) Read
myCube = cube // 2) Background render starts
```kcl
import "tests/inputs/cube.step" as cube // 1) Read / Background render starts
// --- perform other operations and calculations or setup here ---
// --- perform other operations and calculations here ---
myCube
|> translate(z=10) // 3) Blocks only here
```
##### 2. Encapsulate Imports in Modules
Keep `main.kcl` free of reads and initialization; wrap them:
```norun
// imports.kcl
import "tests/inputs/cube.step" as cube // Read only
export myCube = cube // Kick off rendering
```
```norun
// main.kcl
import myCube from "imports.kcl" // Import the initialized object
// ... computations ...
myCube
|> translate(z=10) // Blocking call at the end
```
##### 3. Avoid Immediate Method Calls
```norun
import "tests/inputs/cube.step" as cube
cube
|> translate(z=10) // Blocks immediately, negating parallelism
|> translate(z=10) // 2) Blocks only here
```
Both calling methods right on `cube` immediately or leaving an implicit import without assignment introduce blocking.
#### 2. Split heavy work into separate modules
#### Future Improvements
Place computationally expensive or IOheavy work into its own module so it can render in parallel while `main.kcl` continues.
#### Future improvements
Upcoming releases will autoanalyse dependencies and only block when truly necessary. Until then, explicit deferral will give you the best performance.
Upcoming releases will autoanalyze dependencies and only block when truly necessary. Until then, explicit deferral and modular wrapping give you the best performance.

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

View File

@ -1076,7 +1076,7 @@ mod test {
#[for_each_std_mod]
#[tokio::test(flavor = "multi_thread")]
async fn test_examples() {
async fn kcl_test_examples() {
let std = walk_prelude();
let mut errs = Vec::new();
for d in std {

View File

@ -459,7 +459,7 @@ impl ExecutorContext {
exec_state.add_path_to_source_id(resolved_path.clone(), id);
let format = super::import::format_from_annotations(attrs, path, source_range)?;
let geom = super::import::import_foreign(path, format, exec_state, self, source_range).await?;
exec_state.add_module(id, resolved_path, ModuleRepr::Foreign(geom));
exec_state.add_module(id, resolved_path, ModuleRepr::Foreign(geom, None));
Ok(id)
}
ImportPath::Std { .. } => {
@ -501,7 +501,7 @@ impl ExecutorContext {
*cache = Some((val, er, items.clone()));
(er, items)
}),
ModuleRepr::Foreign(geom) => Err(KclError::Semantic(KclErrorDetails {
ModuleRepr::Foreign(geom, _) => Err(KclError::Semantic(KclErrorDetails {
message: "Cannot import items from foreign modules".to_owned(),
source_ranges: vec![geom.source_range],
})),
@ -546,9 +546,20 @@ impl ExecutorContext {
Err(e) => Err(e),
}
}
ModuleRepr::Foreign(geom) => super::import::send_to_engine(geom.clone(), self)
ModuleRepr::Foreign(_, Some(imported)) => Ok(Some(imported.clone())),
ModuleRepr::Foreign(geom, cached) => {
let result = super::import::send_to_engine(geom.clone(), self)
.await
.map(|geom| Some(KclValue::ImportedGeometry(geom))),
.map(|geom| Some(KclValue::ImportedGeometry(geom)));
match result {
Ok(val) => {
*cached = val.clone();
Ok(val)
}
Err(e) => Err(e),
}
}
ModuleRepr::Dummy => unreachable!(),
};

View File

@ -752,7 +752,12 @@ impl ExecutorContext {
let mut universe = std::collections::HashMap::new();
let default_planes = self.engine.get_default_planes().read().await.clone();
crate::walk::import_universe(self, &program.ast, &mut universe, exec_state)
crate::walk::import_universe(
self,
&ModuleRepr::Kcl(program.ast.clone(), None),
&mut universe,
exec_state,
)
.await
.map_err(|err| {
let module_id_to_module_path: IndexMap<ModuleId, ModulePath> = exec_state
@ -799,16 +804,12 @@ impl ExecutorContext {
#[allow(clippy::type_complexity)]
let (results_tx, mut results_rx): (
tokio::sync::mpsc::Sender<(
ModuleId,
ModulePath,
Result<(Option<KclValue>, EnvironmentRef, Vec<String>), KclError>,
)>,
tokio::sync::mpsc::Sender<(ModuleId, ModulePath, Result<ModuleRepr, KclError>)>,
tokio::sync::mpsc::Receiver<_>,
) = tokio::sync::mpsc::channel(1);
for module in modules {
let Some((import_stmt, module_id, module_path, program)) = universe.get(&module) else {
let Some((import_stmt, module_id, module_path, repr)) = universe.get(&module) else {
return Err(KclErrorWithOutputs::no_outputs(KclError::Internal(KclErrorDetails {
message: format!("Module {module} not found in universe"),
source_ranges: Default::default(),
@ -816,12 +817,41 @@ impl ExecutorContext {
};
let module_id = *module_id;
let module_path = module_path.clone();
let program = program.clone();
let repr = repr.clone();
let exec_state = exec_state.clone();
let exec_ctxt = self.clone();
let results_tx = results_tx.clone();
let source_range = SourceRange::from(import_stmt);
let exec_module = async |exec_ctxt: &ExecutorContext,
repr: &ModuleRepr,
module_id: ModuleId,
module_path: &ModulePath,
exec_state: &mut ExecState,
source_range: SourceRange|
-> Result<ModuleRepr, KclError> {
match repr {
ModuleRepr::Kcl(program, _) => {
let result = exec_ctxt
.exec_module_from_ast(program, module_id, module_path, exec_state, source_range, false)
.await;
result.map(|val| ModuleRepr::Kcl(program.clone(), Some(val)))
}
ModuleRepr::Foreign(geom, _) => {
let result = crate::execution::import::send_to_engine(geom.clone(), exec_ctxt)
.await
.map(|geom| Some(KclValue::ImportedGeometry(geom)));
result.map(|val| ModuleRepr::Foreign(geom.clone(), val))
}
ModuleRepr::Dummy | ModuleRepr::Root => Err(KclError::Internal(KclErrorDetails {
message: format!("Module {module_path} not found in universe"),
source_ranges: vec![source_range],
})),
}
};
#[cfg(target_arch = "wasm32")]
{
wasm_bindgen_futures::spawn_local(async move {
@ -829,14 +859,13 @@ impl ExecutorContext {
let mut exec_state = exec_state;
let exec_ctxt = exec_ctxt;
let result = exec_ctxt
.exec_module_from_ast(
&program,
let result = exec_module(
&exec_ctxt,
&repr,
module_id,
&module_path,
&mut exec_state,
source_range,
false,
)
.await;
@ -852,14 +881,13 @@ impl ExecutorContext {
let mut exec_state = exec_state;
let exec_ctxt = exec_ctxt;
let result = exec_ctxt
.exec_module_from_ast(
&program,
let result = exec_module(
&exec_ctxt,
&repr,
module_id,
&module_path,
&mut exec_state,
source_range,
false,
)
.await;
@ -875,13 +903,24 @@ impl ExecutorContext {
while let Some((module_id, _, result)) = results_rx.recv().await {
match result {
Ok((val, session_data, variables)) => {
Ok(new_repr) => {
let mut repr = exec_state.global.module_infos[&module_id].take_repr();
let ModuleRepr::Kcl(_, cache) = &mut repr else {
continue;
match &mut repr {
ModuleRepr::Kcl(_, cache) => {
let ModuleRepr::Kcl(_, session_data) = new_repr else {
unreachable!();
};
*cache = Some((val, session_data, variables));
*cache = session_data;
}
ModuleRepr::Foreign(_, cache) => {
let ModuleRepr::Foreign(_, session_data) = new_repr else {
unreachable!();
};
*cache = session_data;
}
ModuleRepr::Dummy | ModuleRepr::Root => unreachable!(),
}
exec_state.global.module_infos[&module_id].restore_repr(repr);
}

View File

@ -124,7 +124,7 @@ pub enum ModuleRepr {
Root,
// AST, memory, exported names
Kcl(Node<Program>, Option<(Option<KclValue>, EnvironmentRef, Vec<String>)>),
Foreign(PreImportedGeometry),
Foreign(PreImportedGeometry, Option<KclValue>),
Dummy,
}

View File

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

View File

@ -8,7 +8,7 @@ use anyhow::Result;
use crate::{
errors::KclErrorDetails,
modules::{ModulePath, ModuleRepr},
parsing::ast::types::{ImportPath, ImportStatement, Node as AstNode, NodeRef, Program},
parsing::ast::types::{ImportPath, ImportStatement, Node as AstNode},
walk::{Node, Visitable},
ExecState, ExecutorContext, KclError, ModuleId, SourceRange,
};
@ -20,7 +20,7 @@ type Dependency = (String, String);
type Graph = Vec<Dependency>;
type DependencyInfo = (AstNode<ImportStatement>, ModuleId, ModulePath, AstNode<Program>);
type DependencyInfo = (AstNode<ImportStatement>, ModuleId, ModulePath, ModuleRepr);
type Universe = HashMap<String, DependencyInfo>;
/// Process a number of programs, returning the graph of dependencies.
@ -32,9 +32,9 @@ type Universe = HashMap<String, DependencyInfo>;
pub fn import_graph(progs: &Universe, ctx: &ExecutorContext) -> Result<Vec<Vec<String>>, KclError> {
let mut graph = Graph::new();
for (name, (_, _, _, program)) in progs.iter() {
for (name, (_, _, _, repr)) in progs.iter() {
graph.extend(
import_dependencies(program, ctx)?
import_dependencies(repr, ctx)?
.into_iter()
.map(|(dependency, _, _)| (name.clone(), dependency))
.collect::<Vec<_>>(),
@ -118,18 +118,19 @@ fn topsort(all_modules: &[&str], graph: Graph) -> Result<Vec<Vec<String>>, KclEr
type ImportDependencies = Vec<(String, AstNode<ImportStatement>, ModulePath)>;
pub(crate) fn import_dependencies(
prog: NodeRef<Program>,
ctx: &ExecutorContext,
) -> Result<ImportDependencies, KclError> {
let ret = Arc::new(Mutex::new(vec![]));
pub(crate) fn import_dependencies(repr: &ModuleRepr, ctx: &ExecutorContext) -> Result<ImportDependencies, KclError> {
let ModuleRepr::Kcl(prog, _) = repr else {
// It has no dependencies, so return an empty list.
return Ok(vec![]);
};
let ret = Arc::new(Mutex::new(vec![]));
fn walk(ret: Arc<Mutex<ImportDependencies>>, node: Node<'_>, ctx: &ExecutorContext) -> Result<(), KclError> {
if let Node::ImportStatement(is) = node {
// We only care about Kcl imports for now.
if let ImportPath::Kcl { filename } = &is.path {
// We only care about Kcl and Foreign imports for now.
let resolved_path = ModulePath::from_import_path(&is.path, &ctx.settings.project_directory);
match &is.path {
ImportPath::Kcl { filename } => {
// We need to lock the mutex to push the dependency.
// This is a bit of a hack, but it works for now.
ret.lock()
@ -141,6 +142,19 @@ pub(crate) fn import_dependencies(
})?
.push((filename.to_string(), is.clone(), resolved_path));
}
ImportPath::Foreign { path } => {
ret.lock()
.map_err(|err| {
KclError::Internal(KclErrorDetails {
message: format!("Failed to lock mutex: {}", err),
source_ranges: Default::default(),
})
})?
.push((path.to_string(), is.clone(), resolved_path));
}
ImportPath::Std { .. } => { // do nothing
}
}
}
for child in node.children().iter() {
@ -164,11 +178,11 @@ pub(crate) fn import_dependencies(
pub(crate) async fn import_universe(
ctx: &ExecutorContext,
prog: NodeRef<'_, Program>,
repr: &ModuleRepr,
out: &mut Universe,
exec_state: &mut ExecState,
) -> Result<(), KclError> {
let modules = import_dependencies(prog, ctx)?;
let modules = import_dependencies(repr, ctx)?;
for (filename, import_stmt, module_path) in modules {
if out.contains_key(&filename) {
continue;
@ -178,26 +192,21 @@ pub(crate) async fn import_universe(
.open_module(&import_stmt.path, &[], exec_state, Default::default())
.await?;
let program = {
let repr = {
let Some(module_info) = exec_state.get_module(module_id) else {
return Err(KclError::Internal(KclErrorDetails {
message: format!("Module {} not found", module_id),
source_ranges: vec![import_stmt.into()],
}));
};
let ModuleRepr::Kcl(program, _) = &module_info.repr else {
// if it's not a KCL module we can skip it since it has no
// dependencies.
continue;
};
program.clone()
module_info.repr.clone()
};
out.insert(
filename.clone(),
(import_stmt.clone(), module_id, module_path.clone(), program.clone()),
(import_stmt.clone(), module_id, module_path.clone(), repr.clone()),
);
Box::pin(import_universe(ctx, &program, out, exec_state)).await?;
Box::pin(import_universe(ctx, &repr, out, exec_state)).await?;
}
Ok(())
@ -206,7 +215,7 @@ pub(crate) async fn import_universe(
#[cfg(test)]
mod tests {
use super::*;
use crate::parsing::ast::types::ImportSelector;
use crate::parsing::ast::types::{ImportSelector, Program};
macro_rules! kcl {
( $kcl:expr ) => {{
@ -224,7 +233,7 @@ mod tests {
}),
ModuleId::default(),
ModulePath::Local { value: "".into() },
program,
ModuleRepr::Kcl(program, None),
)
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

View File

@ -0,0 +1,3 @@
import "../inputs/cube.step" as cube
clone(cube)

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,6 @@
---
source: kcl-lib/src/simulation_tests.rs
description: Artifact graph flowchart multiple-foreign-imports-all-render.kcl
extension: md
snapshot_kind: binary
---

View File

@ -0,0 +1,3 @@
```mermaid
flowchart LR
```

View File

@ -0,0 +1,504 @@
---
source: kcl-lib/src/simulation_tests.rs
description: Result of parsing multiple-foreign-imports-all-render.kcl
---
{
"Ok": {
"body": [
{
"commentStart": 0,
"end": 0,
"path": {
"type": "Foreign",
"path": "../inputs/cube.step"
},
"selector": {
"type": "None",
"alias": {
"commentStart": 0,
"end": 0,
"name": "cube",
"start": 0,
"type": "Identifier"
}
},
"start": 0,
"type": "ImportStatement",
"type": "ImportStatement"
},
{
"commentStart": 0,
"end": 0,
"path": {
"type": "Kcl",
"filename": "othercube.kcl"
},
"selector": {
"type": "None",
"alias": {
"commentStart": 0,
"end": 0,
"name": "othercube",
"start": 0,
"type": "Identifier"
}
},
"start": 0,
"type": "ImportStatement",
"type": "ImportStatement"
},
{
"commentStart": 0,
"end": 0,
"path": {
"type": "Kcl",
"filename": "anothercube.kcl"
},
"selector": {
"type": "None",
"alias": {
"commentStart": 0,
"end": 0,
"name": "anothercube",
"start": 0,
"type": "Identifier"
}
},
"start": 0,
"type": "ImportStatement",
"type": "ImportStatement"
},
{
"commentStart": 0,
"declaration": {
"commentStart": 0,
"end": 0,
"id": {
"commentStart": 0,
"end": 0,
"name": "model",
"start": 0,
"type": "Identifier"
},
"init": {
"abs_path": false,
"commentStart": 0,
"end": 0,
"name": {
"commentStart": 0,
"end": 0,
"name": "cube",
"start": 0,
"type": "Identifier"
},
"path": [],
"start": 0,
"type": "Name",
"type": "Name"
},
"start": 0,
"type": "VariableDeclarator"
},
"end": 0,
"kind": "const",
"start": 0,
"type": "VariableDeclaration",
"type": "VariableDeclaration"
},
{
"commentStart": 0,
"end": 0,
"expression": {
"body": [
{
"abs_path": false,
"commentStart": 0,
"end": 0,
"name": {
"commentStart": 0,
"end": 0,
"name": "othercube",
"start": 0,
"type": "Identifier"
},
"path": [],
"start": 0,
"type": "Name",
"type": "Name"
},
{
"arguments": [
{
"type": "LabeledArg",
"label": {
"commentStart": 0,
"end": 0,
"name": "x",
"start": 0,
"type": "Identifier"
},
"arg": {
"commentStart": 0,
"end": 0,
"raw": "1020",
"start": 0,
"type": "Literal",
"type": "Literal",
"value": {
"value": 1020.0,
"suffix": "None"
}
}
}
],
"callee": {
"abs_path": false,
"commentStart": 0,
"end": 0,
"name": {
"commentStart": 0,
"end": 0,
"name": "translate",
"start": 0,
"type": "Identifier"
},
"path": [],
"start": 0,
"type": "Name"
},
"commentStart": 0,
"end": 0,
"start": 0,
"type": "CallExpressionKw",
"type": "CallExpressionKw",
"unlabeled": null
},
{
"arguments": [
{
"type": "LabeledArg",
"label": {
"commentStart": 0,
"end": 0,
"name": "color",
"start": 0,
"type": "Identifier"
},
"arg": {
"commentStart": 0,
"end": 0,
"raw": "\"#ff001f\"",
"start": 0,
"type": "Literal",
"type": "Literal",
"value": "#ff001f"
}
},
{
"type": "LabeledArg",
"label": {
"commentStart": 0,
"end": 0,
"name": "metalness",
"start": 0,
"type": "Identifier"
},
"arg": {
"commentStart": 0,
"end": 0,
"raw": "50",
"start": 0,
"type": "Literal",
"type": "Literal",
"value": {
"value": 50.0,
"suffix": "None"
}
}
},
{
"type": "LabeledArg",
"label": {
"commentStart": 0,
"end": 0,
"name": "roughness",
"start": 0,
"type": "Identifier"
},
"arg": {
"commentStart": 0,
"end": 0,
"raw": "50",
"start": 0,
"type": "Literal",
"type": "Literal",
"value": {
"value": 50.0,
"suffix": "None"
}
}
}
],
"callee": {
"abs_path": false,
"commentStart": 0,
"end": 0,
"name": {
"commentStart": 0,
"end": 0,
"name": "appearance",
"start": 0,
"type": "Identifier"
},
"path": [],
"start": 0,
"type": "Name"
},
"commentStart": 0,
"end": 0,
"start": 0,
"type": "CallExpressionKw",
"type": "CallExpressionKw",
"unlabeled": null
}
],
"commentStart": 0,
"end": 0,
"start": 0,
"type": "PipeExpression",
"type": "PipeExpression"
},
"start": 0,
"type": "ExpressionStatement",
"type": "ExpressionStatement"
},
{
"commentStart": 0,
"end": 0,
"expression": {
"body": [
{
"abs_path": false,
"commentStart": 0,
"end": 0,
"name": {
"commentStart": 0,
"end": 0,
"name": "anothercube",
"start": 0,
"type": "Identifier"
},
"path": [],
"start": 0,
"type": "Name",
"type": "Name"
},
{
"arguments": [
{
"type": "LabeledArg",
"label": {
"commentStart": 0,
"end": 0,
"name": "x",
"start": 0,
"type": "Identifier"
},
"arg": {
"argument": {
"commentStart": 0,
"end": 0,
"raw": "1020",
"start": 0,
"type": "Literal",
"type": "Literal",
"value": {
"value": 1020.0,
"suffix": "None"
}
},
"commentStart": 0,
"end": 0,
"operator": "-",
"start": 0,
"type": "UnaryExpression",
"type": "UnaryExpression"
}
}
],
"callee": {
"abs_path": false,
"commentStart": 0,
"end": 0,
"name": {
"commentStart": 0,
"end": 0,
"name": "translate",
"start": 0,
"type": "Identifier"
},
"path": [],
"start": 0,
"type": "Name"
},
"commentStart": 0,
"end": 0,
"start": 0,
"type": "CallExpressionKw",
"type": "CallExpressionKw",
"unlabeled": null
},
{
"arguments": [
{
"type": "LabeledArg",
"label": {
"commentStart": 0,
"end": 0,
"name": "color",
"start": 0,
"type": "Identifier"
},
"arg": {
"commentStart": 0,
"end": 0,
"raw": "\"#ff0000\"",
"start": 0,
"type": "Literal",
"type": "Literal",
"value": "#ff0000"
}
},
{
"type": "LabeledArg",
"label": {
"commentStart": 0,
"end": 0,
"name": "metalness",
"start": 0,
"type": "Identifier"
},
"arg": {
"commentStart": 0,
"end": 0,
"raw": "50",
"start": 0,
"type": "Literal",
"type": "Literal",
"value": {
"value": 50.0,
"suffix": "None"
}
}
},
{
"type": "LabeledArg",
"label": {
"commentStart": 0,
"end": 0,
"name": "roughness",
"start": 0,
"type": "Identifier"
},
"arg": {
"commentStart": 0,
"end": 0,
"raw": "50",
"start": 0,
"type": "Literal",
"type": "Literal",
"value": {
"value": 50.0,
"suffix": "None"
}
}
}
],
"callee": {
"abs_path": false,
"commentStart": 0,
"end": 0,
"name": {
"commentStart": 0,
"end": 0,
"name": "appearance",
"start": 0,
"type": "Identifier"
},
"path": [],
"start": 0,
"type": "Name"
},
"commentStart": 0,
"end": 0,
"start": 0,
"type": "CallExpressionKw",
"type": "CallExpressionKw",
"unlabeled": null
}
],
"commentStart": 0,
"end": 0,
"start": 0,
"type": "PipeExpression",
"type": "PipeExpression"
},
"start": 0,
"type": "ExpressionStatement",
"type": "ExpressionStatement"
}
],
"commentStart": 0,
"end": 0,
"nonCodeMeta": {
"nonCodeNodes": {
"2": [
{
"commentStart": 0,
"end": 0,
"start": 0,
"type": "NonCodeNode",
"value": {
"type": "newLine"
}
}
],
"3": [
{
"commentStart": 0,
"end": 0,
"start": 0,
"type": "NonCodeNode",
"value": {
"type": "newLine"
}
}
],
"4": [
{
"commentStart": 0,
"end": 0,
"start": 0,
"type": "NonCodeNode",
"value": {
"type": "newLine"
}
}
],
"5": [
{
"commentStart": 0,
"end": 0,
"start": 0,
"type": "NonCodeNode",
"value": {
"type": "newLine"
}
}
]
},
"startNodes": []
},
"start": 0
}
}

View File

@ -0,0 +1,22 @@
import "../inputs/cube.step" as cube
import "othercube.kcl" as othercube
import "anothercube.kcl" as anothercube
model = cube
othercube
|> translate(x=1020)
|> appearance(
color = "#ff001f",
metalness = 50,
roughness = 50
)
anothercube
|> translate(x=-1020)
|> appearance(
color = "#ff0000",
metalness = 50,
roughness = 50
)

View File

@ -0,0 +1,96 @@
---
source: kcl-lib/src/simulation_tests.rs
description: Operations executed multiple-foreign-imports-all-render.kcl
---
[
{
"type": "GroupBegin",
"group": {
"type": "ModuleInstance",
"name": "cube",
"moduleId": 6
},
"sourceRange": []
},
{
"type": "GroupEnd"
},
{
"type": "GroupBegin",
"group": {
"type": "ModuleInstance",
"name": "othercube",
"moduleId": 7
},
"sourceRange": []
},
{
"type": "GroupBegin",
"group": {
"type": "ModuleInstance",
"name": "cube",
"moduleId": 6
},
"sourceRange": []
},
{
"type": "GroupEnd"
},
{
"labeledArgs": {
"geometry": {
"value": {
"type": "ImportedGeometry",
"artifact_id": "[uuid]"
},
"sourceRange": []
}
},
"name": "clone",
"sourceRange": [],
"type": "StdLibCall",
"unlabeledArg": null
},
{
"type": "GroupEnd"
},
{
"type": "GroupBegin",
"group": {
"type": "ModuleInstance",
"name": "anothercube",
"moduleId": 8
},
"sourceRange": []
},
{
"type": "GroupBegin",
"group": {
"type": "ModuleInstance",
"name": "cube",
"moduleId": 6
},
"sourceRange": []
},
{
"type": "GroupEnd"
},
{
"labeledArgs": {
"geometry": {
"value": {
"type": "ImportedGeometry",
"artifact_id": "[uuid]"
},
"sourceRange": []
}
},
"name": "clone",
"sourceRange": [],
"type": "StdLibCall",
"unlabeledArg": null
},
{
"type": "GroupEnd"
}
]

View File

@ -0,0 +1,3 @@
import "../inputs/cube.step" as cube
clone(cube)

View File

@ -0,0 +1,25 @@
---
source: kcl-lib/src/simulation_tests.rs
description: Variables in memory after executing multiple-foreign-imports-all-render.kcl
---
{
"anothercube": {
"type": "Module",
"value": 8
},
"cube": {
"type": "Module",
"value": 6
},
"model": {
"type": "ImportedGeometry",
"id": "[uuid]",
"value": [
"cube.step"
]
},
"othercube": {
"type": "Module",
"value": 7
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

View File

@ -0,0 +1,17 @@
---
source: kcl-lib/src/simulation_tests.rs
description: Result of unparsing multiple-foreign-imports-all-render.kcl
---
import "../inputs/cube.step" as cube
import "othercube.kcl" as othercube
import "anothercube.kcl" as anothercube
model = cube
othercube
|> translate(x = 1020)
|> appearance(color = "#ff001f", metalness = 50, roughness = 50)
anothercube
|> translate(x = -1020)
|> appearance(color = "#ff0000", metalness = 50, roughness = 50)

View File

@ -0,0 +1,7 @@
---
source: kcl-lib/src/simulation_tests.rs
description: Result of unparsing tests/multiple-foreign-imports-all-render/anothercube.kcl
---
import "../inputs/cube.step" as cube
clone(cube)

View File

@ -0,0 +1,7 @@
---
source: kcl-lib/src/simulation_tests.rs
description: Result of unparsing tests/multiple-foreign-imports-all-render/othercube.kcl
---
import "../inputs/cube.step" as cube
clone(cube)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 87 KiB

After

Width:  |  Height:  |  Size: 87 KiB