feat(Parser): implement Parser
Add a simple parser implementation that supports only field reads.
This commit is contained in:
parent
acd7035347
commit
01e6c7b70c
|
@ -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) {}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
export class ClosureMap {
|
||||
getter(name:string) {
|
||||
return new Function('o', 'return o.' + name + ';');
|
||||
}
|
||||
}
|
|
@ -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,
|
|
@ -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<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();
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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']);
|
||||
|
|
|
@ -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<Token> = [];
|
||||
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<int> = lex("j");
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue