diff --git a/README.md b/README.md index a351e65..8823e1e 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,13 @@ It shows how to integrate ANTLR with monaco. ![Calc Example](./doc/images/calc_example.png) +## Features + +* Text editing provided by [monaco](https://github.com/microsoft/monaco-editor) +* Syntax highlighting for custom language +* Syntax error reporting with expected alternatives +* Semantic error reporting for undeclared and redeclared variables + ## Generating the lexer and the parser ``` diff --git a/src/main/typescript/ParserFacade.ts b/src/main/typescript/ParserFacade.ts index cc9000a..206eee1 100644 --- a/src/main/typescript/ParserFacade.ts +++ b/src/main/typescript/ParserFacade.ts @@ -137,5 +137,65 @@ export function validate(input) : Error[] { parser._errHandler = new CalcErrorStrategy(); const tree = parser.compilationUnit(); + + const scope = []; // ? Should be a Set but can't use it unless typescript targets ES6+ + + for (const input of tree.inputs) { + + if (input.ID() === null) continue; + + const inputIdentifier = input.ID().symbol; + + if (scope.some(x => x === inputIdentifier.text)) { + errors.push(createErrorAt(inputIdentifier, "input already declared")); + } + else { + scope.push(inputIdentifier.text); + } + } + + for (const calculation of tree.calcs) { + validateExpression(calculation.value); + + if (!scope.some(x => x === calculation.target.text)) { + scope.push(calculation.target.text); + } + } + + for (const output of tree.outputs) { + + if (output.ID() === null) continue; + + const outputIdentifier = output.ID().symbol; + + if (outputIdentifier.text === "") continue; + + if (!scope.some(x => x === outputIdentifier.text)) { + errors.push(createErrorAt(outputIdentifier, "undeclared symbol")); + } + } + + function validateExpression(expression) { + if (expression === null || expression.children === null) return; + + for (const child of expression.children) { + if (child.children) { + validateExpression(child); + } + else if (child.symbol && child.symbol.type == CalcLexer.ID) { + + const symbol = child.symbol; + + if (!scope.some(x => x == symbol.text)) { + errors.push(createErrorAt(symbol, "undeclared symbol")); + } + } + } + } + return errors; } + +function createErrorAt(node, message) { + return new Error(node.line, node.line, node.column + 1, node.column + node.stop - node.start + 2, message); +} diff --git a/src/test/javascript/parsingTest.js b/src/test/javascript/parsingTest.js index 9294653..78731eb 100644 --- a/src/test/javascript/parsingTest.js +++ b/src/test/javascript/parsingTest.js @@ -64,12 +64,12 @@ describe('Basic parsing of simple script', function () { describe('Validation of simple errors on single lines', function () { describe('should have recognize missing operand', function () { - parseAndCheckErrors("o = i + \n", [ + parseAndCheckErrors("o = 1 + \n", [ new parserFacade.Error(1, 1, 8, 9, "mismatched input '\\n' expecting {NUMBER_LIT, ID, '(', '-'}") ]); }); describe('should have recognize extra operator', function () { - parseAndCheckErrors("o = i +* 2 \n", [ + parseAndCheckErrors("o = 1 +* 2 \n", [ new parserFacade.Error(1, 1, 7, 8, "extraneous input '*' expecting {NUMBER_LIT, ID, '(', '-'}") ]); }); @@ -131,3 +131,65 @@ describe('Unrecognized tokens cause errors', function () { ]); }); }); + +describe('Semantic validation', function () { + describe('should report input already declared', function () { + let input = "input i\ninput i\n"; + parseAndCheckErrors(input, [ + new parserFacade.Error(2, 2, 7, 8, "input already declared") + ]); + }); + describe('should report input already declared twice', function () { + let input = "input i\ninput i\ninput i\n"; + parseAndCheckErrors(input, [ + new parserFacade.Error(2, 2, 7, 8, "input already declared"), + new parserFacade.Error(3, 3, 7, 8, "input already declared") + ]); + }); + describe('should report undeclared symbol', function () { + let input = "o = i\n"; + parseAndCheckErrors(input, [ + new parserFacade.Error(1, 1, 5, 6, "undeclared symbol") + ]); + }); + describe('should report undeclared symbol in line 4', function () { + let input = "input i\no = i\no = o\no = u\n"; + parseAndCheckErrors(input, [ + new parserFacade.Error(4, 4, 5, 6, "undeclared symbol") + ]); + }); + describe('should report undeclared symbol for output', function () { + let input = "output o\n"; + parseAndCheckErrors(input, [ + new parserFacade.Error(1, 1, 8, 9, "undeclared symbol") + ]); + }); + describe('should report undeclared symbol for second output', function () { + let input = "input i\no = i\noutput o\noutput x\n"; + parseAndCheckErrors(input, [ + new parserFacade.Error(4, 4, 8, 9, "undeclared symbol") + ]); + }); +}); + +describe('Semantic validation of examples being edited', function () { + describe('deleting input identifier', function () { + let input = "input a\ninput\ninput a\n"; + parseAndCheckErrors(input, [ + new parserFacade.Error(2, 2, 5, 6, "missing ID at '\\n'"), + new parserFacade.Error(3, 3, 7, 8, "input already declared") + ]); + }); + describe('deleting calculation value still declares the target', function () { + let input = "a = 1 + 1\nb = \nc = a + b\n"; + parseAndCheckErrors(input, [ + new parserFacade.Error(2, 2, 4, 5, "mismatched input '\\n' expecting {NUMBER_LIT, ID, '(', '-'}"), + ]); + }); + describe('deleting output identifier', function () { + let input = "input a\noutput\noutput a\n"; + parseAndCheckErrors(input, [ + new parserFacade.Error(2, 2, 6, 7, "missing ID at '\\n'"), + ]); + }); +});