import { ToolTip } from 'lang/langHelpers' import { Selection, Selections } from 'lib/selections' import { ArrayExpression, ArtifactGraph, BinaryExpression, CallExpression, Expr, ExpressionStatement, ObjectExpression, ObjectProperty, PathToNode, PipeExpression, Program, ProgramMemory, ReturnStatement, sketchFromKclValue, sketchFromKclValueOptional, SourceRange, SyntaxType, topLevelRange, VariableDeclaration, VariableDeclarator, } from './wasm' import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils' import { createIdentifier, splitPathAtLastIndex } from './modifyAst' import { getSketchSegmentFromSourceRange } from './std/sketchConstraints' import { getAngle } from '../lib/utils' import { getFirstArg } from './std/sketch' import { getConstraintLevelFromSourceRange, getConstraintType, } from './std/sketchcombos' import { err, Reason } from 'lib/trap' import { ImportStatement } from 'wasm-lib/kcl/bindings/ImportStatement' import { Node } from 'wasm-lib/kcl/bindings/Node' import { codeRefFromRange } from './std/artifactGraph' /** * Retrieves a node from a given path within a Program node structure, optionally stopping at a specified node type. * This function navigates through the AST (Abstract Syntax Tree) based on the provided path, attempting to locate * and return the node at the end of this path. * By default it will return the node of the deepest "stopAt" type encountered, or the node at the end of the path if no "stopAt" type is provided. * If the "returnEarly" flag is set to true, the function will return as soon as a node of the specified type is found. */ export function getNodeFromPath( node: Program, path: PathToNode, stopAt?: SyntaxType | SyntaxType[], returnEarly = false ): | { node: T shallowPath: PathToNode deepPath: PathToNode } | Error { let currentNode = node as any let stopAtNode = null let successfulPaths: PathToNode = [] let pathsExplored: PathToNode = [] for (const pathItem of path) { if (typeof currentNode[pathItem[0]] !== 'object') { if (stopAtNode) { return { node: stopAtNode, shallowPath: pathsExplored, deepPath: successfulPaths, } } return new Error('not an object') } currentNode = currentNode?.[pathItem[0]] successfulPaths.push(pathItem) if (!stopAtNode) { pathsExplored.push(pathItem) } if ( typeof stopAt !== 'undefined' && (Array.isArray(stopAt) ? stopAt.includes(currentNode.type) : currentNode.type === stopAt) ) { // it will match the deepest node of the type // instead of returning at the first match stopAtNode = currentNode if (returnEarly) { return { node: stopAtNode, shallowPath: pathsExplored, deepPath: successfulPaths, } } } } return { node: stopAtNode || currentNode, shallowPath: pathsExplored, deepPath: successfulPaths, } } /** * Functions the same as getNodeFromPath, but returns a curried function that can be called with the stopAt and returnEarly arguments. */ export function getNodeFromPathCurry( node: Program, path: PathToNode ): ( stopAt?: SyntaxType | SyntaxType[], returnEarly?: boolean ) => | { node: T path: PathToNode } | Error { return (stopAt?: SyntaxType | SyntaxType[], returnEarly = false) => { const _node1 = getNodeFromPath(node, path, stopAt, returnEarly) if (err(_node1)) return _node1 const { node: _node, shallowPath } = _node1 return { node: _node, path: shallowPath, } } } type KCLNode = Node< | Expr | ExpressionStatement | VariableDeclaration | VariableDeclarator | ReturnStatement > export function traverse( node: KCLNode | Node, option: { enter?: (node: KCLNode, pathToNode: PathToNode) => void leave?: (node: KCLNode) => void }, pathToNode: PathToNode = [] ) { const _node = node as KCLNode option?.enter?.(_node, pathToNode) const _traverse = (node: KCLNode, pathToNode: PathToNode) => traverse(node, option, pathToNode) if (_node.type === 'VariableDeclaration') { _traverse(_node.declaration, [ ...pathToNode, ['declaration', 'VariableDeclaration'], ]) } else if (_node.type === 'VariableDeclarator') { _traverse(_node.init, [...pathToNode, ['init', '']]) } else if (_node.type === 'PipeExpression') { _node.body.forEach((expression, index) => _traverse(expression, [ ...pathToNode, ['body', 'PipeExpression'], [index, 'index'], ]) ) } else if (_node.type === 'CallExpression') { _traverse(_node.callee, [...pathToNode, ['callee', 'CallExpression']]) _node.arguments.forEach((arg, index) => _traverse(arg, [ ...pathToNode, ['arguments', 'CallExpression'], [index, 'index'], ]) ) } else if (_node.type === 'BinaryExpression') { _traverse(_node.left, [...pathToNode, ['left', 'BinaryExpression']]) _traverse(_node.right, [...pathToNode, ['right', 'BinaryExpression']]) } else if (_node.type === 'Identifier') { // do nothing } else if (_node.type === 'Literal') { // do nothing } else if (_node.type === 'TagDeclarator') { // do nothing } else if (_node.type === 'ArrayExpression') { _node.elements.forEach((el, index) => _traverse(el, [ ...pathToNode, ['elements', 'ArrayExpression'], [index, 'index'], ]) ) } else if (_node.type === 'ObjectExpression') { _node.properties.forEach(({ key, value }, index) => { _traverse(key, [ ...pathToNode, ['properties', 'ObjectExpression'], [index, 'index'], ['key', 'Property'], ]) _traverse(value, [ ...pathToNode, ['properties', 'ObjectExpression'], [index, 'index'], ['value', 'Property'], ]) }) } else if (_node.type === 'UnaryExpression') { _traverse(_node.argument, [...pathToNode, ['argument', 'UnaryExpression']]) } else if (_node.type === 'MemberExpression') { // hmm this smell _traverse(_node.object, [...pathToNode, ['object', 'MemberExpression']]) _traverse(_node.property, [...pathToNode, ['property', 'MemberExpression']]) } else if ('body' in _node && Array.isArray(_node.body)) { _node.body.forEach((expression, index) => _traverse(expression, [...pathToNode, ['body', ''], [index, 'index']]) ) } option?.leave?.(_node) } export interface PrevVariable { key: string value: T } export function findAllPreviousVariablesPath( ast: Program, programMemory: ProgramMemory, path: PathToNode, type: 'number' | 'string' = 'number' ): { variables: PrevVariable[] bodyPath: PathToNode insertIndex: number } { const _node1 = getNodeFromPath(ast, path, 'VariableDeclaration') if (err(_node1)) { console.error(_node1) return { variables: [], bodyPath: [], insertIndex: 0, } } const { shallowPath: pathToDec, node } = _node1 const startRange = (node as any).start const { index: insertIndex, path: bodyPath } = splitPathAtLastIndex(pathToDec) const _node2 = getNodeFromPath(ast, bodyPath) if (err(_node2)) { console.error(_node2) return { variables: [], bodyPath: [], insertIndex: 0, } } const { node: bodyItems } = _node2 const variables: PrevVariable[] = [] bodyItems?.forEach?.((item) => { if (item.type !== 'VariableDeclaration' || item.end > startRange) return const varName = item.declaration.id.name const varValue = programMemory?.get(varName) if (!varValue || typeof varValue?.value !== type) return variables.push({ key: varName, value: varValue.value, }) }) return { insertIndex, bodyPath: bodyPath, variables, } } export function findAllPreviousVariables( ast: Program, programMemory: ProgramMemory, sourceRange: SourceRange, type: 'number' | 'string' = 'number' ): { variables: PrevVariable[] bodyPath: PathToNode insertIndex: number } { const path = getNodePathFromSourceRange(ast, sourceRange) return findAllPreviousVariablesPath(ast, programMemory, path, type) } type ReplacerFn = ( _ast: Node, varName: string ) => { modifiedAst: Node; pathToReplaced: PathToNode } | Error export function isNodeSafeToReplacePath( ast: Program, path: PathToNode ): | { isSafe: boolean value: Expr replacer: ReplacerFn } | Error { if (path[path.length - 1][0] === 'callee') { path = path.slice(0, -1) } const acceptedNodeTypes: SyntaxType[] = [ 'BinaryExpression', 'Identifier', 'CallExpression', 'Literal', 'UnaryExpression', ] const _node1 = getNodeFromPath(ast, path, acceptedNodeTypes) if (err(_node1)) return _node1 const { node: value, deepPath: outPath } = _node1 const _node2 = getNodeFromPath(ast, path, 'BinaryExpression') if (err(_node2)) return _node2 const { node: binValue, shallowPath: outBinPath } = _node2 // binaryExpression should take precedence const [finVal, finPath] = (binValue as Expr)?.type === 'BinaryExpression' ? [binValue, outBinPath] : [value, outPath] const replaceNodeWithIdentifier: ReplacerFn = (_ast, varName) => { const identifier = createIdentifier(varName) const last = finPath[finPath.length - 1] const pathToReplaced = structuredClone(finPath) const index = pathToReplaced[1][0] if (typeof index !== 'number') { return new Error( `Expected number index, but found: ${typeof index} ${index}` ) } pathToReplaced[1][0] = index + 1 const startPath = finPath.slice(0, -1) const _nodeToReplace = getNodeFromPath(_ast, startPath) if (err(_nodeToReplace)) return _nodeToReplace const nodeToReplace = _nodeToReplace.node as any nodeToReplace[last[0]] = identifier return { modifiedAst: _ast, pathToReplaced } } const hasPipeSub = isTypeInValue(finVal as Expr, 'PipeSubstitution') const isIdentifierCallee = path[path.length - 1][0] !== 'callee' return { isSafe: !hasPipeSub && isIdentifierCallee && acceptedNodeTypes.includes((finVal as any)?.type) && finPath.map(([_, type]) => type).includes('VariableDeclaration'), value: finVal as Expr, replacer: replaceNodeWithIdentifier, } } export function isNodeSafeToReplace( ast: Node, sourceRange: SourceRange ): | { isSafe: boolean value: Node replacer: ReplacerFn } | Error { let path = getNodePathFromSourceRange(ast, sourceRange) return isNodeSafeToReplacePath(ast, path) } export function isTypeInValue(node: Expr, syntaxType: SyntaxType): boolean { if (node.type === syntaxType) return true if (node.type === 'BinaryExpression') return isTypeInBinExp(node, syntaxType) if (node.type === 'CallExpression') return isTypeInCallExp(node, syntaxType) if (node.type === 'ArrayExpression') return isTypeInArrayExp(node, syntaxType) return false } function isTypeInBinExp( node: BinaryExpression, syntaxType: SyntaxType ): boolean { if (node.type === syntaxType) return true if (node.left.type === syntaxType) return true if (node.right.type === syntaxType) return true return ( isTypeInValue(node.left, syntaxType) || isTypeInValue(node.right, syntaxType) ) } function isTypeInCallExp( node: CallExpression, syntaxType: SyntaxType ): boolean { if (node.callee.type === syntaxType) return true return node.arguments.some((arg) => isTypeInValue(arg, syntaxType)) } function isTypeInArrayExp( node: ArrayExpression, syntaxType: SyntaxType ): boolean { return node.elements.some((el) => isTypeInValue(el, syntaxType)) } export function isLinesParallelAndConstrained( ast: Program, artifactGraph: ArtifactGraph, programMemory: ProgramMemory, primaryLine: Selection, secondaryLine: Selection ): | { isParallelAndConstrained: boolean selection: Selection | null } | Error { try { const EPSILON = 0.005 const primaryPath = getNodePathFromSourceRange( ast, primaryLine?.codeRef?.range ) const secondaryPath = getNodePathFromSourceRange( ast, secondaryLine?.codeRef?.range ) const _secondaryNode = getNodeFromPath( ast, secondaryPath, 'CallExpression' ) if (err(_secondaryNode)) return _secondaryNode const secondaryNode = _secondaryNode.node const _varDec = getNodeFromPath(ast, primaryPath, 'VariableDeclaration') if (err(_varDec)) return _varDec const varDec = _varDec.node const varName = (varDec as VariableDeclaration)?.declaration.id?.name const sg = sketchFromKclValue(programMemory?.get(varName), varName) if (err(sg)) return sg const _primarySegment = getSketchSegmentFromSourceRange( sg, primaryLine?.codeRef?.range ) if (err(_primarySegment)) return _primarySegment const primarySegment = _primarySegment.segment const _segment = getSketchSegmentFromSourceRange( sg, secondaryLine?.codeRef?.range ) if (err(_segment)) return _segment const { segment: secondarySegment, index: secondaryIndex } = _segment const primaryAngle = getAngle(primarySegment.from, primarySegment.to) const secondaryAngle = getAngle(secondarySegment.from, secondarySegment.to) const secondaryAngleAlt = getAngle( secondarySegment.to, secondarySegment.from ) const isParallel = Math.abs(primaryAngle - secondaryAngle) < EPSILON || Math.abs(primaryAngle - secondaryAngleAlt) < EPSILON // is secondary line fully constrain, or has constrain type of 'angle' const secondaryFirstArg = getFirstArg(secondaryNode) if (err(secondaryFirstArg)) return secondaryFirstArg const constraintType = getConstraintType( secondaryFirstArg.val, secondaryNode.callee.name as ToolTip ) const constraintLevelMeta = getConstraintLevelFromSourceRange( secondaryLine?.codeRef.range, ast ) if (err(constraintLevelMeta)) { console.error(constraintLevelMeta) return { isParallelAndConstrained: false, selection: null, } } const constraintLevel = constraintLevelMeta.level const isConstrained = constraintType === 'angle' || constraintLevel === 'full' // get the previous segment const prevSegment = sg.paths[secondaryIndex - 1] const prevSourceRange = prevSegment.__geoMeta.sourceRange const isParallelAndConstrained = isParallel && isConstrained && !!prevSourceRange return { isParallelAndConstrained, selection: { codeRef: codeRefFromRange(prevSourceRange, ast), artifact: artifactGraph.get(prevSegment.__geoMeta.id), }, } } catch (e) { return { isParallelAndConstrained: false, selection: null, } } } export function hasExtrudeSketch({ ast, selection, programMemory, }: { ast: Program selection: Selection programMemory: ProgramMemory }): boolean { const varDecMeta = getNodeFromPath( ast, selection?.codeRef?.pathToNode, 'VariableDeclaration' ) if (err(varDecMeta)) { console.error(varDecMeta) return false } const varDec = varDecMeta.node if (varDec.type !== 'VariableDeclaration') return false const varName = varDec.declaration.id.name const varValue = programMemory?.get(varName) return ( varValue?.type === 'Solid' || !(sketchFromKclValueOptional(varValue, varName) instanceof Reason) ) } export function artifactIsPlaneWithPaths(selectionRanges: Selections) { return ( selectionRanges.graphSelections.length && selectionRanges.graphSelections[0].artifact?.type === 'plane' && selectionRanges.graphSelections[0].artifact.pathIds.length ) } export function isSingleCursorInPipe( selectionRanges: Selections, ast: Program ) { if (selectionRanges.graphSelections.length !== 1) return false const selection = selectionRanges.graphSelections[0] const pathToNode = getNodePathFromSourceRange(ast, selection?.codeRef?.range) const nodeTypes = pathToNode.map(([, type]) => type) if (nodeTypes.includes('FunctionExpression')) return false if (!nodeTypes.includes('VariableDeclaration')) return false if (nodeTypes.includes('PipeExpression')) return true return false } export function findUsesOfTagInPipe( ast: Program, pathToNode: PathToNode ): SourceRange[] { const stdlibFunctionsThatTakeTagInputs = [ 'segAng', 'segEndX', 'segEndY', 'segLen', ] const nodeMeta = getNodeFromPath( ast, pathToNode, 'CallExpression' ) if (err(nodeMeta)) { console.error(nodeMeta) return [] } const node = nodeMeta.node if (node.type !== 'CallExpression') return [] const tagIndex = node.callee.name === 'close' ? 1 : 2 const thirdParam = node.arguments[tagIndex] if ( !(thirdParam?.type === 'TagDeclarator' || thirdParam?.type === 'Identifier') ) return [] const tag = thirdParam?.type === 'TagDeclarator' ? String(thirdParam.value) : thirdParam.name const varDec = getNodeFromPath>( ast, pathToNode, 'VariableDeclaration' ) if (err(varDec)) { console.error(varDec) return [] } const dependentRanges: SourceRange[] = [] traverse(varDec.node, { enter: (node) => { if ( node.type !== 'CallExpression' || !stdlibFunctionsThatTakeTagInputs.includes(node.callee.name) ) return const tagArg = node.arguments[0] if (!(tagArg.type === 'TagDeclarator' || tagArg.type === 'Identifier')) return const tagArgValue = tagArg.type === 'TagDeclarator' ? String(tagArg.value) : tagArg.name if (tagArgValue === tag) dependentRanges.push(topLevelRange(node.start, node.end)) }, }) return dependentRanges } export function hasSketchPipeBeenExtruded(selection: Selection, ast: Program) { const _node = getNodeFromPath>( ast, selection.codeRef.pathToNode, 'PipeExpression' ) if (err(_node)) return false const { node: pipeExpression } = _node if (pipeExpression.type !== 'PipeExpression') return false const _varDec = getNodeFromPath( ast, selection.codeRef.pathToNode, 'VariableDeclarator' ) if (err(_varDec)) return false const varDec = _varDec.node if (varDec.type !== 'VariableDeclarator') return false let extruded = false // option 1: extrude or revolve is called in the sketch pipe traverse(pipeExpression, { enter(node) { if ( node.type === 'CallExpression' && (node.callee.name === 'extrude' || node.callee.name === 'revolve') ) { extruded = true } }, }) // option 2: extrude or revolve is called in the separate pipe if (!extruded) { traverse(ast as any, { enter(node) { if ( node.type === 'CallExpression' && node.callee.type === 'Identifier' && (node.callee.name === 'extrude' || node.callee.name === 'revolve' || node.callee.name === 'loft') && node.arguments?.[1]?.type === 'Identifier' && node.arguments[1].name === varDec.id.name ) { extruded = true } }, }) } return extruded } /** File must contain at least one sketch that has not been extruded already */ export function doesSceneHaveSweepableSketch(ast: Node, count = 1) { const theMap: any = {} traverse(ast as any, { enter(node) { if ( node.type === 'VariableDeclarator' && node.init?.type === 'PipeExpression' ) { let hasStartProfileAt = false let hasStartSketchOn = false let hasClose = false let hasCircle = false for (const pipe of node.init.body) { if ( pipe.type === 'CallExpression' && pipe.callee.name === 'startProfileAt' ) { hasStartProfileAt = true } if ( pipe.type === 'CallExpression' && pipe.callee.name === 'startSketchOn' ) { hasStartSketchOn = true } if (pipe.type === 'CallExpression' && pipe.callee.name === 'close') { hasClose = true } if (pipe.type === 'CallExpression' && pipe.callee.name === 'circle') { hasCircle = true } } if ( (hasStartProfileAt || hasCircle) && hasStartSketchOn && (hasClose || hasCircle) ) { theMap[node.id.name] = true } } else if ( node.type === 'CallExpression' && (node.callee.name === 'extrude' || node.callee.name === 'revolve') && node.arguments[1]?.type === 'Identifier' && theMap?.[node?.arguments?.[1]?.name] ) { delete theMap[node.arguments[1].name] } }, }) return Object.keys(theMap).length >= count } export function doesSceneHaveExtrudedSketch(ast: Node) { const theMap: any = {} traverse(ast as any, { enter(node) { if ( node.type === 'VariableDeclarator' && node.init?.type === 'PipeExpression' ) { for (const pipe of node.init.body) { if ( pipe.type === 'CallExpression' && pipe.callee.name === 'extrude' ) { theMap[node.id.name] = true break } } } else if ( node.type === 'CallExpression' && node.callee.name === 'extrude' && node.arguments[1]?.type === 'Identifier' ) { theMap[node.moduleId] = true } }, }) return Object.keys(theMap).length > 0 } export function getObjExprProperty( node: ObjectExpression, propName: string ): { expr: ObjectProperty['value']; index: number } | null { const index = node.properties.findIndex(({ key }) => key.name === propName) if (index === -1) return null return { expr: node.properties[index].value, index } }