chore(packaging): move files to match target file structure

This commit is contained in:
Yegor Jbanov
2015-02-04 23:05:13 -08:00
parent 7ce4f66cdc
commit 3820609f24
149 changed files with 0 additions and 77 deletions

View File

@ -0,0 +1,53 @@
import {isPresent} from 'facade/src/lang';
import {List, ListWrapper} from 'facade/src/collection';
import {ChangeDetector, CHECK_ALWAYS, CHECK_ONCE, CHECKED, DETACHED} from './interfaces';
export class AbstractChangeDetector extends ChangeDetector {
children:List;
parent:ChangeDetector;
status:string;
constructor() {
this.children = [];
this.status = CHECK_ALWAYS;
}
addChild(cd:ChangeDetector) {
ListWrapper.push(this.children, cd);
cd.parent = this;
}
removeChild(cd:ChangeDetector) {
ListWrapper.remove(this.children, cd);
}
remove() {
this.parent.removeChild(this);
}
detectChanges() {
this._detectChanges(false);
}
checkNoChanges() {
this._detectChanges(true);
}
_detectChanges(throwOnChange:boolean) {
if (this.mode === DETACHED || this.mode === CHECKED) return;
this.detectChangesInRecords(throwOnChange);
this._detectChangesInChildren(throwOnChange);
if (this.mode === CHECK_ONCE) this.mode = CHECKED;
}
detectChangesInRecords(throwOnChange:boolean){}
_detectChangesInChildren(throwOnChange:boolean) {
var children = this.children;
for(var i = 0; i < children.length; ++i) {
children[i]._detectChanges(throwOnChange);
}
}
}

View File

@ -0,0 +1,653 @@
import {
isListLikeIterable,
iterateListLike,
ListWrapper,
MapWrapper
} from 'facade/src/collection';
import {
int,
isBlank,
isPresent,
stringify,
getMapKey,
looseIdentical,
} from 'facade/src/lang';
export class ArrayChanges {
_collection;
_length:int;
_linkedRecords:_DuplicateMap;
_unlinkedRecords:_DuplicateMap;
_previousItHead:CollectionChangeRecord<V>;
_itHead:CollectionChangeRecord<V>;
_itTail:CollectionChangeRecord<V>;
_additionsHead:CollectionChangeRecord<V>;
_additionsTail:CollectionChangeRecord<V>;
_movesHead:CollectionChangeRecord<V>;
_movesTail:CollectionChangeRecord<V> ;
_removalsHead:CollectionChangeRecord<V>;
_removalsTail:CollectionChangeRecord<V>;
constructor() {
this._collection = null;
this._length = null;
/// Keeps track of the used records at any point in time (during & across `_check()` calls)
this._linkedRecords = null;
/// Keeps track of the removed records at any point in time during `_check()` calls.
this._unlinkedRecords = null;
this._previousItHead = null;
this._itHead = null;
this._itTail = null;
this._additionsHead = null;
this._additionsTail = null;
this._movesHead = null;
this._movesTail = null;
this._removalsHead = null;
this._removalsTail = null;
}
static supports(obj):boolean {
return isListLikeIterable(obj);
}
supportsObj(obj):boolean {
return ArrayChanges.supports(obj);
}
get collection() {
return this._collection;
}
get length():int {
return this._length;
}
forEachItem(fn:Function) {
var record:CollectionChangeRecord;
for (record = this._itHead; record !== null; record = record._next) {
fn(record);
}
}
forEachPreviousItem(fn:Function) {
var record:CollectionChangeRecord;
for (record = this._previousItHead; record !== null; record = record._nextPrevious) {
fn(record);
}
}
forEachAddedItem(fn:Function){
var record:CollectionChangeRecord;
for (record = this._additionsHead; record !== null; record = record._nextAdded) {
fn(record);
}
}
forEachMovedItem(fn:Function) {
var record:CollectionChangeRecord;
for (record = this._movesHead; record !== null; record = record._nextMoved) {
fn(record);
}
}
forEachRemovedItem(fn:Function){
var record:CollectionChangeRecord;
for (record = this._removalsHead; record !== null; record = record._nextRemoved) {
fn(record);
}
}
// todo(vicb): optim for UnmodifiableListView (frozen arrays)
check(collection):boolean {
this._reset();
var record:CollectionChangeRecord = this._itHead;
var mayBeDirty:boolean = false;
var index:int, item;
if (ListWrapper.isList(collection)) {
var list = collection;
this._length = collection.length;
for (index = 0; index < this._length; index++) {
item = list[index];
if (record === null || !looseIdentical(record.item, item)) {
record = this._mismatch(record, item, index);
mayBeDirty = true;
} else if (mayBeDirty) {
// TODO(misko): can we limit this to duplicates only?
record = this._verifyReinsertion(record, item, index);
}
record = record._next;
}
} else {
index = 0;
iterateListLike(collection, (item) => {
if (record === null || !looseIdentical(record.item, item)) {
record = this._mismatch(record, item, index);
mayBeDirty = true;
} else if (mayBeDirty) {
// TODO(misko): can we limit this to duplicates only?
record = this._verifyReinsertion(record, item, index);
}
record = record._next;
index++
});
this._length = index;
}
this._truncate(record);
this._collection = collection;
return this.isDirty;
}
// CollectionChanges is considered dirty if it has any additions, moves or removals.
get isDirty():boolean {
return this._additionsHead !== null ||
this._movesHead !== null ||
this._removalsHead !== null;
}
/**
* Reset the state of the change objects to show no changes. This means set previousKey to
* currentKey, and clear all of the queues (additions, moves, removals).
* Set the previousIndexes of moved and added items to their currentIndexes
* Reset the list of additions, moves and removals
*/
_reset() {
if (this.isDirty) {
var record:CollectionChangeRecord;
var nextRecord:CollectionChangeRecord;
for (record = this._previousItHead = this._itHead; record !== null; record = record._next) {
record._nextPrevious = record._next;
}
for (record = this._additionsHead; record !== null; record = record._nextAdded) {
record.previousIndex = record.currentIndex;
}
this._additionsHead = this._additionsTail = null;
for (record = this._movesHead; record !== null; record = nextRecord) {
record.previousIndex = record.currentIndex;
nextRecord = record._nextMoved;
}
this._movesHead = this._movesTail = null;
this._removalsHead = this._removalsTail = null;
// todo(vicb) when assert gets supported
// assert(!this.isDirty);
}
}
/**
* This is the core function which handles differences between collections.
*
* - [record] is the record which we saw at this position last time. If null then it is a new
* item.
* - [item] is the current item in the collection
* - [index] is the position of the item in the collection
*/
_mismatch(record:CollectionChangeRecord, item, index:int):CollectionChangeRecord {
// The previous record after which we will append the current one.
var previousRecord:CollectionChangeRecord;
if (record === null) {
previousRecord = this._itTail;
} else {
previousRecord = record._prev;
// Remove the record from the collection since we know it does not match the item.
this._remove(record);
}
// Attempt to see if we have seen the item before.
record = this._linkedRecords === null ? null : this._linkedRecords.get(item, index);
if (record !== null) {
// We have seen this before, we need to move it forward in the collection.
this._moveAfter(record, previousRecord, index);
} else {
// Never seen it, check evicted list.
record = this._unlinkedRecords === null ? null : this._unlinkedRecords.get(item);
if (record !== null) {
// It is an item which we have evicted earlier: reinsert it back into the list.
this._reinsertAfter(record, previousRecord, index);
} else {
// It is a new item: add it.
record = this._addAfter(new CollectionChangeRecord(item), previousRecord, index);
}
}
return record;
}
/**
* This check is only needed if an array contains duplicates. (Short circuit of nothing dirty)
*
* Use case: `[a, a]` => `[b, a, a]`
*
* If we did not have this check then the insertion of `b` would:
* 1) evict first `a`
* 2) insert `b` at `0` index.
* 3) leave `a` at index `1` as is. <-- this is wrong!
* 3) reinsert `a` at index 2. <-- this is wrong!
*
* The correct behavior is:
* 1) evict first `a`
* 2) insert `b` at `0` index.
* 3) reinsert `a` at index 1.
* 3) move `a` at from `1` to `2`.
*
*
* Double check that we have not evicted a duplicate item. We need to check if the item type may
* have already been removed:
* The insertion of b will evict the first 'a'. If we don't reinsert it now it will be reinserted
* at the end. Which will show up as the two 'a's switching position. This is incorrect, since a
* better way to think of it is as insert of 'b' rather then switch 'a' with 'b' and then add 'a'
* at the end.
*/
_verifyReinsertion(record:CollectionChangeRecord, item, index:int):CollectionChangeRecord {
var reinsertRecord:CollectionChangeRecord = this._unlinkedRecords === null ?
null : this._unlinkedRecords.get(item);
if (reinsertRecord !== null) {
record = this._reinsertAfter(reinsertRecord, record._prev, index);
} else if (record.currentIndex != index) {
record.currentIndex = index;
this._addToMoves(record, index);
}
return record;
}
/**
* Get rid of any excess [CollectionChangeRecord]s from the previous collection
*
* - [record] The first excess [CollectionChangeRecord].
*/
_truncate(record:CollectionChangeRecord) {
// Anything after that needs to be removed;
while (record !== null) {
var nextRecord:CollectionChangeRecord = record._next;
this._addToRemovals(this._unlink(record));
record = nextRecord;
}
if (this._unlinkedRecords !== null) {
this._unlinkedRecords.clear();
}
if (this._additionsTail !== null) {
this._additionsTail._nextAdded = null;
}
if (this._movesTail !== null) {
this._movesTail._nextMoved = null;
}
if (this._itTail !== null) {
this._itTail._next = null;
}
if (this._removalsTail !== null) {
this._removalsTail._nextRemoved = null;
}
}
_reinsertAfter(record:CollectionChangeRecord, prevRecord:CollectionChangeRecord,
index:int):CollectionChangeRecord {
if (this._unlinkedRecords !== null) {
this._unlinkedRecords.remove(record);
}
var prev = record._prevRemoved;
var next = record._nextRemoved;
if (prev === null) {
this._removalsHead = next;
} else {
prev._nextRemoved = next;
}
if (next === null) {
this._removalsTail = prev;
} else {
next._prevRemoved = prev;
}
this._insertAfter(record, prevRecord, index);
this._addToMoves(record, index);
return record;
}
_moveAfter(record:CollectionChangeRecord, prevRecord:CollectionChangeRecord,
index:int):CollectionChangeRecord {
this._unlink(record);
this._insertAfter(record, prevRecord, index);
this._addToMoves(record, index);
return record;
}
_addAfter(record:CollectionChangeRecord, prevRecord:CollectionChangeRecord,
index:int):CollectionChangeRecord {
this._insertAfter(record, prevRecord, index);
if (this._additionsTail === null) {
// todo(vicb)
//assert(this._additionsHead === null);
this._additionsTail = this._additionsHead = record;
} else {
// todo(vicb)
//assert(_additionsTail._nextAdded === null);
//assert(record._nextAdded === null);
this._additionsTail = this._additionsTail._nextAdded = record;
}
return record;
}
_insertAfter(record:CollectionChangeRecord, prevRecord:CollectionChangeRecord,
index:int):CollectionChangeRecord {
// todo(vicb)
//assert(record != prevRecord);
//assert(record._next === null);
//assert(record._prev === null);
var next:CollectionChangeRecord = prevRecord === null ? this._itHead :prevRecord._next;
// todo(vicb)
//assert(next != record);
//assert(prevRecord != record);
record._next = next;
record._prev = prevRecord;
if (next === null) {
this._itTail = record;
} else {
next._prev = record;
}
if (prevRecord === null) {
this._itHead = record;
} else {
prevRecord._next = record;
}
if (this._linkedRecords === null) {
this._linkedRecords = new _DuplicateMap();
}
this._linkedRecords.put(record);
record.currentIndex = index;
return record;
}
_remove(record:CollectionChangeRecord):CollectionChangeRecord {
return this._addToRemovals(this._unlink(record));
}
_unlink(record:CollectionChangeRecord):CollectionChangeRecord {
if (this._linkedRecords !== null) {
this._linkedRecords.remove(record);
}
var prev = record._prev;
var next = record._next;
// todo(vicb)
//assert((record._prev = null) === null);
//assert((record._next = null) === null);
if (prev === null) {
this._itHead = next;
} else {
prev._next = next;
}
if (next === null) {
this._itTail = prev;
} else {
next._prev = prev;
}
return record;
}
_addToMoves(record:CollectionChangeRecord, toIndex:int):CollectionChangeRecord {
// todo(vicb)
//assert(record._nextMoved === null);
if (record.previousIndex === toIndex) {
return record;
}
if (this._movesTail === null) {
// todo(vicb)
//assert(_movesHead === null);
this._movesTail = this._movesHead = record;
} else {
// todo(vicb)
//assert(_movesTail._nextMoved === null);
this._movesTail = this._movesTail._nextMoved = record;
}
return record;
}
_addToRemovals(record:CollectionChangeRecord):CollectionChangeRecord {
if (this._unlinkedRecords === null) {
this._unlinkedRecords = new _DuplicateMap();
}
this._unlinkedRecords.put(record);
record.currentIndex = null;
record._nextRemoved = null;
if (this._removalsTail === null) {
// todo(vicb)
//assert(_removalsHead === null);
this._removalsTail = this._removalsHead = record;
record._prevRemoved = null;
} else {
// todo(vicb)
//assert(_removalsTail._nextRemoved === null);
//assert(record._nextRemoved === null);
record._prevRemoved = this._removalsTail;
this._removalsTail = this._removalsTail._nextRemoved = record;
}
return record;
}
toString():string {
var record:CollectionChangeRecord;
var list = [];
for (record = this._itHead; record !== null; record = record._next) {
ListWrapper.push(list, record);
}
var previous = [];
for (record = this._previousItHead; record !== null; record = record._nextPrevious) {
ListWrapper.push(previous, record);
}
var additions = [];
for (record = this._additionsHead; record !== null; record = record._nextAdded) {
ListWrapper.push(additions, record);
}
var moves = [];
for (record = this._movesHead; record !== null; record = record._nextMoved) {
ListWrapper.push(moves, record);
}
var removals = [];
for (record = this._removalsHead; record !== null; record = record._nextRemoved) {
ListWrapper.push(removals, record);
}
return "collection: " + list.join(', ') + "\n" +
"previous: " + previous.join(', ') + "\n" +
"additions: " + additions.join(', ') + "\n" +
"moves: " + moves.join(', ') + "\n" +
"removals: " + removals.join(', ') + "\n";
}
}
export class CollectionChangeRecord {
currentIndex:int;
previousIndex:int;
item;
_nextPrevious:CollectionChangeRecord;
_prev:CollectionChangeRecord; _next:CollectionChangeRecord;
_prevDup:CollectionChangeRecord; _nextDup:CollectionChangeRecord;
_prevRemoved:CollectionChangeRecord; _nextRemoved:CollectionChangeRecord;
_nextAdded:CollectionChangeRecord;
_nextMoved:CollectionChangeRecord;
constructor(item) {
this.currentIndex = null;
this.previousIndex = null;
this.item = item;
this._nextPrevious = null;
this._prev = null;
this._next = null;
this._prevDup = null;
this._nextDup = null;
this._prevRemoved = null;
this._nextRemoved = null;
this._nextAdded = null;
this._nextMoved = null;
}
toString():string {
return this.previousIndex === this.currentIndex ?
stringify(this.item) :
stringify(this.item) + '[' + stringify(this.previousIndex) + '->' +
stringify(this.currentIndex) + ']';
}
}
// A linked list of CollectionChangeRecords with the same CollectionChangeRecord.item
class _DuplicateItemRecordList {
_head:CollectionChangeRecord;
_tail:CollectionChangeRecord;
constructor() {
this._head = null;
this._tail = null;
}
/**
* Append the record to the list of duplicates.
*
* Note: by design all records in the list of duplicates hold the same value in record.item.
*/
add(record:CollectionChangeRecord) {
if (this._head === null) {
this._head = this._tail = record;
record._nextDup = null;
record._prevDup = null;
} else {
// todo(vicb)
//assert(record.item == _head.item ||
// record.item is num && record.item.isNaN && _head.item is num && _head.item.isNaN);
this._tail._nextDup = record;
record._prevDup = this._tail;
record._nextDup = null;
this._tail = record;
}
}
// Returns a CollectionChangeRecord having CollectionChangeRecord.item == item and
// CollectionChangeRecord.currentIndex >= afterIndex
get(item, afterIndex:int):CollectionChangeRecord {
var record:CollectionChangeRecord;
for (record = this._head; record !== null; record = record._nextDup) {
if ((afterIndex === null || afterIndex < record.currentIndex) &&
looseIdentical(record.item, item)) {
return record;
}
}
return null;
}
/**
* Remove one [CollectionChangeRecord] from the list of duplicates.
*
* Returns whether the list of duplicates is empty.
*/
remove(record:CollectionChangeRecord):boolean {
// todo(vicb)
//assert(() {
// // verify that the record being removed is in the list.
// for (CollectionChangeRecord cursor = _head; cursor != null; cursor = cursor._nextDup) {
// if (identical(cursor, record)) return true;
// }
// return false;
//});
var prev:CollectionChangeRecord = record._prevDup;
var next:CollectionChangeRecord = record._nextDup;
if (prev === null) {
this._head = next;
} else {
prev._nextDup = next;
}
if (next === null) {
this._tail = prev;
} else {
next._prevDup = prev;
}
return this._head === null;
}
}
class _DuplicateMap {
map:Map;
constructor() {
this.map = MapWrapper.create();
}
put(record:CollectionChangeRecord) {
// todo(vicb) handle corner cases
var key = getMapKey(record.item);
var duplicates = MapWrapper.get(this.map, key);
if (!isPresent(duplicates)) {
duplicates = new _DuplicateItemRecordList();
MapWrapper.set(this.map, key, duplicates);
}
duplicates.add(record);
}
/**
* Retrieve the `value` using key. Because the CollectionChangeRecord value maybe one which we
* have already iterated over, we use the afterIndex to pretend it is not there.
*
* Use case: `[a, b, c, a, a]` if we are at index `3` which is the second `a` then asking if we
* have any more `a`s needs to return the last `a` not the first or second.
*/
get(value, afterIndex = null):CollectionChangeRecord {
var key = getMapKey(value);
var recordList = MapWrapper.get(this.map, key);
return isBlank(recordList) ? null : recordList.get(value, afterIndex);
}
/**
* Removes an [CollectionChangeRecord] from the list of duplicates.
*
* The list of duplicates also is removed from the map if it gets empty.
*/
remove(record:CollectionChangeRecord):CollectionChangeRecord {
var key = getMapKey(record.item);
// todo(vicb)
//assert(this.map.containsKey(key));
var recordList:_DuplicateItemRecordList = MapWrapper.get(this.map, key);
// Remove the list of duplicates when it gets empty
if (recordList.remove(record)) {
MapWrapper.delete(this.map, key);
}
return record;
}
get isEmpty():boolean {
return MapWrapper.size(this.map) === 0;
}
clear() {
MapWrapper.clear(this.map);
}
toString():string {
return '_DuplicateMap(' + stringify(this.map) + ')';
}
}

