diff --git a/e2e/playwright/sketch-tests.spec.ts b/e2e/playwright/sketch-tests.spec.ts index 980f357c5..e96ad60a5 100644 --- a/e2e/playwright/sketch-tests.spec.ts +++ b/e2e/playwright/sketch-tests.spec.ts @@ -943,6 +943,110 @@ sketch002 = startSketchOn(extrude001, 'END') `.replace(/\s/g, '') ) }) + + /* TODO: once we fix bug turn on. + test('empty-scene default-planes act as expected when spaces in file', async ({ + page, + browserName, + }) => { + + const u = await getUtils(page) + await page.setViewportSize({ width: 1200, height: 500 }) + + await u.waitForAuthSkipAppStart() + + await u.openDebugPanel() + await u.expectCmdLog('[data-message-type="execution-done"]') + await u.closeDebugPanel() + + const XYPlanePoint = { x: 774, y: 116 } as const + const unHoveredColor: [number, number, number] = [47, 47, 93] + expect( + await u.getGreatestPixDiff(XYPlanePoint, unHoveredColor) + ).toBeLessThan(8) + + await page.mouse.move(XYPlanePoint.x, XYPlanePoint.y) + await page.waitForTimeout(200) + + // color should not change for having been hovered + expect( + await u.getGreatestPixDiff(XYPlanePoint, unHoveredColor) + ).toBeLessThan(8) + + await u.openAndClearDebugPanel() + + // Fill with spaces + await u.codeLocator.fill(` +`) + + await u.openDebugPanel() + await u.expectCmdLog('[data-message-type="execution-done"]') + await u.closeDebugPanel() + + expect( + await u.getGreatestPixDiff(XYPlanePoint, unHoveredColor) + ).toBeLessThan(8) + + await page.mouse.move(XYPlanePoint.x, XYPlanePoint.y) + await page.waitForTimeout(200) + + // color should not change for having been hovered + expect( + await u.getGreatestPixDiff(XYPlanePoint, unHoveredColor) + ).toBeLessThan(8) + }) + + test('empty-scene default-planes act as expected when only code comments in file', async ({ + page, + browserName, + }) => { + + const u = await getUtils(page) + await page.setViewportSize({ width: 1200, height: 500 }) + + await u.waitForAuthSkipAppStart() + + await u.openDebugPanel() + await u.expectCmdLog('[data-message-type="execution-done"]') + await u.closeDebugPanel() + + const XYPlanePoint = { x: 774, y: 116 } as const + const unHoveredColor: [number, number, number] = [47, 47, 93] + expect( + await u.getGreatestPixDiff(XYPlanePoint, unHoveredColor) + ).toBeLessThan(8) + + await page.mouse.move(XYPlanePoint.x, XYPlanePoint.y) + await page.waitForTimeout(200) + + // color should not change for having been hovered + expect( + await u.getGreatestPixDiff(XYPlanePoint, unHoveredColor) + ).toBeLessThan(8) + + await u.openAndClearDebugPanel() + + // Fill with spaces + await u.codeLocator.fill(`// this is a code comments ya nerds +`) + + await u.openDebugPanel() + await u.expectCmdLog('[data-message-type="execution-done"]') + await u.closeDebugPanel() + + expect( + await u.getGreatestPixDiff(XYPlanePoint, unHoveredColor) + ).toBeLessThan(8) + + await page.mouse.move(XYPlanePoint.x, XYPlanePoint.y) + await page.waitForTimeout(200) + + // color should not change for having been hovered + expect( + await u.getGreatestPixDiff(XYPlanePoint, unHoveredColor) + ).toBeLessThan(8) + })*/ + test('empty-scene default-planes act as expected', async ({ page, browserName, diff --git a/src/clientSideScene/CameraControls.ts b/src/clientSideScene/CameraControls.ts index a0d9553d2..a11b3690f 100644 --- a/src/clientSideScene/CameraControls.ts +++ b/src/clientSideScene/CameraControls.ts @@ -155,7 +155,6 @@ export class CameraControls { this.camera.zoom = camProps.zoom || 1 } this.camera.updateProjectionMatrix() - console.log('doing this thing', camProps) this.update(true) } diff --git a/src/clientSideScene/ClientSideSceneComp.tsx b/src/clientSideScene/ClientSideSceneComp.tsx index 9f7dba921..f83be4740 100644 --- a/src/clientSideScene/ClientSideSceneComp.tsx +++ b/src/clientSideScene/ClientSideSceneComp.tsx @@ -31,6 +31,7 @@ import { recast, defaultSourceRange, resultIsOk, + ProgramMemory, } from 'lang/wasm' import { CustomIcon, CustomIconName } from 'components/CustomIcon' import { ConstrainInfo } from 'lang/std/stdTypes' @@ -420,9 +421,9 @@ export async function deleteSegment({ const testExecute = await executeAst({ ast: modifiedAst, - idGenerator: kclManager.execState.idGenerator, - useFakeExecutor: true, engineCommandManager: engineCommandManager, + // We make sure to send an empty program memory to denote we mean mock mode. + programMemoryOverride: ProgramMemory.empty(), }) if (testExecute.errors.length) { toast.error('Segment tag used outside of current Sketch. Could not delete.') diff --git a/src/clientSideScene/sceneEntities.ts b/src/clientSideScene/sceneEntities.ts index 75dc75864..8fc5e2d8e 100644 --- a/src/clientSideScene/sceneEntities.ts +++ b/src/clientSideScene/sceneEntities.ts @@ -498,10 +498,9 @@ export class SceneEntities { const { execState } = await executeAst({ ast: truncatedAst, - useFakeExecutor: true, engineCommandManager: this.engineCommandManager, + // We make sure to send an empty program memory to denote we mean mock mode. programMemoryOverride, - idGenerator: kclManager.execState.idGenerator, }) const programMemory = execState.memory const sketch = sketchFromPathToNode({ @@ -955,10 +954,9 @@ export class SceneEntities { const { execState } = await executeAst({ ast: truncatedAst, - useFakeExecutor: true, engineCommandManager: this.engineCommandManager, + // We make sure to send an empty program memory to denote we mean mock mode. programMemoryOverride, - idGenerator: kclManager.execState.idGenerator, }) const programMemory = execState.memory this.sceneProgramMemory = programMemory @@ -1019,10 +1017,9 @@ export class SceneEntities { const { execState } = await executeAst({ ast: _ast, - useFakeExecutor: true, engineCommandManager: this.engineCommandManager, + // We make sure to send an empty program memory to denote we mean mock mode. programMemoryOverride, - idGenerator: kclManager.execState.idGenerator, }) const programMemory = execState.memory @@ -1120,10 +1117,9 @@ export class SceneEntities { const { execState } = await executeAst({ ast: truncatedAst, - useFakeExecutor: true, engineCommandManager: this.engineCommandManager, + // We make sure to send an empty program memory to denote we mean mock mode. programMemoryOverride, - idGenerator: kclManager.execState.idGenerator, }) const programMemory = execState.memory this.sceneProgramMemory = programMemory @@ -1187,10 +1183,9 @@ export class SceneEntities { const { execState } = await executeAst({ ast: _ast, - useFakeExecutor: true, engineCommandManager: this.engineCommandManager, + // We make sure to send an empty program memory to denote we mean mock mode. programMemoryOverride, - idGenerator: kclManager.execState.idGenerator, }) const programMemory = execState.memory @@ -1306,10 +1301,9 @@ export class SceneEntities { const { execState } = await executeAst({ ast: modded, - useFakeExecutor: true, engineCommandManager: this.engineCommandManager, + // We make sure to send an empty program memory to denote we mean mock mode. programMemoryOverride, - idGenerator: kclManager.execState.idGenerator, }) const programMemory = execState.memory this.sceneProgramMemory = programMemory @@ -1691,10 +1685,9 @@ export class SceneEntities { codeManager.updateCodeEditor(code) const { execState } = await executeAst({ ast: truncatedAst, - useFakeExecutor: true, engineCommandManager: this.engineCommandManager, + // We make sure to send an empty program memory to denote we mean mock mode. programMemoryOverride, - idGenerator: kclManager.execState.idGenerator, }) const programMemory = execState.memory this.sceneProgramMemory = programMemory diff --git a/src/components/AvailableVarsHelpers.tsx b/src/components/AvailableVarsHelpers.tsx index 7f8956d5e..5c6cc4d0b 100644 --- a/src/components/AvailableVarsHelpers.tsx +++ b/src/components/AvailableVarsHelpers.tsx @@ -163,9 +163,8 @@ export function useCalc({ executeAst({ ast, engineCommandManager, - useFakeExecutor: true, + // We make sure to send an empty program memory to denote we mean mock mode. programMemoryOverride: kclManager.programMemory.clone(), - idGenerator: kclManager.execState.idGenerator, }).then(({ execState }) => { const resultDeclaration = ast.body.find( (a) => diff --git a/src/components/ModelingSidebar/ModelingPanes/MemoryPane.test.tsx b/src/components/ModelingSidebar/ModelingPanes/MemoryPane.test.tsx index 8d142ddfe..cbe1d0fbb 100644 --- a/src/components/ModelingSidebar/ModelingPanes/MemoryPane.test.tsx +++ b/src/components/ModelingSidebar/ModelingPanes/MemoryPane.test.tsx @@ -34,6 +34,10 @@ describe('processMemory', () => { expect(output.myVar).toEqual(5) expect(output.otherVar).toEqual(3) expect(output).toEqual({ + HALF_TURN: 180, + QUARTER_TURN: 90, + THREE_QUARTER_TURN: 270, + ZERO: 0, myVar: 5, myFn: '__function(a)__', otherVar: 3, diff --git a/src/components/ProjectSidebarMenu.tsx b/src/components/ProjectSidebarMenu.tsx index be58d9918..74218288c 100644 --- a/src/components/ProjectSidebarMenu.tsx +++ b/src/components/ProjectSidebarMenu.tsx @@ -68,8 +68,8 @@ function AppLogoLink({ data-testid="app-logo" onClick={() => { onProjectClose(file || null, project?.path || null, false) - // Clear the scene and end the session. - engineCommandManager.endSession() + // Clear the scene. + engineCommandManager.clearScene() }} to={PATHS.HOME} className={wrapperClassName + ' hover:before:brightness-110'} @@ -190,8 +190,8 @@ function ProjectMenuPopover({ className: !isDesktop() ? 'hidden' : '', onClick: () => { onProjectClose(file || null, project?.path || null, true) - // Clear the scene and end the session. - engineCommandManager.endSession() + // Clear the scene. + engineCommandManager.clearScene() }, }, ].filter( diff --git a/src/lang/KclSingleton.ts b/src/lang/KclSingleton.ts index 1426be879..c38940e70 100644 --- a/src/lang/KclSingleton.ts +++ b/src/lang/KclSingleton.ts @@ -88,7 +88,7 @@ export class KclManager { this._programMemoryCallBack(programMemory) } - set execState(execState) { + private set execState(execState) { this._execState = execState this.programMemory = execState.memory } @@ -227,12 +227,6 @@ export class KclManager { this.addDiagnostics(complilationErrorsToDiagnostics(result.warnings)) if (result.errors.length > 0) { this._hasErrors = true - // TODO: re-eval if session should end? - for (const e of result.errors) - if (e.message === 'file is empty') { - this.engineCommandManager?.endSession() - break - } return null } @@ -276,12 +270,9 @@ export class KclManager { this._cancelTokens.set(currentExecutionId, false) this.isExecuting = true - // Make sure we clear before starting again. End session will do this. - this.engineCommandManager?.endSession() await this.ensureWasmInit() const { logs, errors, execState, isInterrupted } = await executeAst({ ast, - idGenerator: this.execState.idGenerator, engineCommandManager: this.engineCommandManager, }) @@ -331,8 +322,6 @@ export class KclManager { this.logs = logs // Do not add the errors since the program was interrupted and the error is not a real KCL error this.addDiagnostics(isInterrupted ? [] : kclErrorsToDiagnostics(errors)) - // Reset the next ID index so that we reuse the previous IDs next time. - execState.idGenerator.nextId = 0 this.execState = execState if (!errors.length) { this.lastSuccessfulProgramMemory = execState.memory @@ -373,9 +362,9 @@ export class KclManager { const { logs, errors, execState } = await executeAst({ ast: newAst, - idGenerator: this.execState.idGenerator, engineCommandManager: this.engineCommandManager, - useFakeExecutor: true, + // We make sure to send an empty program memory to denote we mean mock mode. + programMemoryOverride: ProgramMemory.empty(), }) this._logs = logs diff --git a/src/lang/langHelpers.ts b/src/lang/langHelpers.ts index 9a07aae98..3e604705d 100644 --- a/src/lang/langHelpers.ts +++ b/src/lang/langHelpers.ts @@ -2,7 +2,6 @@ import { Program, _executor, ProgramMemory, - programMemoryInit, kclLint, emptyExecState, ExecState, @@ -11,7 +10,6 @@ import { enginelessExecutor } from 'lib/testHelpers' import { EngineCommandManager } from 'lang/std/engineConnection' import { KCLError } from 'lang/errors' import { Diagnostic } from '@codemirror/lint' -import { IdGenerator } from 'wasm-lib/kcl/bindings/IdGenerator' import { Node } from 'wasm-lib/kcl/bindings/Node' export type ToolTip = @@ -49,15 +47,13 @@ export const toolTips: Array = [ export async function executeAst({ ast, engineCommandManager, - useFakeExecutor = false, + // If you set programMemoryOverride we assume you mean mock mode. Since that + // is the only way to go about it. programMemoryOverride, - idGenerator, }: { ast: Node engineCommandManager: EngineCommandManager - useFakeExecutor?: boolean programMemoryOverride?: ProgramMemory - idGenerator?: IdGenerator isInterrupted?: boolean }): Promise<{ logs: string[] @@ -66,22 +62,14 @@ export async function executeAst({ isInterrupted: boolean }> { try { - if (!useFakeExecutor) { - engineCommandManager.endSession() - // eslint-disable-next-line @typescript-eslint/no-floating-promises - engineCommandManager.startNewSession() - } - const execState = await (useFakeExecutor - ? enginelessExecutor(ast, programMemoryOverride || programMemoryInit()) - : _executor( - ast, - programMemoryInit(), - idGenerator, - engineCommandManager, - false - )) + const execState = await (programMemoryOverride + ? enginelessExecutor(ast, programMemoryOverride) + : _executor(ast, engineCommandManager)) + + await engineCommandManager.waitForAllCommands( + programMemoryOverride !== undefined + ) - await engineCommandManager.waitForAllCommands(useFakeExecutor) return { logs: [], errors: [], diff --git a/src/lang/std/engineConnection.ts b/src/lang/std/engineConnection.ts index 6aca2062b..6070cca09 100644 --- a/src/lang/std/engineConnection.ts +++ b/src/lang/std/engineConnection.ts @@ -1879,7 +1879,7 @@ export class EngineCommandManager extends EventTarget { } return JSON.stringify(this.defaultPlanes) } - endSession() { + clearScene(): void { const deleteCmd: EngineCommand = { type: 'modeling_cmd_req', cmd_id: uuidv4(), diff --git a/src/lang/wasm.ts b/src/lang/wasm.ts index cfa32c43d..62bae87e5 100644 --- a/src/lang/wasm.ts +++ b/src/lang/wasm.ts @@ -35,7 +35,6 @@ import { Configuration } from 'wasm-lib/kcl/bindings/Configuration' import { DeepPartial } from 'lib/types' import { ProjectConfiguration } from 'wasm-lib/kcl/bindings/ProjectConfiguration' import { Sketch } from '../wasm-lib/kcl/bindings/Sketch' -import { IdGenerator } from 'wasm-lib/kcl/bindings/IdGenerator' import { ExecState as RawExecState } from '../wasm-lib/kcl/bindings/ExecState' import { ProgramMemory as RawProgramMemory } from '../wasm-lib/kcl/bindings/ProgramMemory' import { EnvironmentRef } from '../wasm-lib/kcl/bindings/EnvironmentRef' @@ -216,7 +215,6 @@ export const isPathToNodeNumber = ( export interface ExecState { memory: ProgramMemory - idGenerator: IdGenerator } /** @@ -226,21 +224,12 @@ export interface ExecState { export function emptyExecState(): ExecState { return { memory: ProgramMemory.empty(), - idGenerator: defaultIdGenerator(), } } function execStateFromRaw(raw: RawExecState): ExecState { return { memory: ProgramMemory.fromRaw(raw.memory), - idGenerator: raw.idGenerator, - } -} - -export function defaultIdGenerator(): IdGenerator { - return { - nextId: 0, - ids: [], } } @@ -254,6 +243,19 @@ function emptyEnvironment(): Environment { return { bindings: {}, parent: null } } +function emptyRootEnvironment(): Environment { + return { + // This is dumb this is copied from rust. + bindings: { + ZERO: { type: 'Number', value: 0.0, __meta: [] }, + QUARTER_TURN: { type: 'Number', value: 90.0, __meta: [] }, + HALF_TURN: { type: 'Number', value: 180.0, __meta: [] }, + THREE_QUARTER_TURN: { type: 'Number', value: 270.0, __meta: [] }, + }, + parent: null, + } +} + /** * This duplicates logic in Rust. The hope is to keep ProgramMemory internals * isolated from the rest of the TypeScript code so that we can move it to Rust @@ -276,7 +278,7 @@ export class ProgramMemory { } constructor( - environments: Environment[] = [emptyEnvironment()], + environments: Environment[] = [emptyRootEnvironment()], currentEnv: EnvironmentRef = ROOT_ENVIRONMENT_REF, returnVal: KclValue | null = null ) { @@ -463,36 +465,31 @@ export function sketchFromKclValue( export const executor = async ( node: Node, - programMemory: ProgramMemory | Error = ProgramMemory.empty(), - idGenerator: IdGenerator = defaultIdGenerator(), engineCommandManager: EngineCommandManager, - isMock: boolean = false + programMemoryOverride: ProgramMemory | Error | null = null ): Promise => { - if (err(programMemory)) return Promise.reject(programMemory) + if (programMemoryOverride !== null && err(programMemoryOverride)) + return Promise.reject(programMemoryOverride) // eslint-disable-next-line @typescript-eslint/no-floating-promises engineCommandManager.startNewSession() const _programMemory = await _executor( node, - programMemory, - idGenerator, engineCommandManager, - isMock + programMemoryOverride ) await engineCommandManager.waitForAllCommands() - engineCommandManager.endSession() return _programMemory } export const _executor = async ( node: Node, - programMemory: ProgramMemory | Error = ProgramMemory.empty(), - idGenerator: IdGenerator = defaultIdGenerator(), engineCommandManager: EngineCommandManager, - isMock: boolean + programMemoryOverride: ProgramMemory | Error | null = null ): Promise => { - if (err(programMemory)) return Promise.reject(programMemory) + if (programMemoryOverride !== null && err(programMemoryOverride)) + return Promise.reject(programMemoryOverride) try { let baseUnit = 'mm' @@ -505,13 +502,10 @@ export const _executor = async ( } const execState: RawExecState = await execute_wasm( JSON.stringify(node), - JSON.stringify(programMemory.toRaw()), - JSON.stringify(idGenerator), + JSON.stringify(programMemoryOverride?.toRaw() || null), baseUnit, engineCommandManager, - fileSystemManager, - undefined, - isMock + fileSystemManager ) return execStateFromRaw(execState) } catch (e: any) { diff --git a/src/lib/desktopFS.ts b/src/lib/desktopFS.ts index 6cde7f726..efa611c5f 100644 --- a/src/lib/desktopFS.ts +++ b/src/lib/desktopFS.ts @@ -116,8 +116,8 @@ export async function createAndOpenNewTutorialProject({ ) => void navigate: (path: string) => void }) { - // Clear the scene and end the session. - engineCommandManager.endSession() + // Clear the scene. + engineCommandManager.clearScene() // Create a new project with the onboarding project name const configuration = await readAppSettingsFile() diff --git a/src/lib/testHelpers.ts b/src/lib/testHelpers.ts index 2454e1b0a..4f545ad11 100644 --- a/src/lib/testHelpers.ts +++ b/src/lib/testHelpers.ts @@ -4,7 +4,6 @@ import { _executor, SourceRange, ExecState, - defaultIdGenerator, } from '../lang/wasm' import { EngineCommandManager, @@ -16,7 +15,6 @@ import { v4 as uuidv4 } from 'uuid' import { DefaultPlanes } from 'wasm-lib/kcl/bindings/DefaultPlanes' import { err, reportRejection } from 'lib/trap' import { toSync } from './utils' -import { IdGenerator } from 'wasm-lib/kcl/bindings/IdGenerator' import { Node } from 'wasm-lib/kcl/bindings/Node' type WebSocketResponse = Models['WebSocketResponse_type'] @@ -86,10 +84,9 @@ class MockEngineCommandManager { export async function enginelessExecutor( ast: Node, - pm: ProgramMemory | Error = ProgramMemory.empty(), - idGenerator: IdGenerator = defaultIdGenerator() + pmo: ProgramMemory | Error = ProgramMemory.empty() ): Promise { - if (err(pm)) return Promise.reject(pm) + if (pmo !== null && err(pmo)) return Promise.reject(pmo) const mockEngineCommandManager = new MockEngineCommandManager({ setIsStreamReady: () => {}, @@ -97,21 +94,14 @@ export async function enginelessExecutor( }) as any as EngineCommandManager // eslint-disable-next-line @typescript-eslint/no-floating-promises mockEngineCommandManager.startNewSession() - const execState = await _executor( - ast, - pm, - idGenerator, - mockEngineCommandManager, - true - ) + const execState = await _executor(ast, mockEngineCommandManager, pmo) await mockEngineCommandManager.waitForAllCommands() return execState } export async function executor( ast: Node, - pm: ProgramMemory = ProgramMemory.empty(), - idGenerator: IdGenerator = defaultIdGenerator() + pmo: ProgramMemory = ProgramMemory.empty() ): Promise { const engineCommandManager = new EngineCommandManager() engineCommandManager.start({ @@ -133,13 +123,7 @@ export async function executor( toSync(async () => { // eslint-disable-next-line @typescript-eslint/no-floating-promises engineCommandManager.startNewSession() - const execState = await _executor( - ast, - pm, - idGenerator, - engineCommandManager, - false - ) + const execState = await _executor(ast, engineCommandManager, pmo) await engineCommandManager.waitForAllCommands() resolve(execState) }, reportRejection) diff --git a/src/lib/useCalculateKclExpression.ts b/src/lib/useCalculateKclExpression.ts index 51f133564..284137828 100644 --- a/src/lib/useCalculateKclExpression.ts +++ b/src/lib/useCalculateKclExpression.ts @@ -103,9 +103,8 @@ export function useCalculateKclExpression({ const { execState } = await executeAst({ ast, engineCommandManager, - useFakeExecutor: true, + // We make sure to send an empty program memory to denote we mean mock mode. programMemoryOverride: kclManager.programMemory.clone(), - idGenerator: kclManager.execState.idGenerator, }) const resultDeclaration = ast.body.find( (a) => diff --git a/src/machines/modelingMachine.ts b/src/machines/modelingMachine.ts index 17c4d618a..6930547ed 100644 --- a/src/machines/modelingMachine.ts +++ b/src/machines/modelingMachine.ts @@ -1,5 +1,6 @@ import { PathToNode, + ProgramMemory, VariableDeclaration, VariableDeclarator, parse, @@ -730,9 +731,9 @@ export const modelingMachine = setup({ const testExecute = await executeAst({ ast: modifiedAst, - idGenerator: kclManager.execState.idGenerator, - useFakeExecutor: true, engineCommandManager, + // We make sure to send an empty program memory to denote we mean mock mode. + programMemoryOverride: ProgramMemory.empty(), }) if (testExecute.errors.length) { toast.error('Unable to delete part') diff --git a/src/wasm-lib/Cargo.lock b/src/wasm-lib/Cargo.lock index 9c8edca31..8006e4f68 100644 --- a/src/wasm-lib/Cargo.lock +++ b/src/wasm-lib/Cargo.lock @@ -4308,6 +4308,7 @@ dependencies = [ "kcl-lib", "kittycad", "kittycad-modeling-cmds", + "lazy_static", "pretty_assertions", "reqwest", "serde_json", diff --git a/src/wasm-lib/Cargo.toml b/src/wasm-lib/Cargo.toml index f537690fc..5a5359edf 100644 --- a/src/wasm-lib/Cargo.toml +++ b/src/wasm-lib/Cargo.toml @@ -15,6 +15,7 @@ data-encoding = "2.6.0" gloo-utils = "0.2.0" kcl-lib = { path = "kcl" } kittycad.workspace = true +lazy_static = "1.5.0" serde_json = "1.0.128" tokio = { version = "1.41.1", features = ["sync"] } toml = "0.8.19" diff --git a/src/wasm-lib/derive-docs/src/lib.rs b/src/wasm-lib/derive-docs/src/lib.rs index 3669ba105..1a7dc5c82 100644 --- a/src/wasm-lib/derive-docs/src/lib.rs +++ b/src/wasm-lib/derive-docs/src/lib.rs @@ -778,7 +778,7 @@ fn generate_code_block_test(fn_name: &str, code_block: &str, index: usize) -> pr context_type: crate::executor::ContextType::Mock, }; - ctx.run(&program, &mut crate::ExecState::default()).await.unwrap(); + ctx.run(program.into(), &mut crate::ExecState::default()).await.unwrap(); } #[tokio::test(flavor = "multi_thread", worker_threads = 5)] diff --git a/src/wasm-lib/derive-docs/tests/args_with_lifetime.gen b/src/wasm-lib/derive-docs/tests/args_with_lifetime.gen index 32e218be8..e5157f3c5 100644 --- a/src/wasm-lib/derive-docs/tests/args_with_lifetime.gen +++ b/src/wasm-lib/derive-docs/tests/args_with_lifetime.gen @@ -14,7 +14,7 @@ mod test_examples_someFn { settings: Default::default(), context_type: crate::executor::ContextType::Mock, }; - ctx.run(&program, &mut crate::ExecState::default()) + ctx.run(program.into(), &mut crate::ExecState::default()) .await .unwrap(); } diff --git a/src/wasm-lib/derive-docs/tests/args_with_refs.gen b/src/wasm-lib/derive-docs/tests/args_with_refs.gen index 731e208c8..ab2a3786d 100644 --- a/src/wasm-lib/derive-docs/tests/args_with_refs.gen +++ b/src/wasm-lib/derive-docs/tests/args_with_refs.gen @@ -14,7 +14,7 @@ mod test_examples_someFn { settings: Default::default(), context_type: crate::executor::ContextType::Mock, }; - ctx.run(&program, &mut crate::ExecState::default()) + ctx.run(program.into(), &mut crate::ExecState::default()) .await .unwrap(); } diff --git a/src/wasm-lib/derive-docs/tests/array.gen b/src/wasm-lib/derive-docs/tests/array.gen index 6e52a2e70..891a5f2c2 100644 --- a/src/wasm-lib/derive-docs/tests/array.gen +++ b/src/wasm-lib/derive-docs/tests/array.gen @@ -15,7 +15,7 @@ mod test_examples_show { settings: Default::default(), context_type: crate::executor::ContextType::Mock, }; - ctx.run(&program, &mut crate::ExecState::default()) + ctx.run(program.into(), &mut crate::ExecState::default()) .await .unwrap(); } @@ -49,7 +49,7 @@ mod test_examples_show { settings: Default::default(), context_type: crate::executor::ContextType::Mock, }; - ctx.run(&program, &mut crate::ExecState::default()) + ctx.run(program.into(), &mut crate::ExecState::default()) .await .unwrap(); } diff --git a/src/wasm-lib/derive-docs/tests/box.gen b/src/wasm-lib/derive-docs/tests/box.gen index 79c578d90..648514848 100644 --- a/src/wasm-lib/derive-docs/tests/box.gen +++ b/src/wasm-lib/derive-docs/tests/box.gen @@ -15,7 +15,7 @@ mod test_examples_show { settings: Default::default(), context_type: crate::executor::ContextType::Mock, }; - ctx.run(&program, &mut crate::ExecState::default()) + ctx.run(program.into(), &mut crate::ExecState::default()) .await .unwrap(); } diff --git a/src/wasm-lib/derive-docs/tests/doc_comment_with_code.gen b/src/wasm-lib/derive-docs/tests/doc_comment_with_code.gen index de85bbe1b..038771738 100644 --- a/src/wasm-lib/derive-docs/tests/doc_comment_with_code.gen +++ b/src/wasm-lib/derive-docs/tests/doc_comment_with_code.gen @@ -16,7 +16,7 @@ mod test_examples_my_func { settings: Default::default(), context_type: crate::executor::ContextType::Mock, }; - ctx.run(&program, &mut crate::ExecState::default()) + ctx.run(program.into(), &mut crate::ExecState::default()) .await .unwrap(); } @@ -50,7 +50,7 @@ mod test_examples_my_func { settings: Default::default(), context_type: crate::executor::ContextType::Mock, }; - ctx.run(&program, &mut crate::ExecState::default()) + ctx.run(program.into(), &mut crate::ExecState::default()) .await .unwrap(); } diff --git a/src/wasm-lib/derive-docs/tests/lineTo.gen b/src/wasm-lib/derive-docs/tests/lineTo.gen index 27f1f7c8c..725806bdc 100644 --- a/src/wasm-lib/derive-docs/tests/lineTo.gen +++ b/src/wasm-lib/derive-docs/tests/lineTo.gen @@ -16,7 +16,7 @@ mod test_examples_line_to { settings: Default::default(), context_type: crate::executor::ContextType::Mock, }; - ctx.run(&program, &mut crate::ExecState::default()) + ctx.run(program.into(), &mut crate::ExecState::default()) .await .unwrap(); } @@ -50,7 +50,7 @@ mod test_examples_line_to { settings: Default::default(), context_type: crate::executor::ContextType::Mock, }; - ctx.run(&program, &mut crate::ExecState::default()) + ctx.run(program.into(), &mut crate::ExecState::default()) .await .unwrap(); } diff --git a/src/wasm-lib/derive-docs/tests/min.gen b/src/wasm-lib/derive-docs/tests/min.gen index 96c7040da..bd02474ce 100644 --- a/src/wasm-lib/derive-docs/tests/min.gen +++ b/src/wasm-lib/derive-docs/tests/min.gen @@ -15,7 +15,7 @@ mod test_examples_min { settings: Default::default(), context_type: crate::executor::ContextType::Mock, }; - ctx.run(&program, &mut crate::ExecState::default()) + ctx.run(program.into(), &mut crate::ExecState::default()) .await .unwrap(); } @@ -49,7 +49,7 @@ mod test_examples_min { settings: Default::default(), context_type: crate::executor::ContextType::Mock, }; - ctx.run(&program, &mut crate::ExecState::default()) + ctx.run(program.into(), &mut crate::ExecState::default()) .await .unwrap(); } diff --git a/src/wasm-lib/derive-docs/tests/option.gen b/src/wasm-lib/derive-docs/tests/option.gen index 21e80e32d..b6ad9ef25 100644 --- a/src/wasm-lib/derive-docs/tests/option.gen +++ b/src/wasm-lib/derive-docs/tests/option.gen @@ -15,7 +15,7 @@ mod test_examples_show { settings: Default::default(), context_type: crate::executor::ContextType::Mock, }; - ctx.run(&program, &mut crate::ExecState::default()) + ctx.run(program.into(), &mut crate::ExecState::default()) .await .unwrap(); } diff --git a/src/wasm-lib/derive-docs/tests/option_input_format.gen b/src/wasm-lib/derive-docs/tests/option_input_format.gen index fe40eb931..e2918a608 100644 --- a/src/wasm-lib/derive-docs/tests/option_input_format.gen +++ b/src/wasm-lib/derive-docs/tests/option_input_format.gen @@ -15,7 +15,7 @@ mod test_examples_import { settings: Default::default(), context_type: crate::executor::ContextType::Mock, }; - ctx.run(&program, &mut crate::ExecState::default()) + ctx.run(program.into(), &mut crate::ExecState::default()) .await .unwrap(); } diff --git a/src/wasm-lib/derive-docs/tests/return_vec_box_sketch.gen b/src/wasm-lib/derive-docs/tests/return_vec_box_sketch.gen index 72292beeb..c9fa22f4e 100644 --- a/src/wasm-lib/derive-docs/tests/return_vec_box_sketch.gen +++ b/src/wasm-lib/derive-docs/tests/return_vec_box_sketch.gen @@ -15,7 +15,7 @@ mod test_examples_import { settings: Default::default(), context_type: crate::executor::ContextType::Mock, }; - ctx.run(&program, &mut crate::ExecState::default()) + ctx.run(program.into(), &mut crate::ExecState::default()) .await .unwrap(); } diff --git a/src/wasm-lib/derive-docs/tests/return_vec_sketch.gen b/src/wasm-lib/derive-docs/tests/return_vec_sketch.gen index c0c97a13b..e00391e37 100644 --- a/src/wasm-lib/derive-docs/tests/return_vec_sketch.gen +++ b/src/wasm-lib/derive-docs/tests/return_vec_sketch.gen @@ -15,7 +15,7 @@ mod test_examples_import { settings: Default::default(), context_type: crate::executor::ContextType::Mock, }; - ctx.run(&program, &mut crate::ExecState::default()) + ctx.run(program.into(), &mut crate::ExecState::default()) .await .unwrap(); } diff --git a/src/wasm-lib/derive-docs/tests/show.gen b/src/wasm-lib/derive-docs/tests/show.gen index cfd2bc35f..f0b74f8f4 100644 --- a/src/wasm-lib/derive-docs/tests/show.gen +++ b/src/wasm-lib/derive-docs/tests/show.gen @@ -15,7 +15,7 @@ mod test_examples_show { settings: Default::default(), context_type: crate::executor::ContextType::Mock, }; - ctx.run(&program, &mut crate::ExecState::default()) + ctx.run(program.into(), &mut crate::ExecState::default()) .await .unwrap(); } diff --git a/src/wasm-lib/derive-docs/tests/test_args_with_exec_state.gen b/src/wasm-lib/derive-docs/tests/test_args_with_exec_state.gen index d5735273f..d41823404 100644 --- a/src/wasm-lib/derive-docs/tests/test_args_with_exec_state.gen +++ b/src/wasm-lib/derive-docs/tests/test_args_with_exec_state.gen @@ -14,7 +14,7 @@ mod test_examples_some_function { settings: Default::default(), context_type: crate::executor::ContextType::Mock, }; - ctx.run(&program, &mut crate::ExecState::default()) + ctx.run(program.into(), &mut crate::ExecState::default()) .await .unwrap(); } diff --git a/src/wasm-lib/kcl-to-core/src/conn_mock_core.rs b/src/wasm-lib/kcl-to-core/src/conn_mock_core.rs index c3dea7144..7780381b0 100644 --- a/src/wasm-lib/kcl-to-core/src/conn_mock_core.rs +++ b/src/wasm-lib/kcl-to-core/src/conn_mock_core.rs @@ -1,3 +1,8 @@ +use std::{ + collections::HashMap, + sync::{Arc, Mutex}, +}; + use anyhow::Result; use indexmap::IndexMap; use kcl_lib::{ @@ -11,10 +16,6 @@ use kittycad_modeling_cmds::{ shared::PathSegment::{self, *}, websocket::{ModelingBatch, ModelingCmdReq, OkWebSocketResponseData, WebSocketRequest, WebSocketResponse}, }; -use std::{ - collections::HashMap, - sync::{Arc, Mutex}, -}; use tokio::sync::RwLock; const CPP_PREFIX: &str = "const double scaleFactor = 100;\n"; diff --git a/src/wasm-lib/kcl-to-core/src/lib.rs b/src/wasm-lib/kcl-to-core/src/lib.rs index 8c00f341e..ae8187d7d 100644 --- a/src/wasm-lib/kcl-to-core/src/lib.rs +++ b/src/wasm-lib/kcl-to-core/src/lib.rs @@ -1,6 +1,7 @@ +use std::sync::{Arc, Mutex}; + use anyhow::Result; use kcl_lib::{ExecState, ExecutorContext}; -use std::sync::{Arc, Mutex}; #[cfg(not(target_arch = "wasm32"))] mod conn_mock_core; @@ -15,7 +16,7 @@ pub async fn kcl_to_engine_core(code: &str) -> Result { let ctx = ExecutorContext::new_forwarded_mock(Arc::new(Box::new( crate::conn_mock_core::EngineConnection::new(ref_result).await?, ))); - ctx.run(&program, &mut ExecState::default()).await?; + ctx.run(program.into(), &mut ExecState::default()).await?; let result = result.lock().expect("mutex lock").clone(); Ok(result) diff --git a/src/wasm-lib/kcl-to-core/src/tool.rs b/src/wasm-lib/kcl-to-core/src/tool.rs index 626b0b880..6706c7cb1 100644 --- a/src/wasm-lib/kcl-to-core/src/tool.rs +++ b/src/wasm-lib/kcl-to-core/src/tool.rs @@ -1,6 +1,7 @@ -use kcl_to_core::*; use std::{env, fs}; +use kcl_to_core::*; + #[tokio::main] async fn main() { let args: Vec = env::args().collect(); diff --git a/src/wasm-lib/kcl/src/ast/mod.rs b/src/wasm-lib/kcl/src/ast/mod.rs new file mode 100644 index 000000000..feef6f7cc --- /dev/null +++ b/src/wasm-lib/kcl/src/ast/mod.rs @@ -0,0 +1,3 @@ +pub mod cache; +pub mod modify; +pub mod types; diff --git a/src/wasm-lib/kcl/src/executor.rs b/src/wasm-lib/kcl/src/executor.rs index 755b1fffb..6ca4952c2 100644 --- a/src/wasm-lib/kcl/src/executor.rs +++ b/src/wasm-lib/kcl/src/executor.rs @@ -25,7 +25,10 @@ use crate::{ engine::{EngineManager, ExecutionKind}, errors::{KclError, KclErrorDetails}, fs::{FileManager, FileSystem}, - parsing::ast::types::{BodyItem, Expr, FunctionExpression, ItemVisibility, Node, NodeRef, TagDeclarator, TagNode}, + parsing::ast::{ + cache::{get_changed_program, CacheInformation}, + types::{BodyItem, Expr, FunctionExpression, ItemVisibility, Node, NodeRef, TagDeclarator, TagNode}, + }, settings::types::UnitLength, source_range::{ModuleId, SourceRange}, std::{args::Arg, StdLib}, @@ -55,9 +58,6 @@ pub struct ExecState { pub path_to_source_id: IndexMap, /// Map from module ID to module info. pub module_infos: IndexMap, - /// 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, } impl ExecState { @@ -1484,7 +1484,8 @@ pub struct ExecutorContext { } /// The executor settings. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)] +#[ts(export)] pub struct ExecutorSettings { /// The unit to use in modeling dimensions. pub units: UnitLength, @@ -1785,18 +1786,21 @@ impl ExecutorContext { &self, exec_state: &mut ExecState, source_range: crate::executor::SourceRange, - ) -> Result<()> { + ) -> Result<(), KclError> { self.engine .clear_scene(&mut exec_state.id_generator, source_range) .await?; + + // We do not create the planes here as the post hook in wasm will do that + // AND if we aren't in wasm it doesn't really matter. Ok(()) } /// Perform the execution of a program. /// You can optionally pass in some initialization memory. /// Kurt uses this for partial execution. - pub async fn run(&self, program: &Program, exec_state: &mut ExecState) -> Result<(), KclError> { - self.run_with_session_data(program, exec_state).await?; + pub async fn run(&self, cache_info: CacheInformation, exec_state: &mut ExecState) -> Result<(), KclError> { + self.run_with_session_data(cache_info, exec_state).await?; Ok(()) } @@ -1805,10 +1809,27 @@ impl ExecutorContext { /// Kurt uses this for partial execution. pub async fn run_with_session_data( &self, - program: &Program, + cache_info: CacheInformation, exec_state: &mut ExecState, ) -> Result, KclError> { let _stats = crate::log::LogPerfStats::new("Interpretation"); + + // Get the program that actually changed from the old and new information. + let cache_result = get_changed_program(cache_info.clone(), &self.settings); + + // Check if we don't need to re-execute. + let Some(cache_result) = cache_result else { + return Ok(None); + }; + + if cache_result.clear_scene && !self.is_mock() { + // We don't do this in mock mode since there is no engine connection + // anyways and from the TS side we override memory and don't want to clear it. + self.reset_scene(exec_state, Default::default()).await?; + // Pop the execution state, since we are starting fresh. + *exec_state = Default::default(); + } + // TODO: Use the top-level file's path. exec_state.add_module(std::path::PathBuf::from("")); // Before we even start executing the program, set the units. @@ -1829,7 +1850,7 @@ impl ExecutorContext { ) .await?; - self.inner_execute(&program.ast, exec_state, crate::executor::BodyType::Root) + self.inner_execute(&cache_result.program, exec_state, crate::executor::BodyType::Root) .await?; let session_data = self.engine.get_session_data(); Ok(session_data) @@ -1857,11 +1878,7 @@ impl ExecutorContext { source_ranges: vec![source_range], })); } - 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) - }; + let resolved_path = std::path::PathBuf::from(&path); if exec_state.import_stack.contains(&resolved_path) { return Err(KclError::ImportCycle(KclErrorDetails { message: format!( @@ -2097,7 +2114,7 @@ impl ExecutorContext { program: &Program, exec_state: &mut ExecState, ) -> std::result::Result { - self.run(program, exec_state).await?; + self.run(program.clone().into(), exec_state).await?; // Zoom to fit. self.engine @@ -2243,7 +2260,7 @@ mod tests { context_type: ContextType::Mock, }; let mut exec_state = ExecState::default(); - ctx.run(&program, &mut exec_state).await?; + ctx.run(program.into(), &mut exec_state).await?; Ok(exec_state.memory) } diff --git a/src/wasm-lib/kcl/src/lib.rs b/src/wasm-lib/kcl/src/lib.rs index 05bf07795..a6bde55cb 100644 --- a/src/wasm-lib/kcl/src/lib.rs +++ b/src/wasm-lib/kcl/src/lib.rs @@ -89,7 +89,11 @@ pub use lsp::{ copilot::Backend as CopilotLspBackend, kcl::{Backend as KclLspBackend, Server as KclLspServerSubCommand}, }; -pub use parsing::ast::{modify::modify_ast_for_sketch, types::FormatOptions}; +pub use parsing::ast::{ + cache::{CacheInformation, OldAstState}, + modify::modify_ast_for_sketch, + types::FormatOptions, +}; pub use settings::types::{project::ProjectConfiguration, Configuration, UnitLength}; pub use source_range::{ModuleId, SourceRange}; @@ -125,7 +129,7 @@ use crate::log::{log, logln}; #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct Program { #[serde(flatten)] - ast: parsing::ast::types::Node, + pub ast: parsing::ast::types::Node, } #[cfg(any(test, feature = "lsp-test-util"))] diff --git a/src/wasm-lib/kcl/src/lsp/kcl/mod.rs b/src/wasm-lib/kcl/src/lsp/kcl/mod.rs index 9a023bcfe..b5cb024f8 100644 --- a/src/wasm-lib/kcl/src/lsp/kcl/mod.rs +++ b/src/wasm-lib/kcl/src/lsp/kcl/mod.rs @@ -45,11 +45,14 @@ use crate::{ errors::Suggestion, lsp::{backend::Backend as _, util::IntoDiagnostic}, parsing::{ - ast::types::{Expr, Node, VariableKind}, + ast::{ + cache::{CacheInformation, OldAstState}, + types::{Expr, Node, VariableKind}, + }, token::TokenType, PIPE_OPERATOR, }, - ExecState, ModuleId, Program, SourceRange, + ModuleId, Program, SourceRange, }; lazy_static::lazy_static! { @@ -105,6 +108,12 @@ pub struct Backend { pub token_map: DashMap>, /// AST maps. pub ast_map: DashMap>, + /// Last successful execution. + /// This gets set to None when execution errors, or we want to bust the cache on purpose to + /// force a re-execution. + /// We do not need to manually bust the cache for changed units, that's handled by the cache + /// information. + pub last_successful_ast_state: Arc>>, /// Memory maps. pub memory_map: DashMap, /// Current code. @@ -189,6 +198,7 @@ impl Backend { diagnostics_map: Default::default(), symbols_map: Default::default(), semantic_tokens_map: Default::default(), + last_successful_ast_state: Default::default(), is_initialized: Default::default(), }) } @@ -262,6 +272,13 @@ impl crate::lsp::backend::Backend for Backend { } async fn inner_on_change(&self, params: TextDocumentItem, force: bool) { + if force { + // Bust the execution cache. + let mut old_ast_state = self.last_successful_ast_state.write().await; + *old_ast_state = None; + drop(old_ast_state); + } + let filename = params.uri.to_string(); // We already updated the code map in the shared backend. @@ -674,23 +691,43 @@ impl Backend { return Ok(()); } - let mut exec_state = ExecState::default(); + let mut last_successful_ast_state = self.last_successful_ast_state.write().await; - // Clear the scene, before we execute so it's not fugly as shit. - executor_ctx - .engine - .clear_scene(&mut exec_state.id_generator, SourceRange::default()) - .await?; + let mut exec_state = if let Some(last_successful_ast_state) = last_successful_ast_state.clone() { + last_successful_ast_state.exec_state + } else { + Default::default() + }; - if let Err(err) = executor_ctx.run(ast, &mut exec_state).await { + if let Err(err) = executor_ctx + .run( + CacheInformation { + old: last_successful_ast_state.clone(), + new_ast: ast.ast.clone(), + }, + &mut exec_state, + ) + .await + { self.memory_map.remove(params.uri.as_str()); self.add_to_diagnostics(params, &[err], false).await; + // Update the last successful ast state to be None. + *last_successful_ast_state = None; + // Since we already published the diagnostics we don't really care about the error // string. return Err(anyhow::anyhow!("failed to execute code")); } + // Update the last successful ast state. + *last_successful_ast_state = Some(OldAstState { + ast: ast.ast.clone(), + exec_state: exec_state.clone(), + settings: executor_ctx.settings.clone(), + }); + drop(last_successful_ast_state); + self.memory_map .insert(params.uri.to_string(), exec_state.memory.clone()); diff --git a/src/wasm-lib/kcl/src/lsp/test_util.rs b/src/wasm-lib/kcl/src/lsp/test_util.rs index a97cdce20..3480ce8eb 100644 --- a/src/wasm-lib/kcl/src/lsp/test_util.rs +++ b/src/wasm-lib/kcl/src/lsp/test_util.rs @@ -37,6 +37,7 @@ pub async fn kcl_lsp_server(execute: bool) -> Result { can_send_telemetry: true, executor_ctx: Arc::new(tokio::sync::RwLock::new(executor_ctx)), can_execute: Arc::new(tokio::sync::RwLock::new(can_execute)), + last_successful_ast_state: Default::default(), is_initialized: Default::default(), }) .custom_method("kcl/updateUnits", crate::lsp::kcl::Backend::update_units) diff --git a/src/wasm-lib/kcl/src/parsing/ast/cache.rs b/src/wasm-lib/kcl/src/parsing/ast/cache.rs new file mode 100644 index 000000000..628f5b0c4 --- /dev/null +++ b/src/wasm-lib/kcl/src/parsing/ast/cache.rs @@ -0,0 +1,373 @@ +//! Functions for helping with caching an ast and finding the parts the changed. + +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use crate::{ + executor::ExecState, + parsing::ast::types::{Node, Program}, +}; + +/// Information for the caching an AST and smartly re-executing it if we can. +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)] +#[ts(export)] +pub struct CacheInformation { + /// The old information. + pub old: Option, + /// The new ast to executed. + pub new_ast: Node, +} + +/// The old ast and program memory. +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)] +#[ts(export)] +pub struct OldAstState { + /// The ast. + pub ast: Node, + /// The exec state. + pub exec_state: ExecState, + /// The last settings used for execution. + pub settings: crate::executor::ExecutorSettings, +} + +impl From for CacheInformation { + fn from(program: crate::Program) -> Self { + CacheInformation { + old: None, + new_ast: program.ast, + } + } +} + +/// The result of a cache check. +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)] +#[ts(export)] +pub struct CacheResult { + /// Should we clear the scene and start over? + pub clear_scene: bool, + /// The program that needs to be executed. + pub program: Node, +} + +// Given an old ast, old program memory and new ast, find the parts of the code that need to be +// re-executed. +// This function should never error, because in the case of any internal error, we should just pop +// the cache. +pub fn get_changed_program( + info: CacheInformation, + new_settings: &crate::executor::ExecutorSettings, +) -> Option { + let Some(old) = info.old else { + // We have no old info, we need to re-execute the whole thing. + return Some(CacheResult { + clear_scene: true, + program: info.new_ast, + }); + }; + + // If the settings are different we need to bust the cache. + // We specifically do this before checking if they are the exact same. + if old.settings != *new_settings { + return Some(CacheResult { + clear_scene: true, + program: info.new_ast, + }); + } + + // If the ASTs are the EXACT same we return None. + // We don't even need to waste time computing the digests. + if old.ast == info.new_ast { + return None; + } + + let mut old_ast = old.ast.inner; + old_ast.compute_digest(); + let mut new_ast = info.new_ast.inner.clone(); + new_ast.compute_digest(); + + // Check if the digest is the same. + if old_ast.digest == new_ast.digest { + return None; + } + + // Check if the changes were only to Non-code areas, like comments or whitespace. + + // For any unhandled cases just re-execute the whole thing. + Some(CacheResult { + clear_scene: true, + program: info.new_ast, + }) +} + +#[cfg(test)] +mod tests { + use std::sync::Arc; + + use anyhow::Result; + use pretty_assertions::assert_eq; + + use super::*; + + async fn execute(program: &crate::Program) -> Result { + let ctx = crate::executor::ExecutorContext { + engine: Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().await?)), + fs: Arc::new(crate::fs::FileManager::new()), + stdlib: Arc::new(crate::std::StdLib::new()), + settings: Default::default(), + context_type: crate::executor::ContextType::Mock, + }; + let mut exec_state = crate::executor::ExecState::default(); + ctx.run(program.clone().into(), &mut exec_state).await?; + + Ok(exec_state) + } + + // Easy case where we have no old ast and memory. + // We need to re-execute everything. + #[test] + fn test_get_changed_program_no_old_information() { + let new = r#"// Remove the end face for the extrusion. +firstSketch = startSketchOn('XY') + |> startProfileAt([-12, 12], %) + |> line([24, 0], %) + |> line([0, -24], %) + |> line([-24, 0], %) + |> close(%) + |> extrude(6, %) + +// Remove the end face for the extrusion. +shell({ faces = ['end'], thickness = 0.25 }, firstSketch)"#; + let program = crate::Program::parse_no_errs(new).unwrap().ast; + + let result = get_changed_program( + CacheInformation { + old: None, + new_ast: program.clone(), + }, + &Default::default(), + ); + + assert!(result.is_some()); + + let result = result.unwrap(); + + assert_eq!(result.program, program); + assert!(result.clear_scene); + } + + #[tokio::test(flavor = "multi_thread")] + async fn test_get_changed_program_same_code() { + let new = r#"// Remove the end face for the extrusion. +firstSketch = startSketchOn('XY') + |> startProfileAt([-12, 12], %) + |> line([24, 0], %) + |> line([0, -24], %) + |> line([-24, 0], %) + |> close(%) + |> extrude(6, %) + +// Remove the end face for the extrusion. +shell({ faces = ['end'], thickness = 0.25 }, firstSketch)"#; + let program = crate::Program::parse_no_errs(new).unwrap(); + + let executed = execute(&program).await.unwrap(); + + let result = get_changed_program( + CacheInformation { + old: Some(OldAstState { + ast: program.ast.clone(), + exec_state: executed, + settings: Default::default(), + }), + new_ast: program.ast.clone(), + }, + &Default::default(), + ); + + assert_eq!(result, None); + } + + #[tokio::test(flavor = "multi_thread")] + async fn test_get_changed_program_same_code_changed_whitespace() { + let old = r#" // Remove the end face for the extrusion. +firstSketch = startSketchOn('XY') + |> startProfileAt([-12, 12], %) + |> line([24, 0], %) + |> line([0, -24], %) + |> line([-24, 0], %) + |> close(%) + |> extrude(6, %) + +// Remove the end face for the extrusion. +shell({ faces = ['end'], thickness = 0.25 }, firstSketch) "#; + + let new = r#"// Remove the end face for the extrusion. +firstSketch = startSketchOn('XY') + |> startProfileAt([-12, 12], %) + |> line([24, 0], %) + |> line([0, -24], %) + |> line([-24, 0], %) + |> close(%) + |> extrude(6, %) + +// Remove the end face for the extrusion. +shell({ faces = ['end'], thickness = 0.25 }, firstSketch)"#; + let program_old = crate::Program::parse_no_errs(old).unwrap(); + + let executed = execute(&program_old).await.unwrap(); + + let program_new = crate::Program::parse_no_errs(new).unwrap(); + + let result = get_changed_program( + CacheInformation { + old: Some(OldAstState { + ast: program_old.ast.clone(), + exec_state: executed, + settings: Default::default(), + }), + new_ast: program_new.ast.clone(), + }, + &Default::default(), + ); + + assert_eq!(result, None); + } + + #[tokio::test(flavor = "multi_thread")] + async fn test_get_changed_program_same_code_changed_code_comment_start_of_program() { + let old = r#" // Removed the end face for the extrusion. +firstSketch = startSketchOn('XY') + |> startProfileAt([-12, 12], %) + |> line([24, 0], %) + |> line([0, -24], %) + |> line([-24, 0], %) + |> close(%) + |> extrude(6, %) + +// Remove the end face for the extrusion. +shell({ faces = ['end'], thickness = 0.25 }, firstSketch) "#; + + let new = r#"// Remove the end face for the extrusion. +firstSketch = startSketchOn('XY') + |> startProfileAt([-12, 12], %) + |> line([24, 0], %) + |> line([0, -24], %) + |> line([-24, 0], %) + |> close(%) + |> extrude(6, %) + +// Remove the end face for the extrusion. +shell({ faces = ['end'], thickness = 0.25 }, firstSketch)"#; + let program_old = crate::Program::parse_no_errs(old).unwrap(); + + let executed = execute(&program_old).await.unwrap(); + + let program_new = crate::Program::parse_no_errs(new).unwrap(); + + let result = get_changed_program( + CacheInformation { + old: Some(OldAstState { + ast: program_old.ast.clone(), + exec_state: executed, + settings: Default::default(), + }), + new_ast: program_new.ast.clone(), + }, + &Default::default(), + ); + + assert_eq!(result, None); + } + + #[tokio::test(flavor = "multi_thread")] + async fn test_get_changed_program_same_code_changed_code_comments() { + let old = r#" // Removed the end face for the extrusion. +firstSketch = startSketchOn('XY') + |> startProfileAt([-12, 12], %) + |> line([24, 0], %) + |> line([0, -24], %) + |> line([-24, 0], %) // my thing + |> close(%) + |> extrude(6, %) + +// Remove the end face for the extrusion. +shell({ faces = ['end'], thickness = 0.25 }, firstSketch) "#; + + let new = r#"// Remove the end face for the extrusion. +firstSketch = startSketchOn('XY') + |> startProfileAt([-12, 12], %) + |> line([24, 0], %) + |> line([0, -24], %) + |> line([-24, 0], %) + |> close(%) + |> extrude(6, %) + +// Remove the end face for the extrusion. +shell({ faces = ['end'], thickness = 0.25 }, firstSketch)"#; + let program_old = crate::Program::parse_no_errs(old).unwrap(); + + let executed = execute(&program_old).await.unwrap(); + + let program_new = crate::Program::parse_no_errs(new).unwrap(); + + let result = get_changed_program( + CacheInformation { + old: Some(OldAstState { + ast: program_old.ast.clone(), + exec_state: executed, + settings: Default::default(), + }), + new_ast: program_new.ast.clone(), + }, + &Default::default(), + ); + + assert!(result.is_some()); + + let result = result.unwrap(); + + assert_eq!(result.program, program_new.ast); + assert!(result.clear_scene); + } + + // Changing the units with the exact same file should bust the cache. + #[tokio::test(flavor = "multi_thread")] + async fn test_get_changed_program_same_code_but_different_units() { + let new = r#"// Remove the end face for the extrusion. +firstSketch = startSketchOn('XY') + |> startProfileAt([-12, 12], %) + |> line([24, 0], %) + |> line([0, -24], %) + |> line([-24, 0], %) + |> close(%) + |> extrude(6, %) + +// Remove the end face for the extrusion. +shell({ faces = ['end'], thickness = 0.25 }, firstSketch)"#; + let program = crate::Program::parse_no_errs(new).unwrap(); + + let executed = execute(&program).await.unwrap(); + + let result = get_changed_program( + CacheInformation { + old: Some(OldAstState { + ast: program.ast.clone(), + exec_state: executed, + settings: Default::default(), + }), + new_ast: program.ast.clone(), + }, + &crate::ExecutorSettings { + units: crate::UnitLength::Cm, + ..Default::default() + }, + ); + + assert!(result.is_some()); + + let result = result.unwrap(); + + assert_eq!(result.program, program.ast); + assert!(result.clear_scene); + } +} diff --git a/src/wasm-lib/kcl/src/parsing/ast/mod.rs b/src/wasm-lib/kcl/src/parsing/ast/mod.rs index fa2e039ae..d4fd0bb35 100644 --- a/src/wasm-lib/kcl/src/parsing/ast/mod.rs +++ b/src/wasm-lib/kcl/src/parsing/ast/mod.rs @@ -1,3 +1,4 @@ +pub(crate) mod cache; pub(crate) mod digest; pub(crate) mod execute; pub mod modify; diff --git a/src/wasm-lib/kcl/src/parsing/ast/source_range.rs b/src/wasm-lib/kcl/src/parsing/ast/source_range.rs index f76976c75..436c5db85 100644 --- a/src/wasm-lib/kcl/src/parsing/ast/source_range.rs +++ b/src/wasm-lib/kcl/src/parsing/ast/source_range.rs @@ -1,5 +1,7 @@ -use crate::parsing::ast::types::{BinaryPart, BodyItem, Expr, LiteralIdentifier, MemberObject}; -use crate::source_range::ModuleId; +use crate::{ + parsing::ast::types::{BinaryPart, BodyItem, Expr, LiteralIdentifier, MemberObject}, + source_range::ModuleId, +}; impl BodyItem { pub fn module_id(&self) -> ModuleId { diff --git a/src/wasm-lib/kcl/src/parsing/math.rs b/src/wasm-lib/kcl/src/parsing/math.rs index 3f4e08be5..9cf718b86 100644 --- a/src/wasm-lib/kcl/src/parsing/math.rs +++ b/src/wasm-lib/kcl/src/parsing/math.rs @@ -1,13 +1,12 @@ // TODO optimise size of CompilationError #![allow(clippy::result_large_err)] +use super::CompilationError; use crate::{ parsing::ast::types::{BinaryExpression, BinaryOperator, BinaryPart, Node}, SourceRange, }; -use super::CompilationError; - /// Parses a list of tokens (in infix order, i.e. as the user typed them) /// into a binary expression tree. pub fn parse(infix_tokens: Vec) -> Result, CompilationError> { @@ -127,8 +126,7 @@ impl From for BinaryExpressionToken { #[cfg(test)] mod tests { use super::*; - use crate::parsing::ast::types::Literal; - use crate::source_range::ModuleId; + use crate::{parsing::ast::types::Literal, source_range::ModuleId}; #[test] fn parse_and_evaluate() { diff --git a/src/wasm-lib/src/wasm.rs b/src/wasm-lib/src/wasm.rs index 171b12309..3bceb3d83 100644 --- a/src/wasm-lib/src/wasm.rs +++ b/src/wasm-lib/src/wasm.rs @@ -4,27 +4,42 @@ use std::{str::FromStr, sync::Arc}; use futures::stream::TryStreamExt; use gloo_utils::format::JsValueSerdeExt; -use kcl_lib::{CoreDump, EngineManager, ExecState, ModuleId, Program}; +use kcl_lib::{CacheInformation, CoreDump, EngineManager, ExecState, ModuleId, OldAstState, Program}; +use tokio::sync::RwLock; use tower_lsp::{LspService, Server}; use wasm_bindgen::prelude::*; +lazy_static::lazy_static! { + /// A static mutable lock for updating the last successful execution state for the cache. + static ref OLD_AST_MEMORY: Arc>> = Default::default(); +} + +// Read the old ast memory from the lock, this should never fail since +// in failure scenarios we should just bust the cache and send back None as the previous +// state. +async fn read_old_ast_memory() -> Option { + let lock = OLD_AST_MEMORY.read().await; + lock.clone() +} + // wasm_bindgen wrapper for execute #[wasm_bindgen] pub async fn execute_wasm( program_str: &str, - memory_str: &str, - id_generator_str: &str, + program_memory_override_str: &str, units: &str, engine_manager: kcl_lib::wasm_engine::EngineCommandManager, fs_manager: kcl_lib::wasm_engine::FileSystemManager, - project_directory: Option, - is_mock: bool, ) -> Result { console_error_panic_hook::set_once(); let program: Program = serde_json::from_str(program_str).map_err(|e| e.to_string())?; - let memory: kcl_lib::exec::ProgramMemory = serde_json::from_str(memory_str).map_err(|e| e.to_string())?; - let id_generator: kcl_lib::exec::IdGenerator = serde_json::from_str(id_generator_str).map_err(|e| e.to_string())?; + let program_memory_override: Option = + serde_json::from_str(program_memory_override_str).map_err(|e| e.to_string())?; + + // If we have a program memory override, assume we are in mock mode. + // You cannot override the memory in non-mock mode. + let is_mock = program_memory_override.is_some(); let units = kcl_lib::UnitLength::from_str(units).map_err(|e| e.to_string())?; let ctx = if is_mock { @@ -33,14 +48,55 @@ pub async fn execute_wasm( kcl_lib::ExecutorContext::new(engine_manager, fs_manager, units).await? }; - let mut exec_state = ExecState { - memory, - id_generator, - project_directory, - ..ExecState::default() - }; + let mut exec_state = ExecState { ..ExecState::default() }; - ctx.run(&program, &mut exec_state).await.map_err(String::from)?; + let mut old_ast_memory = None; + + // Populate from the old exec state if it exists. + if let Some(program_memory_override) = program_memory_override { + exec_state.memory = program_memory_override; + } else { + // If we are in mock mode, we don't want to use any cache. + if let Some(old) = read_old_ast_memory().await { + exec_state = old.exec_state.clone(); + old_ast_memory = Some(old); + } + } + + if let Err(err) = ctx + .run( + CacheInformation { + old: old_ast_memory, + new_ast: program.ast.clone(), + }, + &mut exec_state, + ) + .await + .map_err(String::from) + { + if !is_mock { + // We don't use the cache in mock mode. + let mut current_cache = OLD_AST_MEMORY.write().await; + // Set the cache to None. + *current_cache = None; + } + + // Throw the error. + return Err(err); + } + + if !is_mock { + // We don't use the cache in mock mode. + let mut current_cache = OLD_AST_MEMORY.write().await; + + // If we aren't in mock mode, save this as the last successful execution to the cache. + *current_cache = Some(OldAstState { + ast: program.ast.clone(), + exec_state: exec_state.clone(), + settings: ctx.settings.clone(), + }); + drop(current_cache); + } // The serde-wasm-bindgen does not work here because of weird HashMap issues so we use the // gloo-serialize crate instead. diff --git a/src/wasm-lib/tests/modify/main.rs b/src/wasm-lib/tests/modify/main.rs index ea99388f2..677eb895d 100644 --- a/src/wasm-lib/tests/modify/main.rs +++ b/src/wasm-lib/tests/modify/main.rs @@ -11,7 +11,7 @@ async fn setup(code: &str, name: &str) -> Result<(ExecutorContext, Program, Modu let program = Program::parse_no_errs(code)?; let ctx = kcl_lib::ExecutorContext::new_with_default_client(Default::default()).await?; let mut exec_state = ExecState::default(); - ctx.run(&program, &mut exec_state).await?; + ctx.run(program.clone().into(), &mut exec_state).await?; // We need to get the sketch ID. // Get the sketch ID from memory.