diff --git a/src/App.tsx b/src/App.tsx index 2969b2402..2f3cb15a1 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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() { - + @@ -288,6 +295,7 @@ export function App() { + diff --git a/src/components/Logs.tsx b/src/components/Logs.tsx index d7bcc5dac..bafb14935 100644 --- a/src/components/Logs.tsx +++ b/src/components/Logs.tsx @@ -38,3 +38,36 @@ export const Logs = () => { ) } + +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 ( + + + + + + + + + ) +} diff --git a/src/lang/abstractSyntaxTree.ts b/src/lang/abstractSyntaxTree.ts index c27708aab..7293bbd17 100644 --- a/src/lang/abstractSyntaxTree.ts +++ b/src/lang/abstractSyntaxTree.ts @@ -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] diff --git a/src/lang/astMathExpressions.ts b/src/lang/astMathExpressions.ts index b506e428b..267f168f3 100644 --- a/src/lang/astMathExpressions.ts +++ b/src/lang/astMathExpressions.ts @@ -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 diff --git a/src/lang/errors.ts b/src/lang/errors.ts new file mode 100644 index 000000000..f1b629040 --- /dev/null +++ b/src/lang/errors.ts @@ -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) + } +} diff --git a/src/lang/executor.ts b/src/lang/executor.ts index 9e9a78f91..83361e7df 100644 --- a/src/lang/executor.ts +++ b/src/lang/executor.ts @@ -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 ) => { 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 => { 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 } - 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) { diff --git a/src/useStore.ts b/src/useStore.ts index b238be56f..32e58a0c2 100644 --- a/src/useStore.ts +++ b/src/useStore.ts @@ -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()( set((state) => ({ logs: [...state.logs, log] })) } }, + kclErrors: [], + addKCLError: (e) => { + set((state) => ({ kclErrors: [...state.kclErrors, e] })) + }, resetLogs: () => { set({ logs: [] }) },