feat(change_detection): reimplement change detection

This commit is contained in:
vsavkin
2015-01-14 13:51:16 -08:00
parent 22653707d9
commit 9957c1338e
30 changed files with 1257 additions and 2233 deletions

View File

@ -1,7 +1,9 @@
export {ChangeDetectionError, ChangeDetector} from './change_detector';
export {AST, ASTWithSource} from './parser/ast';
export {AST} from './parser/ast';
export {Lexer} from './parser/lexer';
export {Parser} from './parser/parser';
export {ProtoRecordRange, RecordRange, ChangeDispatcher} from './record_range';
export {ProtoRecord, Record} from './record';
export {ContextWithVariableBindings} from './parser/context_with_variable_bindings';
export {ExpressionChangedAfterItHasBeenChecked, ChangeDetectionError} from './exceptions';
export {ChangeRecord, ChangeDispatcher, ChangeDetector} from './interfaces';
export {ProtoChangeDetector} from './proto_change_detector';
export {DynamicChangeDetector} from './dynamic_change_detector';

View File

@ -1,87 +0,0 @@
import {ProtoRecordRange, RecordRange} from './record_range';
import {ProtoRecord, Record} from './record';
import {int, isPresent, isBlank} from 'facade/lang';
import {ListWrapper, List} from 'facade/collection';
export * from './record';
export * from './record_range'
class ExpressionChangedAfterItHasBeenChecked extends Error {
message:string;
constructor(record:Record) {
this.message = `Expression '${record.expressionAsString()}' has changed after it was checked. ` +
`Previous value: '${record.previousValue}'. Current value: '${record.currentValue}'`;
}
toString():string {
return this.message;
}
}
export class ChangeDetector {
_rootRecordRange:RecordRange;
_enforceNoNewChanges:boolean;
constructor(recordRange:RecordRange, enforceNoNewChanges:boolean = false) {
this._rootRecordRange = recordRange;
this._enforceNoNewChanges = enforceNoNewChanges;
}
detectChanges():int {
var count = this._detectChanges(false);
if (this._enforceNoNewChanges) {
this._detectChanges(true)
}
return count;
}
_detectChanges(throwOnChange:boolean):int {
var count = 0;
var updatedRecords = null;
var record = this._rootRecordRange.findFirstEnabledRecord();
var currentRange, currentGroup;
while (isPresent(record)) {
if (record.check()) {
count++;
if (record.terminatesExpression()) {
if (throwOnChange) throw new ExpressionChangedAfterItHasBeenChecked(record);
currentRange = record.recordRange;
currentGroup = record.groupMemento();
updatedRecords = this._addRecord(updatedRecords, record);
}
}
if (isPresent(updatedRecords)) {
var nextEnabled = record.nextEnabled;
if (isBlank(nextEnabled) || // we have reached the last enabled record
currentRange !== nextEnabled.recordRange || // the next record is in a different range
currentGroup !== nextEnabled.groupMemento()) { // the next record is in a different group
currentRange.dispatcher.onRecordChange(currentGroup, updatedRecords);
updatedRecords = null;
}
}
record = record.findNextEnabled();
}
return count;
}
_addRecord(updatedRecords:List, record:Record) {
if (isBlank(updatedRecords)) {
updatedRecords = _singleElementList;
updatedRecords[0] = record;
} else if (updatedRecords === _singleElementList) {
updatedRecords = [_singleElementList[0], record];
} else {
ListWrapper.push(updatedRecords, record);
}
return updatedRecords;
}
}
var _singleElementList = [null];

View File

