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, '')
)
})
/* 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,

View File

@ -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)
}

View File

@ -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.')

View File

@ -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

View File

@ -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) =>

View File

@ -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,

View File

@ -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(

View File

@ -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

View File

@ -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<ToolTip> = [
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<Program>
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: [],

View File

@ -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(),

View File

@ -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<Program>,
programMemory: ProgramMemory | Error = ProgramMemory.empty(),
idGenerator: IdGenerator = defaultIdGenerator(),
engineCommandManager: EngineCommandManager,
isMock: boolean = false
programMemoryOverride: ProgramMemory | Error | null = null
): 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
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<Program>,
programMemory: ProgramMemory | Error = ProgramMemory.empty(),
idGenerator: IdGenerator = defaultIdGenerator(),
engineCommandManager: EngineCommandManager,
isMock: boolean
programMemoryOverride: ProgramMemory | Error | null = null
): Promise<ExecState> => {
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) {

View File

@ -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()

View File

@ -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<Program>,
pm: ProgramMemory | Error = ProgramMemory.empty(),
idGenerator: IdGenerator = defaultIdGenerator()
pmo: ProgramMemory | Error = ProgramMemory.empty()
): Promise<ExecState> {
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<Program>,
pm: ProgramMemory = ProgramMemory.empty(),
idGenerator: IdGenerator = defaultIdGenerator()
pmo: ProgramMemory = ProgramMemory.empty()
): Promise<ExecState> {
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)

View File

@ -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) =>

View File

@ -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')

View File

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

View File

@ -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"

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,
};
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)]

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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";

View File

@ -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<String> {
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)

View File

@ -1,6 +1,7 @@
use kcl_to_core::*;
use std::{env, fs};
use kcl_to_core::*;
#[tokio::main]
async fn main() {
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},
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<std::path::PathBuf, ModuleId>,
/// Map from module ID to module info.
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 {
@ -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<Option<ModelingSessionData>, 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<TakeSnapshot, ExecError> {
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)
}

View File

@ -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<parsing::ast::types::Program>,
pub ast: parsing::ast::types::Node<parsing::ast::types::Program>,
}
#[cfg(any(test, feature = "lsp-test-util"))]

View File

@ -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<String, Vec<crate::parsing::token::Token>>,
/// AST maps.
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.
pub memory_map: DashMap<String, crate::executor::ProgramMemory>,
/// 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());

View File

@ -37,6 +37,7 @@ pub async fn kcl_lsp_server(execute: bool) -> Result<crate::lsp::kcl::Backend> {
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)

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 execute;
pub mod modify;

View File

@ -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 {

View File

@ -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<BinaryExpressionToken>) -> Result<Node<BinaryExpression>, CompilationError> {
@ -127,8 +126,7 @@ impl From<BinaryOperator> 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() {

View File

@ -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<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]
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<String>,
is_mock: bool,
) -> Result<JsValue, String> {
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<kcl_lib::exec::ProgramMemory> =
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.

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 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.