Skip to content

Commit

Permalink
Implement a validation for Language constructor
Browse files Browse the repository at this point in the history
This commit will close the issue #16
  • Loading branch information
takamin committed Feb 8, 2021
1 parent 0dc8e19 commit 8930e25
Show file tree
Hide file tree
Showing 4 changed files with 136 additions and 23 deletions.
49 changes: 41 additions & 8 deletions lib/language.js
Expand Up @@ -21,11 +21,48 @@ class Language {
constructor(rules) {
/** @type {string} */
this.root = rules[0].name;

/** @type {Record<string, SyntaxRule>} */
this.rules = {};
rules.forEach((rule) => {
this.rules[rule.name] = rule;
});
this.rules = Language.convertRules(rules);
}
/**
* Convert array of SyntaxRules to map.
* @private
* @static
* @param {SyntaxRule[]} rules array of syntax definition.
* @return {Record<string, SyntaxRule>} A syntax map.
*/
static convertRules(rules) {
const syntaxMap = {};
rules.forEach((rule) => (syntaxMap[rule.name] = rule));

// Check if a syntax exists
const syntaxNames = Object.keys(syntaxMap);
for(let iSyntax = 0; iSyntax < rules.length; iSyntax++) {
const syntax = rules[iSyntax];
for(let iRule = 0; iRule < syntax.rules.length; iRule++) {
const terms = syntax.rules[iRule];
for(let iTerm = 0; iTerm < terms.length; iTerm++) {
const term = terms[iTerm];
if(typeof term === "string") {
const specName = term.replace(/[*]$/, "");
debug(`.convertRules - [${iSyntax}-${
syntax.name}.${iRule}.${iTerm}]: ${specName}`);
if(!syntaxNames.includes(specName)) {
const message = [
`Undefined syntax name ${specName}`,
`at ${syntax.name}[${iSyntax}][${iTerm}]`,
].join(" ");
debug(`.convertRules - Throws ${message}`);
throw new Error(message);
}
}
}
}
}
debug(`.convertRules - Returns ${
JSON.stringify(syntaxMap, null, 2)}`);
return syntaxMap;
}
/**
* Create a syntax rule as an element for parameter of the constructor.
Expand Down Expand Up @@ -86,10 +123,6 @@ class Language {
_parse(name, tokenList, iTokStart) {
debug(`parse ${name}`);
const syntax = this.rules[name];
if(!syntax) {
debug(`parse ${name} FATAL: No syntax rule`);
throw new Error(`FATAL: No syntax rule for ${JSON.stringify(name)}`);
}
const term = this.createTerm(name);
const rules = syntax.rules;
let nTok = 0;
Expand Down
18 changes: 17 additions & 1 deletion lib/syntax-rule.js
Expand Up @@ -14,10 +14,26 @@ class SyntaxRule {
* @param {Evaluator|null} evaluator evaluator function
*/
constructor(name, rules, evaluator) {
// Check the length of rule
if(!rules || !Array.isArray(rules) || rules.length == 0) {
throw new Error(`No rule defined in syntax rule ${name}`);
}
for(const elements of rules) {
// Check the length of rule
if(!elements || !Array.isArray(elements) || elements.length == 0) {
throw new Error(`No rule defined in syntax rule ${name}`);
}
// Check types
for(const element of elements) {
if(typeof element !== "string" && !(element instanceof LexElement)) {
throw new Error("Type of elements in syntax rules should be string or LexElement");
throw new Error(`Invalid type of term in syntax rule ${name}`);
}
}
// Check left recursion
const firstElement = elements[0];
if(typeof firstElement === "string") {
if(firstElement === name) {
throw new Error(`Left recursion is found in syntax rule ${name}`);
}
}
}
Expand Down
77 changes: 63 additions & 14 deletions test/language.test.js
Expand Up @@ -2,25 +2,41 @@
const assert = require("chai").assert;
const Language = require("../lib/language.js");
const {syntax, literal: lit, numlit} = Language;
const langCalc = require("../sample/calc.js");
const debug = require("debug")("Language");
describe("Language", () => {
describe("parse", () => {
it("should throw, if syntax does not exits", ()=>{
assert.throw(()=>{
const lang = new Language([
syntax("calc", [["expression"]]),
]);
lang.parse("1+2");
describe("constructor", () => {
describe("validation", ()=>{
it("should throw when the referenced rule is not declared", ()=>{
assert.throw(()=>{
new Language([
syntax("invalid-syntax",
[["additive-expression"]]),
syntax("additive-expression",
[
["integer-constant", lit("-"), "additive-expression"],
["integer-constant"],
],
(term) => {
const terms = term.contents();
const [a, ope, b] = terms;
return !ope ? a : ope == "+" ? a + b: a - b;
}),
// syntax("integer-constant", [[numlit]], (term) => parseInt(term.str())),
]);
})
});
});
});
describe("parse", () => {
it("should be error the expression is incomplete", ()=>{
const langCalc = require("../sample/calc.js");
const expr = `1 + `;
const tokens = langCalc.tokenize(expr);
const result = langCalc.parse(tokens);
assert.instanceOf(result.error, Error);
});
it("should accept a token list", ()=>{
const langCalc = require("../sample/calc.js");
const expr = `1 + 2`;
const tokens = langCalc.tokenize(expr);
const result = langCalc.parse(tokens);
Expand Down Expand Up @@ -94,26 +110,33 @@ describe("Language", () => {
});
describe("Countermeasure by repeating specifier", ()=>{
describe("Repeating rule", ()=>{
const lang = new Language([
syntax("repeat-term",
[[lit("A"), "repeat*"]]),
syntax("repeat",
[[lit("."), lit("A") ]]),
]);
const repeatTerm = () => {
const lang = new Language([
syntax("repeat-term",
[[lit("A"), "repeat*"]]),
syntax("repeat",
[[lit("."), lit("A") ]]),
]);
return lang;
};
it("should not throw", ()=>{
assert.doesNotThrow(()=>{
const lang = repeatTerm();
lang.parse("A.A.A");
});
});
it("should not be error for no repeating", ()=>{
const lang = repeatTerm();
const result = lang.parse("A");
assert.isNull(result.error);
});
it("should not be error for repeating one time", ()=>{
const lang = repeatTerm();
const result = lang.parse("A.A");
assert.isNull(result.error);
});
it("should not be error for more repeating", ()=>{
const lang = repeatTerm();
const result = lang.parse("A.A.A");
assert.isNull(result.error);
});
Expand Down Expand Up @@ -184,102 +207,117 @@ describe("Language", () => {
describe("calc.js sample implementation", () => {
describe("correct expression", () => {
it("`1` should be 1", () => {
const langCalc = require("../sample/calc.js");
const expr = `1`;
const result = langCalc.parse(expr);
const value = langCalc.evaluate(result);
assert.isNull(result.error);
assert.equal(value, 1);
});
it("`(1)` should be 1", ()=>{
const langCalc = require("../sample/calc.js");
const expr = `(1)`;
const result = langCalc.parse(expr);
const value = langCalc.evaluate(result);
assert.isNull(result.error);
assert.equal(value, 1);
});
it("`1 + 2` should be 3", ()=>{
const langCalc = require("../sample/calc.js");
const expr = `1 + 2`;
const result = langCalc.parse(expr);
const value = langCalc.evaluate(result);
assert.isNull(result.error);
assert.equal(value, 3);
});
it("`3 * 4` should be 12", ()=>{
const langCalc = require("../sample/calc.js");
const expr = `3 * 4`;
const result = langCalc.parse(expr);
const value = langCalc.evaluate(result);
assert.isNull(result.error);
assert.equal(value, 12);
});
it("`5 - 6` should be -1", ()=>{
const langCalc = require("../sample/calc.js");
const expr = `5 - 6`;
const result = langCalc.parse(expr);
const value = langCalc.evaluate(result);
assert.isNull(result.error);
assert.equal(value, -1);
});
it("`7 / 8` should be 0.875", ()=>{
const langCalc = require("../sample/calc.js");
const expr = `7 / 8`;
const result = langCalc.parse(expr);
const value = langCalc.evaluate(result);
assert.isNull(result.error);
assert.equal(value, 0.875);
});
it("`1 - 2 - 3` should be -4", ()=>{
const langCalc = require("../sample/calc.js");
const expr = `1 - 2 - 3`;
const result = langCalc.parse(expr);
const value = langCalc.evaluate(result);
assert.isNull(result.error);
assert.equal(value, -4);
});
it("`1 * 2 + 3 * 4` should be 14", ()=>{
const langCalc = require("../sample/calc.js");
const expr = `1 * 2 + 3 * 4`;
const result = langCalc.parse(expr);
const value = langCalc.evaluate(result);
assert.isNull(result.error);
assert.equal(value, 14);
});
it("`1 / (2 + 3) + 4` should be 4.2", ()=>{
const langCalc = require("../sample/calc.js");
const expr = `1 / (2 + 3) + 4`;
const result = langCalc.parse(expr);
const value = langCalc.evaluate(result);
assert.isNull(result.error);
assert.equal(value, 4.2);
});
it("`(1 + 2) * (3 + 4)` should be 21", ()=>{
const langCalc = require("../sample/calc.js");
const expr = `(1 + 2) * (3 + 4)`;
const result = langCalc.parse(expr);
const value = langCalc.evaluate(result);
assert.isNull(result.error);
assert.equal(value, 21);
});
it("`(1 + 2) * ((3 + 4) / 2)` should be 10.5 (only parsing)", ()=>{
const langCalc = require("../sample/calc.js");
const expr = `(1 + 2) * ((3 + 4) / 2)`;
const result = langCalc.parse(expr);
assert.isNull(result.error);
}).timeout(5000);
it("`(1 + 2) * ((3 + 4) / 2)` should be 10.5", ()=>{
const langCalc = require("../sample/calc.js");
const expr = `(1 + 2) * ((3 + 4) / 2)`;
const result = langCalc.parse(expr);
const value = langCalc.evaluate(result);
assert.isNull(result.error);
assert.equal(value, 10.5);
}).timeout(5000);
it("`1.5 * 2` should be 3", ()=>{
const langCalc = require("../sample/calc.js");
const expr = `1.5 * 2`;
const result = langCalc.parse(expr);
const value = langCalc.evaluate(result);
assert.isNull(result.error);
assert.equal(value, 3);
});
it("`1.5e+2 * -2` should be -300", ()=>{
const langCalc = require("../sample/calc.js");
const expr = `1.5e+2 * -2`;
const result = langCalc.parse(expr);
const value = langCalc.evaluate(result);
assert.isNull(result.error);
assert.equal(value, -300);
});
it("`1.5e2 * 2` should be 300", ()=>{
const langCalc = require("../sample/calc.js");
const expr = `1.5e2 * 2`;
const result = langCalc.parse(expr);
const value = langCalc.evaluate(result);
Expand All @@ -288,6 +326,7 @@ describe("Language", () => {
});
describe("Multi lines", ()=>{
it("should not be error even if the expression contains LF", ()=>{
const langCalc = require("../sample/calc.js");
const eol = "\n";
const expr = `1${eol}+ 2${eol}+3${eol} +4`;
const result = langCalc.parse(expr);
Expand All @@ -296,6 +335,7 @@ describe("Language", () => {
assert.equal(value, 10);
});
it("should not be error even if the expression contains CR-LF", ()=>{
const langCalc = require("../sample/calc.js");
const eol = "\r\n";
const expr = `1${eol}+ 2${eol}+3${eol} +4`;
const result = langCalc.parse(expr);
Expand All @@ -307,31 +347,36 @@ describe("Language", () => {
describe("Term#toString", ()=>{
describe("with no syntax error", ()=>{
it("should not throw", ()=>{
const langCalc = require("../sample/calc.js");
assert.doesNotThrow(()=>{
const result = langCalc.parse("1");
result.toString();
});
});
it("should not throw", ()=>{
const langCalc = require("../sample/calc.js");
assert.doesNotThrow(()=>{
const result = langCalc.parse("1 + 2) * (3 + 4)");
result.toString();
});
});
it("should returns string", ()=>{
const langCalc = require("../sample/calc.js");
const result = langCalc.parse("1");
const s = result.toString();
assert.isString(s);
});
});
describe("with syntax error", ()=>{
it("should not throw", ()=>{
const langCalc = require("../sample/calc.js");
assert.doesNotThrow(()=>{
const result = langCalc.parse("(1 + 2) * xyz(3 + 4) / 2)");
result.toString();
});
});
it("should returns string", ()=>{
const langCalc = require("../sample/calc.js");
const result = langCalc.parse("(1 + 2) * xyz(3 + 4) / 2)");
const s = result.toString();
assert.isString(s);
Expand All @@ -341,23 +386,27 @@ describe("Language", () => {
});
describe("parsing error", () => {
it("`+` should be parser error", ()=>{
const langCalc = require("../sample/calc.js");
const expr = `+`;
const result = langCalc.parse(expr);
assert.instanceOf(result.error, Error);
});
it("`1 + 2 * 3 + 4)` should be parser error", () => {
const langCalc = require("../sample/calc.js");
const expr = `1 + 2 * 3 + 4)`;
const result = langCalc.parse(expr);
assert.instanceOf(result.error, Error);
});
it("`1 + 2 * (3 + 4` should be parser error", () => {
const langCalc = require("../sample/calc.js");
const expr = `1 + 2 * (3 + 4`;
const result = langCalc.parse(expr);
assert.instanceOf(result.error, Error);
});
});
describe("evaluation error", () => {
it("`1 .5 * 2` should throw ", () => {
const langCalc = require("../sample/calc.js");
const expr = `1 .5 * 2`;
const result = langCalc.parse(expr);
assert.throw(()=>{
Expand Down

0 comments on commit 8930e25

Please sign in to comment.