start of cache: don't re-execute on whitespace / top level code comment changes (#4663)

* start

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

working for whitespace

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

pull thru

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

fix wasm

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

pull thru to js start

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

actually use the cache in ts

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

rust owns clearing the scene

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

fixes

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

empty

stupid log

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

updates

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

* updates

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

fix tests

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

updatez

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

updates

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

save the state

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

save the state

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

updates

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

use the old memory

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

cleanup to use the old exec state

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

fices

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

updates;

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

fixes

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

fmt

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

updates

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

fixes

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

updates

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

cleanup

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

fixes

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

* rebase and compile

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

* Look at this (photo)Graph *in the voice of Nickelback*

* fix the lsp to use the cache

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

* add comment

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

* use a global static instead;

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

* fix rust test

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

* cleanup more

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

* cleanups

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

* cleanup the api even more

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

* updates

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

* updates

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

* Look at this (photo)Graph *in the voice of Nickelback*

* bust the cache on unit changes

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

* updates

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

* updates

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

* updates

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

* Look at this (photo)Graph *in the voice of Nickelback*

* stupid codespell

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
This commit is contained in:
Jess Frazelle
2024-12-05 19:51:06 -08:00
committed by GitHub
parent 9e57034873
commit 441d957228
45 changed files with 741 additions and 189 deletions

View File

