Integrate Parser (#4)

Integrate parser
This commit is contained in:
Jake Grossman
2021-10-26 12:48:04 -05:00
committed by GitHub
parent 9db476dd39
commit d105544596
15 changed files with 1234 additions and 16 deletions

View File

@@ -5,10 +5,12 @@ import { runTests } from '@vscode/test-electron';
async function main() {
try {
// The folder containing package.json
// Passed to `--extensionDevelopmentPath`
const extensionDevelopmentPath: string = path.resolve(__dirname, '../../');
// The path to the test runner script
const extensionTestsPath: string = path.resolve(__dirname, './suite/index');
// Passed to `--extensionTestsPath`
const extensionTestsPath: string = path.resolve(__dirname, './suites/index');
// Download VS Code, unzip it and run the integration test
await runTests({ extensionDevelopmentPath, extensionTestsPath });

View File

@@ -1,13 +0,0 @@
import * as assert from 'assert';
import * as vscode from 'vscode';
import { after } from 'mocha';
suite('Dummy Test Suite', () => {
after(() => {
vscode.window.showInformationMessage('All tests passed!');
});
test('Dummy Test', () => {
assert.strictEqual(0 === 0, true);
});
});

View File

@@ -35,4 +35,4 @@ export function run(): Promise<void> {
}
});
});
}
}

View File

