* Start porting artifact graph creation to Rust * Add most of artifact graph creation * Add handling loft command from recent PR * Refactor artifact merge code so that it errors when a new artifact type is added * Add sweep subtype * Finish implementation of build artifact graph * Fix wasm.ts to use new combined generated ts-rs file * Fix Rust lints * Fix lints * Fix up replacement code * Add artifact graph to WASM outcome * Add artifact graph to simulation test output * Add new artifact graph output snapshots * Fix wall field and reduce unreachable code * Change field order for subtype * Change subtype to be determined from the request, like the TS * Fix plane sweep_id * Condense code * Change ID types to be properly optional * Change to favor the new ID, the same as TS * Fix to make error impossible * Rename artifact type tag values to match TS * Fix name of field on Cap * Update outputs * Change to use Rust source range * Update output snapshots * Add conversion to mermaid mind map and add to snapshot tests * Add new mermaid mind map output * Add flowchart * Remove raw artifact graph from tests * Remove JSON artifact graph output * Update output file with header * Update output after adding flowchart * Fix flowchart to not have duplicate edges, one in each direction * Fix not not output duplicate edges in flowcharts * Change flowchart edge style to be more obvious when a direction is missing * Update output after deduplication of edges * Fix not not skip sketch-on-face artifacts * Add docs * Fix edge iteration order to be stable * Update output after fixing order * Port TS artifactGraph.test.ts tests to simulation tests * Add grouping segments and solid2ds with their path * Update output flowcharts since grouping paths * Remove TS artifactGraph tests * Remove unused d3 dependencies * Fix to track loft ID on paths * Add command ID to error messages * Move artifact graph test code to a separate file since it's a large file * Reduce function visibility * Remove TS artifact graph code * Fix spelling error with serde * Add TODO for edge cut consumed ID * Add comment about mermaid edge rank * Fix mermaid flowchart edge cuts to appear as children of their edges * Update output since fixing flowchart order * Fix to always build the artifact graph even when there's a KCL error * Add artifact graph to error output * Change optional ID merge to match TS * Remove redundant SourceRange definition * Remove Rust-flavored default source range function * Add helper for source range creation * Update doc comment for the website * Update docs after doc comment change * Fix to save engine responses in execution cache * Remove unused import * Fix to not call WASM function before beforeAll callback is run * Remove more unused imports
505 lines
14 KiB
TypeScript
505 lines
14 KiB
TypeScript
import fs from 'node:fs'
|
|
|
|
import {
|
|
assertParse,
|
|
ProgramMemory,
|
|
Sketch,
|
|
initPromise,
|
|
sketchFromKclValue,
|
|
defaultArtifactGraph,
|
|
topLevelRange,
|
|
} from './wasm'
|
|
import { enginelessExecutor } from '../lib/testHelpers'
|
|
import { KCLError } from './errors'
|
|
|
|
beforeAll(async () => {
|
|
await initPromise
|
|
})
|
|
|
|
describe('test executor', () => {
|
|
it('test assigning two variables, the second summing with the first', async () => {
|
|
const code = `const myVar = 5
|
|
const newVar = myVar + 1`
|
|
const mem = await exe(code)
|
|
expect(mem.get('myVar')?.value).toBe(5)
|
|
expect(mem.get('newVar')?.value).toBe(6)
|
|
})
|
|
it('test assigning a var with a string', async () => {
|
|
const code = `const myVar = "a str"`
|
|
const mem = await exe(code)
|
|
expect(mem.get('myVar')?.value).toBe('a str')
|
|
})
|
|
it('test assigning a var by cont concatenating two strings string execute', async () => {
|
|
const code = fs.readFileSync(
|
|
'./src/lang/testExamples/variableDeclaration.cado',
|
|
'utf-8'
|
|
)
|
|
const mem = await exe(code)
|
|
expect(mem.get('myVar')?.value).toBe('a str another str')
|
|
})
|
|
it('fn funcN = () => {} execute', async () => {
|
|
const mem = await exe(
|
|
[
|
|
'fn funcN = (a, b) => {',
|
|
' return a + b',
|
|
'}',
|
|
'const theVar = 60',
|
|
'const magicNum = funcN(9, theVar)',
|
|
].join('\n')
|
|
)
|
|
expect(mem.get('theVar')?.value).toBe(60)
|
|
expect(mem.get('magicNum')?.value).toBe(69)
|
|
})
|
|
it('sketch declaration', async () => {
|
|
let code = `const mySketch = startSketchOn('XY')
|
|
|> startProfileAt([0,0], %)
|
|
|> lineTo([0,2], %, $myPath)
|
|
|> lineTo([2,3], %)
|
|
|> lineTo([5,-1], %, $rightPath)
|
|
// |> close(%)
|
|
`
|
|
const mem = await exe(code)
|
|
// geo is three js buffer geometry and is very bloated to have in tests
|
|
const sk = mem.get('mySketch')
|
|
expect(sk?.type).toEqual('Sketch')
|
|
if (sk?.type !== 'Sketch') {
|
|
return
|
|
}
|
|
|
|
const minusGeo = sk?.value?.paths
|
|
expect(minusGeo).toEqual([
|
|
{
|
|
type: 'ToPoint',
|
|
to: [0, 2],
|
|
from: [0, 0],
|
|
__geoMeta: {
|
|
sourceRange: [72, 97, 0],
|
|
id: expect.any(String),
|
|
},
|
|
tag: {
|
|
end: 96,
|
|
start: 89,
|
|
type: 'TagDeclarator',
|
|
value: 'myPath',
|
|
},
|
|
},
|
|
{
|
|
type: 'ToPoint',
|
|
to: [2, 3],
|
|
from: [0, 2],
|
|
tag: null,
|
|
__geoMeta: {
|
|
sourceRange: [103, 119, 0],
|
|
id: expect.any(String),
|
|
},
|
|
},
|
|
{
|
|
type: 'ToPoint',
|
|
to: [5, -1],
|
|
from: [2, 3],
|
|
__geoMeta: {
|
|
sourceRange: [125, 154, 0],
|
|
id: expect.any(String),
|
|
},
|
|
tag: {
|
|
end: 153,
|
|
start: 143,
|
|
type: 'TagDeclarator',
|
|
value: 'rightPath',
|
|
},
|
|
},
|
|
])
|
|
})
|
|
|
|
it('pipe binary expression into call expression', async () => {
|
|
const code = [
|
|
'fn myFn = (a) => { return a + 1 }',
|
|
'const myVar = 5 + 1 |> myFn(%)',
|
|
].join('\n')
|
|
const mem = await exe(code)
|
|
expect(mem.get('myVar')?.value).toBe(7)
|
|
})
|
|
|
|
// Enable rotations #152
|
|
// it('rotated sketch', async () => {
|
|
// const code = [
|
|
// 'const mySk1 = startSketchAt([0,0])',
|
|
// ' |> lineTo([1,1], %)',
|
|
// ' |> lineTo([0, 1], %, "myPath")',
|
|
// ' |> lineTo([1, 1], %)',
|
|
// 'const rotated = rx(90, mySk1)',
|
|
// ].join('\n')
|
|
// const mem = await exe(code)
|
|
// expect(mem.get('mySk1')?.value).toHaveLength(3)
|
|
// expect(mem.get('rotated')?.type).toBe('Sketch')
|
|
// if (
|
|
// mem.get('mySk1')?.type !== 'Sketch' ||
|
|
// mem.get('rotated')?.type !== 'Sketch'
|
|
// )
|
|
// throw new Error('not a sketch')
|
|
// expect(mem.get('mySk1')?.rotation).toEqual([0, 0, 0, 1])
|
|
// expect(mem.get('rotated')?.rotation.map((a) => a.toFixed(4))).toEqual([
|
|
// '0.7071',
|
|
// '0.0000',
|
|
// '0.0000',
|
|
// '0.7071',
|
|
// ])
|
|
// })
|
|
|
|
it('execute pipe sketch into call expression', async () => {
|
|
// Enable rotations #152
|
|
const code = [
|
|
"const mySk1 = startSketchOn('XY')",
|
|
' |> startProfileAt([0,0], %)',
|
|
' |> lineTo([1,1], %)',
|
|
' |> lineTo([0, 1], %, $myPath)',
|
|
' |> lineTo([1,1], %)',
|
|
// ' |> rx(90, %)',
|
|
].join('\n')
|
|
const mem = await exe(code)
|
|
expect(mem.get('mySk1')).toEqual({
|
|
type: 'Sketch',
|
|
value: {
|
|
type: 'Sketch',
|
|
on: expect.any(Object),
|
|
start: {
|
|
to: [0, 0],
|
|
from: [0, 0],
|
|
tag: null,
|
|
__geoMeta: {
|
|
id: expect.any(String),
|
|
sourceRange: [39, 63, 0],
|
|
},
|
|
},
|
|
tags: {
|
|
myPath: {
|
|
__meta: [
|
|
{
|
|
sourceRange: [109, 116, 0],
|
|
},
|
|
],
|
|
type: 'TagIdentifier',
|
|
value: 'myPath',
|
|
info: expect.any(Object),
|
|
},
|
|
},
|
|
paths: [
|
|
{
|
|
type: 'ToPoint',
|
|
to: [1, 1],
|
|
from: [0, 0],
|
|
tag: null,
|
|
__geoMeta: {
|
|
sourceRange: [69, 85, 0],
|
|
id: expect.any(String),
|
|
},
|
|
},
|
|
{
|
|
type: 'ToPoint',
|
|
to: [0, 1],
|
|
from: [1, 1],
|
|
__geoMeta: {
|
|
sourceRange: [91, 117, 0],
|
|
id: expect.any(String),
|
|
},
|
|
tag: {
|
|
end: 116,
|
|
start: 109,
|
|
type: 'TagDeclarator',
|
|
value: 'myPath',
|
|
},
|
|
},
|
|
{
|
|
type: 'ToPoint',
|
|
to: [1, 1],
|
|
from: [0, 1],
|
|
tag: null,
|
|
__geoMeta: {
|
|
sourceRange: [123, 139, 0],
|
|
id: expect.any(String),
|
|
},
|
|
},
|
|
],
|
|
id: expect.any(String),
|
|
__meta: [{ sourceRange: [39, 63, 0] }],
|
|
},
|
|
})
|
|
})
|
|
it('execute array expression', async () => {
|
|
const code = ['const three = 3', "const yo = [1, '2', three, 4 + 5]"].join(
|
|
'\n'
|
|
)
|
|
const mem = await exe(code)
|
|
// TODO path to node is probably wrong here, zero indexes are not correct
|
|
expect(mem.get('three')).toEqual({
|
|
type: 'Number',
|
|
value: 3,
|
|
__meta: [
|
|
{
|
|
sourceRange: [14, 15, 0],
|
|
},
|
|
],
|
|
})
|
|
expect(mem.get('yo')).toEqual({
|
|
type: 'Array',
|
|
value: [
|
|
{ type: 'Number', value: 1, __meta: [{ sourceRange: [28, 29, 0] }] },
|
|
{ type: 'String', value: '2', __meta: [{ sourceRange: [31, 34, 0] }] },
|
|
{ type: 'Number', value: 3, __meta: [{ sourceRange: [14, 15, 0] }] },
|
|
{
|
|
type: 'Number',
|
|
value: 9,
|
|
__meta: [{ sourceRange: [43, 44, 0] }, { sourceRange: [47, 48, 0] }],
|
|
},
|
|
],
|
|
__meta: [
|
|
{
|
|
sourceRange: [27, 49, 0],
|
|
},
|
|
],
|
|
})
|
|
// Check that there are no other variables or environments.
|
|
expect(mem.numEnvironments()).toBe(1)
|
|
expect(mem.numVariables(0)).toBe(2)
|
|
})
|
|
it('execute object expression', async () => {
|
|
const code = [
|
|
'const three = 3',
|
|
"const yo = {aStr: 'str', anum: 2, identifier: three, binExp: 4 + 5}",
|
|
].join('\n')
|
|
const mem = await exe(code)
|
|
expect(mem.get('yo')).toEqual({
|
|
type: 'Object',
|
|
value: {
|
|
aStr: {
|
|
type: 'String',
|
|
value: 'str',
|
|
__meta: [{ sourceRange: [34, 39, 0] }],
|
|
},
|
|
anum: {
|
|
type: 'Number',
|
|
value: 2,
|
|
__meta: [{ sourceRange: [47, 48, 0] }],
|
|
},
|
|
identifier: {
|
|
type: 'Number',
|
|
value: 3,
|
|
__meta: [{ sourceRange: [14, 15, 0] }],
|
|
},
|
|
binExp: {
|
|
type: 'Number',
|
|
value: 9,
|
|
__meta: [{ sourceRange: [77, 78, 0] }, { sourceRange: [81, 82, 0] }],
|
|
},
|
|
},
|
|
__meta: [
|
|
{
|
|
sourceRange: [27, 83, 0],
|
|
},
|
|
],
|
|
})
|
|
})
|
|
it('execute memberExpression', async () => {
|
|
const code = ["const yo = {a: {b: '123'}}", "const myVar = yo.a['b']"].join(
|
|
'\n'
|
|
)
|
|
const mem = await exe(code)
|
|
expect(mem.get('myVar')).toEqual({
|
|
type: 'String',
|
|
value: '123',
|
|
__meta: [
|
|
{
|
|
sourceRange: [19, 24, 0],
|
|
},
|
|
],
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('testing math operators', () => {
|
|
it('can sum', async () => {
|
|
const code = ['const myVar = 1 + 2'].join('\n')
|
|
const mem = await exe(code)
|
|
expect(mem.get('myVar')?.value).toBe(3)
|
|
})
|
|
it('can subtract', async () => {
|
|
const code = ['const myVar = 1 - 2'].join('\n')
|
|
const mem = await exe(code)
|
|
expect(mem.get('myVar')?.value).toBe(-1)
|
|
})
|
|
it('can multiply', async () => {
|
|
const code = ['const myVar = 1 * 2'].join('\n')
|
|
const mem = await exe(code)
|
|
expect(mem.get('myVar')?.value).toBe(2)
|
|
})
|
|
it('can divide', async () => {
|
|
const code = ['const myVar = 1 / 2'].join('\n')
|
|
const mem = await exe(code)
|
|
expect(mem.get('myVar')?.value).toBe(0.5)
|
|
})
|
|
it('can modulus', async () => {
|
|
const code = ['const myVar = 5 % 2'].join('\n')
|
|
const mem = await exe(code)
|
|
expect(mem.get('myVar')?.value).toBe(1)
|
|
})
|
|
it('can do multiple operations', async () => {
|
|
const code = ['const myVar = 1 + 2 * 3'].join('\n')
|
|
const mem = await exe(code)
|
|
expect(mem.get('myVar')?.value).toBe(7)
|
|
})
|
|
it('big example with parans', async () => {
|
|
const code = ['const myVar = 1 + 2 * (3 - 4) / -5 + 6'].join('\n')
|
|
const mem = await exe(code)
|
|
expect(mem.get('myVar')?.value).toBe(7.4)
|
|
})
|
|
it('with identifier', async () => {
|
|
const code = ['const yo = 6', 'const myVar = yo / 2'].join('\n')
|
|
const mem = await exe(code)
|
|
expect(mem.get('myVar')?.value).toBe(3)
|
|
})
|
|
it('with lots of testing', async () => {
|
|
const code = ['const myVar = 2 * ((2 + 3 ) / 4 + 5)'].join('\n')
|
|
const mem = await exe(code)
|
|
expect(mem.get('myVar')?.value).toBe(12.5)
|
|
})
|
|
it('with callExpression at start', async () => {
|
|
const code = 'const myVar = min(4, 100) + 2'
|
|
const mem = await exe(code)
|
|
expect(mem.get('myVar')?.value).toBe(6)
|
|
})
|
|
it('with callExpression at end', async () => {
|
|
const code = 'const myVar = 2 + min(4, 100)'
|
|
const mem = await exe(code)
|
|
expect(mem.get('myVar')?.value).toBe(6)
|
|
})
|
|
it('with nested callExpression', async () => {
|
|
const code = 'const myVar = 2 + min(100, legLen(5, 3))'
|
|
const mem = await exe(code)
|
|
expect(mem.get('myVar')?.value).toBe(6)
|
|
})
|
|
it('with unaryExpression', async () => {
|
|
const code = 'const myVar = -min(100, 3)'
|
|
const mem = await exe(code)
|
|
expect(mem.get('myVar')?.value).toBe(-3)
|
|
})
|
|
it('with unaryExpression in callExpression', async () => {
|
|
const code = 'const myVar = min(-legLen(5, 4), 5)'
|
|
const code2 = 'const myVar = min(5 , -legLen(5, 4))'
|
|
const mem = await exe(code)
|
|
const mem2 = await exe(code2)
|
|
expect(mem.get('myVar')?.value).toBe(-3)
|
|
expect(mem.get('myVar')?.value).toBe(mem2.get('myVar')?.value)
|
|
})
|
|
it('with unaryExpression in ArrayExpression', async () => {
|
|
const code = 'const myVar = [1,-legLen(5, 4)]'
|
|
const mem = await exe(code)
|
|
expect(mem.get('myVar')?.value).toEqual([
|
|
{
|
|
__meta: [
|
|
{
|
|
sourceRange: [15, 16, 0],
|
|
},
|
|
],
|
|
type: 'Number',
|
|
value: 1,
|
|
},
|
|
{
|
|
__meta: [
|
|
{
|
|
sourceRange: [17, 30, 0],
|
|
},
|
|
],
|
|
type: 'Number',
|
|
value: -3,
|
|
},
|
|
])
|
|
})
|
|
it('with unaryExpression in ArrayExpression in CallExpression, checking nothing funny happens when used in a sketch', async () => {
|
|
const code = [
|
|
"const part001 = startSketchOn('XY')",
|
|
' |> startProfileAt([0, 0], %)',
|
|
'|> line([-2.21, -legLen(5, min(3, 999))], %)',
|
|
].join('\n')
|
|
const mem = await exe(code)
|
|
const sketch = sketchFromKclValue(mem.get('part001'), 'part001')
|
|
// result of `-legLen(5, min(3, 999))` should be -4
|
|
const yVal = (sketch as Sketch).paths?.[0]?.to?.[1]
|
|
expect(yVal).toBe(-4)
|
|
})
|
|
it('test that % substitution feeds down CallExp->ArrExp->UnaryExp->CallExp', async () => {
|
|
const code = [
|
|
`const myVar = 3`,
|
|
`const part001 = startSketchOn('XY')`,
|
|
` |> startProfileAt([0, 0], %)`,
|
|
` |> line([3, 4], %, $seg01)`,
|
|
` |> line([`,
|
|
` min(segLen(seg01), myVar),`,
|
|
` -legLen(segLen(seg01), myVar)`,
|
|
`], %)`,
|
|
``,
|
|
].join('\n')
|
|
const mem = await exe(code)
|
|
const sketch = sketchFromKclValue(mem.get('part001'), 'part001')
|
|
// expect -legLen(segLen('seg01'), myVar) to equal -4 setting the y value back to 0
|
|
expect((sketch as Sketch).paths?.[1]?.from).toEqual([3, 4])
|
|
expect((sketch as Sketch).paths?.[1]?.to).toEqual([6, 0])
|
|
const removedUnaryExp = code.replace(
|
|
`-legLen(segLen(seg01), myVar)`,
|
|
`legLen(segLen(seg01), myVar)`
|
|
)
|
|
const removedUnaryExpMem = await exe(removedUnaryExp)
|
|
const removedUnaryExpMemSketch = sketchFromKclValue(
|
|
removedUnaryExpMem.get('part001'),
|
|
'part001'
|
|
)
|
|
|
|
// without the minus sign, the y value should be 8
|
|
expect((removedUnaryExpMemSketch as Sketch).paths?.[1]?.to).toEqual([6, 8])
|
|
})
|
|
it('with nested callExpression and binaryExpression', async () => {
|
|
const code = 'const myVar = 2 + min(100, -1 + legLen(5, 3))'
|
|
const mem = await exe(code)
|
|
expect(mem.get('myVar')?.value).toBe(5)
|
|
})
|
|
it('can do power of math', async () => {
|
|
const code = 'const myNeg2 = 4 ^ 2 - 3 ^ 2 * 2'
|
|
const mem = await exe(code)
|
|
expect(mem.get('myNeg2')?.value).toBe(-2)
|
|
})
|
|
})
|
|
|
|
describe('Testing Errors', () => {
|
|
it('should throw an error when a variable is not defined', async () => {
|
|
const code = `const myVar = 5
|
|
const theExtrude = startSketchOn('XY')
|
|
|> startProfileAt([0, 0], %)
|
|
|> line([-2.4, 5], %)
|
|
|> line(myVarZ, %)
|
|
|> line([5,5], %)
|
|
|> close(%)
|
|
|> extrude(4, %)`
|
|
await expect(exe(code)).rejects.toEqual(
|
|
new KCLError(
|
|
'undefined_value',
|
|
'memory item key `myVarZ` is not defined',
|
|
topLevelRange(129, 135),
|
|
[],
|
|
[],
|
|
defaultArtifactGraph()
|
|
)
|
|
)
|
|
})
|
|
})
|
|
|
|
// helpers
|
|
|
|
async function exe(
|
|
code: string,
|
|
programMemory: ProgramMemory = ProgramMemory.empty()
|
|
) {
|
|
const ast = assertParse(code)
|
|
|
|
const execState = await enginelessExecutor(ast, programMemory)
|
|
return execState.memory
|
|
}
|