Skip to content

Commit

Permalink
[feat] Add parsing error highlights
Browse files Browse the repository at this point in the history
  • Loading branch information
LeahHirst committed Oct 2, 2024
1 parent 29f09cd commit 49a5048
Show file tree
Hide file tree
Showing 6 changed files with 200 additions and 55 deletions.
71 changes: 44 additions & 27 deletions apps/site/src/components/playground/Editor.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, { useEffect, useMemo, useState } from 'react';
import { Editor as MonacoEditor, useMonaco, type Monaco } from '@monaco-editor/react';
import { Editor as MonacoEditor, useMonaco } from '@monaco-editor/react';
import styled from '@emotion/styled';
import { NospaceIR, Instruction } from '@repo/parser';
import { NospaceIR } from '@repo/parser';
import Button from './Button';
import Dropdown, { DropdownAction } from './Dropdown';
import { registerNospace, registerWhitespace, registerNossembly } from '@repo/language-support'
Expand Down Expand Up @@ -65,17 +65,47 @@ export default function Editor() {
}

const editor = monaco.editor.getEditors()[0];

editor.onDidChangeCursorPosition((e) => {
setCursorPos([e.position.lineNumber, e.position.column]);
});

registerNospace(monaco);
registerWhitespace(monaco);
registerNossembly(monaco);

monaco.editor.setModelLanguage(editor.getModel()!, 'nossembly');
}, [monaco]);

const highlightErrors = React.useCallback((lang: string) => {
if (!monaco) {
return;
}

const editor = monaco.editor.getEditors()[0];

// Todo: debounce
const parsed = getProgram(lang, editor.getValue());
monaco.editor.setModelMarkers(editor.getModel()!, 'owner', []);
monaco.editor.setModelMarkers(editor.getModel()!, 'owner', parsed.parseErrors.map(error => ({
startLineNumber: error.meta.startLn + 1,
endLineNumber: error.meta.endLn + 1,
startColumn: error.meta.startCol + 1,
endColumn: error.meta.endCol + 1,
message: `ParseError: ${error.message}`,
severity: monaco.MarkerSeverity.Error,
})));
}, [monaco, language]);

React.useEffect(() => {
if (!monaco) {
return;
}

const editor = monaco.editor.getEditors()[0];

editor.onDidChangeCursorPosition((e) => {
setCursorPos([e.position.lineNumber, e.position.column]);
highlightErrors(language);
});
}, [monaco, language, highlightErrors]);

const changeLanguage = React.useCallback((lang: string) => {
if (!monaco) {
return;
Expand All @@ -98,16 +128,6 @@ export default function Editor() {
text,
forceMoveMarkers: true,
}]);
monaco.editor.setModelMarkers(editor.getModel()!, 'owner', [
{
startLineNumber: lnNumber,
endLineNumber: lnNumber,
startColumn: colNumber,
endColumn: colNumber + 1,
message: 'TypeError',
severity: monaco.MarkerSeverity.Error,
},
]);
}, [monaco, lnNumber, colNumber]);

const monacoOptions = useMemo(() => ({
Expand All @@ -123,18 +143,9 @@ export default function Editor() {
nonBasicASCII: false,
ambiguousCharacters: false,
},
insertSpaces: false,
} as editor.IStandaloneEditorConstructionOptions), []);

useEffect(() => {
if (!monaco) {
return;
}

monaco.editor.getEditors()[0].updateOptions({
autoIndent: language === 'Nossembly' ? 'advanced' : 'none',
});
}, [monaco, language]);