@@ -0,0 +1,279 @@
import * as assert from 'assert';
import * as vscode from 'vscode';
import { after } from 'mocha';
import Lexer from '../../../pylex/lexer';
import LineToken from '../../../pylex/token';
import { EOFTOKEN, Symbol } from '../../../pylex/token';
suite('Lexer Test Suite', () => {
after(() => {
vscode.window.showInformationMessage('All tests passed!');
});
test('Empty String', () => {
let l: Lexer = new Lexer(undefined);
assert.deepStrictEqual(l.currToken(), EOFTOKEN);
});
test('Undefined', () => {
let l: Lexer = new Lexer('');
assert.deepStrictEqual(l.currToken(), EOFTOKEN);
});
test('Whitespace', () => {
let l: Lexer = new Lexer(' \t\t'.repeat(4).repeat(4));
assert.deepStrictEqual(l.currToken(), EOFTOKEN);
});
test('Non-Whitespace with no construct', () => {
let l: Lexer = new Lexer('foobar');
assert.deepStrictEqual(l.currToken(), new LineToken(Symbol.INDENT, 0, 0));
});
test('getIndent() accuracy, spaces', () => {
for (var i = 0; i < 100; i++) {
let l: Lexer = new Lexer(' '.repeat(i) + 'foobar');
assert.strictEqual(l.currToken().indentLevel, i);
}
});
test('getIndent() accuracy, tabs', () => {
for (var i = 0; i < 100; i++) {
let l: Lexer = new Lexer('\t'.repeat(i) + 'foobar', {size: 4, hard: true});
assert.strictEqual(l.currToken().indentLevel, i);
}
});
test('getIndent() accuracy, spaces with incomplete tab', () => {
for (var i = 0; i < 100; i++) {
for (var j = 1; j <= 3; j++) {
let l: Lexer = new Lexer(' '.repeat(i) + ' '.repeat(j) + 'foobar', {size: 4, hard: false});
assert.strictEqual(l.currToken().indentLevel, i+1);
}
}
});
test('class definition', () => {
let l: Lexer = new Lexer('class Foobar(object):');
assert.deepStrictEqual(l.currToken(), new LineToken(Symbol.CLASS, 0, 0, 'Foobar'));
});
test('function definition', () => {
let l: Lexer = new Lexer('def Barbaz(this, that, andTheOther):');
assert.deepStrictEqual(l.currToken(), new LineToken(Symbol.FUNCTION, 0, 0, 'Barbaz'));
});
test('if statement', () => {
let l: Lexer = new Lexer('if True and bar == baz:');
assert.deepStrictEqual(l.currToken(), new LineToken(Symbol.IF, 0, 0, 'True and bar == baz'));
});
test('elif statement', () => {
let l: Lexer = new Lexer('elif name == "bar" and True:');
assert.deepStrictEqual(l.currToken(), new LineToken(Symbol.ELIF, 0, 0, 'name == "bar" and True'));
});
test('else statement', () => {
let l: Lexer = new Lexer('else:');
assert.deepStrictEqual(l.currToken(), new LineToken(Symbol.ELSE, 0, 0));
});
test('for loop', () => {
let l: Lexer = new Lexer('for pickle in pickleJars:');
assert.deepStrictEqual(l.currToken(), new LineToken(Symbol.FOR, 0, 0, 'pickle in pickleJars'));
});
test('while loop', () => {
let l: Lexer = new Lexer('while numCookies < capacity:');
assert.deepStrictEqual(l.currToken(), new LineToken(Symbol.WHILE, 0, 0, 'numCookies < capacity'));
});
test('try statement', () => {
let l: Lexer = new Lexer('try:');
assert.deepStrictEqual(l.currToken(), new LineToken(Symbol.TRY, 0, 0));
});
test('except statement with attr', () => {
let l: Lexer = new Lexer('except NameError:');
assert.deepStrictEqual(l.currToken(), new LineToken(Symbol.EXCEPT, 0, 0, 'NameError'));
});
test('except statement with no attr', () => {
let l: Lexer = new Lexer('except:');
assert.deepStrictEqual(l.currToken(), new LineToken(Symbol.EXCEPT, 0, 0));
});
test('finally statement', () => {
let l: Lexer = new Lexer('finally:');
assert.deepStrictEqual(l.currToken(), new LineToken(Symbol.FINALLY, 0, 0));
});
test('with statement', () => {
let l: Lexer = new Lexer('with open(file) as f:');
assert.deepStrictEqual(l.currToken(), new LineToken(Symbol.WITH, 0, 0, 'open(file) as f'));
});
test('restart()', () => {
let l: Lexer = new Lexer('with open(file as f:');
l.restart('if is_old():');
assert.deepStrictEqual(l.currToken(), new LineToken(Symbol.IF, 0, 0, 'is_old()'));
});
test('next() ignores empty lines', () => {
let lines: string[] = [
'if wurst_available():',
'',
' eat_wurst()'
];
let l: Lexer = new Lexer(lines.join('\n'));
l.next();
assert.deepStrictEqual(l.currToken(), new LineToken(Symbol.INDENT, 2, 1));
});
test('retract() ignores empty lines', () => {
let lines: string[] = [
'if wurst_available():',
'',
' eat_wurst()'
];
let l: Lexer = new Lexer(lines.join('\n'));
l.next();
l.retract();
assert.deepStrictEqual(l.currToken(), new LineToken(Symbol.IF, 0, 0, 'wurst_available()'));
});
test('next() ignores whitespace lines', () => {
let lines: string[] = [
'if wurst_available():',
' \t \t ',
' eat_wurst()'
];
let l: Lexer = new Lexer(lines.join('\n'));
l.next();
assert.deepStrictEqual(l.currToken(), new LineToken(Symbol.INDENT, 2, 1));
});
test('retract() ignores whitespace lines', () => {
let lines: string[] = [
'if wurst_available():',
' \t \t ',
' eat_wurst()'
];
let l: Lexer = new Lexer(lines.join('\n'));
// Advance to end of input
// Eliminates dependence on next()
// skipping whitespace
do {} while (l.next() !== EOFTOKEN);
l.retract(); // retract past EOFTOKEn
l.retract();
assert.deepStrictEqual(l.currToken(), new LineToken(Symbol.IF, 0, 0, 'wurst_available()'));
});
test('next() ignores comment lines', () => {
let lines: string[] = [
'if wurst_available():',
' \t # I hate testing \t',
' eat_wurst()'
];
let l: Lexer = new Lexer(lines.join('\n'));
l.next();
assert.deepStrictEqual(l.currToken(), new LineToken(Symbol.INDENT, 2, 1));
});
test('retract() ignores comment lines', () => {
let lines: string[] = [
'if wurst_available():',
' \t # \t',
' eat_wurst()'
];
let l: Lexer = new Lexer(lines.join('\n'));
// Advance to end of input
// Eliminates dependence on next()
// skipping comment
do {} while (l.next() !== EOFTOKEN);
l.retract(); // retract past EOFTOKEn
l.retract();
assert.deepStrictEqual(l.currToken(), new LineToken(Symbol.IF, 0, 0, 'wurst_available()'));
});
test('next() out of range', () => {
let l: Lexer = new Lexer('foo = zaboomafoo');
l.next();
assert.throws(() => l.next());
});
test('retract() out of range', () => {
let l: Lexer = new Lexer('next_token = lexer.next()');
assert.throws(() => l.retract());
});
test('retract() validate argument', () => {
let l: Lexer = new Lexer();
// Negative
assert.throws(() => l.retract(-1));
// Zero, it doesn't make sense to retract 0 :P
assert.throws(() => l.retract(0));
});
test('retract() 1-100', () => {
let lines: string[] = Array.from(Array(100), (_, i) => 'line' + i);
let reference: LineToken[] = lines.map((_, i) => {
return new LineToken(Symbol.INDENT, i, 0);
});
for (var i = 0; i < 100; i++) {
let l: Lexer = new Lexer(lines.join('\n'));
// advance to EOF
do {} while (l.next() !== EOFTOKEN);
// try retract
l.retract(i+1);
assert.deepStrictEqual(l.currToken(), reference[99-i]);
}
});
test('2 full lex and retract passes', () => {
let lines: string[] = Array.from(Array(100), (_, i)=> 'line' + i);
let reference: LineToken[] = lines.map((_, i) => {
return new LineToken(Symbol.INDENT, i, 0);
});
let l: Lexer = new Lexer(lines.join('\n'));
// Twice
for (var _ of [0,1]) {
// advance to EOF
for (var i = 0; i < lines.length; i++) {
assert.deepStrictEqual(l.currToken(), reference[i]);
l.next();
}
// retract to start
for (var i = lines.length - 1; i >= 0; i--) {
l.retract();
assert.deepStrictEqual(l.currToken(), reference[i]);
}
}
});
});

