From 01e6c7b70c7ce33623a809ab29883a66503730ae Mon Sep 17 00:00:00 2001 From: vsavkin Date: Tue, 28 Oct 2014 12:22:38 -0400 Subject: [PATCH] feat(Parser): implement Parser Add a simple parser implementation that supports only field reads. --- modules/change_detection/src/parser/ast.js | 39 +++++++++ .../src/parser/closure_map.dart | 8 ++ .../src/parser/closure_map.es6 | 5 ++ .../src/parser/{scanner.js => lexer.js} | 85 +++++++++++-------- modules/change_detection/src/parser/parser.js | 79 +++++++++++++++++ modules/change_detection/src/watch_group.js | 81 +++++++++++++----- .../test/change_detector_spec.js | 25 +++++- .../parser/{scanner_spec.js => lexer_spec.js} | 13 +-- .../test/parser/parser_spec.js | 38 +++++++++ modules/facade/src/collection.dart | 3 + modules/facade/src/collection.es6 | 3 + 11 files changed, 305 insertions(+), 74 deletions(-) create mode 100644 modules/change_detection/src/parser/ast.js create mode 100644 modules/change_detection/src/parser/closure_map.dart create mode 100644 modules/change_detection/src/parser/closure_map.es6 rename modules/change_detection/src/parser/{scanner.js => lexer.js} (88%) rename modules/change_detection/test/parser/{scanner_spec.js => lexer_spec.js} (96%) create mode 100644 modules/change_detection/test/parser/parser_spec.js diff --git a/modules/change_detection/src/parser/ast.js b/modules/change_detection/src/parser/ast.js new file mode 100644 index 0000000000..eaa3428683 --- /dev/null +++ b/modules/change_detection/src/parser/ast.js @@ -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) {} +} \ No newline at end of file diff --git a/modules/change_detection/src/parser/closure_map.dart b/modules/change_detection/src/parser/closure_map.dart new file mode 100644 index 0000000000..8dc5b50179 --- /dev/null +++ b/modules/change_detection/src/parser/closure_map.dart @@ -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; + } +} \ No newline at end of file diff --git a/modules/change_detection/src/parser/closure_map.es6 b/modules/change_detection/src/parser/closure_map.es6 new file mode 100644 index 0000000000..a7635f06e1 --- /dev/null +++ b/modules/change_detection/src/parser/closure_map.es6 @@ -0,0 +1,5 @@ +export class ClosureMap { + getter(name:string) { + return new Function('o', 'return o.' + name + ';'); + } +} \ No newline at end of file diff --git a/modules/change_detection/src/parser/scanner.js b/modules/change_detection/src/parser/lexer.js similarity index 88% rename from modules/change_detection/src/parser/scanner.js rename to modules/change_detection/src/parser/lexer.js index bf7e8ef0ba..9899bd6f99 100644 --- a/modules/change_detection/src/parser/scanner.js +++ b/modules/change_detection/src/parser/lexer.js @@ -1,13 +1,25 @@ import {List, ListWrapper, SetWrapper} from "facade/collection"; import {int, FIELD, NumberWrapper, StringJoiner, StringWrapper} from "facade/lang"; -// TODO(chirayu): Rewrite as consts when possible. -export var TOKEN_TYPE_CHARACTER = 1; -export var TOKEN_TYPE_IDENTIFIER = 2; -export var TOKEN_TYPE_KEYWORD = 3; -export var TOKEN_TYPE_STRING = 4; -export var TOKEN_TYPE_OPERATOR = 5; -export var TOKEN_TYPE_NUMBER = 6; +export const TOKEN_TYPE_CHARACTER = 1; +export const TOKEN_TYPE_IDENTIFIER = 2; +export const TOKEN_TYPE_KEYWORD = 3; +export const TOKEN_TYPE_STRING = 4; +export const TOKEN_TYPE_OPERATOR = 5; +export const 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 { @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; -const $TAB = 9; -const $LF = 10; -const $VTAB = 11; -const $FF = 12; -const $CR = 13; -const $SPACE = 32; -const $BANG = 33; -const $DQ = 34; -const $$ = 36; -const $PERCENT = 37; -const $AMPERSAND = 38; -const $SQ = 39; -const $LPAREN = 40; -const $RPAREN = 41; -const $STAR = 42; -const $PLUS = 43; -const $COMMA = 44; -const $MINUS = 45; -const $PERIOD = 46; -const $SLASH = 47; -const $COLON = 58; -const $SEMICOLON = 59; -const $LT = 60; -const $EQ = 61; -const $GT = 62; -const $QUESTION = 63; +export const $EOF = 0; +export const $TAB = 9; +export const $LF = 10; +export const $VTAB = 11; +export const $FF = 12; +export const $CR = 13; +export const $SPACE = 32; +export const $BANG = 33; +export const $DQ = 34; +export const $$ = 36; +export const $PERCENT = 37; +export const $AMPERSAND = 38; +export const $SQ = 39; +export const $LPAREN = 40; +export const $RPAREN = 41; +export const $STAR = 42; +export const $PLUS = 43; +export const $COMMA = 44; +export const $MINUS = 45; +export const $PERIOD = 46; +export const $SLASH = 47; +export const $COLON = 58; +export const $SEMICOLON = 59; +export const $LT = 60; +export const $EQ = 61; +export const $GT = 62; +export const $QUESTION = 63; const $0 = 48; const $9 = 57; @@ -173,7 +185,7 @@ export class ScannerError extends Error { } } -export class Scanner { +class _Scanner { @FIELD('final input:String') @FIELD('final length:int') @FIELD('peek:int') @@ -191,7 +203,6 @@ export class Scanner { this.peek = ++this.index >= this.length ? $EOF : StringWrapper.charCodeAt(this.input, this.index); } - scanToken():Token { var input = this.input, length = this.length, diff --git a/modules/change_detection/src/parser/parser.js b/modules/change_detection/src/parser/parser.js index 465d66a8f2..8ae97bc21c 100644 --- a/modules/change_detection/src/parser/parser.js +++ b/modules/change_detection/src/parser/parser.js @@ -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 { + @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') + @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(); + } +} \ No newline at end of file diff --git a/modules/change_detection/src/watch_group.js b/modules/change_detection/src/watch_group.js index 75914eed73..bc57a08644 100644 --- a/modules/change_detection/src/watch_group.js +++ b/modules/change_detection/src/watch_group.js @@ -1,45 +1,42 @@ import {ProtoRecord, Record} from './record'; -import {FIELD} from 'facade/lang'; -import {ListWrapper} from 'facade/collection'; +import {FIELD, IMPLEMENTS, isBlank, isPresent} from 'facade/lang'; +import {AST, FieldRead, ImplicitReceiver, AstVisitor} from './parser/ast'; export class ProtoWatchGroup { - @FIELD('final headRecord:ProtoRecord') - @FIELD('final tailRecord:ProtoRecord') + @FIELD('headRecord:ProtoRecord') + @FIELD('tailRecord:ProtoRecord') constructor() { this.headRecord = 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 * detecting a change. * @param shallow Should collections be shallow watched */ - watch(expression:string, + watch(ast:AST, memento, shallow = false) { - var parts = expression.split('.'); - var protoRecords = ListWrapper.createFixedSize(parts.length); + var creator = new ProtoRecordCreator(this); + creator.createRecordsFromAST(ast, memento); + this._addRecords(creator.headRecord, creator.tailRecord); + } - for (var i = parts.length - 1; i >= 0; i--) { - protoRecords[i] = new ProtoRecord(this, parts[i], memento); - memento = null; - } - - for (var i = 0; i < parts.length; i++) { - var protoRecord = protoRecords[i]; - if (this.headRecord === null) { - this.headRecord = this.tailRecord = protoRecord; - } else { - this.tailRecord.next = protoRecord; - protoRecord.prev = this.tailRecord; - this.tailRecord = protoRecord; - } + // try to encapsulate this behavior in some class (e.g., LinkedList) + // so we can say: group.appendList(creator.list); + _addRecords(head:ProtoRecord, tail:ProtoRecord) { + if (isBlank(this.headRecord)) { + this.headRecord = head; + } else { + this.tailRecord.next = head; + head.prev = this.tailRecord; } + this.tailRecord = tail; } instantiate(dispatcher:WatchGroupDispatcher):WatchGroup { @@ -109,3 +106,41 @@ export class WatchGroupDispatcher { // The record holds the previous value at the time of the call 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; + } + } +} \ No newline at end of file diff --git a/modules/change_detection/test/change_detector_spec.js b/modules/change_detection/test/change_detector_spec.js index 20599d9392..d9d679cc6b 100644 --- a/modules/change_detection/test/change_detector_spec.js +++ b/modules/change_detection/test/change_detector_spec.js @@ -1,33 +1,47 @@ import {describe, it, xit, expect} from 'test_lib/test_lib'; import {List, ListWrapper} from 'facade/collection'; +import {ImplicitReceiver, FieldRead} from 'change_detection/parser/ast'; +import {ClosureMap} from 'change_detection/parser/closure_map'; import { ChangeDetector, ProtoWatchGroup, WatchGroup, - WatchGroupDispatcher + WatchGroupDispatcher, + ProtoRecord } from 'change_detection/change_detector'; import {Record} from 'change_detection/record'; 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('ChangeDetection', function() { it('should do simple watching', function() { var person = new Person('misko', 38); var pwg = new ProtoWatchGroup(); - pwg.watch('name', 'name'); - pwg.watch('age', 'age'); + pwg.watch(ast('name'), 'name'); + pwg.watch(ast('age'), 'age'); var dispatcher = new LoggingDispatcher(); var wg = pwg.instantiate(dispatcher); wg.setContext(person); + var cd = new ChangeDetector(wg); cd.detectChanges(); expect(dispatcher.log).toEqual(['name=misko', 'age=38']); + dispatcher.clear(); cd.detectChanges(); expect(dispatcher.log).toEqual([]); + person.age = 1; person.name = "Misko"; cd.detectChanges(); @@ -38,16 +52,19 @@ export function main() { var address = new Address('Grenoble'); var person = new Person('Victor', 36, address); var pwg = new ProtoWatchGroup(); - pwg.watch('address.city', 'address.city', false); + pwg.watch(ast('address.city'), 'address.city', false); var dispatcher = new LoggingDispatcher(); var wg = pwg.instantiate(dispatcher); wg.setContext(person); + var cd = new ChangeDetector(wg); cd.detectChanges(); expect(dispatcher.log).toEqual(['address.city=Grenoble']); + dispatcher.clear(); cd.detectChanges(); expect(dispatcher.log).toEqual([]); + address.city = 'Mountain View'; cd.detectChanges(); expect(dispatcher.log).toEqual(['address.city=Mountain View']); diff --git a/modules/change_detection/test/parser/scanner_spec.js b/modules/change_detection/test/parser/lexer_spec.js similarity index 96% rename from modules/change_detection/test/parser/scanner_spec.js rename to modules/change_detection/test/parser/lexer_spec.js index 2473212447..e4b5c9ef5d 100644 --- a/modules/change_detection/test/parser/scanner_spec.js +++ b/modules/change_detection/test/parser/lexer_spec.js @@ -1,17 +1,10 @@ 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 {StringWrapper, int} from "facade/lang"; function lex(text:string):List { - var scanner:Scanner = new Scanner(text); - var tokens:List = []; - var token:Token = scanner.scanToken(); - while (token != null) { - ListWrapper.push(tokens, token); - token = scanner.scanToken(); - } - return tokens; + return new Lexer().tokenize(text); } function expectToken(token, index) { @@ -56,7 +49,7 @@ function expectKeywordToken(token, index, keyword) { export function main() { - describe('scanner', function() { + describe('lexer', function() { describe('token', function() { it('should tokenize a simple identifier', function() { var tokens:List = lex("j"); diff --git a/modules/change_detection/test/parser/parser_spec.js b/modules/change_detection/test/parser/parser_spec.js new file mode 100644 index 0000000000..9e0dbe85d9 --- /dev/null +++ b/modules/change_detection/test/parser/parser_spec.js @@ -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); + }); + }); + }); +} \ No newline at end of file diff --git a/modules/facade/src/collection.dart b/modules/facade/src/collection.dart index 9809b8080b..6e5aac3754 100644 --- a/modules/facade/src/collection.dart +++ b/modules/facade/src/collection.dart @@ -39,6 +39,9 @@ class ListWrapper { static forEach(list, fn) { list.forEach(fn); } + static reduce(List list, Function fn, init) { + return list.fold(init, fn); + } static first(List list) => list.first; static last(List list) => list.last; static List reversed(List list) => list.reversed.toList(); diff --git a/modules/facade/src/collection.es6 b/modules/facade/src/collection.es6 index 49a60b8043..a41c9bdc77 100644 --- a/modules/facade/src/collection.es6 +++ b/modules/facade/src/collection.es6 @@ -60,6 +60,9 @@ export class ListWrapper { } return null; } + static reduce(list:List, fn:Function, init) { + return list.reduce(fn, init); +} static filter(array, pred:Function) { return array.filter(pred); }