View File

@ -0,0 +1,10 @@
library change_detectoin.change_detection_jit_generator;
class ChangeDetectorJITGenerator {
ChangeDetectorJITGenerator(typeName, records) {
}
generate() {
throw "Not supported in Dart";
}
}

View File

@ -0,0 +1,401 @@
import {isPresent, isBlank, BaseException, Type} from 'facade/src/lang';
import {List, ListWrapper, MapWrapper, StringMapWrapper} from 'facade/src/collection';
import {ContextWithVariableBindings} from './parser/context_with_variable_bindings';
import {AbstractChangeDetector} from './abstract_change_detector';
import {ChangeDetectionUtil} from './change_detection_util';
import {
ProtoRecord,
RECORD_TYPE_SELF,
RECORD_TYPE_PROPERTY,
RECORD_TYPE_INVOKE_METHOD,
RECORD_TYPE_CONST,
RECORD_TYPE_INVOKE_CLOSURE,
RECORD_TYPE_PRIMITIVE_OP,
RECORD_TYPE_KEYED_ACCESS,
RECORD_TYPE_INVOKE_FORMATTER,
RECORD_TYPE_STRUCTURAL_CHECK,
RECORD_TYPE_INTERPOLATE,
ProtoChangeDetector
} from './proto_change_detector';
/**
* The code generator takes a list of proto records and creates a function/class
* that "emulates" what the developer would write by hand to implement the same
* kind of behaviour.
*
* For example: An expression `address.city` will result in the following class:
*
* var ChangeDetector0 = function ChangeDetector0(dispatcher, formatters, protos) {
* AbstractChangeDetector.call(this);
* this.dispatcher = dispatcher;
* this.formatters = formatters;
* this.protos = protos;
*
* this.context = null;
* this.address0 = null;
* this.city1 = null;
* }
* ChangeDetector0.prototype = Object.create(AbstractChangeDetector.prototype);
*
* ChangeDetector0.prototype.detectChangesInRecords = function(throwOnChange) {
* var address0;
* var city1;
* var change;
* var changes = null;
* var temp;
* var context = this.context;
*
* temp = ChangeDetectionUtil.findContext("address", context);
* if (temp instanceof ContextWithVariableBindings) {
* address0 = temp.get('address');
* } else {
* address0 = temp.address;
* }
*
* if (address0 !== this.address0) {
* this.address0 = address0;
* }
*
* city1 = address0.city;
* if (city1 !== this.city1) {
* changes = ChangeDetectionUtil.addRecord(changes,
* ChangeDetectionUtil.simpleChangeRecord(this.protos[1].bindingMemento, this.city1, city1));
* this.city1 = city1;
* }
*
* if (changes.length > 0) {
* if(throwOnChange) ChangeDetectionUtil.throwOnChange(this.protos[1], changes[0]);
* this.dispatcher.onRecordChange('address.city', changes);
* changes = null;
* }
* }
*
*
* ChangeDetector0.prototype.setContext = function(context) {
* this.context = context;
* }
*
* return ChangeDetector0;
*
*
* The only thing the generated class depends on is the super class AbstractChangeDetector.
*
* The implementation comprises two parts:
* * ChangeDetectorJITGenerator has the logic of how everything fits together.
* * template functions (e.g., constructorTemplate) define what code is generated.
*/
var ABSTRACT_CHANGE_DETECTOR = "AbstractChangeDetector";
var UTIL = "ChangeDetectionUtil";
var DISPATCHER_ACCESSOR = "this.dispatcher";
var FORMATTERS_ACCESSOR = "this.formatters";
var PROTOS_ACCESSOR = "this.protos";
var CHANGE_LOCAL = "change";
var CHANGES_LOCAL = "changes";
var TEMP_LOCAL = "temp";
function typeTemplate(type:string, cons:string, detectChanges:string, setContext:string):string {
return `
${cons}
${detectChanges}
${setContext};
return function(dispatcher, formatters) {
return new ${type}(dispatcher, formatters, protos);
}
`;
}
function constructorTemplate(type:string, fieldsDefinitions:string):string {
return `
var ${type} = function ${type}(dispatcher, formatters, protos) {
${ABSTRACT_CHANGE_DETECTOR}.call(this);
${DISPATCHER_ACCESSOR} = dispatcher;
${FORMATTERS_ACCESSOR} = formatters;
${PROTOS_ACCESSOR} = protos;
${fieldsDefinitions}
}
${type}.prototype = Object.create(${ABSTRACT_CHANGE_DETECTOR}.prototype);
`;
}
function setContextTemplate(type:string):string {
return `
${type}.prototype.setContext = function(context) {
this.context = context;
}
`;
}
function detectChangesTemplate(type:string, body:string):string {
return `
${type}.prototype.detectChangesInRecords = function(throwOnChange) {
${body}
}
`;
}
function bodyTemplate(localDefinitions:string, changeDefinitions:string, records:string):string {
return `
${localDefinitions}
${changeDefinitions}
var ${TEMP_LOCAL};
var ${CHANGE_LOCAL};
var ${CHANGES_LOCAL} = null;
context = this.context;
${records}
`;
}
function notifyTemplate(index:number):string{
return `
if (${CHANGES_LOCAL} && ${CHANGES_LOCAL}.length > 0) {
if(throwOnChange) ${UTIL}.throwOnChange(${PROTOS_ACCESSOR}[${index}], ${CHANGES_LOCAL}[0]);
${DISPATCHER_ACCESSOR}.onRecordChange(${PROTOS_ACCESSOR}[${index}].groupMemento, ${CHANGES_LOCAL});
${CHANGES_LOCAL} = null;
}
`;
}
function structuralCheckTemplate(selfIndex:number, field:string, context:string, notify:string):string{
return `
${CHANGE_LOCAL} = ${UTIL}.structuralCheck(${field}, ${context});
if (${CHANGE_LOCAL}) {
${CHANGES_LOCAL} = ${UTIL}.addRecord(${CHANGES_LOCAL},
${UTIL}.changeRecord(${PROTOS_ACCESSOR}[${selfIndex}].bindingMemento, ${CHANGE_LOCAL}));
${field} = ${CHANGE_LOCAL}.currentValue;
}
${notify}
`;
}
function referenceCheckTemplate(assignment, newValue, oldValue, change, addRecord, notify) {
return `
${assignment}
if (${newValue} !== ${oldValue} || (${newValue} !== ${newValue}) && (${oldValue} !== ${oldValue})) {
${change} = true;
${addRecord}
${oldValue} = ${newValue};
}
${notify}
`;
}
function assignmentTemplate(field:string, value:string) {
return `${field} = ${value};`;
}
function propertyReadTemplate(name:string, context:string, newValue:string) {
return `
${TEMP_LOCAL} = ${UTIL}.findContext("${name}", ${context});
if (${TEMP_LOCAL} instanceof ContextWithVariableBindings) {
${newValue} = ${TEMP_LOCAL}.get('${name}');
} else {
${newValue} = ${TEMP_LOCAL}.${name};
}
`;
}
function localDefinitionsTemplate(names:List):string {
return names.map((n) => `var ${n};`).join("\n");
}
function changeDefinitionsTemplate(names:List):string {
return names.map((n) => `var ${n} = false;`).join("\n");
}
function fieldDefinitionsTemplate(names:List):string {
return names.map((n) => `${n} = ${UTIL}.unitialized();`).join("\n");
}
function ifChangedGuardTemplate(changeNames:List, body:string):string {
var cond = changeNames.join(" || ");
return `
if (${cond}) {
${body}
}
`;
}
function addSimpleChangeRecordTemplate(protoIndex:number, oldValue:string, newValue:string) {
return `${CHANGES_LOCAL} = ${UTIL}.addRecord(${CHANGES_LOCAL},
${UTIL}.simpleChangeRecord(${PROTOS_ACCESSOR}[${protoIndex}].bindingMemento, ${oldValue}, ${newValue}));`;
}
export class ChangeDetectorJITGenerator {
typeName:string;
records:List<ProtoRecord>;
localNames:List<String>;
changeNames:List<String>;
fieldNames:List<String>;
constructor(typeName:string, records:List<ProtoRecord>) {
this.typeName = typeName;
this.records = records;
this.localNames = this.getLocalNames(records);
this.changeNames = this.getChangeNames(this.localNames);
this.fieldNames = this.getFieldNames(this.localNames);
}
getLocalNames(records:List<ProtoRecord>):List<String> {
var index = 0;
var names = records.map((r) => {
var sanitizedName = r.name.replace(new RegExp("\\W", "g"), '');
return `${sanitizedName}${index++}`
});
return ["context"].concat(names);
}
getChangeNames(localNames:List<String>):List<String> {
return localNames.map((n) => `change_${n}`);
}
getFieldNames(localNames:List<String>):List<String> {
return localNames.map((n) => `this.${n}`);
}
generate():Function {
var text = typeTemplate(this.typeName, this.genConstructor(), this.genDetectChanges(), this.genSetContext());
return new Function('AbstractChangeDetector', 'ChangeDetectionUtil', 'ContextWithVariableBindings', 'protos', text)(AbstractChangeDetector, ChangeDetectionUtil, ContextWithVariableBindings, this.records);
}
genConstructor():string {
return constructorTemplate(this.typeName, fieldDefinitionsTemplate(this.fieldNames));
}
genSetContext():string {
return setContextTemplate(this.typeName);
}
genDetectChanges():string {
var body = this.genBody();
return detectChangesTemplate(this.typeName, body);
}
genBody():string {
var rec = this.records.map((r) => this.genRecord(r)).join("\n");
return bodyTemplate(this.genLocalDefinitions(), this.genChangeDefinitions(), rec);
}
genLocalDefinitions():string {
return localDefinitionsTemplate(this.localNames);
}
genChangeDefinitions():string {
return changeDefinitionsTemplate(this.changeNames);
}
genRecord(r:ProtoRecord):string {
if (r.mode == RECORD_TYPE_STRUCTURAL_CHECK) {
return this.getStructuralCheck(r);
} else {
return this.genReferenceCheck(r);
}
}
getStructuralCheck(r:ProtoRecord):string {
var field = this.fieldNames[r.selfIndex];
var context = this.localNames[r.contextIndex];
return structuralCheckTemplate(r.selfIndex - 1, field, context, this.genNotify(r));
}
genReferenceCheck(r:ProtoRecord):string {
var newValue = this.localNames[r.selfIndex];
var oldValue = this.fieldNames[r.selfIndex];
var change = this.changeNames[r.selfIndex];
var assignment = this.genUpdateCurrentValue(r);
var addRecord = addSimpleChangeRecordTemplate(r.selfIndex - 1, oldValue, newValue);
var notify = this.genNotify(r);
var check = referenceCheckTemplate(assignment, newValue, oldValue, change, r.lastInBinding ? addRecord : '', notify);;
if (r.isPureFunction()) {
return this.ifChangedGuard(r, check);
} else {
return check;
}
}
genUpdateCurrentValue(r:ProtoRecord):string {
var context = this.localNames[r.contextIndex];
var newValue = this.localNames[r.selfIndex];
var args = this.genArgs(r);
switch (r.mode) {
case RECORD_TYPE_SELF:
return assignmentTemplate(newValue, context);
case RECORD_TYPE_CONST:
return `${newValue} = ${this.genLiteral(r.funcOrValue)}`;
case RECORD_TYPE_PROPERTY:
if (r.contextIndex == 0) { // only the first property read can be a local
return propertyReadTemplate(r.name, context, newValue);
} else {
return assignmentTemplate(newValue, `${context}.${r.name}`);
}
case RECORD_TYPE_INVOKE_METHOD:
return assignmentTemplate(newValue, `${context}.${r.name}(${args})`);
case RECORD_TYPE_INVOKE_CLOSURE:
return assignmentTemplate(newValue, `${context}(${args})`);
case RECORD_TYPE_PRIMITIVE_OP:
return assignmentTemplate(newValue, `${UTIL}.${r.name}(${args})`);
case RECORD_TYPE_INTERPOLATE:
return assignmentTemplate(newValue, this.genInterpolation(r));
case RECORD_TYPE_INVOKE_FORMATTER:
return assignmentTemplate(newValue, `${FORMATTERS_ACCESSOR}.get("${r.name}")(${args})`);
case RECORD_TYPE_KEYED_ACCESS:
var key = this.localNames[r.args[0]];
return assignmentTemplate(newValue, `${context}[${key}]`);
default:
throw new BaseException(`Unknown operation ${r.mode}`);
}
}
ifChangedGuard(r:ProtoRecord, body:string):string {
return ifChangedGuardTemplate(r.args.map((a) => this.changeNames[a]), body);
}
genInterpolation(r:ProtoRecord):string{
var res = "";
for (var i = 0; i < r.args.length; ++i) {
res += this.genLiteral(r.fixedArgs[i]);
res += " + ";
res += this.localNames[r.args[i]];
res += " + ";
}
res += this.genLiteral(r.fixedArgs[r.args.length]);
return res;
}
genLiteral(value):string {
return JSON.stringify(value);
}
genNotify(r):string{
return r.lastInGroup ? notifyTemplate(r.selfIndex - 1) : '';
}
genArgs(r:ProtoRecord):string {
return r.args.map((arg) => this.localNames[arg]).join(", ");
}
}