View File

@@ -0,0 +1,121 @@
import * as assert from 'assert';
import * as vscode from 'vscode';
import { after } from 'mocha';
import { deparent } from '../../util';
import LineToken from '../../../pylex/token';
import { Symbol } from '../../../pylex/token';
import LexNode from '../../../pylex/node';
suite('LexNode Test Suite', () => {
after(() => {
vscode.window.showInformationMessage('All tests passed!');
});
test('children() of leaf', () => {
let n: LexNode = new LexNode('leafLexNode', vscode.TreeItemCollapsibleState.None, new LineToken(Symbol.INDENT, 0, 0));
assert.strictEqual(n.children(), null);
});
test('children() of internal node', () => {
let children: LexNode[] = [
new LexNode('leafLexNode1', vscode.TreeItemCollapsibleState.None, new LineToken(Symbol.WHILE, 2, 1)),
new LexNode('leafLexNode2', vscode.TreeItemCollapsibleState.None, new LineToken(Symbol.WHILE, 3, 1)),
new LexNode('leafLexNode3', vscode.TreeItemCollapsibleState.None, new LineToken(Symbol.WHILE, 4, 1)),
new LexNode('leafLexNode4', vscode.TreeItemCollapsibleState.None, new LineToken(Symbol.WHILE, 5, 1)),
new LexNode('leafLexNode5', vscode.TreeItemCollapsibleState.None, new LineToken(Symbol.WHILE, 6, 1))
];
let parent: LexNode = new LexNode(
'internalLexNode',
vscode.TreeItemCollapsibleState.None,
new LineToken(Symbol.FUNCTION, 0, 0, 'foobar'),
children
);
assert.notStrictEqual(parent.children(), null);
assert.notStrictEqual(parent.children(), []);
assert.strictEqual(parent.children()!.length, children.length);
for (var i = 0; i < children.length; i++) {}
assert.strictEqual(parent.children()![i], children[i]);
}
);
test('adopt() to empty', () => {
let children: LexNode[] = [
new LexNode('leafLexNode1', vscode.TreeItemCollapsibleState.None, new LineToken(Symbol.WHILE, 2, 1)),
new LexNode('leafLexNode2', vscode.TreeItemCollapsibleState.None, new LineToken(Symbol.WHILE, 3, 1)),
new LexNode('leafLexNode3', vscode.TreeItemCollapsibleState.None, new LineToken(Symbol.WHILE, 4, 1)),
new LexNode('leafLexNode4', vscode.TreeItemCollapsibleState.None, new LineToken(Symbol.WHILE, 5, 1)),
new LexNode('leafLexNode5', vscode.TreeItemCollapsibleState.None, new LineToken(Symbol.WHILE, 6, 1))
];
let testParent: LexNode = new LexNode(
'internalLexNode',
vscode.TreeItemCollapsibleState.None,
new LineToken(Symbol.FUNCTION, 1, 0, 'foobar')
);
let referenceParent: LexNode = new LexNode(
'internalLexNode',
vscode.TreeItemCollapsibleState.None,
new LineToken(Symbol.FUNCTION, 1, 0, 'foobar'),
children
);
// parentify reference childdren
referenceParent = new LexNode(
referenceParent.label,
referenceParent.collapsibleState,
referenceParent.token,
referenceParent.children()!.map(c => new LexNode(c.label, c.collapsibleState, c.token, null, referenceParent))
);
testParent.adopt(children);
assert.deepStrictEqual(deparent(testParent), deparent(referenceParent));
});
test('adopt() to !empty', () => {
let children1: LexNode[] = [
new LexNode('leafLexNode1', vscode.TreeItemCollapsibleState.None, new LineToken(Symbol.WHILE, 2, 1)),
];
let children2: LexNode[] = [
new LexNode('leafLexNode2', vscode.TreeItemCollapsibleState.None, new LineToken(Symbol.WHILE, 3, 1)),
new LexNode('leafLexNode3', vscode.TreeItemCollapsibleState.None, new LineToken(Symbol.WHILE, 4, 1)),
];
let testParent: LexNode = new LexNode(
'internalLexNode',
vscode.TreeItemCollapsibleState.None,
new LineToken(Symbol.FUNCTION, 1, 0, 'foobar'),
children1,
);
let referenceParent: LexNode = new LexNode(
'internalLexNode',
vscode.TreeItemCollapsibleState.None,
new LineToken(Symbol.FUNCTION, 1, 0, 'foobar'),
children1.concat(children2),
);
testParent.adopt(children2);
assert.deepStrictEqual(deparent(testParent), deparent(referenceParent));
});
test('tooltip without line number', () => {
let testTooltip: string | vscode.MarkdownString | undefined = new LexNode('leafLexNode', vscode.TreeItemCollapsibleState.None, new LineToken(Symbol.WHILE, -1, -11)).tooltip;
let referenceTooltip: string = "leafLexNode";
assert.notStrictEqual(testTooltip, undefined);
assert.strictEqual(testTooltip, referenceTooltip);
});
test('tooltip with line number', () => {
let testTooltip: string | vscode.MarkdownString | undefined = new LexNode('leafLexNode', vscode.TreeItemCollapsibleState.None, new LineToken(Symbol.WHILE, 6, 1)).tooltip;
let referenceTooltip: string = "leafLexNode: 7"; // 7 because it's 0 indexed in memory, but editor lines start at 1
assert.notStrictEqual(testTooltip, undefined);
assert.strictEqual(testTooltip, referenceTooltip);
});
});

