Collect structured errors from parsing/executing KCL (#187)

Currently, syntax/semantic errors in the user's source code result in vanilla JS exceptions being thrown, so they show up in the console. Instead, this PR:

- Adds a new type KCLError
- KCL syntax/semantic errors when parsing/executing the source code now throw KCLErrors instead of vanilla JS exceptions.
- KCL errors are caught and logged to a new "Errors" panel, instead of the browser console.
This commit is contained in:
Adam Chalmers
2023-07-26 14:10:30 -05:00
committed by GitHub
parent 6838e96723
commit 0d010b60e5
7 changed files with 287 additions and 72 deletions

View File

@ -12,7 +12,7 @@ import {
addLineHighlight,
} from './editor/highlightextension'
import { Selections, useStore } from './useStore'
import { Logs } from './components/Logs'
import { Logs, KCLErrors } from './components/Logs'
import { PanelHeader } from './components/PanelHeader'
import { MemoryPanel } from './components/MemoryPanel'
import { useHotKeyListener } from './hooks/useHotKeyListener'
@ -22,6 +22,7 @@ import { EngineCommandManager } from './lang/std/engineConnection'
import { isOverlap } from './lib/utils'
import { SetToken } from './components/TokenInput'
import { AppHeader } from './components/AppHeader'
import { KCLError } from './lang/errors'
export function App() {
const cam = useRef()
@ -32,6 +33,7 @@ export function App() {
setSelectionRanges,
selectionRanges,
addLog,
addKCLError,
code,
setCode,
setAst,
@ -79,6 +81,7 @@ export function App() {
token: s.token,
formatCode: s.formatCode,
debugPanel: s.debugPanel,
addKCLError: s.addKCLError,
}))
// const onChange = React.useCallback((value: string, viewUpdate: ViewUpdate) => {
const onChange = (value: string, viewUpdate: ViewUpdate) => {
@ -249,9 +252,13 @@ export function App() {
setError()
})
} catch (e: any) {
setError('problem')
console.log(e)
addLog(e)
if (e instanceof KCLError) {
addKCLError(e)
} else {
setError('problem')
console.log(e)
addLog(e)
}
}
}
asyncWrap()
@ -261,7 +268,7 @@ export function App() {
<AppHeader />
<ModalContainer />
<Allotment snap={true}>
<Allotment vertical defaultSizes={[5, 400, 1, 1]} minSize={20}>
<Allotment vertical defaultSizes={[5, 400, 1, 1, 200]} minSize={20}>
<SetToken />
<div className="h-full flex flex-col items-start">
<PanelHeader title="Editor" />
@ -288,6 +295,7 @@ export function App() {
</div>
<MemoryPanel />
<Logs />
<KCLErrors />
</Allotment>
<Allotment vertical defaultSizes={[40, 400]} minSize={20}>
<Stream />

View File

@ -38,3 +38,36 @@ export const Logs = () => {
</div>
)
}
export const KCLErrors = () => {
const { kclErrors } = useStore(({ kclErrors }) => ({
kclErrors,
}))
useEffect(() => {
const element = document.querySelector('.console-tile')
if (element) {
element.scrollTop = element.scrollHeight - element.clientHeight
}
}, [kclErrors])
return (
<div>
<PanelHeader title="KCL Errors" />
<div className="h-full relative">
<div className="absolute inset-0 flex flex-col items-start">
<ReactJsonTypeHack
src={kclErrors}
collapsed={1}
collapseStringsAfterLength={60}
enableClipboard={false}
displayArrayKey={false}
displayDataTypes={false}
displayObjectSize={true}
indentWidth={2}
quotesOnKeys={false}
name={false}
/>
</div>
</div>
</div>
)
}

View File

@ -1,5 +1,6 @@
import { Token } from './tokeniser'
import { parseExpression } from './astMathExpressions'
import { KCLSyntaxError, KCLUnimplementedError } from './errors'
import {
BinaryPart,
BodyItem,
@ -128,6 +129,11 @@ function makeArguments(
}
}
const nextBraceOrCommaToken = nextMeaningfulToken(tokens, argumentToken.index)
if (nextBraceOrCommaToken.token == undefined) {
throw new KCLSyntaxError('Expected argument', [
[argumentToken.token.start, argumentToken.token.end],
])
}
const isIdentifierOrLiteral =
nextBraceOrCommaToken.token.type === 'comma' ||
nextBraceOrCommaToken.token.type === 'brace'
@ -282,7 +288,10 @@ function makeArguments(
) {
return makeArguments(tokens, argumentToken.index, previousArgs)
}
throw new Error('Expected a previous Argument if statement to match')
throw new KCLSyntaxError(
'Expected a previous Argument if statement to match',
[[argumentToken.token.start, argumentToken.token.end]]
)
}
function makeVariableDeclaration(
@ -406,14 +415,18 @@ function makeValue(
lastIndex: arrowFunctionLastIndex,
}
} else {
throw new Error('TODO - handle expression with braces')
throw new KCLUnimplementedError('expression with braces', [
[currentToken.start, currentToken.end],
])
}
}
if (currentToken.type === 'operator' && currentToken.value === '-') {
const { expression, lastIndex } = makeUnaryExpression(tokens, index)
return { value: expression, lastIndex }
}
throw new Error('Expected a previous Value if statement to match')
throw new KCLSyntaxError('Expected a previous Value if statement to match', [
[currentToken.start, currentToken.end],
])
}
function makeVariableDeclarators(
@ -505,7 +518,9 @@ function makeArrayElements(
nextToken.token.type === 'brace' && nextToken.token.value === ']'
const isComma = nextToken.token.type === 'comma'
if (!isClosingBrace && !isComma) {
throw new Error('Expected a comma or closing brace')
throw new KCLSyntaxError('Expected a comma or closing brace', [
[nextToken.token.start, nextToken.token.end],
])
}
const nextCallIndex = isClosingBrace
? nextToken.index
@ -617,7 +632,10 @@ function makeMemberExpression(
const keysInfo = collectObjectKeys(tokens, index)
const lastKey = keysInfo[keysInfo.length - 1]
const firstKey = keysInfo.shift()
if (!firstKey) throw new Error('Expected a key')
if (!firstKey)
throw new KCLSyntaxError('Expected a key', [
[currentToken.start, currentToken.end],
])
const root = makeIdentifier(tokens, index)
let memberExpression: MemberExpression = {
type: 'MemberExpression',
@ -807,7 +825,10 @@ function makePipeBody(
value = val.value
lastIndex = val.lastIndex
} else {
throw new Error('Expected a previous PipeValue if statement to match')
throw new KCLSyntaxError(
'Expected a previous PipeValue if statement to match',
[[currentToken.start, currentToken.end]]
)
}
const nextPipeToken = hasPipeOperator(tokens, index)
@ -1073,7 +1094,7 @@ function makeBody(
lastIndex,
}
}
throw new Error('Unexpected token')
throw new KCLSyntaxError('Unexpected token', [[token.start, token.end]])
}
export const abstractSyntaxTree = (tokens: Token[]): Program => {
const { body, nonCodeMeta } = makeBody({ tokens })
@ -1227,15 +1248,26 @@ export function findClosingBrace(
if (isFirstCall) {
searchOpeningBrace = currentToken.value
if (!['(', '{', '['].includes(searchOpeningBrace)) {
throw new Error(
`expected to be started on a opening brace ( { [, instead found '${searchOpeningBrace}'`
throw new KCLSyntaxError(
`expected to be started on a opening brace ( { [, instead found '${searchOpeningBrace}'`,
[[currentToken.start, currentToken.end]]
)
}
}
const foundClosingBrace =
_braceCount === 1 &&
currentToken.value === closingBraceMap[searchOpeningBrace]
const foundClosingBrace = (() => {
try {
return (
_braceCount === 1 &&
currentToken.value === closingBraceMap[searchOpeningBrace]
)
} catch (e: any) {
throw new KCLSyntaxError('Missing a closing brace', [
[currentToken.start, currentToken.end],
])
}
})()
const foundAnotherOpeningBrace = currentToken.value === searchOpeningBrace
const foundAnotherClosingBrace =
currentToken.value === closingBraceMap[searchOpeningBrace]

View File

@ -10,6 +10,7 @@ import {
isNotCodeToken,
} from './abstractSyntaxTree'
import { Token } from './tokeniser'
import { KCLSyntaxError } from './errors'
export function reversePolishNotation(
tokens: Token[],
@ -82,7 +83,9 @@ export function reversePolishNotation(
if (isNotCodeToken(currentToken)) {
return reversePolishNotation(tokens.slice(1), previousPostfix, operators)
}
throw new Error('Unknown token')
throw new KCLSyntaxError('Unknown token', [
[currentToken.start, currentToken.end],
])
}
interface ParenthesisToken {
@ -204,21 +207,27 @@ const buildTree = (
}
export function parseExpression(tokens: Token[]): BinaryExpression {
const treeWithMabyeBadTopLevelStartEnd = buildTree(
const treeWithMaybeBadTopLevelStartEnd = buildTree(
reversePolishNotation(tokens)
)
const left = treeWithMabyeBadTopLevelStartEnd?.left as any
const start = left?.startExtended || treeWithMabyeBadTopLevelStartEnd?.start
const left = treeWithMaybeBadTopLevelStartEnd?.left as any
const start = left?.startExtended || treeWithMaybeBadTopLevelStartEnd?.start
if (left == undefined || left == null) {
throw new KCLSyntaxError(
'syntax',
tokens.map((token) => [token.start, token.end])
) // Add text
}
delete left.startExtended
delete left.endExtended
const right = treeWithMabyeBadTopLevelStartEnd?.right as any
const end = right?.endExtended || treeWithMabyeBadTopLevelStartEnd?.end
const right = treeWithMaybeBadTopLevelStartEnd?.right as any
const end = right?.endExtended || treeWithMaybeBadTopLevelStartEnd?.end
delete right.startExtended
delete right.endExtended
const tree: BinaryExpression = {
...treeWithMabyeBadTopLevelStartEnd,
...treeWithMaybeBadTopLevelStartEnd,
start,
end,
left,
@ -232,7 +241,7 @@ function _precedence(operator: Token): number {
}
export function precedence(operator: string): number {
// might be useful for refenecne to make it match
// might be useful for reference to make it match
// another commonly used lang https://www.w3schools.com/js/js_precedence.asp
if (['+', '-'].includes(operator)) {
return 11

57
src/lang/errors.ts Normal file
View File

@ -0,0 +1,57 @@
export class KCLError {
kind: string | undefined
sourceRanges: [number, number][]
msg: string
constructor(
kind: string | undefined,
msg: string,
sourceRanges: [number, number][]
) {
this.kind = kind
this.msg = msg
this.sourceRanges = sourceRanges
Object.setPrototypeOf(this, KCLError.prototype)
}
}
export class KCLSyntaxError extends KCLError {
constructor(msg: string, sourceRanges: [number, number][]) {
super('syntax', msg, sourceRanges)
Object.setPrototypeOf(this, KCLSyntaxError.prototype)
}
}
export class KCLSemanticError extends KCLError {
constructor(msg: string, sourceRanges: [number, number][]) {
super('semantic', msg, sourceRanges)
Object.setPrototypeOf(this, KCLSemanticError.prototype)
}
}
export class KCLTypeError extends KCLError {
constructor(msg: string, sourceRanges: [number, number][]) {
super('type', msg, sourceRanges)
Object.setPrototypeOf(this, KCLTypeError.prototype)
}
}
export class KCLUnimplementedError extends KCLError {
constructor(msg: string, sourceRanges: [number, number][]) {
super('unimplemented feature', msg, sourceRanges)
Object.setPrototypeOf(this, KCLUnimplementedError.prototype)
}
}
export class KCLValueAlreadyDefined extends KCLError {
constructor(key: string, sourceRanges: [number, number][]) {
super('name', `Key ${key} was already defined elsewhere`, sourceRanges)
Object.setPrototypeOf(this, KCLValueAlreadyDefined.prototype)
}
}
export class KCLUndefinedValueError extends KCLError {
constructor(key: string, sourceRanges: [number, number][]) {
super('name', `Key ${key} has not been defined`, sourceRanges)
Object.setPrototypeOf(this, KCLUndefinedValueError.prototype)
}
}

View File

@ -12,6 +12,13 @@ import {
} from './abstractSyntaxTreeTypes'
import { InternalFnNames } from './std/stdTypes'
import { internalFns } from './std/std'
import {
KCLUndefinedValueError,
KCLValueAlreadyDefined,
KCLSyntaxError,
KCLSemanticError,
KCLTypeError,
} from './errors'
import {
EngineCommandManager,
ArtifactMap,
@ -124,11 +131,12 @@ export interface ProgramMemory {
const addItemToMemory = (
programMemory: ProgramMemory,
key: string,
sourceRange: [[number, number]],
value: MemoryItem | Promise<MemoryItem>
) => {
const _programMemory = programMemory
if (_programMemory.root[key] || _programMemory.pendingMemory[key]) {
throw new Error(`Memory item ${key} already exists`)
throw new KCLValueAlreadyDefined(key, sourceRange)
}
if (value instanceof Promise) {
_programMemory.pendingMemory[key] = value
@ -155,7 +163,8 @@ const promisifyMemoryItem = async (obj: MemoryItem) => {
const getMemoryItem = async (
programMemory: ProgramMemory,
key: string
key: string,
sourceRanges: [number, number][]
): Promise<MemoryItem> => {
if (programMemory.root[key]) {
return programMemory.root[key]
@ -163,7 +172,7 @@ const getMemoryItem = async (
if (programMemory.pendingMemory[key]) {
return programMemory.pendingMemory[key] as Promise<MemoryItem>
}
throw new Error(`Memory item ${key} not found`)
throw new KCLUndefinedValueError(`Memory item ${key} not found`, sourceRanges)
}
export const executor = async (
@ -249,27 +258,43 @@ export const _executor = async (
_programMemory = addItemToMemory(
_programMemory,
variableName,
[sourceRange],
value
)
} else {
_programMemory = addItemToMemory(_programMemory, variableName, {
type: 'userVal',
value,
__meta,
})
_programMemory = addItemToMemory(
_programMemory,
variableName,
[sourceRange],
{
type: 'userVal',
value,
__meta,
}
)
}
} else if (declaration.init.type === 'Identifier') {
_programMemory = addItemToMemory(_programMemory, variableName, {
type: 'userVal',
value: _programMemory.root[declaration.init.name].value,
__meta,
})
_programMemory = addItemToMemory(
_programMemory,
variableName,
[sourceRange],
{
type: 'userVal',
value: _programMemory.root[declaration.init.name].value,
__meta,
}
)
} else if (declaration.init.type === 'Literal') {
_programMemory = addItemToMemory(_programMemory, variableName, {
type: 'userVal',
value: declaration.init.value,
__meta,
})
_programMemory = addItemToMemory(
_programMemory,
variableName,
[sourceRange],
{
type: 'userVal',
value: declaration.init.value,
__meta,
}
)
} else if (declaration.init.type === 'BinaryExpression') {
const prom = getBinaryExpressionResult(
declaration.init,
@ -280,6 +305,7 @@ export const _executor = async (
_programMemory = addItemToMemory(
_programMemory,
variableName,
[sourceRange],
promisifyMemoryItem({
type: 'userVal',
value: prom,
@ -296,6 +322,7 @@ export const _executor = async (
_programMemory = addItemToMemory(
_programMemory,
variableName,
[sourceRange],
promisifyMemoryItem({
type: 'userVal',
value: prom,
@ -332,7 +359,11 @@ export const _executor = async (
value: await prom,
}
} else if (element.type === 'Identifier') {
const node = await getMemoryItem(_programMemory, element.name)
const node = await getMemoryItem(
_programMemory,
element.name,
[[element.start, element.end]]
)
return {
value: node.value,
__meta: node.__meta[node.__meta.length - 1],
@ -348,8 +379,11 @@ export const _executor = async (
value: await prom,
}
} else {
throw new Error(
`Unexpected element type ${element.type} in array expression`
throw new KCLSyntaxError(
`Unexpected element type ${element.type} in array expression`,
// TODO: Refactor this whole block into a `switch` so that we have a specific
// type here and can put a sourceRange.
[]
)
}
}
@ -358,11 +392,16 @@ export const _executor = async (
const meta = awaitedValueInfo
.filter(({ __meta }) => __meta)
.map(({ __meta }) => __meta) as Metadata[]
_programMemory = addItemToMemory(_programMemory, variableName, {
type: 'userVal',
value: awaitedValueInfo.map(({ value }) => value),
__meta: [...__meta, ...meta],
})
_programMemory = addItemToMemory(
_programMemory,
variableName,
[sourceRange],
{
type: 'userVal',
value: awaitedValueInfo.map(({ value }) => value),
__meta: [...__meta, ...meta],
}
)
} else if (declaration.init.type === 'ObjectExpression') {
const prom = executeObjectExpression(
_programMemory,
@ -373,6 +412,7 @@ export const _executor = async (
_programMemory = addItemToMemory(
_programMemory,
variableName,
[sourceRange],
promisifyMemoryItem({
type: 'userVal',
value: prom,
@ -385,6 +425,7 @@ export const _executor = async (
_programMemory = addItemToMemory(
_programMemory,
declaration.id.name,
[sourceRange],
{
type: 'userVal',
value: async (...args: any[]) => {
@ -397,20 +438,27 @@ export const _executor = async (
},
}
if (args.length > fnInit.params.length) {
throw new Error(
`Too many arguments passed to function ${declaration.id.name}`
throw new KCLSyntaxError(
`Too many arguments passed to function ${declaration.id.name}`,
[[declaration.start, declaration.end]]
)
} else if (args.length < fnInit.params.length) {
throw new Error(
`Too few arguments passed to function ${declaration.id.name}`
throw new KCLSyntaxError(
`Too few arguments passed to function ${declaration.id.name}`,
[[declaration.start, declaration.end]]
)
}
fnInit.params.forEach((param, index) => {
fnMemory = addItemToMemory(fnMemory, param.name, {
type: 'userVal',
value: args[index],
__meta,
})
fnMemory = addItemToMemory(
fnMemory,
param.name,
[sourceRange],
{
type: 'userVal',
value: args[index],
__meta,
}
)
})
const prom = _executor(
fnInit.body,
@ -437,6 +485,7 @@ export const _executor = async (
_programMemory = addItemToMemory(
_programMemory,
variableName,
[sourceRange],
promisifyMemoryItem({
type: 'userVal',
value: prom,
@ -454,6 +503,7 @@ export const _executor = async (
_programMemory = addItemToMemory(
_programMemory,
variableName,
[sourceRange],
prom.then((a) => {
return a?.type === 'sketchGroup' || a?.type === 'extrudeGroup'
? a
@ -465,8 +515,9 @@ export const _executor = async (
})
)
} else {
throw new Error(
'Unsupported declaration type: ' + declaration.init.type
throw new KCLSyntaxError(
'Unsupported declaration type: ' + declaration.init.type,
[[declaration.start, declaration.end]]
)
}
})
@ -483,10 +534,18 @@ export const _executor = async (
})
if ('show' === functionName) {
if (options.bodyType !== 'root') {
throw new Error(`Cannot call ${functionName} outside of a root`)
throw new KCLSemanticError(
`Cannot call ${functionName} outside of a root`,
[[statement.start, statement.end]]
)
}
_programMemory.return = expression.arguments as any // todo memory redo
} else {
if (_programMemory.root[functionName] == undefined) {
throw new KCLSemanticError(`No such name ${functionName} defined`, [
[statement.start, statement.end],
])
}
_programMemory.root[functionName].value(...args)
}
}
@ -693,7 +752,9 @@ async function executePipeBody(
)
}
throw new Error('Invalid pipe expression')
throw new KCLSyntaxError('Invalid pipe expression', [
[expression.start, expression.end],
])
}
async function executeObjectExpression(
@ -742,7 +803,9 @@ async function executeObjectExpression(
obj[property.key.name] = await prom
} else if (property.value.type === 'Identifier') {
obj[property.key.name] = (
await getMemoryItem(_programMemory, property.value.name)
await getMemoryItem(_programMemory, property.value.name, [
[property.value.start, property.value.end],
])
).value
} else if (property.value.type === 'ObjectExpression') {
const prom = executeObjectExpression(
@ -780,13 +843,15 @@ async function executeObjectExpression(
proms.push(prom)
obj[property.key.name] = await prom
} else {
throw new Error(
`Unexpected property type ${property.value.type} in object expression`
throw new KCLSyntaxError(
`Unexpected property type ${property.value.type} in object expression`,
[[property.value.start, property.value.end]]
)
}
} else {
throw new Error(
`Unexpected property type ${property.type} in object expression`
throw new KCLSyntaxError(
`Unexpected property type ${property.type} in object expression`,
[[property.value.start, property.value.end]]
)
}
})
@ -850,7 +915,7 @@ async function executeArrayExpression(
}
)
}
throw new Error('Invalid argument type')
throw new KCLTypeError('Invalid argument type', [[el.start, el.end]])
})
)
}
@ -891,7 +956,9 @@ async function executeCallExpression(
return arg.value
} else if (arg.type === 'Identifier') {
await new Promise((r) => setTimeout(r)) // push into next even loop, but also probably should fix this
const temp = await getMemoryItem(programMemory, arg.name)
const temp = await getMemoryItem(programMemory, arg.name, [
[arg.start, arg.end],
])
return temp?.type === 'userVal' ? temp.value : temp
} else if (arg.type === 'PipeSubstitution') {
return previousResults[expressionIndex - 1]
@ -933,7 +1000,9 @@ async function executeCallExpression(
_pipeInfo
)
}
throw new Error('Invalid argument type in function call')
throw new KCLSyntaxError('Invalid argument type in function call', [
[arg.start, arg.end],
])
})
)
if (functionName in internalFns) {

View File

@ -20,6 +20,7 @@ import {
SourceRangeMap,
EngineCommandManager,
} from './lang/std/engineConnection'
import { KCLError } from './lang/errors'
export type Selection = {
type: 'default' | 'line-end' | 'line-mid'
@ -122,6 +123,8 @@ export interface StoreState {
setGuiMode: (guiMode: GuiModes) => void
logs: string[]
addLog: (log: string) => void
kclErrors: KCLError[]
addKCLError: (err: KCLError) => void
resetLogs: () => void
ast: Program | null
setAst: (ast: Program | null) => void
@ -251,6 +254,10 @@ export const useStore = create<StoreState>()(
set((state) => ({ logs: [...state.logs, log] }))
}
},
kclErrors: [],
addKCLError: (e) => {
set((state) => ({ kclErrors: [...state.kclErrors, e] }))
},
resetLogs: () => {
set({ logs: [] })
},