Surface warnings to frontend and LSP (#4603)

* Send multiple errors and warnings to the frontend and LSP

Signed-off-by: Nick Cameron <nrc@ncameron.org>

* Refactor the parser to use CompilationError for parsing errors rather than KclError

Signed-off-by: Nick Cameron <nrc@ncameron.org>

* Refactoring: move CompilationError, etc.

Signed-off-by: Nick Cameron <nrc@ncameron.org>

* Integrate compilation errors with the frontend and CodeMirror

Signed-off-by: Nick Cameron <nrc@ncameron.org>

* Fix tests

Signed-off-by: Nick Cameron <nrc@ncameron.org>

* Review comments

Signed-off-by: Nick Cameron <nrc@ncameron.org>

* Fix module id/source range stuff

Signed-off-by: Nick Cameron <nrc@ncameron.org>

* More test fixups

Signed-off-by: Nick Cameron <nrc@ncameron.org>

---------

Signed-off-by: Nick Cameron <nrc@ncameron.org>
This commit is contained in:
Nick Cameron
2024-12-06 13:57:31 +13:00
committed by GitHub
parent 513c76ecc8
commit eb96d6539c
76 changed files with 1461 additions and 3139 deletions

View File

@ -99,7 +99,7 @@ yarn tron:start
This will start the application and hot-reload on changes.
Devtools can be opened with the usual Cmd/Ctrl-Shift-I.
Devtools can be opened with the usual Cmd-Opt-I (Mac) or Ctrl-Shift-I (Linux and Windows).
To build, run `yarn tron:package`.

View File

@ -518,7 +518,10 @@ test.describe('Editor tests', () => {
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
})
test('error with 2 source ranges gets 2 diagnostics', async ({ page }) => {
// TODO currently multiple source ranges are not supported
test.skip('error with 2 source ranges gets 2 diagnostics', async ({
page,
}) => {
const u = await getUtils(page)
await page.addInitScript(async () => {
localStorage.setItem(

View File

@ -29,6 +29,8 @@ import {
Expr,
parse,
recast,
defaultSourceRange,
resultIsOk,
} from 'lang/wasm'
import { CustomIcon, CustomIconName } from 'components/CustomIcon'
import { ConstrainInfo } from 'lang/std/stdTypes'
@ -412,8 +414,9 @@ export async function deleteSegment({
if (err(modifiedAst)) return Promise.reject(modifiedAst)
const newCode = recast(modifiedAst)
modifiedAst = parse(newCode)
if (err(modifiedAst)) return Promise.reject(modifiedAst)
const pResult = parse(newCode)
if (err(pResult) || !resultIsOk(pResult)) return Promise.reject(pResult)
modifiedAst = pResult.program
const testExecute = await executeAst({
ast: modifiedAst,
@ -590,7 +593,9 @@ const ConstraintSymbol = ({
if (err(_node)) return
const node = _node.node
const range: SourceRange = node ? [node.start, node.end] : [0, 0]
const range: SourceRange = node
? [node.start, node.end, true]
: defaultSourceRange()
if (_type === 'intersectionTag') return null
@ -612,7 +617,7 @@ const ConstraintSymbol = ({
editorManager.setHighlightRange([range])
}}
onMouseLeave={() => {
editorManager.setHighlightRange([[0, 0]])
editorManager.setHighlightRange([defaultSourceRange()])
}}
// disabled={isConstrained || !convertToVarEnabled}
// disabled={implicitDesc} TODO why does this change styles that are hard to override?
@ -627,10 +632,12 @@ const ConstraintSymbol = ({
})
} else if (isConstrained) {
try {
const parsed = parse(recast(kclManager.ast))
if (trap(parsed)) return Promise.reject(parsed)
const pResult = parse(recast(kclManager.ast))
if (trap(pResult) || !resultIsOk(pResult))
return Promise.reject(pResult)
const _node1 = getNodeFromPath<CallExpression>(
parsed,
pResult.program!,
pathToNode,
'CallExpression',
true

View File

@ -48,6 +48,9 @@ import {
VariableDeclarator,
sketchFromKclValue,
sketchFromKclValueOptional,
defaultSourceRange,
sourceRangeFromRust,
resultIsOk,
} from 'lang/wasm'
import {
engineCommandManager,
@ -530,7 +533,7 @@ export class SceneEntities {
const segPathToNode = getNodePathFromSourceRange(
maybeModdedAst,
sketch.start.__geoMeta.sourceRange
sourceRangeFromRust(sketch.start.__geoMeta.sourceRange)
)
if (sketch?.paths?.[0]?.type !== 'Circle') {
const _profileStart = createProfileStartHandle({
@ -552,7 +555,7 @@ export class SceneEntities {
sketch.paths.forEach((segment, index) => {
let segPathToNode = getNodePathFromSourceRange(
maybeModdedAst,
segment.__geoMeta.sourceRange
sourceRangeFromRust(segment.__geoMeta.sourceRange)
)
if (
draftExpressionsIndices &&
@ -561,12 +564,12 @@ export class SceneEntities {
const previousSegment = sketch.paths[index - 1] || sketch.start
const previousSegmentPathToNode = getNodePathFromSourceRange(
maybeModdedAst,
previousSegment.__geoMeta.sourceRange
sourceRangeFromRust(previousSegment.__geoMeta.sourceRange)
)
const bodyIndex = previousSegmentPathToNode[1][0]
segPathToNode = getNodePathFromSourceRange(
truncatedAst,
segment.__geoMeta.sourceRange
sourceRangeFromRust(segment.__geoMeta.sourceRange)
)
segPathToNode[1][0] = bodyIndex
}
@ -575,7 +578,10 @@ export class SceneEntities {
index <= draftExpressionsIndices.end &&
index >= draftExpressionsIndices.start
const isSelected = selectionRanges?.graphSelections.some((selection) =>
isOverlap(selection?.codeRef?.range, segment.__geoMeta.sourceRange)
isOverlap(
selection?.codeRef?.range,
sourceRangeFromRust(segment.__geoMeta.sourceRange)
)
)
let seg: Group
@ -657,13 +663,11 @@ export class SceneEntities {
}
updateAstAndRejigSketch = async (
sketchPathToNode: PathToNode,
modifiedAst: Node<Program> | Error,
modifiedAst: Node<Program>,
forward: [number, number, number],
up: [number, number, number],
origin: [number, number, number]
) => {
if (err(modifiedAst)) return modifiedAst
const nextAst = await kclManager.updateAst(modifiedAst, false)
await this.tearDownSketch({ removeAxis: false })
sceneInfra.resetMouseListeners()
@ -721,8 +725,9 @@ export class SceneEntities {
pathToNode: sketchPathToNode,
})
if (trap(mod)) return Promise.reject(mod)
const modifiedAst = parse(recast(mod.modifiedAst))
if (trap(modifiedAst)) return Promise.reject(modifiedAst)
const pResult = parse(recast(mod.modifiedAst))
if (trap(pResult) || !resultIsOk(pResult)) return Promise.reject(pResult)
const modifiedAst = pResult.program
const draftExpressionsIndices = { start: index, end: index }
@ -914,9 +919,9 @@ export class SceneEntities {
...getRectangleCallExpressions(rectangleOrigin, tags),
])
let _recastAst = parse(recast(_ast))
if (trap(_recastAst)) return Promise.reject(_recastAst)
_ast = _recastAst
const pResult = parse(recast(_ast))
if (trap(pResult) || !resultIsOk(pResult)) return Promise.reject(pResult)
_ast = pResult.program
const { programMemoryOverride, truncatedAst } = await this.setupSketch({
sketchPathToNode,
@ -998,9 +1003,10 @@ export class SceneEntities {
updateRectangleSketch(sketchInit, x, y, tags[0])
const newCode = recast(_ast)
let _recastAst = parse(newCode)
if (trap(_recastAst)) return
_ast = _recastAst
const pResult = parse(newCode)
if (trap(pResult) || !resultIsOk(pResult))
return Promise.reject(pResult)
_ast = pResult.program
// Update the primary AST and unequip the rectangle tool
await kclManager.executeAstMock(_ast)
@ -1071,9 +1077,9 @@ export class SceneEntities {
...getRectangleCallExpressions(rectangleOrigin, tags),
])
let _recastAst = parse(recast(_ast))
if (trap(_recastAst)) return Promise.reject(_recastAst)
_ast = _recastAst
const pResult = parse(recast(_ast))
if (trap(pResult) || !resultIsOk(pResult)) return Promise.reject(pResult)
_ast = pResult.program
const { programMemoryOverride, truncatedAst } = await this.setupSketch({
sketchPathToNode,
@ -1165,9 +1171,10 @@ export class SceneEntities {
rectangleOrigin[1]
)
let _recastAst = parse(recast(_ast))
if (trap(_recastAst)) return
_ast = _recastAst
const pResult = parse(recast(_ast))
if (trap(pResult) || !resultIsOk(pResult))
return Promise.reject(pResult)
_ast = pResult.program
// Update the primary AST and unequip the rectangle tool
await kclManager.executeAstMock(_ast)
@ -1241,9 +1248,9 @@ export class SceneEntities {
]),
])
let _recastAst = parse(recast(_ast))
if (trap(_recastAst)) return Promise.reject(_recastAst)
_ast = _recastAst
const pResult = parse(recast(_ast))
if (trap(pResult) || !resultIsOk(pResult)) return Promise.reject(pResult)
_ast = pResult.program
// do a quick mock execution to get the program memory up-to-date
await kclManager.executeAstMock(_ast)
@ -1365,9 +1372,10 @@ export class SceneEntities {
const newCode = recast(modded)
if (err(newCode)) return
let _recastAst = parse(newCode)
if (trap(_recastAst)) return Promise.reject(_recastAst)
_ast = _recastAst
const pResult = parse(newCode)
if (trap(pResult) || !resultIsOk(pResult))
return Promise.reject(pResult)
_ast = pResult.program
// Update the primary AST and unequip the rectangle tool
await kclManager.executeAstMock(_ast)
@ -1660,7 +1668,7 @@ export class SceneEntities {
kclManager.programMemory,
{
type: 'sourceRange',
sourceRange: [node.start, node.end],
sourceRange: [node.start, node.end, true],
},
getChangeSketchInput()
)
@ -1750,7 +1758,7 @@ export class SceneEntities {
): (() => SegmentOverlayPayload | null) => {
const segPathToNode = getNodePathFromSourceRange(
modifiedAst,
segment.__geoMeta.sourceRange
sourceRangeFromRust(segment.__geoMeta.sourceRange)
)
const sgPaths = sketch.paths
const originalPathToNodeStr = JSON.stringify(segPathToNode)
@ -1901,8 +1909,10 @@ export class SceneEntities {
SEGMENT_BODIES_PLUS_PROFILE_START
)
if (parent?.userData?.pathToNode) {
const updatedAst = parse(recast(kclManager.ast))
if (trap(updatedAst)) return
const pResult = parse(recast(kclManager.ast))
if (trap(pResult) || !resultIsOk(pResult))
return Promise.reject(pResult)
const updatedAst = pResult.program
const _node = getNodeFromPath<Node<CallExpression>>(
updatedAst,
parent.userData.pathToNode,
@ -1910,7 +1920,7 @@ export class SceneEntities {
)
if (trap(_node, { suppress: true })) return
const node = _node.node
editorManager.setHighlightRange([[node.start, node.end]])
editorManager.setHighlightRange([[node.start, node.end, true]])
const yellow = 0xffff00
colorSegment(selected, yellow)
const extraSegmentGroup = parent.getObjectByName(EXTRA_SEGMENT_HANDLE)
@ -1955,10 +1965,10 @@ export class SceneEntities {
})
return
}
editorManager.setHighlightRange([[0, 0]])
editorManager.setHighlightRange([defaultSourceRange()])
},
onMouseLeave: ({ selected, ...rest }: OnMouseEnterLeaveArgs) => {
editorManager.setHighlightRange([[0, 0]])
editorManager.setHighlightRange([defaultSourceRange()])
const parent = getParentGroup(
selected,
SEGMENT_BODIES_PLUS_PROFILE_START
@ -2087,8 +2097,10 @@ function prepareTruncatedMemoryAndAst(
).body.push(newSegment)
// update source ranges to section we just added.
// hacks like this wouldn't be needed if the AST put pathToNode info in memory/sketch segments
const updatedSrcRangeAst = parse(recast(_ast)) // get source ranges correct since unfortunately we still rely on them
if (err(updatedSrcRangeAst)) return updatedSrcRangeAst
const pResult = parse(recast(_ast)) // get source ranges correct since unfortunately we still rely on them
if (trap(pResult) || !resultIsOk(pResult))
return Error('Unexpected compilation error')
const updatedSrcRangeAst = pResult.program
const lastPipeItem = (
(updatedSrcRangeAst.body[bodyIndex] as VariableDeclaration)

View File

@ -5,6 +5,7 @@ import { useEffect, useRef, useState } from 'react'
import { trap } from 'lib/trap'
import { codeToIdSelections } from 'lib/selections'
import { codeRefFromRange } from 'lang/std/artifactGraph'
import { defaultSourceRange } from 'lang/wasm'
export function AstExplorer() {
const { context } = useModelingContext()
@ -46,7 +47,7 @@ export function AstExplorer() {
<div
className="h-full relative"
onMouseLeave={(e) => {
editorManager.setHighlightRange([[0, 0]])
editorManager.setHighlightRange([defaultSourceRange()])
}}
>
<pre className="text-xs">
@ -115,15 +116,19 @@ function DisplayObj({
hasCursor ? 'bg-violet-100/80 dark:bg-violet-100/25' : ''
}`}
onMouseEnter={(e) => {
editorManager.setHighlightRange([[obj?.start || 0, obj.end]])
editorManager.setHighlightRange([[obj?.start || 0, obj.end, true]])
e.stopPropagation()
}}
onMouseMove={(e) => {
e.stopPropagation()
editorManager.setHighlightRange([[obj?.start || 0, obj.end]])
editorManager.setHighlightRange([[obj?.start || 0, obj.end, true]])
}}
onClick={(e) => {
const range: [number, number] = [obj?.start || 0, obj.end || 0]
const range: [number, number, boolean] = [
obj?.start || 0,
obj.end || 0,
true,
]
const idInfo = codeToIdSelections([
{ codeRef: codeRefFromRange(range, kclManager.ast) },
])[0]

View File

@ -1,5 +1,11 @@
import { useEffect, useState, useRef } from 'react'
import { parse, BinaryPart, Expr, ProgramMemory } from '../lang/wasm'
import {
parse,
BinaryPart,
Expr,
ProgramMemory,
resultIsOk,
} from '../lang/wasm'
import {
createIdentifier,
createLiteral,
@ -141,8 +147,9 @@ export function useCalc({
useEffect(() => {
try {
const code = `const __result__ = ${value}`
const ast = parse(code)
if (trap(ast)) return
const pResult = parse(code)
if (trap(pResult) || !resultIsOk(pResult)) return
const ast = pResult.program
const _programMem: ProgramMemory = ProgramMemory.empty()
for (const { key, value } of availableVarInfo.variables) {
const error = _programMem.set(key, {

View File

@ -67,7 +67,7 @@ import {
sketchOnOffsetPlane,
startSketchOnDefault,
} from 'lang/modifyAst'
import { Program, parse, recast } from 'lang/wasm'
import { Program, parse, recast, resultIsOk } from 'lang/wasm'
import {
doesSceneHaveSweepableSketch,
getNodePathFromSourceRange,
@ -612,15 +612,11 @@ export const ModelingMachineProvider = ({
)
},
'Has exportable geometry': () => {
if (
kclManager.kclErrors.length === 0 &&
kclManager.ast.body.length > 0
)
if (!kclManager.hasErrors() && kclManager.ast.body.length > 0)
return true
else {
let errorMessage = 'Unable to Export '
if (kclManager.kclErrors.length > 0)
errorMessage += 'due to KCL Errors'
if (kclManager.hasErrors()) errorMessage += 'due to KCL Errors'
else if (kclManager.ast.body.length === 0)
errorMessage += 'due to Empty Scene'
console.error(errorMessage)
@ -738,7 +734,11 @@ export const ModelingMachineProvider = ({
constraint: 'setHorzDistance',
selectionRanges,
})
const _modifiedAst = parse(recast(modifiedAst))
const pResult = parse(recast(modifiedAst))
if (trap(pResult) || !resultIsOk(pResult))
return Promise.reject(new Error('Unexpected compilation error'))
const _modifiedAst = pResult.program
if (!sketchDetails)
return Promise.reject(new Error('No sketch details'))
const updatedPathToNode = updatePathToNodeFromMap(
@ -779,7 +779,10 @@ export const ModelingMachineProvider = ({
constraint: 'setVertDistance',
selectionRanges,
})
const _modifiedAst = parse(recast(modifiedAst))
const pResult = parse(recast(modifiedAst))
if (trap(pResult) || !resultIsOk(pResult))
return Promise.reject(new Error('Unexpected compilation error'))
const _modifiedAst = pResult.program
if (!sketchDetails)
return Promise.reject(new Error('No sketch details'))
const updatedPathToNode = updatePathToNodeFromMap(
@ -827,7 +830,10 @@ export const ModelingMachineProvider = ({
selectionRanges,
angleOrLength: 'setAngle',
}))
const _modifiedAst = parse(recast(modifiedAst))
const pResult = parse(recast(modifiedAst))
if (trap(pResult) || !resultIsOk(pResult))
return Promise.reject(new Error('Unexpected compilation error'))
const _modifiedAst = pResult.program
if (err(_modifiedAst)) return Promise.reject(_modifiedAst)
if (!sketchDetails)
@ -869,7 +875,10 @@ export const ModelingMachineProvider = ({
await applyConstraintAngleLength({
selectionRanges,
})
const _modifiedAst = parse(recast(modifiedAst))
const pResult = parse(recast(modifiedAst))
if (trap(pResult) || !resultIsOk(pResult))
return Promise.reject(new Error('Unexpected compilation error'))
const _modifiedAst = pResult.program
if (!sketchDetails)
return Promise.reject(new Error('No sketch details'))
const updatedPathToNode = updatePathToNodeFromMap(
@ -909,7 +918,10 @@ export const ModelingMachineProvider = ({
await applyConstraintIntersect({
selectionRanges,
})
const _modifiedAst = parse(recast(modifiedAst))
const pResult = parse(recast(modifiedAst))
if (trap(pResult) || !resultIsOk(pResult))
return Promise.reject(new Error('Unexpected compilation error'))
const _modifiedAst = pResult.program
if (!sketchDetails)
return Promise.reject(new Error('No sketch details'))
const updatedPathToNode = updatePathToNodeFromMap(
@ -950,7 +962,10 @@ export const ModelingMachineProvider = ({
constraint: 'xAbs',
selectionRanges,
})
const _modifiedAst = parse(recast(modifiedAst))
const pResult = parse(recast(modifiedAst))
if (trap(pResult) || !resultIsOk(pResult))
return Promise.reject(new Error('Unexpected compilation error'))
const _modifiedAst = pResult.program
if (!sketchDetails)
return Promise.reject(new Error('No sketch details'))
const updatedPathToNode = updatePathToNodeFromMap(
@ -991,7 +1006,10 @@ export const ModelingMachineProvider = ({
constraint: 'yAbs',
selectionRanges,
})
const _modifiedAst = parse(recast(modifiedAst))
const pResult = parse(recast(modifiedAst))
if (trap(pResult) || !resultIsOk(pResult))
return Promise.reject(new Error('Unexpected compilation error'))
const _modifiedAst = pResult.program
if (!sketchDetails)
return Promise.reject(new Error('No sketch details'))
const updatedPathToNode = updatePathToNodeFromMap(
@ -1032,9 +1050,10 @@ export const ModelingMachineProvider = ({
const { variableName } = await getVarNameModal({
valueName: data?.variableName || 'var',
})
let parsed = parse(recast(kclManager.ast))
if (trap(parsed)) return Promise.reject(parsed)
parsed = parsed as Node<Program>
let pResult = parse(recast(kclManager.ast))
if (trap(pResult) || !resultIsOk(pResult))
return Promise.reject(new Error('Unexpected compilation error'))
let parsed = pResult.program
const { modifiedAst: _modifiedAst, pathToReplacedNode } =
moveValueIntoNewVariablePath(
@ -1043,7 +1062,11 @@ export const ModelingMachineProvider = ({
data?.pathToNode || [],
variableName
)
parsed = parse(recast(_modifiedAst))
pResult = parse(recast(_modifiedAst))
if (trap(pResult) || !resultIsOk(pResult))
return Promise.reject(new Error('Unexpected compilation error'))
parsed = pResult.program
if (trap(parsed)) return Promise.reject(parsed)
parsed = parsed as Node<Program>
if (!pathToReplacedNode)

View File

@ -1,6 +1,6 @@
import { processMemory } from './MemoryPane'
import { enginelessExecutor } from '../../../lib/testHelpers'
import { initPromise, parse, ProgramMemory } from '../../../lang/wasm'
import { assertParse, initPromise, ProgramMemory } from '../../../lang/wasm'
beforeAll(async () => {
await initPromise
@ -28,7 +28,7 @@ describe('processMemory', () => {
|> lineTo([0.98, 5.16], %)
|> lineTo([2.15, 4.32], %)
// |> rx(90, %)`
const ast = parse(code)
const ast = assertParse(code)
const execState = await enginelessExecutor(ast, ProgramMemory.empty())
const output = processMemory(execState.memory)
expect(output.myVar).toEqual(5)

View File

@ -90,7 +90,7 @@ export const sidebarPanes: SidebarPane[] = [
keybinding: 'Shift + C',
showBadge: {
value: ({ kclContext }) => {
return kclContext.errors.length
return kclContext.diagnostics.length
},
onClick: (e) => {
e.preventDefault()

View File

@ -53,7 +53,7 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
settings: settings.context,
platform: getPlatformString(),
}),
[kclContext.errors, settings.context]
[kclContext.diagnostics, settings.context]
)
const sidebarActions: SidebarAction[] = [

View File

@ -40,7 +40,10 @@ export function removeConstrainingValuesInfo({
otherSelections: [],
graphSelections: nodes.map(
(node): Selection => ({
codeRef: codeRefFromRange([node.start, node.end], kclManager.ast),
codeRef: codeRefFromRange(
[node.start, node.end, true],
kclManager.ast
),
})
),
}

View File

@ -139,7 +139,9 @@ export default class EditorManager {
}
setHighlightRange(range: Array<Selection['codeRef']['range']>): void {
this._highlightRange = range
this._highlightRange = range.map((s): [number, number] => {
return [s[0], s[1]]
})
const selectionsWithSafeEnds = range.map((s): [number, number] => {
const safeEnd = Math.min(s[1], this._editorView?.state.doc.length || s[1])

View File

@ -18,7 +18,7 @@ import {
import { err, reportRejection } from 'lib/trap'
import { DefaultPlaneStr, getFaceDetails } from 'clientSideScene/sceneEntities'
import { getNodeFromPath, getNodePathFromSourceRange } from 'lang/queryAst'
import { CallExpression } from 'lang/wasm'
import { CallExpression, defaultSourceRange } from 'lang/wasm'
import { EdgeCutInfo, ExtrudeFacePlane } from 'machines/modelingMachine'
export function useEngineConnectionSubscriptions() {
@ -46,7 +46,7 @@ export function useEngineConnectionSubscriptions() {
(editorManager.highlightRange[0][0] !== 0 &&
editorManager.highlightRange[0][1] !== 0)
) {
editorManager.setHighlightRange([[0, 0]])
editorManager.setHighlightRange([defaultSourceRange()])
}
},
})
@ -201,7 +201,7 @@ export function useEngineConnectionSubscriptions() {
const { z_axis, y_axis, origin } = faceInfo
const sketchPathToNode = getNodePathFromSourceRange(
kclManager.ast,
err(codeRef) ? [0, 0] : codeRef.range
err(codeRef) ? defaultSourceRange() : codeRef.range
)
const getEdgeCutMeta = (): null | EdgeCutInfo => {

View File

@ -1,15 +1,15 @@
import { KCLError } from './errors'
import { createContext, useContext, useEffect, useState } from 'react'
import { type IndexLoaderData } from 'lib/types'
import { useLoaderData } from 'react-router-dom'
import { codeManager, kclManager } from 'lib/singletons'
import { Diagnostic } from '@codemirror/lint'
const KclContext = createContext({
code: codeManager?.code || '',
programMemory: kclManager?.programMemory,
ast: kclManager?.ast,
isExecuting: kclManager?.isExecuting,
errors: kclManager?.kclErrors,
diagnostics: kclManager?.diagnostics,
logs: kclManager?.logs,
wasmInitFailed: kclManager?.wasmInitFailed,
})
@ -32,7 +32,7 @@ export function KclContextProvider({
const [programMemory, setProgramMemory] = useState(kclManager.programMemory)
const [ast, setAst] = useState(kclManager.ast)
const [isExecuting, setIsExecuting] = useState(false)
const [errors, setErrors] = useState<KCLError[]>([])
const [diagnostics, setErrors] = useState<Diagnostic[]>([])
const [logs, setLogs] = useState<string[]>([])
const [wasmInitFailed, setWasmInitFailed] = useState(false)
@ -57,7 +57,7 @@ export function KclContextProvider({
programMemory,
ast,
isExecuting,
errors,
diagnostics,
logs,
wasmInitFailed,
}}

View File

@ -1,6 +1,10 @@
import { executeAst, lintAst } from 'lang/langHelpers'
import { Selections } from 'lib/selections'
import { KCLError, kclErrorsToDiagnostics } from './errors'
import {
KCLError,
complilationErrorsToDiagnostics,
kclErrorsToDiagnostics,
} from './errors'
import { uuidv4 } from 'lib/utils'
import { EngineCommandManager } from './std/engineConnection'
import { err } from 'lib/trap'
@ -51,11 +55,11 @@ export class KclManager {
private _programMemory: ProgramMemory = ProgramMemory.empty()
lastSuccessfulProgramMemory: ProgramMemory = ProgramMemory.empty()
private _logs: string[] = []
private _lints: Diagnostic[] = []
private _kclErrors: KCLError[] = []
private _diagnostics: Diagnostic[] = []
private _isExecuting = false
private _executeIsStale: ExecuteArgs | null = null
private _wasmInitFailed = true
private _hasErrors = false
engineCommandManager: EngineCommandManager
@ -63,7 +67,7 @@ export class KclManager {
private _astCallBack: (arg: Node<Program>) => void = () => {}
private _programMemoryCallBack: (arg: ProgramMemory) => void = () => {}
private _logsCallBack: (arg: string[]) => void = () => {}
private _kclErrorsCallBack: (arg: KCLError[]) => void = () => {}
private _kclErrorsCallBack: (errors: Diagnostic[]) => void = () => {}
private _wasmInitFailedCallback: (arg: boolean) => void = () => {}
private _executeCallback: () => void = () => {}
@ -101,38 +105,28 @@ export class KclManager {
this._logsCallBack(logs)
}
get lints() {
return this._lints
get diagnostics() {
return this._diagnostics
}
set lints(lints) {
if (lints === this._lints) return
this._lints = lints
// Run the lints through the diagnostics.
this.kclErrors = this._kclErrors
}
get kclErrors() {
return this._kclErrors
}
set kclErrors(kclErrors) {
if (kclErrors === this._kclErrors && this.lints.length === 0) return
this._kclErrors = kclErrors
set diagnostics(ds) {
if (ds === this._diagnostics) return
this._diagnostics = ds
this.setDiagnosticsForCurrentErrors()
this._kclErrorsCallBack(kclErrors)
}
addDiagnostics(ds: Diagnostic[]) {
if (ds.length === 0) return
this.diagnostics = this.diagnostics.concat(ds)
}
hasErrors(): boolean {
return this._hasErrors
}
setDiagnosticsForCurrentErrors() {
let diagnostics = kclErrorsToDiagnostics(this.kclErrors)
if (this.lints.length > 0) {
diagnostics = diagnostics.concat(this.lints)
}
editorManager?.setDiagnostics(diagnostics)
}
addKclErrors(kclErrors: KCLError[]) {
if (kclErrors.length === 0) return
this.kclErrors = this.kclErrors.concat(kclErrors)
editorManager?.setDiagnostics(this.diagnostics)
this._kclErrorsCallBack(this.diagnostics)
}
get isExecuting() {
@ -188,7 +182,7 @@ export class KclManager {
setProgramMemory: (arg: ProgramMemory) => void
setAst: (arg: Node<Program>) => void
setLogs: (arg: string[]) => void
setKclErrors: (arg: KCLError[]) => void
setKclErrors: (errors: Diagnostic[]) => void
setIsExecuting: (arg: boolean) => void
setWasmInitFailed: (arg: boolean) => void
}) {
@ -218,19 +212,34 @@ export class KclManager {
}
safeParse(code: string): Node<Program> | null {
const ast = parse(code)
this.lints = []
this.kclErrors = []
if (!err(ast)) return ast
const kclerror: KCLError = ast as KCLError
const result = parse(code)
this.diagnostics = []
this._hasErrors = false
this.addKclErrors([kclerror])
// TODO: re-eval if session should end?
if (kclerror.msg === 'file is empty')
this.engineCommandManager?.endSession()
if (err(result)) {
const kclerror: KCLError = result as KCLError
this.diagnostics = kclErrorsToDiagnostics([kclerror])
this._hasErrors = true
return null
}
this.addDiagnostics(complilationErrorsToDiagnostics(result.errors))
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
}
return result.program
}
async ensureWasmInit() {
try {
await initPromise
@ -279,7 +288,7 @@ export class KclManager {
// Program was not interrupted, setup the scene
// Do not send send scene commands if the program was interrupted, go to clean up
if (!isInterrupted) {
this.lints = await lintAst({ ast: ast })
this.addDiagnostics(await lintAst({ ast: ast }))
sceneInfra.modelingSend({ type: 'code edit during sketch' })
setSelectionFilterToDefault(execState.memory, this.engineCommandManager)
@ -321,7 +330,7 @@ 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.addKclErrors(isInterrupted ? [] : 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
@ -370,7 +379,7 @@ export class KclManager {
})
this._logs = logs
this._kclErrors = errors
this.addDiagnostics(kclErrorsToDiagnostics(errors))
this._execState = execState
this._programMemory = execState.memory
if (!errors.length) {
@ -398,7 +407,7 @@ export class KclManager {
...artifact,
codeRef: {
...artifact.codeRef,
range: [node.start, node.end],
range: [node.start, node.end, true],
},
})
}
@ -490,7 +499,7 @@ export class KclManager {
if (start && end) {
returnVal.graphSelections.push({
codeRef: {
range: [start, end],
range: [start, end, true],
pathToNode: path,
},
})

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,4 @@
import { parse, initPromise } from './wasm'
import { assertParse, initPromise } from './wasm'
import { enginelessExecutor } from '../lib/testHelpers'
beforeAll(async () => {
@ -14,7 +14,7 @@ const mySketch001 = startSketchOn('XY')
|> lineTo([-1.59, -1.54], %)
|> lineTo([0.46, -5.82], %)
// |> rx(45, %)`
const execState = await enginelessExecutor(parse(code))
const execState = await enginelessExecutor(assertParse(code))
// @ts-ignore
const sketch001 = execState.memory.get('mySketch001')
expect(sketch001).toEqual({
@ -67,7 +67,7 @@ const mySketch001 = startSketchOn('XY')
|> lineTo([0.46, -5.82], %)
// |> rx(45, %)
|> extrude(2, %)`
const execState = await enginelessExecutor(parse(code))
const execState = await enginelessExecutor(assertParse(code))
// @ts-ignore
const sketch001 = execState.memory.get('mySketch001')
expect(sketch001).toEqual({
@ -147,7 +147,7 @@ const sk2 = startSketchOn('XY')
|> extrude(2, %)
`
const execState = await enginelessExecutor(parse(code))
const execState = await enginelessExecutor(assertParse(code))
const programMemory = execState.memory
// @ts-ignore
const geos = [programMemory.get('theExtrude'), programMemory.get('sk2')]

View File

@ -8,20 +8,14 @@ describe('test kclErrToDiagnostic', () => {
message: '',
kind: 'semantic',
msg: 'Semantic error',
sourceRanges: [
[0, 1, 0],
[2, 3, 0],
],
sourceRange: [0, 1, true],
},
{
name: '',
message: '',
kind: 'type',
msg: 'Type error',
sourceRanges: [
[4, 5, 0],
[6, 7, 0],
],
sourceRange: [4, 5, true],
},
]
const diagnostics = kclErrorsToDiagnostics(errors)
@ -32,24 +26,12 @@ describe('test kclErrToDiagnostic', () => {
message: 'Semantic error',
severity: 'error',
},
{
from: 2,
to: 3,
message: 'Semantic error',
severity: 'error',
},
{
from: 4,
to: 5,
message: 'Type error',
severity: 'error',
},
{
from: 6,
to: 7,
message: 'Type error',
severity: 'error',
},
])
})
})

View File

@ -1,88 +1,90 @@
import { KclError as RustKclError } from '../wasm-lib/kcl/bindings/KclError'
import { CompilationError } from 'wasm-lib/kcl/bindings/CompilationError'
import { Diagnostic as CodeMirrorDiagnostic } from '@codemirror/lint'
import { posToOffset } from '@kittycad/codemirror-lsp-client'
import { Diagnostic as LspDiagnostic } from 'vscode-languageserver-protocol'
import { Text } from '@codemirror/state'
const TOP_LEVEL_MODULE_ID = 0
import { EditorView } from 'codemirror'
import { SourceRange } from 'lang/wasm'
type ExtractKind<T> = T extends { kind: infer K } ? K : never
export class KCLError extends Error {
kind: ExtractKind<RustKclError> | 'name'
sourceRanges: [number, number, number][]
sourceRange: SourceRange
msg: string
constructor(
kind: ExtractKind<RustKclError> | 'name',
msg: string,
sourceRanges: [number, number, number][]
sourceRange: SourceRange
) {
super()
this.kind = kind
this.msg = msg
this.sourceRanges = sourceRanges
this.sourceRange = sourceRange
Object.setPrototypeOf(this, KCLError.prototype)
}
}
export class KCLLexicalError extends KCLError {
constructor(msg: string, sourceRanges: [number, number, number][]) {
super('lexical', msg, sourceRanges)
constructor(msg: string, sourceRange: SourceRange) {
super('lexical', msg, sourceRange)
Object.setPrototypeOf(this, KCLSyntaxError.prototype)
}
}
export class KCLInternalError extends KCLError {
constructor(msg: string, sourceRanges: [number, number, number][]) {
super('internal', msg, sourceRanges)
constructor(msg: string, sourceRange: SourceRange) {
super('internal', msg, sourceRange)
Object.setPrototypeOf(this, KCLSyntaxError.prototype)
}
}
export class KCLSyntaxError extends KCLError {
constructor(msg: string, sourceRanges: [number, number, number][]) {
super('syntax', msg, sourceRanges)
constructor(msg: string, sourceRange: SourceRange) {
super('syntax', msg, sourceRange)
Object.setPrototypeOf(this, KCLSyntaxError.prototype)
}
}
export class KCLSemanticError extends KCLError {
constructor(msg: string, sourceRanges: [number, number, number][]) {
super('semantic', msg, sourceRanges)
constructor(msg: string, sourceRange: SourceRange) {
super('semantic', msg, sourceRange)
Object.setPrototypeOf(this, KCLSemanticError.prototype)
}
}
export class KCLTypeError extends KCLError {
constructor(msg: string, sourceRanges: [number, number, number][]) {
super('type', msg, sourceRanges)
constructor(msg: string, sourceRange: SourceRange) {
super('type', msg, sourceRange)
Object.setPrototypeOf(this, KCLTypeError.prototype)
}
}
export class KCLUnimplementedError extends KCLError {
constructor(msg: string, sourceRanges: [number, number, number][]) {
super('unimplemented', msg, sourceRanges)
constructor(msg: string, sourceRange: SourceRange) {
super('unimplemented', msg, sourceRange)
Object.setPrototypeOf(this, KCLUnimplementedError.prototype)
}
}
export class KCLUnexpectedError extends KCLError {
constructor(msg: string, sourceRanges: [number, number, number][]) {
super('unexpected', msg, sourceRanges)
constructor(msg: string, sourceRange: SourceRange) {
super('unexpected', msg, sourceRange)
Object.setPrototypeOf(this, KCLUnexpectedError.prototype)
}
}
export class KCLValueAlreadyDefined extends KCLError {
constructor(key: string, sourceRanges: [number, number, number][]) {
super('name', `Key ${key} was already defined elsewhere`, sourceRanges)
constructor(key: string, sourceRange: SourceRange) {
super('name', `Key ${key} was already defined elsewhere`, sourceRange)
Object.setPrototypeOf(this, KCLValueAlreadyDefined.prototype)
}
}
export class KCLUndefinedValueError extends KCLError {
constructor(key: string, sourceRanges: [number, number, number][]) {
super('name', `Key ${key} has not been defined`, sourceRanges)
constructor(key: string, sourceRange: SourceRange) {
super('name', `Key ${key} has not been defined`, sourceRange)
Object.setPrototypeOf(this, KCLUndefinedValueError.prototype)
}
}
@ -99,27 +101,14 @@ export function lspDiagnosticsToKclErrors(
.flatMap(
({ range, message }) =>
new KCLError('unexpected', message, [
[
posToOffset(doc, range.start)!,
posToOffset(doc, range.end)!,
TOP_LEVEL_MODULE_ID,
],
true,
])
)
.filter(({ sourceRanges }) => {
const [from, to, moduleId] = sourceRanges[0]
return (
from !== null &&
to !== null &&
from !== undefined &&
to !== undefined &&
// Filter out errors that are not from the top-level module.
moduleId === TOP_LEVEL_MODULE_ID
)
})
.sort((a, b) => {
const c = a.sourceRanges[0][0]
const d = b.sourceRanges[0][0]
const c = a.sourceRange[0]
const d = b.sourceRange[0]
switch (true) {
case c < d:
return -1
@ -137,17 +126,48 @@ export function lspDiagnosticsToKclErrors(
export function kclErrorsToDiagnostics(
errors: KCLError[]
): CodeMirrorDiagnostic[] {
return errors?.flatMap((err) => {
const sourceRanges: CodeMirrorDiagnostic[] = err.sourceRanges
// Filter out errors that are not from the top-level module.
.filter(([_start, _end, moduleId]) => moduleId === TOP_LEVEL_MODULE_ID)
.map(([from, to]) => {
return { from, to, message: err.msg, severity: 'error' }
})
// Make sure we didn't filter out all the source ranges.
if (sourceRanges.length === 0) {
sourceRanges.push({ from: 0, to: 0, message: err.msg, severity: 'error' })
return errors
?.filter((err) => err.sourceRange[2])
.map((err) => {
return {
from: err.sourceRange[0],
to: err.sourceRange[1],
message: err.msg,
severity: 'error',
}
})
}
export function complilationErrorsToDiagnostics(
errors: CompilationError[]
): CodeMirrorDiagnostic[] {
return errors
?.filter((err) => err.sourceRange[2] === 0)
.map((err) => {
let severity: any = 'error'
if (err.severity === 'Warning') {
severity = 'warning'
}
let actions
const suggestion = err.suggestion
if (suggestion) {
actions = [
{
name: suggestion.title,
apply: (view: EditorView, from: number, to: number) => {
view.dispatch({
changes: { from, to, insert: suggestion.insert },
})
},
},
]
}
return {
from: err.sourceRange[0],
to: err.sourceRange[1],
message: err.message,
severity,
actions,
}
return sourceRanges
})
}

View File

@ -1,7 +1,7 @@
import fs from 'node:fs'
import {
parse,
assertParse,
ProgramMemory,
Sketch,
initPromise,
@ -472,7 +472,7 @@ describe('Testing Errors', () => {
const theExtrude = startSketchOn('XY')
|> startProfileAt([0, 0], %)
|> line([-2.4, 5], %)
|> line([-0.76], myVarZ, %)
|> line(myVarZ, %)
|> line([5,5], %)
|> close(%)
|> extrude(4, %)`
@ -480,7 +480,7 @@ const theExtrude = startSketchOn('XY')
new KCLError(
'undefined_value',
'memory item key `myVarZ` is not defined',
[[129, 135, 0]]
[129, 135, true]
)
)
})
@ -492,7 +492,7 @@ async function exe(
code: string,
programMemory: ProgramMemory = ProgramMemory.empty()
) {
const ast = parse(code)
const ast = assertParse(code)
const execState = await enginelessExecutor(ast, programMemory)
return execState.memory

View File

@ -1,5 +1,5 @@
import { getNodePathFromSourceRange, getNodeFromPath } from './queryAst'
import { Identifier, parse, initPromise, Parameter } from './wasm'
import { Identifier, assertParse, initPromise, Parameter } from './wasm'
import { err } from 'lib/trap'
beforeAll(async () => {
@ -17,19 +17,19 @@ const sk3 = startSketchAt([0, 0])
`
const subStr = 'lineTo([3, 4], %, $yo)'
const lineToSubstringIndex = code.indexOf(subStr)
const sourceRange: [number, number] = [
const sourceRange: [number, number, boolean] = [
lineToSubstringIndex,
lineToSubstringIndex + subStr.length,
true,
]
const ast = parse(code)
if (err(ast)) throw ast
const ast = assertParse(code)
const nodePath = getNodePathFromSourceRange(ast, sourceRange)
const _node = getNodeFromPath<any>(ast, nodePath)
if (err(_node)) throw _node
const { node } = _node
expect([node.start, node.end]).toEqual(sourceRange)
expect([node.start, node.end, true]).toEqual(sourceRange)
expect(node.type).toBe('CallExpression')
})
it('gets path right for function definition params', () => {
@ -45,13 +45,13 @@ const sk3 = startSketchAt([0, 0])
const b1 = cube([0,0], 10)`
const subStr = 'pos, scale'
const subStrIndex = code.indexOf(subStr)
const sourceRange: [number, number] = [
const sourceRange: [number, number, boolean] = [
subStrIndex,
subStrIndex + 'pos'.length,
true,
]
const ast = parse(code)
if (err(ast)) throw ast
const ast = assertParse(code)
const nodePath = getNodePathFromSourceRange(ast, sourceRange)
const _node = getNodeFromPath<Parameter>(ast, nodePath)
if (err(_node)) throw _node
@ -82,13 +82,13 @@ const b1 = cube([0,0], 10)`
const b1 = cube([0,0], 10)`
const subStr = 'scale, 0'
const subStrIndex = code.indexOf(subStr)
const sourceRange: [number, number] = [
const sourceRange: [number, number, boolean] = [
subStrIndex,
subStrIndex + 'scale'.length,
true,
]
const ast = parse(code)
if (err(ast)) throw ast
const ast = assertParse(code)
const nodePath = getNodePathFromSourceRange(ast, sourceRange)
const _node = getNodeFromPath<Identifier>(ast, nodePath)
if (err(_node)) throw _node

View File

@ -1,6 +1,5 @@
import { parse, initPromise, programMemoryInit } from './wasm'
import { assertParse, initPromise, programMemoryInit } from './wasm'
import { enginelessExecutor } from '../lib/testHelpers'
import { assert } from 'vitest'
// These unit tests makes web requests to a public github repository.
interface KclSampleFile {
@ -58,8 +57,7 @@ describe('Test KCL Samples from public Github repository', () => {
files.forEach((file: KclSampleFile) => {
it(`should parse ${file.filename} without errors`, async () => {
const code = await getKclSampleCodeFromGithub(file.filename)
const parsed = parse(code)
assert(!(parsed instanceof Error))
assertParse(code)
}, 1000)
})
})
@ -71,9 +69,8 @@ describe('Test KCL Samples from public Github repository', () => {
for (let i = 0; i < files.length; i++) {
const file: KclSampleFile = files[i]
const code = await getKclSampleCodeFromGithub(file.filename)
const parsed = parse(code)
assert(!(parsed instanceof Error))
await enginelessExecutor(parsed, programMemoryInit())
const ast = assertParse(code)
await enginelessExecutor(ast, programMemoryInit())
}
},
files.length * 1000

View File

@ -1,4 +1,4 @@
import { parse, recast, initPromise, Identifier } from './wasm'
import { assertParse, recast, initPromise, Identifier } from './wasm'
import {
createLiteral,
createIdentifier,
@ -146,10 +146,13 @@ function giveSketchFnCallTagTestHelper(
// giveSketchFnCallTag inputs and outputs an ast, which is very verbose for testing
// this wrapper changes the input and output to code
// making it more of an integration test, but easier to read the test intention is the goal
const ast = parse(code)
if (err(ast)) throw ast
const ast = assertParse(code)
const start = code.indexOf(searchStr)
const range: [number, number] = [start, start + searchStr.length]
const range: [number, number, boolean] = [
start,
start + searchStr.length,
true,
]
const sketchRes = giveSketchFnCallTag(ast, range)
if (err(sketchRes)) throw sketchRes
const { modifiedAst, tag, isTagExisting } = sketchRes
@ -221,14 +224,13 @@ part001 = startSketchOn('XY')
|> angledLine([jkl(yo) + 2, 3.09], %)
yo2 = hmm([identifierGuy + 5])`
it('should move a binary expression into a new variable', async () => {
const ast = parse(code)
if (err(ast)) throw ast
const ast = assertParse(code)
const execState = await enginelessExecutor(ast)
const startIndex = code.indexOf('100 + 100') + 1
const { modifiedAst } = moveValueIntoNewVariable(
ast,
execState.memory,
[startIndex, startIndex],
[startIndex, startIndex, true],
'newVar'
)
const newCode = recast(modifiedAst)
@ -236,14 +238,13 @@ yo2 = hmm([identifierGuy + 5])`
expect(newCode).toContain(`angledLine([newVar, 3.09], %)`)
})
it('should move a value into a new variable', async () => {
const ast = parse(code)
if (err(ast)) throw ast
const ast = assertParse(code)
const execState = await enginelessExecutor(ast)
const startIndex = code.indexOf('2.8') + 1
const { modifiedAst } = moveValueIntoNewVariable(
ast,
execState.memory,
[startIndex, startIndex],
[startIndex, startIndex, true],
'newVar'
)
const newCode = recast(modifiedAst)
@ -251,14 +252,13 @@ yo2 = hmm([identifierGuy + 5])`
expect(newCode).toContain(`line([newVar, 0], %)`)
})
it('should move a callExpression into a new variable', async () => {
const ast = parse(code)
if (err(ast)) throw ast
const ast = assertParse(code)
const execState = await enginelessExecutor(ast)
const startIndex = code.indexOf('def(')
const { modifiedAst } = moveValueIntoNewVariable(
ast,
execState.memory,
[startIndex, startIndex],
[startIndex, startIndex, true],
'newVar'
)
const newCode = recast(modifiedAst)
@ -266,14 +266,13 @@ yo2 = hmm([identifierGuy + 5])`
expect(newCode).toContain(`angledLine([newVar, 3.09], %)`)
})
it('should move a binary expression with call expression into a new variable', async () => {
const ast = parse(code)
if (err(ast)) throw ast
const ast = assertParse(code)
const execState = await enginelessExecutor(ast)
const startIndex = code.indexOf('jkl(') + 1
const { modifiedAst } = moveValueIntoNewVariable(
ast,
execState.memory,
[startIndex, startIndex],
[startIndex, startIndex, true],
'newVar'
)
const newCode = recast(modifiedAst)
@ -281,14 +280,13 @@ yo2 = hmm([identifierGuy + 5])`
expect(newCode).toContain(`angledLine([newVar, 3.09], %)`)
})
it('should move a identifier into a new variable', async () => {
const ast = parse(code)
if (err(ast)) throw ast
const ast = assertParse(code)
const execState = await enginelessExecutor(ast)
const startIndex = code.indexOf('identifierGuy +') + 1
const { modifiedAst } = moveValueIntoNewVariable(
ast,
execState.memory,
[startIndex, startIndex],
[startIndex, startIndex, true],
'newVar'
)
const newCode = recast(modifiedAst)
@ -305,19 +303,20 @@ describe('testing sketchOnExtrudedFace', () => {
|> line([8.62, -9.57], %)
|> close(%)
|> extrude(5 + 7, %)`
const ast = parse(code)
if (err(ast)) throw ast
const ast = assertParse(code)
const segmentSnippet = `line([9.7, 9.19], %)`
const segmentRange: [number, number] = [
const segmentRange: [number, number, boolean] = [
code.indexOf(segmentSnippet),
code.indexOf(segmentSnippet) + segmentSnippet.length,
true,
]
const segmentPathToNode = getNodePathFromSourceRange(ast, segmentRange)
const extrudeSnippet = `extrude(5 + 7, %)`
const extrudeRange: [number, number] = [
const extrudeRange: [number, number, boolean] = [
code.indexOf(extrudeSnippet),
code.indexOf(extrudeSnippet) + extrudeSnippet.length,
true,
]
const extrudePathToNode = getNodePathFromSourceRange(ast, extrudeRange)
@ -345,18 +344,19 @@ sketch001 = startSketchOn(part001, seg01)`)
|> line([8.62, -9.57], %)
|> close(%)
|> extrude(5 + 7, %)`
const ast = parse(code)
if (err(ast)) throw ast
const ast = assertParse(code)
const segmentSnippet = `close(%)`
const segmentRange: [number, number] = [
const segmentRange: [number, number, boolean] = [
code.indexOf(segmentSnippet),
code.indexOf(segmentSnippet) + segmentSnippet.length,
true,
]
const segmentPathToNode = getNodePathFromSourceRange(ast, segmentRange)
const extrudeSnippet = `extrude(5 + 7, %)`
const extrudeRange: [number, number] = [
const extrudeRange: [number, number, boolean] = [
code.indexOf(extrudeSnippet),
code.indexOf(extrudeSnippet) + extrudeSnippet.length,
true,
]
const extrudePathToNode = getNodePathFromSourceRange(ast, extrudeRange)
@ -384,18 +384,19 @@ sketch001 = startSketchOn(part001, seg01)`)
|> line([8.62, -9.57], %)
|> close(%)
|> extrude(5 + 7, %)`
const ast = parse(code)
if (err(ast)) throw ast
const ast = assertParse(code)
const sketchSnippet = `startProfileAt([3.58, 2.06], %)`
const sketchRange: [number, number] = [
const sketchRange: [number, number, boolean] = [
code.indexOf(sketchSnippet),
code.indexOf(sketchSnippet) + sketchSnippet.length,
true,
]
const sketchPathToNode = getNodePathFromSourceRange(ast, sketchRange)
const extrudeSnippet = `extrude(5 + 7, %)`
const extrudeRange: [number, number] = [
const extrudeRange: [number, number, boolean] = [
code.indexOf(extrudeSnippet),
code.indexOf(extrudeSnippet) + extrudeSnippet.length,
true,
]
const extrudePathToNode = getNodePathFromSourceRange(ast, extrudeRange)
@ -432,18 +433,19 @@ sketch001 = startSketchOn(part001, 'END')`)
|> line([-17.67, 0.85], %)
|> close(%)
part001 = extrude(5 + 7, sketch001)`
const ast = parse(code)
if (err(ast)) throw ast
const ast = assertParse(code)
const segmentSnippet = `line([4.99, -0.46], %)`
const segmentRange: [number, number] = [
const segmentRange: [number, number, boolean] = [
code.indexOf(segmentSnippet),
code.indexOf(segmentSnippet) + segmentSnippet.length,
true,
]
const segmentPathToNode = getNodePathFromSourceRange(ast, segmentRange)
const extrudeSnippet = `extrude(5 + 7, sketch001)`
const extrudeRange: [number, number] = [
const extrudeRange: [number, number, boolean] = [
code.indexOf(extrudeSnippet),
code.indexOf(extrudeSnippet) + extrudeSnippet.length,
true,
]
const extrudePathToNode = getNodePathFromSourceRange(ast, extrudeRange)
@ -466,13 +468,13 @@ describe('Testing deleteSegmentFromPipeExpression', () => {
|> line([306.21, 198.82], %)
|> line([306.21, 198.85], %, $a)
|> line([306.21, 198.87], %)`
const ast = parse(code)
if (err(ast)) throw ast
const ast = assertParse(code)
const execState = await enginelessExecutor(ast)
const lineOfInterest = 'line([306.21, 198.85], %, $a)'
const range: [number, number] = [
const range: [number, number, boolean] = [
code.indexOf(lineOfInterest),
code.indexOf(lineOfInterest) + lineOfInterest.length,
true,
]
const pathToNode = getNodePathFromSourceRange(ast, range)
const modifiedAst = deleteSegmentFromPipeExpression(
@ -544,13 +546,13 @@ ${!replace1 ? ` |> ${line}\n` : ''} |> angledLine([-65, ${
],
])(`%s`, async (_, line, [replace1, replace2]) => {
const code = makeCode(line)
const ast = parse(code)
if (err(ast)) throw ast
const ast = assertParse(code)
const execState = await enginelessExecutor(ast)
const lineOfInterest = line
const range: [number, number] = [
const range: [number, number, boolean] = [
code.indexOf(lineOfInterest),
code.indexOf(lineOfInterest) + lineOfInterest.length,
true,
]
const pathToNode = getNodePathFromSourceRange(ast, range)
const dependentSegments = findUsesOfTagInPipe(ast, pathToNode)
@ -632,14 +634,14 @@ describe('Testing removeSingleConstraintInfo', () => {
],
['tangentialArcTo([3.14 + 0, 13.14], %)', 'arrayIndex', 1],
] as const)('stdlib fn: %s', async (expectedFinish, key, value) => {
const ast = parse(code)
if (err(ast)) throw ast
const ast = assertParse(code)
const execState = await enginelessExecutor(ast)
const lineOfInterest = expectedFinish.split('(')[0] + '('
const range: [number, number] = [
const range: [number, number, boolean] = [
code.indexOf(lineOfInterest) + 1,
code.indexOf(lineOfInterest) + lineOfInterest.length,
true,
]
const pathToNode = getNodePathFromSourceRange(ast, range)
let argPosition: SimplifiedArgDetails
@ -686,14 +688,14 @@ describe('Testing removeSingleConstraintInfo', () => {
['angledLineToX([12.14 + 0, 12], %)', 'arrayIndex', 1],
['angledLineToY([30, 10.14 + 0], %)', 'arrayIndex', 0],
])('stdlib fn: %s', async (expectedFinish, key, value) => {
const ast = parse(code)
if (err(ast)) throw ast
const ast = assertParse(code)
const execState = await enginelessExecutor(ast)
const lineOfInterest = expectedFinish.split('(')[0] + '('
const range: [number, number] = [
const range: [number, number, boolean] = [
code.indexOf(lineOfInterest) + 1,
code.indexOf(lineOfInterest) + lineOfInterest.length,
true,
]
let argPosition: SimplifiedArgDetails
if (key === 'arrayIndex' && typeof value === 'number') {
@ -883,14 +885,14 @@ sketch002 = startSketchOn({
'%s',
async (name, { codeBefore, codeAfter, lineOfInterest, type }) => {
// const lineOfInterest = 'line([-2.94, 2.7], %)'
const ast = parse(codeBefore)
if (err(ast)) throw ast
const ast = assertParse(codeBefore)
const execState = await enginelessExecutor(ast)
// deleteFromSelection
const range: [number, number] = [
const range: [number, number, boolean] = [
codeBefore.indexOf(lineOfInterest),
codeBefore.indexOf(lineOfInterest) + lineOfInterest.length,
true,
]
const artifact = { type } as Artifact
const newAst = await deleteFromSelection(

View File

@ -1,5 +1,5 @@
import {
parse,
assertParse,
recast,
initPromise,
PathToNode,
@ -78,9 +78,10 @@ const runGetPathToExtrudeForSegmentSelectionTest = async (
code: string,
expectedExtrudeSnippet: string
): CallExpression | PipeExpression | Error {
const extrudeRange: [number, number] = [
const extrudeRange: [number, number, boolean] = [
code.indexOf(expectedExtrudeSnippet),
code.indexOf(expectedExtrudeSnippet) + expectedExtrudeSnippet.length,
true,
]
const expectedExtrudePath = getNodePathFromSourceRange(ast, extrudeRange)
const expectedExtrudeNodeResult = getNodeFromPath<
@ -109,14 +110,13 @@ const runGetPathToExtrudeForSegmentSelectionTest = async (
}
// ast
const astOrError = parse(code)
if (err(astOrError)) return new Error('AST not found')
const ast = astOrError
const ast = assertParse(code)
// selection
const segmentRange: [number, number] = [
const segmentRange: [number, number, boolean] = [
code.indexOf(selectedSegmentSnippet),
code.indexOf(selectedSegmentSnippet) + selectedSegmentSnippet.length,
true,
]
const selection: Selections = {
graphSelections: [
@ -263,17 +263,14 @@ const runModifyAstCloneWithEdgeTreatmentAndTag = async (
expectedCode: string
) => {
// ast
const astOrError = parse(code)
if (err(astOrError)) {
return new Error('AST not found')
}
const ast = astOrError
const ast = assertParse(code)
// selection
const segmentRanges: Array<[number, number]> = selectionSnippets.map(
const segmentRanges: Array<[number, number, boolean]> = selectionSnippets.map(
(selectionSnippet) => [
code.indexOf(selectionSnippet),
code.indexOf(selectionSnippet) + selectionSnippet.length,
true,
]
)
@ -603,12 +600,12 @@ extrude001 = extrude(-5, sketch001)
}, %)
`
it('should correctly identify getOppositeEdge and baseEdge edges', () => {
const ast = parse(code)
if (err(ast)) return
const ast = assertParse(code)
const lineOfInterest = `line([7.11, 3.48], %, $seg01)`
const range: [number, number] = [
const range: [number, number, boolean] = [
code.indexOf(lineOfInterest),
code.indexOf(lineOfInterest) + lineOfInterest.length,
true,
]
const pathToNode = getNodePathFromSourceRange(ast, range)
if (err(pathToNode)) return
@ -622,12 +619,12 @@ extrude001 = extrude(-5, sketch001)
expect(edges).toEqual(['getOppositeEdge', 'baseEdge'])
})
it('should correctly identify getPreviousAdjacentEdge edges', () => {
const ast = parse(code)
if (err(ast)) return
const ast = assertParse(code)
const lineOfInterest = `line([-6.37, 3.88], %, $seg02)`
const range: [number, number] = [
const range: [number, number, boolean] = [
code.indexOf(lineOfInterest),
code.indexOf(lineOfInterest) + lineOfInterest.length,
true,
]
const pathToNode = getNodePathFromSourceRange(ast, range)
if (err(pathToNode)) return
@ -641,12 +638,12 @@ extrude001 = extrude(-5, sketch001)
expect(edges).toEqual(['getPreviousAdjacentEdge'])
})
it('should correctly identify no edges', () => {
const ast = parse(code)
if (err(ast)) return
const ast = assertParse(code)
const lineOfInterest = `line([-3.29, -13.85], %)`
const range: [number, number] = [
const range: [number, number, boolean] = [
code.indexOf(lineOfInterest),
code.indexOf(lineOfInterest) + lineOfInterest.length,
true,
]
const pathToNode = getNodePathFromSourceRange(ast, range)
if (err(pathToNode)) return
@ -667,19 +664,15 @@ describe('Testing button states', () => {
segmentSnippet: string,
expectedState: boolean
) => {
// ast
const astOrError = parse(code)
if (err(astOrError)) {
return new Error('AST not found')
}
const ast = astOrError
const ast = assertParse(code)
const range: [number, number] = segmentSnippet
const range: [number, number, boolean] = segmentSnippet
? [
code.indexOf(segmentSnippet),
code.indexOf(segmentSnippet) + segmentSnippet.length,
true,
]
: [ast.end, ast.end] // empty line in the end of the code
: [ast.end, ast.end, true] // empty line in the end of the code
const selectionRanges: Selections = {
graphSelections: [

View File

@ -1,4 +1,10 @@
import { parse, recast, initPromise, PathToNode, Identifier } from './wasm'
import {
assertParse,
recast,
initPromise,
PathToNode,
Identifier,
} from './wasm'
import {
findAllPreviousVariables,
isNodeSafeToReplace,
@ -45,14 +51,13 @@ part001 = startSketchOn('XY')
variableBelowShouldNotBeIncluded = 3
`
const rangeStart = code.indexOf('// selection-range-7ish-before-this') - 7
const ast = parse(code)
if (err(ast)) throw ast
const ast = assertParse(code)
const execState = await enginelessExecutor(ast)
const { variables, bodyPath, insertIndex } = findAllPreviousVariables(
ast,
execState.memory,
[rangeStart, rangeStart]
[rangeStart, rangeStart, true]
)
expect(variables).toEqual([
{ key: 'baseThick', value: 1 },
@ -80,10 +85,9 @@ describe('testing argIsNotIdentifier', () => {
yo = 5 + 6
yo2 = hmm([identifierGuy + 5])`
it('find a safe binaryExpression', () => {
const ast = parse(code)
if (err(ast)) throw ast
const ast = assertParse(code)
const rangeStart = code.indexOf('100 + 100') + 2
const result = isNodeSafeToReplace(ast, [rangeStart, rangeStart])
const result = isNodeSafeToReplace(ast, [rangeStart, rangeStart, true])
if (err(result)) throw result
expect(result.isSafe).toBe(true)
expect(result.value?.type).toBe('BinaryExpression')
@ -94,20 +98,18 @@ yo2 = hmm([identifierGuy + 5])`
expect(outCode).toContain(`angledLine([replaceName, 3.09], %)`)
})
it('find a safe Identifier', () => {
const ast = parse(code)
if (err(ast)) throw ast
const ast = assertParse(code)
const rangeStart = code.indexOf('abc')
const result = isNodeSafeToReplace(ast, [rangeStart, rangeStart])
const result = isNodeSafeToReplace(ast, [rangeStart, rangeStart, true])
if (err(result)) throw result
expect(result.isSafe).toBe(true)
expect(result.value?.type).toBe('Identifier')
expect(code.slice(result.value.start, result.value.end)).toBe('abc')
})
it('find a safe CallExpression', () => {
const ast = parse(code)
if (err(ast)) throw ast
const ast = assertParse(code)
const rangeStart = code.indexOf('def')
const result = isNodeSafeToReplace(ast, [rangeStart, rangeStart])
const result = isNodeSafeToReplace(ast, [rangeStart, rangeStart, true])
if (err(result)) throw result
expect(result.isSafe).toBe(true)
expect(result.value?.type).toBe('CallExpression')
@ -118,10 +120,9 @@ yo2 = hmm([identifierGuy + 5])`
expect(outCode).toContain(`angledLine([replaceName, 3.09], %)`)
})
it('find an UNsafe CallExpression, as it has a PipeSubstitution', () => {
const ast = parse(code)
if (err(ast)) throw ast
const ast = assertParse(code)
const rangeStart = code.indexOf('ghi')
const range: [number, number] = [rangeStart, rangeStart]
const range: [number, number, boolean] = [rangeStart, rangeStart, true]
const result = isNodeSafeToReplace(ast, range)
if (err(result)) throw result
expect(result.isSafe).toBe(false)
@ -129,10 +130,9 @@ yo2 = hmm([identifierGuy + 5])`
expect(code.slice(result.value.start, result.value.end)).toBe('ghi(%)')
})
it('find an UNsafe Identifier, as it is a callee', () => {
const ast = parse(code)
if (err(ast)) throw ast
const ast = assertParse(code)
const rangeStart = code.indexOf('ine([2.8,')
const result = isNodeSafeToReplace(ast, [rangeStart, rangeStart])
const result = isNodeSafeToReplace(ast, [rangeStart, rangeStart, true])
if (err(result)) throw result
expect(result.isSafe).toBe(false)
expect(result.value?.type).toBe('CallExpression')
@ -141,10 +141,9 @@ yo2 = hmm([identifierGuy + 5])`
)
})
it("find a safe BinaryExpression that's assigned to a variable", () => {
const ast = parse(code)
if (err(ast)) throw ast
const ast = assertParse(code)
const rangeStart = code.indexOf('5 + 6') + 1
const result = isNodeSafeToReplace(ast, [rangeStart, rangeStart])
const result = isNodeSafeToReplace(ast, [rangeStart, rangeStart, true])
if (err(result)) throw result
expect(result.isSafe).toBe(true)
expect(result.value?.type).toBe('BinaryExpression')
@ -155,10 +154,9 @@ yo2 = hmm([identifierGuy + 5])`
expect(outCode).toContain(`yo = replaceName`)
})
it('find a safe BinaryExpression that has a CallExpression within', () => {
const ast = parse(code)
if (err(ast)) throw ast
const ast = assertParse(code)
const rangeStart = code.indexOf('jkl') + 1
const result = isNodeSafeToReplace(ast, [rangeStart, rangeStart])
const result = isNodeSafeToReplace(ast, [rangeStart, rangeStart, true])
if (err(result)) throw result
expect(result.isSafe).toBe(true)
expect(result.value?.type).toBe('BinaryExpression')
@ -172,11 +170,10 @@ yo2 = hmm([identifierGuy + 5])`
expect(outCode).toContain(`angledLine([replaceName, 3.09], %)`)
})
it('find a safe BinaryExpression within a CallExpression', () => {
const ast = parse(code)
if (err(ast)) throw ast
const ast = assertParse(code)
const rangeStart = code.indexOf('identifierGuy') + 1
const result = isNodeSafeToReplace(ast, [rangeStart, rangeStart])
const result = isNodeSafeToReplace(ast, [rangeStart, rangeStart, true])
if (err(result)) throw result
expect(result.isSafe).toBe(true)
@ -223,10 +220,13 @@ describe('testing getNodePathFromSourceRange', () => {
it('finds the second line when cursor is put at the end', () => {
const searchLn = `line([0.94, 2.61], %)`
const sourceIndex = code.indexOf(searchLn) + searchLn.length
const ast = parse(code)
if (err(ast)) throw ast
const ast = assertParse(code)
const result = getNodePathFromSourceRange(ast, [sourceIndex, sourceIndex])
const result = getNodePathFromSourceRange(ast, [
sourceIndex,
sourceIndex,
true,
])
expect(result).toEqual([
['body', ''],
[0, 'index'],
@ -240,10 +240,13 @@ describe('testing getNodePathFromSourceRange', () => {
it('finds the last line when cursor is put at the end', () => {
const searchLn = `line([-0.21, -1.4], %)`
const sourceIndex = code.indexOf(searchLn) + searchLn.length
const ast = parse(code)
if (err(ast)) throw ast
const ast = assertParse(code)
const result = getNodePathFromSourceRange(ast, [sourceIndex, sourceIndex])
const result = getNodePathFromSourceRange(ast, [
sourceIndex,
sourceIndex,
true,
])
const expected = [
['body', ''],
[0, 'index'],
@ -259,12 +262,14 @@ describe('testing getNodePathFromSourceRange', () => {
const startResult = getNodePathFromSourceRange(ast, [
startSourceIndex,
startSourceIndex,
true,
])
expect(startResult).toEqual([...expected, ['callee', 'CallExpression']])
// expect similar result when whole line is selected
const selectWholeThing = getNodePathFromSourceRange(ast, [
startSourceIndex,
sourceIndex,
true,
])
expect(selectWholeThing).toEqual(expected)
})
@ -278,10 +283,13 @@ describe('testing getNodePathFromSourceRange', () => {
}`
const searchLn = `x > y`
const sourceIndex = code.indexOf(searchLn)
const ast = parse(code)
if (err(ast)) throw ast
const ast = assertParse(code)
const result = getNodePathFromSourceRange(ast, [sourceIndex, sourceIndex])
const result = getNodePathFromSourceRange(ast, [
sourceIndex,
sourceIndex,
true,
])
expect(result).toEqual([
['body', ''],
[1, 'index'],
@ -306,10 +314,13 @@ describe('testing getNodePathFromSourceRange', () => {
}`
const searchLn = `x + 1`
const sourceIndex = code.indexOf(searchLn)
const ast = parse(code)
if (err(ast)) throw ast
const ast = assertParse(code)
const result = getNodePathFromSourceRange(ast, [sourceIndex, sourceIndex])
const result = getNodePathFromSourceRange(ast, [
sourceIndex,
sourceIndex,
true,
])
expect(result).toEqual([
['body', ''],
[1, 'index'],
@ -332,10 +343,13 @@ describe('testing getNodePathFromSourceRange', () => {
const code = `import foo, bar as baz from 'thing.kcl'`
const searchLn = `bar`
const sourceIndex = code.indexOf(searchLn)
const ast = parse(code)
if (err(ast)) throw ast
const ast = assertParse(code)
const result = getNodePathFromSourceRange(ast, [sourceIndex, sourceIndex])
const result = getNodePathFromSourceRange(ast, [
sourceIndex,
sourceIndex,
true,
])
expect(result).toEqual([
['body', ''],
[0, 'index'],
@ -360,14 +374,13 @@ part001 = startSketchAt([-1.41, 3.46])
|> angledLine([-175, segLen(seg01)], %)
|> close(%)
`
const ast = parse(exampleCode)
if (err(ast)) throw ast
const ast = assertParse(exampleCode)
const result = doesPipeHaveCallExp({
calleeName: 'close',
ast,
selection: {
codeRef: codeRefFromRange([100, 101], ast),
codeRef: codeRefFromRange([100, 101, true], ast),
},
})
expect(result).toEqual(true)
@ -382,14 +395,13 @@ part001 = startSketchAt([-1.41, 3.46])
|> close(%)
|> extrude(1, %)
`
const ast = parse(exampleCode)
if (err(ast)) throw ast
const ast = assertParse(exampleCode)
const result = doesPipeHaveCallExp({
calleeName: 'extrude',
ast,
selection: {
codeRef: codeRefFromRange([100, 101], ast),
codeRef: codeRefFromRange([100, 101, true], ast),
},
})
expect(result).toEqual(true)
@ -402,28 +414,26 @@ part001 = startSketchAt([-1.41, 3.46])
|> line([-3.22, -7.36], %)
|> angledLine([-175, segLen(seg01)], %)
`
const ast = parse(exampleCode)
if (err(ast)) throw ast
const ast = assertParse(exampleCode)
const result = doesPipeHaveCallExp({
calleeName: 'close',
ast,
selection: {
codeRef: codeRefFromRange([100, 101], ast),
codeRef: codeRefFromRange([100, 101, true], ast),
},
})
expect(result).toEqual(false)
})
it('returns false if not a pipe', () => {
const exampleCode = `length001 = 2`
const ast = parse(exampleCode)
if (err(ast)) throw ast
const ast = assertParse(exampleCode)
const result = doesPipeHaveCallExp({
calleeName: 'close',
ast,
selection: {
codeRef: codeRefFromRange([9, 10], ast),
codeRef: codeRefFromRange([9, 10, true], ast),
},
})
expect(result).toEqual(false)
@ -438,14 +448,13 @@ part001 = startSketchAt([-1.41, 3.46])
|> angledLine([-35, length001], %)
|> line([-3.22, -7.36], %)
|> angledLine([-175, segLen(seg01)], %)`
const ast = parse(exampleCode)
if (err(ast)) throw ast
const ast = assertParse(exampleCode)
const execState = await enginelessExecutor(ast)
const result = hasExtrudeSketch({
ast,
selection: {
codeRef: codeRefFromRange([100, 101], ast),
codeRef: codeRefFromRange([100, 101, true], ast),
},
programMemory: execState.memory,
})
@ -459,14 +468,13 @@ part001 = startSketchAt([-1.41, 3.46])
|> line([-3.22, -7.36], %)
|> angledLine([-175, segLen(seg01)], %)
|> extrude(1, %)`
const ast = parse(exampleCode)
if (err(ast)) throw ast
const ast = assertParse(exampleCode)
const execState = await enginelessExecutor(ast)
const result = hasExtrudeSketch({
ast,
selection: {
codeRef: codeRefFromRange([100, 101], ast),
codeRef: codeRefFromRange([100, 101, true], ast),
},
programMemory: execState.memory,
})
@ -474,14 +482,13 @@ part001 = startSketchAt([-1.41, 3.46])
})
it('finds nothing', async () => {
const exampleCode = `length001 = 2`
const ast = parse(exampleCode)
if (err(ast)) throw ast
const ast = assertParse(exampleCode)
const execState = await enginelessExecutor(ast)
const result = hasExtrudeSketch({
ast,
selection: {
codeRef: codeRefFromRange([10, 11], ast),
codeRef: codeRefFromRange([10, 11, true], ast),
},
programMemory: execState.memory,
})
@ -498,8 +505,7 @@ describe('Testing findUsesOfTagInPipe', () => {
|> line([306.21, 198.87], %)
|> angledLine([65, segLen(seg01)], %)`
it('finds the current segment', async () => {
const ast = parse(exampleCode)
if (err(ast)) throw ast
const ast = assertParse(exampleCode)
const lineOfInterest = `198.85], %, $seg01`
const characterIndex =
@ -507,6 +513,7 @@ describe('Testing findUsesOfTagInPipe', () => {
const pathToNode = getNodePathFromSourceRange(ast, [
characterIndex,
characterIndex,
true,
])
const result = findUsesOfTagInPipe(ast, pathToNode)
expect(result).toHaveLength(2)
@ -515,8 +522,7 @@ describe('Testing findUsesOfTagInPipe', () => {
})
})
it('find no tag if line has no tag', () => {
const ast = parse(exampleCode)
if (err(ast)) throw ast
const ast = assertParse(exampleCode)
const lineOfInterest = `line([306.21, 198.82], %)`
const characterIndex =
@ -524,6 +530,7 @@ describe('Testing findUsesOfTagInPipe', () => {
const pathToNode = getNodePathFromSourceRange(ast, [
characterIndex,
characterIndex,
true,
])
const result = findUsesOfTagInPipe(ast, pathToNode)
expect(result).toHaveLength(0)
@ -564,42 +571,39 @@ sketch003 = startSketchOn(extrude001, 'END')
|> extrude(3.14, %)
`
it('identifies sketch001 pipe as extruded (extrusion after pipe)', async () => {
const ast = parse(exampleCode)
if (err(ast)) throw ast
const ast = assertParse(exampleCode)
const lineOfInterest = `line([4.99, -0.46], %, $seg01)`
const characterIndex =
exampleCode.indexOf(lineOfInterest) + lineOfInterest.length
const extruded = hasSketchPipeBeenExtruded(
{
codeRef: codeRefFromRange([characterIndex, characterIndex], ast),
codeRef: codeRefFromRange([characterIndex, characterIndex, true], ast),
},
ast
)
expect(extruded).toBeTruthy()
})
it('identifies sketch002 pipe as not extruded', async () => {
const ast = parse(exampleCode)
if (err(ast)) throw ast
const ast = assertParse(exampleCode)
const lineOfInterest = `line([2.45, -0.2], %)`
const characterIndex =
exampleCode.indexOf(lineOfInterest) + lineOfInterest.length
const extruded = hasSketchPipeBeenExtruded(
{
codeRef: codeRefFromRange([characterIndex, characterIndex], ast),
codeRef: codeRefFromRange([characterIndex, characterIndex, true], ast),
},
ast
)
expect(extruded).toBeFalsy()
})
it('identifies sketch003 pipe as extruded (extrusion within pipe)', async () => {
const ast = parse(exampleCode)
if (err(ast)) throw ast
const ast = assertParse(exampleCode)
const lineOfInterest = `|> line([3.12, 1.74], %)`
const characterIndex =
exampleCode.indexOf(lineOfInterest) + lineOfInterest.length
const extruded = hasSketchPipeBeenExtruded(
{
codeRef: codeRefFromRange([characterIndex, characterIndex], ast),
codeRef: codeRefFromRange([characterIndex, characterIndex, true], ast),
},
ast
)
@ -623,8 +627,7 @@ sketch002 = startSketchOn(extrude001, $seg01)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
`
const ast = parse(exampleCode)
if (err(ast)) throw ast
const ast = assertParse(exampleCode)
const extrudable = doesSceneHaveSweepableSketch(ast)
expect(extrudable).toBeTruthy()
})
@ -635,8 +638,7 @@ plane001 = offsetPlane('XZ', 2)
sketch002 = startSketchOn(plane001)
|> circle({ center = [0, 0], radius = 3 }, %)
`
const ast = parse(exampleCode)
if (err(ast)) throw ast
const ast = assertParse(exampleCode)
const extrudable = doesSceneHaveSweepableSketch(ast, 2)
expect(extrudable).toBeTruthy()
})
@ -649,8 +651,7 @@ sketch002 = startSketchOn(plane001)
|> close(%)
extrude001 = extrude(10, sketch001)
`
const ast = parse(exampleCode)
if (err(ast)) throw ast
const ast = assertParse(exampleCode)
const extrudable = doesSceneHaveSweepableSketch(ast)
expect(extrudable).toBeFalsy()
})
@ -678,8 +679,7 @@ myNestedVar = [
}
]
`
const ast = parse(code)
if (err(ast)) throw ast
const ast = assertParse(code)
let pathToNode: PathToNode = []
traverse(ast, {
enter: (node, path) => {
@ -701,6 +701,7 @@ myNestedVar = [
const pathToNode2 = getNodePathFromSourceRange(ast, [
literalIndex + 2,
literalIndex + 2,
true,
])
expect(pathToNode).toEqual(pathToNode2)
})

View File

@ -16,6 +16,7 @@ import {
sketchFromKclValue,
sketchFromKclValueOptional,
SourceRange,
sourceRangeFromRust,
SyntaxType,
VariableDeclaration,
VariableDeclarator,
@ -669,7 +670,7 @@ export function isNodeSafeToReplacePath(
export function isNodeSafeToReplace(
ast: Node<Program>,
sourceRange: [number, number]
sourceRange: SourceRange
):
| {
isSafe: boolean
@ -821,7 +822,7 @@ export function isLinesParallelAndConstrained(
return {
isParallelAndConstrained,
selection: {
codeRef: codeRefFromRange(prevSourceRange, ast),
codeRef: codeRefFromRange(sourceRangeFromRust(prevSourceRange), ast),
artifact: artifactGraph.get(prevSegment.__geoMeta.id),
},
}
@ -957,7 +958,8 @@ export function findUsesOfTagInPipe(
return
const tagArgValue =
tagArg.type === 'TagDeclarator' ? String(tagArg.value) : tagArg.name
if (tagArgValue === tag) dependentRanges.push([node.start, node.end])
if (tagArgValue === tag)
dependentRanges.push([node.start, node.end, true])
},
})
return dependentRanges

View File

@ -1,4 +1,4 @@
import { parse, Program, recast, initPromise } from './wasm'
import { assertParse, Program, recast, initPromise } from './wasm'
import fs from 'node:fs'
import { err } from 'lib/trap'
@ -394,8 +394,6 @@ describe('it recasts binary expression using brackets where needed', () => {
// helpers
function code2ast(code: string): { ast: Program } {
const ast = parse(code)
// eslint-ignore-next-line
if (err(ast)) throw ast
const ast = assertParse(code)
return { ast }
}

View File

@ -1,4 +1,4 @@
import { makeDefaultPlanes, parse, initPromise, Program } from 'lang/wasm'
import { makeDefaultPlanes, assertParse, initPromise, Program } from 'lang/wasm'
import { Models } from '@kittycad/lib'
import {
OrderedCommand,
@ -148,11 +148,7 @@ beforeAll(async () => {
][]
const cacheToWriteToFileTemp: Partial<CacheShape> = {}
for (const [codeKey, code] of cacheEntries) {
const ast = parse(code)
if (err(ast)) {
console.error(ast)
return Promise.reject(ast)
}
const ast = assertParse(code)
await kclManager.executeAst({ ast })
cacheToWriteToFileTemp[codeKey] = {
@ -403,11 +399,7 @@ describe('capture graph of sketchOnFaceOnFace...', () => {
})
function getCommands(codeKey: CodeKey): CacheShape[CodeKey] & { ast: Program } {
const ast = parse(codeKey)
if (err(ast)) {
console.error(ast)
throw ast
}
const ast = assertParse(codeKey)
const file = fs.readFileSync(fullPath, 'utf-8')
const parsed: CacheShape = JSON.parse(file)
// these either already exist from the last run, or were created in

View File

@ -1,4 +1,4 @@
import { SourceRange } from 'lang/wasm'
import { defaultSourceRange, SourceRange } from 'lang/wasm'
import { VITE_KC_API_WS_MODELING_URL, VITE_KC_DEV_TOKEN } from 'env'
import { Models } from '@kittycad/lib'
import { exportSave } from 'lib/exportSave'
@ -2014,7 +2014,7 @@ export class EngineCommandManager extends EventTarget {
{
command,
idToRangeMap: {},
range: [0, 0],
range: defaultSourceRange(),
},
true // isSceneCommand
)

View File

@ -8,7 +8,7 @@ import {
getConstraintInfo,
} from './sketch'
import {
parse,
assertParse,
recast,
initPromise,
SourceRange,
@ -115,8 +115,7 @@ describe('testing changeSketchArguments', () => {
`
const code = genCode(lineToChange)
const expectedCode = genCode(lineAfterChange)
const ast = parse(code)
if (err(ast)) return ast
const ast = assertParse(code)
const execState = await enginelessExecutor(ast)
const sourceStart = code.indexOf(lineToChange)
@ -125,7 +124,7 @@ describe('testing changeSketchArguments', () => {
execState.memory,
{
type: 'sourceRange',
sourceRange: [sourceStart, sourceStart + lineToChange.length],
sourceRange: [sourceStart, sourceStart + lineToChange.length, true],
},
{
type: 'straight-segment',
@ -148,8 +147,7 @@ mySketch001 = startSketchOn('XY')
// |> rx(45, %)
|> lineTo([-1.59, -1.54], %)
|> lineTo([0.46, -5.82], %)`
const ast = parse(code)
if (err(ast)) return ast
const ast = assertParse(code)
const execState = await enginelessExecutor(ast)
const sourceStart = code.indexOf(lineToChange)
@ -220,12 +218,13 @@ describe('testing addTagForSketchOnFace', () => {
|> lineTo([0.46, -5.82], %)
`
const code = genCode(originalLine)
const ast = parse(code)
const ast = assertParse(code)
await enginelessExecutor(ast)
const sourceStart = code.indexOf(originalLine)
const sourceRange: [number, number] = [
const sourceRange: [number, number, boolean] = [
sourceStart,
sourceStart + originalLine.length,
true,
]
if (err(ast)) return ast
const pathToNode = getNodePathFromSourceRange(ast, sourceRange)
@ -291,13 +290,14 @@ extrude001 = extrude(100, sketch001)
${insertCode}
`
const code = genCode(originalChamfer)
const ast = parse(code)
const ast = assertParse(code)
await enginelessExecutor(ast)
const sourceStart = code.indexOf(originalChamfer)
const extraChars = originalChamfer.indexOf('chamfer')
const sourceRange: [number, number] = [
const sourceRange: [number, number, boolean] = [
sourceStart + extraChars,
sourceStart + originalChamfer.length - extraChars,
true,
]
if (err(ast)) throw ast
@ -335,7 +335,7 @@ describe('testing getConstraintInfo', () => {
|> lineTo([6.14, 3.14], %)
|> xLineTo(8, %)
|> yLineTo(5, %)
|> yLine(3.14, %, 'a')
|> yLine(3.14, %, $a)
|> xLine(3.14, %)
|> angledLineOfXLength({
angle = 3.14,
@ -355,11 +355,11 @@ describe('testing getConstraintInfo', () => {
}, %)
|> angledLineThatIntersects({
angle = 3.14,
intersectTag = 'a',
intersectTag = a,
offset = 0
}, %)
|> tangentialArcTo([3.14, 13.14], %)`
const ast = parse(code)
const ast = assertParse(code)
test.each([
[
'line',
@ -368,7 +368,7 @@ describe('testing getConstraintInfo', () => {
type: 'xRelative',
isConstrained: false,
value: '3',
sourceRange: [78, 79],
sourceRange: [78, 79, true],
argPosition: { type: 'arrayItem', index: 0 },
pathToNode: expect.any(Array),
stdLibFnName: 'line',
@ -377,7 +377,7 @@ describe('testing getConstraintInfo', () => {
type: 'yRelative',
isConstrained: false,
value: '4',
sourceRange: [81, 82],
sourceRange: [81, 82, true],
argPosition: { type: 'arrayItem', index: 1 },
pathToNode: expect.any(Array),
stdLibFnName: 'line',
@ -391,7 +391,7 @@ describe('testing getConstraintInfo', () => {
type: 'angle',
isConstrained: false,
value: '3.14',
sourceRange: [117, 121],
sourceRange: [118, 122, true],
argPosition: { type: 'objectProperty', key: 'angle' },
pathToNode: expect.any(Array),
stdLibFnName: 'angledLine',
@ -400,7 +400,7 @@ describe('testing getConstraintInfo', () => {
type: 'length',
isConstrained: false,
value: '3.14',
sourceRange: [135, 139],
sourceRange: [137, 141, true],
argPosition: { type: 'objectProperty', key: 'length' },
pathToNode: expect.any(Array),
stdLibFnName: 'angledLine',
@ -414,7 +414,7 @@ describe('testing getConstraintInfo', () => {
type: 'xAbsolute',
isConstrained: false,
value: '6.14',
sourceRange: [162, 166],
sourceRange: [164, 168, true],
argPosition: { type: 'arrayItem', index: 0 },
pathToNode: expect.any(Array),
stdLibFnName: 'lineTo',
@ -423,7 +423,7 @@ describe('testing getConstraintInfo', () => {
type: 'yAbsolute',
isConstrained: false,
value: '3.14',
sourceRange: [168, 172],
sourceRange: [170, 174, true],
argPosition: { type: 'arrayItem', index: 1 },
pathToNode: expect.any(Array),
stdLibFnName: 'lineTo',
@ -437,7 +437,7 @@ describe('testing getConstraintInfo', () => {
type: 'horizontal',
isConstrained: true,
value: 'xLineTo',
sourceRange: [183, 190],
sourceRange: [185, 192, true],
argPosition: undefined,
pathToNode: expect.any(Array),
stdLibFnName: 'xLineTo',
@ -446,7 +446,7 @@ describe('testing getConstraintInfo', () => {
type: 'xAbsolute',
isConstrained: false,
value: '8',
sourceRange: [191, 192],
sourceRange: [193, 194, true],
argPosition: { type: 'singleValue' },
pathToNode: expect.any(Array),
stdLibFnName: 'xLineTo',
@ -460,7 +460,7 @@ describe('testing getConstraintInfo', () => {
type: 'vertical',
isConstrained: true,
value: 'yLineTo',
sourceRange: [202, 209],
sourceRange: [204, 211, true],
argPosition: undefined,
pathToNode: expect.any(Array),
stdLibFnName: 'yLineTo',
@ -469,7 +469,7 @@ describe('testing getConstraintInfo', () => {
type: 'yAbsolute',
isConstrained: false,
value: '5',
sourceRange: [210, 211],
sourceRange: [212, 213, true],
argPosition: { type: 'singleValue' },
pathToNode: expect.any(Array),
stdLibFnName: 'yLineTo',
@ -483,7 +483,7 @@ describe('testing getConstraintInfo', () => {
type: 'vertical',
isConstrained: true,
value: 'yLine',
sourceRange: [221, 226],
sourceRange: [223, 228, true],
argPosition: undefined,
pathToNode: expect.any(Array),
stdLibFnName: 'yLine',
@ -492,7 +492,7 @@ describe('testing getConstraintInfo', () => {
type: 'yRelative',
isConstrained: false,
value: '3.14',
sourceRange: [227, 231],
sourceRange: [229, 233, true],
argPosition: { type: 'singleValue' },
pathToNode: expect.any(Array),
stdLibFnName: 'yLine',
@ -506,7 +506,7 @@ describe('testing getConstraintInfo', () => {
type: 'horizontal',
isConstrained: true,
value: 'xLine',
sourceRange: [246, 251],
sourceRange: [247, 252, true],
argPosition: undefined,
pathToNode: expect.any(Array),
stdLibFnName: 'xLine',
@ -515,7 +515,7 @@ describe('testing getConstraintInfo', () => {
type: 'xRelative',
isConstrained: false,
value: '3.14',
sourceRange: [252, 256],
sourceRange: [253, 257, true],
argPosition: { type: 'singleValue' },
pathToNode: expect.any(Array),
stdLibFnName: 'xLine',
@ -529,7 +529,7 @@ describe('testing getConstraintInfo', () => {
type: 'angle',
isConstrained: false,
value: '3.14',
sourceRange: [299, 303],
sourceRange: [301, 305, true],
argPosition: { type: 'objectProperty', key: 'angle' },
pathToNode: expect.any(Array),
stdLibFnName: 'angledLineOfXLength',
@ -538,7 +538,7 @@ describe('testing getConstraintInfo', () => {
type: 'xRelative',
isConstrained: false,
value: '3.14',
sourceRange: [317, 321],
sourceRange: [320, 324, true],
argPosition: { type: 'objectProperty', key: 'length' },
pathToNode: expect.any(Array),
stdLibFnName: 'angledLineOfXLength',
@ -552,7 +552,7 @@ describe('testing getConstraintInfo', () => {
type: 'angle',
isConstrained: false,
value: '30',
sourceRange: [369, 371],
sourceRange: [373, 375, true],
argPosition: { type: 'objectProperty', key: 'angle' },
pathToNode: expect.any(Array),
stdLibFnName: 'angledLineOfYLength',
@ -561,7 +561,7 @@ describe('testing getConstraintInfo', () => {
type: 'yRelative',
isConstrained: false,
value: '3',
sourceRange: [385, 386],
sourceRange: [390, 391, true],
argPosition: { type: 'objectProperty', key: 'length' },
pathToNode: expect.any(Array),
stdLibFnName: 'angledLineOfYLength',
@ -575,7 +575,7 @@ describe('testing getConstraintInfo', () => {
type: 'angle',
isConstrained: false,
value: '12.14',
sourceRange: [428, 433],
sourceRange: [434, 439, true],
argPosition: { type: 'objectProperty', key: 'angle' },
pathToNode: expect.any(Array),
stdLibFnName: 'angledLineToX',
@ -584,7 +584,7 @@ describe('testing getConstraintInfo', () => {
type: 'xAbsolute',
isConstrained: false,
value: '12',
sourceRange: [443, 445],
sourceRange: [450, 452, true],
argPosition: { type: 'objectProperty', key: 'to' },
pathToNode: expect.any(Array),
stdLibFnName: 'angledLineToX',
@ -598,7 +598,7 @@ describe('testing getConstraintInfo', () => {
type: 'angle',
isConstrained: false,
value: '30',
sourceRange: [487, 489],
sourceRange: [495, 497, true],
argPosition: { type: 'objectProperty', key: 'angle' },
pathToNode: expect.any(Array),
stdLibFnName: 'angledLineToY',
@ -607,7 +607,7 @@ describe('testing getConstraintInfo', () => {
type: 'yAbsolute',
isConstrained: false,
value: '10.14',
sourceRange: [499, 504],
sourceRange: [508, 513, true],
argPosition: { type: 'objectProperty', key: 'to' },
pathToNode: expect.any(Array),
stdLibFnName: 'angledLineToY',
@ -621,7 +621,7 @@ describe('testing getConstraintInfo', () => {
type: 'angle',
isConstrained: false,
value: '3.14',
sourceRange: [557, 561],
sourceRange: [567, 571, true],
argPosition: { type: 'objectProperty', key: 'angle' },
pathToNode: expect.any(Array),
stdLibFnName: 'angledLineThatIntersects',
@ -630,7 +630,7 @@ describe('testing getConstraintInfo', () => {
type: 'intersectionOffset',
isConstrained: false,
value: '0',
sourceRange: [598, 599],
sourceRange: [608, 609, true],
argPosition: { type: 'objectProperty', key: 'offset' },
pathToNode: expect.any(Array),
stdLibFnName: 'angledLineThatIntersects',
@ -638,8 +638,8 @@ describe('testing getConstraintInfo', () => {
{
type: 'intersectionTag',
isConstrained: false,
value: "'a'",
sourceRange: [581, 584],
value: 'a',
sourceRange: [592, 593, true],
argPosition: {
key: 'intersectTag',
type: 'objectProperty',
@ -656,7 +656,7 @@ describe('testing getConstraintInfo', () => {
type: 'tangentialWithPrevious',
isConstrained: true,
value: 'tangentialArcTo',
sourceRange: [613, 628],
sourceRange: [623, 638, true],
argPosition: undefined,
pathToNode: expect.any(Array),
stdLibFnName: 'tangentialArcTo',
@ -665,7 +665,7 @@ describe('testing getConstraintInfo', () => {
type: 'xAbsolute',
isConstrained: false,
value: '3.14',
sourceRange: [630, 634],
sourceRange: [640, 644, true],
argPosition: { type: 'arrayItem', index: 0 },
pathToNode: expect.any(Array),
stdLibFnName: 'tangentialArcTo',
@ -674,7 +674,7 @@ describe('testing getConstraintInfo', () => {
type: 'yAbsolute',
isConstrained: false,
value: '13.14',
sourceRange: [636, 641],
sourceRange: [646, 651, true],
argPosition: { type: 'arrayItem', index: 1 },
pathToNode: expect.any(Array),
stdLibFnName: 'tangentialArcTo',
@ -685,6 +685,7 @@ describe('testing getConstraintInfo', () => {
const sourceRange: SourceRange = [
code.indexOf(functionName),
code.indexOf(functionName) + functionName.length,
true,
]
if (err(ast)) return ast
const pathToNode = getNodePathFromSourceRange(ast, sourceRange)
@ -706,7 +707,7 @@ describe('testing getConstraintInfo', () => {
|> lineTo([6.14, 3.14], %)
|> xLineTo(8, %)
|> yLineTo(5, %)
|> yLine(3.14, %, 'a')
|> yLine(3.14, %, $a)
|> xLine(3.14, %)
|> angledLineOfXLength([3.14, 3.14], %)
|> angledLineOfYLength([30, 3], %)
@ -714,11 +715,11 @@ describe('testing getConstraintInfo', () => {
|> angledLineToY([30, 10], %)
|> angledLineThatIntersects({
angle = 3.14,
intersectTag = 'a',
intersectTag = a,
offset = 0
}, %)
|> tangentialArcTo([3.14, 13.14], %)`
const ast = parse(code)
const ast = assertParse(code)
test.each([
[
`angledLine(`,
@ -727,7 +728,7 @@ describe('testing getConstraintInfo', () => {
type: 'angle',
isConstrained: false,
value: '3.14',
sourceRange: [112, 116],
sourceRange: [112, 116, true],
argPosition: { type: 'arrayItem', index: 0 },
pathToNode: expect.any(Array),
stdLibFnName: 'angledLine',
@ -736,7 +737,7 @@ describe('testing getConstraintInfo', () => {
type: 'length',
isConstrained: false,
value: '3.14',
sourceRange: [118, 122],
sourceRange: [118, 122, true],
argPosition: { type: 'arrayItem', index: 1 },
pathToNode: expect.any(Array),
stdLibFnName: 'angledLine',
@ -750,7 +751,7 @@ describe('testing getConstraintInfo', () => {
type: 'angle',
isConstrained: false,
value: '3.14',
sourceRange: [278, 282],
sourceRange: [277, 281, true],
argPosition: { type: 'arrayItem', index: 0 },
pathToNode: expect.any(Array),
stdLibFnName: 'angledLineOfXLength',
@ -759,7 +760,7 @@ describe('testing getConstraintInfo', () => {
type: 'xRelative',
isConstrained: false,
value: '3.14',
sourceRange: [284, 288],
sourceRange: [283, 287, true],
argPosition: { type: 'arrayItem', index: 1 },
pathToNode: expect.any(Array),
stdLibFnName: 'angledLineOfXLength',
@ -773,7 +774,7 @@ describe('testing getConstraintInfo', () => {
type: 'angle',
isConstrained: false,
value: '30',
sourceRange: [322, 324],
sourceRange: [321, 323, true],
argPosition: { type: 'arrayItem', index: 0 },
pathToNode: expect.any(Array),
stdLibFnName: 'angledLineOfYLength',
@ -782,7 +783,7 @@ describe('testing getConstraintInfo', () => {
type: 'yRelative',
isConstrained: false,
value: '3',
sourceRange: [326, 327],
sourceRange: [325, 326, true],
argPosition: { type: 'arrayItem', index: 1 },
pathToNode: expect.any(Array),
stdLibFnName: 'angledLineOfYLength',
@ -796,7 +797,7 @@ describe('testing getConstraintInfo', () => {
type: 'angle',
isConstrained: false,
value: '12',
sourceRange: [355, 357],
sourceRange: [354, 356, true],
argPosition: { type: 'arrayItem', index: 0 },
pathToNode: expect.any(Array),
stdLibFnName: 'angledLineToX',
@ -805,7 +806,7 @@ describe('testing getConstraintInfo', () => {
type: 'xAbsolute',
isConstrained: false,
value: '12',
sourceRange: [359, 361],
sourceRange: [358, 360, true],
argPosition: { type: 'arrayItem', index: 1 },
pathToNode: expect.any(Array),
stdLibFnName: 'angledLineToX',
@ -819,7 +820,7 @@ describe('testing getConstraintInfo', () => {
type: 'angle',
isConstrained: false,
value: '30',
sourceRange: [389, 391],
sourceRange: [388, 390, true],
argPosition: { type: 'arrayItem', index: 0 },
pathToNode: expect.any(Array),
stdLibFnName: 'angledLineToY',
@ -828,7 +829,7 @@ describe('testing getConstraintInfo', () => {
type: 'yAbsolute',
isConstrained: false,
value: '10',
sourceRange: [393, 395],
sourceRange: [392, 394, true],
argPosition: { type: 'arrayItem', index: 1 },
pathToNode: expect.any(Array),
stdLibFnName: 'angledLineToY',
@ -839,6 +840,7 @@ describe('testing getConstraintInfo', () => {
const sourceRange: SourceRange = [
code.indexOf(functionName),
code.indexOf(functionName) + functionName.length,
true,
]
if (err(ast)) return ast
const pathToNode = getNodePathFromSourceRange(ast, sourceRange)
@ -860,7 +862,7 @@ describe('testing getConstraintInfo', () => {
|> lineTo([6.14 + 0, 3.14 + 0], %)
|> xLineTo(8 + 0, %)
|> yLineTo(5 + 0, %)
|> yLine(3.14 + 0, %, 'a')
|> yLine(3.14 + 0, %, $a)
|> xLine(3.14 + 0, %)
|> angledLineOfXLength({ angle = 3.14 + 0, length = 3.14 + 0 }, %)
|> angledLineOfYLength({ angle = 30 + 0, length = 3 + 0 }, %)
@ -868,11 +870,11 @@ describe('testing getConstraintInfo', () => {
|> angledLineToY({ angle = 30 + 0, to = 10.14 + 0 }, %)
|> angledLineThatIntersects({
angle = 3.14 + 0,
intersectTag = 'a',
intersectTag = a,
offset = 0 + 0
}, %)
|> tangentialArcTo([3.14 + 0, 13.14 + 0], %)`
const ast = parse(code)
const ast = assertParse(code)
test.each([
[
'line',
@ -881,7 +883,7 @@ describe('testing getConstraintInfo', () => {
type: 'xRelative',
isConstrained: true,
value: '3 + 0',
sourceRange: [83, 88],
sourceRange: [83, 88, true],
argPosition: { type: 'arrayItem', index: 0 },
pathToNode: expect.any(Array),
stdLibFnName: 'line',
@ -890,7 +892,7 @@ describe('testing getConstraintInfo', () => {
type: 'yRelative',
isConstrained: true,
value: '4 + 0',
sourceRange: [90, 95],
sourceRange: [90, 95, true],
argPosition: { type: 'arrayItem', index: 1 },
pathToNode: expect.any(Array),
stdLibFnName: 'line',
@ -904,7 +906,7 @@ describe('testing getConstraintInfo', () => {
type: 'angle',
isConstrained: true,
value: '3.14 + 0',
sourceRange: [128, 136],
sourceRange: [129, 137, true],
argPosition: { type: 'objectProperty', key: 'angle' },
pathToNode: expect.any(Array),
stdLibFnName: 'angledLine',
@ -913,7 +915,7 @@ describe('testing getConstraintInfo', () => {
type: 'length',
isConstrained: true,
value: '3.14 + 0',
sourceRange: [146, 154],
sourceRange: [148, 156, true],
argPosition: { type: 'objectProperty', key: 'length' },
pathToNode: expect.any(Array),
stdLibFnName: 'angledLine',
@ -927,7 +929,7 @@ describe('testing getConstraintInfo', () => {
type: 'xAbsolute',
isConstrained: true,
value: '6.14 + 0',
sourceRange: [176, 184],
sourceRange: [178, 186, true],
argPosition: { type: 'arrayItem', index: 0 },
pathToNode: expect.any(Array),
stdLibFnName: 'lineTo',
@ -936,7 +938,7 @@ describe('testing getConstraintInfo', () => {
type: 'yAbsolute',
isConstrained: true,
value: '3.14 + 0',
sourceRange: [186, 194],
sourceRange: [188, 196, true],
argPosition: { type: 'arrayItem', index: 1 },
pathToNode: expect.any(Array),
stdLibFnName: 'lineTo',
@ -950,7 +952,7 @@ describe('testing getConstraintInfo', () => {
type: 'horizontal',
isConstrained: true,
value: 'xLineTo',
sourceRange: [207, 214],
sourceRange: [209, 216, true],
argPosition: undefined,
pathToNode: expect.any(Array),
stdLibFnName: 'xLineTo',
@ -959,7 +961,7 @@ describe('testing getConstraintInfo', () => {
type: 'xAbsolute',
isConstrained: true,
value: '8 + 0',
sourceRange: [215, 220],
sourceRange: [217, 222, true],
argPosition: { type: 'singleValue' },
pathToNode: expect.any(Array),
stdLibFnName: 'xLineTo',
@ -973,7 +975,7 @@ describe('testing getConstraintInfo', () => {
type: 'vertical',
isConstrained: true,
value: 'yLineTo',
sourceRange: [232, 239],
sourceRange: [234, 241, true],
argPosition: undefined,
pathToNode: expect.any(Array),
stdLibFnName: 'yLineTo',
@ -982,7 +984,7 @@ describe('testing getConstraintInfo', () => {
type: 'yAbsolute',
isConstrained: true,
value: '5 + 0',
sourceRange: [240, 245],
sourceRange: [242, 247, true],
argPosition: { type: 'singleValue' },
pathToNode: expect.any(Array),
stdLibFnName: 'yLineTo',
@ -996,7 +998,7 @@ describe('testing getConstraintInfo', () => {
type: 'vertical',
isConstrained: true,
value: 'yLine',
sourceRange: [257, 262],
sourceRange: [259, 264, true],
argPosition: undefined,
pathToNode: expect.any(Array),
stdLibFnName: 'yLine',
@ -1005,7 +1007,7 @@ describe('testing getConstraintInfo', () => {
type: 'yRelative',
isConstrained: true,
value: '3.14 + 0',
sourceRange: [263, 271],
sourceRange: [265, 273, true],
argPosition: { type: 'singleValue' },
pathToNode: expect.any(Array),
stdLibFnName: 'yLine',
@ -1019,7 +1021,7 @@ describe('testing getConstraintInfo', () => {
type: 'horizontal',
isConstrained: true,
value: 'xLine',
sourceRange: [288, 293],
sourceRange: [289, 294, true],
argPosition: undefined,
pathToNode: expect.any(Array),
stdLibFnName: 'xLine',
@ -1028,7 +1030,7 @@ describe('testing getConstraintInfo', () => {
type: 'xRelative',
isConstrained: true,
value: '3.14 + 0',
sourceRange: [294, 302],
sourceRange: [295, 303, true],
argPosition: { type: 'singleValue' },
pathToNode: expect.any(Array),
stdLibFnName: 'xLine',
@ -1042,7 +1044,7 @@ describe('testing getConstraintInfo', () => {
type: 'angle',
isConstrained: true,
value: '3.14 + 0',
sourceRange: [343, 351],
sourceRange: [345, 353, true],
argPosition: { type: 'objectProperty', key: 'angle' },
pathToNode: expect.any(Array),
stdLibFnName: 'angledLineOfXLength',
@ -1051,7 +1053,7 @@ describe('testing getConstraintInfo', () => {
type: 'xRelative',
isConstrained: true,
value: '3.14 + 0',
sourceRange: [361, 369],
sourceRange: [364, 372, true],
argPosition: { type: 'objectProperty', key: 'length' },
pathToNode: expect.any(Array),
stdLibFnName: 'angledLineOfXLength',
@ -1065,7 +1067,7 @@ describe('testing getConstraintInfo', () => {
type: 'angle',
isConstrained: true,
value: '30 + 0',
sourceRange: [412, 418],
sourceRange: [416, 422, true],
argPosition: { type: 'objectProperty', key: 'angle' },
pathToNode: expect.any(Array),
stdLibFnName: 'angledLineOfYLength',
@ -1074,7 +1076,7 @@ describe('testing getConstraintInfo', () => {
type: 'yRelative',
isConstrained: true,
value: '3 + 0',
sourceRange: [428, 433],
sourceRange: [433, 438, true],
argPosition: { type: 'objectProperty', key: 'length' },
pathToNode: expect.any(Array),
stdLibFnName: 'angledLineOfYLength',
@ -1088,7 +1090,7 @@ describe('testing getConstraintInfo', () => {
type: 'angle',
isConstrained: true,
value: '12.14 + 0',
sourceRange: [470, 479],
sourceRange: [476, 485, true],
argPosition: { type: 'objectProperty', key: 'angle' },
pathToNode: expect.any(Array),
stdLibFnName: 'angledLineToX',
@ -1097,7 +1099,7 @@ describe('testing getConstraintInfo', () => {
type: 'xAbsolute',
isConstrained: true,
value: '12 + 0',
sourceRange: [485, 491],
sourceRange: [492, 498, true],
argPosition: { type: 'objectProperty', key: 'to' },
pathToNode: expect.any(Array),
stdLibFnName: 'angledLineToX',
@ -1111,7 +1113,7 @@ describe('testing getConstraintInfo', () => {
type: 'angle',
isConstrained: true,
value: '30 + 0',
sourceRange: [528, 534],
sourceRange: [536, 542, true],
argPosition: { type: 'objectProperty', key: 'angle' },
pathToNode: expect.any(Array),
stdLibFnName: 'angledLineToY',
@ -1120,7 +1122,7 @@ describe('testing getConstraintInfo', () => {
type: 'yAbsolute',
isConstrained: true,
value: '10.14 + 0',
sourceRange: [540, 549],
sourceRange: [549, 558, true],
argPosition: { type: 'objectProperty', key: 'to' },
pathToNode: expect.any(Array),
stdLibFnName: 'angledLineToY',
@ -1134,7 +1136,7 @@ describe('testing getConstraintInfo', () => {
type: 'angle',
isConstrained: true,
value: '3.14 + 0',
sourceRange: [606, 614],
sourceRange: [616, 624, true],
argPosition: { type: 'objectProperty', key: 'angle' },
pathToNode: expect.any(Array),
stdLibFnName: 'angledLineThatIntersects',
@ -1143,7 +1145,7 @@ describe('testing getConstraintInfo', () => {
type: 'intersectionOffset',
isConstrained: true,
value: '0 + 0',
sourceRange: [661, 666],
sourceRange: [671, 676, true],
argPosition: { type: 'objectProperty', key: 'offset' },
pathToNode: expect.any(Array),
stdLibFnName: 'angledLineThatIntersects',
@ -1151,8 +1153,8 @@ describe('testing getConstraintInfo', () => {
{
type: 'intersectionTag',
isConstrained: false,
value: "'a'",
sourceRange: [639, 642],
value: 'a',
sourceRange: [650, 651, true],
argPosition: { key: 'intersectTag', type: 'objectProperty' },
pathToNode: expect.any(Array),
stdLibFnName: 'angledLineThatIntersects',
@ -1166,7 +1168,7 @@ describe('testing getConstraintInfo', () => {
type: 'tangentialWithPrevious',
isConstrained: true,
value: 'tangentialArcTo',
sourceRange: [687, 702],
sourceRange: [697, 712, true],
argPosition: undefined,
pathToNode: expect.any(Array),
stdLibFnName: 'tangentialArcTo',
@ -1175,7 +1177,7 @@ describe('testing getConstraintInfo', () => {
type: 'xAbsolute',
isConstrained: true,
value: '3.14 + 0',
sourceRange: [704, 712],
sourceRange: [714, 722, true],
argPosition: { type: 'arrayItem', index: 0 },
pathToNode: expect.any(Array),
stdLibFnName: 'tangentialArcTo',
@ -1184,7 +1186,7 @@ describe('testing getConstraintInfo', () => {
type: 'yAbsolute',
isConstrained: true,
value: '13.14 + 0',
sourceRange: [714, 723],
sourceRange: [724, 733, true],
argPosition: { type: 'arrayItem', index: 1 },
pathToNode: expect.any(Array),
stdLibFnName: 'tangentialArcTo',
@ -1195,6 +1197,7 @@ describe('testing getConstraintInfo', () => {
const sourceRange: SourceRange = [
code.indexOf(functionName),
code.indexOf(functionName) + functionName.length,
true,
]
if (err(ast)) return ast
const pathToNode = getNodePathFromSourceRange(ast, sourceRange)

View File

@ -222,7 +222,7 @@ const commonConstraintInfoHelper = (
code.slice(input1.start, input1.end),
stdLibFnName,
isArr ? abbreviatedInputs[0].arrayInput : abbreviatedInputs[0].objInput,
[input1.start, input1.end],
[input1.start, input1.end, true],
pathToFirstArg
)
)
@ -234,7 +234,7 @@ const commonConstraintInfoHelper = (
code.slice(input2.start, input2.end),
stdLibFnName,
isArr ? abbreviatedInputs[1].arrayInput : abbreviatedInputs[1].objInput,
[input2.start, input2.end],
[input2.start, input2.end, true],
pathToSecondArg
)
)
@ -266,7 +266,7 @@ const horzVertConstraintInfoHelper = (
callee.name,
stdLibFnName,
undefined,
[callee.start, callee.end],
[callee.start, callee.end, true],
pathToCallee
),
constrainInfo(
@ -275,7 +275,7 @@ const horzVertConstraintInfoHelper = (
code.slice(firstArg.start, firstArg.end),
stdLibFnName,
abbreviatedInput,
[firstArg.start, firstArg.end],
[firstArg.start, firstArg.end, true],
pathToFirstArg
),
]
@ -905,7 +905,7 @@ export const tangentialArcTo: SketchLineHelper = {
callee.name,
'tangentialArcTo',
undefined,
[callee.start, callee.end],
[callee.start, callee.end, true],
pathToCallee
),
constrainInfo(
@ -914,7 +914,7 @@ export const tangentialArcTo: SketchLineHelper = {
code.slice(firstArg.elements[0].start, firstArg.elements[0].end),
'tangentialArcTo',
0,
[firstArg.elements[0].start, firstArg.elements[0].end],
[firstArg.elements[0].start, firstArg.elements[0].end, true],
pathToFirstArg
),
constrainInfo(
@ -923,7 +923,7 @@ export const tangentialArcTo: SketchLineHelper = {
code.slice(firstArg.elements[1].start, firstArg.elements[1].end),
'tangentialArcTo',
1,
[firstArg.elements[1].start, firstArg.elements[1].end],
[firstArg.elements[1].start, firstArg.elements[1].end, true],
pathToSecondArg
),
]
@ -1052,7 +1052,7 @@ export const circle: SketchLineHelper = {
code.slice(radiusDetails.expr.start, radiusDetails.expr.end),
'circle',
'radius',
[radiusDetails.expr.start, radiusDetails.expr.end],
[radiusDetails.expr.start, radiusDetails.expr.end, true],
pathToRadiusLiteral
),
{
@ -1064,6 +1064,7 @@ export const circle: SketchLineHelper = {
sourceRange: [
centerDetails.expr.elements[0].start,
centerDetails.expr.elements[0].end,
true,
],
pathToNode: pathToXArg,
value: code.slice(
@ -1085,6 +1086,7 @@ export const circle: SketchLineHelper = {
sourceRange: [
centerDetails.expr.elements[1].start,
centerDetails.expr.elements[1].end,
true,
],
pathToNode: pathToYArg,
value: code.slice(
@ -1761,7 +1763,7 @@ export const angledLineThatIntersects: SketchLineHelper = {
code.slice(angle.start, angle.end),
'angledLineThatIntersects',
'angle',
[angle.start, angle.end],
[angle.start, angle.end, true],
pathToAngleProp
)
)
@ -1780,7 +1782,7 @@ export const angledLineThatIntersects: SketchLineHelper = {
code.slice(offset.start, offset.end),
'angledLineThatIntersects',
'offset',
[offset.start, offset.end],
[offset.start, offset.end, true],
pathToOffsetProp
)
)
@ -1799,7 +1801,7 @@ export const angledLineThatIntersects: SketchLineHelper = {
code.slice(tag.start, tag.end),
'angledLineThatIntersects',
'intersectTag',
[tag.start, tag.end],
[tag.start, tag.end, true],
pathToTagProp
)
returnVal.push(info)

View File

@ -1,5 +1,5 @@
import {
parse,
assertParse,
Sketch,
recast,
initPromise,
@ -31,12 +31,11 @@ async function testingSwapSketchFnCall({
constraintType: ConstraintType
}): Promise<{
newCode: string
originalRange: [number, number]
originalRange: [number, number, boolean]
}> {
const startIndex = inputCode.indexOf(callToSwap)
const range: SourceRange = [startIndex, startIndex + callToSwap.length]
const ast = parse(inputCode)
if (err(ast)) return Promise.reject(ast)
const range: SourceRange = [startIndex, startIndex + callToSwap.length, true]
const ast = assertParse(inputCode)
const execState = await enginelessExecutor(ast)
const selections = {
@ -370,13 +369,13 @@ part001 = startSketchOn('XY')
|> line([2.14, 1.35], %) // normal-segment
|> xLine(3.54, %)`
it('normal case works', async () => {
const execState = await enginelessExecutor(parse(code))
const execState = await enginelessExecutor(assertParse(code))
const index = code.indexOf('// normal-segment') - 7
const sg = sketchFromKclValue(
execState.memory.get('part001'),
'part001'
) as Sketch
const _segment = getSketchSegmentFromSourceRange(sg, [index, index])
const _segment = getSketchSegmentFromSourceRange(sg, [index, index, true])
if (err(_segment)) throw _segment
const { __geoMeta, ...segment } = _segment.segment
expect(segment).toEqual({
@ -387,11 +386,11 @@ part001 = startSketchOn('XY')
})
})
it('verify it works when the segment is in the `start` property', async () => {
const execState = await enginelessExecutor(parse(code))
const execState = await enginelessExecutor(assertParse(code))
const index = code.indexOf('// segment-in-start') - 7
const _segment = getSketchSegmentFromSourceRange(
sketchFromKclValue(execState.memory.get('part001'), 'part001') as Sketch,
[index, index]
[index, index, true]
)
if (err(_segment)) throw _segment
const { __geoMeta, ...segment } = _segment.segment

View File

@ -31,7 +31,7 @@ export function getSketchSegmentFromPathToNode(
const node = nodeMeta.node
if (!node || typeof node.start !== 'number' || !node.end)
return new Error('no node found')
const sourceRange: SourceRange = [node.start, node.end]
const sourceRange: SourceRange = [node.start, node.end, true]
return getSketchSegmentFromSourceRange(sketch, sourceRange)
}
export function getSketchSegmentFromSourceRange(

View File

@ -1,4 +1,4 @@
import { parse, Expr, recast, initPromise, Program } from '../wasm'
import { assertParse, Expr, recast, initPromise, Program } from '../wasm'
import {
getConstraintType,
getTransformInfos,
@ -66,8 +66,7 @@ describe('testing getConstraintType', () => {
function getConstraintTypeFromSourceHelper(
code: string
): ReturnType<typeof getConstraintType> | Error {
const ast = parse(code)
if (err(ast)) return ast
const ast = assertParse(code)
const args = (ast.body[0] as any).expression.arguments[0].elements as [
Expr,
@ -79,8 +78,7 @@ function getConstraintTypeFromSourceHelper(
function getConstraintTypeFromSourceHelper2(
code: string
): ReturnType<typeof getConstraintType> | Error {
const ast = parse(code)
if (err(ast)) return ast
const ast = assertParse(code)
const arg = (ast.body[0] as any).expression.arguments[0] as Expr
const fnName = (ast.body[0] as any).expression.callee.name as ToolTip
@ -127,7 +125,7 @@ describe('testing transformAstForSketchLines for equal length constraint', () =>
)
}
const start = codeBeforeLine + line.indexOf('|> ' + 5)
const range: [number, number] = [start, start]
const range: [number, number, boolean] = [start, start, true]
return {
codeRef: codeRefFromRange(range, ast),
}
@ -137,8 +135,7 @@ describe('testing transformAstForSketchLines for equal length constraint', () =>
inputCode: string,
selectionRanges: Selections['graphSelections']
) {
const ast = parse(inputCode)
if (err(ast)) return Promise.reject(ast)
const ast = assertParse(inputCode)
const execState = await enginelessExecutor(ast)
const transformInfos = getTransformInfos(
makeSelections(selectionRanges.slice(1)),
@ -161,8 +158,7 @@ describe('testing transformAstForSketchLines for equal length constraint', () =>
}
it(`Should reorder when user selects first-to-last`, async () => {
const ast = parse(inputScript)
if (err(ast)) return Promise.reject(ast)
const ast = assertParse(inputScript)
const selectionRanges: Selections['graphSelections'] = [
selectLine(inputScript, 3, ast),
selectLine(inputScript, 4, ast),
@ -173,8 +169,7 @@ describe('testing transformAstForSketchLines for equal length constraint', () =>
})
it(`Should reorder when user selects last-to-first`, async () => {
const ast = parse(inputScript)
if (err(ast)) return Promise.reject(ast)
const ast = assertParse(inputScript)
const selectionRanges: Selections['graphSelections'] = [
selectLine(inputScript, 4, ast),
selectLine(inputScript, 3, ast),
@ -293,8 +288,7 @@ part001 = startSketchOn('XY')
|> yLine(segLen(seg01), %) // ln-yLineTo-free should convert to yLine
`
it('should transform the ast', async () => {
const ast = parse(inputScript)
if (err(ast)) return Promise.reject(ast)
const ast = assertParse(inputScript)
const selectionRanges: Selections['graphSelections'] = inputScript
.split('\n')
@ -303,7 +297,7 @@ part001 = startSketchOn('XY')
const comment = ln.split('//')[1]
const start = inputScript.indexOf('//' + comment) - 7
return {
codeRef: codeRefFromRange([start, start], ast),
codeRef: codeRefFromRange([start, start, true], ast),
}
})
@ -383,8 +377,7 @@ part001 = startSketchOn('XY')
|> xLineTo(myVar3, %) // select for horizontal constraint 10
|> angledLineToY([301, myVar], %) // select for vertical constraint 10
`
const ast = parse(inputScript)
if (err(ast)) return Promise.reject(ast)
const ast = assertParse(inputScript)
const selectionRanges: Selections['graphSelections'] = inputScript
.split('\n')
@ -393,7 +386,7 @@ part001 = startSketchOn('XY')
const comment = ln.split('//')[1]
const start = inputScript.indexOf('//' + comment) - 7
return {
codeRef: codeRefFromRange([start, start], ast),
codeRef: codeRefFromRange([start, start, true], ast),
}
})
@ -444,8 +437,7 @@ part001 = startSketchOn('XY')
|> angledLineToX([333, myVar3], %) // select for horizontal constraint 10
|> yLineTo(myVar, %) // select for vertical constraint 10
`
const ast = parse(inputScript)
if (err(ast)) return Promise.reject(ast)
const ast = assertParse(inputScript)
const selectionRanges: Selections['graphSelections'] = inputScript
.split('\n')
@ -454,7 +446,7 @@ part001 = startSketchOn('XY')
const comment = ln.split('//')[1]
const start = inputScript.indexOf('//' + comment) - 7
return {
codeRef: codeRefFromRange([start, start], ast),
codeRef: codeRefFromRange([start, start, true], ast),
}
})
@ -538,8 +530,7 @@ async function helperThing(
linesOfInterest: string[],
constraint: ConstraintType
): Promise<string> {
const ast = parse(inputScript)
if (err(ast)) return Promise.reject(ast)
const ast = assertParse(inputScript)
const selectionRanges: Selections['graphSelections'] = inputScript
.split('\n')
@ -550,7 +541,7 @@ async function helperThing(
const comment = ln.split('//')[1]
const start = inputScript.indexOf('//' + comment) - 7
return {
codeRef: codeRefFromRange([start, start], ast),
codeRef: codeRefFromRange([start, start, true], ast),
}
})
@ -606,7 +597,7 @@ part001 = startSketchOn('XY')
|> line([-1.49, 1.06], %) // free
|> xLine(-3.43 + 0, %) // full
|> angledLineOfXLength([243 + 0, 1.2 + 0], %) // full`
const ast = parse(code)
const ast = assertParse(code)
const constraintLevels: ConstraintLevel[] = ['full', 'partial', 'free']
constraintLevels.forEach((constraintLevel) => {
const recursivelySearchCommentsAndCheckConstraintLevel = (
@ -619,7 +610,7 @@ part001 = startSketchOn('XY')
}
const offsetIndex = index - 7
const expectedConstraintLevel = getConstraintLevelFromSourceRange(
[offsetIndex, offsetIndex],
[offsetIndex, offsetIndex, true],
ast
)
if (err(expectedConstraintLevel)) {

View File

@ -1,4 +1,4 @@
import { parse, initPromise } from '../wasm'
import { assertParse, initPromise } from '../wasm'
import { enginelessExecutor } from '../../lib/testHelpers'
beforeAll(async () => {
@ -17,9 +17,9 @@ describe('testing angledLineThatIntersects', () => {
offset: ${offset},
}, %, $yo2)
intersect = segEndX(yo2)`
const execState = await enginelessExecutor(parse(code('-1')))
const execState = await enginelessExecutor(assertParse(code('-1')))
expect(execState.memory.get('intersect')?.value).toBe(1 + Math.sqrt(2))
const noOffset = await enginelessExecutor(parse(code('0')))
const noOffset = await enginelessExecutor(assertParse(code('0')))
expect(noOffset.memory.get('intersect')?.value).toBeCloseTo(1)
})
})

View File

@ -1,13 +1,18 @@
import { err } from 'lib/trap'
import { parse } from './wasm'
import { parse, ParseResult } from './wasm'
import { enginelessExecutor } from 'lib/testHelpers'
import { Node } from 'wasm-lib/kcl/bindings/Node'
import { Program } from '../wasm-lib/kcl/bindings/Program'
it('can execute parsed AST', async () => {
const code = `x = 1
// A comment.`
const ast = parse(code)
expect(err(ast)).toEqual(false)
const execState = await enginelessExecutor(ast)
expect(err(ast)).toEqual(false)
const result = parse(code)
expect(err(result)).toEqual(false)
const pResult = result as ParseResult
expect(pResult.errors.length).toEqual(0)
expect(pResult.program).not.toEqual(null)
const execState = await enginelessExecutor(pResult.program as Node<Program>)
expect(err(execState)).toEqual(false)
expect(execState.memory.get('x')?.value).toEqual(1)
})

View File

@ -41,6 +41,8 @@ import { ProgramMemory as RawProgramMemory } from '../wasm-lib/kcl/bindings/Prog
import { EnvironmentRef } from '../wasm-lib/kcl/bindings/EnvironmentRef'
import { Environment } from '../wasm-lib/kcl/bindings/Environment'
import { Node } from 'wasm-lib/kcl/bindings/Node'
import { CompilationError } from 'wasm-lib/kcl/bindings/CompilationError'
import { SourceRange as RustSourceRange } from 'wasm-lib/kcl/bindings/SourceRange'
export type { Program } from '../wasm-lib/kcl/bindings/Program'
export type { Expr } from '../wasm-lib/kcl/bindings/Expr'
@ -84,13 +86,22 @@ export type SyntaxType =
| 'NonCodeNode'
| 'UnaryExpression'
export type { SourceRange } from '../wasm-lib/kcl/bindings/SourceRange'
export type { Path } from '../wasm-lib/kcl/bindings/Path'
export type { Sketch } from '../wasm-lib/kcl/bindings/Sketch'
export type { Solid } from '../wasm-lib/kcl/bindings/Solid'
export type { KclValue } from '../wasm-lib/kcl/bindings/KclValue'
export type { ExtrudeSurface } from '../wasm-lib/kcl/bindings/ExtrudeSurface'
export type SourceRange = [number, number, boolean]
export function sourceRangeFromRust(s: RustSourceRange): SourceRange {
return [s[0], s[1], s[2] === 0]
}
export function defaultSourceRange(): SourceRange {
return [0, 0, true]
}
export const wasmUrl = () => {
// For when we're in electron (file based) or web server (network based)
// For some reason relative paths don't work as expected. Otherwise we would
@ -120,26 +131,81 @@ const initialise = async () => {
export const initPromise = initialise()
export const rangeTypeFix = (ranges: number[][]): [number, number, number][] =>
ranges.map(([start, end, moduleId]) => [start, end, moduleId])
const splitErrors = (
input: CompilationError[]
): { errors: CompilationError[]; warnings: CompilationError[] } => {
let errors = []
let warnings = []
for (const i of input) {
if (i.severity === 'Warning') {
warnings.push(i)
} else {
errors.push(i)
}
}
export const parse = (code: string | Error): Node<Program> | Error => {
return { errors, warnings }
}
export class ParseResult {
program: Node<Program> | null
errors: CompilationError[]
warnings: CompilationError[]
constructor(
program: Node<Program> | null,
errors: CompilationError[],
warnings: CompilationError[]
) {
this.program = program
this.errors = errors
this.warnings = warnings
}
}
class SuccessParseResult extends ParseResult {
program: Node<Program>
constructor(
program: Node<Program>,
errors: CompilationError[],
warnings: CompilationError[]
) {
super(program, errors, warnings)
this.program = program
}
}
export function resultIsOk(result: ParseResult): result is SuccessParseResult {
return !!result.program && result.errors.length === 0
}
export const parse = (code: string | Error): ParseResult | Error => {
if (err(code)) return code
try {
const program: Node<Program> = parse_wasm(code)
return program
const parsed: [Node<Program>, CompilationError[]] = parse_wasm(code)
let errs = splitErrors(parsed[1])
return new ParseResult(parsed[0], errs.errors, errs.warnings)
} catch (e: any) {
// throw e
const parsed: RustKclError = JSON.parse(e.toString())
return new KCLError(
parsed.kind,
parsed.msg,
rangeTypeFix(parsed.sourceRanges)
sourceRangeFromRust(parsed.sourceRanges[0])
)
}
}
// Parse and throw an exception if there are any errors (probably not suitable for use outside of testing).
export const assertParse = (code: string): Node<Program> => {
const result = parse(code)
// eslint-disable-next-line suggest-no-throw/suggest-no-throw
if (err(result) || !resultIsOk(result)) throw result
return result.program
}
export type PathToNode = [string | number, string][]
export const isPathToNodeNumber = (
@ -454,7 +520,7 @@ export const _executor = async (
const kclError = new KCLError(
parsed.kind,
parsed.msg,
rangeTypeFix(parsed.sourceRanges)
sourceRangeFromRust(parsed.sourceRanges[0])
)
return Promise.reject(kclError)
@ -527,7 +593,7 @@ export const modifyAstForSketch = async (
const kclError = new KCLError(
parsed.kind,
parsed.msg,
rangeTypeFix(parsed.sourceRanges)
sourceRangeFromRust(parsed.sourceRanges[0])
)
console.log(kclError)
@ -595,7 +661,7 @@ export function programMemoryInit(): ProgramMemory | Error {
return new KCLError(
parsed.kind,
parsed.msg,
rangeTypeFix(parsed.sourceRanges)
sourceRangeFromRust(parsed.sourceRanges[0])
)
}
}

View File

@ -30,27 +30,27 @@ const bracketLeg1Sketch = startSketchOn('XY')
|> line([-shelfMountL + filletRadius, 0], %)
|> close(%)
|> hole(circle({
center: [1, 1],
radius: mountingHoleDiameter / 2
center = [1, 1],
radius = mountingHoleDiameter / 2
}, %), %)
|> hole(circle({
center: [shelfMountL - 1.5, width - 1],
radius: mountingHoleDiameter / 2
center = [shelfMountL - 1.5, width - 1],
radius = mountingHoleDiameter / 2
}, %), %)
|> hole(circle({
center: [1, width - 1],
radius: mountingHoleDiameter / 2
center = [1, width - 1],
radius = mountingHoleDiameter / 2
}, %), %)
|> hole(circle({
center: [shelfMountL - 1.5, 1],
radius: mountingHoleDiameter / 2
center = [shelfMountL - 1.5, 1],
radius = mountingHoleDiameter / 2
}, %), %)
// Extrude the leg 2 bracket sketch
const bracketLeg1Extrude = extrude(thickness, bracketLeg1Sketch)
|> fillet({
radius: extFilletRadius,
tags: [
radius = extFilletRadius,
tags = [
getNextAdjacentEdge(fillet1),
getNextAdjacentEdge(fillet2)
]
@ -61,15 +61,15 @@ const filletSketch = startSketchOn('XZ')
|> startProfileAt([0, 0], %)
|> line([0, thickness], %)
|> arc({
angleEnd: 180,
angleStart: 90,
radius: filletRadius + thickness
angleEnd = 180,
angleStart = 90,
radius = filletRadius + thickness
}, %)
|> line([thickness, 0], %)
|> arc({
angleEnd: 90,
angleStart: 180,
radius: filletRadius
angleEnd = 90,
angleStart = 180,
radius = filletRadius
}, %)
// Sketch the bend
@ -77,11 +77,11 @@ const filletExtrude = extrude(-width, filletSketch)
// Create a custom plane for the leg that sits on the wall
const customPlane = {
plane: {
origin: { x: -filletRadius, y: 0, z: 0 },
xAxis: { x: 0, y: 1, z: 0 },
yAxis: { x: 0, y: 0, z: 1 },
zAxis: { x: 1, y: 0, z: 0 }
plane = {
origin = { x = -filletRadius, y = 0, z = 0 },
xAxis = { x = 0, y = 1, z = 0 },
yAxis = { x = 0, y = 0, z = 1 },
zAxis = { x = 1, y = 0, z = 0 }
}
}
@ -93,19 +93,19 @@ const bracketLeg2Sketch = startSketchOn(customPlane)
|> line([-width, 0], %, $fillet4)
|> close(%)
|> hole(circle({
center: [1, -1.5],
radius: mountingHoleDiameter / 2
center = [1, -1.5],
radius = mountingHoleDiameter / 2
}, %), %)
|> hole(circle({
center: [5, -1.5],
radius: mountingHoleDiameter / 2
center = [5, -1.5],
radius = mountingHoleDiameter / 2
}, %), %)
// Extrude the second leg
const bracketLeg2Extrude = extrude(-thickness, bracketLeg2Sketch)
|> fillet({
radius: extFilletRadius,
tags: [
radius = extFilletRadius,
tags = [
getNextAdjacentEdge(fillet3),
getNextAdjacentEdge(fillet4)
]

View File

@ -5,7 +5,12 @@ import {
kclManager,
sceneEntitiesManager,
} from 'lib/singletons'
import { CallExpression, SourceRange, Expr } from 'lang/wasm'
import {
CallExpression,
SourceRange,
Expr,
defaultSourceRange,
} from 'lang/wasm'
import { ModelingMachineEvent } from 'machines/modelingMachine'
import { isNonNullable, uuidv4 } from 'lib/utils'
import { EditorSelection, SelectionRange } from '@codemirror/state'
@ -266,7 +271,7 @@ export function getEventForSegmentSelection(
selectionType: 'singleCodeCursor',
selection: {
codeRef: {
range: [node.node.start, node.node.end],
range: [node.node.start, node.node.end, true],
pathToNode: group.userData.pathToNode,
},
},
@ -309,10 +314,11 @@ export function handleSelectionBatch({
selectionToEngine.push({
type: 'default',
id: artifact?.id,
range: getCodeRefsByArtifactId(
range:
getCodeRefsByArtifactId(
artifact.id,
engineCommandManager.artifactGraph
)?.[0].range || [0, 0],
)?.[0].range || defaultSourceRange(),
})
})
const engineEvents: Models['WebSocketRequest_type'][] =
@ -376,10 +382,10 @@ export function processCodeMirrorRanges({
if (!isChange) return null
const codeBasedSelections: Selections['graphSelections'] =
codeMirrorRanges.map(({ from, to }) => {
const pathToNode = getNodePathFromSourceRange(ast, [from, to])
const pathToNode = getNodePathFromSourceRange(ast, [from, to, true])
return {
codeRef: {
range: [from, to],
range: [from, to, true],
pathToNode,
},
}
@ -442,7 +448,7 @@ function updateSceneObjectColors(codeBasedSelections: Selection[]) {
if (err(nodeMeta)) return
const node = nodeMeta.node
const groupHasCursor = codeBasedSelections.some((selection) => {
return isOverlap(selection?.codeRef?.range, [node.start, node.end])
return isOverlap(selection?.codeRef?.range, [node.start, node.end, true])
})
const color = groupHasCursor
@ -925,7 +931,7 @@ export function updateSelections(
return {
artifact: artifact,
codeRef: {
range: [node.start, node.end],
range: [node.start, node.end, true],
pathToNode: pathToNode,
},
}
@ -939,7 +945,7 @@ export function updateSelections(
if (err(node)) return node
pathToNodeBasedSelections.push({
codeRef: {
range: [node.node.start, node.node.end],
range: [node.node.start, node.node.end, true],
pathToNode: pathToNode,
},
})

View File

@ -85,11 +85,10 @@ class MockEngineCommandManager {
}
export async function enginelessExecutor(
ast: Node<Program> | Error,
ast: Node<Program>,
pm: ProgramMemory | Error = ProgramMemory.empty(),
idGenerator: IdGenerator = defaultIdGenerator()
): Promise<ExecState> {
if (err(ast)) return Promise.reject(ast)
if (err(pm)) return Promise.reject(pm)
const mockEngineCommandManager = new MockEngineCommandManager({

View File

@ -3,7 +3,7 @@ import { kclManager, engineCommandManager } from 'lib/singletons'
import { useKclContext } from 'lang/KclProvider'
import { findUniqueName } from 'lang/modifyAst'
import { PrevVariable, findAllPreviousVariables } from 'lang/queryAst'
import { ProgramMemory, Expr, parse } from 'lang/wasm'
import { ProgramMemory, Expr, parse, resultIsOk } from 'lang/wasm'
import { useEffect, useRef, useState } from 'react'
import { executeAst } from 'lang/langHelpers'
import { err, trap } from 'lib/trap'
@ -87,9 +87,9 @@ export function useCalculateKclExpression({
useEffect(() => {
const execAstAndSetResult = async () => {
const _code = `const __result__ = ${value}`
const ast = parse(_code)
if (err(ast)) return
if (trap(ast, { suppress: true })) return
const pResult = parse(_code)
if (err(pResult) || !resultIsOk(pResult)) return
const ast = pResult.program
const _programMem: ProgramMemory = ProgramMemory.empty()
for (const { key, value } of availableVarInfo.variables) {

View File

@ -9,13 +9,13 @@ import {
import { SourceRange } from '../lang/wasm'
describe('testing isOverlapping', () => {
testBothOrders([0, 3], [3, 10])
testBothOrders([0, 5], [3, 4])
testBothOrders([0, 5], [5, 10])
testBothOrders([0, 5], [6, 10], false)
testBothOrders([0, 5], [-1, 1])
testBothOrders([0, 5], [-1, 0])
testBothOrders([0, 5], [-2, -1], false)
testBothOrders([0, 3, true], [3, 10, true])
testBothOrders([0, 5, true], [3, 4, true])
testBothOrders([0, 5, true], [5, 10, true])
testBothOrders([0, 5, true], [6, 10, true], false)
testBothOrders([0, 5, true], [-1, 1, true])
testBothOrders([0, 5, true], [-1, 0, true])
testBothOrders([0, 5, true], [-2, -1, true], false)
})
function testBothOrders(a: SourceRange, b: SourceRange, result = true) {

View File

@ -4,6 +4,7 @@ import {
VariableDeclarator,
parse,
recast,
resultIsOk,
} from 'lang/wasm'
import {
Axis,
@ -542,8 +543,11 @@ export const modelingMachine = setup({
if (event.type !== 'Convert to variable') return false
if (!event.data) return false
const ast = parse(recast(kclManager.ast))
if (err(ast)) return false
const isSafeRetVal = isNodeSafeToReplacePath(ast, event.data.pathToNode)
if (err(ast) || !ast.program || ast.errors.length > 0) return false
const isSafeRetVal = isNodeSafeToReplacePath(
ast.program,
event.data.pathToNode
)
if (err(isSafeRetVal)) return false
return isSafeRetVal.isSafe
},
@ -1332,9 +1336,12 @@ export const modelingMachine = setup({
return
}
const recastAst = parse(recast(modifiedAst))
if (err(recastAst) || !resultIsOk(recastAst)) return
const updatedAst = await sceneEntitiesManager.updateAstAndRejigSketch(
sketchDetails?.sketchPathToNode || [],
parse(recast(modifiedAst)),
recastAst.program,
sketchDetails.zAxis,
sketchDetails.yAxis,
sketchDetails.origin

View File

@ -182,7 +182,7 @@ fn do_stdlib_inner(
quote! {
let code_blocks = vec![#(#cb),*];
code_blocks.iter().map(|cb| {
let program = crate::Program::parse(cb).unwrap();
let program = crate::Program::parse_no_errs(cb).unwrap();
let mut options: crate::parsing::ast::types::FormatOptions = Default::default();
options.insert_final_newline = false;
@ -769,7 +769,7 @@ fn generate_code_block_test(fn_name: &str, code_block: &str, index: usize) -> pr
quote! {
#[tokio::test(flavor = "multi_thread")]
async fn #test_name_mock() {
let program = crate::Program::parse(#code_block).unwrap();
let program = crate::Program::parse_no_errs(#code_block).unwrap();
let ctx = crate::executor::ExecutorContext {
engine: std::sync::Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().await.unwrap())),
fs: std::sync::Arc::new(crate::fs::FileManager::new()),

View File

@ -2,7 +2,7 @@
mod test_examples_someFn {
#[tokio::test(flavor = "multi_thread")]
async fn test_mock_example_someFn0() {
let program = crate::Program::parse("someFn()").unwrap();
let program = crate::Program::parse_no_errs("someFn()").unwrap();
let ctx = crate::executor::ExecutorContext {
engine: std::sync::Arc::new(Box::new(
crate::engine::conn_mock::EngineConnection::new()
@ -118,7 +118,7 @@ impl crate::docs::StdLibFn for SomeFn {
code_blocks
.iter()
.map(|cb| {
let program = crate::Program::parse(cb).unwrap();
let program = crate::Program::parse_no_errs(cb).unwrap();
let mut options: crate::parsing::ast::types::FormatOptions = Default::default();
options.insert_final_newline = false;
program.ast.recast(&options, 0)

View File

@ -2,7 +2,7 @@
mod test_examples_someFn {
#[tokio::test(flavor = "multi_thread")]
async fn test_mock_example_someFn0() {
let program = crate::Program::parse("someFn()").unwrap();
let program = crate::Program::parse_no_errs("someFn()").unwrap();
let ctx = crate::executor::ExecutorContext {
engine: std::sync::Arc::new(Box::new(
crate::engine::conn_mock::EngineConnection::new()
@ -118,7 +118,7 @@ impl crate::docs::StdLibFn for SomeFn {
code_blocks
.iter()
.map(|cb| {
let program = crate::Program::parse(cb).unwrap();
let program = crate::Program::parse_no_errs(cb).unwrap();
let mut options: crate::parsing::ast::types::FormatOptions = Default::default();
options.insert_final_newline = false;
program.ast.recast(&options, 0)

View File

@ -3,7 +3,7 @@ mod test_examples_show {
#[tokio::test(flavor = "multi_thread")]
async fn test_mock_example_show0() {
let program =
crate::Program::parse("This is another code block.\nyes sirrr.\nshow").unwrap();
crate::Program::parse_no_errs("This is another code block.\nyes sirrr.\nshow").unwrap();
let ctx = crate::executor::ExecutorContext {
engine: std::sync::Arc::new(Box::new(
crate::engine::conn_mock::EngineConnection::new()
@ -36,7 +36,8 @@ mod test_examples_show {
#[tokio::test(flavor = "multi_thread")]
async fn test_mock_example_show1() {
let program = crate::Program::parse("This is code.\nIt does other shit.\nshow").unwrap();
let program =
crate::Program::parse_no_errs("This is code.\nIt does other shit.\nshow").unwrap();
let ctx = crate::executor::ExecutorContext {
engine: std::sync::Arc::new(Box::new(
crate::engine::conn_mock::EngineConnection::new()
@ -155,7 +156,7 @@ impl crate::docs::StdLibFn for Show {
code_blocks
.iter()
.map(|cb| {
let program = crate::Program::parse(cb).unwrap();
let program = crate::Program::parse_no_errs(cb).unwrap();
let mut options: crate::parsing::ast::types::FormatOptions = Default::default();
options.insert_final_newline = false;
program.ast.recast(&options, 0)

View File

@ -2,7 +2,8 @@
mod test_examples_show {
#[tokio::test(flavor = "multi_thread")]
async fn test_mock_example_show0() {
let program = crate::Program::parse("This is code.\nIt does other shit.\nshow").unwrap();
let program =
crate::Program::parse_no_errs("This is code.\nIt does other shit.\nshow").unwrap();
let ctx = crate::executor::ExecutorContext {
engine: std::sync::Arc::new(Box::new(
crate::engine::conn_mock::EngineConnection::new()
@ -118,7 +119,7 @@ impl crate::docs::StdLibFn for Show {
code_blocks
.iter()
.map(|cb| {
let program = crate::Program::parse(cb).unwrap();
let program = crate::Program::parse_no_errs(cb).unwrap();
let mut options: crate::parsing::ast::types::FormatOptions = Default::default();
options.insert_final_newline = false;
program.ast.recast(&options, 0)

View File

@ -3,7 +3,8 @@ mod test_examples_my_func {
#[tokio::test(flavor = "multi_thread")]
async fn test_mock_example_my_func0() {
let program =
crate::Program::parse("This is another code block.\nyes sirrr.\nmyFunc").unwrap();
crate::Program::parse_no_errs("This is another code block.\nyes sirrr.\nmyFunc")
.unwrap();
let ctx = crate::executor::ExecutorContext {
engine: std::sync::Arc::new(Box::new(
crate::engine::conn_mock::EngineConnection::new()
@ -36,7 +37,8 @@ mod test_examples_my_func {
#[tokio::test(flavor = "multi_thread")]
async fn test_mock_example_my_func1() {
let program = crate::Program::parse("This is code.\nIt does other shit.\nmyFunc").unwrap();
let program =
crate::Program::parse_no_errs("This is code.\nIt does other shit.\nmyFunc").unwrap();
let ctx = crate::executor::ExecutorContext {
engine: std::sync::Arc::new(Box::new(
crate::engine::conn_mock::EngineConnection::new()
@ -155,7 +157,7 @@ impl crate::docs::StdLibFn for MyFunc {
code_blocks
.iter()
.map(|cb| {
let program = crate::Program::parse(cb).unwrap();
let program = crate::Program::parse_no_errs(cb).unwrap();
let mut options: crate::parsing::ast::types::FormatOptions = Default::default();
options.insert_final_newline = false;
program.ast.recast(&options, 0)

View File

@ -3,7 +3,8 @@ mod test_examples_line_to {
#[tokio::test(flavor = "multi_thread")]
async fn test_mock_example_line_to0() {
let program =
crate::Program::parse("This is another code block.\nyes sirrr.\nlineTo").unwrap();
crate::Program::parse_no_errs("This is another code block.\nyes sirrr.\nlineTo")
.unwrap();
let ctx = crate::executor::ExecutorContext {
engine: std::sync::Arc::new(Box::new(
crate::engine::conn_mock::EngineConnection::new()
@ -36,7 +37,8 @@ mod test_examples_line_to {
#[tokio::test(flavor = "multi_thread")]
async fn test_mock_example_line_to1() {
let program = crate::Program::parse("This is code.\nIt does other shit.\nlineTo").unwrap();
let program =
crate::Program::parse_no_errs("This is code.\nIt does other shit.\nlineTo").unwrap();
let ctx = crate::executor::ExecutorContext {
engine: std::sync::Arc::new(Box::new(
crate::engine::conn_mock::EngineConnection::new()
@ -164,7 +166,7 @@ impl crate::docs::StdLibFn for LineTo {
code_blocks
.iter()
.map(|cb| {
let program = crate::Program::parse(cb).unwrap();
let program = crate::Program::parse_no_errs(cb).unwrap();
let mut options: crate::parsing::ast::types::FormatOptions = Default::default();
options.insert_final_newline = false;
program.ast.recast(&options, 0)

View File

@ -3,7 +3,7 @@ mod test_examples_min {
#[tokio::test(flavor = "multi_thread")]
async fn test_mock_example_min0() {
let program =
crate::Program::parse("This is another code block.\nyes sirrr.\nmin").unwrap();
crate::Program::parse_no_errs("This is another code block.\nyes sirrr.\nmin").unwrap();
let ctx = crate::executor::ExecutorContext {
engine: std::sync::Arc::new(Box::new(
crate::engine::conn_mock::EngineConnection::new()
@ -36,7 +36,8 @@ mod test_examples_min {
#[tokio::test(flavor = "multi_thread")]
async fn test_mock_example_min1() {
let program = crate::Program::parse("This is code.\nIt does other shit.\nmin").unwrap();
let program =
crate::Program::parse_no_errs("This is code.\nIt does other shit.\nmin").unwrap();
let ctx = crate::executor::ExecutorContext {
engine: std::sync::Arc::new(Box::new(
crate::engine::conn_mock::EngineConnection::new()
@ -155,7 +156,7 @@ impl crate::docs::StdLibFn for Min {
code_blocks
.iter()
.map(|cb| {
let program = crate::Program::parse(cb).unwrap();
let program = crate::Program::parse_no_errs(cb).unwrap();
let mut options: crate::parsing::ast::types::FormatOptions = Default::default();
options.insert_final_newline = false;
program.ast.recast(&options, 0)

View File

@ -2,7 +2,8 @@
mod test_examples_show {
#[tokio::test(flavor = "multi_thread")]
async fn test_mock_example_show0() {
let program = crate::Program::parse("This is code.\nIt does other shit.\nshow").unwrap();
let program =
crate::Program::parse_no_errs("This is code.\nIt does other shit.\nshow").unwrap();
let ctx = crate::executor::ExecutorContext {
engine: std::sync::Arc::new(Box::new(
crate::engine::conn_mock::EngineConnection::new()
@ -118,7 +119,7 @@ impl crate::docs::StdLibFn for Show {
code_blocks
.iter()
.map(|cb| {
let program = crate::Program::parse(cb).unwrap();
let program = crate::Program::parse_no_errs(cb).unwrap();
let mut options: crate::parsing::ast::types::FormatOptions = Default::default();
options.insert_final_newline = false;
program.ast.recast(&options, 0)

View File

@ -2,7 +2,8 @@
mod test_examples_import {
#[tokio::test(flavor = "multi_thread")]
async fn test_mock_example_import0() {
let program = crate::Program::parse("This is code.\nIt does other shit.\nimport").unwrap();
let program =
crate::Program::parse_no_errs("This is code.\nIt does other shit.\nimport").unwrap();
let ctx = crate::executor::ExecutorContext {
engine: std::sync::Arc::new(Box::new(
crate::engine::conn_mock::EngineConnection::new()
@ -118,7 +119,7 @@ impl crate::docs::StdLibFn for Import {
code_blocks
.iter()
.map(|cb| {
let program = crate::Program::parse(cb).unwrap();
let program = crate::Program::parse_no_errs(cb).unwrap();
let mut options: crate::parsing::ast::types::FormatOptions = Default::default();
options.insert_final_newline = false;
program.ast.recast(&options, 0)

View File

@ -2,7 +2,8 @@
mod test_examples_import {
#[tokio::test(flavor = "multi_thread")]
async fn test_mock_example_import0() {
let program = crate::Program::parse("This is code.\nIt does other shit.\nimport").unwrap();
let program =
crate::Program::parse_no_errs("This is code.\nIt does other shit.\nimport").unwrap();
let ctx = crate::executor::ExecutorContext {
engine: std::sync::Arc::new(Box::new(
crate::engine::conn_mock::EngineConnection::new()
@ -118,7 +119,7 @@ impl crate::docs::StdLibFn for Import {
code_blocks
.iter()
.map(|cb| {
let program = crate::Program::parse(cb).unwrap();
let program = crate::Program::parse_no_errs(cb).unwrap();
let mut options: crate::parsing::ast::types::FormatOptions = Default::default();
options.insert_final_newline = false;
program.ast.recast(&options, 0)

View File

@ -2,7 +2,8 @@
mod test_examples_import {
#[tokio::test(flavor = "multi_thread")]
async fn test_mock_example_import0() {
let program = crate::Program::parse("This is code.\nIt does other shit.\nimport").unwrap();
let program =
crate::Program::parse_no_errs("This is code.\nIt does other shit.\nimport").unwrap();
let ctx = crate::executor::ExecutorContext {
engine: std::sync::Arc::new(Box::new(
crate::engine::conn_mock::EngineConnection::new()
@ -118,7 +119,7 @@ impl crate::docs::StdLibFn for Import {
code_blocks
.iter()
.map(|cb| {
let program = crate::Program::parse(cb).unwrap();
let program = crate::Program::parse_no_errs(cb).unwrap();
let mut options: crate::parsing::ast::types::FormatOptions = Default::default();
options.insert_final_newline = false;
program.ast.recast(&options, 0)

View File

@ -2,7 +2,8 @@
mod test_examples_show {
#[tokio::test(flavor = "multi_thread")]
async fn test_mock_example_show0() {
let program = crate::Program::parse("This is code.\nIt does other shit.\nshow").unwrap();
let program =
crate::Program::parse_no_errs("This is code.\nIt does other shit.\nshow").unwrap();
let ctx = crate::executor::ExecutorContext {
engine: std::sync::Arc::new(Box::new(
crate::engine::conn_mock::EngineConnection::new()
@ -118,7 +119,7 @@ impl crate::docs::StdLibFn for Show {
code_blocks
.iter()
.map(|cb| {
let program = crate::Program::parse(cb).unwrap();
let program = crate::Program::parse_no_errs(cb).unwrap();
let mut options: crate::parsing::ast::types::FormatOptions = Default::default();
options.insert_final_newline = false;
program.ast.recast(&options, 0)

View File

@ -2,7 +2,7 @@
mod test_examples_some_function {
#[tokio::test(flavor = "multi_thread")]
async fn test_mock_example_some_function0() {
let program = crate::Program::parse("someFunction()").unwrap();
let program = crate::Program::parse_no_errs("someFunction()").unwrap();
let ctx = crate::executor::ExecutorContext {
engine: std::sync::Arc::new(Box::new(
crate::engine::conn_mock::EngineConnection::new()
@ -112,7 +112,7 @@ impl crate::docs::StdLibFn for SomeFunction {
code_blocks
.iter()
.map(|cb| {
let program = crate::Program::parse(cb).unwrap();
let program = crate::Program::parse_no_errs(cb).unwrap();
let mut options: crate::parsing::ast::types::FormatOptions = Default::default();
options.insert_final_newline = false;
program.ast.recast(&options, 0)

View File

@ -158,7 +158,7 @@ async fn snapshot_endpoint(body: Bytes, state: ExecutorContext) -> Response<Body
};
let RequestBody { kcl_program, test_name } = body;
let program = match Program::parse(&kcl_program) {
let program = match Program::parse_no_errs(&kcl_program) {
Ok(pr) => pr,
Err(e) => return bad_request(format!("Parse error: {e}")),
};

View File

@ -7,7 +7,7 @@ mod conn_mock_core;
///Converts the given kcl code to an engine test
pub async fn kcl_to_engine_core(code: &str) -> Result<String> {
let program = kcl_lib::Program::parse(code)?;
let program = kcl_lib::Program::parse_no_errs(code)?;
let result = Arc::new(Mutex::new("".into()));
let ref_result = Arc::clone(&result);

View File

@ -9,7 +9,7 @@ pub fn bench_digest(c: &mut Criterion) {
("mike_stress_test", MIKE_STRESS_TEST_PROGRAM),
("lsystem", LSYSTEM_PROGRAM),
] {
let prog = kcl_lib::Program::parse(file).unwrap();
let prog = kcl_lib::Program::parse_no_errs(file).unwrap();
c.bench_function(&format!("digest_{name}"), move |b| {
let prog = prog.clone();

View File

@ -177,7 +177,7 @@ impl KclError {
}
}
pub fn override_source_ranges(&self, source_ranges: Vec<SourceRange>) -> Self {
pub(crate) fn override_source_ranges(&self, source_ranges: Vec<SourceRange>) -> Self {
let mut new = self.clone();
match &mut new {
KclError::Lexical(e) => e.source_ranges = source_ranges,
@ -197,7 +197,7 @@ impl KclError {
new
}
pub fn add_source_ranges(&self, source_ranges: Vec<SourceRange>) -> Self {
pub(crate) fn add_source_ranges(&self, source_ranges: Vec<SourceRange>) -> Self {
let mut new = self.clone();
match &mut new {
KclError::Lexical(e) => e.source_ranges.extend(source_ranges),
@ -279,3 +279,114 @@ impl From<KclError> for pyo3::PyErr {
pyo3::exceptions::PyException::new_err(error.to_string())
}
}
/// An error which occurred during parsing, etc.
#[derive(Debug, Clone, Serialize, Deserialize, ts_rs::TS)]
#[ts(export)]
pub struct CompilationError {
#[serde(rename = "sourceRange")]
pub source_range: SourceRange,
#[serde(rename = "contextRange")]
pub context_range: Option<SourceRange>,
pub message: String,
pub suggestion: Option<Suggestion>,
pub severity: Severity,
pub tag: Tag,
}
impl CompilationError {
#[allow(dead_code)]
pub(crate) fn err(source_range: SourceRange, message: impl ToString) -> CompilationError {
CompilationError {
source_range,
context_range: None,
message: message.to_string(),
suggestion: None,
severity: Severity::Error,
tag: Tag::None,
}
}
pub(crate) fn fatal(source_range: SourceRange, message: impl ToString) -> CompilationError {
CompilationError {
source_range,
context_range: None,
message: message.to_string(),
suggestion: None,
severity: Severity::Fatal,
tag: Tag::None,
}
}
pub(crate) fn with_suggestion(
source_range: SourceRange,
context_range: Option<SourceRange>,
message: impl ToString,
suggestion: Option<(impl ToString, impl ToString)>,
tag: Tag,
) -> CompilationError {
CompilationError {
source_range,
context_range,
message: message.to_string(),
suggestion: suggestion.map(|(t, i)| Suggestion {
title: t.to_string(),
insert: i.to_string(),
}),
severity: Severity::Error,
tag,
}
}
#[cfg(test)]
pub fn apply_suggestion(&self, src: &str) -> Option<String> {
let suggestion = self.suggestion.as_ref()?;
Some(format!(
"{}{}{}",
&src[0..self.source_range.start()],
suggestion.insert,
&src[self.source_range.end()..]
))
}
}
impl From<CompilationError> for KclErrorDetails {
fn from(err: CompilationError) -> Self {
KclErrorDetails {
source_ranges: vec![err.source_range],
message: err.message,
}
}
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Serialize, Deserialize, ts_rs::TS)]
#[ts(export)]
pub enum Severity {
Warning,
Error,
Fatal,
}
impl Severity {
pub fn is_err(self) -> bool {
match self {
Severity::Warning => false,
Severity::Error | Severity::Fatal => true,
}
}
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Serialize, Deserialize, ts_rs::TS)]
#[ts(export)]
pub enum Tag {
Deprecated,
Unnecessary,
None,
}
#[derive(Debug, Clone, Serialize, Deserialize, ts_rs::TS)]
#[ts(export)]
pub struct Suggestion {
pub title: String,
pub insert: String,
}

View File

@ -2241,7 +2241,7 @@ mod tests {
use crate::parsing::ast::types::{Identifier, Node, Parameter};
pub async fn parse_execute(code: &str) -> Result<ProgramMemory> {
let program = Program::parse(code)?;
let program = Program::parse_no_errs(code)?;
let ctx = ExecutorContext {
engine: Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().await?)),

View File

@ -83,7 +83,7 @@ mod wasm;
pub use coredump::CoreDump;
pub use engine::{EngineManager, ExecutionKind};
pub use errors::{ConnectionError, ExecError, KclError};
pub use errors::{CompilationError, ConnectionError, ExecError, KclError};
pub use executor::{ExecState, ExecutorContext, ExecutorSettings};
pub use lsp::{
copilot::Backend as CopilotLspBackend,
@ -134,10 +134,17 @@ pub use lsp::test_util::copilot_lsp_server;
pub use lsp::test_util::kcl_lsp_server;
impl Program {
pub fn parse(input: &str) -> Result<Program, KclError> {
pub fn parse(input: &str) -> Result<(Option<Program>, Vec<CompilationError>), KclError> {
let module_id = ModuleId::default();
let tokens = parsing::token::lexer(input, module_id)?;
let (ast, errs) = parsing::parse_tokens(tokens).0?;
Ok((ast.map(|ast| Program { ast }), errs))
}
pub fn parse_no_errs(input: &str) -> Result<Program, KclError> {
let module_id = ModuleId::default();
let tokens = parsing::token::lexer(input, module_id)?;
// TODO handle parsing errors properly
let ast = parsing::parse_tokens(tokens).parse_errs_as_err()?;
Ok(Program { ast })

View File

@ -20,7 +20,8 @@ use sha2::Digest;
use tower_lsp::{
jsonrpc::Result as RpcResult,
lsp_types::{
CompletionItem, CompletionItemKind, CompletionOptions, CompletionParams, CompletionResponse, CreateFilesParams,
CodeAction, CodeActionKind, CodeActionOrCommand, CodeActionParams, CodeActionResponse, CompletionItem,
CompletionItemKind, CompletionOptions, CompletionParams, CompletionResponse, CreateFilesParams,
DeleteFilesParams, Diagnostic, DiagnosticOptions, DiagnosticServerCapabilities, DiagnosticSeverity,
DidChangeConfigurationParams, DidChangeTextDocumentParams, DidChangeWatchedFilesParams,
DidChangeWorkspaceFoldersParams, DidCloseTextDocumentParams, DidOpenTextDocumentParams,
@ -41,6 +42,7 @@ use tower_lsp::{
};
use crate::{
errors::Suggestion,
lsp::{backend::Backend as _, util::IntoDiagnostic},
parsing::{
ast::types::{Expr, Node, VariableKind},
@ -190,6 +192,12 @@ impl Backend {
is_initialized: Default::default(),
})
}
fn remove_from_ast_maps(&self, filename: &str) {
self.ast_map.remove(filename);
self.symbols_map.remove(filename);
self.memory_map.remove(filename);
}
}
// Implement the shared backend trait for the language server.
@ -264,10 +272,8 @@ impl crate::lsp::backend::Backend for Backend {
Err(err) => {
self.add_to_diagnostics(&params, &[err], true).await;
self.token_map.remove(&filename);
self.ast_map.remove(&filename);
self.symbols_map.remove(&filename);
self.remove_from_ast_maps(&filename);
self.semantic_tokens_map.remove(&filename);
self.memory_map.remove(&filename);
return;
}
};
@ -300,19 +306,28 @@ impl crate::lsp::backend::Backend for Backend {
}
// Lets update the ast.
let result = crate::parsing::parse_tokens(tokens.clone());
// TODO handle parse errors properly
let mut ast = match result.parse_errs_as_err() {
Ok(ast) => ast,
let (ast, errs) = match crate::parsing::parse_tokens(tokens.clone()).0 {
Ok(result) => result,
Err(err) => {
self.add_to_diagnostics(&params, &[err], true).await;
self.ast_map.remove(&filename);
self.symbols_map.remove(&filename);
self.memory_map.remove(&filename);
self.remove_from_ast_maps(&filename);
return;
}
};
self.add_to_diagnostics(&params, &errs, true).await;
if errs.iter().any(|e| e.severity == crate::errors::Severity::Fatal) {
self.remove_from_ast_maps(&filename);
return;
}
let Some(mut ast) = ast else {
self.remove_from_ast_maps(&filename);
return;
};
// Here we will want to store the digest and compare, but for now
// we're doing this in a non-load-bearing capacity so we can remove
// this if it backfires and only hork the LSP.
@ -327,7 +342,7 @@ impl crate::lsp::backend::Backend for Backend {
None => true,
};
if !ast_changed && !force && has_memory && !self.has_diagnostics(params.uri.as_ref()).await {
if !ast_changed && !force && has_memory {
// Return early if the ast did not change and we don't need to force.
return;
}
@ -1380,6 +1395,41 @@ impl LanguageServer for Backend {
Ok(Some(folding_ranges))
}
async fn code_action(&self, params: CodeActionParams) -> RpcResult<Option<CodeActionResponse>> {
let actions = params
.context
.diagnostics
.into_iter()
.filter_map(|diagnostic| {
let suggestion = diagnostic
.data
.as_ref()
.and_then(|data| serde_json::from_value::<Suggestion>(data.clone()).ok())?;
let edit = TextEdit {
range: diagnostic.range,
new_text: suggestion.insert,
};
let changes = HashMap::from([(params.text_document.uri.clone(), vec![edit])]);
Some(CodeActionOrCommand::CodeAction(CodeAction {
title: suggestion.title,
kind: Some(CodeActionKind::QUICKFIX),
diagnostics: Some(vec![diagnostic]),
edit: Some(WorkspaceEdit {
changes: Some(changes),
document_changes: None,
change_annotations: None,
}),
command: None,
is_preferred: Some(true),
disabled: None,
data: None,
}))
})
.collect();
Ok(Some(actions))
}
}
/// Get completions from our stdlib.

View File

@ -9,4 +9,45 @@ pub mod test_util;
mod tests;
pub mod util;
use tower_lsp::lsp_types::{Diagnostic, DiagnosticSeverity, DiagnosticTag};
pub use util::IntoDiagnostic;
use crate::{
errors::{Severity, Tag},
CompilationError,
};
impl IntoDiagnostic for CompilationError {
fn to_lsp_diagnostic(&self, code: &str) -> Diagnostic {
let edit = self.suggestion.as_ref().map(|s| serde_json::to_value(s).unwrap());
Diagnostic {
range: self.source_range.to_lsp_range(code),
severity: Some(self.severity()),
code: None,
code_description: None,
source: Some("kcl".to_string()),
message: self.message.clone(),
related_information: None,
tags: self.tag.to_lsp_tags(),
data: edit,
}
}
fn severity(&self) -> DiagnosticSeverity {
match self.severity {
Severity::Warning => DiagnosticSeverity::WARNING,
_ => DiagnosticSeverity::ERROR,
}
}
}
impl Tag {
fn to_lsp_tags(self) -> Option<Vec<DiagnosticTag>> {
match self {
Tag::Deprecated => Some(vec![DiagnosticTag::DEPRECATED]),
Tag::Unnecessary => Some(vec![DiagnosticTag::UNNECESSARY]),
Tag::None => None,
}
}
}

View File

@ -12,6 +12,22 @@ use crate::{
parsing::ast::types::{Node, Program},
};
#[track_caller]
fn assert_diagnostic_count(diagnostics: Option<&Vec<Diagnostic>>, n: usize) {
let Some(diagnostics) = diagnostics else {
assert_eq!(n, 0, "No diagnostics");
return;
};
assert_eq!(
diagnostics
.iter()
.filter(|d| d.severity.as_ref().unwrap() != &tower_lsp::lsp_types::DiagnosticSeverity::WARNING)
.count(),
n,
"expected {n} errors, found {diagnostics:#?}"
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 12)]
async fn test_updating_kcl_lsp_files() {
let server = kcl_lsp_server(false).await.unwrap();
@ -1061,9 +1077,8 @@ fn myFn = (param1) => {
})
.await;
// Assure we have no diagnostics.
let diagnostics = server.diagnostics_map.get("file:///test.kcl");
assert!(diagnostics.is_none());
// Assure we have no errors.
assert_diagnostic_count(server.diagnostics_map.get("file:///test.kcl").as_deref(), 0);
// Get the token map.
let token_map = server.token_map.get("file:///test.kcl").unwrap().clone();
@ -2287,8 +2302,7 @@ async fn test_kcl_lsp_diagnostics_on_parse_error() {
.await;
// Get the diagnostics.
let diagnostics = server.diagnostics_map.get("file:///test.kcl").unwrap().clone();
assert_eq!(diagnostics.len(), 1);
assert_diagnostic_count(server.diagnostics_map.get("file:///test.kcl").as_deref(), 1);
// Update the text.
let new_text = r#"const thing = 2"#.to_string();
@ -2308,8 +2322,7 @@ async fn test_kcl_lsp_diagnostics_on_parse_error() {
.await;
// Get the diagnostics.
let diagnostics = server.diagnostics_map.get("file:///test.kcl");
assert!(diagnostics.is_none());
assert_diagnostic_count(server.diagnostics_map.get("file:///test.kcl").as_deref(), 0);
}
#[tokio::test(flavor = "multi_thread")]
@ -2340,8 +2353,7 @@ async fn kcl_test_kcl_lsp_diagnostics_on_execution_error() {
.await;
// Get the diagnostics.
let diagnostics = server.diagnostics_map.get("file:///test.kcl").unwrap().clone();
assert_eq!(diagnostics.len(), 1);
assert_diagnostic_count(server.diagnostics_map.get("file:///test.kcl").as_deref(), 1);
// Update the text.
let new_text = r#"const part001 = startSketchOn('XY')
@ -2368,15 +2380,7 @@ async fn kcl_test_kcl_lsp_diagnostics_on_execution_error() {
.await;
// Get the diagnostics.
let diagnostics = server.diagnostics_map.get("file:///test.kcl");
if let Some(diagnostics) = diagnostics {
let ds: Vec<Diagnostic> = diagnostics.to_owned();
eprintln!("Expected no diagnostics, but found some.");
for d in ds {
eprintln!("{:?}: {}", d.severity, d.message);
}
panic!();
}
assert_diagnostic_count(server.diagnostics_map.get("file:///test.kcl").as_deref(), 0);
}
#[tokio::test(flavor = "multi_thread")]
@ -2467,8 +2471,7 @@ async fn kcl_test_kcl_lsp_code_unchanged_but_has_diagnostics_reexecute() {
assert!(memory != ProgramMemory::default());
// Assure we have no diagnostics.
let diagnostics = server.diagnostics_map.get("file:///test.kcl");
assert!(diagnostics.is_none());
assert_diagnostic_count(server.diagnostics_map.get("file:///test.kcl").as_deref(), 0);
// Add some fake diagnostics.
server.diagnostics_map.insert(
@ -2489,8 +2492,7 @@ async fn kcl_test_kcl_lsp_code_unchanged_but_has_diagnostics_reexecute() {
}],
);
// Assure we have one diagnostics.
let diagnostics = server.diagnostics_map.get("file:///test.kcl").unwrap().clone();
assert_eq!(diagnostics.len(), 1);
assert_diagnostic_count(server.diagnostics_map.get("file:///test.kcl").as_deref(), 1);
// Clear the ast and memory.
server
@ -2527,8 +2529,7 @@ async fn kcl_test_kcl_lsp_code_unchanged_but_has_diagnostics_reexecute() {
assert!(memory != ProgramMemory::default());
// Assure we have no diagnostics.
let diagnostics = server.diagnostics_map.get("file:///test.kcl");
assert!(diagnostics.is_none());
assert_diagnostic_count(server.diagnostics_map.get("file:///test.kcl").as_deref(), 0);
}
#[tokio::test(flavor = "multi_thread")]
@ -2563,8 +2564,7 @@ async fn kcl_test_kcl_lsp_code_and_ast_unchanged_but_has_diagnostics_reexecute()
assert!(memory != ProgramMemory::default());
// Assure we have no diagnostics.
let diagnostics = server.diagnostics_map.get("file:///test.kcl");
assert!(diagnostics.is_none());
assert_diagnostic_count(server.diagnostics_map.get("file:///test.kcl").as_deref(), 0);
// Add some fake diagnostics.
server.diagnostics_map.insert(
@ -2585,8 +2585,7 @@ async fn kcl_test_kcl_lsp_code_and_ast_unchanged_but_has_diagnostics_reexecute()
}],
);
// Assure we have one diagnostics.
let diagnostics = server.diagnostics_map.get("file:///test.kcl").unwrap().clone();
assert_eq!(diagnostics.len(), 1);
assert_diagnostic_count(server.diagnostics_map.get("file:///test.kcl").as_deref(), 1);
// Clear ONLY the memory.
server
@ -2618,8 +2617,7 @@ async fn kcl_test_kcl_lsp_code_and_ast_unchanged_but_has_diagnostics_reexecute()
assert!(memory != ProgramMemory::default());
// Assure we have no diagnostics.
let diagnostics = server.diagnostics_map.get("file:///test.kcl");
assert!(diagnostics.is_none());
assert_diagnostic_count(server.diagnostics_map.get("file:///test.kcl").as_deref(), 0);
}
#[tokio::test(flavor = "multi_thread")]
@ -2654,8 +2652,7 @@ async fn kcl_test_kcl_lsp_code_and_ast_units_unchanged_but_has_diagnostics_reexe
assert!(memory != ProgramMemory::default());
// Assure we have no diagnostics.
let diagnostics = server.diagnostics_map.get("file:///test.kcl");
assert!(diagnostics.is_none());
assert_diagnostic_count(server.diagnostics_map.get("file:///test.kcl").as_deref(), 0);
// Add some fake diagnostics.
server.diagnostics_map.insert(
@ -2676,8 +2673,7 @@ async fn kcl_test_kcl_lsp_code_and_ast_units_unchanged_but_has_diagnostics_reexe
}],
);
// Assure we have one diagnostics.
let diagnostics = server.diagnostics_map.get("file:///test.kcl").unwrap().clone();
assert_eq!(diagnostics.len(), 1);
assert_diagnostic_count(server.diagnostics_map.get("file:///test.kcl").as_deref(), 1);
// Clear ONLY the memory.
server
@ -2712,8 +2708,7 @@ async fn kcl_test_kcl_lsp_code_and_ast_units_unchanged_but_has_diagnostics_reexe
assert!(memory != ProgramMemory::default());
// Assure we have no diagnostics.
let diagnostics = server.diagnostics_map.get("file:///test.kcl");
assert!(diagnostics.is_none());
assert_diagnostic_count(server.diagnostics_map.get("file:///test.kcl").as_deref(), 0);
}
#[tokio::test(flavor = "multi_thread")]
@ -2748,8 +2743,7 @@ async fn kcl_test_kcl_lsp_code_and_ast_units_unchanged_but_has_memory_reexecute_
assert!(memory != ProgramMemory::default());
// Assure we have no diagnostics.
let diagnostics = server.diagnostics_map.get("file:///test.kcl");
assert!(diagnostics.is_none());
assert_diagnostic_count(server.diagnostics_map.get("file:///test.kcl").as_deref(), 0);
// Clear ONLY the memory.
server
@ -2784,8 +2778,7 @@ async fn kcl_test_kcl_lsp_code_and_ast_units_unchanged_but_has_memory_reexecute_
assert!(memory != ProgramMemory::default());
// Assure we have no diagnostics.
let diagnostics = server.diagnostics_map.get("file:///test.kcl");
assert!(diagnostics.is_none());
assert_diagnostic_count(server.diagnostics_map.get("file:///test.kcl").as_deref(), 0);
}
#[tokio::test(flavor = "multi_thread")]
@ -2820,8 +2813,7 @@ async fn kcl_test_kcl_lsp_cant_execute_set() {
assert!(memory != ProgramMemory::default());
// Assure we have no diagnostics.
let diagnostics = server.diagnostics_map.get("file:///test.kcl");
assert!(diagnostics.is_none());
assert_diagnostic_count(server.diagnostics_map.get("file:///test.kcl").as_deref(), 0);
// Clear ONLY the memory.
server
@ -2855,8 +2847,7 @@ async fn kcl_test_kcl_lsp_cant_execute_set() {
assert!(memory != ProgramMemory::default());
// Assure we have no diagnostics.
let diagnostics = server.diagnostics_map.get("file:///test.kcl");
assert!(diagnostics.is_none());
assert_diagnostic_count(server.diagnostics_map.get("file:///test.kcl").as_deref(), 0);
// Clear ONLY the memory.
server
@ -2903,8 +2894,7 @@ async fn kcl_test_kcl_lsp_cant_execute_set() {
assert!(memory == ProgramMemory::default());
// Assure we have no diagnostics.
let diagnostics = server.diagnostics_map.get("file:///test.kcl");
assert!(diagnostics.is_none());
assert_diagnostic_count(server.diagnostics_map.get("file:///test.kcl").as_deref(), 0);
// Set that we CAN execute.
server
@ -2939,8 +2929,7 @@ async fn kcl_test_kcl_lsp_cant_execute_set() {
assert!(memory != ProgramMemory::default());
// Assure we have no diagnostics.
let diagnostics = server.diagnostics_map.get("file:///test.kcl");
assert!(diagnostics.is_none());
assert_diagnostic_count(server.diagnostics_map.get("file:///test.kcl").as_deref(), 0);
}
#[tokio::test(flavor = "multi_thread")]
@ -3018,8 +3007,7 @@ async fn kcl_test_kcl_lsp_code_with_parse_error_and_ast_unchanged_but_has_diagno
assert!(ast.is_none());
// Assure we have one diagnostics.
let diagnostics = server.diagnostics_map.get("file:///test.kcl").unwrap().clone();
assert_eq!(diagnostics.len(), 1);
assert_diagnostic_count(server.diagnostics_map.get("file:///test.kcl").as_deref(), 1);
// Send change file, but the code is the same.
server
@ -3041,8 +3029,7 @@ async fn kcl_test_kcl_lsp_code_with_parse_error_and_ast_unchanged_but_has_diagno
assert!(ast.is_none());
// Assure we have one diagnostics.
let diagnostics = server.diagnostics_map.get("file:///test.kcl").unwrap().clone();
assert_eq!(diagnostics.len(), 1);
assert_diagnostic_count(server.diagnostics_map.get("file:///test.kcl").as_deref(), 1);
}
#[tokio::test(flavor = "multi_thread")]
@ -3075,9 +3062,7 @@ const part001 = startSketchOn('XY')
assert!(ast != Node::<Program>::default());
// Assure we have one diagnostics.
let diagnostics = server.diagnostics_map.get("file:///test.kcl").unwrap().clone();
assert_eq!(diagnostics.len(), 1);
assert_diagnostic_count(server.diagnostics_map.get("file:///test.kcl").as_deref(), 1);
// Send change file, but the code is the same.
server
.did_change(tower_lsp::lsp_types::DidChangeTextDocumentParams {
@ -3098,8 +3083,7 @@ const part001 = startSketchOn('XY')
assert!(ast != Node::<Program>::default());
// Assure we have one diagnostics.
let diagnostics = server.diagnostics_map.get("file:///test.kcl").unwrap().clone();
assert_eq!(diagnostics.len(), 1);
assert_diagnostic_count(server.diagnostics_map.get("file:///test.kcl").as_deref(), 1);
}
#[tokio::test(flavor = "multi_thread")]
@ -3132,8 +3116,7 @@ const part001 = startSketchOn('XY')
assert!(ast.is_none());
// Assure we have diagnostics.
let diagnostics = server.diagnostics_map.get("file:///test.kcl").unwrap().clone();
assert_eq!(diagnostics.len(), 1);
assert_diagnostic_count(server.diagnostics_map.get("file:///test.kcl").as_deref(), 1);
// Send change file, but the code is the same.
server
@ -3155,8 +3138,7 @@ const part001 = startSketchOn('XY')
assert!(ast.is_none());
// Assure we have one diagnostics.
let diagnostics = server.diagnostics_map.get("file:///test.kcl").unwrap().clone();
assert_eq!(diagnostics.len(), 1);
assert_diagnostic_count(server.diagnostics_map.get("file:///test.kcl").as_deref(), 1);
}
#[tokio::test(flavor = "multi_thread")]
@ -3185,9 +3167,9 @@ const part001 = startSketchOn('XY')
.await;
// Assure we have diagnostics.
let diagnostics = server.diagnostics_map.get("file:///test.kcl").unwrap().clone();
// Check the diagnostics.
assert_eq!(diagnostics.len(), 2);
assert_diagnostic_count(server.diagnostics_map.get("file:///test.kcl").as_deref(), 2);
// Get the ast.
let ast = server.ast_map.get("file:///test.kcl").unwrap().clone();
@ -3219,9 +3201,9 @@ const part001 = startSketchOn('XY')
assert!(memory.is_none());
// Assure we have diagnostics.
let diagnostics = server.diagnostics_map.get("file:///test.kcl").unwrap().clone();
// Check the diagnostics.
assert_eq!(diagnostics.len(), 2);
assert_diagnostic_count(server.diagnostics_map.get("file:///test.kcl").as_deref(), 2);
}
#[tokio::test(flavor = "multi_thread")]
@ -3250,9 +3232,9 @@ const part001 = startSketchOn('XY')
.await;
// Assure we have diagnostics.
let diagnostics = server.diagnostics_map.get("file:///test.kcl").unwrap().clone();
// Check the diagnostics.
assert_eq!(diagnostics.len(), 2);
assert_diagnostic_count(server.diagnostics_map.get("file:///test.kcl").as_deref(), 2);
// Get the ast.
let ast = server.ast_map.get("file:///test.kcl").unwrap().clone();
@ -3292,9 +3274,9 @@ const NEW_LINT = 1"#
assert!(memory.is_none());
// Assure we have diagnostics.
let diagnostics = server.diagnostics_map.get("file:///test.kcl").unwrap().clone();
// Check the diagnostics.
assert_eq!(diagnostics.len(), 2);
assert_diagnostic_count(server.diagnostics_map.get("file:///test.kcl").as_deref(), 2);
}
#[tokio::test(flavor = "multi_thread")]
@ -3323,9 +3305,9 @@ const part001 = startSketchOn('XY')
.await;
// Assure we have diagnostics.
let diagnostics = server.diagnostics_map.get("file:///test.kcl").unwrap().clone();
// Check the diagnostics.
assert_eq!(diagnostics.len(), 1);
assert_diagnostic_count(server.diagnostics_map.get("file:///test.kcl").as_deref(), 1);
// Get the ast.
let ast = server.ast_map.get("file:///test.kcl");
@ -3365,9 +3347,9 @@ const NEW_LINT = 1"#
assert!(memory.is_none());
// Assure we have diagnostics.
let diagnostics = server.diagnostics_map.get("file:///test.kcl").unwrap().clone();
// Check the diagnostics.
assert_eq!(diagnostics.len(), 1);
assert_diagnostic_count(server.diagnostics_map.get("file:///test.kcl").as_deref(), 1);
}
#[tokio::test(flavor = "multi_thread")]
@ -3396,9 +3378,9 @@ const part001 = startSketchOn('XY')
.await;
// Assure we have diagnostics.
let diagnostics = server.diagnostics_map.get("file:///test.kcl").unwrap().clone();
// Check the diagnostics.
assert_eq!(diagnostics.len(), 1);
assert_diagnostic_count(server.diagnostics_map.get("file:///test.kcl").as_deref(), 1);
// Get the ast.
let ast = server.ast_map.get("file:///test.kcl").unwrap().clone();
@ -3456,9 +3438,9 @@ const NEW_LINT = 1"#
assert!(memory.is_none());
// Assure we have diagnostics.
let diagnostics = server.diagnostics_map.get("file:///test.kcl").unwrap().clone();
// Check the diagnostics.
assert_eq!(diagnostics.len(), 1);
assert_diagnostic_count(server.diagnostics_map.get("file:///test.kcl").as_deref(), 1);
}
#[tokio::test(flavor = "multi_thread")]
@ -3487,9 +3469,9 @@ const part001 = startSketchOn('XY')
.await;
// Assure we have diagnostics.
let diagnostics = server.diagnostics_map.get("file:///test.kcl").unwrap().clone();
// Check the diagnostics.
assert_eq!(diagnostics.len(), 1);
assert_diagnostic_count(server.diagnostics_map.get("file:///test.kcl").as_deref(), 1);
// Get the token map.
let token_map = server.token_map.get("file:///test.kcl").unwrap().clone();
@ -3555,9 +3537,9 @@ const part001 = startSketchOn('XY')
assert!(memory.is_none());
// Assure we have diagnostics.
let diagnostics = server.diagnostics_map.get("file:///test.kcl").unwrap().clone();
// Check the diagnostics.
assert_eq!(diagnostics.len(), 2);
assert_diagnostic_count(server.diagnostics_map.get("file:///test.kcl").as_deref(), 2);
}
#[tokio::test(flavor = "multi_thread")]

View File

@ -438,6 +438,7 @@ impl Node<CallExpression> {
let previous_dynamic_state = std::mem::replace(&mut exec_state.dynamic_state, fn_dynamic_state);
let result = func.call_fn(fn_args, exec_state, ctx.clone()).await.map_err(|e| {
// Add the call expression to the source ranges.
// TODO currently ignored by the frontend
e.add_source_ranges(vec![source_range])
});
exec_state.dynamic_state = previous_dynamic_state;

View File

@ -1,211 +0,0 @@
use winnow::{error::StrContext, stream::Stream};
use crate::{
errors::{KclError, KclErrorDetails},
parsing::token::Token,
SourceRange,
};
/// Accumulate context while backtracking errors
/// Very similar to [`winnow::error::ContextError`] type,
/// but the 'cause' field is always a [`KclError`],
/// instead of a dynamic [`std::error::Error`] trait object.
#[derive(Debug, Clone)]
pub struct ContextError<C = StrContext> {
pub context: Vec<C>,
pub cause: Option<KclError>,
}
/// An error which occurred during parsing.
///
/// In contrast to Winnow errors which may not be an actual error but just an attempted parse which
/// didn't work out, these are errors which are always a result of incorrect user code and which should
/// be presented to the user.
#[derive(Debug, Clone)]
pub(crate) struct ParseError {
pub source_range: SourceRange,
#[allow(dead_code)]
pub context_range: Option<SourceRange>,
pub message: String,
#[allow(dead_code)]
pub suggestion: Option<String>,
pub severity: Severity,
}
impl ParseError {
pub(super) fn err(source_range: SourceRange, message: impl ToString) -> ParseError {
ParseError {
source_range,
context_range: None,
message: message.to_string(),
suggestion: None,
severity: Severity::Error,
}
}
pub(super) fn with_suggestion(
source_range: SourceRange,
context_range: Option<SourceRange>,
message: impl ToString,
suggestion: Option<impl ToString>,
) -> ParseError {
ParseError {
source_range,
context_range,
message: message.to_string(),
suggestion: suggestion.map(|s| s.to_string()),
severity: Severity::Error,
}
}
#[cfg(test)]
pub fn apply_suggestion(&self, src: &str) -> Option<String> {
let suggestion = self.suggestion.as_ref()?;
Some(format!(
"{}{}{}",
&src[0..self.source_range.start()],
suggestion,
&src[self.source_range.end()..]
))
}
}
impl From<ParseError> for KclError {
fn from(err: ParseError) -> Self {
KclError::Syntax(KclErrorDetails {
source_ranges: vec![err.source_range],
message: err.message,
})
}
}
#[derive(Debug, Clone)]
pub(crate) enum Severity {
#[allow(dead_code)]
Warning,
Error,
}
/// Helper enum for the below conversion of Winnow errors into either a parse error or an unexpected
/// error.
// TODO we should optimise the size of SourceRange and thus ParseError
#[allow(clippy::large_enum_variant)]
pub(super) enum ErrorKind {
Parse(ParseError),
Internal(KclError),
}
impl ErrorKind {
#[cfg(test)]
pub fn unwrap_parse_error(self) -> ParseError {
match self {
ErrorKind::Parse(parse_error) => parse_error,
ErrorKind::Internal(_) => panic!(),
}
}
}
impl From<winnow::error::ParseError<&[Token], ContextError>> for ErrorKind {
fn from(err: winnow::error::ParseError<&[Token], ContextError>) -> Self {
let Some(last_token) = err.input().last() else {
return ErrorKind::Parse(ParseError::err(Default::default(), "file is empty"));
};
let (input, offset, err) = (err.input().to_vec(), err.offset(), err.into_inner());
if let Some(e) = err.cause {
return match e {
KclError::Syntax(details) => ErrorKind::Parse(ParseError::err(
details.source_ranges.into_iter().next().unwrap(),
details.message,
)),
e => ErrorKind::Internal(e),
};
}
// See docs on `offset`.
if offset >= input.len() {
let context = err.context.first();
return ErrorKind::Parse(ParseError::err(
last_token.as_source_range(),
match context {
Some(what) => format!("Unexpected end of file. The compiler {what}"),
None => "Unexpected end of file while still parsing".to_owned(),
},
));
}
let bad_token = &input[offset];
// TODO: Add the Winnow parser context to the error.
// See https://github.com/KittyCAD/modeling-app/issues/784
ErrorKind::Parse(ParseError::err(
bad_token.as_source_range(),
format!("Unexpected token: {}", bad_token.value),
))
}
}
impl<C> From<KclError> for ContextError<C> {
fn from(e: KclError) -> Self {
Self {
context: Default::default(),
cause: Some(e),
}
}
}
impl<C> std::default::Default for ContextError<C> {
fn default() -> Self {
Self {
context: Default::default(),
cause: None,
}
}
}
impl<I, C> winnow::error::ParserError<I> for ContextError<C>
where
I: Stream,
{
#[inline]
fn from_error_kind(_input: &I, _kind: winnow::error::ErrorKind) -> Self {
Self::default()
}
#[inline]
fn append(
self,
_input: &I,
_input_checkpoint: &<I as Stream>::Checkpoint,
_kind: winnow::error::ErrorKind,
) -> Self {
self
}
#[inline]
fn or(self, other: Self) -> Self {
other
}
}
impl<C, I> winnow::error::AddContext<I, C> for ContextError<C>
where
I: Stream,
{
#[inline]
fn add_context(mut self, _input: &I, _input_checkpoint: &<I as Stream>::Checkpoint, ctx: C) -> Self {
self.context.push(ctx);
self
}
}
impl<C, I> winnow::error::FromExternalError<I, KclError> for ContextError<C> {
#[inline]
fn from_external_error(_input: &I, _kind: winnow::error::ErrorKind, e: KclError) -> Self {
let mut err = Self::default();
{
err.cause = Some(e);
}
err
}
}

View File

@ -1,24 +1,25 @@
// TODO optimise size of CompilationError
#![allow(clippy::result_large_err)]
use crate::{
errors::{KclError, KclErrorDetails},
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>, KclError> {
pub fn parse(infix_tokens: Vec<BinaryExpressionToken>) -> Result<Node<BinaryExpression>, CompilationError> {
let rpn = postfix(infix_tokens);
evaluate(rpn)
}
/// Parses a list of tokens (in postfix order) into a binary expression tree.
fn evaluate(rpn: Vec<BinaryExpressionToken>) -> Result<Node<BinaryExpression>, KclError> {
let source_ranges = source_range(&rpn);
fn evaluate(rpn: Vec<BinaryExpressionToken>) -> Result<Node<BinaryExpression>, CompilationError> {
let source_range = source_range(&rpn);
let mut operand_stack: Vec<BinaryPart> = Vec::new();
let e = KclError::Internal(KclErrorDetails {
source_ranges,
message: "error parsing binary math expressions".to_owned(),
});
let e = CompilationError::fatal(source_range, "error parsing binary math expressions");
for item in rpn {
let expr = match item {
BinaryExpressionToken::Operator(operator) => {
@ -57,7 +58,7 @@ fn evaluate(rpn: Vec<BinaryExpressionToken>) -> Result<Node<BinaryExpression>, K
}
}
fn source_range(tokens: &[BinaryExpressionToken]) -> Vec<SourceRange> {
fn source_range(tokens: &[BinaryExpressionToken]) -> SourceRange {
let sources: Vec<_> = tokens
.iter()
.filter_map(|op| match op {
@ -66,8 +67,8 @@ fn source_range(tokens: &[BinaryExpressionToken]) -> Vec<SourceRange> {
})
.collect();
match (sources.first(), sources.last()) {
(Some((start, _, module_id)), Some((_, end, _))) => vec![SourceRange::new(*start, *end, *module_id)],
_ => Vec::new(),
(Some((start, _, module_id)), Some((_, end, _))) => SourceRange::new(*start, *end, *module_id),
_ => panic!(),
}
}

View File

@ -1,7 +1,5 @@
use parser::ParseContext;
use crate::{
errors::{KclError, KclErrorDetails},
errors::{CompilationError, KclError, KclErrorDetails},
parsing::{
ast::types::{Node, Program},
token::{Token, TokenType},
@ -10,7 +8,6 @@ use crate::{
};
pub(crate) mod ast;
mod error;
mod math;
pub(crate) mod parser;
pub(crate) mod token;
@ -86,7 +83,7 @@ pub fn parse_tokens(tokens: Vec<Token>) -> ParseResult {
/// - if there are no errors, then the Option will be Some
/// - if the Option is None, then there will be at least one error in the ParseContext.
#[derive(Debug, Clone)]
pub(crate) struct ParseResult(pub Result<(Option<Node<Program>>, ParseContext), KclError>);
pub(crate) struct ParseResult(pub Result<(Option<Node<Program>>, Vec<CompilationError>), KclError>);
impl ParseResult {
#[cfg(test)]
@ -101,23 +98,23 @@ impl ParseResult {
#[cfg(test)]
pub fn is_ok(&self) -> bool {
match &self.0 {
Ok((p, pc)) => p.is_some() && pc.errors.is_empty(),
Ok((p, errs)) => p.is_some() && !errs.iter().any(|e| e.severity.is_err()),
Err(_) => false,
}
}
#[cfg(test)]
#[track_caller]
pub fn unwrap_errs(&self) -> &[error::ParseError] {
&self.0.as_ref().unwrap().1.errors
pub fn unwrap_errs(&self) -> impl Iterator<Item = &CompilationError> {
self.0.as_ref().unwrap().1.iter().filter(|e| e.severity.is_err())
}
/// Treat parsing errors as an Error.
pub fn parse_errs_as_err(self) -> Result<Node<Program>, KclError> {
let (p, errs) = self.0?;
if !errs.errors.is_empty() {
// TODO could summarise all errors rather than just the first one.
return Err(errs.errors.into_iter().next().unwrap().into());
if let Some(err) = errs.iter().find(|e| e.severity.is_err()) {
return Err(KclError::Syntax(err.clone().into()));
}
match p {
Some(p) => Ok(p),
@ -126,21 +123,21 @@ impl ParseResult {
}
}
impl From<Result<(Option<Node<Program>>, ParseContext), KclError>> for ParseResult {
fn from(r: Result<(Option<Node<Program>>, ParseContext), KclError>) -> ParseResult {
impl From<Result<(Option<Node<Program>>, Vec<CompilationError>), KclError>> for ParseResult {
fn from(r: Result<(Option<Node<Program>>, Vec<CompilationError>), KclError>) -> ParseResult {
ParseResult(r)
}
}
impl From<(Option<Node<Program>>, ParseContext)> for ParseResult {
fn from(p: (Option<Node<Program>>, ParseContext)) -> ParseResult {
impl From<(Option<Node<Program>>, Vec<CompilationError>)> for ParseResult {
fn from(p: (Option<Node<Program>>, Vec<CompilationError>)) -> ParseResult {
ParseResult(Ok(p))
}
}
impl From<Node<Program>> for ParseResult {
fn from(p: Node<Program>) -> ParseResult {
ParseResult(Ok((Some(p), ParseContext::default())))
ParseResult(Ok((Some(p), vec![])))
}
}

File diff suppressed because it is too large Load Diff

View File

@ -24,24 +24,15 @@ impl ModuleId {
}
#[derive(Debug, Default, Deserialize, Serialize, PartialEq, Copy, Clone, ts_rs::TS, JsonSchema, Hash, Eq)]
#[ts(export, as = "TsSourceRange")]
#[ts(export, type = "[number, number, number]")]
pub struct SourceRange([usize; 3]);
#[derive(Debug, Default, Deserialize, Serialize, PartialEq, Copy, Clone, ts_rs::TS, JsonSchema, Hash, Eq)]
struct TsSourceRange(#[ts(type = "[number, number]")] [usize; 2]);
impl From<[usize; 3]> for SourceRange {
fn from(value: [usize; 3]) -> Self {
Self(value)
}
}
impl From<SourceRange> for TsSourceRange {
fn from(value: SourceRange) -> Self {
Self([value.start(), value.end()])
}
}
impl From<&SourceRange> for miette::SourceSpan {
fn from(source_range: &SourceRange) -> Self {
let length = source_range.end() - source_range.start();

View File

@ -296,7 +296,7 @@ async fn inner_pattern_transform<'a>(
// Build the vec of transforms, one for each repetition.
let mut transform = Vec::with_capacity(usize::try_from(total_instances).unwrap());
if total_instances < 1 {
return Err(KclError::Syntax(KclErrorDetails {
return Err(KclError::Semantic(KclErrorDetails {
source_ranges: vec![args.source_range],
message: MUST_HAVE_ONE_INSTANCE.to_owned(),
}));
@ -333,7 +333,7 @@ async fn inner_pattern_transform_2d<'a>(
// Build the vec of transforms, one for each repetition.
let mut transform = Vec::with_capacity(usize::try_from(total_instances).unwrap());
if total_instances < 1 {
return Err(KclError::Syntax(KclErrorDetails {
return Err(KclError::Semantic(KclErrorDetails {
source_ranges: vec![args.source_range],
message: MUST_HAVE_ONE_INSTANCE.to_owned(),
}));
@ -1035,7 +1035,7 @@ async fn pattern_circular(
return Ok(Geometries::from(geometry));
}
RepetitionsNeeded::Invalid => {
return Err(KclError::Syntax(KclErrorDetails {
return Err(KclError::Semantic(KclErrorDetails {
source_ranges: vec![args.source_range],
message: MUST_HAVE_ONE_INSTANCE.to_owned(),
}));

View File

@ -17,7 +17,7 @@ pub struct RequestBody {
/// This returns the bytes of the snapshot.
pub async fn execute_and_snapshot(code: &str, units: UnitLength) -> Result<image::DynamicImage, ExecError> {
let ctx = new_context(units, true).await?;
let program = Program::parse(code)?;
let program = Program::parse_no_errs(code)?;
do_execute_and_snapshot(&ctx, program).await.map(|(_state, snap)| snap)
}
@ -35,7 +35,7 @@ pub async fn execute_and_snapshot_ast(
pub async fn execute_and_snapshot_no_auth(code: &str, units: UnitLength) -> Result<image::DynamicImage, ExecError> {
let ctx = new_context(units, false).await?;
let program = Program::parse(code)?;
let program = Program::parse_no_errs(code)?;
do_execute_and_snapshot(&ctx, program).await.map(|(_state, snap)| snap)
}

View File

@ -162,10 +162,10 @@ pub fn deserialize_files(data: &[u8]) -> Result<JsValue, JsError> {
pub fn parse_wasm(js: &str) -> Result<JsValue, String> {
console_error_panic_hook::set_once();
let program = Program::parse(js).map_err(String::from)?;
let (program, errs) = Program::parse(js).map_err(String::from)?;
// The serde-wasm-bindgen does not work here because of weird HashMap issues so we use the
// gloo-serialize crate instead.
JsValue::from_serde(&program).map_err(|e| e.to_string())
JsValue::from_serde(&(program, errs)).map_err(|e| e.to_string())
}
// wasm_bindgen wrapper for recast

View File

@ -8,7 +8,7 @@ use pretty_assertions::assert_eq;
/// Setup the engine and parse code for an ast.
async fn setup(code: &str, name: &str) -> Result<(ExecutorContext, Program, ModuleId, uuid::Uuid)> {
let program = Program::parse(code)?;
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?;