View File

@@ -0,0 +1,239 @@
import * as assert from 'assert';
import * as vscode from 'vscode';
import { after } from 'mocha';
import { deparent, root } from '../../util';
import Parser from '../../../pylex/parser';
import LexNode from '../../../pylex/node';
import LineToken from '../../../pylex/token';
import { Symbol } from '../../../pylex/token';
type ParserTest = {
name: string,
input: string[],
output: LexNode,
};
const tests: ParserTest[] = [
{
name: 'No Input',
input: [ ],
output: root(null),
},
{
name: 'Single line without construct',
input: [ 'foo = "Yellow M&Ms make me angry >:(' ],
output: root(null),
},
{
name: 'Single line with construct',
input: [ 'for x of y:' ],
output: root([
new LexNode(
'for x of y',
vscode.TreeItemCollapsibleState.None,
new LineToken(Symbol.FOR, 0, 0, 'x of y')
)
]),
},
{
name: 'Sequential lines, without construct',
input: [
'bar = "Blue M&Ms make me happy <:)"',
'reba = "A hard working gal"'
],
output: root(null),
},
{
name: 'Sequential lines, with, then without construct',
input: [
'if radioshack:',
' print radioshack.hours',
'billy = "Scrubbly Bubbles!"'
],
output: root([
new LexNode('if radioshack',
vscode.TreeItemCollapsibleState.None,
new LineToken(Symbol.IF, 0, 0, 'radioshack'))
])
},
{
name: 'Sequential lines, without, then with construct',
input: [
'billy = "Scrubbly Bubbles!"',
'if radioshack:',
' print radioshack.hours'
],
output: root([
new LexNode('if radioshack',
vscode.TreeItemCollapsibleState.None,
new LineToken(Symbol.IF, 1, 0, 'radioshack'))
])
},
{
name: 'Sequential lines with constructs',
input: [
'if yummy:',
' print("HOoray!")',
'elif just_ok:',
' print("Do you have anything else?")',
'else:',
' print("You really eat this?")',
],
output: root([
new LexNode('if yummy',
vscode.TreeItemCollapsibleState.None,
new LineToken(Symbol.IF, 0, 0, 'yummy')),
new LexNode('elif just_ok',
vscode.TreeItemCollapsibleState.None,
new LineToken(Symbol.ELIF, 2, 0, 'just_ok')),
new LexNode('else',
vscode.TreeItemCollapsibleState.None,
new LineToken(Symbol.ELSE, 4, 0)),
])
},
{
name: 'Singly Nested Block',
input: [
'if yummy:',
' if in_my_tummy:',
' exclaim("Scrumdiddlyumptious!")'
],
output: root([
new LexNode('if yummy',
vscode.TreeItemCollapsibleState.None,
new LineToken(Symbol.IF, 0, 0, 'yummy'),
[
new LexNode('if in_my_tummy',
vscode.TreeItemCollapsibleState.None,
new LineToken(Symbol.IF, 1, 1, 'in_my_tummy'))
]
)
])
},
{
name: 'Singly Nested Block, then Block',
input: [
'if yummy:',
' if in_my_tummy:',
' exclaim("Scrumdiddlyumptious!")',
'else:',
' exclaim("DAESGUSTEN~)"'
],
output: root([
new LexNode('if yummy',
vscode.TreeItemCollapsibleState.None,
new LineToken(Symbol.IF, 0, 0, 'yummy'),
[
new LexNode('if in_my_tummy',
vscode.TreeItemCollapsibleState.None,
new LineToken(Symbol.IF, 1, 1, 'in_my_tummy'))
]
),
new LexNode('else',
vscode.TreeItemCollapsibleState.None,
new LineToken(Symbol.ELSE, 3, 0),
)
])
},
{
name: 'Doubly Nested Block',
input: [
'if yummy:',
' if in_my_tummy:',
' if looks_like_a_mummy:',
' print("you have a spot on your tummy"',
'else:',
' print("Food is food...")'
],
output: root([
new LexNode('if yummy',
vscode.TreeItemCollapsibleState.None,
new LineToken(Symbol.IF, 0, 0, 'yummy'),
[
new LexNode('if in_my_tummy',
vscode.TreeItemCollapsibleState.None,
new LineToken(Symbol.IF, 1, 1, 'in_my_tummy'),
[
new LexNode('if looks_like_a_mummy',
vscode.TreeItemCollapsibleState.None,
new LineToken(Symbol.IF, 2, 2, 'looks_like_a_mummy'))
]
)
]
),
new LexNode('else',
vscode.TreeItemCollapsibleState.None,
new LineToken(Symbol.ELSE, 4, 0),
)
])
},
{
name: 'Doubly Nested Block, with multiple indent resets',
input: [
'if yummy:',
' if in_my_tummy:',
' if looks_like_a_mummy:',
' print("you have a spot on your tummy"',
' else:',
' print("eek! a zombie!)',
' elif in_my_mouth:',
' print("ill be in my tummy soon!"',
'else:',
' print("Food is food...")'
],
output: root([
new LexNode('if yummy',
vscode.TreeItemCollapsibleState.None,
new LineToken(Symbol.IF, 0, 0, 'yummy'),
[
new LexNode('if in_my_tummy',
vscode.TreeItemCollapsibleState.None,
new LineToken(Symbol.IF, 1, 1, 'in_my_tummy'),
[
new LexNode('if looks_like_a_mummy',
vscode.TreeItemCollapsibleState.None,
new LineToken(Symbol.IF, 2, 2, 'looks_like_a_mummy')),
new LexNode('else',
vscode.TreeItemCollapsibleState.None,
new LineToken(Symbol.ELSE, 4, 2))
]
),
new LexNode('elif in_my_mouth',
vscode.TreeItemCollapsibleState.None,
new LineToken(Symbol.ELIF, 6, 1, 'in_my_mouth'))
]
),
new LexNode('else',
vscode.TreeItemCollapsibleState.None,
new LineToken(Symbol.ELSE, 8, 0)
)
])
}
];
suite('Parser Test Suite', () => {
after(() => {
vscode.window.showInformationMessage('All tests passed!');
});
for (var t of tests) {
let currTest = t; // without this, all test calls get the last test
test(currTest.name, () => {
let result: LexNode = deparent(new Parser(currTest.input.join('\n')).parse());
process.stdout.write(Object.entries(result).toString());
assert.deepStrictEqual(result, currTest.output);
});
}
});

57
src/test/util.ts Normal file
View File

@@ -0,0 +1,57 @@
import * as vscode from 'vscode';
import LexNode from '../pylex/node';
/**
* TODO: Eliminate need for me.
* Recursively deparents a LexNode tree. Needed
* because I wasn't able to iterate the circular parent-child
* relationship by hand
*/
function deparent(root: null): null;
function deparent(root: LexNode): LexNode;
function deparent(root: any): any {
if (root === null) {
return root;
} else {
if (root.children() !== null) {
return new LexNode(
root.label,
root.collapsibleState,
root.token,
root.children()!.map(deparent),
);
} else {
return new LexNode(
root.label,
root.collapsibleState,
root.token,
null,
null
);
}
}
}
/**
* "Roots" a list of lexNodes to match the parser
*
* Required to properly test the output of the parser,
* since the parent child-relationship can't be modeled
* exhaustively otherwise
*/
function root(nodes: LexNode[] | null): LexNode {
return new LexNode(
"root",
vscode.TreeItemCollapsibleState.None,
null,
nodes,
null
);
}
export {
deparent,
root,
};