View File

@ -0,0 +1,225 @@
import {isPresent, isBlank, BaseException, Type} from 'facade/src/lang';
import {List, ListWrapper, MapWrapper, StringMapWrapper} from 'facade/src/collection';
import {ContextWithVariableBindings} from './parser/context_with_variable_bindings';
import {ArrayChanges} from './array_changes';
import {KeyValueChanges} from './keyvalue_changes';
import {ProtoRecord} from './proto_change_detector';
import {ExpressionChangedAfterItHasBeenChecked} from './exceptions';
import {ChangeRecord, ChangeDetector, CHECK_ALWAYS, CHECK_ONCE, CHECKED, DETACHED} from './interfaces';
export var uninitialized = new Object();
export class SimpleChange {
previousValue:any;
currentValue:any;
constructor(previousValue:any, currentValue:any) {
this.previousValue = previousValue;
this.currentValue = currentValue;
}
}
var _simpleChangesIndex = 0;
var _simpleChanges = [
new SimpleChange(null, null),
new SimpleChange(null, null),
new SimpleChange(null, null),
new SimpleChange(null, null),
new SimpleChange(null, null),
new SimpleChange(null, null),
new SimpleChange(null, null),
new SimpleChange(null, null),
new SimpleChange(null, null),
new SimpleChange(null, null),
new SimpleChange(null, null),
new SimpleChange(null, null),
new SimpleChange(null, null),
new SimpleChange(null, null),
new SimpleChange(null, null),
new SimpleChange(null, null),
new SimpleChange(null, null),
new SimpleChange(null, null),
new SimpleChange(null, null),
new SimpleChange(null, null)
]
var _changeRecordsIndex = 0;
var _changeRecords = [
new ChangeRecord(null, null),
new ChangeRecord(null, null),
new ChangeRecord(null, null),
new ChangeRecord(null, null),
new ChangeRecord(null, null),
new ChangeRecord(null, null),
new ChangeRecord(null, null),
new ChangeRecord(null, null),
new ChangeRecord(null, null),
new ChangeRecord(null, null),
new ChangeRecord(null, null),
new ChangeRecord(null, null),
new ChangeRecord(null, null),
new ChangeRecord(null, null),
new ChangeRecord(null, null),
new ChangeRecord(null, null),
new ChangeRecord(null, null),
new ChangeRecord(null, null),
new ChangeRecord(null, null),
new ChangeRecord(null, null)
]
function _simpleChange(previousValue, currentValue) {
var index = _simpleChangesIndex++ % 20;
var s = _simpleChanges[index];
s.previousValue = previousValue;
s.currentValue = currentValue;
return s;
}
function _changeRecord(bindingMemento, change) {
var index = _changeRecordsIndex++ % 20;
var s = _changeRecords[index];
s.bindingMemento = bindingMemento;
s.change = change;
return s;
}
var _singleElementList = [null];
function _isBlank(val):boolean {
return isBlank(val) || val === uninitialized;
}
export class ChangeDetectionUtil {
static unitialized() {
return uninitialized;
}
static arrayFn0() { return []; }
static arrayFn1(a1) { return [a1]; }
static arrayFn2(a1, a2) { return [a1, a2]; }
static arrayFn3(a1, a2, a3) { return [a1, a2, a3]; }
static arrayFn4(a1, a2, a3, a4) { return [a1, a2, a3, a4]; }
static arrayFn5(a1, a2, a3, a4, a5) { return [a1, a2, a3, a4, a5]; }
static arrayFn6(a1, a2, a3, a4, a5, a6) { return [a1, a2, a3, a4, a5, a6]; }
static arrayFn7(a1, a2, a3, a4, a5, a6, a7) { return [a1, a2, a3, a4, a5, a6, a7]; }
static arrayFn8(a1, a2, a3, a4, a5, a6, a7, a8) { return [a1, a2, a3, a4, a5, a6, a7, a8]; }
static arrayFn9(a1, a2, a3, a4, a5, a6, a7, a8, a9) { return [a1, a2, a3, a4, a5, a6, a7, a8, a9]; }
static operation_negate(value) {return !value;}
static operation_add(left, right) {return left + right;}
static operation_subtract(left, right) {return left - right;}
static operation_multiply(left, right) {return left * right;}
static operation_divide(left, right) {return left / right;}
static operation_remainder(left, right) {return left % right;}
static operation_equals(left, right) {return left == right;}
static operation_not_equals(left, right) {return left != right;}
static operation_less_then(left, right) {return left < right;}
static operation_greater_then(left, right) {return left > right;}
static operation_less_or_equals_then(left, right) {return left <= right;}
static operation_greater_or_equals_then(left, right) {return left >= right;}
static operation_logical_and(left, right) {return left && right;}
static operation_logical_or(left, right) {return left || right;}
static cond(cond, trueVal, falseVal) {return cond ? trueVal : falseVal;}
static mapFn(keys:List) {
function buildMap(values) {
var res = StringMapWrapper.create();
for(var i = 0; i < keys.length; ++i) {
StringMapWrapper.set(res, keys[i], values[i]);
}
return res;
}
switch (keys.length) {
case 0: return () => [];
case 1: return (a1) => buildMap([a1]);
case 2: return (a1, a2) => buildMap([a1, a2]);
case 3: return (a1, a2, a3) => buildMap([a1, a2, a3]);
case 4: return (a1, a2, a3, a4) => buildMap([a1, a2, a3, a4]);
case 5: return (a1, a2, a3, a4, a5) => buildMap([a1, a2, a3, a4, a5]);
case 6: return (a1, a2, a3, a4, a5, a6) => buildMap([a1, a2, a3, a4, a5, a6]);
case 7: return (a1, a2, a3, a4, a5, a6, a7) => buildMap([a1, a2, a3, a4, a5, a6, a7]);
case 8: return (a1, a2, a3, a4, a5, a6, a7, a8) => buildMap([a1, a2, a3, a4, a5, a6, a7, a8]);
case 9: return (a1, a2, a3, a4, a5, a6, a7, a8, a9) => buildMap([a1, a2, a3, a4, a5, a6, a7, a8, a9]);
default: throw new BaseException(`Does not support literal maps with more than 9 elements`);
}
}
static keyedAccess(obj, args) {
return obj[args[0]];
}
static structuralCheck(self, context) {
if (_isBlank(self) && _isBlank(context)) {
return null;
} else if (_isBlank(context)) {
return new SimpleChange(null, null);
}
if (_isBlank(self)) {
if (ArrayChanges.supports(context)) {
self = new ArrayChanges();
} else if (KeyValueChanges.supports(context)) {
self = new KeyValueChanges();
}
}
if (isBlank(self) || !self.supportsObj(context)) {
throw new BaseException(`Unsupported type (${context})`);
}
if (self.check(context)) {
return new SimpleChange(null, self); // TODO: don't wrap and return self instead
} else {
return null;
}
}
static findContext(name:string, c){
while (c instanceof ContextWithVariableBindings) {
if (c.hasBinding(name)) {
return c;
}
c = c.parent;
}
return c;
}
static throwOnChange(proto:ProtoRecord, change) {
throw new ExpressionChangedAfterItHasBeenChecked(proto, change);
}
static simpleChange(previousValue:any, currentValue:any):SimpleChange {
return _simpleChange(previousValue, currentValue);
}
static changeRecord(memento:any, change:any):ChangeRecord {
return _changeRecord(memento, change);
}
static simpleChangeRecord(memento:any, previousValue:any, currentValue:any):ChangeRecord {
return _changeRecord(memento, _simpleChange(previousValue, currentValue));
}
static addRecord(updatedRecords:List, changeRecord:ChangeRecord):List {
if (isBlank(updatedRecords)) {
updatedRecords = _singleElementList;
updatedRecords[0] = changeRecord;
} else if (updatedRecords === _singleElementList) {
updatedRecords = [_singleElementList[0], changeRecord];
} else {
ListWrapper.push(updatedRecords, changeRecord);
}
return updatedRecords;
}
static markPathToRootAsCheckOnce(cd:ChangeDetector) {
var c = cd;
while(isPresent(c) && c.mode != DETACHED) {
if (c.mode === CHECKED) c.mode = CHECK_ONCE;
c = c.parent;
}
}
}

View File

@ -0,0 +1,88 @@
import {isPresent} from 'facade/src/lang';
import {List, ListWrapper, Map, MapWrapper} from 'facade/src/collection';
import {RECORD_TYPE_SELF, ProtoRecord} from './proto_change_detector';
/**
* Removes "duplicate" records. It assuming that record evaluation does not
* have side-effects.
*
* Records that are not last in bindings are removed and all the indices
* of the records that depend on them are updated.
*
* Records that are last in bindings CANNOT be removed, and instead are
* replaced with very cheap SELF records.
*/
export function coalesce(records:List<ProtoRecord>):List<ProtoRecord> {
var res = ListWrapper.create();
var indexMap = MapWrapper.create();
for (var i = 0; i < records.length; ++i) {
var r = records[i];
var record = _replaceIndices(r, res.length + 1, indexMap);
var matchingRecord = _findMatching(record, res);
if (isPresent(matchingRecord) && record.lastInBinding) {
ListWrapper.push(res, _selfRecord(record, matchingRecord.selfIndex, res.length + 1));
MapWrapper.set(indexMap, r.selfIndex, matchingRecord.selfIndex);
} else if (isPresent(matchingRecord) && !record.lastInBinding) {
MapWrapper.set(indexMap, r.selfIndex, matchingRecord.selfIndex);
} else {
ListWrapper.push(res, record);
MapWrapper.set(indexMap, r.selfIndex, record.selfIndex);
}
}
return res;
}
function _selfRecord(r:ProtoRecord, contextIndex:number, selfIndex:number):ProtoRecord {
return new ProtoRecord(
RECORD_TYPE_SELF,
"self",
null,
[],
r.fixedArgs,
contextIndex,
selfIndex,
r.bindingMemento,
r.groupMemento,
r.expressionAsString,
r.lastInBinding,
r.lastInGroup
);
}
function _findMatching(r:ProtoRecord, rs:List<ProtoRecord>){
return ListWrapper.find(rs, (rr) =>
rr.mode === r.mode &&
rr.funcOrValue === r.funcOrValue &&
rr.contextIndex === r.contextIndex &&
ListWrapper.equals(rr.args, r.args)
);
}
function _replaceIndices(r:ProtoRecord, selfIndex:number, indexMap:Map) {
var args = ListWrapper.map(r.args, (a) => _map(indexMap, a));
var contextIndex = _map(indexMap, r.contextIndex);
return new ProtoRecord(
r.mode,
r.name,
r.funcOrValue,
args,
r.fixedArgs,
contextIndex,
selfIndex,
r.bindingMemento,
r.groupMemento,
r.expressionAsString,
r.lastInBinding,
r.lastInGroup
);
}
function _map(indexMap:Map, value:number) {
var r = MapWrapper.get(indexMap, value)
return isPresent(r) ? r : value;
}