@ -0,0 +1,259 @@
import {isPresent, isBlank, BaseException, FunctionWrapper} from 'facade/lang';
import {List, ListWrapper, MapWrapper, StringMapWrapper} from 'facade/collection';
import {ContextWithVariableBindings} from './parser/context_with_variable_bindings';
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_INVOKE_PURE_FUNCTION,
RECORD_TYPE_INVOKE_FORMATTER,
RECORD_TYPE_STRUCTURAL_CHECK,
ProtoChangeDetector
} from './proto_change_detector';
import {ChangeDetector, ChangeRecord, ChangeDispatcher} from './interfaces';
import {ExpressionChangedAfterItHasBeenChecked, ChangeDetectionError} from './exceptions';
class SimpleChange {
previousValue:any;
currentValue:any;
constructor(previousValue:any, currentValue:any) {
this.previousValue = previousValue;
this.currentValue = currentValue;
}
}
export class DynamicChangeDetector extends ChangeDetector {
dispatcher:any;
formatters:Map;
children:List;
values:List;
protos:List<ProtoRecord>;
parent:ChangeDetector;
constructor(dispatcher:any, formatters:Map, protoRecords:List<ProtoRecord>) {
this.dispatcher = dispatcher;
this.formatters = formatters;
this.values = ListWrapper.createFixedSize(protoRecords.length + 1);
this.protos = protoRecords;
this.children = [];
}
addChild(cd:ChangeDetector) {
ListWrapper.push(this.children, cd);
cd.parent = this;
}
removeChild(cd:ChangeDetector) {
ListWrapper.remove(this.children, cd);
}
remove() {
this.parent.removeChild(this);
}
setContext(context:any) {
this.values[0] = context;
}
detectChanges() {
this._detectChanges(false);
}
checkNoChanges() {
this._detectChanges(true);
}
_detectChanges(throwOnChange:boolean) {
this._detectChangesInRecords(throwOnChange);
this._detectChangesInChildren(throwOnChange);
}
_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);
// only when the terminal record, which ends a binding, changes
// we need to add it to a list of changed records
if (isPresent(change) && proto.terminal) {
if (throwOnChange) throw new ExpressionChangedAfterItHasBeenChecked(proto, change);
currentGroup = proto.groupMemento;
updatedRecords = this._addRecord(updatedRecords, proto, change);
}
if (isPresent(updatedRecords)) {
var lastRecordOfCurrentGroup = protos.length == i + 1 ||
currentGroup !== protos[i + 1].groupMemento;
if (lastRecordOfCurrentGroup) {
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) {
var prevValue = this._readSelf(proto);
var currValue = this._calculateCurrValue(proto);
if (! isSame(prevValue, currValue)) {
this._writeSelf(proto, currValue);
return new SimpleChange(prevValue, currValue);
} else {
return null;
}
}
_calculateCurrValue(proto) {
switch (proto.mode) {
case RECORD_TYPE_SELF:
throw new BaseException("Cannot evaluate self");
case RECORD_TYPE_CONST:
return proto.funcOrValue;
case RECORD_TYPE_PROPERTY:
var context = this._readContext(proto);
while (context instanceof ContextWithVariableBindings) {
if (context.hasBinding(proto.name)) {
return context.get(proto.name);
}
context = context.parent;
}
var propertyGetter:Function = proto.funcOrValue;
return propertyGetter(context);
case RECORD_TYPE_INVOKE_METHOD:
var methodInvoker:Function = proto.funcOrValue;
return methodInvoker(this._readContext(proto), this._readArgs(proto));
case RECORD_TYPE_INVOKE_CLOSURE:
return FunctionWrapper.apply(this._readContext(proto), this._readArgs(proto));
case RECORD_TYPE_INVOKE_PURE_FUNCTION:
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) {
var self = this._readSelf(proto);
var context = this._readContext(proto);
if (isBlank(self)) {
if (ArrayChanges.supports(context)) {
self = new ArrayChanges();
} else if (KeyValueChanges.supports(context)) {
self = new KeyValueChanges();
}
}
if (ArrayChanges.supports(context)) {
if (self.check(context)) {
this._writeSelf(proto, self);
return new SimpleChange(null, self); // TODO: don't wrap and return self instead
}
} else if (KeyValueChanges.supports(context)) {
if (self.check(context)) {
this._writeSelf(proto, self);
return new SimpleChange(null, self); // TODO: don't wrap and return self instead
}
} else if (context == null) {
this._writeSelf(proto, null);
return new SimpleChange(null, null);
} else {
throw new BaseException(`Unsupported type (${context})`);
}
}
_addRecord(updatedRecords:List, proto:ProtoRecord, change):List {
// we can use a pool of change records not to create extra garbage
var record = new ChangeRecord(proto.bindingMemento, change);
if (isBlank(updatedRecords)) {
updatedRecords = _singleElementList;
updatedRecords[0] = record;
} else if (updatedRecords === _singleElementList) {
updatedRecords = [_singleElementList[0], record];
} else {
ListWrapper.push(updatedRecords, record);
}
return updatedRecords;
}
_detectChangesInChildren(throwOnChange:boolean) {
var children = this.children;
for(var i = 0; i < children.length; ++i) {
children[i]._detectChanges(throwOnChange);
}
}
_readContext(proto) {
return this.values[proto.contextIndex];
}
_readSelf(proto) {
return this.values[proto.record_type_selfIndex];
}
_writeSelf(proto, value) {
this.values[proto.record_type_selfIndex] = value;
}
_readArgs(proto) {
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,36 @@
import {List} from 'facade/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;
}
}
export class ChangeDispatcher {
onRecordChange(groupMemento, records:List<ChangeRecord>) {}
}
export class ChangeDetector {
parent:ChangeDetector;
addChild(cd:ChangeDetector) {}
removeChild(cd:ChangeDetector) {}
remove() {}
setContext(context:any) {}
detectChanges() {}
checkNoChanges() {}
}

View File

@ -15,7 +15,7 @@ export class AST {
throw new BaseException("Not supported");
}
visit(visitor, args) {
visit(visitor) {
}
toString():string {
@ -28,12 +28,12 @@ export class EmptyExpr extends AST {
return null;
}
visit(visitor, args) {
visit(visitor) {
//do nothing
}
}
export class Collection extends AST {
export class Structural extends AST {
value:AST;
constructor(value:AST) {
this.value = value;
@ -43,8 +43,8 @@ export class Collection extends AST {
return value.eval(context);
}
visit(visitor, args) {
visitor.visitCollection(this, args);
visit(visitor) {
return visitor.visitStructural(this);
}
}
@ -53,8 +53,8 @@ export class ImplicitReceiver extends AST {
return context;
}
visit(visitor, args) {
visitor.visitImplicitReceiver(this, args);
visit(visitor) {
return visitor.visitImplicitReceiver(this);
}
}
@ -76,8 +76,8 @@ export class Chain extends AST {
return result;
}
visit(visitor, args) {
visitor.visitChain(this, args);
visit(visitor) {
return visitor.visitChain(this);
}
}
@ -99,8 +99,8 @@ export class Conditional extends AST {
}
}
visit(visitor, args) {
visitor.visitConditional(this, args);
visit(visitor) {
return visitor.visitConditional(this);
}
}
@ -146,8 +146,8 @@ export class AccessMember extends AST {
return this.setter(evaluatedContext, value);
}
visit(visitor, args) {
visitor.visitAccessMember(this, args);
visit(visitor) {
return visitor.visitAccessMember(this);
}
}
@ -176,8 +176,8 @@ export class KeyedAccess extends AST {
return value;
}
visit(visitor, args) {
visitor.visitKeyedAccess(this, args);
visit(visitor) {
return visitor.visitKeyedAccess(this);
}
}
@ -193,8 +193,8 @@ export class Formatter extends AST {
this.allArgs = ListWrapper.concat([exp], args);
}
visit(visitor, args) {
visitor.visitFormatter(this, args);
visit(visitor) {
return visitor.visitFormatter(this);
}
}
@ -208,8 +208,8 @@ export class LiteralPrimitive extends AST {
return this.value;
}
visit(visitor, args) {
visitor.visitLiteralPrimitive(this, args);
visit(visitor) {
return visitor.visitLiteralPrimitive(this);
}
}
@ -223,8 +223,8 @@ export class LiteralArray extends AST {
return ListWrapper.map(this.expressions, (e) => e.eval(context));
}
visit(visitor, args) {
visitor.visitLiteralArray(this, args);
visit(visitor) {
return visitor.visitLiteralArray(this);
}
}
@ -244,8 +244,8 @@ export class LiteralMap extends AST {
return res;
}
visit(visitor, args) {
visitor.visitLiteralMap(this, args);
visit(visitor) {
return visitor.visitLiteralMap(this);
}
}
@ -285,8 +285,8 @@ export class Binary extends AST {
throw 'Internal error [$operation] not handled';
}
visit(visitor, args) {
visitor.visitBinary(this, args);
visit(visitor) {
return visitor.visitBinary(this);
}
}
@ -300,8 +300,8 @@ export class PrefixNot extends AST {
return !this.expression.eval(context);
}
visit(visitor, args) {
visitor.visitPrefixNot(this, args);
visit(visitor) {
return visitor.visitPrefixNot(this);
}
}
@ -317,8 +317,8 @@ export class Assignment extends AST {
return this.target.assign(context, this.value.eval(context));
}
visit(visitor, args) {
visitor.visitAssignment(this, args);
visit(visitor) {
return visitor.visitAssignment(this);
}
}
@ -349,8 +349,8 @@ export class MethodCall extends AST {
return this.fn(evaluatedContext, evaluatedArgs);
}
visit(visitor, args) {
visitor.visitMethodCall(this, args);
visit(visitor) {
return visitor.visitMethodCall(this);
}
}
@ -370,8 +370,8 @@ export class FunctionCall extends AST {
return FunctionWrapper.apply(obj, evalList(context, this.args));
}
visit(visitor, args) {
visitor.visitFunctionCall(this, args);
visit(visitor) {
return visitor.visitFunctionCall(this);
}
}
@ -397,8 +397,8 @@ export class ASTWithSource extends AST {
return this.ast.assign(context, value);
}
visit(visitor, args) {
return this.ast.visit(visitor, args);
visit(visitor) {
return this.ast.visit(visitor);
}
toString():string {
@ -420,21 +420,21 @@ export class TemplateBinding {
//INTERFACE
export class AstVisitor {
visitAccessMember(ast:AccessMember, args) {}
visitAssignment(ast:Assignment, args) {}
visitBinary(ast:Binary, args) {}
visitChain(ast:Chain, args){}
visitCollection(ast:Collection, args) {}
visitConditional(ast:Conditional, args) {}
visitFormatter(ast:Formatter, args) {}
visitFunctionCall(ast:FunctionCall, args) {}
visitImplicitReceiver(ast:ImplicitReceiver, args) {}
visitKeyedAccess(ast:KeyedAccess, args) {}
visitLiteralArray(ast:LiteralArray, args) {}
visitLiteralMap(ast:LiteralMap, args) {}
visitLiteralPrimitive(ast:LiteralPrimitive, args) {}
visitMethodCall(ast:MethodCall, args) {}
visitPrefixNot(ast:PrefixNot, args) {}
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]];

View File

@ -0,0 +1,277 @@
import {isPresent, isBlank, BaseException} from 'facade/lang';
import {List, ListWrapper, MapWrapper, StringMapWrapper} from 'facade/collection';
import {
AccessMember,
Assignment,
AST,
ASTWithSource,
AstVisitor,
Binary,
Chain,
Structural,
Conditional,
Formatter,
FunctionCall,
ImplicitReceiver,
KeyedAccess,
LiteralArray,
LiteralMap,
LiteralPrimitive,
MethodCall,
PrefixNot
} from './parser/ast';
import {ContextWithVariableBindings} from './parser/context_with_variable_bindings';
import {ChangeDispatcher, ChangeDetector} from './interfaces';
import {DynamicChangeDetector} from './dynamic_change_detector';
export const RECORD_TYPE_SELF = 0;
export const RECORD_TYPE_PROPERTY = 1;
export const RECORD_TYPE_INVOKE_METHOD = 2;
export const RECORD_TYPE_CONST = 3;
export const RECORD_TYPE_INVOKE_CLOSURE = 4;
export const RECORD_TYPE_INVOKE_PURE_FUNCTION = 5;
export const RECORD_TYPE_INVOKE_FORMATTER = 6;
export const RECORD_TYPE_STRUCTURAL_CHECK = 10;
export class ProtoRecord {
mode:number;
name:string;
funcOrValue:any;
args:List;
contextIndex:number;
record_type_selfIndex:number;
bindingMemento:any;
groupMemento:any;
terminal:boolean;
expressionAsString:string;
constructor(mode:number,
name:string,
funcOrValue,
args:List,
contextIndex:number,
record_type_selfIndex:number,
bindingMemento:any,
groupMemento:any,
terminal:boolean,
expressionAsString:string) {
this.mode = mode;
this.name = name;
this.funcOrValue = funcOrValue;
this.args = args;
this.contextIndex = contextIndex;
this.record_type_selfIndex = record_type_selfIndex;
this.bindingMemento = bindingMemento;
this.groupMemento = groupMemento;
this.terminal = terminal;
this.expressionAsString = expressionAsString;
}
}
export class ProtoChangeDetector {
records:List<ProtoRecord>;
constructor() {
this.records = [];
}
addAst(ast:AST, bindingMemento:any, groupMemento:any = null, structural:boolean = false) {
if (structural) ast = new Structural(ast);
var c = new ProtoOperationsCreator(bindingMemento, groupMemento,
this.records.length, ast.toString());
ast.visit(c);
if (! ListWrapper.isEmpty(c.protoRecords)) {
var last = ListWrapper.last(c.protoRecords);
last.terminal = true;
this.records = ListWrapper.concat(this.records, c.protoRecords);
}
}
instantiate(dispatcher:any, formatters:Map) {
return new DynamicChangeDetector(dispatcher, formatters, this.records);
}
}
class ProtoOperationsCreator {
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;
}
visitImplicitReceiver(ast:ImplicitReceiver) {
return 0;
}
visitLiteralPrimitive(ast:LiteralPrimitive) {
return this._addRecord(RECORD_TYPE_CONST, null, ast.value, [], 0);
}
visitAccessMember(ast:AccessMember) {
var receiver = ast.receiver.visit(this);
return this._addRecord(RECORD_TYPE_PROPERTY, ast.name, ast.getter, [], receiver);
}
visitFormatter(ast:Formatter) {
return this._addRecord(RECORD_TYPE_INVOKE_FORMATTER, ast.name, ast.name, this._visitAll(ast.allArgs), 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, receiver);
}
visitFunctionCall(ast:FunctionCall) {
var target = ast.target.visit(this);
var args = this._visitAll(ast.args);
return this._addRecord(RECORD_TYPE_INVOKE_CLOSURE, null, null, args, target);
}
visitLiteralArray(ast:LiteralArray) {
return this._addRecord(RECORD_TYPE_INVOKE_PURE_FUNCTION, "Array()", _arrayFn(ast.expressions.length),
this._visitAll(ast.expressions), 0);
}
visitLiteralMap(ast:LiteralMap) {
return this._addRecord(RECORD_TYPE_INVOKE_PURE_FUNCTION, "Map()", _mapFn(ast.keys, ast.values.length),
this._visitAll(ast.values), 0);
}
visitBinary(ast:Binary) {
var left = ast.left.visit(this);
var right = ast.right.visit(this);
return this._addRecord(RECORD_TYPE_INVOKE_PURE_FUNCTION, ast.operation, _operationToFunction(ast.operation), [left, right], 0);
}
visitPrefixNot(ast:PrefixNot) {
var exp = ast.expression.visit(this)
return this._addRecord(RECORD_TYPE_INVOKE_PURE_FUNCTION, "-", _operation_negate, [exp], 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_INVOKE_PURE_FUNCTION, "?:", _cond, [c,t,f], 0);
}
visitStructural(ast:Structural) {
var value = ast.value.visit(this);
return this._addRecord(RECORD_TYPE_STRUCTURAL_CHECK, "record_type_structural_check", null, [], value);
}
visitKeyedAccess(ast:KeyedAccess) {
var obj = ast.obj.visit(this);
var key = ast.key.visit(this);
return this._addRecord(RECORD_TYPE_INVOKE_METHOD, "[]", _keyedAccess, [key], 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, context) {
var record_type_selfIndex = ++ this.contextIndex;
ListWrapper.push(this.protoRecords,
new ProtoRecord(type, name, funcOrValue, args, context, record_type_selfIndex,
this.bindingMemento, this.groupMemento, false, this.expressionAsString));
return record_type_selfIndex;
}
}
function _arrayFn(length:int) {
switch (length) {
case 0: return () => [];
case 1: return (a1) => [a1];
case 2: return (a1, a2) => [a1, a2];
case 3: return (a1, a2, a3) => [a1, a2, a3];
case 4: return (a1, a2, a3, a4) => [a1, a2, a3, a4];
case 5: return (a1, a2, a3, a4, a5) => [a1, a2, a3, a4, a5];
case 6: return (a1, a2, a3, a4, a5, a6) => [a1, a2, a3, a4, a5, a6];
case 7: return (a1, a2, a3, a4, a5, a6, a7) => [a1, a2, a3, a4, a5, a6, a7];
case 8: return (a1, a2, a3, a4, a5, a6, a7, a8) => [a1, a2, a3, a4, a5, a6, a7, a8];
case 9: return (a1, a2, a3, a4, a5, a6, a7, a8, a9) => [a1, a2, a3, a4, a5, a6, a7, a8, a9];
default: throw new BaseException(`Does not support literal arrays with more than 9 elements`);
}
}
function _mapFn(keys:List, length:int) {
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 (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`);
}
}
function _operationToFunction(operation:string):Function {
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 _operation_negate(value) {return !value;}
function _operation_add(left, right) {return left + right;}
function _operation_subtract(left, right) {return left - right;}
function _operation_multiply(left, right) {return left * right;}
function _operation_divide(left, right) {return left / right;}
function _operation_remainder(left, right) {return left % right;}
function _operation_equals(left, right) {return left == right;}
function _operation_not_equals(left, right) {return left != right;}
function _operation_less_then(left, right) {return left < right;}
function _operation_greater_then(left, right) {return left > right;}
function _operation_less_or_equals_then(left, right) {return left <= right;}
function _operation_greater_or_equals_then(left, right) {return left >= right;}
function _operation_logical_and(left, right) {return left && right;}
function _operation_logical_or(left, right) {return left || right;}
function _cond(cond, trueVal, falseVal) {return cond ? trueVal : falseVal;}
function _keyedAccess(obj, args) {
return obj[args[0]];
}

View File

@ -1,505 +0,0 @@
import {ProtoRecordRange, RecordRange} from './record_range';
import {FIELD, isPresent, isBlank, int, StringWrapper, FunctionWrapper, BaseException} from 'facade/lang';
import {List, Map, ListWrapper, MapWrapper} from 'facade/collection';
import {ArrayChanges} from './array_changes';
import {KeyValueChanges} from './keyvalue_changes';
var _fresh = new Object();
const RECORD_TYPE_MASK = 0x000f;
export const RECORD_TYPE_CONST = 0x0000;
export const RECORD_TYPE_INVOKE_CLOSURE = 0x0001;
export const RECORD_TYPE_INVOKE_FORMATTER = 0x0002;
export const RECORD_TYPE_INVOKE_METHOD = 0x0003;
export const RECORD_TYPE_INVOKE_PURE_FUNCTION = 0x0004;
const RECORD_TYPE_ARRAY = 0x0005;
const RECORD_TYPE_KEY_VALUE = 0x0006;
const RECORD_TYPE_MARKER = 0x0007;
export const RECORD_TYPE_PROPERTY = 0x0008;
const RECORD_TYPE_NULL= 0x0009;
const RECORD_FLAG_DISABLED = 0x0100;
export const RECORD_FLAG_IMPLICIT_RECEIVER = 0x0200;
export const RECORD_FLAG_COLLECTION = 0x0400;
/**
* For now we are dropping expression coalescence. We can always add it later, but
* real world numbers show that it does not provide significant benefits.
*/
export class ProtoRecord {
recordRange:ProtoRecordRange;
_mode:int;
context:any;
funcOrValue:any;
arity:int;
name:string;
dest:any;
groupMemento:any;
expressionAsString:string;
next:ProtoRecord;
recordInConstruction:Record;
constructor(recordRange:ProtoRecordRange,
mode:int,
funcOrValue,
arity:int,
name:string,
dest,
groupMemento,
expressionAsString:string) {
this.recordRange = recordRange;
this._mode = mode;
this.funcOrValue = funcOrValue;
this.arity = arity;
this.name = name;
this.dest = dest;
this.groupMemento = groupMemento;
this.expressionAsString = expressionAsString;
this.next = null;
// The concrete Record instantiated from this ProtoRecord
this.recordInConstruction = null;
}
setIsImplicitReceiver() {
this._mode |= RECORD_FLAG_IMPLICIT_RECEIVER;
}
}
/**
* Represents a Record for keeping track of changes. A change is a difference between previous
* and current value.
*
* By default changes are detected using dirty checking, but a notifier can be present which can
* notify the records of changes by means other than dirty checking. For example Object.observe
* or events on DOM elements.
*
* DESIGN NOTES:
* - No inheritance allowed so that code is monomorphic for performance.
* - Atomic watch operations
* - Defaults to dirty checking
* - Keep this object as lean as possible. (Lean in number of fields)
*/
export class Record {
recordRange:RecordRange;
protoRecord:ProtoRecord;
next:Record;
prev:Record;
/// This reference can change.
nextEnabled:Record;
/// This reference can change.
prevEnabled:Record;
previousValue;
currentValue;
_mode:int;
context;
funcOrValue;
args:List;
// Opaque data which will be the target of notification.
// If the object is instance of Record, then it it is directly processed
// Otherwise it is the context used by ChangeDispatcher.
dest;
constructor(recordRange:RecordRange, protoRecord:ProtoRecord, formatters:Map) {
this.recordRange = recordRange;
this.protoRecord = protoRecord;
this.next = null;
this.prev = null;
this.nextEnabled = null;
this.prevEnabled = null;
this.dest = null;
this.previousValue = null;
this.context = null;
this.funcOrValue = null;
this.args = null;
if (isBlank(protoRecord)) {
this._mode = RECORD_TYPE_MARKER | RECORD_FLAG_DISABLED;
return;
}
this._mode = protoRecord._mode;
// Return early for collections, further init delayed until updateContext()
if (this.isCollection()) return;
this.currentValue = _fresh;
var type = this.getType();
if (type === RECORD_TYPE_CONST) {
this.funcOrValue = protoRecord.funcOrValue;
} else if (type === RECORD_TYPE_INVOKE_PURE_FUNCTION) {
this.funcOrValue = protoRecord.funcOrValue;
this.args = ListWrapper.createFixedSize(protoRecord.arity);
} else if (type === RECORD_TYPE_INVOKE_FORMATTER) {
this.funcOrValue = MapWrapper.get(formatters, protoRecord.funcOrValue);
this.args = ListWrapper.createFixedSize(protoRecord.arity);
} else if (type === RECORD_TYPE_INVOKE_METHOD) {
this.funcOrValue = protoRecord.funcOrValue;
this.args = ListWrapper.createFixedSize(protoRecord.arity);
} else if (type === RECORD_TYPE_INVOKE_CLOSURE) {
this.args = ListWrapper.createFixedSize(protoRecord.arity);
} else if (type === RECORD_TYPE_PROPERTY) {
this.funcOrValue = protoRecord.funcOrValue;
}
}
// getters & setters perform much worse on some browsers
// see http://jsperf.com/vicb-getter-vs-function
getType():int {
return this._mode & RECORD_TYPE_MASK;
}
setType(value:int) {
this._mode = (this._mode & ~RECORD_TYPE_MASK) | value;
}
isDisabled():boolean {
return (this._mode & RECORD_FLAG_DISABLED) === RECORD_FLAG_DISABLED;
}
isEnabled():boolean {
return !this.isDisabled();
}
_setDisabled(value:boolean) {
if (value) {
this._mode |= RECORD_FLAG_DISABLED;
} else {
this._mode &= ~RECORD_FLAG_DISABLED;
}
}
enable() {
if (this.isEnabled()) return;
var prevEnabled = this.findPrevEnabled();
var nextEnabled = this.findNextEnabled();
this.prevEnabled = prevEnabled;
this.nextEnabled = nextEnabled;
if (isPresent(prevEnabled)) prevEnabled.nextEnabled = this;
if (isPresent(nextEnabled)) nextEnabled.prevEnabled = this;
this._setDisabled(false);
}
disable() {
var prevEnabled = this.prevEnabled;
var nextEnabled = this.nextEnabled;
if (isPresent(prevEnabled)) prevEnabled.nextEnabled = nextEnabled;
if (isPresent(nextEnabled)) nextEnabled.prevEnabled = prevEnabled;
this._setDisabled(true);
}
isImplicitReceiver():boolean {
return (this._mode & RECORD_FLAG_IMPLICIT_RECEIVER) === RECORD_FLAG_IMPLICIT_RECEIVER;
}
isCollection():boolean {
return (this._mode & RECORD_FLAG_COLLECTION) === RECORD_FLAG_COLLECTION;
}
static createMarker(rr:RecordRange):Record {
return new Record(rr, null, null);
}
check():boolean {
if (this.isCollection()) {
return this._checkCollection();
} else {
return this._checkSingleRecord();
}
}
_checkSingleRecord():boolean {
this.previousValue = this.currentValue;
this.currentValue = this._calculateNewValue();
if (isSame(this.previousValue, this.currentValue)) return false;
this._updateDestination();
return true;
}
_updateDestination() {
if (this.dest instanceof Record) {
if (isPresent(this.protoRecord.dest.position)) {
this.dest.updateArg(this.currentValue, this.protoRecord.dest.position);
} else {
this.dest.updateContext(this.currentValue);
}
}
}
// return whether the content has changed
_checkCollection():boolean {
switch(this.getType()) {
case RECORD_TYPE_KEY_VALUE:
var kvChangeDetector:KeyValueChanges = this.currentValue;
return kvChangeDetector.check(this.context);
case RECORD_TYPE_ARRAY:
var arrayChangeDetector:ArrayChanges = this.currentValue;
return arrayChangeDetector.check(this.context);
case RECORD_TYPE_NULL:
// no need to check the content again unless the context changes
this.disable();
this.currentValue = null;
return true;
default:
throw new BaseException(`Unsupported record type (${this.getType()})`);
}
}
_calculateNewValue() {
try {
return this.__calculateNewValue();
} catch (e) {
throw new ChangeDetectionError(this, e);
}
}
__calculateNewValue() {
switch (this.getType()) {
case RECORD_TYPE_PROPERTY:
var propertyGetter:Function = this.funcOrValue;
return propertyGetter(this.context);
case RECORD_TYPE_INVOKE_METHOD:
var methodInvoker:Function = this.funcOrValue;
return methodInvoker(this.context, this.args);
case RECORD_TYPE_INVOKE_CLOSURE:
return FunctionWrapper.apply(this.context, this.args);
case RECORD_TYPE_INVOKE_PURE_FUNCTION:
case RECORD_TYPE_INVOKE_FORMATTER:
this.disable();
return FunctionWrapper.apply(this.funcOrValue, this.args);
case RECORD_TYPE_CONST:
this.disable();
return this.funcOrValue;
default:
throw new BaseException(`Unsupported record type (${this.getType()})`);
}
}
updateArg(value, position:int) {
this.args[position] = value;
this.enable();
}
updateContext(value) {
this.context = value;
this.enable();
if (this.isCollection()) {
if (ArrayChanges.supports(value)) {
if (this.getType() != RECORD_TYPE_ARRAY) {
this.setType(RECORD_TYPE_ARRAY);
this.currentValue = new ArrayChanges();
}
return;
}
if (KeyValueChanges.supports(value)) {
if (this.getType() != RECORD_TYPE_KEY_VALUE) {
this.setType(RECORD_TYPE_KEY_VALUE);
this.currentValue = new KeyValueChanges();
}
return;
}
if (isBlank(value)) {
this.setType(RECORD_TYPE_NULL);
} else {
throw new BaseException("Collection records must be array like, map like or null");
}
}
}
terminatesExpression():boolean {
return !(this.dest instanceof Record);
}
isMarkerRecord():boolean {
return this.getType() == RECORD_TYPE_MARKER;
}
expressionMemento() {
return this.protoRecord.dest;
}
expressionAsString() {
return this.protoRecord.expressionAsString;
}
groupMemento() {
return isPresent(this.protoRecord) ? this.protoRecord.groupMemento : null;
}
/**
* Returns the next enabled record. This search is not limited to the current range.
*
* [H ER1 T] [H ER2 T] _nextEnable(ER1) will return ER2
*
* The function skips disabled ranges.
*/
findNextEnabled() {
if (this.isEnabled()) return this.nextEnabled;
var record = this.next;
while (isPresent(record) && record.isDisabled()) {
if (record.isMarkerRecord() && record.recordRange.disabled) {
record = record.recordRange.tailRecord.next;
} else {
record = record.next;
}
}
return record;
}
/**
* Returns the prev enabled record. This search is not limited to the current range.
*
* [H ER1 T] [H ER2 T] _nextEnable(ER2) will return ER1
*
* The function skips disabled ranges.
*/
findPrevEnabled() {
if (this.isEnabled()) return this.prevEnabled;
var record = this.prev;
while (isPresent(record) && record.isDisabled()) {
if (record.isMarkerRecord() && record.recordRange.disabled) {
record = record.recordRange.headRecord.prev;
} else {
record = record.prev;
}
}
return record;
}
inspect() {
return _inspect(this);
}
inspectRange() {
return this.recordRange.inspect();
}
}
function _inspect(record:Record) {
function mode() {
switch (record.getType()) {
case RECORD_TYPE_PROPERTY:
return "property";
case RECORD_TYPE_INVOKE_METHOD:
return "invoke_method";
case RECORD_TYPE_INVOKE_CLOSURE:
return "invoke_closure";
case RECORD_TYPE_INVOKE_PURE_FUNCTION:
return "pure_function";
case RECORD_TYPE_INVOKE_FORMATTER:
return "invoke_formatter";
case RECORD_TYPE_CONST:
return "const";
case RECORD_TYPE_KEY_VALUE:
return "key_value";
case RECORD_TYPE_ARRAY:
return "array";
case RECORD_TYPE_NULL:
return "null";
case RECORD_TYPE_MARKER:
return "marker";
default:
return "unexpected type!";
}
}
function disabled() {
return record.isDisabled() ? "disabled" : "enabled";
}
function description() {
var name = isPresent(record.protoRecord) ? record.protoRecord.name : "";
var exp = isPresent(record.protoRecord) ? record.protoRecord.expressionAsString : "";
var currValue = record.currentValue;
var context = record.context;
return `${mode()}, ${name}, ${disabled()} ` +
` Current: ${currValue}, Context: ${context} in [${exp}]`;
}
if (isBlank(record)) return null;
if (!(record instanceof Record)) return record;
return new _RecordInspect(description(), record);
}
class _RecordInspect {
description:string;
record:Record;
constructor(description:string,record:Record) {
this.description = description;
this.record = record;
}
get next() {
return _inspect(this.record.next);
}
get nextEnabled() {
return _inspect(this.record.nextEnabled);
}
get dest() {
return _inspect(this.record.dest);
}
}
function isSame(a, b) {
if (a === b) return true;
if ((a !== a) && (b !== b)) return true;
return false;
}
export class ChangeDetectionError extends Error {
message:string;
originalException:any;
location:string;
constructor(record:Record, originalException:any) {
this.originalException = originalException;
this.location = record.protoRecord.expressionAsString;
this.message = `${this.originalException} in [${this.location}]`;
}
toString():string {
return this.message;
}
}

View File

@ -1,558 +0,0 @@
import {
ProtoRecord,
Record,
RECORD_FLAG_COLLECTION,
RECORD_FLAG_IMPLICIT_RECEIVER,
RECORD_TYPE_CONST,
RECORD_TYPE_INVOKE_CLOSURE,
RECORD_TYPE_INVOKE_FORMATTER,
RECORD_TYPE_INVOKE_METHOD,
RECORD_TYPE_INVOKE_PURE_FUNCTION,
RECORD_TYPE_PROPERTY
} from './record';
import {FIELD, IMPLEMENTS, isBlank, isPresent, int, autoConvertAdd, BaseException,
NumberWrapper} from 'facade/lang';
import {List, Map, ListWrapper, MapWrapper, StringMapWrapper} from 'facade/collection';
import {ContextWithVariableBindings} from './parser/context_with_variable_bindings';
import {
AccessMember,
Assignment,
AST,
AstVisitor,
Binary,
Chain,
Collection,
Conditional,
Formatter,
FunctionCall,
ImplicitReceiver,
KeyedAccess,
LiteralArray,
LiteralMap,
LiteralPrimitive,
MethodCall,
PrefixNot
} from './parser/ast';
export class ProtoRecordRange {
recordCreator: ProtoRecordCreator;
constructor() {
this.recordCreator = null;
}
/**
* Parses [ast] into [ProtoRecord]s and adds them to [ProtoRecordRange].
*
* @param astWithSource The expression to watch
* @param expressionMemento an opaque object which will be passed to ChangeDispatcher on
* detecting a change.
* @param groupMemento
* @param content Whether to watch collection content (true) or reference (false, default)
*/
addRecordsFromAST(ast:AST,
expressionMemento,
groupMemento,
content:boolean = false)
{
if (this.recordCreator === null) {
this.recordCreator = new ProtoRecordCreator(this);
}
if (content) {
ast = new Collection(ast);
}
this.recordCreator.createRecordsFromAST(ast, expressionMemento, groupMemento);
}
// TODO(rado): the type annotation should be dispatcher:ChangeDispatcher.
// but @Implements is not ready yet.
instantiate(dispatcher, formatters:Map):RecordRange {
var recordRange:RecordRange = new RecordRange(this, dispatcher);
if (this.recordCreator !== null) {
this._createRecords(recordRange, formatters);
this._setDestination();
}
return recordRange;
}
_createRecords(recordRange:RecordRange, formatters:Map) {
for (var proto = this.recordCreator.headRecord; proto != null; proto = proto.next) {
var record = new Record(recordRange, proto, formatters);
proto.recordInConstruction = record;
recordRange.addRecord(record);
}
}
_setDestination() {
for (var proto = this.recordCreator.headRecord; proto != null; proto = proto.next) {
if (proto.dest instanceof Destination) {
proto.recordInConstruction.dest = proto.dest.record.recordInConstruction;
} else {
proto.recordInConstruction.dest = proto.dest;
}
proto.recordInConstruction = null;
}
}
}
export class RecordRange {
protoRecordRange:ProtoRecordRange;
dispatcher:any; //ChangeDispatcher
headRecord:Record;
tailRecord:Record;
disabled:boolean;
// TODO(rado): the type annotation should be dispatcher:ChangeDispatcher.
// but @Implements is not ready yet.
constructor(protoRecordRange:ProtoRecordRange, dispatcher) {
this.protoRecordRange = protoRecordRange;
this.dispatcher = dispatcher;
this.disabled = false;
this.headRecord = Record.createMarker(this);
this.tailRecord = Record.createMarker(this);
_link(this.headRecord, this.tailRecord);
}
/// addRecord assumes that the record is newly created, so it is enabled.
addRecord(record:Record) {
var lastRecord = this.tailRecord.prev;
_link(lastRecord, record);
if (!lastRecord.isDisabled()) {
_linkEnabled(lastRecord, record);
}
_link(record, this.tailRecord);
}
addRange(child:RecordRange) {
var lastRecord = this.tailRecord.prev;
var prevEnabledRecord = this.tailRecord.findPrevEnabled();
var nextEnabledRerord = this.tailRecord.findNextEnabled();
var firstEnabledChildRecord = child.findFirstEnabledRecord();
var lastEnabledChildRecord = child.findLastEnabledRecord();
_link(lastRecord, child.headRecord);
_link(child.tailRecord, this.tailRecord);
if (isPresent(prevEnabledRecord) && isPresent(firstEnabledChildRecord)) {
_linkEnabled(prevEnabledRecord, firstEnabledChildRecord);
}
if (isPresent(nextEnabledRerord) && isPresent(lastEnabledChildRecord)) {
_linkEnabled(lastEnabledChildRecord, nextEnabledRerord);
}
}
remove() {
var firstEnabledChildRecord = this.findFirstEnabledRecord();
var next = this.tailRecord.next;
var prev = this.headRecord.prev;
_link(prev, next);
if (isPresent(firstEnabledChildRecord)) {
var lastEnabledChildRecord = this.findLastEnabledRecord();
var nextEnabled = lastEnabledChildRecord.nextEnabled;
var prevEnabled = firstEnabledChildRecord.prevEnabled;
if (isPresent(nextEnabled)) nextEnabled.prevEnabled = prevEnabled;
if (isPresent(prevEnabled)) prevEnabled.nextEnabled = nextEnabled;
}
}
disable() {
var firstEnabledChildRecord = this.findFirstEnabledRecord();
if (isPresent(firstEnabledChildRecord)) {
// There could be a last enabled record only if first enabled exists
var lastEnabledChildRecord = this.findLastEnabledRecord();
var nextEnabled = lastEnabledChildRecord.nextEnabled;
var prevEnabled = firstEnabledChildRecord.prevEnabled;
if (isPresent(nextEnabled)) nextEnabled.prevEnabled = prevEnabled;
if (isPresent(prevEnabled)) prevEnabled.nextEnabled = nextEnabled;
}
this.disabled = true;
}
enable() {
var prevEnabledRecord = this.headRecord.findPrevEnabled();
var nextEnabledRecord = this.tailRecord.findNextEnabled();
var firstEnabledthisRecord = this.findFirstEnabledRecord();
var lastEnabledthisRecord = this.findLastEnabledRecord();
if (isPresent(firstEnabledthisRecord) && isPresent(prevEnabledRecord)){
_linkEnabled(prevEnabledRecord, firstEnabledthisRecord);
}
if (isPresent(lastEnabledthisRecord) && isPresent(nextEnabledRecord)){
_linkEnabled(lastEnabledthisRecord, nextEnabledRecord);
}
this.disabled = false;
}
/**
* Returns the first enabled record in the current range.
*
* [H ER1 ER2 R3 T] returns ER1
* [H R1 ER2 R3 T] returns ER2
*
* If no enabled records, returns null.
*
* [H R1 R2 R3 T] returns null
*
* The function skips disabled sub ranges.
*/
findFirstEnabledRecord() {
var record = this.headRecord.next;
while (record !== this.tailRecord && record.isDisabled()) {
if (record.isMarkerRecord() && record.recordRange.disabled) {
record = record.recordRange.tailRecord.next;
} else {
record = record.next;
}
}
return record === this.tailRecord ? null : record;
}
/**
* Returns the last enabled record in the current range.
*
* [H ER1 ER2 R3 T] returns ER2
* [H R1 ER2 R3 T] returns ER2
*
* If no enabled records, returns null.
*
* [H R1 R2 R3 T] returns null
*
* The function skips disabled sub ranges.
*/
findLastEnabledRecord() {
var record = this.tailRecord.prev;
while (record !== this.headRecord && record.isDisabled()) {
if (record.isMarkerRecord() && record.recordRange.disabled) {
record = record.recordRange.headRecord.prev;
} else {
record = record.prev;
}
}
return record === this.headRecord ? null : record;
}
/**
* Sets the context (the object) on which the change detection expressions will
* dereference themselves on. Since the RecordRange can be reused the context
* can be re-set many times during the lifetime of the RecordRange.
*
* @param context the new context for change detection for the current RecordRange
*/
setContext(context) {
for (var record:Record = this.headRecord;
record != null;
record = record.next) {
if (record.isImplicitReceiver()) {
this._setContextForRecord(context, record);
}
}
}
_setContextForRecord(context, record:Record) {
var proto = record.protoRecord;
while (context instanceof ContextWithVariableBindings) {
if (context.hasBinding(proto.name)) {
this._setVarBindingGetter(context, record, proto);
return;
}
context = context.parent;
}
this._setRegularGetter(context, record, proto);
}
_setVarBindingGetter(context, record:Record, proto:ProtoRecord) {
record.funcOrValue = _mapGetter(proto.name);
record.updateContext(context.varBindings);
}
_setRegularGetter(context, record:Record, proto:ProtoRecord) {
record.funcOrValue = proto.funcOrValue;
record.updateContext(context);
}
inspect() {
return _inspect(this);
}
}
function _inspect(recordRange:RecordRange) {
var res = [];
for(var r = recordRange.headRecord.next; r != recordRange.tailRecord; r = r.next){
ListWrapper.push(res, r.inspect().description);
}
return res;
}
function _link(a:Record, b:Record) {
a.next = b;
b.prev = a;
}
function _linkEnabled(a:Record, b:Record) {
a.nextEnabled = b;
b.prevEnabled = a;
}
export class ChangeDispatcher {
onRecordChange(groupMemento, records:List<Record>) {}
}
//todo: vsavkin: Create Array and Context destinations?
class Destination {
record:ProtoRecord;
position:int;
constructor(record:ProtoRecord, position:int) {
this.record = record;
this.position = position;
}
}
@IMPLEMENTS(AstVisitor)
class ProtoRecordCreator {
protoRecordRange:ProtoRecordRange;
headRecord:ProtoRecord;
tailRecord:ProtoRecord;
groupMemento:any;
expressionAsString:string;
constructor(protoRecordRange) {
this.protoRecordRange = protoRecordRange;
this.headRecord = null;
this.tailRecord = null;
this.expressionAsString = null;
}
visitImplicitReceiver(ast:ImplicitReceiver, args) {
throw new BaseException('Should never visit an implicit receiver');
}
visitLiteralPrimitive(ast:LiteralPrimitive, dest) {
this.add(this.construct(RECORD_TYPE_CONST, ast.value, 0, null, dest));
}
visitBinary(ast:Binary, dest) {
var record = this.construct(RECORD_TYPE_INVOKE_PURE_FUNCTION,
_operationToFunction(ast.operation), 2, ast.operation, dest);
ast.left.visit(this, new Destination(record, 0));
ast.right.visit(this, new Destination(record, 1));
this.add(record);
}
visitPrefixNot(ast:PrefixNot, dest) {
var record = this.construct(RECORD_TYPE_INVOKE_PURE_FUNCTION, _operation_negate, 1, "-", dest);
ast.expression.visit(this, new Destination(record, 0));
this.add(record);
}
visitAccessMember(ast:AccessMember, dest) {
var record = this.construct(RECORD_TYPE_PROPERTY, ast.getter, 0, ast.name, dest);
if (ast.receiver instanceof ImplicitReceiver) {
record.setIsImplicitReceiver();
} else {
ast.receiver.visit(this, new Destination(record, null));
}
this.add(record);
}
visitFormatter(ast:Formatter, dest) {
var record = this.construct(RECORD_TYPE_INVOKE_FORMATTER, ast.name, ast.allArgs.length, ast.name, dest);
for (var i = 0; i < ast.allArgs.length; ++i) {
ast.allArgs[i].visit(this, new Destination(record, i));
}
this.add(record);
}
visitMethodCall(ast:MethodCall, dest) {
var record = this.construct(RECORD_TYPE_INVOKE_METHOD, ast.fn, ast.args.length, ast.name, dest);
for (var i = 0; i < ast.args.length; ++i) {
ast.args[i].visit(this, new Destination(record, i));
}
if (ast.receiver instanceof ImplicitReceiver) {
record.setIsImplicitReceiver();
} else {
ast.receiver.visit(this, new Destination(record, null));
}
this.add(record);
}
visitFunctionCall(ast:FunctionCall, dest) {
var record = this.construct(RECORD_TYPE_INVOKE_CLOSURE, null, ast.args.length, null, dest);
ast.target.visit(this, new Destination(record, null));
for (var i = 0; i < ast.args.length; ++i) {
ast.args[i].visit(this, new Destination(record, i));
}
this.add(record);
}
visitCollection(ast: Collection, dest) {
var record = this.construct(RECORD_FLAG_COLLECTION, null, null, null, dest);
ast.value.visit(this, new Destination(record, null));
this.add(record);
}
visitConditional(ast:Conditional, dest) {
var record = this.construct(RECORD_TYPE_INVOKE_PURE_FUNCTION, _cond, 3, "?:", dest);
ast.condition.visit(this, new Destination(record, 0));
ast.trueExp.visit(this, new Destination(record, 1));
ast.falseExp.visit(this, new Destination(record, 2));
this.add(record);
}
visitKeyedAccess(ast:KeyedAccess, dest) {
var record = this.construct(RECORD_TYPE_INVOKE_METHOD, _keyedAccess, 1, "[]", dest);
ast.obj.visit(this, new Destination(record, null));
ast.key.visit(this, new Destination(record, 0));
this.add(record);
}
visitLiteralArray(ast:LiteralArray, dest) {
var length = ast.expressions.length;
var record = this.construct(RECORD_TYPE_INVOKE_PURE_FUNCTION, _arrayFn(length), length, "Array()", dest);
for (var i = 0; i < length; ++i) {
ast.expressions[i].visit(this, new Destination(record, i));
}
this.add(record);
}
visitLiteralMap(ast:LiteralMap, dest) {
var length = ast.values.length;
var record = this.construct(RECORD_TYPE_INVOKE_PURE_FUNCTION, _mapFn(ast.keys, length), length, "Map()", dest);
for (var i = 0; i < length; ++i) {
ast.values[i].visit(this, new Destination(record, i));
}
this.add(record);
}
visitChain(ast:Chain, dest){this._unsupported();}
visitAssignment(ast:Assignment, dest) {this._unsupported();}
visitTemplateBindings(ast, dest) {this._unsupported();}
createRecordsFromAST(ast:AST, expressionMemento:any, groupMemento:any){
this.groupMemento = groupMemento;
this.expressionAsString = ast.toString();
ast.visit(this, expressionMemento);
}
construct(recordType, funcOrValue, arity, name, dest) {
return new ProtoRecord(this.protoRecordRange, recordType, funcOrValue, arity,
name, dest, this.groupMemento, this.expressionAsString);
}
add(protoRecord:ProtoRecord) {
if (this.headRecord === null) {
this.headRecord = this.tailRecord = protoRecord;
} else {
this.tailRecord.next = protoRecord;
this.tailRecord = protoRecord;
}
}
_unsupported() {
throw new BaseException("Unsupported");
}
}
function _operationToFunction(operation:string):Function {
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 _operation_negate(value) {return !value;}
function _operation_add(left, right) {return left + right;}
function _operation_subtract(left, right) {return left - right;}
function _operation_multiply(left, right) {return left * right;}
function _operation_divide(left, right) {return left / right;}
function _operation_remainder(left, right) {return left % right;}
function _operation_equals(left, right) {return left == right;}
function _operation_not_equals(left, right) {return left != right;}
function _operation_less_then(left, right) {return left < right;}
function _operation_greater_then(left, right) {return left > right;}
function _operation_less_or_equals_then(left, right) {return left <= right;}
function _operation_greater_or_equals_then(left, right) {return left >= right;}
function _operation_logical_and(left, right) {return left && right;}
function _operation_logical_or(left, right) {return left || right;}
function _cond(cond, trueVal, falseVal) {return cond ? trueVal : falseVal;}
function _arrayFn(length:int) {
switch (length) {
case 0: return () => [];
case 1: return (a1) => [a1];
case 2: return (a1, a2) => [a1, a2];
case 3: return (a1, a2, a3) => [a1, a2, a3];
case 4: return (a1, a2, a3, a4) => [a1, a2, a3, a4];
case 5: return (a1, a2, a3, a4, a5) => [a1, a2, a3, a4, a5];
case 6: return (a1, a2, a3, a4, a5, a6) => [a1, a2, a3, a4, a5, a6];
case 7: return (a1, a2, a3, a4, a5, a6, a7) => [a1, a2, a3, a4, a5, a6, a7];
case 8: return (a1, a2, a3, a4, a5, a6, a7, a8) => [a1, a2, a3, a4, a5, a6, a7, a8];
case 9: return (a1, a2, a3, a4, a5, a6, a7, a8, a9) => [a1, a2, a3, a4, a5, a6, a7, a8, a9];
default: throw new BaseException(`Does not support literal arrays with more than 9 elements`);
}
}
function _mapFn(keys:List, length:int) {
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 (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`);
}
}
//TODO: cache the getters
function _mapGetter(key) {
return function(map) {
return MapWrapper.get(map, key);
}
}
function _keyedAccess(obj, args) {
return obj[args[0]];
}