feat(Parser): implement Parser

Add a simple parser implementation that supports only field reads.
This commit is contained in:
vsavkin 2014-10-28 12:22:38 -04:00
parent acd7035347
commit 01e6c7b70c
11 changed files with 305 additions and 74 deletions

View File

@ -0,0 +1,39 @@
export class AST {
eval(context) {
}
visit(visitor) {
}
}
export class ImplicitReceiver extends AST {
eval(context) {
return context;
}
visit(visitor) {
visitor.visitImplicitReceiver(this);
}
}
export class FieldRead extends AST {
constructor(receiver:AST, name:string, getter:Function) {
this.receiver = receiver;
this.name = name;
this.getter = getter;
}
eval(context) {
return this.getter(this.receiver.eval(context));
}
visit(visitor) {
visitor.visitFieldRead(this);
}
}
//INTERFACE
export class AstVisitor {
visitImplicitReceiver(ast:ImplicitReceiver) {}
visitFieldRead(ast:FieldRead) {}
}

View File

@ -0,0 +1,8 @@
import 'dart:mirrors';
class ClosureMap {
Function getter(String name) {
var symbol = new Symbol(name);
return (receiver) => reflect(receiver).getField(symbol).reflectee;
}
}

View File

@ -0,0 +1,5 @@
export class ClosureMap {
getter(name:string) {
return new Function('o', 'return o.' + name + ';');
}
}

View File