View File

@ -0,0 +1,215 @@
import {isPresent, isBlank, BaseException, FunctionWrapper} from 'facade/src/lang';
import {List, ListWrapper, MapWrapper, StringMapWrapper} from 'facade/src/collection';
import {ContextWithVariableBindings} from './parser/context_with_variable_bindings';
import {AbstractChangeDetector} from './abstract_change_detector';
import {ChangeDetectionUtil, SimpleChange, uninitialized} from './change_detection_util';
import {ArrayChanges} from './array_changes';
import {KeyValueChanges} from './keyvalue_changes';
import {
ProtoRecord,
RECORD_TYPE_SELF,
RECORD_TYPE_PROPERTY,
RECORD_TYPE_INVOKE_METHOD,
RECORD_TYPE_CONST,
RECORD_TYPE_INVOKE_CLOSURE,
RECORD_TYPE_PRIMITIVE_OP,
RECORD_TYPE_KEYED_ACCESS,
RECORD_TYPE_INVOKE_FORMATTER,
RECORD_TYPE_STRUCTURAL_CHECK,
RECORD_TYPE_INTERPOLATE,
ProtoChangeDetector
} from './proto_change_detector';
import {ChangeDetector, ChangeDispatcher} from './interfaces';
import {ExpressionChangedAfterItHasBeenChecked, ChangeDetectionError} from './exceptions';
export class DynamicChangeDetector extends AbstractChangeDetector {
dispatcher:any;
formatters:Map;
values:List;
changes:List;
protos:List<ProtoRecord>;
constructor(dispatcher:any, formatters:Map, protoRecords:List<ProtoRecord>) {
super();
this.dispatcher = dispatcher;
this.formatters = formatters;
this.values = ListWrapper.createFixedSize(protoRecords.length + 1);
ListWrapper.fill(this.values, uninitialized);
this.changes = ListWrapper.createFixedSize(protoRecords.length + 1);
this.protos = protoRecords;
}
setContext(context:any) {
this.values[0] = context;
}
detectChangesInRecords(throwOnChange:boolean) {
var protos:List<ProtoRecord> = this.protos;
var updatedRecords = null;
var currentGroup = null;
for (var i = 0; i < protos.length; ++i) {
var proto:ProtoRecord = protos[i];
var change = this._check(proto);
if (isPresent(change)) {
currentGroup = proto.groupMemento;
var record = ChangeDetectionUtil.changeRecord(proto.bindingMemento, change);
updatedRecords = ChangeDetectionUtil.addRecord(updatedRecords, record);
}
if (proto.lastInGroup && isPresent(updatedRecords)) {
if (throwOnChange) ChangeDetectionUtil.throwOnChange(proto, updatedRecords[0]);
this.dispatcher.onRecordChange(currentGroup, updatedRecords);
updatedRecords = null;
}
}
}
_check(proto:ProtoRecord) {
try {
if (proto.mode == RECORD_TYPE_STRUCTURAL_CHECK) {
return this._structuralCheck(proto);
} else {
return this._referenceCheck(proto);
}
} catch (e) {
throw new ChangeDetectionError(proto, e);
}
}
_referenceCheck(proto:ProtoRecord) {
if (this._pureFuncAndArgsDidNotChange(proto)) {
this._setChanged(proto, false);
return null;
}
var prevValue = this._readSelf(proto);
var currValue = this._calculateCurrValue(proto);
if (!isSame(prevValue, currValue)) {
this._writeSelf(proto, currValue);
this._setChanged(proto, true);
if (proto.lastInBinding) {
return ChangeDetectionUtil.simpleChange(prevValue, currValue);
} else {
return null;
}
} else {
this._setChanged(proto, false);
return null;
}
}
_calculateCurrValue(proto:ProtoRecord) {
switch (proto.mode) {
case RECORD_TYPE_SELF:
return this._readContext(proto);
case RECORD_TYPE_CONST:
return proto.funcOrValue;
case RECORD_TYPE_PROPERTY:
var context = this._readContext(proto);
var c = ChangeDetectionUtil.findContext(proto.name, context);
if (c instanceof ContextWithVariableBindings) {
return c.get(proto.name);
} else {
var propertyGetter:Function = proto.funcOrValue;
return propertyGetter(c);
}
break;
case RECORD_TYPE_INVOKE_METHOD:
var methodInvoker:Function = proto.funcOrValue;
return methodInvoker(this._readContext(proto), this._readArgs(proto));
case RECORD_TYPE_KEYED_ACCESS:
var arg = this._readArgs(proto)[0];
return this._readContext(proto)[arg];
case RECORD_TYPE_INVOKE_CLOSURE:
return FunctionWrapper.apply(this._readContext(proto), this._readArgs(proto));
case RECORD_TYPE_INTERPOLATE:
case RECORD_TYPE_PRIMITIVE_OP:
return FunctionWrapper.apply(proto.funcOrValue, this._readArgs(proto));
case RECORD_TYPE_INVOKE_FORMATTER:
var formatter = MapWrapper.get(this.formatters, proto.funcOrValue);
return FunctionWrapper.apply(formatter, this._readArgs(proto));
default:
throw new BaseException(`Unknown operation ${proto.mode}`);
}
}
_structuralCheck(proto:ProtoRecord) {
var self = this._readSelf(proto);
var context = this._readContext(proto);
var change = ChangeDetectionUtil.structuralCheck(self, context);
if (isPresent(change)) {
this._writeSelf(proto, change.currentValue);
}
return change;
}
_readContext(proto:ProtoRecord) {
return this.values[proto.contextIndex];
}
_readSelf(proto:ProtoRecord) {
return this.values[proto.selfIndex];
}
_writeSelf(proto:ProtoRecord, value) {
this.values[proto.selfIndex] = value;
}
_setChanged(proto:ProtoRecord, value:boolean) {
this.changes[proto.selfIndex] = value;
}
_pureFuncAndArgsDidNotChange(proto:ProtoRecord):boolean {
return proto.isPureFunction() && !this._argsChanged(proto);
}
_argsChanged(proto:ProtoRecord):boolean {
var args = proto.args;
for(var i = 0; i < args.length; ++i) {
if (this.changes[args[i]]) {
return true;
}
}
return false;
}
_readArgs(proto:ProtoRecord) {
var res = ListWrapper.createFixedSize(proto.args.length);
var args = proto.args;
for (var i = 0; i < args.length; ++i) {
res[i] = this.values[args[i]];
}
return res;
}
}
var _singleElementList = [null];
function isSame(a, b) {
if (a === b) return true;
if (a instanceof String && b instanceof String && a == b) return true;
if ((a !== a) && (b !== b)) return true;
return false;
}

View File

@ -0,0 +1,30 @@
import {ProtoRecord} from './proto_change_detector';
export class ExpressionChangedAfterItHasBeenChecked extends Error {
message:string;
constructor(proto:ProtoRecord, change:any) {
this.message = `Expression '${proto.expressionAsString}' has changed after it was checked. ` +
`Previous value: '${change.previousValue}'. Current value: '${change.currentValue}'`;
}
toString():string {
return this.message;
}
}
export class ChangeDetectionError extends Error {
message:string;
originalException:any;
location:string;
constructor(proto:ProtoRecord, originalException:any) {
this.originalException = originalException;
this.location = proto.expressionAsString;
this.message = `${this.originalException} in [${this.location}]`;
}
toString():string {
return this.message;
}
}

View File

@ -0,0 +1,63 @@
import {List} from 'facade/src/collection';
export class ChangeRecord {
bindingMemento:any;
change:any;
constructor(bindingMemento, change) {
this.bindingMemento = bindingMemento;
this.change = change;
}
//REMOVE IT
get currentValue() {
return this.change.currentValue;
}
get previousValue() {
return this.change.previousValue;
}
}
/**
* CHECK_ONCE means that after calling detectChanges the mode of the change detector
* will become CHECKED.
*/
export const CHECK_ONCE="CHECK_ONCE";
/**
* CHECKED means that the change detector should be skipped until its mode changes to
* CHECK_ONCE or CHECK_ALWAYS.
*/
export const CHECKED="CHECKED";
/**
* CHECK_ALWAYS means that after calling detectChanges the mode of the change detector
* will remain CHECK_ALWAYS.
*/
export const CHECK_ALWAYS="ALWAYS_CHECK";
/**
* DETACHED means that the change detector sub tree is not a part of the main tree and
* should be skipped.
*/
export const DETACHED="DETACHED";
export class ChangeDispatcher {
onRecordChange(groupMemento, records:List<ChangeRecord>) {}
}
export class ChangeDetector {
parent:ChangeDetector;
mode:string;
addChild(cd:ChangeDetector) {}
removeChild(cd:ChangeDetector) {}
remove() {}
setContext(context:any) {}
markPathToRootAsCheckOnce() {}
detectChanges() {}
checkNoChanges() {}
}

View File

