import type { ArtifactCommand, ArtifactId, ArtifactGraph as RustArtifactGraph, } from '@rust/kcl-lib/bindings/Artifact' import type { CompilationError } from '@rust/kcl-lib/bindings/CompilationError' import type { Configuration } from '@rust/kcl-lib/bindings/Configuration' import type { CoreDumpInfo } from '@rust/kcl-lib/bindings/CoreDumpInfo' import type { DefaultPlanes } from '@rust/kcl-lib/bindings/DefaultPlanes' import type { Discovered } from '@rust/kcl-lib/bindings/Discovered' import type { ExecOutcome as RustExecOutcome } from '@rust/kcl-lib/bindings/ExecOutcome' import type { KclError as RustKclError } from '@rust/kcl-lib/bindings/KclError' import type { KclErrorWithOutputs } from '@rust/kcl-lib/bindings/KclErrorWithOutputs' import type { KclValue } from '@rust/kcl-lib/bindings/KclValue' import type { MetaSettings } from '@rust/kcl-lib/bindings/MetaSettings' import type { UnitAngle, UnitLength } from '@rust/kcl-lib/bindings/ModelingCmd' import type { ModulePath } from '@rust/kcl-lib/bindings/ModulePath' import type { Node } from '@rust/kcl-lib/bindings/Node' import type { NumericSuffix } from '@rust/kcl-lib/bindings/NumericSuffix' import type { Operation } from '@rust/kcl-lib/bindings/Operation' import type { Program } from '@rust/kcl-lib/bindings/Program' import type { ProjectConfiguration } from '@rust/kcl-lib/bindings/ProjectConfiguration' import type { Sketch } from '@rust/kcl-lib/bindings/Sketch' import type { SourceRange } from '@rust/kcl-lib/bindings/SourceRange' import type { UnitAngle as UnitAng } from '@rust/kcl-lib/bindings/UnitAngle' import type { UnitLen } from '@rust/kcl-lib/bindings/UnitLen' import { KCLError } from '@src/lang/errors' import { getNodePathFromSourceRange } from '@src/lang/queryAstNodePathUtils' import { type Artifact, defaultArtifactGraph, } from '@src/lang/std/artifactGraph' import type { Coords2d } from '@src/lang/std/sketch' import { DEFAULT_DEFAULT_ANGLE_UNIT, DEFAULT_DEFAULT_LENGTH_UNIT, } from '@src/lib/constants' import type { CoreDumpManager } from '@src/lib/coredump' import openWindow from '@src/lib/openWindow' import { Reason, err } from '@src/lib/trap' import type { DeepPartial } from '@src/lib/types' import { isArray } from '@src/lib/utils' import { base64_decode, change_kcl_settings, coredump, default_app_settings, default_project_settings, format_number, get_kcl_version, get_tangential_arc_to_info, is_kcl_empty_or_only_settings, is_points_ccw, kcl_lint, kcl_settings, parse_app_settings, parse_project_settings, parse_wasm, recast_wasm, serialize_configuration, serialize_project_configuration, } from '@src/lib/wasm_lib_wrapper' export type { ArrayExpression } from '@rust/kcl-lib/bindings/ArrayExpression' export type { Artifact, ArtifactCommand, ArtifactId, Cap as CapArtifact, CodeRef, CompositeSolid as CompositeSolidArtifact, EdgeCut, Path as PathArtifact, Plane as PlaneArtifact, Segment as SegmentArtifact, Solid2d as Solid2dArtifact, Sweep as SweepArtifact, SweepEdge, Wall as WallArtifact, } from '@rust/kcl-lib/bindings/Artifact' export type { BinaryExpression } from '@rust/kcl-lib/bindings/BinaryExpression' export type { BinaryPart } from '@rust/kcl-lib/bindings/BinaryPart' export type { CallExpression } from '@rust/kcl-lib/bindings/CallExpression' export type { CallExpressionKw } from '@rust/kcl-lib/bindings/CallExpressionKw' export type { Configuration } from '@rust/kcl-lib/bindings/Configuration' export type { Expr } from '@rust/kcl-lib/bindings/Expr' export type { ExpressionStatement } from '@rust/kcl-lib/bindings/ExpressionStatement' export type { Identifier } from '@rust/kcl-lib/bindings/Identifier' export type { LabeledArg } from '@rust/kcl-lib/bindings/LabeledArg' export type { Literal } from '@rust/kcl-lib/bindings/Literal' export type { LiteralValue } from '@rust/kcl-lib/bindings/LiteralValue' export type { MemberExpression } from '@rust/kcl-lib/bindings/MemberExpression' export type { Name } from '@rust/kcl-lib/bindings/Name' export type { NumericSuffix } from '@rust/kcl-lib/bindings/NumericSuffix' export type { ObjectExpression } from '@rust/kcl-lib/bindings/ObjectExpression' export type { ObjectProperty } from '@rust/kcl-lib/bindings/ObjectProperty' export type { Parameter } from '@rust/kcl-lib/bindings/Parameter' export type { PipeExpression } from '@rust/kcl-lib/bindings/PipeExpression' export type { PipeSubstitution } from '@rust/kcl-lib/bindings/PipeSubstitution' export type { Program } from '@rust/kcl-lib/bindings/Program' export type { ReturnStatement } from '@rust/kcl-lib/bindings/ReturnStatement' export type { SourceRange } from '@rust/kcl-lib/bindings/SourceRange' export type { UnaryExpression } from '@rust/kcl-lib/bindings/UnaryExpression' export type { VariableDeclaration } from '@rust/kcl-lib/bindings/VariableDeclaration' export type { VariableDeclarator } from '@rust/kcl-lib/bindings/VariableDeclarator' export type SyntaxType = | 'Program' | 'ExpressionStatement' | 'BinaryExpression' | 'CallExpression' | 'CallExpressionKw' | 'Name' | 'ReturnStatement' | 'VariableDeclaration' | 'VariableDeclarator' | 'MemberExpression' | 'ArrayExpression' | 'ObjectExpression' | 'ObjectProperty' | 'FunctionExpression' | 'PipeExpression' | 'PipeSubstitution' | 'Literal' | 'LiteralValue' | 'NonCodeNode' | 'UnaryExpression' | 'ImportStatement' export type { ExtrudeSurface } from '@rust/kcl-lib/bindings/ExtrudeSurface' export type { KclValue } from '@rust/kcl-lib/bindings/KclValue' export type { Path } from '@rust/kcl-lib/bindings/Path' export type { Sketch } from '@rust/kcl-lib/bindings/Sketch' export type { Solid } from '@rust/kcl-lib/bindings/Solid' /** * Convert a SourceRange as used inside the KCL interpreter into the above one for use in the * frontend (essentially we're eagerly checking whether the frontend should care about the SourceRange * so as not to expose details of the interpreter's current representation of module ids throughout * the frontend). */ export function sourceRangeFromRust(s: SourceRange): SourceRange { return [s[0], s[1], s[2]] } /** * Create a default SourceRange for testing or as a placeholder. */ export function defaultSourceRange(): SourceRange { return [0, 0, 0] } function firstSourceRange(error: RustKclError): SourceRange { return error.sourceRanges.length > 0 ? sourceRangeFromRust(error.sourceRanges[0]) : defaultSourceRange() } const splitErrors = ( input: CompilationError[] ): { errors: CompilationError[]; warnings: CompilationError[] } => { let errors = [] let warnings = [] for (const i of input) { if (i.severity === 'Warning') { warnings.push(i) } else { errors.push(i) } } return { errors, warnings } } export class ParseResult { program: Node | null errors: CompilationError[] warnings: CompilationError[] constructor( program: Node | null, errors: CompilationError[], warnings: CompilationError[] ) { this.program = program this.errors = errors this.warnings = warnings } } /** * Parsing was successful. There is guaranteed to be an AST and no fatal errors. There may or may * not be warnings or non-fatal errors. */ class SuccessParseResult extends ParseResult { program: Node constructor( program: Node, errors: CompilationError[], warnings: CompilationError[] ) { super(program, errors, warnings) this.program = program } } export function resultIsOk(result: ParseResult): result is SuccessParseResult { return !!result.program && result.errors.length === 0 } export const parse = (code: string | Error): ParseResult | Error => { if (err(code)) return code try { const parsed: [Node, CompilationError[]] = parse_wasm(code) let errs = splitErrors(parsed[1]) return new ParseResult(parsed[0], errs.errors, errs.warnings) } catch (e: any) { // throw e console.error(e.toString()) const parsed: RustKclError = JSON.parse(e.toString()) return new KCLError( parsed.kind, parsed.msg, firstSourceRange(parsed), [], [], defaultArtifactGraph(), {}, null ) } } /** * Parse and throw an exception if there are any errors (probably not suitable for use outside of testing). */ export function assertParse(code: string): Node { const result = parse(code) // eslint-disable-next-line suggest-no-throw/suggest-no-throw if (err(result)) throw result if (!resultIsOk(result)) { // eslint-disable-next-line suggest-no-throw/suggest-no-throw throw new Error( `parse result contains errors: ${result.errors.map((err) => err.message).join('\n')}`, { cause: result } ) } return result.program } export type VariableMap = { [key in string]?: KclValue } export type PathToNode = [string | number, string][] export const isPathToNodeNumber = ( pathToNode: string | number ): pathToNode is number => { return typeof pathToNode === 'number' } export const isPathToNode = (input: unknown): input is PathToNode => isArray(input) && isArray(input[0]) && input[0].length == 2 && (typeof input[0][0] === 'number' || typeof input[0][0] === 'string') && typeof input[0][1] === 'string' export interface ExecState { variables: { [key in string]?: KclValue } operations: Operation[] artifactCommands: ArtifactCommand[] artifactGraph: ArtifactGraph errors: CompilationError[] filenames: { [x: number]: ModulePath | undefined } defaultPlanes: DefaultPlanes | null } /** * Create an empty ExecState. This is useful on init to prevent needing an * Option. */ export function emptyExecState(): ExecState { return { variables: {}, operations: [], artifactCommands: [], artifactGraph: defaultArtifactGraph(), errors: [], filenames: [], defaultPlanes: null, } } export function execStateFromRust( execOutcome: RustExecOutcome, program: Node ): ExecState { const artifactGraph = rustArtifactGraphToMap(execOutcome.artifactGraph) // We haven't ported pathToNode logic to Rust yet, so we need to fill it in. for (const [_id, artifact] of artifactGraph) { if (!artifact) continue if (!('codeRef' in artifact)) continue const pathToNode = getNodePathFromSourceRange( program, sourceRangeFromRust(artifact.codeRef.range) ) artifact.codeRef.pathToNode = pathToNode } return { variables: execOutcome.variables, operations: execOutcome.operations, artifactCommands: execOutcome.artifactCommands, artifactGraph, errors: execOutcome.errors, filenames: execOutcome.filenames, defaultPlanes: execOutcome.defaultPlanes, } } export function mockExecStateFromRust(execOutcome: RustExecOutcome): ExecState { return { variables: execOutcome.variables, operations: execOutcome.operations, artifactCommands: execOutcome.artifactCommands, artifactGraph: new Map(), errors: execOutcome.errors, filenames: execOutcome.filenames, defaultPlanes: execOutcome.defaultPlanes, } } export type ArtifactGraph = Map function rustArtifactGraphToMap( rustArtifactGraph: RustArtifactGraph ): ArtifactGraph { const map = new Map() for (const [id, artifact] of Object.entries(rustArtifactGraph.map)) { if (!artifact) continue map.set(id, artifact) } return map } // TODO: In the future, make the parameter be a KclValue. export function sketchFromKclValueOptional( obj: any, varName: string | null ): Sketch | Reason { if (obj?.value?.type === 'Sketch') return obj.value if (obj?.value?.type === 'Solid') return obj.value.sketch if (obj?.type === 'Sketch') return obj.value if (obj?.type === 'Solid') return obj.value.sketch if (!varName) { varName = 'a KCL value' } const actualType = obj?.value?.type ?? obj?.type if (actualType) { return new Reason( `Expected ${varName} to be a sketch or solid, but it was ${actualType} instead.` ) } else { return new Reason(`Expected ${varName} to be a sketch, but it wasn't.`) } } // TODO: In the future, make the parameter be a KclValue. export function sketchFromKclValue( obj: any, varName: string | null ): Sketch | Error { const result = sketchFromKclValueOptional(obj, varName) if (result instanceof Reason) { return result.toError() } return result } export const errFromErrWithOutputs = (e: any): KCLError => { const parsed: KclErrorWithOutputs = JSON.parse(e.toString()) return new KCLError( parsed.error.kind, parsed.error.msg, firstSourceRange(parsed.error), parsed.operations, parsed.artifactCommands, rustArtifactGraphToMap(parsed.artifactGraph), parsed.filenames, parsed.defaultPlanes ) } export const kclLint = async (ast: Program): Promise> => { try { const discovered_findings: Array = await kcl_lint( JSON.stringify(ast) ) return discovered_findings } catch (e: any) { return Promise.reject(e) } } export const recast = (ast: Program): string | Error => { return recast_wasm(JSON.stringify(ast)) } /** * Format a number with suffix as KCL. */ export function formatNumber(value: number, suffix: NumericSuffix): string { return format_number(value, JSON.stringify(suffix)) } export function isPointsCCW(points: Coords2d[]): number { return is_points_ccw(new Float64Array(points.flat())) } export function getTangentialArcToInfo({ arcStartPoint, arcEndPoint, tanPreviousPoint, obtuse = true, }: { arcStartPoint: Coords2d arcEndPoint: Coords2d tanPreviousPoint: Coords2d obtuse?: boolean }): { center: Coords2d arcMidPoint: Coords2d radius: number startAngle: number endAngle: number ccw: boolean arcLength: number } { const result = get_tangential_arc_to_info( arcStartPoint[0], arcStartPoint[1], arcEndPoint[0], arcEndPoint[1], tanPreviousPoint[0], tanPreviousPoint[1], obtuse ) return { center: [result.center_x, result.center_y], arcMidPoint: [result.arc_mid_point_x, result.arc_mid_point_y], radius: result.radius, startAngle: result.start_angle, endAngle: result.end_angle, ccw: result.ccw > 0, arcLength: result.arc_length, } } export async function coreDump( coreDumpManager: CoreDumpManager, openGithubIssue: boolean = false ): Promise { try { console.warn('CoreDump: Initializing core dump') const dump: CoreDumpInfo = await coredump(coreDumpManager) /* NOTE: this console output of the coredump should include the field `github_issue_url` which is not in the uploaded coredump file. `github_issue_url` is added after the file is uploaded and is only needed for the openWindow operation which creates a new GitHub issue for the user. */ if (openGithubIssue && dump.github_issue_url) { // eslint-disable-next-line @typescript-eslint/no-floating-promises openWindow(dump.github_issue_url) } else { console.error( 'github_issue_url undefined. Unable to create GitHub issue for coredump.' ) } console.log('CoreDump: final coredump', dump) console.log('CoreDump: final coredump JSON', JSON.stringify(dump)) return dump } catch (e: any) { console.error('CoreDump: error', e) return Promise.reject(new Error(`Error getting core dump: ${e}`)) } } export function defaultAppSettings(): DeepPartial | Error { return default_app_settings() } export function parseAppSettings( toml: string ): DeepPartial | Error { return parse_app_settings(toml) } export function defaultProjectSettings(): | DeepPartial | Error { return default_project_settings() } export function parseProjectSettings( toml: string ): DeepPartial | Error { return parse_project_settings(toml) } export function base64Decode(base64: string): ArrayBuffer | Error { try { const decoded = base64_decode(base64) return new Uint8Array(decoded).buffer } catch (e) { console.error('Caught error decoding base64 string', e) return new Error('Caught error decoding base64 string', { cause: e }) } } /** * Get the meta settings for the KCL. If no settings were set in the file, * returns null. */ export function kclSettings( kcl: string | Node ): MetaSettings | null | Error { let program: Node if (typeof kcl === 'string') { const parseResult = parse(kcl) if (err(parseResult)) return parseResult if (!resultIsOk(parseResult)) { return new Error(`parse result had errors`, { cause: parseResult }) } program = parseResult.program } else { program = kcl } try { return kcl_settings(JSON.stringify(program)) } catch (e) { return new Error('Caught error getting kcl settings', { cause: e }) } } /** * Change the meta settings for the kcl file. * @returns the new kcl string with the updated settings. */ export function changeKclSettings( kcl: string, settings: MetaSettings ): string | Error { try { return change_kcl_settings(kcl, JSON.stringify(settings)) } catch (e) { console.error('Caught error changing kcl settings', e) return new Error('Caught error changing kcl settings', { cause: e }) } } /** * Returns true if the given KCL is empty or only contains settings that would * be auto-generated. */ export function isKclEmptyOrOnlySettings(kcl: string): boolean { if (kcl === '') { // Fast path. return true } try { return is_kcl_empty_or_only_settings(kcl) } catch (e) { console.debug('Caught error checking if KCL is empty', e) // If there's a parse error, it can't be empty or auto-generated. return false } } /** * Convert a `UnitLength` (used in settings and modeling commands) to a * `UnitLen` (used in execution). */ export function unitLengthToUnitLen(input: UnitLength): UnitLen { switch (input) { case 'm': return { type: 'M' } case 'cm': return { type: 'Cm' } case 'yd': return { type: 'Yards' } case 'ft': return { type: 'Feet' } case 'in': return { type: 'Inches' } default: return { type: 'Mm' } } } /** * Convert `UnitLen` (used in execution) to `UnitLength` (used in settings * and modeling commands). */ export function unitLenToUnitLength(input: UnitLen): UnitLength { switch (input.type) { case 'M': return 'm' case 'Cm': return 'cm' case 'Yards': return 'yd' case 'Feet': return 'ft' case 'Inches': return 'in' default: return DEFAULT_DEFAULT_LENGTH_UNIT } } /** * Convert a `UnitAngle` (used in modeling commands) to a `UnitAng` (used in * execution). */ export function unitAngleToUnitAng(input: UnitAngle): UnitAng { switch (input) { case 'radians': return { type: 'Radians' } default: return { type: 'Degrees' } } } /** * Convert `UnitAng` (used in execution) to `UnitAngle` (used in modeling * commands). */ export function unitAngToUnitAngle(input: UnitAng): UnitAngle { switch (input.type) { case 'Radians': return 'radians' default: return DEFAULT_DEFAULT_ANGLE_UNIT } } /** * Get the KCL version currently being used. */ export function getKclVersion(): string { return get_kcl_version() } /** * Serialize a project configuration to a TOML string. */ export function serializeConfiguration( configuration: DeepPartial ): string | Error { try { return serialize_configuration(configuration) } catch (e: any) { return new Error(`Error serializing configuration: ${e}`) } } /** * Serialize a project configuration to a TOML string. */ export function serializeProjectConfiguration( configuration: DeepPartial ): string | Error { try { return serialize_project_configuration(configuration) } catch (e: any) { return new Error(`Error serializing project configuration: ${e}`) } }