@ -1,13 +1,25 @@
import {List, ListWrapper, SetWrapper} from "facade/collection"; import {List, ListWrapper, SetWrapper} from "facade/collection";
import {int, FIELD, NumberWrapper, StringJoiner, StringWrapper} from "facade/lang"; import {int, FIELD, NumberWrapper, StringJoiner, StringWrapper} from "facade/lang";
// TODO(chirayu): Rewrite as consts when possible. export const TOKEN_TYPE_CHARACTER = 1;
export var TOKEN_TYPE_CHARACTER = 1; export const TOKEN_TYPE_IDENTIFIER = 2;
export var TOKEN_TYPE_IDENTIFIER = 2; export const TOKEN_TYPE_KEYWORD = 3;
export var TOKEN_TYPE_KEYWORD = 3; export const TOKEN_TYPE_STRING = 4;
export var TOKEN_TYPE_STRING = 4; export const TOKEN_TYPE_OPERATOR = 5;
export var TOKEN_TYPE_OPERATOR = 5; export const TOKEN_TYPE_NUMBER = 6;
export var TOKEN_TYPE_NUMBER = 6;
export class Lexer {
tokenize(text:string):List {
var scanner = new _Scanner(text);
var tokens = [];
var token = scanner.scanToken();
while (token != null) {
ListWrapper.push(tokens, token);
token = scanner.scanToken();
}
return tokens;
}
}
export class Token { export class Token {
@FIELD('final index:int') @FIELD('final index:int')
@ -107,35 +119,35 @@ function newNumberToken(index:int, n:number):Token {
} }
var EOF:Token = new Token(-1, 0, 0, ""); export var EOF:Token = new Token(-1, 0, 0, "");
const $EOF = 0; export const $EOF = 0;
const $TAB = 9; export const $TAB = 9;
const $LF = 10; export const $LF = 10;
const $VTAB = 11; export const $VTAB = 11;
const $FF = 12; export const $FF = 12;
const $CR = 13; export const $CR = 13;
const $SPACE = 32; export const $SPACE = 32;
const $BANG = 33; export const $BANG = 33;
const $DQ = 34; export const $DQ = 34;
const $$ = 36; export const $$ = 36;
const $PERCENT = 37; export const $PERCENT = 37;
const $AMPERSAND = 38; export const $AMPERSAND = 38;
const $SQ = 39; export const $SQ = 39;
const $LPAREN = 40; export const $LPAREN = 40;
const $RPAREN = 41; export const $RPAREN = 41;
const $STAR = 42; export const $STAR = 42;
const $PLUS = 43; export const $PLUS = 43;
const $COMMA = 44; export const $COMMA = 44;
const $MINUS = 45; export const $MINUS = 45;
const $PERIOD = 46; export const $PERIOD = 46;
const $SLASH = 47; export const $SLASH = 47;
const $COLON = 58; export const $COLON = 58;
const $SEMICOLON = 59; export const $SEMICOLON = 59;
const $LT = 60; export const $LT = 60;
const $EQ = 61; export const $EQ = 61;
const $GT = 62; export const $GT = 62;
const $QUESTION = 63; export const $QUESTION = 63;
const $0 = 48; const $0 = 48;
const $9 = 57; const $9 = 57;
@ -173,7 +185,7 @@ export class ScannerError extends Error {
} }
} }
export class Scanner { class _Scanner {
@FIELD('final input:String') @FIELD('final input:String')
@FIELD('final length:int') @FIELD('final length:int')
@FIELD('peek:int') @FIELD('peek:int')
@ -191,7 +203,6 @@ export class Scanner {
this.peek = ++this.index >= this.length ? $EOF : StringWrapper.charCodeAt(this.input, this.index); this.peek = ++this.index >= this.length ? $EOF : StringWrapper.charCodeAt(this.input, this.index);
} }
scanToken():Token { scanToken():Token {
var input = this.input, var input = this.input,
length = this.length, length = this.length,

View File

@ -1,2 +1,81 @@
import {FIELD, int} from 'facade/lang';
import {ListWrapper, List} from 'facade/collection';
import {Lexer, EOF, Token, $PERIOD} from './lexer';
import {ClosureMap} from './closure_map';
import {AST, ImplicitReceiver, FieldRead} from './ast';
var _implicitReceiver = new ImplicitReceiver();
export class Parser { export class Parser {
@FIELD('final _lexer:Lexer')
@FIELD('final _closureMap:ClosureMap')
constructor(lexer:Lexer, closureMap:ClosureMap){
this._lexer = lexer;
this._closureMap = closureMap;
}
parse(input:string):AST {
var tokens = this._lexer.tokenize(input);
return new _ParseAST(tokens, this._closureMap).parseChain();
}
}
class _ParseAST {
@FIELD('final tokens:List<Token>')
@FIELD('final closureMap:ClosureMap')
@FIELD('index:int')
constructor(tokens:List, closureMap:ClosureMap) {
this.tokens = tokens;
this.index = 0;
this.closureMap = closureMap;
}
peek(offset:int):Token {
var i = this.index + offset;
return i < this.tokens.length ? this.tokens[i] : EOF;
}
get next():Token {
return this.peek(0);
}
advance() {
this.index ++;
}
optionalCharacter(code:int):boolean {
if (this.next.isCharacter(code)) {
this.advance();
return true;
} else {
return false;
}
}
parseChain():AST {
var exprs = [];
while (this.index < this.tokens.length) {
ListWrapper.push(exprs, this.parseAccess());
}
return ListWrapper.first(exprs);
}
parseAccess():AST {
var result = this.parseFieldRead(_implicitReceiver);
while(this.optionalCharacter($PERIOD)) {
result = this.parseFieldRead(result);
}
return result;
}
parseFieldRead(receiver):AST {
var id = this.parseIdentifier();
return new FieldRead(receiver, id, this.closureMap.getter(id));
}
parseIdentifier():string {
var n = this.next;
this.advance();
return n.toString();
}
} }

View File

@ -1,45 +1,42 @@
import {ProtoRecord, Record} from './record'; import {ProtoRecord, Record} from './record';
import {FIELD} from 'facade/lang'; import {FIELD, IMPLEMENTS, isBlank, isPresent} from 'facade/lang';
import {ListWrapper} from 'facade/collection'; import {AST, FieldRead, ImplicitReceiver, AstVisitor} from './parser/ast';
export class ProtoWatchGroup { export class ProtoWatchGroup {
@FIELD('final headRecord:ProtoRecord') @FIELD('headRecord:ProtoRecord')
@FIELD('final tailRecord:ProtoRecord') @FIELD('tailRecord:ProtoRecord')
constructor() { constructor() {
this.headRecord = null; this.headRecord = null;
this.tailRecord = null; this.tailRecord = null;
} }
/** /**
* Parses [expression] into [ProtoRecord]s and adds them to [ProtoWatchGroup]. * Parses [ast] into [ProtoRecord]s and adds them to [ProtoWatchGroup].
* *
* @param expression The expression to watch * @param ast The expression to watch
* @param memento an opaque object which will be passed to WatchGroupDispatcher on * @param memento an opaque object which will be passed to WatchGroupDispatcher on
* detecting a change. * detecting a change.
* @param shallow Should collections be shallow watched * @param shallow Should collections be shallow watched
*/ */
watch(expression:string, watch(ast:AST,
memento, memento,
shallow = false) shallow = false)
{ {
var parts = expression.split('.'); var creator = new ProtoRecordCreator(this);
var protoRecords = ListWrapper.createFixedSize(parts.length); creator.createRecordsFromAST(ast, memento);
this._addRecords(creator.headRecord, creator.tailRecord);
}
for (var i = parts.length - 1; i >= 0; i--) { // try to encapsulate this behavior in some class (e.g., LinkedList)
protoRecords[i] = new ProtoRecord(this, parts[i], memento); // so we can say: group.appendList(creator.list);
memento = null; _addRecords(head:ProtoRecord, tail:ProtoRecord) {
} if (isBlank(this.headRecord)) {
this.headRecord = head;
for (var i = 0; i < parts.length; i++) { } else {
var protoRecord = protoRecords[i]; this.tailRecord.next = head;
if (this.headRecord === null) { head.prev = this.tailRecord;
this.headRecord = this.tailRecord = protoRecord;
} else {
this.tailRecord.next = protoRecord;
protoRecord.prev = this.tailRecord;
this.tailRecord = protoRecord;
}
} }
this.tailRecord = tail;
} }
instantiate(dispatcher:WatchGroupDispatcher):WatchGroup { instantiate(dispatcher:WatchGroupDispatcher):WatchGroup {
@ -109,3 +106,41 @@ export class WatchGroupDispatcher {
// The record holds the previous value at the time of the call // The record holds the previous value at the time of the call
onRecordChange(record:Record, context) {} onRecordChange(record:Record, context) {}
} }
@IMPLEMENTS(AstVisitor)
class ProtoRecordCreator {
@FIELD('final protoWatchGroup:ProtoWatchGroup')
@FIELD('headRecord:ProtoRecord')
@FIELD('tailRecord:ProtoRecord')
constructor(protoWatchGroup) {
this.protoWatchGroup = protoWatchGroup;
this.headRecord = null;
this.tailRecord = null;
}
visitImplicitReceiver(ast:ImplicitReceiver) {
//do nothing
}
visitFieldRead(ast:FieldRead) {
ast.receiver.visit(this);
this.add(new ProtoRecord(this.protoWatchGroup, ast.name, null));
}
createRecordsFromAST(ast:AST, memento){
ast.visit(this);
if (isPresent(this.tailRecord)) {
this.tailRecord.dispatchMemento = memento;
}
}
add(protoRecord:ProtoRecord) {
if (this.headRecord === null) {
this.headRecord = this.tailRecord = protoRecord;
} else {
this.tailRecord.next = protoRecord;
protoRecord.prev = this.tailRecord;
this.tailRecord = protoRecord;
}
}
}

View File

@ -1,33 +1,47 @@
import {describe, it, xit, expect} from 'test_lib/test_lib'; import {describe, it, xit, expect} from 'test_lib/test_lib';
import {List, ListWrapper} from 'facade/collection'; import {List, ListWrapper} from 'facade/collection';
import {ImplicitReceiver, FieldRead} from 'change_detection/parser/ast';
import {ClosureMap} from 'change_detection/parser/closure_map';
import { import {
ChangeDetector, ChangeDetector,
ProtoWatchGroup, ProtoWatchGroup,
WatchGroup, WatchGroup,
WatchGroupDispatcher WatchGroupDispatcher,
ProtoRecord
} from 'change_detection/change_detector'; } from 'change_detection/change_detector';
import {Record} from 'change_detection/record'; import {Record} from 'change_detection/record';
export function main() { export function main() {
function ast(exp:string) {
var parts = exp.split(".");
var cm = new ClosureMap();
return ListWrapper.reduce(parts, function (ast, fieldName) {
return new FieldRead(ast, fieldName, cm.getter(fieldName));
}, new ImplicitReceiver());
}
describe('change_detection', function() { describe('change_detection', function() {
describe('ChangeDetection', function() { describe('ChangeDetection', function() {
it('should do simple watching', function() { it('should do simple watching', function() {
var person = new Person('misko', 38); var person = new Person('misko', 38);
var pwg = new ProtoWatchGroup(); var pwg = new ProtoWatchGroup();
pwg.watch('name', 'name'); pwg.watch(ast('name'), 'name');
pwg.watch('age', 'age'); pwg.watch(ast('age'), 'age');
var dispatcher = new LoggingDispatcher(); var dispatcher = new LoggingDispatcher();
var wg = pwg.instantiate(dispatcher); var wg = pwg.instantiate(dispatcher);
wg.setContext(person); wg.setContext(person);
var cd = new ChangeDetector(wg); var cd = new ChangeDetector(wg);
cd.detectChanges(); cd.detectChanges();
expect(dispatcher.log).toEqual(['name=misko', 'age=38']); expect(dispatcher.log).toEqual(['name=misko', 'age=38']);
dispatcher.clear(); dispatcher.clear();
cd.detectChanges(); cd.detectChanges();
expect(dispatcher.log).toEqual([]); expect(dispatcher.log).toEqual([]);
person.age = 1; person.age = 1;
person.name = "Misko"; person.name = "Misko";
cd.detectChanges(); cd.detectChanges();
@ -38,16 +52,19 @@ export function main() {
var address = new Address('Grenoble'); var address = new Address('Grenoble');
var person = new Person('Victor', 36, address); var person = new Person('Victor', 36, address);
var pwg = new ProtoWatchGroup(); var pwg = new ProtoWatchGroup();
pwg.watch('address.city', 'address.city', false); pwg.watch(ast('address.city'), 'address.city', false);
var dispatcher = new LoggingDispatcher(); var dispatcher = new LoggingDispatcher();
var wg = pwg.instantiate(dispatcher); var wg = pwg.instantiate(dispatcher);
wg.setContext(person); wg.setContext(person);
var cd = new ChangeDetector(wg); var cd = new ChangeDetector(wg);
cd.detectChanges(); cd.detectChanges();
expect(dispatcher.log).toEqual(['address.city=Grenoble']); expect(dispatcher.log).toEqual(['address.city=Grenoble']);
dispatcher.clear(); dispatcher.clear();
cd.detectChanges(); cd.detectChanges();
expect(dispatcher.log).toEqual([]); expect(dispatcher.log).toEqual([]);
address.city = 'Mountain View'; address.city = 'Mountain View';
cd.detectChanges(); cd.detectChanges();
expect(dispatcher.log).toEqual(['address.city=Mountain View']); expect(dispatcher.log).toEqual(['address.city=Mountain View']);

View File

@ -1,17 +1,10 @@
import {describe, it, expect} from 'test_lib/test_lib'; import {describe, it, expect} from 'test_lib/test_lib';
import {Scanner, Token} from 'change_detection/parser/scanner'; import {Lexer, Token} from 'change_detection/parser/lexer';
import {List, ListWrapper} from "facade/collection"; import {List, ListWrapper} from "facade/collection";
import {StringWrapper, int} from "facade/lang"; import {StringWrapper, int} from "facade/lang";
function lex(text:string):List { function lex(text:string):List {
var scanner:Scanner = new Scanner(text); return new Lexer().tokenize(text);
var tokens:List<Token> = [];
var token:Token = scanner.scanToken();
while (token != null) {
ListWrapper.push(tokens, token);
token = scanner.scanToken();
}
return tokens;
} }
function expectToken(token, index) { function expectToken(token, index) {
@ -56,7 +49,7 @@ function expectKeywordToken(token, index, keyword) {
export function main() { export function main() {
describe('scanner', function() { describe('lexer', function() {
describe('token', function() { describe('token', function() {
it('should tokenize a simple identifier', function() { it('should tokenize a simple identifier', function() {
var tokens:List<int> = lex("j"); var tokens:List<int> = lex("j");

View File

@ -0,0 +1,38 @@
import {ddescribe, describe, it, expect, beforeEach} from 'test_lib/test_lib';
import {Parser} from 'change_detection/parser/parser';
import {Lexer} from 'change_detection/parser/lexer';
import {ClosureMap} from 'change_detection/parser/closure_map';
class TestData {
constructor(a) {
this.a = a;
}
}
export function main() {
function td({a}) {
return new TestData(a);
}
describe("parser", () => {
describe("field access", () => {
var parser;
beforeEach(() => {
parser = new Parser(new Lexer(), new ClosureMap());
});
it("should parse field access",() => {
var exp = parser.parse("a");
var context = td({a: 999});
expect(exp.eval(context)).toEqual(999);
});
it("should parse nested field access",() => {
var exp = parser.parse("a.a");
var context = td({a: td({a: 999})});
expect(exp.eval(context)).toEqual(999);
});
});
});
}

View File

@ -39,6 +39,9 @@ class ListWrapper {
static forEach(list, fn) { static forEach(list, fn) {
list.forEach(fn); list.forEach(fn);
} }
static reduce(List list, Function fn, init) {
return list.fold(init, fn);
}
static first(List list) => list.first; static first(List list) => list.first;
static last(List list) => list.last; static last(List list) => list.last;
static List reversed(List list) => list.reversed.toList(); static List reversed(List list) => list.reversed.toList();

View File

@ -60,6 +60,9 @@ export class ListWrapper {
} }
return null; return null;
} }
static reduce(list:List, fn:Function, init) {
return list.reduce(fn, init);
}
static filter(array, pred:Function) { static filter(array, pred:Function) {
return array.filter(pred); return array.filter(pred);
} }