@ -0,0 +1,363 @@
import {ListWrapper, MapWrapper, StringMapWrapper} from 'facade/src/collection';
import {stringify, looseIdentical, isJsObject} from 'facade/src/lang';
export class KeyValueChanges {
_records:Map;
_mapHead:KVChangeRecord;
_previousMapHead:KVChangeRecord;
_changesHead:KVChangeRecord;
_changesTail:KVChangeRecord;
_additionsHead:KVChangeRecord;
_additionsTail:KVChangeRecord;
_removalsHead:KVChangeRecord;
_removalsTail:KVChangeRecord;
constructor() {
this._records = MapWrapper.create();
this._mapHead = null;
this._previousMapHead = null;
this._changesHead = null;
this._changesTail = null;
this._additionsHead = null;
this._additionsTail = null;
this._removalsHead = null;
this._removalsTail = null;
}
static supports(obj):boolean {
return obj instanceof Map || isJsObject(obj);
}
supportsObj(obj):boolean {
return KeyValueChanges.supports(obj);
}
get isDirty():boolean {
return this._additionsHead !== null ||
this._changesHead !== null ||
this._removalsHead !== null;
}
forEachItem(fn:Function) {
var record:KVChangeRecord;
for (record = this._mapHead; record !== null; record = record._next) {
fn(record);
}
}
forEachPreviousItem(fn:Function) {
var record:KVChangeRecord;
for (record = this._previousMapHead; record !== null; record = record._nextPrevious) {
fn(record);
}
}
forEachChangedItem(fn:Function) {
var record:KVChangeRecord;
for (record = this._changesHead; record !== null; record = record._nextChanged) {
fn(record);
}
}
forEachAddedItem(fn:Function){
var record:KVChangeRecord;
for (record = this._additionsHead; record !== null; record = record._nextAdded) {
fn(record);
}
}
forEachRemovedItem(fn:Function){
var record:KVChangeRecord;
for (record = this._removalsHead; record !== null; record = record._nextRemoved) {
fn(record);
}
}
check(map):boolean {
this._reset();
var records = this._records;
var oldSeqRecord:KVChangeRecord = this._mapHead;
var lastOldSeqRecord:KVChangeRecord = null;
var lastNewSeqRecord:KVChangeRecord = null;
var seqChanged:boolean = false;
this._forEach(map, (value, key) => {
var newSeqRecord;
if (oldSeqRecord !== null && key === oldSeqRecord.key) {
newSeqRecord = oldSeqRecord;
if (!looseIdentical(value, oldSeqRecord._currentValue)) {
oldSeqRecord._previousValue = oldSeqRecord._currentValue;
oldSeqRecord._currentValue = value;
this._addToChanges(oldSeqRecord);
}
} else {
seqChanged = true;
if (oldSeqRecord !== null) {
oldSeqRecord._next = null;
this._removeFromSeq(lastOldSeqRecord, oldSeqRecord);
this._addToRemovals(oldSeqRecord);
}
if (MapWrapper.contains(records, key)) {
newSeqRecord = MapWrapper.get(records, key);
} else {
newSeqRecord = new KVChangeRecord(key);
MapWrapper.set(records, key, newSeqRecord);
newSeqRecord._currentValue = value;
this._addToAdditions(newSeqRecord);
}
}
if (seqChanged) {
if (this._isInRemovals(newSeqRecord)) {
this._removeFromRemovals(newSeqRecord);
}
if (lastNewSeqRecord == null) {
this._mapHead = newSeqRecord;
} else {
lastNewSeqRecord._next = newSeqRecord;
}
}
lastOldSeqRecord = oldSeqRecord;
lastNewSeqRecord = newSeqRecord;
oldSeqRecord = oldSeqRecord === null ? null : oldSeqRecord._next;
});
this._truncate(lastOldSeqRecord, oldSeqRecord);
return this.isDirty;
}
_reset() {
if (this.isDirty) {
var record:KVChangeRecord;
// Record the state of the mapping
for (record = this._previousMapHead = this._mapHead;
record !== null;
record = record._next) {
record._nextPrevious = record._next;
}
for (record = this._changesHead; record !== null; record = record._nextChanged) {
record._previousValue = record._currentValue;
}
for (record = this._additionsHead; record != null; record = record._nextAdded) {
record._previousValue = record._currentValue;
}
// todo(vicb) once assert is supported
//assert(() {
// var r = _changesHead;
// while (r != null) {
// var nextRecord = r._nextChanged;
// r._nextChanged = null;
// r = nextRecord;
// }
//
// r = _additionsHead;
// while (r != null) {
// var nextRecord = r._nextAdded;
// r._nextAdded = null;
// r = nextRecord;
// }
//
// r = _removalsHead;
// while (r != null) {
// var nextRecord = r._nextRemoved;
// r._nextRemoved = null;
// r = nextRecord;
// }
//
// return true;
//});
this._changesHead = this._changesTail = null;
this._additionsHead = this._additionsTail = null;
this._removalsHead = this._removalsTail = null;
}
}
_truncate(lastRecord:KVChangeRecord, record:KVChangeRecord) {
while (record !== null) {
if (lastRecord === null) {
this._mapHead = null;
} else {
lastRecord._next = null;
}
var nextRecord = record._next;
// todo(vicb) assert
//assert((() {
// record._next = null;
// return true;
//}));
this._addToRemovals(record);
lastRecord = record;
record = nextRecord;
}
for (var rec:KVChangeRecord = this._removalsHead; rec !== null; rec = rec._nextRemoved) {
rec._previousValue = rec._currentValue;
rec._currentValue = null;
MapWrapper.delete(this._records, rec.key);
}
}
_isInRemovals(record:KVChangeRecord) {
return record === this._removalsHead ||
record._nextRemoved !== null ||
record._prevRemoved !== null;
}
_addToRemovals(record:KVChangeRecord) {
// todo(vicb) assert
//assert(record._next == null);
//assert(record._nextAdded == null);
//assert(record._nextChanged == null);
//assert(record._nextRemoved == null);
//assert(record._prevRemoved == null);
if (this._removalsHead === null) {
this._removalsHead = this._removalsTail = record;
} else {
this._removalsTail._nextRemoved = record;
record._prevRemoved = this._removalsTail;
this._removalsTail = record;
}
}
_removeFromSeq(prev:KVChangeRecord, record:KVChangeRecord) {
var next = record._next;
if (prev === null) {
this._mapHead = next;
} else {
prev._next = next;
}
// todo(vicb) assert
//assert((() {
// record._next = null;
// return true;
//})());
}
_removeFromRemovals(record:KVChangeRecord) {
// todo(vicb) assert
//assert(record._next == null);
//assert(record._nextAdded == null);
//assert(record._nextChanged == null);
var prev = record._prevRemoved;
var next = record._nextRemoved;
if (prev === null) {
this._removalsHead = next;
} else {
prev._nextRemoved = next;
}
if (next === null) {
this._removalsTail = prev;
} else {
next._prevRemoved = prev;
}
record._prevRemoved = record._nextRemoved = null;
}
_addToAdditions(record:KVChangeRecord) {
// todo(vicb): assert
//assert(record._next == null);
//assert(record._nextAdded == null);
//assert(record._nextChanged == null);
//assert(record._nextRemoved == null);
//assert(record._prevRemoved == null);
if (this._additionsHead === null) {
this._additionsHead = this._additionsTail = record;
} else {
this._additionsTail._nextAdded = record;
this._additionsTail = record;
}
}
_addToChanges(record:KVChangeRecord) {
// todo(vicb) assert
//assert(record._nextAdded == null);
//assert(record._nextChanged == null);
//assert(record._nextRemoved == null);
//assert(record._prevRemoved == null);
if (this._changesHead === null) {
this._changesHead = this._changesTail = record;
} else {
this._changesTail._nextChanged = record;
this._changesTail = record;
}
}
toString():string {
var items = [];
var previous = [];
var changes = [];
var additions = [];
var removals = [];
var record:KVChangeRecord;
for (record = this._mapHead; record !== null; record = record._next) {
ListWrapper.push(items, stringify(record));
}
for (record = this._previousMapHead; record !== null; record = record._nextPrevious) {
ListWrapper.push(previous, stringify(record));
}
for (record = this._changesHead; record !== null; record = record._nextChanged) {
ListWrapper.push(changes, stringify(record));
}
for (record = this._additionsHead; record !== null; record = record._nextAdded) {
ListWrapper.push(additions, stringify(record));
}
for (record = this._removalsHead; record !== null; record = record._nextRemoved) {
ListWrapper.push(removals, stringify(record));
}
return "map: " + items.join(', ') + "\n" +
"previous: " + previous.join(', ') + "\n" +
"additions: " + additions.join(', ') + "\n" +
"changes: " + changes.join(', ') + "\n" +
"removals: " + removals.join(', ') + "\n";
}
_forEach(obj, fn:Function) {
if (obj instanceof Map) {
MapWrapper.forEach(obj, fn);
} else {
StringMapWrapper.forEach(obj, fn);
}
}
}
export class KVChangeRecord {
key;
_previousValue;
_currentValue;
_nextPrevious:KVChangeRecord;
_next:KVChangeRecord;
_nextAdded:KVChangeRecord;
_nextRemoved:KVChangeRecord;
_prevRemoved:KVChangeRecord;
_nextChanged:KVChangeRecord;
constructor(key) {
this.key = key;
this._previousValue = null;
this._currentValue = null;
this._nextPrevious = null;
this._next = null;
this._nextAdded = null;
this._nextRemoved = null;
this._prevRemoved = null;
this._nextChanged = null;
}
toString():string {
return looseIdentical(this._previousValue, this._currentValue) ?
stringify(this.key) :
(stringify(this.key) + '[' + stringify(this._previousValue) + '->' +
stringify(this._currentValue) + ']');
}
}

View File

@ -0,0 +1,467 @@
import {FIELD, autoConvertAdd, isBlank, isPresent, FunctionWrapper, BaseException} from "facade/src/lang";
import {List, Map, ListWrapper, StringMapWrapper} from "facade/src/collection";
import {ContextWithVariableBindings} from "./context_with_variable_bindings";
export class AST {
eval(context) {
throw new BaseException("Not supported");
}
get isAssignable() {
return false;
}
assign(context, value) {
throw new BaseException("Not supported");
}
visit(visitor) {
}
toString():string {
return "AST";
}
}
export class EmptyExpr extends AST {
eval(context) {
return null;
}
visit(visitor) {
//do nothing
}
}
export class Structural extends AST {
value:AST;
constructor(value:AST) {
this.value = value;
}
eval(context) {
return value.eval(context);
}
visit(visitor) {
return visitor.visitStructural(this);
}
}
export class ImplicitReceiver extends AST {
eval(context) {
return context;
}
visit(visitor) {
return visitor.visitImplicitReceiver(this);
}
}
/**
* Multiple expressions separated by a semicolon.
*/
export class Chain extends AST {
expressions:List;
constructor(expressions:List) {
this.expressions = expressions;
}
eval(context) {
var result;
for (var i = 0; i < this.expressions.length; i++) {
var last = this.expressions[i].eval(context);
if (isPresent(last)) result = last;
}
return result;
}
visit(visitor) {
return visitor.visitChain(this);
}
}
export class Conditional extends AST {
condition:AST;
trueExp:AST;
falseExp:AST;
constructor(condition:AST, trueExp:AST, falseExp:AST){
this.condition = condition;
this.trueExp = trueExp;
this.falseExp = falseExp;
}
eval(context) {
if(this.condition.eval(context)) {
return this.trueExp.eval(context);
} else {
return this.falseExp.eval(context);
}
}
visit(visitor) {
return visitor.visitConditional(this);
}
}
export class AccessMember extends AST {
receiver:AST;
name:string;
getter:Function;
setter:Function;
constructor(receiver:AST, name:string, getter:Function, setter:Function) {
this.receiver = receiver;
this.name = name;
this.getter = getter;
this.setter = setter;
}
eval(context) {
var evaluatedContext = this.receiver.eval(context);
while (evaluatedContext instanceof ContextWithVariableBindings) {
if (evaluatedContext.hasBinding(this.name)) {
return evaluatedContext.get(this.name);
}
evaluatedContext = evaluatedContext.parent;
}
return this.getter(evaluatedContext);
}
get isAssignable() {
return true;
}
assign(context, value) {
var evaluatedContext = this.receiver.eval(context);
while (evaluatedContext instanceof ContextWithVariableBindings) {
if (evaluatedContext.hasBinding(this.name)) {
throw new BaseException(`Cannot reassign a variable binding ${this.name}`)
}
evaluatedContext = evaluatedContext.parent;
}
return this.setter(evaluatedContext, value);
}
visit(visitor) {
return visitor.visitAccessMember(this);
}
}
export class KeyedAccess extends AST {
obj:AST;
key:AST;
constructor(obj:AST, key:AST) {
this.obj = obj;
this.key = key;
}
eval(context) {
var obj = this.obj.eval(context);
var key = this.key.eval(context);
return obj[key];
}
get isAssignable() {
return true;
}
assign(context, value) {
var obj = this.obj.eval(context);
var key = this.key.eval(context);
obj[key] = value;
return value;
}
visit(visitor) {
return visitor.visitKeyedAccess(this);
}
}
export class Formatter extends AST {
exp:AST;
name:string;
args:List<AST>;
allArgs:List<AST>;
constructor(exp:AST, name:string, args:List) {
this.exp = exp;
this.name = name;
this.args = args;
this.allArgs = ListWrapper.concat([exp], args);
}
visit(visitor) {
return visitor.visitFormatter(this);
}
}
export class LiteralPrimitive extends AST {
value;
constructor(value) {
this.value = value;
}
eval(context) {
return this.value;
}
visit(visitor) {
return visitor.visitLiteralPrimitive(this);
}
}
export class LiteralArray extends AST {
expressions:List;
constructor(expressions:List) {
this.expressions = expressions;
}
eval(context) {
return ListWrapper.map(this.expressions, (e) => e.eval(context));
}
visit(visitor) {
return visitor.visitLiteralArray(this);
}
}
export class LiteralMap extends AST {
keys:List;
values:List;
constructor(keys:List, values:List) {
this.keys = keys;
this.values = values;
}
eval(context) {
var res = StringMapWrapper.create();
for(var i = 0; i < this.keys.length; ++i) {
StringMapWrapper.set(res, this.keys[i], this.values[i].eval(context));
}
return res;
}
visit(visitor) {
return visitor.visitLiteralMap(this);
}
}
export class Interpolation extends AST {
strings:List;
expressions:List;
constructor(strings:List, expressions:List) {
this.strings = strings;
this.expressions = expressions;
}
eval(context) {
throw new BaseException("evaluating an Interpolation is not supported");
}
visit(visitor) {
visitor.visitInterpolation(this);
}
}
export class Binary extends AST {
operation:string;
left:AST;
right:AST;
constructor(operation:string, left:AST, right:AST) {
this.operation = operation;
this.left = left;
this.right = right;
}
eval(context) {
var left = this.left.eval(context);
switch (this.operation) {
case '&&': return left && this.right.eval(context);
case '||': return left || this.right.eval(context);
}
var right = this.right.eval(context);
switch (this.operation) {
case '+' : return left + right;
case '-' : return left - right;
case '*' : return left * right;
case '/' : return left / right;
case '%' : return left % right;
case '==' : return left == right;
case '!=' : return left != right;
case '<' : return left < right;
case '>' : return left > right;
case '<=' : return left <= right;
case '>=' : return left >= right;
case '^' : return left ^ right;
case '&' : return left & right;
}
throw 'Internal error [$operation] not handled';
}
visit(visitor) {
return visitor.visitBinary(this);
}
}
export class PrefixNot extends AST {
expression:AST;
constructor(expression:AST) {
this.expression = expression;
}
eval(context) {
return !this.expression.eval(context);
}
visit(visitor) {
return visitor.visitPrefixNot(this);
}
}
export class Assignment extends AST {
target:AST;
value:AST;
constructor(target:AST, value:AST) {
this.target = target;
this.value = value;
}
eval(context) {
return this.target.assign(context, this.value.eval(context));
}
visit(visitor) {
return visitor.visitAssignment(this);
}
}
export class MethodCall extends AST {
receiver:AST;
fn:Function;
args:List;
name:string;
constructor(receiver:AST, name:string, fn:Function, args:List) {
this.receiver = receiver;
this.fn = fn;
this.args = args;
this.name = name;
}
eval(context) {
var evaluatedContext = this.receiver.eval(context);
var evaluatedArgs = evalList(context, this.args);
while (evaluatedContext instanceof ContextWithVariableBindings) {
if (evaluatedContext.hasBinding(this.name)) {
var fn = evaluatedContext.get(this.name);
return FunctionWrapper.apply(fn, evaluatedArgs);
}
evaluatedContext = evaluatedContext.parent;
}
return this.fn(evaluatedContext, evaluatedArgs);
}
visit(visitor) {
return visitor.visitMethodCall(this);
}
}
export class FunctionCall extends AST {
target:AST;
args:List;
constructor(target:AST, args:List) {
this.target = target;
this.args = args;
}
eval(context) {
var obj = this.target.eval(context);
if (! (obj instanceof Function)) {
throw new BaseException(`${obj} is not a function`);
}
return FunctionWrapper.apply(obj, evalList(context, this.args));
}
visit(visitor) {
return visitor.visitFunctionCall(this);
}
}
export class ASTWithSource extends AST {
ast:AST;
source:string;
location:string;
constructor(ast:AST, source:string, location:string) {
this.source = source;
this.location = location;
this.ast = ast;
}
eval(context) {
return this.ast.eval(context);
}
get isAssignable() {
return this.ast.isAssignable;
}
assign(context, value) {
return this.ast.assign(context, value);
}
visit(visitor) {
return this.ast.visit(visitor);
}
toString():string {
return `${this.source} in ${this.location}`;
}
}
export class TemplateBinding {
key:string;
keyIsVar:boolean;
name:string;
expression:ASTWithSource;
constructor(key:string, keyIsVar:boolean, name:string, expression:ASTWithSource) {
this.key = key;
this.keyIsVar = keyIsVar;
// only either name or expression will be filled.
this.name = name;
this.expression = expression;
}
}
//INTERFACE
export class AstVisitor {
visitAccessMember(ast:AccessMember) {}
visitAssignment(ast:Assignment) {}
visitBinary(ast:Binary) {}
visitChain(ast:Chain){}
visitStructural(ast:Structural) {}
visitConditional(ast:Conditional) {}
visitFormatter(ast:Formatter) {}
visitFunctionCall(ast:FunctionCall) {}
visitImplicitReceiver(ast:ImplicitReceiver) {}
visitKeyedAccess(ast:KeyedAccess) {}
visitLiteralArray(ast:LiteralArray) {}
visitLiteralMap(ast:LiteralMap) {}
visitLiteralPrimitive(ast:LiteralPrimitive) {}
visitMethodCall(ast:MethodCall) {}
visitPrefixNot(ast:PrefixNot) {}
}
var _evalListCache = [[],[0],[0,0],[0,0,0],[0,0,0,0],[0,0,0,0,0]];
function evalList(context, exps:List){
var length = exps.length;
var result = _evalListCache[length];
for (var i = 0; i < length; i++) {
result[i] = exps[i].eval(context);
}
return result;
}

