Skip to content
This repository was archived by the owner on Jul 15, 2021. It is now read-only.

Commit ef0c83e

Browse files
authoredApr 29, 2021
equivalence is working!!!!!!! (#28)
1 parent 17b2a11 commit ef0c83e

14 files changed

+8558
-30
lines changed
 

‎codecov.yml

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
coverage:
2+
status:
3+
patch:
4+
default:
5+
target: auto
6+
# this allows a 10% drop from the previous base commit coverage
7+
threshold: 10%
8+
project:
9+
default:
10+
target: auto
11+
# this allows a 10% drop from the previous base commit coverage
12+
threshold: 10%

‎package-lock.json

+8,113-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎src/index.js

+7-6
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,18 @@ import Feedback from "./internal/feedback/Feedback.js";
33
import CFG from "./internal/cfg/CFG.js";
44

55

6-
document.addEventListener('DOMContentLoaded', () => {
6+
document.addEventListener('DOMContentLoaded', async () => {
77
let uiController = new UIController();
88

9-
let [targetPDA, targetCFG] = uiController.generateNewPDA(uiController.difficulty);
9+
let [targetPDA, targetCFG] = await uiController.generatePDA(uiController.difficulty);
1010
let userInputField = document.getElementById("cfg-input");
1111

1212
document.getElementById("submit").addEventListener('click', (e) => {
1313
e.preventDefault();
1414
e.stopPropagation();
1515
e.stopImmediatePropagation();
1616

17-
let feedback = Feedback.for(targetCFG, CFG.fromString(userInputField.innerText));
17+
let feedback = Feedback.for(targetCFG, CFG.fromString(userInputField.value));
1818
uiController.showFeedback(feedback);
1919
})
2020

@@ -23,21 +23,22 @@ document.addEventListener('DOMContentLoaded', () => {
2323
e.stopPropagation();
2424
e.stopImmediatePropagation();
2525

26-
[targetPDA, targetCFG] = uiController.generateNewPDA(uiController.updateDifficulty(-1));
26+
[targetPDA, targetCFG] = uiController.generatePDA(uiController.updateDifficulty(-1));
2727
});
2828

2929
document.getElementById("new_question_button_same").addEventListener('click', (e) => {
3030
e.preventDefault();
3131
e.stopPropagation();
3232
e.stopImmediatePropagation();
3333

34-
[targetPDA, targetCFG] = uiController.generateNewPDA(uiController.difficulty);
34+
[targetPDA, targetCFG] = uiController.generatePDA(uiController.difficulty);
3535
});
3636
document.getElementById("new_question_button_harder").addEventListener('click', (e) => {
3737
e.preventDefault();
3838
e.stopPropagation();
3939
e.stopImmediatePropagation();
4040

41-
[targetPDA, targetCFG] = uiController.generateNewPDA(uiController.updateDifficulty(+1));
41+
let difficulty = uiController.updateDifficulty(+1);
42+
[targetPDA, targetCFG] = uiController.generatePDA(difficulty);
4243
});
4344
})

‎src/internal/cfg/CFG.js

+40
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ import ArrayHelper from "../helper/ArrayHelper.js";
33
import Variable from "./Variable.js";
44
import Rule from "./Rule.js";
55
import Terminal from "./Terminal.js";
6+
import CFGNormalise from "./CFGNormalise.js";
7+
import CFGSimplify from "./CFGSimplify.js";
8+
import CFT from "../cft/CFT.js";
9+
import CFGRemapper from "./CFGRemapper.js";
610

711
export default class CFG {
812
/**
@@ -20,6 +24,27 @@ export default class CFG {
2024

2125
this._rules = Rule.sort(ArrayHelper.distinct(rules));
2226
this._startVariable = startVariable;
27+
this._ghost_variables = [];
28+
}
29+
30+
remap() {
31+
return CFGRemapper.remap(this);
32+
}
33+
34+
normalise() {
35+
return CFGNormalise.normalise(this);
36+
}
37+
38+
simplify() {
39+
return CFGSimplify.simplify(this);
40+
}
41+
42+
/**
43+
* @param maxLength
44+
* @returns {[[Terminal]]}
45+
*/
46+
getAcceptingInputs(maxLength = 50) {
47+
return CFT.fromCFG(this.normalise(), maxLength).filter(sequence => sequence.length <= maxLength);
2348
}
2449

2550
get variables() {
@@ -38,6 +63,21 @@ export default class CFG {
3863
return this._startVariable;
3964
}
4065

66+
nextVariable() {
67+
let usedVariables = Symbol.sort(this.variables.concat(this._ghost_variables)).reverse().filter(s => !Variable.S.equals(s) && !Variable.S0.equals(s));
68+
if (usedVariables.length === 0) {
69+
usedVariables = [new Variable(String.fromCharCode(64))];
70+
}
71+
let lastVariable = usedVariables[0].id;
72+
let nextCharCode = lastVariable.charCodeAt(0) + 1;
73+
if (nextCharCode === 83) {
74+
nextCharCode = 84
75+
}
76+
let nextVariable = new Variable(String.fromCharCode(nextCharCode));
77+
this._ghost_variables.push(nextVariable);
78+
return nextVariable;
79+
}
80+
4181
/**
4282
* @param {Rule[]} rules
4383
* @param {Variable} startVariable

‎src/internal/cfg/CFGNormalise.js

+133
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import CFG from "./CFG.js";
2+
import Rule from "./Rule.js";
3+
import ArrayHelper from "../helper/ArrayHelper.js";
4+
import ObjectHelper from "../helper/ObjectHelper.js";
5+
import Terminal from "./Terminal.js";
6+
import Variable from "./Variable.js";
7+
8+
export default class CFGNormalise {
9+
/**
10+
* Normalises the CFG into Chomsky normal form
11+
*
12+
* @param {CFG} cfg
13+
* @returns {CFG}
14+
*/
15+
static normalise(cfg) {
16+
let newCFG = cfg.simplify();
17+
let newRules = this.step4([...cfg.rules], newCFG);
18+
return CFG.fromRules(newRules, newCFG.startVariable);
19+
}
20+
21+
/**
22+
* Finally, we convert all remaining rules into the proper form. We replace each
23+
* rule A → u1u2 ··· uk, where k ≥ 3 and each ui is a variable or terminal symbol,
24+
* with the rules A → u1A1, A1 → u2A2, A2 → u3A3, ... , and Ak−2 → uk−1uk.
25+
* The Ai’s are new variables. We replace any terminal ui in the preceding rule(s)
26+
* with the new variable Ui and add the rule Ui → ui.
27+
* @param rules
28+
* @param cfg
29+
*/
30+
static step4(rules, cfg) {
31+
let newRules = [];
32+
let switcherRules = [];
33+
Rule.sort(rules).forEach(rule => {
34+
if (rule.outputList.length > 2) {
35+
let generatedRules = this.generateRulesForStep4(rule, cfg);
36+
let matchingRules = [];
37+
generatedRules.forEach(rule => {
38+
let equivalentRule = this.findEquivalentRule(rule, switcherRules);
39+
if (equivalentRule) {
40+
matchingRules.push([rule.inputVariable, equivalentRule.inputVariable]);
41+
}
42+
});
43+
generatedRules = generatedRules.map(rule => {
44+
let matchingRule = matchingRules.find(r => ObjectHelper.equals(r[0], rule.inputVariable));
45+
if (matchingRule) {
46+
return null;
47+
} else {
48+
return new Rule(rule.inputVariable, rule.outputList.map(output => {
49+
let match = matchingRules.find(r => ObjectHelper.equals(r[0], output));
50+
if (match) {
51+
return match[1];
52+
} else {
53+
return output;
54+
}
55+
}))
56+
}
57+
}).filter(x => x !== null);
58+
59+
switcherRules = switcherRules.concat(generatedRules);
60+
} else {
61+
ArrayHelper.push_distinct(newRules, rule);
62+
}
63+
});
64+
65+
newRules = newRules.concat(switcherRules);
66+
67+
return this.replaceTerminals(newRules, cfg);
68+
}
69+
70+
static generateRulesForStep4(rule, cfg) {
71+
let nextVariable = cfg.nextVariable();
72+
let generatedRules = [new Rule(rule.inputVariable, [rule.outputList[0], nextVariable])];
73+
74+
//
75+
for (let i = 1, len = rule.outputList.length - 2; i < len; i++) {
76+
let nextNextVariable = cfg.nextVariable();
77+
generatedRules.push(new Rule(
78+
nextVariable,
79+
[rule.outputList[i], nextNextVariable]
80+
));
81+
nextVariable = nextNextVariable;
82+
}
83+
84+
generatedRules.push(new Rule(
85+
nextVariable,
86+
rule.outputList.slice(rule.outputList.length - 2, rule.outputList.length)
87+
));
88+
89+
return generatedRules;
90+
}
91+
92+
static findEquivalentRule(rule, rules) {
93+
for (let i = 0, len = rules.length; i < len; i++) {
94+
if (ObjectHelper.equals(rule.outputList, rules[i].outputList)) {
95+
return rules[i];
96+
}
97+
}
98+
return null;
99+
}
100+
101+
static replaceTerminals(rules, cfg) {
102+
let terminalSwitching = {};
103+
let newRules = [];
104+
rules.forEach(rule => {
105+
if (rule.isMixedOutput()) {
106+
newRules.push(new Rule(rule.inputVariable, rule.outputList.map(o => {
107+
if (o instanceof Terminal) {
108+
if (!terminalSwitching[o.id]) {
109+
terminalSwitching[o.id] = new Rule(cfg.nextVariable(), [o]);
110+
}
111+
return terminalSwitching[o.id].inputVariable;
112+
} else {
113+
return o;
114+
}
115+
})));
116+
} else {
117+
newRules.push(rule);
118+
}
119+
})
120+
121+
for (let k in terminalSwitching) {
122+
newRules.push(terminalSwitching[k]);
123+
}
124+
125+
return newRules;
126+
}
127+
128+
static newStart(rules, cfg) {
129+
let newRules = rules;
130+
newRules.push(new Rule(Variable.S0, [Variable.S]));
131+
return [newRules, Variable.S0];
132+
}
133+
}

‎src/internal/cfg/Rule.js

+20
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,26 @@ export default class Rule {
5454
return null;
5555
}
5656

57+
isMixedOutput() {
58+
let hasVariable = false;
59+
let hasTerminal = false;
60+
61+
for (let i = 0, len = this.outputList.length; i < len; i++) {
62+
if (this.outputList[i] instanceof Variable) {
63+
hasVariable = true;
64+
if (hasTerminal) {
65+
return true;
66+
}
67+
} else {
68+
hasTerminal = true;
69+
if (hasVariable) {
70+
return true;
71+
}
72+
}
73+
}
74+
return false;
75+
}
76+
5777
/**
5878
*
5979
* @param {Rule[]} rules

‎src/internal/cft/CFT.js

+100
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import MagicMap from "../helper/MagicMap.js";
2+
import Variable from "../cfg/Variable.js";
3+
import ArrayHelper from "../helper/ArrayHelper.js";
4+
import Terminal from "../cfg/Terminal.js";
5+
6+
/**
7+
* CONTEXT-FREE TREE
8+
*/
9+
export default class CFT {
10+
/**z
11+
*
12+
* @param {CFG}cfg
13+
* @param {number} depth
14+
* @returns {[[Terminal]]}
15+
*/
16+
static fromCFG(cfg, depth) {
17+
let rules = cfg.rules;
18+
19+
let variableMappings = new MagicMap();
20+
21+
cfg.variables.forEach(variable => {
22+
variableMappings.set(variable, rules.filter(r => variable.equals(r.inputVariable)).map(r => r.outputList));
23+
});
24+
25+
return CFT.dive([[cfg.startVariable]], variableMappings, depth);
26+
}
27+
28+
/**
29+
* @param layer
30+
* @param variableMappings
31+
* @param remainingDepth
32+
* @returns {[[Terminal]]}
33+
*/
34+
static dive(layer, variableMappings, remainingDepth) {
35+
for (let i = remainingDepth; i > 0; i--) {
36+
layer = layer
37+
.map(sequence => {
38+
let sequenceNewLayer = sequence.map(element => {
39+
if (element instanceof Variable) {
40+
return variableMappings.get(element);
41+
} else {
42+
return [[element]];
43+
}
44+
});
45+
46+
return this.expandMapping(sequenceNewLayer);
47+
})
48+
.flat()
49+
.map(sequence => {
50+
let newSequence = sequence.filter(element => !Terminal.EPSILON.equals(element));
51+
if (newSequence.length > 0) {
52+
return newSequence;
53+
} else {
54+
return [Terminal.EPSILON];
55+
}
56+
});
57+
}
58+
59+
let array = layer.filter(sequence => !sequence.some(element => element instanceof Variable));
60+
return ArrayHelper.distinct(array);
61+
}
62+
63+
/**
64+
* Generates all possible of combinations of concatenations on a collection of array of sequences
65+
*
66+
* e.g.
67+
* [
68+
* [
69+
* [A,B],
70+
* [C,D]
71+
* ],
72+
* [
73+
* [E,F],
74+
* [G]
75+
* ]
76+
* ]
77+
*
78+
* would produce
79+
* [
80+
* [A,B,E,F],
81+
* [A,B,G],
82+
* [C,D,E,F],
83+
* [C,D,G]
84+
* ]
85+
*
86+
*
87+
* @param {[[[Symbol]]]} m
88+
* @returns {[[Symbol]]}
89+
*/
90+
static expandMapping(m) {
91+
//Initialise with the first element from the mapping
92+
let outputs = m.shift();
93+
94+
for (const element of m) {
95+
outputs = element.map(sequence => outputs.map(output => output.concat(sequence))).flat();
96+
}
97+
98+
return outputs;
99+
}
100+
}

‎src/internal/feedback/Feedback.js

+12-2
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,7 @@ export default class Feedback {
33
constructor(targetCFG, studentCFG) {
44
this._targetCFG = targetCFG;
55
this._studentCFG = studentCFG;
6-
this._notes = ["First note", "Second note"];
7-
6+
this._notes = this.generateNotes();
87
}
98

109
get targetCFG() {
@@ -22,4 +21,15 @@ export default class Feedback {
2221
static for(targetCFG, studentCFG) {
2322
return new Feedback(targetCFG, studentCFG);
2423
}
24+
25+
generateNotes() {
26+
let targetAccepts = this._targetCFG.getAcceptingInputs().map(s => s.join("")).sort().reverse();
27+
let studentAccepts = this._studentCFG ? this._studentCFG.getAcceptingInputs().map(s => s.join("")).sort().reverse() : [];
28+
29+
let missingAccepts = targetAccepts.filter(x => !studentAccepts.includes(x));
30+
let extraAccepts = studentAccepts.filter(x => !targetAccepts.includes(x));
31+
32+
return missingAccepts.map(missing => `Your query doesn't accept "${missing}", but the answer does`)
33+
.concat(extraAccepts.map(extra => `Your query accepts "${extra}", when the answer doesn't`));
34+
}
2535
}

‎src/internal/ui/UIController.js

+13-11
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import Renderer from "../helper/Renderer.js";
22
import PDAGenerator from "../generator/PDAGenerator.js";
3-
import CFGRemapper from "../cfg/CFGRemapper.js";
43

54
function sleep(ms) {
65
return new Promise(resolve => setTimeout(resolve, ms));
@@ -25,7 +24,7 @@ export default class UIController {
2524
/**
2625
* @return {(PDA|CFG)[]}
2726
*/
28-
generateNewPDA(difficulty) {
27+
generatePDA(difficulty) {
2928
this._difficultyDisplay.innerText = this.difficulty;
3029

3130
if (this._questionContainer.classList.contains('active')) {
@@ -35,13 +34,12 @@ export default class UIController {
3534
}
3635

3736
let pda = PDAGenerator.generatePDA(difficulty);
38-
sleep(2000).then(r => {
39-
this._questionContainer.classList.remove('disable');
40-
this._pdaRenderer.render(pda);
41-
sleep(100).then(r => this._questionContainer.classList.add('active')); // Give it some time to load image
42-
});
4337

44-
return [pda, CFGRemapper.remap(pda.toCFG())];
38+
this._questionContainer.classList.remove('disable');
39+
this._pdaRenderer.render(pda);
40+
sleep(100).then(() => this._questionContainer.classList.add('active')); // Give it some time to load image
41+
42+
return [pda, pda.toCFG().remap()];
4543
}
4644

4745
showFeedback(feedback) {
@@ -63,9 +61,13 @@ export default class UIController {
6361
this.appendChild(this._correctAnswerBox, v)
6462
})
6563

66-
feedback.notes.forEach(note => {
67-
this.appendChild(this._feedbackBox, note);
68-
})
64+
if (feedback.notes.length === 0) {
65+
this.appendChild(this._feedbackBox, "Congratulations you got it correct!");
66+
} else {
67+
feedback.notes.forEach(note => {
68+
this.appendChild(this._feedbackBox, note);
69+
})
70+
}
6971

7072
let panel = this._answerBox;
7173
panel.style.maxHeight = panel.scrollHeight + "px";

‎test/internal/cfg/CFG.test.js

+51-1
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ test('Throws Error When Creating a CFG from rules and startVariable isnt in rule
5151
test('Throws Error When Creating a CFG from rules and outputList contains a non Terminal or Variable', () => {
5252
expect(() => {
5353
CFG.fromRules([
54-
new Rule(Variable.S,[Symbol.of("a")])
54+
new Rule(Variable.S, [Symbol.of("a")])
5555
])
5656
}).toThrowError();
5757
})
@@ -90,4 +90,54 @@ test('generateVariableStrings', () => {
9090
'A -> a'
9191
]
9292
])
93+
})
94+
95+
test('simplify', () => {
96+
let cfg = CFG.fromRules([
97+
Rule.fromString("S->A"),
98+
Rule.fromString("S->aBA"),
99+
Rule.fromString("A->A"),
100+
Rule.fromString("A->B"),
101+
Rule.fromString("A->a"),
102+
Rule.fromString("B->b"),
103+
Rule.fromString("C->c")
104+
])
105+
106+
let actual1 = cfg.simplify();
107+
expect(actual1).toEqual(CFG.fromRules([
108+
Rule.fromString("S->a"),
109+
Rule.fromString("S->b"),
110+
Rule.fromString("S->aba"),
111+
Rule.fromString("S->abb"),
112+
]))
113+
})
114+
115+
test('normalise', () => {
116+
let cfg = CFG.fromString("S->ASA,S->aB,A->B,A->S,B->b,B->e");
117+
118+
expect(cfg.normalise()).toEqual(CFG.fromString("" +
119+
"S->AB,S->CB," +
120+
"A->B,A->S," +
121+
"B->b,B->e,B->SA," +
122+
"C->a"
123+
));
124+
})
125+
126+
test('remap', () => {
127+
let cfg = CFG.fromString("S->XSX,S->aT,X->T,X->S,T->b,T->e");
128+
129+
expect(cfg.remap()).toEqual(CFG.fromString("S->BSB,S->aA,B->A,B->S,A->b,A->e"))
130+
})
131+
132+
test('getAcceptingInputs', () => {
133+
let cfg = CFG.fromString("S->ASA,S->aB,A->B,A->S,B->b,B->e");
134+
135+
let acceptingInputs = cfg.getAcceptingInputs(3);
136+
expect(acceptingInputs).toEqual([
137+
[Terminal.of("b"), Terminal.of("b")],
138+
[Terminal.of("b")],
139+
[Terminal.EPSILON],
140+
[Terminal.of("a"), Terminal.of("b")],
141+
[Terminal.of("a")]
142+
])
93143
})
+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import CFGNormalise from "../../../src/internal/cfg/CFGNormalise.js";
2+
import Rule from "../../../src/internal/cfg/Rule.js";
3+
import Variable from "../../../src/internal/cfg/Variable.js";
4+
import CFG from "../../../src/internal/cfg/CFG.js";
5+
6+
test('Step 4', () => {
7+
let cfg = CFG.fromRules([
8+
Rule.fromString('S -> ASA'),
9+
Rule.fromString('A -> ASA')
10+
], Variable.S)
11+
12+
let newRules = CFGNormalise.step4(cfg.rules, cfg);
13+
14+
expect(newRules).toEqual(Rule.sort([
15+
Rule.fromString('S -> AB'),
16+
Rule.fromString('A -> AB'),
17+
Rule.fromString('B -> SA'),
18+
]))
19+
})
20+
21+
test('Replace Terminals', () => {
22+
let newRules = CFGNormalise.replaceTerminals([
23+
Rule.fromString('S -> aA'),
24+
Rule.fromString('A -> B'),
25+
Rule.fromString('B -> b')
26+
], CFG.fromRules([Rule.fromString('S -> aA'),
27+
Rule.fromString('A -> B'),
28+
Rule.fromString('B -> b')]));
29+
30+
expect(Rule.sort(newRules)).toEqual(Rule.sort([
31+
Rule.fromString('S -> CA'),
32+
Rule.fromString('A -> B'),
33+
Rule.fromString('B -> b'),
34+
Rule.fromString('C -> a')
35+
]))
36+
})

‎test/internal/cfg/CFGSimplify.test.js

+1-9
Original file line numberDiff line numberDiff line change
@@ -98,12 +98,4 @@ test('simplify with recursion', () => {
9898
Rule.fromString('A->aa'),
9999
Rule.fromString('A->e')
100100
]))
101-
})
102-
103-
104-
// test('test', () => {
105-
// let pda = PDAGenerator.generatePDA(5);
106-
// let cfg = pda.toCFG();
107-
//
108-
// expect(cfg).toEqual(CFG.fromRules([]))
109-
// })
101+
})

‎test/internal/cfg/Rule.test.js

+6
Original file line numberDiff line numberDiff line change
@@ -91,4 +91,10 @@ test('terminates', () => {
9191
expect(Rule.fromString('S->a').terminates()).toBeTruthy();
9292
expect(Rule.fromString('S->A').terminates()).toBeFalsy();
9393
expect(Rule.fromString('S->A').terminates([Variable.of('A')])).toBeTruthy();
94+
})
95+
96+
test('isMixedOutput', () => {
97+
expect(Rule.fromString('S->aaA').isMixedOutput()).toBeTruthy();
98+
expect(Rule.fromString('S->AAa').isMixedOutput()).toBeTruthy();
99+
expect(Rule.fromString('S->a').isMixedOutput()).toBeFalsy();
94100
})

‎test/internal/cft/CFT.test.js

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import CFG from "../../../src/internal/cfg/CFG.js";
2+
import CFT from "../../../src/internal/cft/CFT.js";
3+
import Terminal from "../../../src/internal/cfg/Terminal.js";
4+
5+
test('fromCFG', async () => {
6+
let cfg = CFG.fromString('S->e,S->AD,S->Ab,A->aa,B->CD,B->Cb, C->aa,D->Bb');
7+
8+
let x = await CFT.fromCFG(cfg, 5);
9+
expect(x).toEqual([
10+
[Terminal.EPSILON],
11+
[Terminal.of("a"), Terminal.of("a"), Terminal.of("a"), Terminal.of("a"), Terminal.of("b"), Terminal.of("b")],
12+
[Terminal.of("a"), Terminal.of("a"), Terminal.of("b")]
13+
]);
14+
})

0 commit comments

Comments
 (0)
This repository has been archived.