return (
<Container>
<Toolbar>
Expand All @@ -144,8 +155,14 @@ export default function Editor() {
key={lang}
active={language === lang}
onClick={() => {
monaco?.editor.setModelLanguage(monaco.editor.getEditors()[0].getModel()!, lang.toLowerCase());
const editor = monaco?.editor.getEditors()[0];
monaco?.editor.setModelLanguage(editor?.getModel()!, lang.toLowerCase());
editor?.updateOptions({
autoIndent: lang === 'Nossembly' ? 'advanced' : 'none',
fontSize: lang === 'Nossembly' ? 16.0001 : 16, // horrible hack to force Monaco to update options immediately
});
changeLanguage(lang);
highlightErrors(lang);
}}
>
{lang}
Expand Down
24 changes: 10 additions & 14 deletions packages/parser/src/NospaceIR.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
import { IRArgs, Instruction, Operation, TokenMap, isLabeledOperation, isNumericInstruction, isNumericOperation } from "./interfaces";
import { IRArgs, Instruction, Operation, ParseError, TokenMap, isLabeledOperation, isNumericInstruction, isNumericOperation } from "./interfaces";
import { parseNossembly } from "./parseNossembly";
import { parseRaw } from "./parseRaw";
import { serializeNumber } from "./utils";
import { irToNospace, irToWhitespace, serializeNumber } from "./utils";

export class NospaceIR {
public readonly operations: Operation[];

public readonly tokens: TokenMap;

private constructor({ operations, tokens }: IRArgs) {
public readonly parseErrors: ParseError[];

private constructor({ operations, tokens, parseErrors }: IRArgs) {
this.operations = operations;
this.tokens = tokens;
this.parseErrors = parseErrors;
}

toNossembly() {
Expand Down Expand Up @@ -38,26 +41,19 @@ export class NospaceIR {
}

toWhitespace() {
return this.operations
return irToWhitespace(this.operations
.filter(x => ![
Instruction.Cast,
Instruction.Assert
].includes(x.instruction))
.map(x => this.normalizeOperation(x).filter(Boolean).join(''))
.join('')
.replace(/s/g, ' ')
.replace(/t/g, '\t')
.replace(/n/g, '\n');
.join(''));
}

toNospace() {
return this.operations
return irToNospace(this.operations
.map(x => this.normalizeOperation(x).filter(Boolean).join(''))
.join('')
.replace(/s/g, '\u200B')
.replace(/t/g, '\u200C')
.replace(/n/g, '\u200D')
.replace(/x/g, '\u2060');
.join(''))
}

static fromNossembly(nossembly: string): NospaceIR {
Expand Down
25 changes: 25 additions & 0 deletions packages/parser/src/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export enum Instruction {
Retrieve = 'ttt',
Cast = 'xs',
Assert = 'xt',
Unknown = 'unknown',
};

export const LabelledInstructions = [
Expand Down Expand Up @@ -66,6 +67,29 @@ export type Operation =
| ParameterizedInstruction
| OperationBase<Exclude<Instruction, ParameterizedInstruction['instruction']>>;

type Error = {
message: string;
meta: OperationMeta;
};

export type UnknownInstructionError = Error & {
type: 'unknown_instruction';

};

export type ArgumentError = Error & {
type: 'argument'
};

export type PragmaError = Error & {
type: 'pragma';
};

export type ParseError =
| UnknownInstructionError
| ArgumentError
| PragmaError;

export enum Type {
Never = 'ttn',
Any = 'tsn',
Expand All @@ -78,6 +102,7 @@ export type TokenMap = Map<string, string>;
export type IRArgs = {
operations: Operation[];
tokens: TokenMap;
parseErrors: ParseError[];
};

export function isNumericInstruction(instruction: Instruction): instruction is typeof NumericInstructions[number] {
Expand Down
83 changes: 75 additions & 8 deletions packages/parser/src/parseNossembly.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Instruction, Operation, IRArgs, TokenMap, isNumericInstruction, NumericOperation, isLabelledInstruction } from "./interfaces";
import { Instruction, Operation, IRArgs, TokenMap, isNumericInstruction, NumericOperation, isLabelledInstruction, ParseError } from "./interfaces";
import { serializeNumber } from "./utils";

function hasPragma(line: string) {
Expand All @@ -8,19 +8,33 @@ function hasPragma(line: string) {
export function parseNossembly(nossembly: string): IRArgs {
const operations: Operation[] = [];
const tokens: Map<string, string> = new Map();
const errors: ParseError[] = [];
const globals: Map<string, string> = new Map();

nossembly
.split('\n')
.filter((line) => !!line.trim())
.forEach((line, lineNum) => {
if (!line.trim() || line.trim().startsWith('# ')) {
return;
}

let parts = line.trim().replace(/# /g, '').split(' ');

if (hasPragma(line)) {
const [pragma, key, value, ...rest] = parts;

if (!key || !value) {
throw new Error(`Pragmas must specify both a key and value (L${lineNum})`);
errors.push({
type: 'pragma',
message: 'Pragmas must specify both a key and value',
meta: {
startLn: lineNum,
startCol: line.indexOf(pragma ?? ''),
endCol: line.length,
endLn: lineNum,
},
});
return;
}

switch (pragma) {
Expand All @@ -32,50 +46,103 @@ export function parseNossembly(nossembly: string): IRArgs {
}
case '#define': {
if (rest.length > 0) {
throw new Error(`Incorrect number of arguments for #define pragma on L${lineNum} (expected 2, got ${rest.length + 2})`);
errors.push({
type: 'pragma',
message: 'Incorrect number of arguments for #define pragma',
meta: {
startLn: lineNum,
startCol: line.indexOf(pragma ?? ''),
endCol: line.length,
endLn: lineNum,
},
});
return;
}

globals.set(key, value);
return;
}
default: throw new Error(`Unknown pragma "${pragma}" (L${lineNum})`);
default: {
errors.push({
type: 'pragma',
message: `Unknown pragma "${pragma}"`,
meta: {
startLn: lineNum,
startCol: line.indexOf(pragma ?? ''),
endCol: line.length,
endLn: lineNum,
},
});
return;
}
}
}

const [instructionName, arg, ...rest] = parts;
const [instructionName, arg] = parts;
const [_, instruction] = Object.entries(Instruction)
.find(([name]) => name === instructionName) ?? [];

if (!instruction) {
throw new Error(`Unrecognized instruction "${instructionName}" (L${lineNum})`);
errors.push({
type: 'unknown_instruction',
message: `Unrecognized instruction "${instructionName}"`,
meta: {
startLn: lineNum,
startCol: line.indexOf(instructionName ?? ''),
endCol: line.length,
endLn: lineNum,
},
});
return;
}

const meta = {
startLn: lineNum,
startCol: line.indexOf(instructionName ?? ''),
endLn: lineNum,
endCol: line.length,
};

if (isNumericInstruction(instruction)) {
operations.push({
instruction,
argument: Number(arg),
meta,
});
} else if (isLabelledInstruction(instruction)) {
if (!arg) {
throw new Error('');
errors.push({
type: 'argument',
message: `An argument must be provided to the ${instructionName} instruction`,
meta: {
startLn: lineNum,
startCol: line.indexOf(instructionName ?? ''),
endCol: line.length,
endLn: lineNum,
},
});
return;
}
if (!tokens.has(arg)) {
tokens.set(arg, serializeNumber(tokens.size));
}
operations.push({
instruction,
argument: arg,
meta,
});
} else {
operations.push({
instruction,
argument: undefined,
meta,
});
}
});

return {
operations,
tokens,
parseErrors: errors,
};
}
Loading

0 comments on commit 49a5048

Please sign in to comment.