View File

@ -0,0 +1,38 @@
import {MapWrapper} from 'facade/src/collection';
import {BaseException} from 'facade/src/lang';
export class ContextWithVariableBindings {
parent:any;
/// varBindings' keys are read-only. adding/removing keys is not supported.
varBindings:Map;
constructor(parent:any, varBindings:Map) {
this.parent = parent;
this.varBindings = varBindings;
}
hasBinding(name:string):boolean {
return MapWrapper.contains(this.varBindings, name);
}
get(name:string) {
return MapWrapper.get(this.varBindings, name);
}
set(name:string, value) {
// TODO(rado): consider removing this check if we can guarantee this is not
// exposed to the public API.
if (this.hasBinding(name)) {
MapWrapper.set(this.varBindings, name, value);
} else {
throw new BaseException(
'VariableBindings do not support setting of new keys post-construction.');
}
}
clearValues() {
for (var k of MapWrapper.keys(this.varBindings)) {
MapWrapper.set(this.varBindings, k, null);
}
}
}

View File

@ -0,0 +1,481 @@
import {List, ListWrapper, SetWrapper} from "facade/src/collection";
import {int, FIELD, NumberWrapper, StringJoiner, StringWrapper} from "facade/src/lang";
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 {
text:string;
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 {
index:int;
type:int;
_numValue:number;
_strValue:string;
constructor(index:int, type:int, numValue:number, strValue:string) {
/**
* NOTE: To ensure that this constructor creates the same hidden class each time, ensure that
* all the fields are assigned to in the exact same order in each run of this constructor.
*/
this.index = index;
this.type = type;
this._numValue = numValue;
this._strValue = strValue;
}
isCharacter(code:int):boolean {
return (this.type == TOKEN_TYPE_CHARACTER && this._numValue == code);
}
isNumber():boolean {
return (this.type == TOKEN_TYPE_NUMBER);
}
isString():boolean {
return (this.type == TOKEN_TYPE_STRING);
}
isOperator(operater:string):boolean {
return (this.type == TOKEN_TYPE_OPERATOR && this._strValue == operater);
}
isIdentifier():boolean {
return (this.type == TOKEN_TYPE_IDENTIFIER);
}
isKeyword():boolean {
return (this.type == TOKEN_TYPE_KEYWORD);
}
isKeywordVar():boolean {
return (this.type == TOKEN_TYPE_KEYWORD && this._strValue == "var");
}
isKeywordNull():boolean {
return (this.type == TOKEN_TYPE_KEYWORD && this._strValue == "null");
}
isKeywordUndefined():boolean {
return (this.type == TOKEN_TYPE_KEYWORD && this._strValue == "undefined");
}
isKeywordTrue():boolean {
return (this.type == TOKEN_TYPE_KEYWORD && this._strValue == "true");
}
isKeywordFalse():boolean {
return (this.type == TOKEN_TYPE_KEYWORD && this._strValue == "false");
}
toNumber():number {
// -1 instead of NULL ok?
return (this.type == TOKEN_TYPE_NUMBER) ? this._numValue : -1;
}
toString():string {
var type:int = this.type;
if (type >= TOKEN_TYPE_CHARACTER && type <= TOKEN_TYPE_STRING) {
return this._strValue;
} else if (type == TOKEN_TYPE_NUMBER) {
return this._numValue.toString();
} else {
return null;
}
}
}
function newCharacterToken(index:int, code:int):Token {
return new Token(index, TOKEN_TYPE_CHARACTER, code, StringWrapper.fromCharCode(code));
}
function newIdentifierToken(index:int, text:string):Token {
return new Token(index, TOKEN_TYPE_IDENTIFIER, 0, text);
}
function newKeywordToken(index:int, text:string):Token {
return new Token(index, TOKEN_TYPE_KEYWORD, 0, text);
}
function newOperatorToken(index:int, text:string):Token {
return new Token(index, TOKEN_TYPE_OPERATOR, 0, text);
}
function newStringToken(index:int, text:string):Token {
return new Token(index, TOKEN_TYPE_STRING, 0, text);
}
function newNumberToken(index:int, n:number):Token {
return new Token(index, TOKEN_TYPE_NUMBER, n, "");
}
export var EOF:Token = new Token(-1, 0, 0, "");
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 $HASH = 35;
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;
const $A = 65, $B = 66, $C = 67, $D = 68, $E = 69, $F = 70, $G = 71, $H = 72,
$I = 73, $J = 74, $K = 75, $L = 76, $M = 77, $N = 78, $O = 79, $P = 80,
$Q = 81, $R = 82, $S = 83, $T = 84, $U = 85, $V = 86, $W = 87, $X = 88,
$Y = 89, $Z = 90;
export const $LBRACKET = 91;
export const $BACKSLASH = 92;
export const $RBRACKET = 93;
const $CARET = 94;
const $_ = 95;
const $a = 97, $b = 98, $c = 99, $d = 100, $e = 101, $f = 102, $g = 103,
$h = 104, $i = 105, $j = 106, $k = 107, $l = 108, $m = 109, $n = 110,
$o = 111, $p = 112, $q = 113, $r = 114, $s = 115, $t = 116, $u = 117,
$v = 118, $w = 119, $x = 120, $y = 121, $z = 122;
export const $LBRACE = 123;
export const $BAR = 124;
export const $RBRACE = 125;
const $TILDE = 126;
const $NBSP = 160;
export class ScannerError extends Error {
message:string;
constructor(message) {
this.message = message;
}
toString() {
return this.message;
}
}
class _Scanner {
input:string;
length:int;
peek:int;
index:int;
constructor(input:string) {
this.input = input;
this.length = input.length;
this.peek = 0;
this.index = -1;
this.advance();
}
advance() {
this.peek = ++this.index >= this.length ? $EOF : StringWrapper.charCodeAt(this.input, this.index);
}
scanToken():Token {
var input = this.input,
length = this.length,
peek = this.peek,
index = this.index;
// Skip whitespace.
while (peek <= $SPACE) {
if (++index >= length) {
peek = $EOF;
break;
} else {
peek = StringWrapper.charCodeAt(input, index);
}
}
this.peek = peek;
this.index = index;
if (index >= length) {
return null;
}
// Handle identifiers and numbers.
if (isIdentifierStart(peek)) return this.scanIdentifier();
if (isDigit(peek)) return this.scanNumber(index);
var start:int = index;
switch (peek) {
case $PERIOD:
this.advance();
return isDigit(this.peek) ? this.scanNumber(start) :
newCharacterToken(start, $PERIOD);
case $LPAREN: case $RPAREN:
case $LBRACE: case $RBRACE:
case $LBRACKET: case $RBRACKET:
case $COMMA:
case $COLON:
case $SEMICOLON:
return this.scanCharacter(start, peek);
case $SQ:
case $DQ:
return this.scanString();
case $HASH:
return this.scanOperator(start, StringWrapper.fromCharCode(peek));
case $PLUS:
case $MINUS:
case $STAR:
case $SLASH:
case $PERCENT:
case $CARET:
case $QUESTION:
return this.scanOperator(start, StringWrapper.fromCharCode(peek));
case $LT:
case $GT:
case $BANG:
case $EQ:
return this.scanComplexOperator(start, $EQ, StringWrapper.fromCharCode(peek), '=');
case $AMPERSAND:
return this.scanComplexOperator(start, $AMPERSAND, '&', '&');
case $BAR:
return this.scanComplexOperator(start, $BAR, '|', '|');
case $TILDE:
return this.scanComplexOperator(start, $SLASH, '~', '/');
case $NBSP:
while (isWhitespace(this.peek)) this.advance();
return this.scanToken();
}
this.error(`Unexpected character [${StringWrapper.fromCharCode(peek)}]`, 0);
return null;
}
scanCharacter(start:int, code:int):Token {
assert(this.peek == code);
this.advance();
return newCharacterToken(start, code);
}
scanOperator(start:int, str:string):Token {
assert(this.peek == StringWrapper.charCodeAt(str, 0));
assert(SetWrapper.has(OPERATORS, str));
this.advance();
return newOperatorToken(start, str);
}
scanComplexOperator(start:int, code:int, one:string, two:string):Token {
assert(this.peek == StringWrapper.charCodeAt(one, 0));
this.advance();
var str:string = one;
if (this.peek == code) {
this.advance();
str += two;
}
assert(SetWrapper.has(OPERATORS, str));
return newOperatorToken(start, str);
}
scanIdentifier():Token {
assert(isIdentifierStart(this.peek));
var start:int = this.index;
this.advance();
while (isIdentifierPart(this.peek)) this.advance();
var str:string = this.input.substring(start, this.index);
if (SetWrapper.has(KEYWORDS, str)) {
return newKeywordToken(start, str);
} else {
return newIdentifierToken(start, str);
}
}
scanNumber(start:int):Token {
assert(isDigit(this.peek));
var simple:boolean = (this.index === start);
this.advance(); // Skip initial digit.
while (true) {
if (isDigit(this.peek)) {
// Do nothing.
} else if (this.peek == $PERIOD) {
simple = false;
} else if (isExponentStart(this.peek)) {
this.advance();
if (isExponentSign(this.peek)) this.advance();
if (!isDigit(this.peek)) this.error('Invalid exponent', -1);
simple = false;
} else {
break;
}
this.advance();
}
var str:string = this.input.substring(start, this.index);
// TODO
var value:number = simple ? NumberWrapper.parseIntAutoRadix(str) : NumberWrapper.parseFloat(str);
return newNumberToken(start, value);
}
scanString():Token {
assert(this.peek == $SQ || this.peek == $DQ);
var start:int = this.index;
var quote:int = this.peek;
this.advance(); // Skip initial quote.
var buffer:StringJoiner;
var marker:int = this.index;
var input:string = this.input;
while (this.peek != quote) {
if (this.peek == $BACKSLASH) {
if (buffer == null) buffer = new StringJoiner();
buffer.add(input.substring(marker, this.index));
this.advance();
var unescapedCode:int;
if (this.peek == $u) {
// 4 character hex code for unicode character.
var hex:string = input.substring(this.index + 1, this.index + 5);
try {
unescapedCode = NumberWrapper.parseInt(hex, 16);
} catch (e) {
this.error(`Invalid unicode escape [\\u${hex}]`, 0);
}
for (var i:int = 0; i < 5; i++) {
this.advance();
}
} else {
unescapedCode = unescape(this.peek);
this.advance();
}
buffer.add(StringWrapper.fromCharCode(unescapedCode));
marker = this.index;
} else if (this.peek == $EOF) {
this.error('Unterminated quote', 0);
} else {
this.advance();
}
}
var last:string = input.substring(marker, this.index);
this.advance(); // Skip terminating quote.
// Compute the unescaped string value.
var unescaped:string = last;
if (buffer != null) {
buffer.add(last);
unescaped = buffer.toString();
}
return newStringToken(start, unescaped);
}
error(message:string, offset:int) {
var position:int = this.index + offset;
throw new ScannerError(`Lexer Error: ${message} at column ${position} in expression [${this.input}]`);
}
}
function isWhitespace(code:int):boolean {
return (code >= $TAB && code <= $SPACE) || (code == $NBSP);
}
function isIdentifierStart(code:int):boolean {
return ($a <= code && code <= $z) ||
($A <= code && code <= $Z) ||
(code == $_) ||
(code == $$);
}
function isIdentifierPart(code:int):boolean {
return ($a <= code && code <= $z) ||
($A <= code && code <= $Z) ||
($0 <= code && code <= $9) ||
(code == $_) ||
(code == $$);
}
function isDigit(code:int):boolean {
return $0 <= code && code <= $9;
}
function isExponentStart(code:int):boolean {
return code == $e || code == $E;
}
function isExponentSign(code:int):boolean {
return code == $MINUS || code == $PLUS;
}
function unescape(code:int):int {
switch(code) {
case $n: return $LF;
case $f: return $FF;
case $r: return $CR;
case $t: return $TAB;
case $v: return $VTAB;
default: return code;
}
}
var OPERATORS = SetWrapper.createFromList([
'+',
'-',
'*',
'/',
'~/',
'%',
'^',
'=',
'==',
'!=',
'<',
'>',
'<=',
'>=',
'&&',
'||',
'&',
'|',
'!',
'?',
'#'
]);
var KEYWORDS = SetWrapper.createFromList([
'var',
'null',
'undefined',
'true',
'false',
]);

View File

@ -0,0 +1,521 @@
import {FIELD, int, isBlank, isPresent, BaseException, StringWrapper, RegExpWrapper} from 'facade/src/lang';
import {ListWrapper, List} from 'facade/src/collection';
import {Lexer, EOF, Token, $PERIOD, $COLON, $SEMICOLON, $LBRACKET, $RBRACKET,
$COMMA, $LBRACE, $RBRACE, $LPAREN, $RPAREN} from './lexer';
import {reflector, Reflector} from 'reflection/src/reflection';
import {
AST,
EmptyExpr,
ImplicitReceiver,
AccessMember,
LiteralPrimitive,
Expression,
Binary,
PrefixNot,
Conditional,
Formatter,
Assignment,
Chain,
KeyedAccess,
LiteralArray,
LiteralMap,
Interpolation,
MethodCall,
FunctionCall,
TemplateBindings,
TemplateBinding,
ASTWithSource
} from './ast';
var _implicitReceiver = new ImplicitReceiver();
// TODO(tbosch): Cannot make this const/final right now because of the transpiler...
var INTERPOLATION_REGEXP = RegExpWrapper.create('\\{\\{(.*?)\\}\\}');
var QUOTE_REGEXP = RegExpWrapper.create("'");
export class Parser {
_lexer:Lexer;
_reflector:Reflector;
constructor(lexer:Lexer, providedReflector:Reflector = null){
this._lexer = lexer;
this._reflector = isPresent(providedReflector) ? providedReflector : reflector;
}
parseAction(input:string, location:any):ASTWithSource {
var tokens = this._lexer.tokenize(input);
var ast = new _ParseAST(input, location, tokens, this._reflector, true).parseChain();
return new ASTWithSource(ast, input, location);
}
parseBinding(input:string, location:any):ASTWithSource {
var tokens = this._lexer.tokenize(input);
var ast = new _ParseAST(input, location, tokens, this._reflector, false).parseChain();
return new ASTWithSource(ast, input, location);
}
parseTemplateBindings(input:string, location:any):List<TemplateBinding> {
var tokens = this._lexer.tokenize(input);
return new _ParseAST(input, location, tokens, this._reflector, false).parseTemplateBindings();
}
parseInterpolation(input:string, location:any):ASTWithSource {
var parts = StringWrapper.split(input, INTERPOLATION_REGEXP);
if (parts.length <= 1) {
return null;
}
var strings = [];
var expressions = [];
for (var i=0; i<parts.length; i++) {
var part = parts[i];
if (i%2 === 0) {
// fixed string
ListWrapper.push(strings, part);
} else {
var tokens = this._lexer.tokenize(part);
var ast = new _ParseAST(input, location, tokens, this._reflector, false).parseChain();
ListWrapper.push(expressions, ast);
}
}
return new ASTWithSource(new Interpolation(strings, expressions), input, location);
}
}
class _ParseAST {
input:string;
location:any;
tokens:List<Token>;
reflector:Reflector;
parseAction:boolean;
index:int;
constructor(input:string, location:any, tokens:List, reflector:Reflector, parseAction:boolean) {
this.input = input;
this.location = location;
this.tokens = tokens;
this.index = 0;
this.reflector = reflector;
this.parseAction = parseAction;
}
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);
}
get inputIndex():int {
return (this.index < this.tokens.length) ? this.next.index : this.input.length;
}
advance() {
this.index ++;
}
optionalCharacter(code:int):boolean {
if (this.next.isCharacter(code)) {
this.advance();
return true;
} else {
return false;
}
}
optionalKeywordVar():boolean {
if (this.peekKeywordVar()) {
this.advance();
return true;
} else {
return false;
}
}
peekKeywordVar():boolean {
return this.next.isKeywordVar() || this.next.isOperator('#');
}
expectCharacter(code:int) {
if (this.optionalCharacter(code)) return;
this.error(`Missing expected ${StringWrapper.fromCharCode(code)}`);
}
optionalOperator(op:string):boolean {
if (this.next.isOperator(op)) {
this.advance();
return true;
} else {
return false;
}
}
expectOperator(operator:string) {
if (this.optionalOperator(operator)) return;
this.error(`Missing expected operator ${operator}`);
}
expectIdentifierOrKeyword():string {
var n = this.next;
if (!n.isIdentifier() && !n.isKeyword()) {
this.error(`Unexpected token ${n}, expected identifier or keyword`)
}
this.advance();
return n.toString();
}
expectIdentifierOrKeywordOrString():string {
var n = this.next;
if (!n.isIdentifier() && !n.isKeyword() && !n.isString()) {
this.error(`Unexpected token ${n}, expected identifier, keyword, or string`)
}
this.advance();
return n.toString();
}
parseChain():AST {
var exprs = [];
while (this.index < this.tokens.length) {
var expr = this.parseFormatter();
ListWrapper.push(exprs, expr);
if (this.optionalCharacter($SEMICOLON)) {
if (! this.parseAction) {
this.error("Binding expression cannot contain chained expression");
}
while (this.optionalCharacter($SEMICOLON)){} //read all semicolons
} else if (this.index < this.tokens.length) {
this.error(`Unexpected token '${this.next}'`);
}
}
if (exprs.length == 0) return new EmptyExpr();
if (exprs.length == 1) return exprs[0];
return new Chain(exprs);
}
parseFormatter() {
var result = this.parseExpression();
while (this.optionalOperator("|")) {
if (this.parseAction) {
this.error("Cannot have a formatter in an action expression");
}
var name = this.expectIdentifierOrKeyword();
var args = ListWrapper.create();
while (this.optionalCharacter($COLON)) {
ListWrapper.push(args, this.parseExpression());
}
result = new Formatter(result, name, args);
}
return result;
}
parseExpression() {
var start = this.inputIndex;
var result = this.parseConditional();
while (this.next.isOperator('=')) {
if (!result.isAssignable) {
var end = this.inputIndex;
var expression = this.input.substring(start, end);
this.error(`Expression ${expression} is not assignable`);
}
if (!this.parseAction) {
this.error("Binding expression cannot contain assignments");
}
this.expectOperator('=');
result = new Assignment(result, this.parseConditional());
}
return result;
}
parseConditional() {
var start = this.inputIndex;
var result = this.parseLogicalOr();
if (this.optionalOperator('?')) {
var yes = this.parseExpression();
if (!this.optionalCharacter($COLON)) {
var end = this.inputIndex;
var expression = this.input.substring(start, end);
this.error(`Conditional expression ${expression} requires all 3 expressions`);
}
var no = this.parseExpression();
return new Conditional(result, yes, no);
} else {
return result;
}
}
parseLogicalOr() {
// '||'
var result = this.parseLogicalAnd();
while (this.optionalOperator('||')) {
result = new Binary('||', result, this.parseLogicalAnd());
}
return result;
}
parseLogicalAnd() {
// '&&'
var result = this.parseEquality();
while (this.optionalOperator('&&')) {
result = new Binary('&&', result, this.parseEquality());
}
return result;
}
parseEquality() {
// '==','!='
var result = this.parseRelational();
while (true) {
if (this.optionalOperator('==')) {
result = new Binary('==', result, this.parseRelational());
} else if (this.optionalOperator('!=')) {
result = new Binary('!=', result, this.parseRelational());
} else {
return result;
}
}
}
parseRelational() {
// '<', '>', '<=', '>='
var result = this.parseAdditive();
while (true) {
if (this.optionalOperator('<')) {
result = new Binary('<', result, this.parseAdditive());
} else if (this.optionalOperator('>')) {
result = new Binary('>', result, this.parseAdditive());
} else if (this.optionalOperator('<=')) {
result = new Binary('<=', result, this.parseAdditive());
} else if (this.optionalOperator('>=')) {
result = new Binary('>=', result, this.parseAdditive());
} else {
return result;
}
}
}
parseAdditive() {
// '+', '-'
var result = this.parseMultiplicative();
while (true) {
if (this.optionalOperator('+')) {
result = new Binary('+', result, this.parseMultiplicative());
} else if (this.optionalOperator('-')) {
result = new Binary('-', result, this.parseMultiplicative());
} else {
return result;
}
}
}
parseMultiplicative() {
// '*', '%', '/'
var result = this.parsePrefix();
while (true) {
if (this.optionalOperator('*')) {
result = new Binary('*', result, this.parsePrefix());
} else if (this.optionalOperator('%')) {
result = new Binary('%', result, this.parsePrefix());
} else if (this.optionalOperator('/')) {
result = new Binary('/', result, this.parsePrefix());
} else {
return result;
}
}
}
parsePrefix() {
if (this.optionalOperator('+')) {
return this.parsePrefix();
} else if (this.optionalOperator('-')) {
return new Binary('-', new LiteralPrimitive(0), this.parsePrefix());
} else if (this.optionalOperator('!')) {
return new PrefixNot(this.parsePrefix());
} else {
return this.parseCallChain();
}
}
parseCallChain():AST {
var result = this.parsePrimary();
while (true) {
if (this.optionalCharacter($PERIOD)) {
result = this.parseAccessMemberOrMethodCall(result);
} else if (this.optionalCharacter($LBRACKET)) {
var key = this.parseExpression();
this.expectCharacter($RBRACKET);
result = new KeyedAccess(result, key);
} else if (this.optionalCharacter($LPAREN)) {
var args = this.parseCallArguments();
this.expectCharacter($RPAREN);
result = new FunctionCall(result, args);
} else {
return result;
}
}
}
parsePrimary() {
if (this.optionalCharacter($LPAREN)) {
var result = this.parseFormatter();
this.expectCharacter($RPAREN);
return result;
} else if (this.next.isKeywordNull() || this.next.isKeywordUndefined()) {
this.advance();
return new LiteralPrimitive(null);
} else if (this.next.isKeywordTrue()) {
this.advance();
return new LiteralPrimitive(true);
} else if (this.next.isKeywordFalse()) {
this.advance();
return new LiteralPrimitive(false);
} else if (this.optionalCharacter($LBRACKET)) {
var elements = this.parseExpressionList($RBRACKET);
this.expectCharacter($RBRACKET);
return new LiteralArray(elements);
} else if (this.next.isCharacter($LBRACE)) {
return this.parseLiteralMap();
} else if (this.next.isIdentifier()) {
return this.parseAccessMemberOrMethodCall(_implicitReceiver);
} else if (this.next.isNumber()) {
var value = this.next.toNumber();
this.advance();
return new LiteralPrimitive(value);
} else if (this.next.isString()) {
var value = this.next.toString();
this.advance();
return new LiteralPrimitive(value);
} else if (this.index >= this.tokens.length) {
this.error(`Unexpected end of expression: ${this.input}`);
} else {
this.error(`Unexpected token ${this.next}`);
}
}
parseExpressionList(terminator:int):List {
var result = [];
if (!this.next.isCharacter(terminator)) {
do {
ListWrapper.push(result, this.parseExpression());
} while (this.optionalCharacter($COMMA));
}
return result;
}
parseLiteralMap() {
var keys = [];
var values = [];
this.expectCharacter($LBRACE);
if (!this.optionalCharacter($RBRACE)) {
do {
var key = this.expectIdentifierOrKeywordOrString();
ListWrapper.push(keys, key);
this.expectCharacter($COLON);
ListWrapper.push(values, this.parseExpression());
} while (this.optionalCharacter($COMMA));
this.expectCharacter($RBRACE);
}
return new LiteralMap(keys, values);
}
parseAccessMemberOrMethodCall(receiver):AST {
var id = this.expectIdentifierOrKeyword();
if (this.optionalCharacter($LPAREN)) {
var args = this.parseCallArguments();
this.expectCharacter($RPAREN);
var fn = this.reflector.method(id);
return new MethodCall(receiver, id, fn, args);
} else {
var getter = this.reflector.getter(id);
var setter = this.reflector.setter(id);
return new AccessMember(receiver, id, getter, setter);
}
}
parseCallArguments() {
if (this.next.isCharacter($RPAREN)) return [];
var positionals = [];
do {
ListWrapper.push(positionals, this.parseExpression());
} while (this.optionalCharacter($COMMA))
return positionals;
}
/**
* An identifier, a keyword, a string with an optional `-` inbetween.
*/
expectTemplateBindingKey() {
var result = '';
var operatorFound = false;
do {
result += this.expectIdentifierOrKeywordOrString();
operatorFound = this.optionalOperator('-');
if (operatorFound) {
result += '-';
}
} while (operatorFound);
return result.toString();
}
parseTemplateBindings() {
var bindings = [];
while (this.index < this.tokens.length) {
var keyIsVar:boolean = this.optionalKeywordVar();
var key = this.expectTemplateBindingKey();
this.optionalCharacter($COLON);
var name = null;
var expression = null;
if (this.next !== EOF) {
if (keyIsVar) {
if (this.optionalOperator("=")) {
name = this.expectTemplateBindingKey();
} else {
name = '\$implicit';
}
} else if (!this.peekKeywordVar()) {
var start = this.inputIndex;
var ast = this.parseExpression();
var source = this.input.substring(start, this.inputIndex);
expression = new ASTWithSource(ast, source, this.location);
}
}
ListWrapper.push(bindings, new TemplateBinding(key, keyIsVar, name, expression));
if (!this.optionalCharacter($SEMICOLON)) {
this.optionalCharacter($COMMA);
};
}
return bindings;
}
error(message:string, index:int = null) {
if (isBlank(index)) index = this.index;
var location = (index < this.tokens.length)
? `at column ${this.tokens[index].index + 1} in`
: `at the end of the expression`;
throw new BaseException(`Parser Error: ${message} ${location} [${this.input}] in ${this.location}`);
}
}