@ -943,6 +943,110 @@ sketch002 = startSketchOn(extrude001, 'END')
`.replace(/\s/g, '') `.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 ({ test('empty-scene default-planes act as expected', async ({
page, page,
browserName, browserName,

View File

@ -155,7 +155,6 @@ export class CameraControls {
this.camera.zoom = camProps.zoom || 1 this.camera.zoom = camProps.zoom || 1
} }
this.camera.updateProjectionMatrix() this.camera.updateProjectionMatrix()
console.log('doing this thing', camProps)
this.update(true) this.update(true)
} }

View File

@ -31,6 +31,7 @@ import {
recast, recast,
defaultSourceRange, defaultSourceRange,
resultIsOk, resultIsOk,
ProgramMemory,
} from 'lang/wasm' } from 'lang/wasm'
import { CustomIcon, CustomIconName } from 'components/CustomIcon' import { CustomIcon, CustomIconName } from 'components/CustomIcon'
import { ConstrainInfo } from 'lang/std/stdTypes' import { ConstrainInfo } from 'lang/std/stdTypes'
@ -420,9 +421,9 @@ export async function deleteSegment({
const testExecute = await executeAst({ const testExecute = await executeAst({
ast: modifiedAst, ast: modifiedAst,
idGenerator: kclManager.execState.idGenerator,
useFakeExecutor: true,
engineCommandManager: engineCommandManager, engineCommandManager: engineCommandManager,
// We make sure to send an empty program memory to denote we mean mock mode.
programMemoryOverride: ProgramMemory.empty(),
}) })
if (testExecute.errors.length) { if (testExecute.errors.length) {
toast.error('Segment tag used outside of current Sketch. Could not delete.') toast.error('Segment tag used outside of current Sketch. Could not delete.')

View File

@ -498,10 +498,9 @@ export class SceneEntities {
const { execState } = await executeAst({ const { execState } = await executeAst({
ast: truncatedAst, ast: truncatedAst,
useFakeExecutor: true,
engineCommandManager: this.engineCommandManager, engineCommandManager: this.engineCommandManager,
// We make sure to send an empty program memory to denote we mean mock mode.
programMemoryOverride, programMemoryOverride,
idGenerator: kclManager.execState.idGenerator,
}) })
const programMemory = execState.memory const programMemory = execState.memory
const sketch = sketchFromPathToNode({ const sketch = sketchFromPathToNode({
@ -955,10 +954,9 @@ export class SceneEntities {
const { execState } = await executeAst({ const { execState } = await executeAst({
ast: truncatedAst, ast: truncatedAst,
useFakeExecutor: true,
engineCommandManager: this.engineCommandManager, engineCommandManager: this.engineCommandManager,
// We make sure to send an empty program memory to denote we mean mock mode.
programMemoryOverride, programMemoryOverride,
idGenerator: kclManager.execState.idGenerator,
}) })
const programMemory = execState.memory const programMemory = execState.memory
this.sceneProgramMemory = programMemory this.sceneProgramMemory = programMemory
@ -1019,10 +1017,9 @@ export class SceneEntities {
const { execState } = await executeAst({ const { execState } = await executeAst({
ast: _ast, ast: _ast,
useFakeExecutor: true,
engineCommandManager: this.engineCommandManager, engineCommandManager: this.engineCommandManager,
// We make sure to send an empty program memory to denote we mean mock mode.
programMemoryOverride, programMemoryOverride,
idGenerator: kclManager.execState.idGenerator,
}) })
const programMemory = execState.memory const programMemory = execState.memory
@ -1120,10 +1117,9 @@ export class SceneEntities {
const { execState } = await executeAst({ const { execState } = await executeAst({
ast: truncatedAst, ast: truncatedAst,
useFakeExecutor: true,
engineCommandManager: this.engineCommandManager, engineCommandManager: this.engineCommandManager,
// We make sure to send an empty program memory to denote we mean mock mode.
programMemoryOverride, programMemoryOverride,
idGenerator: kclManager.execState.idGenerator,
}) })
const programMemory = execState.memory const programMemory = execState.memory
this.sceneProgramMemory = programMemory this.sceneProgramMemory = programMemory
@ -1187,10 +1183,9 @@ export class SceneEntities {
const { execState } = await executeAst({ const { execState } = await executeAst({
ast: _ast, ast: _ast,
useFakeExecutor: true,
engineCommandManager: this.engineCommandManager, engineCommandManager: this.engineCommandManager,
// We make sure to send an empty program memory to denote we mean mock mode.
programMemoryOverride, programMemoryOverride,
idGenerator: kclManager.execState.idGenerator,
}) })
const programMemory = execState.memory const programMemory = execState.memory
@ -1306,10 +1301,9 @@ export class SceneEntities {
const { execState } = await executeAst({ const { execState } = await executeAst({
ast: modded, ast: modded,
useFakeExecutor: true,
engineCommandManager: this.engineCommandManager, engineCommandManager: this.engineCommandManager,
// We make sure to send an empty program memory to denote we mean mock mode.
programMemoryOverride, programMemoryOverride,
idGenerator: kclManager.execState.idGenerator,
}) })
const programMemory = execState.memory const programMemory = execState.memory
this.sceneProgramMemory = programMemory this.sceneProgramMemory = programMemory
@ -1691,10 +1685,9 @@ export class SceneEntities {
codeManager.updateCodeEditor(code) codeManager.updateCodeEditor(code)
const { execState } = await executeAst({ const { execState } = await executeAst({
ast: truncatedAst, ast: truncatedAst,
useFakeExecutor: true,
engineCommandManager: this.engineCommandManager, engineCommandManager: this.engineCommandManager,
// We make sure to send an empty program memory to denote we mean mock mode.
programMemoryOverride, programMemoryOverride,
idGenerator: kclManager.execState.idGenerator,
}) })
const programMemory = execState.memory const programMemory = execState.memory
this.sceneProgramMemory = programMemory this.sceneProgramMemory = programMemory

View File

@ -163,9 +163,8 @@ export function useCalc({
executeAst({ executeAst({
ast, ast,
engineCommandManager, engineCommandManager,
useFakeExecutor: true, // We make sure to send an empty program memory to denote we mean mock mode.
programMemoryOverride: kclManager.programMemory.clone(), programMemoryOverride: kclManager.programMemory.clone(),
idGenerator: kclManager.execState.idGenerator,
}).then(({ execState }) => { }).then(({ execState }) => {
const resultDeclaration = ast.body.find( const resultDeclaration = ast.body.find(
(a) => (a) =>

View File

@ -34,6 +34,10 @@ describe('processMemory', () => {
expect(output.myVar).toEqual(5) expect(output.myVar).toEqual(5)
expect(output.otherVar).toEqual(3) expect(output.otherVar).toEqual(3)
expect(output).toEqual({ expect(output).toEqual({
HALF_TURN: 180,
QUARTER_TURN: 90,
THREE_QUARTER_TURN: 270,
ZERO: 0,
myVar: 5, myVar: 5,
myFn: '__function(a)__', myFn: '__function(a)__',
otherVar: 3, otherVar: 3,

View File

@ -68,8 +68,8 @@ function AppLogoLink({
data-testid="app-logo" data-testid="app-logo"
onClick={() => { onClick={() => {
onProjectClose(file || null, project?.path || null, false) onProjectClose(file || null, project?.path || null, false)
// Clear the scene and end the session. // Clear the scene.
engineCommandManager.endSession() engineCommandManager.clearScene()
}} }}
to={PATHS.HOME} to={PATHS.HOME}
className={wrapperClassName + ' hover:before:brightness-110'} className={wrapperClassName + ' hover:before:brightness-110'}
@ -190,8 +190,8 @@ function ProjectMenuPopover({
className: !isDesktop() ? 'hidden' : '', className: !isDesktop() ? 'hidden' : '',
onClick: () => { onClick: () => {
onProjectClose(file || null, project?.path || null, true) onProjectClose(file || null, project?.path || null, true)
// Clear the scene and end the session. // Clear the scene.
engineCommandManager.endSession() engineCommandManager.clearScene()
}, },
}, },
].filter( ].filter(

View File

@ -88,7 +88,7 @@ export class KclManager {
this._programMemoryCallBack(programMemory) this._programMemoryCallBack(programMemory)
} }
set execState(execState) { private set execState(execState) {
this._execState = execState this._execState = execState
this.programMemory = execState.memory this.programMemory = execState.memory
} }
@ -227,12 +227,6 @@ export class KclManager {
this.addDiagnostics(complilationErrorsToDiagnostics(result.warnings)) this.addDiagnostics(complilationErrorsToDiagnostics(result.warnings))
if (result.errors.length > 0) { if (result.errors.length > 0) {
this._hasErrors = true 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 return null
} }
@ -276,12 +270,9 @@ export class KclManager {
this._cancelTokens.set(currentExecutionId, false) this._cancelTokens.set(currentExecutionId, false)
this.isExecuting = true this.isExecuting = true
// Make sure we clear before starting again. End session will do this.
this.engineCommandManager?.endSession()
await this.ensureWasmInit() await this.ensureWasmInit()
const { logs, errors, execState, isInterrupted } = await executeAst({ const { logs, errors, execState, isInterrupted } = await executeAst({
ast, ast,
idGenerator: this.execState.idGenerator,
engineCommandManager: this.engineCommandManager, engineCommandManager: this.engineCommandManager,
}) })
@ -331,8 +322,6 @@ export class KclManager {
this.logs = logs this.logs = logs
// Do not add the errors since the program was interrupted and the error is not a real KCL error // Do not add the errors since the program was interrupted and the error is not a real KCL error
this.addDiagnostics(isInterrupted ? [] : kclErrorsToDiagnostics(errors)) 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 this.execState = execState
if (!errors.length) { if (!errors.length) {
this.lastSuccessfulProgramMemory = execState.memory this.lastSuccessfulProgramMemory = execState.memory
@ -373,9 +362,9 @@ export class KclManager {
const { logs, errors, execState } = await executeAst({ const { logs, errors, execState } = await executeAst({
ast: newAst, ast: newAst,
idGenerator: this.execState.idGenerator,
engineCommandManager: this.engineCommandManager, 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 this._logs = logs

View File

@ -2,7 +2,6 @@ import {
Program, Program,
_executor, _executor,
ProgramMemory, ProgramMemory,
programMemoryInit,
kclLint, kclLint,
emptyExecState, emptyExecState,
ExecState, ExecState,
@ -11,7 +10,6 @@ import { enginelessExecutor } from 'lib/testHelpers'
import { EngineCommandManager } from 'lang/std/engineConnection' import { EngineCommandManager } from 'lang/std/engineConnection'
import { KCLError } from 'lang/errors' import { KCLError } from 'lang/errors'
import { Diagnostic } from '@codemirror/lint' import { Diagnostic } from '@codemirror/lint'
import { IdGenerator } from 'wasm-lib/kcl/bindings/IdGenerator'
import { Node } from 'wasm-lib/kcl/bindings/Node' import { Node } from 'wasm-lib/kcl/bindings/Node'
export type ToolTip = export type ToolTip =
@ -49,15 +47,13 @@ export const toolTips: Array<ToolTip> = [
export async function executeAst({ export async function executeAst({
ast, ast,
engineCommandManager, engineCommandManager,
useFakeExecutor = false, // If you set programMemoryOverride we assume you mean mock mode. Since that
// is the only way to go about it.
programMemoryOverride, programMemoryOverride,
idGenerator,
}: { }: {
ast: Node<Program> ast: Node<Program>
engineCommandManager: EngineCommandManager engineCommandManager: EngineCommandManager
useFakeExecutor?: boolean
programMemoryOverride?: ProgramMemory programMemoryOverride?: ProgramMemory
idGenerator?: IdGenerator
isInterrupted?: boolean isInterrupted?: boolean
}): Promise<{ }): Promise<{
logs: string[] logs: string[]
@ -66,22 +62,14 @@ export async function executeAst({
isInterrupted: boolean isInterrupted: boolean
}> { }> {
try { try {
if (!useFakeExecutor) { const execState = await (programMemoryOverride
engineCommandManager.endSession() ? enginelessExecutor(ast, programMemoryOverride)
// eslint-disable-next-line @typescript-eslint/no-floating-promises : _executor(ast, engineCommandManager))
engineCommandManager.startNewSession()
} await engineCommandManager.waitForAllCommands(
const execState = await (useFakeExecutor programMemoryOverride !== undefined
? enginelessExecutor(ast, programMemoryOverride || programMemoryInit()) )
: _executor(
ast,
programMemoryInit(),
idGenerator,
engineCommandManager,
false
))
await engineCommandManager.waitForAllCommands(useFakeExecutor)
return { return {
logs: [], logs: [],
errors: [], errors: [],

View File

@ -1879,7 +1879,7 @@ export class EngineCommandManager extends EventTarget {
} }
return JSON.stringify(this.defaultPlanes) return JSON.stringify(this.defaultPlanes)
} }
endSession() { clearScene(): void {
const deleteCmd: EngineCommand = { const deleteCmd: EngineCommand = {
type: 'modeling_cmd_req', type: 'modeling_cmd_req',
cmd_id: uuidv4(), cmd_id: uuidv4(),

View File

@ -35,7 +35,6 @@ import { Configuration } from 'wasm-lib/kcl/bindings/Configuration'
import { DeepPartial } from 'lib/types' import { DeepPartial } from 'lib/types'
import { ProjectConfiguration } from 'wasm-lib/kcl/bindings/ProjectConfiguration' import { ProjectConfiguration } from 'wasm-lib/kcl/bindings/ProjectConfiguration'
import { Sketch } from '../wasm-lib/kcl/bindings/Sketch' 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 { ExecState as RawExecState } from '../wasm-lib/kcl/bindings/ExecState'
import { ProgramMemory as RawProgramMemory } from '../wasm-lib/kcl/bindings/ProgramMemory' import { ProgramMemory as RawProgramMemory } from '../wasm-lib/kcl/bindings/ProgramMemory'
import { EnvironmentRef } from '../wasm-lib/kcl/bindings/EnvironmentRef' import { EnvironmentRef } from '../wasm-lib/kcl/bindings/EnvironmentRef'
@ -216,7 +215,6 @@ export const isPathToNodeNumber = (
export interface ExecState { export interface ExecState {
memory: ProgramMemory memory: ProgramMemory
idGenerator: IdGenerator
} }
/** /**
@ -226,21 +224,12 @@ export interface ExecState {
export function emptyExecState(): ExecState { export function emptyExecState(): ExecState {
return { return {
memory: ProgramMemory.empty(), memory: ProgramMemory.empty(),
idGenerator: defaultIdGenerator(),
} }
} }
function execStateFromRaw(raw: RawExecState): ExecState { function execStateFromRaw(raw: RawExecState): ExecState {
return { return {
memory: ProgramMemory.fromRaw(raw.memory), 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 } 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 * 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 * isolated from the rest of the TypeScript code so that we can move it to Rust
@ -276,7 +278,7 @@ export class ProgramMemory {
} }
constructor( constructor(
environments: Environment[] = [emptyEnvironment()], environments: Environment[] = [emptyRootEnvironment()],
currentEnv: EnvironmentRef = ROOT_ENVIRONMENT_REF, currentEnv: EnvironmentRef = ROOT_ENVIRONMENT_REF,
returnVal: KclValue | null = null returnVal: KclValue | null = null
) { ) {
@ -463,36 +465,31 @@ export function sketchFromKclValue(
export const executor = async ( export const executor = async (
node: Node<Program>, node: Node<Program>,
programMemory: ProgramMemory | Error = ProgramMemory.empty(),
idGenerator: IdGenerator = defaultIdGenerator(),
engineCommandManager: EngineCommandManager, engineCommandManager: EngineCommandManager,
isMock: boolean = false programMemoryOverride: ProgramMemory | Error | null = null
): Promise<ExecState> => { ): Promise<ExecState> => {
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 // eslint-disable-next-line @typescript-eslint/no-floating-promises
engineCommandManager.startNewSession() engineCommandManager.startNewSession()
const _programMemory = await _executor( const _programMemory = await _executor(
node, node,
programMemory,
idGenerator,
engineCommandManager, engineCommandManager,
isMock programMemoryOverride
) )
await engineCommandManager.waitForAllCommands() await engineCommandManager.waitForAllCommands()
engineCommandManager.endSession()
return _programMemory return _programMemory
} }
export const _executor = async ( export const _executor = async (
node: Node<Program>, node: Node<Program>,
programMemory: ProgramMemory | Error = ProgramMemory.empty(),
idGenerator: IdGenerator = defaultIdGenerator(),
engineCommandManager: EngineCommandManager, engineCommandManager: EngineCommandManager,
isMock: boolean programMemoryOverride: ProgramMemory | Error | null = null
): Promise<ExecState> => { ): Promise<ExecState> => {
if (err(programMemory)) return Promise.reject(programMemory) if (programMemoryOverride !== null && err(programMemoryOverride))
return Promise.reject(programMemoryOverride)
try { try {
let baseUnit = 'mm' let baseUnit = 'mm'
@ -505,13 +502,10 @@ export const _executor = async (
} }
const execState: RawExecState = await execute_wasm( const execState: RawExecState = await execute_wasm(
JSON.stringify(node), JSON.stringify(node),
JSON.stringify(programMemory.toRaw()), JSON.stringify(programMemoryOverride?.toRaw() || null),
JSON.stringify(idGenerator),
baseUnit, baseUnit,
engineCommandManager, engineCommandManager,
fileSystemManager, fileSystemManager
undefined,
isMock
) )
return execStateFromRaw(execState) return execStateFromRaw(execState)
} catch (e: any) { } catch (e: any) {

View File

@ -116,8 +116,8 @@ export async function createAndOpenNewTutorialProject({
) => void ) => void
navigate: (path: string) => void navigate: (path: string) => void
}) { }) {
// Clear the scene and end the session. // Clear the scene.
engineCommandManager.endSession() engineCommandManager.clearScene()
// Create a new project with the onboarding project name // Create a new project with the onboarding project name
const configuration = await readAppSettingsFile() const configuration = await readAppSettingsFile()

View File

@ -4,7 +4,6 @@ import {
_executor, _executor,
SourceRange, SourceRange,
ExecState, ExecState,
defaultIdGenerator,
} from '../lang/wasm' } from '../lang/wasm'
import { import {
EngineCommandManager, EngineCommandManager,
@ -16,7 +15,6 @@ import { v4 as uuidv4 } from 'uuid'
import { DefaultPlanes } from 'wasm-lib/kcl/bindings/DefaultPlanes' import { DefaultPlanes } from 'wasm-lib/kcl/bindings/DefaultPlanes'
import { err, reportRejection } from 'lib/trap' import { err, reportRejection } from 'lib/trap'
import { toSync } from './utils' import { toSync } from './utils'
import { IdGenerator } from 'wasm-lib/kcl/bindings/IdGenerator'
import { Node } from 'wasm-lib/kcl/bindings/Node' import { Node } from 'wasm-lib/kcl/bindings/Node'
type WebSocketResponse = Models['WebSocketResponse_type'] type WebSocketResponse = Models['WebSocketResponse_type']
@ -86,10 +84,9 @@ class MockEngineCommandManager {
export async function enginelessExecutor( export async function enginelessExecutor(
ast: Node<Program>, ast: Node<Program>,
pm: ProgramMemory | Error = ProgramMemory.empty(), pmo: ProgramMemory | Error = ProgramMemory.empty()
idGenerator: IdGenerator = defaultIdGenerator()
): Promise<ExecState> { ): Promise<ExecState> {
if (err(pm)) return Promise.reject(pm) if (pmo !== null && err(pmo)) return Promise.reject(pmo)
const mockEngineCommandManager = new MockEngineCommandManager({ const mockEngineCommandManager = new MockEngineCommandManager({
setIsStreamReady: () => {}, setIsStreamReady: () => {},
@ -97,21 +94,14 @@ export async function enginelessExecutor(
}) as any as EngineCommandManager }) as any as EngineCommandManager
// eslint-disable-next-line @typescript-eslint/no-floating-promises // eslint-disable-next-line @typescript-eslint/no-floating-promises
mockEngineCommandManager.startNewSession() mockEngineCommandManager.startNewSession()
const execState = await _executor( const execState = await _executor(ast, mockEngineCommandManager, pmo)
ast,
pm,
idGenerator,
mockEngineCommandManager,
true
)
await mockEngineCommandManager.waitForAllCommands() await mockEngineCommandManager.waitForAllCommands()
return execState return execState
} }
export async function executor( export async function executor(
ast: Node<Program>, ast: Node<Program>,
pm: ProgramMemory = ProgramMemory.empty(), pmo: ProgramMemory = ProgramMemory.empty()
idGenerator: IdGenerator = defaultIdGenerator()
): Promise<ExecState> { ): Promise<ExecState> {
const engineCommandManager = new EngineCommandManager() const engineCommandManager = new EngineCommandManager()
engineCommandManager.start({ engineCommandManager.start({
@ -133,13 +123,7 @@ export async function executor(
toSync(async () => { toSync(async () => {
// eslint-disable-next-line @typescript-eslint/no-floating-promises // eslint-disable-next-line @typescript-eslint/no-floating-promises
engineCommandManager.startNewSession() engineCommandManager.startNewSession()
const execState = await _executor( const execState = await _executor(ast, engineCommandManager, pmo)
ast,
pm,
idGenerator,
engineCommandManager,
false
)
await engineCommandManager.waitForAllCommands() await engineCommandManager.waitForAllCommands()
resolve(execState) resolve(execState)
}, reportRejection) }, reportRejection)

View File

@ -103,9 +103,8 @@ export function useCalculateKclExpression({
const { execState } = await executeAst({ const { execState } = await executeAst({
ast, ast,
engineCommandManager, engineCommandManager,
useFakeExecutor: true, // We make sure to send an empty program memory to denote we mean mock mode.
programMemoryOverride: kclManager.programMemory.clone(), programMemoryOverride: kclManager.programMemory.clone(),
idGenerator: kclManager.execState.idGenerator,
}) })
const resultDeclaration = ast.body.find( const resultDeclaration = ast.body.find(
(a) => (a) =>

View File

@ -1,5 +1,6 @@
import { import {
PathToNode, PathToNode,
ProgramMemory,
VariableDeclaration, VariableDeclaration,
VariableDeclarator, VariableDeclarator,
parse, parse,
@ -730,9 +731,9 @@ export const modelingMachine = setup({
const testExecute = await executeAst({ const testExecute = await executeAst({
ast: modifiedAst, 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) { if (testExecute.errors.length) {
toast.error('Unable to delete part') toast.error('Unable to delete part')

View File

@ -4308,6 +4308,7 @@ dependencies = [
"kcl-lib", "kcl-lib",
"kittycad", "kittycad",
"kittycad-modeling-cmds", "kittycad-modeling-cmds",
"lazy_static",
"pretty_assertions", "pretty_assertions",
"reqwest", "reqwest",
"serde_json", "serde_json",

View File

@ -15,6 +15,7 @@ data-encoding = "2.6.0"
gloo-utils = "0.2.0" gloo-utils = "0.2.0"
kcl-lib = { path = "kcl" } kcl-lib = { path = "kcl" }
kittycad.workspace = true kittycad.workspace = true
lazy_static = "1.5.0"
serde_json = "1.0.128" serde_json = "1.0.128"
tokio = { version = "1.41.1", features = ["sync"] } tokio = { version = "1.41.1", features = ["sync"] }
toml = "0.8.19" toml = "0.8.19"

View File

@ -778,7 +778,7 @@ fn generate_code_block_test(fn_name: &str, code_block: &str, index: usize) -> pr
context_type: crate::executor::ContextType::Mock, 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)] #[tokio::test(flavor = "multi_thread", worker_threads = 5)]

View File

@ -14,7 +14,7 @@ mod test_examples_someFn {
settings: Default::default(), settings: Default::default(),
context_type: crate::executor::ContextType::Mock, context_type: crate::executor::ContextType::Mock,
}; };
ctx.run(&program, &mut crate::ExecState::default()) ctx.run(program.into(), &mut crate::ExecState::default())
.await .await
.unwrap(); .unwrap();
} }

View File

@ -14,7 +14,7 @@ mod test_examples_someFn {
settings: Default::default(), settings: Default::default(),
context_type: crate::executor::ContextType::Mock, context_type: crate::executor::ContextType::Mock,
}; };
ctx.run(&program, &mut crate::ExecState::default()) ctx.run(program.into(), &mut crate::ExecState::default())
.await .await
.unwrap(); .unwrap();
} }

View File

@ -15,7 +15,7 @@ mod test_examples_show {
settings: Default::default(), settings: Default::default(),
context_type: crate::executor::ContextType::Mock, context_type: crate::executor::ContextType::Mock,
}; };
ctx.run(&program, &mut crate::ExecState::default()) ctx.run(program.into(), &mut crate::ExecState::default())
.await .await
.unwrap(); .unwrap();
} }
@ -49,7 +49,7 @@ mod test_examples_show {
settings: Default::default(), settings: Default::default(),
context_type: crate::executor::ContextType::Mock, context_type: crate::executor::ContextType::Mock,
}; };
ctx.run(&program, &mut crate::ExecState::default()) ctx.run(program.into(), &mut crate::ExecState::default())
.await .await
.unwrap(); .unwrap();
} }

View File

@ -15,7 +15,7 @@ mod test_examples_show {
settings: Default::default(), settings: Default::default(),
context_type: crate::executor::ContextType::Mock, context_type: crate::executor::ContextType::Mock,
}; };
ctx.run(&program, &mut crate::ExecState::default()) ctx.run(program.into(), &mut crate::ExecState::default())
.await .await
.unwrap(); .unwrap();
} }

View File

@ -16,7 +16,7 @@ mod test_examples_my_func {
settings: Default::default(), settings: Default::default(),
context_type: crate::executor::ContextType::Mock, context_type: crate::executor::ContextType::Mock,
}; };
ctx.run(&program, &mut crate::ExecState::default()) ctx.run(program.into(), &mut crate::ExecState::default())
.await .await
.unwrap(); .unwrap();
} }
@ -50,7 +50,7 @@ mod test_examples_my_func {
settings: Default::default(), settings: Default::default(),
context_type: crate::executor::ContextType::Mock, context_type: crate::executor::ContextType::Mock,
}; };
ctx.run(&program, &mut crate::ExecState::default()) ctx.run(program.into(), &mut crate::ExecState::default())
.await .await
.unwrap(); .unwrap();
} }

View File

@ -16,7 +16,7 @@ mod test_examples_line_to {
settings: Default::default(), settings: Default::default(),
context_type: crate::executor::ContextType::Mock, context_type: crate::executor::ContextType::Mock,
}; };
ctx.run(&program, &mut crate::ExecState::default()) ctx.run(program.into(), &mut crate::ExecState::default())
.await .await
.unwrap(); .unwrap();
} }
@ -50,7 +50,7 @@ mod test_examples_line_to {
settings: Default::default(), settings: Default::default(),
context_type: crate::executor::ContextType::Mock, context_type: crate::executor::ContextType::Mock,
}; };
ctx.run(&program, &mut crate::ExecState::default()) ctx.run(program.into(), &mut crate::ExecState::default())
.await .await
.unwrap(); .unwrap();
} }

View File

@ -15,7 +15,7 @@ mod test_examples_min {
settings: Default::default(), settings: Default::default(),
context_type: crate::executor::ContextType::Mock, context_type: crate::executor::ContextType::Mock,
}; };
ctx.run(&program, &mut crate::ExecState::default()) ctx.run(program.into(), &mut crate::ExecState::default())
.await .await
.unwrap(); .unwrap();
} }
@ -49,7 +49,7 @@ mod test_examples_min {
settings: Default::default(), settings: Default::default(),
context_type: crate::executor::ContextType::Mock, context_type: crate::executor::ContextType::Mock,
}; };
ctx.run(&program, &mut crate::ExecState::default()) ctx.run(program.into(), &mut crate::ExecState::default())
.await .await
.unwrap(); .unwrap();
} }

View File

@ -15,7 +15,7 @@ mod test_examples_show {
settings: Default::default(), settings: Default::default(),
context_type: crate::executor::ContextType::Mock, context_type: crate::executor::ContextType::Mock,
}; };
ctx.run(&program, &mut crate::ExecState::default()) ctx.run(program.into(), &mut crate::ExecState::default())
.await .await
.unwrap(); .unwrap();
} }

View File

@ -15,7 +15,7 @@ mod test_examples_import {
settings: Default::default(), settings: Default::default(),
context_type: crate::executor::ContextType::Mock, context_type: crate::executor::ContextType::Mock,
}; };
ctx.run(&program, &mut crate::ExecState::default()) ctx.run(program.into(), &mut crate::ExecState::default())
.await .await
.unwrap(); .unwrap();
} }

View File

@ -15,7 +15,7 @@ mod test_examples_import {
settings: Default::default(), settings: Default::default(),
context_type: crate::executor::ContextType::Mock, context_type: crate::executor::ContextType::Mock,
}; };
ctx.run(&program, &mut crate::ExecState::default()) ctx.run(program.into(), &mut crate::ExecState::default())
.await .await
.unwrap(); .unwrap();
} }

View File

@ -15,7 +15,7 @@ mod test_examples_import {
settings: Default::default(), settings: Default::default(),
context_type: crate::executor::ContextType::Mock, context_type: crate::executor::ContextType::Mock,
}; };
ctx.run(&program, &mut crate::ExecState::default()) ctx.run(program.into(), &mut crate::ExecState::default())
.await .await
.unwrap(); .unwrap();
} }

View File

@ -15,7 +15,7 @@ mod test_examples_show {
settings: Default::default(), settings: Default::default(),
context_type: crate::executor::ContextType::Mock, context_type: crate::executor::ContextType::Mock,
}; };
ctx.run(&program, &mut crate::ExecState::default()) ctx.run(program.into(), &mut crate::ExecState::default())
.await .await
.unwrap(); .unwrap();
} }

View File

@ -14,7 +14,7 @@ mod test_examples_some_function {
settings: Default::default(), settings: Default::default(),
context_type: crate::executor::ContextType::Mock, context_type: crate::executor::ContextType::Mock,
}; };
ctx.run(&program, &mut crate::ExecState::default()) ctx.run(program.into(), &mut crate::ExecState::default())
.await .await
.unwrap(); .unwrap();
} }

View File

@ -1,3 +1,8 @@
use std::{
collections::HashMap,
sync::{Arc, Mutex},
};
use anyhow::Result; use anyhow::Result;
use indexmap::IndexMap; use indexmap::IndexMap;
use kcl_lib::{ use kcl_lib::{
@ -11,10 +16,6 @@ use kittycad_modeling_cmds::{
shared::PathSegment::{self, *}, shared::PathSegment::{self, *},
websocket::{ModelingBatch, ModelingCmdReq, OkWebSocketResponseData, WebSocketRequest, WebSocketResponse}, websocket::{ModelingBatch, ModelingCmdReq, OkWebSocketResponseData, WebSocketRequest, WebSocketResponse},
}; };
use std::{
collections::HashMap,
sync::{Arc, Mutex},
};
use tokio::sync::RwLock; use tokio::sync::RwLock;
const CPP_PREFIX: &str = "const double scaleFactor = 100;\n"; const CPP_PREFIX: &str = "const double scaleFactor = 100;\n";

View File

@ -1,6 +1,7 @@
use std::sync::{Arc, Mutex};
use anyhow::Result; use anyhow::Result;
use kcl_lib::{ExecState, ExecutorContext}; use kcl_lib::{ExecState, ExecutorContext};
use std::sync::{Arc, Mutex};
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
mod conn_mock_core; mod conn_mock_core;
@ -15,7 +16,7 @@ pub async fn kcl_to_engine_core(code: &str) -> Result<String> {
let ctx = ExecutorContext::new_forwarded_mock(Arc::new(Box::new( let ctx = ExecutorContext::new_forwarded_mock(Arc::new(Box::new(
crate::conn_mock_core::EngineConnection::new(ref_result).await?, 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(); let result = result.lock().expect("mutex lock").clone();
Ok(result) Ok(result)

View File

@ -1,6 +1,7 @@
use kcl_to_core::*;
use std::{env, fs}; use std::{env, fs};
use kcl_to_core::*;
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
let args: Vec<String> = env::args().collect(); let args: Vec<String> = env::args().collect();

View File

@ -0,0 +1,3 @@
pub mod cache;
pub mod modify;
pub mod types;

View File

@ -25,7 +25,10 @@ use crate::{
engine::{EngineManager, ExecutionKind}, engine::{EngineManager, ExecutionKind},
errors::{KclError, KclErrorDetails}, errors::{KclError, KclErrorDetails},
fs::{FileManager, FileSystem}, 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, settings::types::UnitLength,
source_range::{ModuleId, SourceRange}, source_range::{ModuleId, SourceRange},
std::{args::Arg, StdLib}, std::{args::Arg, StdLib},
@ -55,9 +58,6 @@ pub struct ExecState {
pub path_to_source_id: IndexMap<std::path::PathBuf, ModuleId>, pub path_to_source_id: IndexMap<std::path::PathBuf, ModuleId>,
/// Map from module ID to module info. /// Map from module ID to module info.
pub module_infos: IndexMap<ModuleId, ModuleInfo>, pub module_infos: IndexMap<ModuleId, ModuleInfo>,
/// 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<String>,
} }
impl ExecState { impl ExecState {
@ -1484,7 +1484,8 @@ pub struct ExecutorContext {
} }
/// The executor settings. /// The executor settings.
#[derive(Debug, Clone)] #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
pub struct ExecutorSettings { pub struct ExecutorSettings {
/// The unit to use in modeling dimensions. /// The unit to use in modeling dimensions.
pub units: UnitLength, pub units: UnitLength,
@ -1785,18 +1786,21 @@ impl ExecutorContext {
&self, &self,
exec_state: &mut ExecState, exec_state: &mut ExecState,
source_range: crate::executor::SourceRange, source_range: crate::executor::SourceRange,
) -> Result<()> { ) -> Result<(), KclError> {
self.engine self.engine
.clear_scene(&mut exec_state.id_generator, source_range) .clear_scene(&mut exec_state.id_generator, source_range)
.await?; .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(()) Ok(())
} }
/// Perform the execution of a program. /// Perform the execution of a program.
/// You can optionally pass in some initialization memory. /// You can optionally pass in some initialization memory.
/// Kurt uses this for partial execution. /// Kurt uses this for partial execution.
pub async fn run(&self, program: &Program, exec_state: &mut ExecState) -> Result<(), KclError> { pub async fn run(&self, cache_info: CacheInformation, exec_state: &mut ExecState) -> Result<(), KclError> {
self.run_with_session_data(program, exec_state).await?; self.run_with_session_data(cache_info, exec_state).await?;
Ok(()) Ok(())
} }
@ -1805,10 +1809,27 @@ impl ExecutorContext {
/// Kurt uses this for partial execution. /// Kurt uses this for partial execution.
pub async fn run_with_session_data( pub async fn run_with_session_data(
&self, &self,
program: &Program, cache_info: CacheInformation,
exec_state: &mut ExecState, exec_state: &mut ExecState,
) -> Result<Option<ModelingSessionData>, KclError> { ) -> Result<Option<ModelingSessionData>, KclError> {
let _stats = crate::log::LogPerfStats::new("Interpretation"); 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. // TODO: Use the top-level file's path.
exec_state.add_module(std::path::PathBuf::from("")); exec_state.add_module(std::path::PathBuf::from(""));
// Before we even start executing the program, set the units. // Before we even start executing the program, set the units.
@ -1829,7 +1850,7 @@ impl ExecutorContext {
) )
.await?; .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?; .await?;
let session_data = self.engine.get_session_data(); let session_data = self.engine.get_session_data();
Ok(session_data) Ok(session_data)
@ -1857,11 +1878,7 @@ impl ExecutorContext {
source_ranges: vec![source_range], source_ranges: vec![source_range],
})); }));
} }
let resolved_path = if let Some(project_dir) = &exec_state.project_directory { let resolved_path = std::path::PathBuf::from(&path);
std::path::PathBuf::from(project_dir).join(&path)
} else {
std::path::PathBuf::from(&path)
};
if exec_state.import_stack.contains(&resolved_path) { if exec_state.import_stack.contains(&resolved_path) {
return Err(KclError::ImportCycle(KclErrorDetails { return Err(KclError::ImportCycle(KclErrorDetails {
message: format!( message: format!(
@ -2097,7 +2114,7 @@ impl ExecutorContext {
program: &Program, program: &Program,
exec_state: &mut ExecState, exec_state: &mut ExecState,
) -> std::result::Result<TakeSnapshot, ExecError> { ) -> std::result::Result<TakeSnapshot, ExecError> {
self.run(program, exec_state).await?; self.run(program.clone().into(), exec_state).await?;
// Zoom to fit. // Zoom to fit.
self.engine self.engine
@ -2243,7 +2260,7 @@ mod tests {
context_type: ContextType::Mock, context_type: ContextType::Mock,
}; };
let mut exec_state = ExecState::default(); 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) Ok(exec_state.memory)
} }

View File

@ -89,7 +89,11 @@ pub use lsp::{
copilot::Backend as CopilotLspBackend, copilot::Backend as CopilotLspBackend,
kcl::{Backend as KclLspBackend, Server as KclLspServerSubCommand}, 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 settings::types::{project::ProjectConfiguration, Configuration, UnitLength};
pub use source_range::{ModuleId, SourceRange}; pub use source_range::{ModuleId, SourceRange};
@ -125,7 +129,7 @@ use crate::log::{log, logln};
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Program { pub struct Program {
#[serde(flatten)] #[serde(flatten)]
ast: parsing::ast::types::Node<parsing::ast::types::Program>, pub ast: parsing::ast::types::Node<parsing::ast::types::Program>,
} }
#[cfg(any(test, feature = "lsp-test-util"))] #[cfg(any(test, feature = "lsp-test-util"))]

View File

@ -45,11 +45,14 @@ use crate::{
errors::Suggestion, errors::Suggestion,
lsp::{backend::Backend as _, util::IntoDiagnostic}, lsp::{backend::Backend as _, util::IntoDiagnostic},
parsing::{ parsing::{
ast::types::{Expr, Node, VariableKind}, ast::{
cache::{CacheInformation, OldAstState},
types::{Expr, Node, VariableKind},
},
token::TokenType, token::TokenType,
PIPE_OPERATOR, PIPE_OPERATOR,
}, },
ExecState, ModuleId, Program, SourceRange, ModuleId, Program, SourceRange,
}; };
lazy_static::lazy_static! { lazy_static::lazy_static! {
@ -105,6 +108,12 @@ pub struct Backend {
pub token_map: DashMap<String, Vec<crate::parsing::token::Token>>, pub token_map: DashMap<String, Vec<crate::parsing::token::Token>>,
/// AST maps. /// AST maps.
pub ast_map: DashMap<String, Node<crate::parsing::ast::types::Program>>, pub ast_map: DashMap<String, Node<crate::parsing::ast::types::Program>>,
/// 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<RwLock<Option<OldAstState>>>,
/// Memory maps. /// Memory maps.
pub memory_map: DashMap<String, crate::executor::ProgramMemory>, pub memory_map: DashMap<String, crate::executor::ProgramMemory>,
/// Current code. /// Current code.
@ -189,6 +198,7 @@ impl Backend {
diagnostics_map: Default::default(), diagnostics_map: Default::default(),
symbols_map: Default::default(), symbols_map: Default::default(),
semantic_tokens_map: Default::default(), semantic_tokens_map: Default::default(),
last_successful_ast_state: Default::default(),
is_initialized: 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) { 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(); let filename = params.uri.to_string();
// We already updated the code map in the shared backend. // We already updated the code map in the shared backend.
@ -674,23 +691,43 @@ impl Backend {
return Ok(()); 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. let mut exec_state = if let Some(last_successful_ast_state) = last_successful_ast_state.clone() {
executor_ctx last_successful_ast_state.exec_state
.engine } else {
.clear_scene(&mut exec_state.id_generator, SourceRange::default()) Default::default()
.await?; };
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.memory_map.remove(params.uri.as_str());
self.add_to_diagnostics(params, &[err], false).await; 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 // Since we already published the diagnostics we don't really care about the error
// string. // string.
return Err(anyhow::anyhow!("failed to execute code")); 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 self.memory_map
.insert(params.uri.to_string(), exec_state.memory.clone()); .insert(params.uri.to_string(), exec_state.memory.clone());

View File

@ -37,6 +37,7 @@ pub async fn kcl_lsp_server(execute: bool) -> Result<crate::lsp::kcl::Backend> {
can_send_telemetry: true, can_send_telemetry: true,
executor_ctx: Arc::new(tokio::sync::RwLock::new(executor_ctx)), executor_ctx: Arc::new(tokio::sync::RwLock::new(executor_ctx)),
can_execute: Arc::new(tokio::sync::RwLock::new(can_execute)), can_execute: Arc::new(tokio::sync::RwLock::new(can_execute)),
last_successful_ast_state: Default::default(),
is_initialized: Default::default(), is_initialized: Default::default(),
}) })
.custom_method("kcl/updateUnits", crate::lsp::kcl::Backend::update_units) .custom_method("kcl/updateUnits", crate::lsp::kcl::Backend::update_units)

View File

@ -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<OldAstState>,
/// The new ast to executed.
pub new_ast: Node<Program>,
}
/// 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<Program>,
/// The exec state.
pub exec_state: ExecState,
/// The last settings used for execution.
pub settings: crate::executor::ExecutorSettings,
}
impl From<crate::Program> 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<Program>,
}
// 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<CacheResult> {
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<ExecState> {
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);
}
}

View File

@ -1,3 +1,4 @@
pub(crate) mod cache;
pub(crate) mod digest; pub(crate) mod digest;
pub(crate) mod execute; pub(crate) mod execute;
pub mod modify; pub mod modify;

View File

@ -1,5 +1,7 @@
use crate::parsing::ast::types::{BinaryPart, BodyItem, Expr, LiteralIdentifier, MemberObject}; use crate::{
use crate::source_range::ModuleId; parsing::ast::types::{BinaryPart, BodyItem, Expr, LiteralIdentifier, MemberObject},
source_range::ModuleId,
};
impl BodyItem { impl BodyItem {
pub fn module_id(&self) -> ModuleId { pub fn module_id(&self) -> ModuleId {

View File

@ -1,13 +1,12 @@
// TODO optimise size of CompilationError // TODO optimise size of CompilationError
#![allow(clippy::result_large_err)] #![allow(clippy::result_large_err)]
use super::CompilationError;
use crate::{ use crate::{
parsing::ast::types::{BinaryExpression, BinaryOperator, BinaryPart, Node}, parsing::ast::types::{BinaryExpression, BinaryOperator, BinaryPart, Node},
SourceRange, SourceRange,
}; };
use super::CompilationError;
/// Parses a list of tokens (in infix order, i.e. as the user typed them) /// Parses a list of tokens (in infix order, i.e. as the user typed them)
/// into a binary expression tree. /// into a binary expression tree.
pub fn parse(infix_tokens: Vec<BinaryExpressionToken>) -> Result<Node<BinaryExpression>, CompilationError> { pub fn parse(infix_tokens: Vec<BinaryExpressionToken>) -> Result<Node<BinaryExpression>, CompilationError> {
@ -127,8 +126,7 @@ impl From<BinaryOperator> for BinaryExpressionToken {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::parsing::ast::types::Literal; use crate::{parsing::ast::types::Literal, source_range::ModuleId};
use crate::source_range::ModuleId;
#[test] #[test]
fn parse_and_evaluate() { fn parse_and_evaluate() {

View File

@ -4,27 +4,42 @@ use std::{str::FromStr, sync::Arc};
use futures::stream::TryStreamExt; use futures::stream::TryStreamExt;
use gloo_utils::format::JsValueSerdeExt; 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 tower_lsp::{LspService, Server};
use wasm_bindgen::prelude::*; 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<RwLock<Option<OldAstState>>> = 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<OldAstState> {
let lock = OLD_AST_MEMORY.read().await;
lock.clone()
}
// wasm_bindgen wrapper for execute // wasm_bindgen wrapper for execute
#[wasm_bindgen] #[wasm_bindgen]
pub async fn execute_wasm( pub async fn execute_wasm(
program_str: &str, program_str: &str,
memory_str: &str, program_memory_override_str: &str,
id_generator_str: &str,
units: &str, units: &str,
engine_manager: kcl_lib::wasm_engine::EngineCommandManager, engine_manager: kcl_lib::wasm_engine::EngineCommandManager,
fs_manager: kcl_lib::wasm_engine::FileSystemManager, fs_manager: kcl_lib::wasm_engine::FileSystemManager,
project_directory: Option<String>,
is_mock: bool,
) -> Result<JsValue, String> { ) -> Result<JsValue, String> {
console_error_panic_hook::set_once(); console_error_panic_hook::set_once();
let program: Program = serde_json::from_str(program_str).map_err(|e| e.to_string())?; 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 program_memory_override: Option<kcl_lib::exec::ProgramMemory> =
let id_generator: kcl_lib::exec::IdGenerator = serde_json::from_str(id_generator_str).map_err(|e| e.to_string())?; 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 units = kcl_lib::UnitLength::from_str(units).map_err(|e| e.to_string())?;
let ctx = if is_mock { let ctx = if is_mock {
@ -33,14 +48,55 @@ pub async fn execute_wasm(
kcl_lib::ExecutorContext::new(engine_manager, fs_manager, units).await? kcl_lib::ExecutorContext::new(engine_manager, fs_manager, units).await?
}; };
let mut exec_state = ExecState { let mut exec_state = ExecState { ..ExecState::default() };
memory,
id_generator,
project_directory,
..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 // The serde-wasm-bindgen does not work here because of weird HashMap issues so we use the
// gloo-serialize crate instead. // gloo-serialize crate instead.

View File

@ -11,7 +11,7 @@ async fn setup(code: &str, name: &str) -> Result<(ExecutorContext, Program, Modu
let program = Program::parse_no_errs(code)?; let program = Program::parse_no_errs(code)?;
let ctx = kcl_lib::ExecutorContext::new_with_default_client(Default::default()).await?; let ctx = kcl_lib::ExecutorContext::new_with_default_client(Default::default()).await?;
let mut exec_state = ExecState::default(); 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. // We need to get the sketch ID.
// Get the sketch ID from memory. // Get the sketch ID from memory.