View File

@ -0,0 +1,390 @@
import {isPresent, isBlank, BaseException, Type, isString} from 'facade/src/lang';
import {List, ListWrapper, MapWrapper, StringMapWrapper} from 'facade/src/collection';
import {
AccessMember,
Assignment,
AST,
ASTWithSource,
AstVisitor,
Binary,
Chain,
Structural,
Conditional,
Formatter,
FunctionCall,
ImplicitReceiver,
Interpolation,
KeyedAccess,
LiteralArray,
LiteralMap,
LiteralPrimitive,
MethodCall,
PrefixNot
} from './parser/ast';
import {ContextWithVariableBindings} from './parser/context_with_variable_bindings';
import {ChangeRecord, ChangeDispatcher, ChangeDetector} from './interfaces';
import {ChangeDetectionUtil} from './change_detection_util';
import {DynamicChangeDetector} from './dynamic_change_detector';
import {ChangeDetectorJITGenerator} from './change_detection_jit_generator';
import {ArrayChanges} from './array_changes';
import {KeyValueChanges} from './keyvalue_changes';
import {coalesce} from './coalesce';
export const RECORD_TYPE_SELF = 0;
export const RECORD_TYPE_CONST = 1;
export const RECORD_TYPE_PRIMITIVE_OP = 2;
export const RECORD_TYPE_PROPERTY = 3;
export const RECORD_TYPE_INVOKE_METHOD = 4;
export const RECORD_TYPE_INVOKE_CLOSURE = 5;
export const RECORD_TYPE_KEYED_ACCESS = 6;
export const RECORD_TYPE_INVOKE_FORMATTER = 7;
export const RECORD_TYPE_STRUCTURAL_CHECK = 8;
export const RECORD_TYPE_INTERPOLATE = 9;
export class ProtoRecord {
mode:number;
name:string;
funcOrValue:any;
args:List;
fixedArgs:List;
contextIndex:number;
selfIndex:number;
bindingMemento:any;
groupMemento:any;
lastInBinding:boolean;
lastInGroup:boolean;
expressionAsString:string;
constructor(mode:number,
name:string,
funcOrValue,
args:List,
fixedArgs:List,
contextIndex:number,
selfIndex:number,
bindingMemento:any,
groupMemento:any,
expressionAsString:string,
lastInBinding:boolean,
lastInGroup:boolean) {
this.mode = mode;
this.name = name;
this.funcOrValue = funcOrValue;
this.args = args;
this.fixedArgs = fixedArgs;
this.contextIndex = contextIndex;
this.selfIndex = selfIndex;
this.bindingMemento = bindingMemento;
this.groupMemento = groupMemento;
this.lastInBinding = lastInBinding;
this.lastInGroup = lastInGroup;
this.expressionAsString = expressionAsString;
}
isPureFunction():boolean {
return this.mode === RECORD_TYPE_INTERPOLATE ||
this.mode === RECORD_TYPE_INVOKE_FORMATTER ||
this.mode === RECORD_TYPE_PRIMITIVE_OP;
}
}
export class ProtoChangeDetector {
addAst(ast:AST, bindingMemento:any, groupMemento:any = null, structural:boolean = false){}
instantiate(dispatcher:any, formatters:Map):ChangeDetector{
return null;
}
}
export class DynamicProtoChangeDetector extends ProtoChangeDetector {
_records:List<ProtoRecord>;
_recordBuilder:ProtoRecordBuilder;
constructor() {
this._records = null;
this._recordBuilder = new ProtoRecordBuilder();
}
addAst(ast:AST, bindingMemento:any, groupMemento:any = null, structural:boolean = false) {
this._recordBuilder.addAst(ast, bindingMemento, groupMemento, structural);
}
instantiate(dispatcher:any, formatters:Map) {
this._createRecordsIfNecessary();
return new DynamicChangeDetector(dispatcher, formatters, this._records);
}
_createRecordsIfNecessary() {
if (isBlank(this._records)) {
var records = this._recordBuilder.records;
this._records = coalesce(records);
}
}
}
var _jitProtoChangeDetectorClassCounter:number = 0;
export class JitProtoChangeDetector extends ProtoChangeDetector {
_factory:Function;
_recordBuilder:ProtoRecordBuilder;
constructor() {
this._factory = null;
this._recordBuilder = new ProtoRecordBuilder();
}
addAst(ast:AST, bindingMemento:any, groupMemento:any = null, structural:boolean = false) {
this._recordBuilder.addAst(ast, bindingMemento, groupMemento, structural);
}
instantiate(dispatcher:any, formatters:Map) {
this._createFactoryIfNecessary();
return this._factory(dispatcher, formatters);
}
_createFactoryIfNecessary() {
if (isBlank(this._factory)) {
var c = _jitProtoChangeDetectorClassCounter++;
var records = coalesce(this._recordBuilder.records);
var typeName = `ChangeDetector${c}`;
this._factory = new ChangeDetectorJITGenerator(typeName, records).generate();
}
}
}
class ProtoRecordBuilder {
records:List<ProtoRecord>;
constructor() {
this.records = [];
}
addAst(ast:AST, bindingMemento:any, groupMemento:any = null, structural:boolean = false) {
if (structural) ast = new Structural(ast);
var last = ListWrapper.last(this.records);
if (isPresent(last) && last.groupMemento == groupMemento) {
last.lastInGroup = false;
}
var pr = _ConvertAstIntoProtoRecords.convert(ast, bindingMemento, groupMemento, this.records.length);
if (! ListWrapper.isEmpty(pr)) {
var last = ListWrapper.last(pr);
last.lastInBinding = true;
last.lastInGroup = true;
this.records = ListWrapper.concat(this.records, pr);
}
}
}
class _ConvertAstIntoProtoRecords {
protoRecords:List;
bindingMemento:any;
groupMemento:any;
contextIndex:number;
expressionAsString:string;
constructor(bindingMemento:any, groupMemento:any, contextIndex:number, expressionAsString:string) {
this.protoRecords = [];
this.bindingMemento = bindingMemento;
this.groupMemento = groupMemento;
this.contextIndex = contextIndex;
this.expressionAsString = expressionAsString;
}
static convert(ast:AST, bindingMemento:any, groupMemento:any, contextIndex:number) {
var c = new _ConvertAstIntoProtoRecords(bindingMemento, groupMemento, contextIndex, ast.toString());
ast.visit(c);
return c.protoRecords;
}
visitImplicitReceiver(ast:ImplicitReceiver) {
return 0;
}
visitInterpolation(ast:Interpolation) {
var args = this._visitAll(ast.expressions);
return this._addRecord(RECORD_TYPE_INTERPOLATE, "interpolate", _interpolationFn(ast.strings),
args, ast.strings, 0);
}
visitLiteralPrimitive(ast:LiteralPrimitive) {
return this._addRecord(RECORD_TYPE_CONST, "literal", ast.value, [], null, 0);
}
visitAccessMember(ast:AccessMember) {
var receiver = ast.receiver.visit(this);
return this._addRecord(RECORD_TYPE_PROPERTY, ast.name, ast.getter, [], null, receiver);
}
visitFormatter(ast:Formatter) {
return this._addRecord(RECORD_TYPE_INVOKE_FORMATTER, ast.name, ast.name, this._visitAll(ast.allArgs), null, 0);
}
visitMethodCall(ast:MethodCall) {
var receiver = ast.receiver.visit(this);
var args = this._visitAll(ast.args);
return this._addRecord(RECORD_TYPE_INVOKE_METHOD, ast.name, ast.fn, args, null, receiver);
}
visitFunctionCall(ast:FunctionCall) {
var target = ast.target.visit(this);
var args = this._visitAll(ast.args);
return this._addRecord(RECORD_TYPE_INVOKE_CLOSURE, "closure", null, args, null, target);
}
visitLiteralArray(ast:LiteralArray) {
var primitiveName = `arrayFn${ast.expressions.length}`;
return this._addRecord(RECORD_TYPE_PRIMITIVE_OP, primitiveName, _arrayFn(ast.expressions.length),
this._visitAll(ast.expressions), null, 0);
}
visitLiteralMap(ast:LiteralMap) {
return this._addRecord(RECORD_TYPE_PRIMITIVE_OP, _mapPrimitiveName(ast.keys),
ChangeDetectionUtil.mapFn(ast.keys), this._visitAll(ast.values), null, 0);
}
visitBinary(ast:Binary) {
var left = ast.left.visit(this);
var right = ast.right.visit(this);
return this._addRecord(RECORD_TYPE_PRIMITIVE_OP, _operationToPrimitiveName(ast.operation),
_operationToFunction(ast.operation), [left, right], null, 0);
}
visitPrefixNot(ast:PrefixNot) {
var exp = ast.expression.visit(this)
return this._addRecord(RECORD_TYPE_PRIMITIVE_OP, "operation_negate",
ChangeDetectionUtil.operation_negate, [exp], null, 0);
}
visitConditional(ast:Conditional) {
var c = ast.condition.visit(this);
var t = ast.trueExp.visit(this);
var f = ast.falseExp.visit(this);
return this._addRecord(RECORD_TYPE_PRIMITIVE_OP, "cond",
ChangeDetectionUtil.cond, [c,t,f], null, 0);
}
visitStructural(ast:Structural) {
var value = ast.value.visit(this);
return this._addRecord(RECORD_TYPE_STRUCTURAL_CHECK, "structural", null, [], null, value);
}
visitKeyedAccess(ast:KeyedAccess) {
var obj = ast.obj.visit(this);
var key = ast.key.visit(this);
return this._addRecord(RECORD_TYPE_KEYED_ACCESS, "keyedAccess",
ChangeDetectionUtil.keyedAccess, [key], null, obj);
}
_visitAll(asts:List) {
var res = ListWrapper.createFixedSize(asts.length);
for (var i = 0; i < asts.length; ++i) {
res[i] = asts[i].visit(this);
}
return res;
}
_addRecord(type, name, funcOrValue, args, fixedArgs, context) {
var selfIndex = ++ this.contextIndex;
ListWrapper.push(this.protoRecords,
new ProtoRecord(type, name, funcOrValue, args, fixedArgs, context, selfIndex,
this.bindingMemento, this.groupMemento, this.expressionAsString, false, false));
return selfIndex;
}
}
function _arrayFn(length:number):Function {
switch (length) {
case 0: return ChangeDetectionUtil.arrayFn0;
case 1: return ChangeDetectionUtil.arrayFn1;
case 2: return ChangeDetectionUtil.arrayFn2;
case 3: return ChangeDetectionUtil.arrayFn3;
case 4: return ChangeDetectionUtil.arrayFn4;
case 5: return ChangeDetectionUtil.arrayFn5;
case 6: return ChangeDetectionUtil.arrayFn6;
case 7: return ChangeDetectionUtil.arrayFn7;
case 8: return ChangeDetectionUtil.arrayFn8;
case 9: return ChangeDetectionUtil.arrayFn9;
default: throw new BaseException(`Does not support literal maps with more than 9 elements`);
}
}
function _mapPrimitiveName(keys:List) {
var stringifiedKeys = ListWrapper.join(
ListWrapper.map(keys, (k) => isString(k) ? `"${k}"` : `${k}`),
", ");
return `mapFn([${stringifiedKeys}])`;
}
function _operationToPrimitiveName(operation:string):string {
switch(operation) {
case '+' : return "operation_add";
case '-' : return "operation_subtract";
case '*' : return "operation_multiply";
case '/' : return "operation_divide";
case '%' : return "operation_remainder";
case '==' : return "operation_equals";
case '!=' : return "operation_not_equals";
case '<' : return "operation_less_then";
case '>' : return "operation_greater_then";
case '<=' : return "operation_less_or_equals_then";
case '>=' : return "operation_greater_or_equals_then";
case '&&' : return "operation_logical_and";
case '||' : return "operation_logical_or";
default: throw new BaseException(`Unsupported operation ${operation}`);
}
}
function _operationToFunction(operation:string):Function {
switch(operation) {
case '+' : return ChangeDetectionUtil.operation_add;
case '-' : return ChangeDetectionUtil.operation_subtract;
case '*' : return ChangeDetectionUtil.operation_multiply;
case '/' : return ChangeDetectionUtil.operation_divide;
case '%' : return ChangeDetectionUtil.operation_remainder;
case '==' : return ChangeDetectionUtil.operation_equals;
case '!=' : return ChangeDetectionUtil.operation_not_equals;
case '<' : return ChangeDetectionUtil.operation_less_then;
case '>' : return ChangeDetectionUtil.operation_greater_then;
case '<=' : return ChangeDetectionUtil.operation_less_or_equals_then;
case '>=' : return ChangeDetectionUtil.operation_greater_or_equals_then;
case '&&' : return ChangeDetectionUtil.operation_logical_and;
case '||' : return ChangeDetectionUtil.operation_logical_or;
default: throw new BaseException(`Unsupported operation ${operation}`);
}
}
function s(v) {
return isPresent(v) ? '' + v : '';
}
function _interpolationFn(strings:List) {
var length = strings.length;
var c0 = length > 0 ? strings[0] : null;
var c1 = length > 1 ? strings[1] : null;
var c2 = length > 2 ? strings[2] : null;
var c3 = length > 3 ? strings[3] : null;
var c4 = length > 4 ? strings[4] : null;
var c5 = length > 5 ? strings[5] : null;
var c6 = length > 6 ? strings[6] : null;
var c7 = length > 7 ? strings[7] : null;
var c8 = length > 8 ? strings[8] : null;
var c9 = length > 9 ? strings[9] : null;
switch (length - 1) {
case 1: return (a1) => c0 + s(a1) + c1;
case 2: return (a1, a2) => c0 + s(a1) + c1 + s(a2) + c2;
case 3: return (a1, a2, a3) => c0 + s(a1) + c1 + s(a2) + c2 + s(a3) + c3;
case 4: return (a1, a2, a3, a4) => c0 + s(a1) + c1 + s(a2) + c2 + s(a3) + c3 + s(a4) + c4;
case 5: return (a1, a2, a3, a4, a5) => c0 + s(a1) + c1 + s(a2) + c2 + s(a3) + c3 + s(a4) + c4 + s(a5) + c5;
case 6: return (a1, a2, a3, a4, a5, a6) => c0 + s(a1) + c1 + s(a2) + c2 + s(a3) + c3 + s(a4) + c4 + s(a5) + c5 + s(a6) + c6;
case 7: return (a1, a2, a3, a4, a5, a6, a7) => c0 + s(a1) + c1 + s(a2) + c2 + s(a3) + c3 + s(a4) + c4 + s(a5) + c5 + s(a6) + c6 + s(a7) + c7;
case 8: return (a1, a2, a3, a4, a5, a6, a7, a8) => c0 + s(a1) + c1 + s(a2) + c2 + s(a3) + c3 + s(a4) + c4 + s(a5) + c5 + s(a6) + c6 + s(a7) + c7 + s(a8) + c8;
case 9: return (a1, a2, a3, a4, a5, a6, a7, a8, a9) => c0 + s(a1) + c1 + s(a2) + c2 + s(a3) + c3 + s(a4) + c4 + s(a5) + c5 + s(a6) + c6 + s(a7) + c7 + s(a8) + c8 + s(a9) + c9;
default: throw new BaseException(`Does not support more than 9 expressions`);
}
}